Files
gongxue-base/docs/superpowers/specs/2026-07-09-dingtalk-sync-role-selection-design.md

202 lines
6.6 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 | 状态:待实现
## 问题
当前 `DingTalkService.syncOneUser()` 将每个从钉钉同步过来的新用户**无条件创建为 Student**。钉钉组织里教职工和学生混在一起,导致每次同步后管理员需要手动去用户管理页面逐人修正角色(「标记为教职工」「标记为学员」按钮)。
## 方案
将同步入口从 Users 页面移到 IntegrationConfig 页面新增「同步用户」Tab。同步时前端拉取钉钉组织树含用户在 Drawer 内勾选「谁是老师」,导入时后端据此分别创建:
- **老师**勾选User + 指定角色,不建 Student
- **学生**未勾选User + Student不分配角色
---
## 后端
### 1. 新接口:`GET /api/sync/dingtalk/org-tree-with-users`
权限:`sync:read`
Query: `rootDeptId` (可选,默认 1)
返回:部门树,每个节点含 `users` 数组。
```json
{
"success": true,
"data": [
{
"id": 1,
"name": "恭学教育",
"parentId": 0,
"children": [
{
"id": 10,
"name": "教务处",
"parentId": 1,
"children": [],
"users": [
{ "userid": "abc123", "name": "张老师", "mobile": "138..." }
]
}
],
"users": []
}
]
}
```
实现层级:
- `DingTalkService` 新增 `fetchOrgTreeWithUsers(rootDeptId)` — BFS 拉部门 + 按部门拉用户,组装树
- `SyncService` 新增 `getDingTalkOrgTreeWithUsers(rootDeptId)` — 透传
- `SyncController` 新增 `@Get('dingtalk/org-tree-with-users')`
### 2. 新接口:`POST /api/sync/dingtalk/import-users`
权限:`sync:trigger`
Body
```json
{
"users": [
{ "dingUserId": "abc123", "name": "张老师", "mobile": "138...", "roleId": 2 },
{ "dingUserId": "def456", "name": "李同学", "mobile": "139...", "roleId": null }
]
}
```
- `roleId: number` → 老师,创建 User + 分配角色,不创建 Student
- `roleId: null` → 学生,创建 User + Student(status=active),不分配角色
> `roleId: null`(前端传 null, 不是 undefined表示学生。两端统一用 `null` 作为"不是老师"的标记值。
- users 数组为空时 → 返回 `{ teacherCount: 0, studentCount: 0, skipped: 0 }`,不报错
- 已存在 UserDingMapping → 跳过,计入 skipped
返回:
```json
{ "teacherCount": 3, "studentCount": 25, "skipped": 2 }
```
实现层级:
- `SyncService` 新增 `importDingTalkUsers(users)` — 遍历、去重、创建 User/Student/UserDingMapping
- `SyncController` 新增 `@Post('dingtalk/import-users')`
### 3. 旧逻辑不删
`syncAll()` / `syncOneUser()` 保持不变,保持向后兼容。只是前端入口不再走这个路径。
---
## 前端
### 文件:`apps/admin/src/pages/IntegrationConfig/index.tsx`
页面加 Tabs拆两个标签
| Tab | key | 内容 |
|-----|-----|------|
| 钉钉配置 | `config` | 现有表单(不变) |
| 同步用户 | `sync-users` | 新增(见下) |
**钉钉未配置时**「同步用户」Tab 不显示(`config === null`)。
**「同步用户」Tab 结构:**
```
Card
├─ TreeSelect (选起始部门,默认全部)
├─ Button 「获取组织架构」
├─ Drawer (open=hasTree)
│ ├─ Tree 组件
│ │ ├─ 部门节点 (可展开)
│ │ └─ 用户节点 (叶子,不可展开)
│ │ ├─ 姓名 + 手机号
│ │ ├─ Checkbox 「老师」(默认不勾)
│ │ └─ 勾选后Select 角色 (默认「班主任」)
│ └─ Footer
│ ├─ Button 「取消」
│ └─ Button 「导入」(type=primary, loading)
└─ result message (导入后显示)
```
**状态管理:**
```typescript
// 新增 state
const [orgTree, setOrgTree] = useState<TreeNode[]>([]);
const [drawerOpen, setDrawerOpen] = useState(false);
const [fetchingTree, setFetchingTree] = useState(false);
const [importing, setImporting] = useState(false);
const [teacherChecks, setTeacherChecks] = useState<Record<string, boolean>>({});
const [teacherRoles, setTeacherRoles] = useState<Record<string, number>>({});
```
**默认角色解析:**
> `defaultTeacherRoleId` 在组件挂载时从 `/rbac/roles` 加载所有角色后,查找 `name === '班主任'` 的 id 作为默认值。若角色列表里没有「班主任」,取第一个非系统角色的 id。
### 文件:`apps/admin/src/pages/Users/index.tsx`
删除以下内容:
- 「同步钉钉用户」按钮 (PermissionButton, permission `sync:trigger`)
- 同步部门 TreeSelect
- 「标记为教职工」「标记为学员」操作按钮
- 相关 state`syncing``syncDeptId``orgTree`
- 相关函数:`loadOrgTree``handleSyncDingTalk``handleMarkStaff`
- 未使用的 import`CloudDownloadOutlined``TreeSelect`
---
## 数据流
```
[同步用户 Tab]
├─ 选部门 → 点「获取组织架构」
├─ GET /sync/dingtalk/org-tree-with-users?rootDeptId=X
│ DingTalkService.fetchOrgTreeWithUsers()
│ 按部门拉用户 → 去重(dingUserId) → 组装树
├─ Drawer 展示树,勾选老师+选角色
├─ 点「导入」
├─ POST /sync/dingtalk/import-users
│ { users: [{ dingUserId, name, mobile, roleId|null }] }
│ │
│ ├─ 查 UserDingMapping → 已存在则跳过
│ ├─ roleId != null: User + role assignment
│ └─ roleId == null: User + Student(status=active)
│ └─ UserDingMapping(dingUserId, userId, dingName, dingMobile)
└─ 结果: { teacherCount, studentCount, skipped }
```
## 边界情况
| 场景 | 处理 |
|------|------|
| 钉钉未配置 | 不显示「同步用户」Tab |
| 已存在 mapping | 跳过,计入 skipped |
| 手机号为空 | username = `dd_<dingUserId>` |
| 同用户属多部门 | 去重,仅在首个部门展示 |
| 角色下拉选项 | 从 `/rbac/roles` 获取所有非禁用角色 |
| 导入单个失败 | 独立 try/catchlog 错误但不阻断其余 |
## 涉及文件
### 后端 (4 个文件修改)
- `apps/server/src/integration/dingtalk.service.ts` — 新增 `fetchOrgTreeWithUsers`
- `apps/server/src/sync/sync.service.ts` — 新增 `getDingTalkOrgTreeWithUsers``importDingTalkUsers`
- `apps/server/src/sync/sync.controller.ts` — 新增 2 个 endpoint
- `apps/server/src/sync/sync.module.ts` — 注入 User/Student/Role Repository如需
### 前端 (2 个文件修改)
- `apps/admin/src/pages/IntegrationConfig/index.tsx` — 加 Tabs + Drawer
- `apps/admin/src/pages/Users/index.tsx` — 删同步/标记按钮及相关代码