You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 

9.5 KiB

PRD — 用户管理:用户列表模块

Status: proposed Source prototype: D:\code\prototype(React+Antd,dev server http://localhost:5174),页面 src/pages/system/UserManagePage.tsx Backend module: crm-auth,接入点 SystemController(/api/system/**) Grilling 决策记录: 2026-08-01,9 题逐项共识(见下"决策摘要")

1. 目标

照原型"用户管理"页,在后端补齐只读用户列表 + 指标卡统计 + 分配角色能力,使前端用户管理页可对接真实后端。基本信息(姓名/部门/职务/手机号/在职状态)遵从 ADR-0002(OA 唯一来源,CRM 只读),CRM 侧唯一写操作=分配角色。

2. 决策摘要(9 题共识)

# 决策 选项
1 模块范围 列表+分配+指标卡;OA 入职/离职/复职按钮、移交数据留后续
2 在职状态数据模型 AuthUser 新增独立字段 employment_status(active/resigned),与 enabled 并存
3 部门语义 对齐后端 主部门+兼职部门+部门集合;部门列显主部门(兼职小标签);子树筛选按"部门集合 ∩ 子树"
4 OA 只读字段 title(职务)、account(账号)都加,nullable,OA 同步接缝
5 列表接口形状 POST /api/system/users/page + @RequestBody UserPageParam,后端按 deptId 展开子树
6 指标卡接口 独立 GET /api/system/users/stats?deptId=…
7 访问控制 管理员专用,不在 AuthUser 加 @DataScope,列表按用户选的 deptId 子树返回
8 DTO 反范式 全反范式:primaryDeptName + partTimeDepts[{id,name}] + roles[{id,name}]
9 门禁机制 轻量 @PreAuthorize 角色门禁;权限码体系延后

3. 范围(In / Out)

In:

  • 用户分页查询(部门子树 × 在职状态 × 关键词 × 最后登录时间)
  • 四指标卡统计(部门总数 / 在职 / 离职 / 待分配),跟随选中部门子树
  • 复用已有 users/assign-roles(补门禁)
  • AuthUser 新增 employment_status / title / account 三只读字段

Out(显式 no):

  • OA 入职/离职/复职按钮——后端无 OA 集成;employment_status 字段即未来 OA 同步落地点,按钮替换为同步任务。
  • 移交数据——依赖未建的线索/商机/客户/项目四个业务模块,无法落地。
  • 完整权限码体系(SysMenu 加 perms/apiUrl + 接口级鉴权 + 权限点管理页)——独立立项(原型 issue 04),本次只到角色门禁。
  • 用户 CRUD(新增/编辑/删除)——ADR-0002:OA 是用户主数据唯一来源,CRM 不造不删用户;离职用户保留(名下可能挂业务数据)。

4. 数据模型变更(AuthUser / crm_auth_user)

新增列(均 nullable,OA 来源只读,CRM 不写):

类型 说明
employment_status varchar(16) not null default 'active' 在职(active)/离职(resigned);现有存量数据默认 active
title varchar(50) null 职务(OA)
account varchar(50) null 登录账号(OA),参与列表模糊搜索

保留:enabled(账号启用,CRM 可禁用,不等于在职状态)、mobileusernamelast_login_timedept_id(主部门)。兼职部门走既有 sys_user_dept

离职即回收角色:语义保留(employment_status→resigned 时清空 sys_user_role),但触发方是未来 OA 同步任务,不在本模块实现;本模块只保证字段与查询就绪。

5. API 设计

5.1 用户列表

POST /api/system/users/page

  • 权限:@PreAuthorize 管理员角色门禁(意图权限码 system:user:list)
  • 入参 UserPageParam(extends BaseParam):
    • current / size(分页)
    • deptId(Long,可选)——选中部门节点;null=全公司
    • employmentStatus(String,可选)——active/resigned;unassigned 由前端"待分配"卡走特殊路径(见 5.3)
    • keyword(String,可选)——模糊匹配 username / account / mobile(忽略大小写首尾空白)
    • loginFrom / loginTo(LocalDate,可选)——last_login_time 落在 [from 0:00, to 23:59:59];设定区间时从未登录的用户一律排除
  • 后端过滤逻辑:
    • 部门:若 deptId 非空,展开为子树(含自身及全部子孙,沿用 sys_dept.ancestors 祖级链),取用户部门集合(dept_idsys_user_dept.dept_id)与子树交集非空者。null 则不限部门。
    • 状态/关键词/登录时间:上述规则。
  • 返回 Result<PageResult<UserListDTO>>

5.2 指标卡统计

GET /api/system/users/stats?deptId=…

  • 权限:@PreAuthorize 管理员门禁(意图权限码 system:user:list)
  • 入参:deptId(可选,null=全公司)
  • 返回 Result<UserStatsVO>:
    • deptCount——选中节点及其子孙部门数(null 时为全部部门数)
    • activeCount——子树内在职用户数
    • resignedCount——子树内离职用户数
    • unassignedCount——子树内在职且无角色用户数(LEFT JOIN sys_user_role IS NULL)
  • 口径:只随部门子树变,不受 keyword/登录时间影响(与列表过滤解耦)

5.3 待分配卡片

前端点"待分配"卡 → 调 users/page,后端需支持 employmentStatus=unassigned 等价于"在职且无角色"的过滤(实现可在 Service 层把 unassigned 翻成 employment_status=active AND NOT EXISTS(sys_user_role))。statsunassignedCount 与此同口径。

5.4 复用既有接口

  • POST /api/system/users/assign-roles(已存在,补 @PreAuthorize 门禁;意图权限码 system:user:assign-roles)
  • GET /api/system/depts/tree(左下部门树,已存在)
  • POST /api/system/roles/page(分配角色弹窗的角色选项,已存在)

6. UserListDTO(全反范式)

字段 类型 说明
id Long 用户 id
username String 姓名
account String 账号(可空)
title String 职务(可空)
mobile String 手机号
primaryDeptId Long 主部门 id
primaryDeptName String 主部门名
partTimeDepts List<{id,name}> 兼职部门(部门列小标签)
employmentStatus String active/resigned
enabled Boolean 账号是否启用
roles List<{id,name}> 所持角色(空则前端显"无角色")
lastLoginTime LocalDateTime 最后登录时间(null=从未登录)

后端批量查 dept/role 组装(收集 primaryDeptId + partTimeDeptIds → 批量取 SysDept;收集 roleIds → 批量取 SysRole),避免 N+1。

7. 访问控制

  • @EnableMethodSecurity 已开;给 users/pageusers/statsusers/assign-roles@PreAuthorize,基于 admin 角色/authority(DataInitializer 已初始化管理员角色)。
  • 开发期需核实角色→authority 映射是否就绪;若 JWT 未把角色映射为 authority,则用 Service 层 SecurityUtils + sys_user_role 判定 admin(实现细节,不改变"管理员专用"决策)。
  • 不在 AuthUser 上加 @DataScope:用户管理是管理职能,管理员要看全公司;deptId 过滤是用户显式选择,非按当前用户数据范围自动收窄。
  • 意图权限码(待权限码体系落地后切换):system:user:list(page+stats)、system:user:assign-roles

8. 行为规格(对齐原型)

  • 部门树:根节点平铺,无虚拟"全公司"节点;进入默认选首根节点。选中节点含全部子孙。
  • 指标卡"在职/离职/待分配"可点筛选(互斥,再点取消),与搜索区"账号状态"下拉共用同一状态;清空筛选复位。
  • 列:姓名(+当前身份/待分配 tag)/部门(主+兼职小标签)/职务/手机号/状态(在职·离职 tag)/所属角色/最后登录时间/操作。
  • 行内操作(在职):分配角色(弹窗多选,复用 assign-roles);模拟离职/复职/移交数据均为 Out,前端如保留按钮则只作占位,后端不提供。

9. 错误码

复用 crm-basecrm-auth 既有区间(见根 README §7)。本模块不新增业务错误码;参数校验失败走 @Valid+全局异常处理(40001);非管理员访问走 403(由 SecurityExceptionHandlers 返回)。

10. ADR 候选(建议记录,待你确认)

后端 docs/adr/ 尚不存在;README/AuthUserServiceImpl 引用了 ADR-0003/0004/0006(文件未落地),故新 ADR 从 0007 起编号以避让。

  • ADR-0007:用户在职状态独立于账号启用,来源于 OA 同步——employment_statusenabled 分离,理由:OA 管雇佣状态、CRM 管账号启停,二者不可混(离职应禁登录,禁用未必离职)。难拆、非显而易见、有真实取舍 → 满足三条。
  • ADR-0008:用户列表访问控制暂用角色门禁,权限码体系延后——@PreAuthorize 角色门禁先于完整权限码体系(接口级鉴权未落地)。约束未在代码显式、有真实取舍、切换有成本 → 满足三条。

11. 测试计划

  • users/page:① 单部门子树命中主部门用户;② 兼职用户在兼职部门子树命中(部门集合∩子树);③ 状态/关键词/登录时间四道过滤叠加;④ 分页 size 边界;⑤ employmentStatus=unassigned 等价在职且无角色;⑥ loginFrom/to 设定时从未登录者排除。
  • users/stats:四个口径随子树正确;unassignedCount 与"在职且无角色"一致;不随 keyword 变。
  • assign-roles:既有逻辑不变,补门禁后非管理员 → 403。
  • @PreAuthorize:非管理员调三个端点均 403。

12. 依赖与假设

  • 依赖 crm-auth 既有用户/部门/角色模型;DataInitializer 管理员角色;crm-baseBaseParam/PageResult/Result/SecurityUtils/AssertUtils
  • 假设:存量 crm_auth_user 行迁移时 employment_status 默认 active、title/account 为 null(列展示"-",搜索按空匹配)。
  • 无新外部依赖。