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

6.6 KiB
Raw Blame History

钉钉同步 — 用户角色选择设计

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 + 分配角色,不创建 Student
  • roleId: 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/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 (导入后显示)

状态管理:

// 新增 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
  • 「标记为教职工」「标记为学员」操作按钮
  • 相关 statesyncingsyncDeptIdorgTree
  • 相关函数:loadOrgTreehandleSyncDingTalkhandleMarkStaff
  • 未使用的 importCloudDownloadOutlinedTreeSelect

数据流

[同步用户 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 — 新增 getDingTalkOrgTreeWithUsersimportDingTalkUsers
  • 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 — 删同步/标记按钮及相关代码