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

195 lines
5.7 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.

# 钉钉组织导入 — 标记班级功能
**日期**: 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
```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其余进 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、避免无意义重渲染