# 钉钉组织导入 — 标记班级功能 **日期**: 2026-07-09 **状态**: 设计完成 --- ## 概述 在钉钉组织架构导入抽屉(`IntegrationConfig` 页面)中,支持将钉钉部门标记为「班级」。导入时自动创建班级实体,部门下的教师加入班级教师关联,学生加入班级学生关联,勾选「班主任」角色的用户自动成为该班级主班主任。 --- ## 前端改造 ### 1. 树节点增强 **部门节点**右侧新增操作入口: - 未标记班级的部门 → 显示 `🏫 标为班级` 按钮 - 已标记班级的部门 → 显示 `🏫 班级: XXXXX [已标记]`,颜色区分,点击可修改 用户节点无变化。 ### 2. 标记班级 Modal 点击「标为班级」/「已标记」→ 弹出 Modal 表单: |字段|组件|说明| |---|---|---| |班级名称|Input|预填部门名称,可修改| |班级编码|Input|必填,手动输入| |班型|Select|culture / professional / bootcamp / sprint| |开班日期|DatePicker|选填| |结束日期|DatePicker|选填| |最大人数|InputNumber|默认 0(不限制)| |备注|TextArea|选填| 状态管理:`classMarks: Record`,key 为钉钉部门 ID。 ### 3. 导入 payload 调整 点击「导入」时构建的 payload: ```ts { classes: [ { deptId, name, code, classType, startDate?, endDate?, maxStudents?, notes? } ], users: [ { dingUserId: u.userid, name: u.name, mobile: u.mobile, roleId: teacherChecks[u.userid] ? (teacherRoles[u.userid] || defaultTeacherRoleId) : null, dingDeptIds: u.deptIds } ] } ``` ### 4. 状态清理 关闭抽屉时重置 `classMarks`。 --- ## 后端改造 ### 1. DTO 扩展 `apps/server/src/sync/dto/import-users.dto.ts`: ```ts export class ImportUserItemDto { dingUserId: string; name: string; mobile: string; roleId: number | null; dingDeptIds: number[]; // 新增 } export class ImportClassItemDto { deptId: number; name: string; code: string; classType: string; startDate?: string; endDate?: string; maxStudents?: number; notes?: string; } export class ImportUsersDto { classes?: ImportClassItemDto[]; // 新增,可选 users: ImportUserItemDto[]; } ``` ### 2. 服务层逻辑 `apps/server/src/sync/sync.service.ts` — `importDingTalkUsers` 方法改造,全量事务: ``` 1. 解析 classes[],创建 Class 实体,以 deptId → classId 建 Map 2. 遍历 users[]: a. 检查 UserDingMapping 是否已存在 → 跳过 b. 创建 User + 角色分配 / Student(现有逻辑不变) c. 记录 userId → dingDeptIds 映射 3. 遍历每个用户: a. user.dingDeptIds 匹配 classes[].deptId → 找到归属班级 b. roleId = 班主任 → ClassTeacher(roleType=head_teacher), 第一个班主任设 Class.headTeacherId c. roleId != null 且非班主任 → ClassTeacher(roleType 按角色名映射) d. roleId = null → ClassStudent e. 用户在多个部门 → 全部匹配到的班级都建立关联 ``` **返回结构**: ```ts { teacherCount: number; studentCount: number; classCount: number; // 新增 skipped: number; warnings: string[]; // 非致命警告 } ``` ### 3. 教师角色判定 通过角色表查询:导入时勾选的用户分配指定角色后即为「老师」。所有老师在班级关联中统一写入 `ClassTeacher`(`roleType = 'teacher'`),不区分班主任/任课老师。班级对老师为多对多关系。 ### 4. 错误场景 |场景|行为| |---|---| |班级编码重复|事务回滚,返回 `"班级编码 XX 已存在"`| |部门标为班级但无人|班级正常创建(空班),warning| |用户在多个被标记的部门|同时加入所有匹配班级| |无 classes 参数|兼容旧调用,仅导入师生,不创建班级| |角色不存在|事务回滚,返回具体错误| --- ## dingDeptIds 数据来源 `DingTalkService.fetchOrgTreeWithUsers` 返回的 `DingOrgTreeNodeWithUsers` 中,每个 user 已包含钉钉 API 返回的 `department` 字段(用户所属部门 ID 列表)。前端 `DingOrgTreeNodeExt` interface 需新增 `deptIds: number[]` 字段。 --- ## 测试策略 ### 后端单测(`apps/server/src/sync/sync.service.spec.ts`) |用例|预期| |---|---| |单部门标班级 + 1班主任 + 1学生|班级创建,headTeacherId 正确,关联正确| |单部门多班主任|第一个设 headTeacherId,其余进 ClassTeacher,warning| |用户在多个被标记部门|同时加入多个班级| |班级编码重复|事务回滚,错误返回| |空部门|班级创建,warning| |纯学生无老师|班级创建,headTeacherId=null,学生关联正确| |无 classes 参数|向下兼容,行为不变| ### 前端验证(手动 QA) 1. 标记一个部门为班级 → 填表单 → 导入 → 验证班级列表、班主任、学生归属 2. 标记多个部门 → 批量导入 → 每个班独立创建 3. 点击已标记班级 → 修改表单 → 重新导入 4. 无标记班级的普通导入 → 行为不受影响 --- ## 影响范围 |文件|改动| |---|---| |`apps/admin/src/pages/IntegrationConfig/index.tsx`|树节点、Modal、import payload| |`apps/server/src/sync/dto/import-users.dto.ts`|新增 ImportClassItemDto,扩展 ImportUserItemDto| |`apps/server/src/sync/sync.service.ts`|importDingTalkUsers 改造| |`apps/server/src/sync/sync.service.spec.ts`|新增测试用例| |`apps/server/src/integration/dingtalk.service.ts`|org-tree-with-users 返回增加 deptIds| --- ## 实现阶段技能注入 编码 agent 需加载: - `ui-ux-pro-max` — 前端 UI/UX 设计(Ant Design 6 组件选择、交互细节) - `vercel-react-best-practices` — React 性能优化(memo、useMemo、避免无意义重渲染)