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 的"作废"字段查询是显式例外。

业务规则

  1. 参数归一化field / match 缺失或空 → 默认 用户名 / 包含value trim,空串当无过滤;pageNum < 1 → 1;pageSize 缺失 → 20。
  2. pageSize 上限pageSize > 100BizException(40002, "pageSize 超过 100")
  3. field/match 校验
    • field 不在枚举内 → BizException(40001, "field 取值非法")
    • match 不在枚举内 → BizException(40001, "match 取值非法")
    • field=作废/登录日期 时 match 必须是 等于;否则 → BizException(40001, "field/match 组合非法")
  4. 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 → 不加过滤
  5. 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}
  6. 总数查询:另一条 SELECT COUNT(1) FROM tUser u LEFT JOIN tStaff s ON ... WHERE <同条件>
  7. 只读:service 上 @Transactional(readOnly = true)

边界与约束

  • field/match 非法或组合非法40001
  • pageSize > 10040002
  • 登录日期 value 格式错40001
  • value 空 → 不加过滤,返回全部(按 field/match 仍校验合法性)
  • 空结果records=[] + total=0;HTTP 200 / code=0
  • JWT 伪造20001
  • JWT 缺失 → permitAll stub
  • sPasswordHash 不返回(VO 不含字段)

实现范围与边界抉择

  1. 复用 USR-001/002 工程:不新增 mapper;UserMapper 追加自定义 SQL 方法 + 新 VO 即可。
  2. field 枚举映射用 Java enum 或 Map 常量:选 Map<String, FieldDef> 简化(无需新 enum 类);FieldDef 内含物理列名 + 类型 + 允许的 match 集合。
  3. 不支持多字段同时查询:spec 仅描述单 field × match × value;docs/05 同款。多字段后续 REQ 再补。
  4. 不支持自定义排序:固定 ORDER BY u.iIncrement DESC;docs/05 未要求 sort 参数。
  5. 使用 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.iIncrement PK 兜底 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,一个无);调 pageWithFilter field=用户名 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