2026-05-15-REQ-USR-004.md 8.55 KB

req_id: REQ-USR-004 date: 2026-05-15

module: module_usr

Spec: REQ-USR-004 — 查询用户

目标

GET /api/v1/users:超级管理员分页 + 多字段筛选 + 排序查询用户列表,单条记录聚合 sys_user + sys_employee + sys_department 信息(部门名、员工名)。只读,无写副作用,不返回密码。

输入 / 触发

HTTP 入口 GET /api/v1/users,要求 Authorization: Bearer <accessToken> + userType=SUPER_ADMIN

Query 参数(全部可选):

参数 类型 默认 校验
page int 1 @Min(1);< 1 返 40001;大于总页数返"最后一页"数据(不是空列表)
size int 20 @Min(1) @Max(100);越界返 40001
sortField string tCreateDate 白名单:tCreateDate / tLastLoginDate / sUsername / sUserCode;不在白名单返 40003
sortOrder string desc asc / desc;其他值返 40001
queryField string (不筛选) 白名单:username / employeeName / userCode / departmentName / userType / isDeleted / lastLoginDate / createdBy;不在白名单返 40003
matchMode string contains contains / notContains / equals;不在白名单返 40003
queryValue string (不筛选) 任意字符串;空字符串或 null 视为不应用此条件;与 queryField 配对使用——只提供 queryField 不提供 queryValue 也视为不应用
userType string (不筛选) 若提供,必须是 NORMAL / SUPER_ADMIN;其他返 40001
isDeleted boolean (不筛选) true / false

复合规则:所有筛选条件用 AND 拼接:queryField+queryValue 提供时构成一条动态条件;userType / isDeleted 作为额外固定条件叠加。

输出 / 结果

成功 200Result<PageResult<UserListItemVo>>

{
  "code": 200,
  "message": "操作成功",
  "data": {
    "records": [
      {
        "userId": 42,
        "username": "alice",
        "employeeName": "张三",
        "userCode": "U001",
        "departmentName": "技术部",
        "userType": "NORMAL",
        "language": "zh-CN",
        "isDeleted": false,
        "lastLoginDate": "2026-05-15T08:00:00",
        "createdBy": "admin",
        "createdDate": "2026-05-15T07:00:00"
      }
    ],
    "total": 17,
    "page": 1,
    "size": 20
  }
}

字段:

  • userId = sys_user.iIncrement
  • username / userCode / userType / language / isDeleted / lastLoginDate / createdBy / createdDate 直接来自 sys_user
  • employeeName = LEFT JOIN sys_employee.sEmployeeName(用户未关联职员时返回 null)
  • departmentName = LEFT JOIN sys_department.sDepartmentName via sys_employee.iDepartmentId(未关联或部门软删时返回 null)

失败

HTTP code 含义 触发
400 40001 分页 / 类型 / 排序参数错误 page100 / sortOrder 非 asc-desc / userType 非枚举
400 40003 queryField / matchMode / sortField 不在白名单 用户传非法枚举值
401 40101 未携带或无效 Token 鉴权层
403 40301 非超级管理员 角色守卫

业务规则

  1. 鉴权:复用 @RequireSuperAdmin + JwtHandlerInterceptor。
  2. 白名单映射(service 层维护静态 Map):
    • sortField 入参 → SQL 列名:tCreateDate/tLastLoginDate/sUsername/sUserCode 均与列名同名(直接使用)。禁止用户传任意列名——必须白名单匹配。
    • queryField 入参 → SQL 列引用(含 JOIN 别名):
      • usernameu.sUsername
      • employeeNamee.sEmployeeName
      • userCodeu.sUserCode
      • departmentNamed.sDepartmentName
      • userTypeu.sUserType
      • isDeletedu.iIsDeleted
      • lastLoginDateu.tLastLoginDate
      • createdByu.sCreatedBy
  3. matchMode 处理
    • containsLIKE CONCAT('%', #{queryValue}, '%')
    • notContainsNOT LIKE CONCAT('%', #{queryValue}, '%') OR <col> IS NULL
    • equals= #{queryValue}
    • isDeleted(int)/ lastLoginDate(datetime)这类非字符串字段:无论 matchMode 一律按 equals 处理(service 层规范化 queryValue 为对应类型;非法值返 40001)。
  4. 空 queryValue:queryField 给了但 queryValue 为 null / 空串 → 跳过此条件,不参与 WHERE。
  5. 空 queryField + 有 queryValue:跳过(缺 queryField 没法应用)。
  6. 越界 page:先按入参 page/size 查;若返回 records 为空但 total > 0,service 层用 lastPage = (total + size - 1) / size 重新查一次并返回 lastPage 的数据。响应 page 字段反映实际返回的页码(即 lastPage),让前端能感知矫正。
  7. 排序 SQL:在 ORDER BY 前用白名单映射列名 + asc/desc 拼接;用 MyBatis 字符串替换(${})但只限白名单值(白名单已校验,安全)。
  8. 不返回密码:UserListItemVo 不含 sPasswordHash 字段;mapper SELECT 列表显式列出业务列。
  9. N+1 防御:JOIN 而非多次查询;单查询返回所有字段。
  10. 空查询:所有筛选都空时返回全表分页(admin 用例需要"全部 用户"视图)。

边界与约束

  • 白名单兜底:所有动态字段(queryField / matchMode / sortField / sortOrder)必须 service 层先做白名单检查,再拼到 SQL;用户输入永远不直接进 ORDER BY / SELECT。
  • MyBatis 注入:用 #{} 参数化输入;只有列名 / 排序方向用 ${}(已白名单约束)。
  • size 上限 100:与 docs/04 § 3.2 一致。
  • 作废用户参与查询:默认不过滤;用户通过 isDeleted=false 显式过滤启用账号。
  • 登录追踪:本 REQ 不修改 tLastLoginDate / iFailedLoginCount / tLockUntil — 纯查询。
  • 不实现
    • 多字段同时筛选(spec 仅允许单一 queryField,多字段筛选推迟)
    • 自定义列展示(前端事)
    • 导出 / 导入(YAGNI)

依赖的 schema 表 / 字段

只读 sys_user(V1 已建):

  • 读列:iIncrement, sUsername, sUserCode, sUserType, sLanguage, iIsDeleted, tLastLoginDate, sCreatedBy, tCreateDate, iEmployeeId
  • 排序 / 筛选列:sUsername, sUserCode, sUserType, iIsDeleted, tLastLoginDate, sCreatedBy, tCreateDate

只读 sys_employee(V1 已建):

  • LEFT JOIN:iIncrement = u.iEmployeeId
  • 读 / 筛选列:sEmployeeName, iDepartmentId

只读 sys_department(V1 已建):

  • LEFT JOIN:iIncrement = e.iDepartmentId
  • 读 / 筛选列:sDepartmentName

本 REQ 不需要新增 migration

依赖的接口

  • 本 REQ 提供:GET /api/v1/users

验收标准

后端集成测试:

  1. admin token + 默认参数 → 200,返回所有用户(按 tCreateDate desc);total = 实际行数;不含密码字段
  2. page=2 size=2 → 返回第 2 页 2 条;page=2,total 不变
  3. size > 100 → 400 / 40001
  4. page < 1 → 400 / 40001
  5. sortField=sUsername sortOrder=asc → 按 username 升序返回
  6. sortField=nonExisting → 400 / 40003
  7. sortOrder=foo → 400 / 40001
  8. queryField=username matchMode=contains queryValue=ali → 返回含 "ali" 的用户
  9. queryField=username matchMode=equals queryValue=alice → 仅返回 alice
  10. queryField=username matchMode=notContains queryValue=ali → 不含 alice 但含 admin / bob_deleted
  11. queryField=employeeName matchMode=contains queryValue=张 → 返回员工名含张的用户(JOIN)
  12. queryField=departmentName matchMode=equals queryValue=技术部 → 返回部门=技术部的用户(多级 JOIN)
  13. queryField=userType matchMode=equals queryValue=SUPER_ADMIN → 仅 admin
  14. queryField=isDeleted matchMode=equals queryValue=true → 仅 bob_deleted
  15. queryField=invalid → 400 / 40003
  16. matchMode=invalid → 400 / 40003
  17. queryField 提供但 queryValue 为空 → 跳过条件,返回全表(不报错)
  18. userType=NORMAL explicit 参数 → 仅 NORMAL 用户
  19. userType=INVALID → 400 / 40001
  20. isDeleted=false explicit → 仅启用用户
  21. 复合:queryField=username queryValue=al userType=NORMAL isDeleted=false → 仅匹配三条件的用户(alice)
  22. page 越界(page=999 size=10,total=3) → 200,返回最后一页(page=1,1 个 records 或更少),total=3
  23. NORMAL token → 403 / 40301
  24. 无 token → 401 / 40101
  25. 响应不含 sPasswordHash 字段 → JSON 没有 password 相关字段
  26. 空表(无用户) → 200,records=[],total=0