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

**日期**: 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，其余进 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、避免无意义重渲染）
