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 作为额外固定条件叠加。
输出 / 结果
成功 200:Result<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 | 非超级管理员 | 角色守卫 |
业务规则
-
鉴权:复用
@RequireSuperAdmin+ JwtHandlerInterceptor。 -
白名单映射(service 层维护静态 Map):
-
sortField入参 → SQL 列名:tCreateDate/tLastLoginDate/sUsername/sUserCode均与列名同名(直接使用)。禁止用户传任意列名——必须白名单匹配。 -
queryField入参 → SQL 列引用(含 JOIN 别名):-
username→u.sUsername -
employeeName→e.sEmployeeName -
userCode→u.sUserCode -
departmentName→d.sDepartmentName -
userType→u.sUserType -
isDeleted→u.iIsDeleted -
lastLoginDate→u.tLastLoginDate -
createdBy→u.sCreatedBy
-
-
-
matchMode 处理:
-
contains→LIKE CONCAT('%', #{queryValue}, '%') -
notContains→NOT LIKE CONCAT('%', #{queryValue}, '%') OR <col> IS NULL -
equals→= #{queryValue} - 对
isDeleted(int)/lastLoginDate(datetime)这类非字符串字段:无论 matchMode 一律按equals处理(service 层规范化 queryValue 为对应类型;非法值返 40001)。
-
- 空 queryValue:queryField 给了但 queryValue 为 null / 空串 → 跳过此条件,不参与 WHERE。
- 空 queryField + 有 queryValue:跳过(缺 queryField 没法应用)。
-
越界 page:先按入参 page/size 查;若返回 records 为空但 total > 0,service 层用
lastPage = (total + size - 1) / size重新查一次并返回 lastPage 的数据。响应page字段反映实际返回的页码(即 lastPage),让前端能感知矫正。 -
排序 SQL:在 ORDER BY 前用白名单映射列名 + asc/desc 拼接;用 MyBatis 字符串替换(
${})但只限白名单值(白名单已校验,安全)。 - 不返回密码:UserListItemVo 不含 sPasswordHash 字段;mapper SELECT 列表显式列出业务列。
- N+1 防御:JOIN 而非多次查询;单查询返回所有字段。
- 空查询:所有筛选都空时返回全表分页(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
验收标准
后端集成测试:
- admin token + 默认参数 → 200,返回所有用户(按 tCreateDate desc);total = 实际行数;不含密码字段
- page=2 size=2 → 返回第 2 页 2 条;page=2,total 不变
- size > 100 → 400 / 40001
- page < 1 → 400 / 40001
- sortField=sUsername sortOrder=asc → 按 username 升序返回
- sortField=nonExisting → 400 / 40003
- sortOrder=foo → 400 / 40001
- queryField=username matchMode=contains queryValue=ali → 返回含 "ali" 的用户
- queryField=username matchMode=equals queryValue=alice → 仅返回 alice
- queryField=username matchMode=notContains queryValue=ali → 不含 alice 但含 admin / bob_deleted
- queryField=employeeName matchMode=contains queryValue=张 → 返回员工名含张的用户(JOIN)
- queryField=departmentName matchMode=equals queryValue=技术部 → 返回部门=技术部的用户(多级 JOIN)
- queryField=userType matchMode=equals queryValue=SUPER_ADMIN → 仅 admin
- queryField=isDeleted matchMode=equals queryValue=true → 仅 bob_deleted
- queryField=invalid → 400 / 40003
- matchMode=invalid → 400 / 40003
- queryField 提供但 queryValue 为空 → 跳过条件,返回全表(不报错)
- userType=NORMAL explicit 参数 → 仅 NORMAL 用户
- userType=INVALID → 400 / 40001
- isDeleted=false explicit → 仅启用用户
- 复合:queryField=username queryValue=al userType=NORMAL isDeleted=false → 仅匹配三条件的用户(alice)
- page 越界(page=999 size=10,total=3) → 200,返回最后一页(page=1,1 个 records 或更少),total=3
- NORMAL token → 403 / 40301
- 无 token → 401 / 40101
- 响应不含 sPasswordHash 字段 → JSON 没有 password 相关字段
- 空表(无用户) → 200,records=[],total=0