2026-04-30-REQ-USR-003.md
8.07 KB
req_id: REQ-USR-003 date: 2026-04-30
module: module_usr
Spec: REQ-USR-003 — 用户查询
目标
按 field × match × value 单条件检索用户分页列表,输出含 tUser + LEFT JOIN tStaff 字段的 11 列扁平 VO。
输入 / 触发
HTTP 接口(docs/05 § REQ-USR-003)
- Method / Path:
GET /api/usr/users - Auth: 必需(沿用 USR-001 stub:路径已
/api/usr/**permitAll)
Query 参数
| 参数 | 类型 | 必填 | 取值 | 默认 |
|---|---|---|---|---|
field |
String |
否 | 枚举:用户名 / 员工名 / 用户号 / 部门 / 用户类型 / 作废 / 登录日期 / 制单人
|
用户名 |
match |
String |
否 | 枚举:包含 / 不包含 / 等于
|
包含 |
value |
String |
否 | 任意(含布尔/日期字面) | 空 → 不加过滤条件 |
pageNum |
Integer |
否 | ≥ 1 | 1 |
pageSize |
Integer |
否 | 1..100 | 20 |
field × match 兼容矩阵
| field 物理列 | 类型 | 允许的 match |
|---|---|---|
| 用户名 / 员工名 / 用户号 / 部门 / 用户类型 / 制单人 | 字符串 | 包含 / 不包含 / 等于 |
| 作废 | 布尔(tinyint) | 仅 等于(解析 value: true/false/1/0 → 1/0) |
| 登录日期 | 日期 | 仅 等于(按 YYYY-MM-DD 精确日期匹配 DATE(tLastLoginDate) = ?) |
非法组合(如 field=作废 & match=包含)→ BizException(40001, "field/match 取值或组合非法")。
输出 / 结果
成功响应
{
"code": 0,
"msg": "ok",
"data": {
"records": [
{
"iIncrement": 1, "sUserName": "u1", "staffName": "员工A", "sUserNo": "001",
"department": "IT", "sUserType": "普通用户", "sLanguage": "zh",
"bDeleted": false, "tLastLoginDate": "2026-04-30T09:00:00",
"sCreatedBy": "STUB_ADMIN", "tCreateDate": "2026-04-30T08:00:00"
}
],
"total": 42, "pageNum": 1, "pageSize": 20
}
}
VO UserListVO(11 字段)
| 字段 | 类型 | 来源 |
|---|---|---|
iIncrement |
Integer |
tUser.iIncrement |
sUserName |
String |
tUser.sUserName |
staffName |
String |
tStaff.sStaffName(LEFT JOIN,未关联时为 null) |
sUserNo |
String |
tUser.sUserNo |
department |
String |
tStaff.sDepartment(LEFT JOIN) |
sUserType |
String |
tUser.sUserType |
sLanguage |
String |
tUser.sLanguage |
bDeleted |
Boolean |
tUser.bDeleted |
tLastLoginDate |
LocalDateTime |
tUser.tLastLoginDate |
sCreatedBy |
String |
tUser.sCreatedBy |
tCreateDate |
LocalDateTime |
tUser.tCreateDate |
不返回
sPasswordHash/iStaffId/sBrandsId/sSubsidiaryId/sId/ 软删除审计字段(spec § 边界与约束 显式排除敏感字段)。不过滤
bDeleted=0:因为「作废」是用户可主动查询的字段;docs/03 § tUser 业务注记的"默认过滤 bDeleted=0"针对常规列表场景,本 REQ 的"作废"字段查询是显式例外。
业务规则
-
参数归一化:
field/match缺失或空 → 默认用户名/包含;valuetrim,空串当无过滤;pageNum< 1 → 1;pageSize缺失 → 20。 -
pageSize 上限:
pageSize > 100→BizException(40002, "pageSize 超过 100")。 -
field/match 校验:
-
field不在枚举内 →BizException(40001, "field 取值非法") -
match不在枚举内 →BizException(40001, "match 取值非法") - field=作废/登录日期 时 match 必须是
等于;否则 →BizException(40001, "field/match 组合非法")
-
-
value 解析(match=等于 + 非字符串列):
- field=作废:value ∈
[true, false, 1, 0]→ 1/0;其他 → 视为不命中(结果空);空 value → 不加过滤 - field=登录日期:value 必须可被
LocalDate.parse(value)解析为YYYY-MM-DD;不能解析 →BizException(40001, "登录日期值格式非法(应为 YYYY-MM-DD)");空 value → 不加过滤
- field=作废:value ∈
-
SQL 拼装:
sql SELECT u.iIncrement, u.sUserName, s.sStaffName AS staffName, u.sUserNo, s.sDepartment AS department, u.sUserType, u.sLanguage, u.bDeleted, u.tLastLoginDate, u.sCreatedBy, u.tCreateDate FROM tUser u LEFT JOIN tStaff s ON s.iIncrement = u.iStaffId AND s.bDeleted = 0 WHERE <动态条件> ORDER BY u.iIncrement DESC LIMIT #{offset}, #{pageSize}- 字符串列:
WHERE col LIKE CONCAT('%', #{value}, '%')(包含)/NOT LIKE(不包含)/= #{value}(等于) - 布尔列:
WHERE u.bDeleted = #{boolValue} - 日期列:
WHERE DATE(u.tLastLoginDate) = #{dateValue}
- 字符串列:
-
总数查询:另一条
SELECT COUNT(1) FROM tUser u LEFT JOIN tStaff s ON ... WHERE <同条件>。 -
只读:service 上
@Transactional(readOnly = true)。
边界与约束
-
field/match 非法或组合非法 →
40001 -
pageSize > 100 →
40002 -
登录日期 value 格式错 →
40001 - value 空 → 不加过滤,返回全部(按 field/match 仍校验合法性)
-
空结果 →
records=[]+total=0;HTTP 200 /code=0 -
JWT 伪造 →
20001 - JWT 缺失 → permitAll stub
-
sPasswordHash不返回(VO 不含字段)
实现范围与边界抉择
- 复用 USR-001/002 工程:不新增 mapper;UserMapper 追加自定义 SQL 方法 + 新 VO 即可。
-
field 枚举映射用 Java enum 或 Map 常量:选
Map<String, FieldDef>简化(无需新 enum 类);FieldDef内含物理列名 + 类型 + 允许的 match 集合。 - 不支持多字段同时查询:spec 仅描述单 field × match × value;docs/05 同款。多字段后续 REQ 再补。
-
不支持自定义排序:固定
ORDER BY u.iIncrement DESC;docs/05 未要求 sort 参数。 -
使用 MyBatis-Plus
IPage<T>:mapper 方法IPage<UserListVO> pageWithFilter(IPage<UserListVO> page, ...);service 把 IPage 转为响应 Map。
依赖的 schema 表 / 字段
读取:
-
tUser:iIncrement / sUserName / sUserNo / sUserType / sLanguage / bDeleted / tLastLoginDate / sCreatedBy / tCreateDate / iStaffId(用于 JOIN) -
tStaff:iIncrement / sStaffName / sDepartment / bDeleted(用于 JOIN ON)
依赖索引:
-
tUser.uk_user_no/uk_user_name(不直接命中 LIKE 但 ORDER BY iIncrement 走主键) -
tStaff.iIncrementPK 兜底 JOIN
依赖的接口
无。
验收标准
单元测试(追加到 UserServiceImplTest)
-
listWithDefaults_invokesMapperWithUserNameContainsEmpty— 全空参数;mapper 入参 field=用户名 / match=包含 / value="" / page=1 / size=20 -
listWithEmptyValue_skipsFilterCondition -
listWithInvalidField_throws40001 -
listWithInvalidMatch_throws40001 -
listWithIncompatibleFieldMatch_throws40001— field=作废 + match=包含 -
listWithPageSizeExceeds100_throws40002 -
listWithInvalidLoginDateFormat_throws40001— field=登录日期 / match=等于 / value="abc" -
listWithBooleanFieldEqualsTrue_passesIntegerOne— field=作废 / match=等于 / value="true" -
listWithKeywordTrim— value=" abc " → 传给 mapper 时 trim 为 "abc" -
listReturnsEmptyRecords_whenMapperReturnsEmptyPage
Mapper IT(追加到 UserMapperIT)
-
userMapper#pageWithFilter_filtersAndJoins— JdbcTemplate 直插 staff + 2 user(一个有 iStaffId,一个无);调pageWithFilterfield=用户名 match=包含 value="";返回 records=[user1, user2],user1.staffName == "员工X",user2.staffName == null
集成测试(UserControllerIT,追加 8 用例)
-
getDefaults_with_jwt_returns200_andList -
getKeywordContains_filtersByUsername -
getKeywordEquals_filtersExact -
getInvalidField_returns40001 -
getPageSizeExceeds100_returns40002 -
getNoMatch_returnsEmptyArray -
getWithoutJwt_permitAllStub_returns200 -
getTamperedJwt_returns20001
工程验收
-
mvn -B test全绿(110 + 新增 ≥ 19 = ≥ 129 用例) - 响应 records 不含 sPasswordHash 字段
- LEFT JOIN 在用户无 iStaffId 时 staffName/department 为 null