Files
gongxue-base/docs/superpowers/specs/2026-07-09-dingtalk-import-class-marking-design.md

5.7 KiB
Raw Blame History

钉钉组织导入 — 标记班级功能

日期: 2026-07-09 状态: 设计完成


概述

在钉钉组织架构导入抽屉(IntegrationConfig 页面)中,支持将钉钉部门标记为「班级」。导入时自动创建班级实体,部门下的教师加入班级教师关联,学生加入班级学生关联,勾选「班主任」角色的用户自动成为该班级主班主任。


前端改造

1. 树节点增强

部门节点右侧新增操作入口:

  • 未标记班级的部门 → 显示 🏫 标为班级 按钮
  • 已标记班级的部门 → 显示 🏫 班级: XXXXX [已标记],颜色区分,点击可修改

用户节点无变化。

2. 标记班级 Modal

点击「标为班级」/「已标记」→ 弹出 Modal 表单:

字段 组件 说明
班级名称 Input 预填部门名称,可修改
班级编码 Input 必填,手动输入
班型 Select culture / professional / bootcamp / sprint
开班日期 DatePicker 选填
结束日期 DatePicker 选填
最大人数 InputNumber 默认 0不限制
备注 TextArea 选填

状态管理:classMarks: Record<number, ClassMarkForm>key 为钉钉部门 ID。

3. 导入 payload 调整

点击「导入」时构建的 payload

{
  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

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.tsimportDingTalkUsers 方法改造,全量事务:

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. 用户在多个部门 → 全部匹配到的班级都建立关联

返回结构

{
  teacherCount: number;
  studentCount: number;
  classCount: number;    // 新增
  skipped: number;
  warnings: string[];    // 非致命警告
}

3. 教师角色判定

通过角色表查询:导入时勾选的用户分配指定角色后即为「老师」。所有老师在班级关联中统一写入 ClassTeacherroleType = '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其余进 ClassTeacherwarning
用户在多个被标记部门 同时加入多个班级
班级编码重复 事务回滚,错误返回
空部门 班级创建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、避免无意义重渲染