Files
gongxue-base/docs/superpowers/plans/2026-07-09-dingtalk-import-fixes.md

325 lines
10 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# DingTalk 导入链路修复 — 实现计划
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:** 修复钉钉导入链路的两个阻塞 bug前端叶子节点判断、后端 CampusScope 过滤导致学生/考勤数据不可见。
**Architecture:** 教学域(学生、考勤)移除 CampusScope 部门过滤,改为依赖 RBAC 权限码控制访问;管理域(宿舍、财务)保留 CampusScope 不变。
**Tech Stack:** React 19 + TypeScript + NestJS 11 + TypeORM
## Global Constraints
- 遵循现有 NestJS 模块结构
- 后端编译检查:`npx tsc --noEmit -p tsconfig.build.json`
- 前端编译检查:`npx tsc --noEmit`
- 不改动 `syncAll``importDingTalkUsers`、RBAC 权限码
- 不改动 `OccupanciesService``RoomsService``BillsService``ExpensesService`(保留 CampusScope
---
### Task 1: 前端 — 叶子节点判断
**Files:**
- Modify: `apps/admin/src/pages/IntegrationConfig/index.tsx:375-393`
**Interfaces:**
- Consumes: `DingOrgTreeNodeExt` (has `children: DingOrgTreeNodeExt[]`, `users: Array<...>`)
- Produces: `DataNode[]` tree nodes with conditional class-mark button
- [ ] **Step 1: Read current code to confirm line numbers**
Run: `read apps/admin/src/pages/IntegrationConfig/index.tsx:373-395`
- [ ] **Step 2: Modify buildTreeData — add leaf-node condition**
当前 `node.children``node.users` 渲染逻辑不需要改,只需要在 `title` 渲染部分(第 373395 行)用条件包裹"标为班级"按钮:
```tsx
const buildTreeData = useCallback((nodes: DingOrgTreeNodeExt[]): DataNode[] => {
return nodes.map((node) => ({
title: (
<Space size="small">
<span>{node.name}</span>
{node.children.length === 0 && node.users.length > 0 && (
classMarks[node.id] ? (
<Tag
color="blue"
style={{ cursor: 'pointer' }}
onClick={() => openClassModal(node.id, node.name)}
>
班级: {classMarks[node.id].name} [已标记]
</Tag>
) : (
<Button
size="small"
type="link"
icon={<span>🏫</span>}
onClick={() => openClassModal(node.id, node.name)}
>
标为班级
</Button>
)
)}
</Space>
),
key: `dept-${node.id}`,
children: [
...buildTreeData(node.children),
...node.users.map((u) => ({ ... })),
],
}));
}, [classMarks, teacherChecks, teacherRoles, defaultTeacherRoleId, roles]);
```
改动要点:用 `{node.children.length === 0 && node.users.length > 0 && ( ... )}` 包裹原来的三元表达式。只有无子部门且有用户的叶子节点才显示按钮。
- [ ] **Step 3: 验证 TypeScript 编译**
Run: `cd /Users/tiku1/code/gongxue-base/apps/admin && npx tsc --noEmit`
Expected: no errors
- [ ] **Step 4: Commit**
```bash
git add apps/admin/src/pages/IntegrationConfig/index.tsx
git commit -m "fix(admin): only show class-mark button on leaf departments in DingTalk org import"
```
---
### Task 2: 后端 — StudentsService 移除 CampusScope
**Files:**
- Modify: `apps/server/src/students/students.service.ts`
**Interfaces:**
- Consumes: (none external — internal change only)
- Produces: `findAll(query)` returns `Promise<Student[]>` — same signature, no scope filtering
- [ ] **Step 1: Read current code**
Run: `read apps/server/src/students/students.service.ts`
- [ ] **Step 2: Remove CampusScope import**
Delete line 4:
```typescript
import { CampusScope } from '../common/campus-scope';
```
- [ ] **Step 3: Remove constructor parameter**
SWAP lines 14-20 — remove `private readonly scope: CampusScope` and trailing comma on line 18:
```typescript
constructor(
@InjectRepository(Student) private repo: Repository<Student>,
@InjectRepository(ClassStudent) private classStudentRepo: Repository<ClassStudent>,
@InjectRepository(Class) private classRepo: Repository<Class>,
@InjectRepository(AttendanceRecord) private attendanceRepo: Repository<AttendanceRecord>,
) {}
```
- [ ] **Step 4: Remove scope.filter call in findAll**
SWAP lines 22-33 — remove the `filteredWhere` intermediate. `findAll` now uses `where` directly:
```typescript
async findAll(query?: { name?: string; status?: string; includeArchived?: boolean; tenantId?: number | string }) {
const where: FindOptionsWhere<Student> = {};
if (query?.name) where.name = Like(`%${query.name}%`);
if (query?.tenantId) where.tenantId = Number(query.tenantId);
if (query?.status) {
where.status = query.status;
} else if (!query?.includeArchived) {
where.status = Not(In(['archived']));
}
return this.repo.find({ where, order: { createdAt: 'DESC' }, relations: ['tenant'] });
}
```
- [ ] **Step 5: Verify TypeScript compile**
Run: `cd /Users/tiku1/code/gongxue-base/apps/server && npx tsc --noEmit -p tsconfig.build.json`
Expected: no errors (ignore pre-existing errors in files not touched)
- [ ] **Step 6: Commit**
```bash
git add apps/server/src/students/students.service.ts
git commit -m "fix(server): remove CampusScope from StudentsService, classes are teacher-managed not dept-filtered"
```
---
### Task 3: 后端 — AttendanceService 移除 CampusScope
**Files:**
- Modify: `apps/server/src/attendance/attendance.service.ts`
**Interfaces:**
- Consumes: (none external — internal change only)
- Produces: all public method signatures unchanged, no scope filtering applied
- [ ] **Step 1: Read current code to confirm line numbers**
Run: `read apps/server/src/attendance/attendance.service.ts:1-40`
- [ ] **Step 2: Remove CampusScope import (line 9)**
```typescript
import { CampusScope } from '../common/campus-scope';
```
→ Delete this line.
- [ ] **Step 3: Remove constructor parameter (line 39)**
SWAP the constructor body — remove `private readonly scope: CampusScope`:
```typescript
constructor(
@InjectRepository(AttendanceRecord)
private attendanceRepo: Repository<AttendanceRecord>,
@InjectRepository(DingAttendanceRaw)
private dingRawRepo: Repository<DingAttendanceRaw>,
@InjectRepository(Class)
private classRepo: Repository<Class>,
@InjectRepository(Student)
private studentRepo: Repository<Student>,
@InjectRepository(ClassSchedule)
private scheduleRepo: Repository<ClassSchedule>,
@InjectRepository(ClassStudent)
private classStudentRepo: Repository<ClassStudent>,
@InjectRepository(UserDingMapping)
private userDingMappingRepo: Repository<UserDingMapping>,
) {}
```
- [ ] **Step 4: Remove scope block in getSummary (lines 182-185)**
Delete lines 182-185:
```typescript
const scopeIds = await this.scope.getScopeDepartmentIds();
if (scopeIds) {
qb.andWhere('ar.departmentId IN (:...scopeIds)', { scopeIds });
}
```
- [ ] **Step 5: Remove scope block in findAll (lines 287-290)**
Delete lines 287-290:
```typescript
const scopeIds = await this.scope.getScopeDepartmentIds();
if (scopeIds) {
qb.andWhere('ar.departmentId IN (:...scopeIds)', { scopeIds });
}
```
- [ ] **Step 6: Remove scope blocks in getClasses (lines 327-330 and 339)**
Delete lines 327-330:
```typescript
const scopeIds = await this.scope.getScopeDepartmentIds();
if (scopeIds) {
qb.andWhere('ar.departmentId IN (:...scopeIds)', { scopeIds });
}
```
SWAP line 339 — replace `await this.scope.filter({ id: In(classIds) })` with `{ id: In(classIds) }`:
```typescript
const classes = await this.classRepo.find({ where: { id: In(classIds) } });
```
- [ ] **Step 7: Remove scope block in findAllForExport (lines 422-425)**
Delete lines 422-425:
```typescript
const scopeIds = await this.scope.getScopeDepartmentIds();
if (scopeIds) {
qb.andWhere('ar.departmentId IN (:...scopeIds)', { scopeIds });
}
```
- [ ] **Step 8: Remove scope guard in update (lines 460-463)**
Delete lines 460-463:
```typescript
const scopeIds = await this.scope.getScopeDepartmentIds();
if (scopeIds && !scopeIds.includes(record.departmentId)) {
throw new NotFoundException(`AttendanceRecord ${id} not found`);
}
```
- [ ] **Step 9: Remove scope guard in remove (lines 482-485)**
Delete lines 482-485:
```typescript
const scopeIds = await this.scope.getScopeDepartmentIds();
if (scopeIds && !scopeIds.includes(record.departmentId)) {
throw new NotFoundException(`AttendanceRecord ${id} not found`);
}
```
- [ ] **Step 10: Remove scope block in getReport (lines 494-497)**
Delete lines 494-497:
```typescript
const scopeIds = await this.scope.getScopeDepartmentIds();
if (scopeIds) {
qb.andWhere('ar.departmentId IN (:...scopeIds)', { scopeIds });
}
```
- [ ] **Step 11: Remove scope block in getAlerts (lines 570-573)**
Delete lines 570-573:
```typescript
const scopeIds = await this.scope.getScopeDepartmentIds();
if (scopeIds) {
qb.andWhere('a.departmentId IN (:...scopeIds)', { scopeIds });
}
```
- [ ] **Step 12: Verify TypeScript compile**
Run: `cd /Users/tiku1/code/gongxue-base/apps/server && npx tsc --noEmit -p tsconfig.build.json`
Expected: no errors (ignore pre-existing errors in files not touched)
- [ ] **Step 13: Commit**
```bash
git add apps/server/src/attendance/attendance.service.ts
git commit -m "fix(server): remove CampusScope from AttendanceService, attendance is teacher-managed not dept-filtered"
```
---
### Task 4: 端到端验证
- [ ] **Step 1: 启动后端**
```bash
cd /Users/tiku1/code/gongxue-base/apps/server && npm run start:dev
```
- [ ] **Step 2: 启动前端**
```bash
cd /Users/tiku1/code/gongxue-base/apps/admin && npm run dev
```
- [ ] **Step 3: 验证场景**
1. 以非超管老师身份登录,打开集成配置页面
2. 获取钉钉组织架构 → 确认父部门没有"标为班级"按钮,叶子部门有
3. 标记班级、勾选老师、执行导入 → 确认导入成功
4. 导航到学生管理页面 → 确认能看到导入的学生
5. 导航到考勤管理页面 → 确认有数据
- [ ] **Step 4: Commit verification notes**
```bash
git add -A && git commit -m "chore: end-to-end verification notes"
```