6.6 KiB
钉钉同步 — 用户角色选择设计
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 数组。
{
"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:
{
"users": [
{ "dingUserId": "abc123", "name": "张老师", "mobile": "138...", "roleId": 2 },
{ "dingUserId": "def456", "name": "李同学", "mobile": "139...", "roleId": null }
]
}
roleId: number→ 老师,创建 User + 分配角色,不创建 StudentroleId: null→ 学生,创建 User + Student(status=active),不分配角色
roleId: null(前端传 null, 不是 undefined)表示学生。两端统一用null作为"不是老师"的标记值。
-
users 数组为空时 → 返回
{ teacherCount: 0, studentCount: 0, skipped: 0 },不报错 -
已存在 UserDingMapping → 跳过,计入 skipped
返回:
{ "teacherCount": 3, "studentCount": 25, "skipped": 2 }
实现层级:
SyncService新增importDingTalkUsers(users)— 遍历、去重、创建 User/Student/UserDingMappingSyncController新增@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 (导入后显示)
状态管理:
// 新增 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/catch,log 错误但不阻断其余 |
涉及文件
后端 (4 个文件修改)
apps/server/src/integration/dingtalk.service.ts— 新增fetchOrgTreeWithUsersapps/server/src/sync/sync.service.ts— 新增getDingTalkOrgTreeWithUsers、importDingTalkUsersapps/server/src/sync/sync.controller.ts— 新增 2 个 endpointapps/server/src/sync/sync.module.ts— 注入 User/Student/Role Repository(如需)
前端 (2 个文件修改)
apps/admin/src/pages/IntegrationConfig/index.tsx— 加 Tabs + Drawerapps/admin/src/pages/Users/index.tsx— 删同步/标记按钮及相关代码