docs: dingtalk import class marking design spec
This commit is contained in:
@@ -0,0 +1,197 @@
|
||||
# 钉钉组织导入 — 标记班级功能
|
||||
|
||||
**日期**: 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. 班主任判定
|
||||
|
||||
通过角色表查询:查找用户分配的角色中 `code === 'head_teacher'` 或 `name === '班主任'` 的角色。若命中且其 `dingDeptIds` 中存在被标记为班级的部门,则将该用户作为该班级的班主任。
|
||||
|
||||
「班主任」角色 code 尚未定义 → 在角色 seed 中新增 `head_teacher` 作为班主任角色的 code 标识。
|
||||
|
||||
### 4. 错误场景
|
||||
|
||||
|场景|行为|
|
||||
|---|---|
|
||||
|班级编码重复|事务回滚,返回 `"班级编码 XX 已存在"`|
|
||||
|部门标为班级但无人|班级正常创建(空班),warning|
|
||||
|一个班级多班主任|第一个设 headTeacherId,其余写入 ClassTeacher,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、避免无意义重渲染)
|
||||
Reference in New Issue
Block a user