From 3ea7f9a2b76b18c9e0168b0fb6c8825913c476f3 Mon Sep 17 00:00:00 2001 From: wangziqi Date: Thu, 9 Jul 2026 15:17:25 +0800 Subject: [PATCH] docs: add DingTalk import fix implementation plan --- .../plans/2026-07-09-dingtalk-import-fixes.md | 324 ++++++++++++++++++ 1 file changed, 324 insertions(+) create mode 100644 docs/superpowers/plans/2026-07-09-dingtalk-import-fixes.md diff --git a/docs/superpowers/plans/2026-07-09-dingtalk-import-fixes.md b/docs/superpowers/plans/2026-07-09-dingtalk-import-fixes.md new file mode 100644 index 0000000..a838060 --- /dev/null +++ b/docs/superpowers/plans/2026-07-09-dingtalk-import-fixes.md @@ -0,0 +1,324 @@ +# 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` 渲染部分(第 373–395 行)用条件包裹"标为班级"按钮: + +```tsx +const buildTreeData = useCallback((nodes: DingOrgTreeNodeExt[]): DataNode[] => { + return nodes.map((node) => ({ + title: ( + + {node.name} + {node.children.length === 0 && node.users.length > 0 && ( + classMarks[node.id] ? ( + openClassModal(node.id, node.name)} + > + 班级: {classMarks[node.id].name} [已标记] + + ) : ( + + ) + )} + + ), + 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` — 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, + @InjectRepository(ClassStudent) private classStudentRepo: Repository, + @InjectRepository(Class) private classRepo: Repository, + @InjectRepository(AttendanceRecord) private attendanceRepo: Repository, + ) {} +``` + +- [ ] **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 = {}; + 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, + @InjectRepository(DingAttendanceRaw) + private dingRawRepo: Repository, + @InjectRepository(Class) + private classRepo: Repository, + @InjectRepository(Student) + private studentRepo: Repository, + @InjectRepository(ClassSchedule) + private scheduleRepo: Repository, + @InjectRepository(ClassStudent) + private classStudentRepo: Repository, + @InjectRepository(UserDingMapping) + private userDingMappingRepo: Repository, + ) {} +``` + +- [ ] **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" +```