Commit d3ff317da4fd3b369402cf7a1542ee9e64545780
1 parent
111d5015
docs(usr): spec + plan REQ-USR-003
Showing
2 changed files
with
325 additions
and
0 deletions
docs/superpowers/plans/2026-04-30-REQ-USR-003.md
0 → 100644
| 1 | +--- | ||
| 2 | +req_id: REQ-USR-003 | ||
| 3 | +date: 2026-04-30 | ||
| 4 | +spec_ref: docs/superpowers/specs/2026-04-30-REQ-USR-003.md | ||
| 5 | +--- | ||
| 6 | + | ||
| 7 | +# REQ-USR-003 用户查询 Implementation Plan | ||
| 8 | + | ||
| 9 | +> **Execution:** Parent skill `feature-tdd` executes this plan task-by-task. | ||
| 10 | + | ||
| 11 | +**Goal:** 实现 `GET /api/usr/users` 单条件分页查询:tUser LEFT JOIN tStaff,按 field × match × value 动态过滤。 | ||
| 12 | + | ||
| 13 | +**Architecture:** 新增 `UserListVO` + `UserListQuery` 内部封装类(field/match/value/page)+ `UserMapper#pageWithFilter` 自定义动态 SQL + `UserService#list` + controller `@GetMapping`。 | ||
| 14 | + | ||
| 15 | +**Tech Stack:** 沿用;MyBatis 动态 SQL `<script>` + `<foreach>` / `<if>`。 | ||
| 16 | + | ||
| 17 | +--- | ||
| 18 | + | ||
| 19 | +## Schema 改动 | ||
| 20 | + | ||
| 21 | +无。 | ||
| 22 | + | ||
| 23 | +## 文件变更清单 | ||
| 24 | + | ||
| 25 | +### 新增 | ||
| 26 | + | ||
| 27 | +- `backend/src/main/java/com/xly/erp/module/usr/vo/UserListVO.java` | ||
| 28 | +- `backend/src/main/java/com/xly/erp/module/usr/dto/UserListQuery.java`(内部封装规范化后的查询参数) | ||
| 29 | + | ||
| 30 | +### 修改 | ||
| 31 | + | ||
| 32 | +- `backend/src/main/java/com/xly/erp/module/usr/mapper/UserMapper.java` — 追加 `pageWithFilter` + `countWithFilter` | ||
| 33 | +- `backend/src/main/resources/mapper/usr/UserMapper.xml` — 新建 XML(动态 SQL 复杂,注解 @Select 不易读) | ||
| 34 | +- `backend/src/main/java/com/xly/erp/module/usr/service/UserService.java` — 追加 `list(...)` | ||
| 35 | +- `backend/src/main/java/com/xly/erp/module/usr/service/impl/UserServiceImpl.java` — 实现 list(字段映射 + 校验 + 调 mapper) | ||
| 36 | +- `backend/src/main/java/com/xly/erp/module/usr/controller/UserController.java` — 追加 GET 端点 | ||
| 37 | +- 测试:UserMapperIT(+1) / UserServiceImplTest(+10) / UserControllerIT(+8) | ||
| 38 | + | ||
| 39 | +## 任务步骤 | ||
| 40 | + | ||
| 41 | +### Task 1: UserListVO + UserMapper#pageWithFilter / countWithFilter + IT | ||
| 42 | + | ||
| 43 | +**Files:** | ||
| 44 | +- Create: vo/UserListVO.java | ||
| 45 | +- Modify: mapper/UserMapper.java + resources/mapper/usr/UserMapper.xml | ||
| 46 | +- Modify: test/.../mapper/UserMapperIT.java | ||
| 47 | + | ||
| 48 | +**API shape:** | ||
| 49 | +- `UserListVO` 11 字段(参 spec),@JsonProperty 锁定 JSON 名 | ||
| 50 | +- `UserMapper.pageWithFilter`(XML)参数:`@Param("field") String physicalCol, @Param("match") String matchOp, @Param("value") Object value, @Param("offset") int offset, @Param("size") int size`;`physicalCol` 由 service 映射成 `u.sUserName` / `s.sStaffName` 等 | ||
| 51 | +- `UserMapper.countWithFilter` 同参数(除 offset/size) | ||
| 52 | + | ||
| 53 | +**XML 关键结构**: | ||
| 54 | +```xml | ||
| 55 | +<select id="pageWithFilter" resultType="com.xly.erp.module.usr.vo.UserListVO"> | ||
| 56 | + SELECT u.iIncrement, u.sUserName, s.sStaffName AS staffName, u.sUserNo, | ||
| 57 | + s.sDepartment AS department, u.sUserType, u.sLanguage, | ||
| 58 | + u.bDeleted, u.tLastLoginDate, u.sCreatedBy, u.tCreateDate | ||
| 59 | + FROM tUser u | ||
| 60 | + LEFT JOIN tStaff s ON s.iIncrement = u.iStaffId AND s.bDeleted = 0 | ||
| 61 | + <where> | ||
| 62 | + <if test="value != null and value != ''"> | ||
| 63 | + <choose> | ||
| 64 | + <when test="matchOp == 'contains'">${field} LIKE CONCAT('%', #{value}, '%')</when> | ||
| 65 | + <when test="matchOp == 'notContains'">${field} NOT LIKE CONCAT('%', #{value}, '%')</when> | ||
| 66 | + <when test="matchOp == 'equals'"> | ||
| 67 | + <choose> | ||
| 68 | + <when test="field == 'DATE(u.tLastLoginDate)'">${field} = #{value}</when> | ||
| 69 | + <otherwise>${field} = #{value}</otherwise> | ||
| 70 | + </choose> | ||
| 71 | + </when> | ||
| 72 | + </choose> | ||
| 73 | + </if> | ||
| 74 | + </where> | ||
| 75 | + ORDER BY u.iIncrement DESC | ||
| 76 | + LIMIT #{offset}, #{size} | ||
| 77 | +</select> | ||
| 78 | +``` | ||
| 79 | +(XML 复杂度由 service 提供归一化的 `field`(物理列名 with prefix) / `matchOp`(英文常量) / `value` 后大幅简化) | ||
| 80 | + | ||
| 81 | +- [ ] **Step 1: 写失败 IT `pageWithFilter_filtersAndJoins`** | ||
| 82 | +- [ ] **Step 2: 实现 entity/VO + mapper + XML** | ||
| 83 | +- [ ] **Step 3: 子会话验证 PASS** | ||
| 84 | +- [ ] **Step 4: Commit**:`feat(usr): user list mapper + vo with join REQ-USR-003` | ||
| 85 | + | ||
| 86 | +### Task 2: UserListQuery + UserService.list 主流程(合法 + 字段映射) | ||
| 87 | + | ||
| 88 | +**Files:** | ||
| 89 | +- Create: dto/UserListQuery.java(service 内部) | ||
| 90 | +- Modify: service/UserService.java + impl/UserServiceImpl.java | ||
| 91 | +- Modify: test/.../service/UserServiceImplTest.java | ||
| 92 | + | ||
| 93 | +**API shape:** | ||
| 94 | +- `UserService#list(String field, String match, String value, Integer pageNum, Integer pageSize) : Map<String, Object>`(返回 records/total/pageNum/pageSize) | ||
| 95 | +- 内部:归一化(默认值/trim)→ field/match 校验 → value 解析(日期/布尔)→ 调 mapper.countWithFilter + pageWithFilter → 装结果 Map | ||
| 96 | + | ||
| 97 | +**字段映射表**(service 静态常量): | ||
| 98 | +``` | ||
| 99 | +"用户名" -> ("u.sUserName", STRING) | ||
| 100 | +"员工名" -> ("s.sStaffName", STRING) | ||
| 101 | +"用户号" -> ("u.sUserNo", STRING) | ||
| 102 | +"部门" -> ("s.sDepartment", STRING) | ||
| 103 | +"用户类型" -> ("u.sUserType", STRING) | ||
| 104 | +"作废" -> ("u.bDeleted", BOOLEAN) | ||
| 105 | +"登录日期" -> ("DATE(u.tLastLoginDate)", DATE) | ||
| 106 | +"制单人" -> ("u.sCreatedBy", STRING) | ||
| 107 | +``` | ||
| 108 | +match 映射:包含→`contains`,不包含→`notContains`,等于→`equals` | ||
| 109 | + | ||
| 110 | +**校验顺序**:pageSize 上限 → field 枚举 → match 枚举 → field/match 兼容(布尔/日期仅 equals)→ value 解析(日期) | ||
| 111 | + | ||
| 112 | +- [ ] **Step 1: 写失败测试 4 条主流程用例** | ||
| 113 | + - listWithDefaults_invokesMapperWithUserNameContainsEmpty | ||
| 114 | + - listWithEmptyValue_skipsFilterCondition | ||
| 115 | + - listWithKeywordTrim | ||
| 116 | + - listReturnsEmptyRecords_whenMapperReturnsEmptyPage | ||
| 117 | +- [ ] **Step 2: 实现 service** | ||
| 118 | +- [ ] **Step 3: 子会话验证 PASS** | ||
| 119 | +- [ ] **Step 4: Commit**:`feat(usr): user list service + field/match mapping REQ-USR-003` | ||
| 120 | + | ||
| 121 | +### Task 3: Service 异常分支(field/match/兼容/pageSize/日期格式/布尔) | ||
| 122 | + | ||
| 123 | +- [ ] **Step 1: 追加 6 用例** | ||
| 124 | + - listWithInvalidField_throws40001 | ||
| 125 | + - listWithInvalidMatch_throws40001 | ||
| 126 | + - listWithIncompatibleFieldMatch_throws40001 | ||
| 127 | + - listWithPageSizeExceeds100_throws40002 | ||
| 128 | + - listWithInvalidLoginDateFormat_throws40001 | ||
| 129 | + - listWithBooleanFieldEqualsTrue_passesIntegerOne | ||
| 130 | +- [ ] **Step 2: 实现校验分支** | ||
| 131 | +- [ ] **Step 3: 子会话验证 PASS**(期望 18 + 4 + 6 = 28 用例) | ||
| 132 | +- [ ] **Step 4: Commit**:`feat(usr): user list error branches REQ-USR-003` | ||
| 133 | + | ||
| 134 | +### Task 4: Controller GET + 8 IT + 全量回归 | ||
| 135 | + | ||
| 136 | +- [ ] **Step 1: 追加 8 IT**(参 spec 验收清单) | ||
| 137 | +- [ ] **Step 2: 实现 controller GET** | ||
| 138 | +- [ ] **Step 3: 子会话跑全量回归**(期望 ≥ 129 用例) | ||
| 139 | +- [ ] **Step 4: Commit**:`test(usr): user list integration coverage REQ-USR-003` | ||
| 140 | + | ||
| 141 | +## 提交计划 | ||
| 142 | + | ||
| 143 | +| commit | 覆盖 | | ||
| 144 | +|---|---| | ||
| 145 | +| `feat(usr): user list mapper + vo with join REQ-USR-003` | Task 1 | | ||
| 146 | +| `feat(usr): user list service + field/match mapping REQ-USR-003` | Task 2 | | ||
| 147 | +| `feat(usr): user list error branches REQ-USR-003` | Task 3 | | ||
| 148 | +| `test(usr): user list integration coverage REQ-USR-003` | Task 4 | |
docs/superpowers/specs/2026-04-30-REQ-USR-003.md
0 → 100644
| 1 | +--- | ||
| 2 | +req_id: REQ-USR-003 | ||
| 3 | +date: 2026-04-30 | ||
| 4 | +module: module_usr | ||
| 5 | +--- | ||
| 6 | + | ||
| 7 | +# Spec: REQ-USR-003 — 用户查询 | ||
| 8 | + | ||
| 9 | +## 目标 | ||
| 10 | + | ||
| 11 | +按 `field` × `match` × `value` 单条件检索用户分页列表,输出含 tUser + LEFT JOIN tStaff 字段的 11 列扁平 VO。 | ||
| 12 | + | ||
| 13 | +## 输入 / 触发 | ||
| 14 | + | ||
| 15 | +### HTTP 接口(docs/05 § REQ-USR-003) | ||
| 16 | + | ||
| 17 | +- Method / Path: `GET /api/usr/users` | ||
| 18 | +- Auth: 必需(沿用 USR-001 stub:路径已 `/api/usr/**` permitAll) | ||
| 19 | + | ||
| 20 | +### Query 参数 | ||
| 21 | + | ||
| 22 | +| 参数 | 类型 | 必填 | 取值 | 默认 | | ||
| 23 | +|---|---|---|---|---| | ||
| 24 | +| `field` | `String` | 否 | 枚举:`用户名` / `员工名` / `用户号` / `部门` / `用户类型` / `作废` / `登录日期` / `制单人` | `用户名` | | ||
| 25 | +| `match` | `String` | 否 | 枚举:`包含` / `不包含` / `等于` | `包含` | | ||
| 26 | +| `value` | `String` | 否 | 任意(含布尔/日期字面) | 空 → 不加过滤条件 | | ||
| 27 | +| `pageNum` | `Integer` | 否 | ≥ 1 | `1` | | ||
| 28 | +| `pageSize` | `Integer` | 否 | 1..100 | `20` | | ||
| 29 | + | ||
| 30 | +### field × match 兼容矩阵 | ||
| 31 | + | ||
| 32 | +| field 物理列 | 类型 | 允许的 match | | ||
| 33 | +|---|---|---| | ||
| 34 | +| 用户名 / 员工名 / 用户号 / 部门 / 用户类型 / 制单人 | 字符串 | 包含 / 不包含 / 等于 | | ||
| 35 | +| 作废 | 布尔(tinyint) | 仅 `等于`(解析 value: `true`/`false`/`1`/`0` → 1/0) | | ||
| 36 | +| 登录日期 | 日期 | 仅 `等于`(按 `YYYY-MM-DD` 精确日期匹配 `DATE(tLastLoginDate) = ?`) | | ||
| 37 | + | ||
| 38 | +非法组合(如 `field=作废 & match=包含`)→ `BizException(40001, "field/match 取值或组合非法")`。 | ||
| 39 | + | ||
| 40 | +## 输出 / 结果 | ||
| 41 | + | ||
| 42 | +### 成功响应 | ||
| 43 | + | ||
| 44 | +```json | ||
| 45 | +{ | ||
| 46 | + "code": 0, | ||
| 47 | + "msg": "ok", | ||
| 48 | + "data": { | ||
| 49 | + "records": [ | ||
| 50 | + { | ||
| 51 | + "iIncrement": 1, "sUserName": "u1", "staffName": "员工A", "sUserNo": "001", | ||
| 52 | + "department": "IT", "sUserType": "普通用户", "sLanguage": "zh", | ||
| 53 | + "bDeleted": false, "tLastLoginDate": "2026-04-30T09:00:00", | ||
| 54 | + "sCreatedBy": "STUB_ADMIN", "tCreateDate": "2026-04-30T08:00:00" | ||
| 55 | + } | ||
| 56 | + ], | ||
| 57 | + "total": 42, "pageNum": 1, "pageSize": 20 | ||
| 58 | + } | ||
| 59 | +} | ||
| 60 | +``` | ||
| 61 | + | ||
| 62 | +### VO `UserListVO`(11 字段) | ||
| 63 | + | ||
| 64 | +| 字段 | 类型 | 来源 | | ||
| 65 | +|---|---|---| | ||
| 66 | +| `iIncrement` | `Integer` | `tUser.iIncrement` | | ||
| 67 | +| `sUserName` | `String` | `tUser.sUserName` | | ||
| 68 | +| `staffName` | `String` | `tStaff.sStaffName`(LEFT JOIN,未关联时为 null) | | ||
| 69 | +| `sUserNo` | `String` | `tUser.sUserNo` | | ||
| 70 | +| `department` | `String` | `tStaff.sDepartment`(LEFT JOIN) | | ||
| 71 | +| `sUserType` | `String` | `tUser.sUserType` | | ||
| 72 | +| `sLanguage` | `String` | `tUser.sLanguage` | | ||
| 73 | +| `bDeleted` | `Boolean` | `tUser.bDeleted` | | ||
| 74 | +| `tLastLoginDate` | `LocalDateTime` | `tUser.tLastLoginDate` | | ||
| 75 | +| `sCreatedBy` | `String` | `tUser.sCreatedBy` | | ||
| 76 | +| `tCreateDate` | `LocalDateTime` | `tUser.tCreateDate` | | ||
| 77 | + | ||
| 78 | +> **不返回** `sPasswordHash` / `iStaffId` / `sBrandsId` / `sSubsidiaryId` / `sId` / 软删除审计字段(spec § 边界与约束 显式排除敏感字段)。 | ||
| 79 | +> | ||
| 80 | +> **不过滤** `bDeleted=0`:因为「作废」是用户可主动查询的字段;docs/03 § tUser 业务注记的"默认过滤 bDeleted=0"针对常规列表场景,本 REQ 的"作废"字段查询是显式例外。 | ||
| 81 | + | ||
| 82 | +## 业务规则 | ||
| 83 | + | ||
| 84 | +1. **参数归一化**:`field` / `match` 缺失或空 → 默认 `用户名` / `包含`;`value` trim,空串当无过滤;`pageNum` < 1 → 1;`pageSize` 缺失 → 20。 | ||
| 85 | +2. **pageSize 上限**:`pageSize > 100` → `BizException(40002, "pageSize 超过 100")`。 | ||
| 86 | +3. **field/match 校验**: | ||
| 87 | + - `field` 不在枚举内 → `BizException(40001, "field 取值非法")` | ||
| 88 | + - `match` 不在枚举内 → `BizException(40001, "match 取值非法")` | ||
| 89 | + - field=作废/登录日期 时 match 必须是 `等于`;否则 → `BizException(40001, "field/match 组合非法")` | ||
| 90 | +4. **value 解析**(match=等于 + 非字符串列): | ||
| 91 | + - field=作废:value ∈ `[true, false, 1, 0]` → 1/0;其他 → 视为不命中(结果空);空 value → 不加过滤 | ||
| 92 | + - field=登录日期:value 必须可被 `LocalDate.parse(value)` 解析为 `YYYY-MM-DD`;不能解析 → `BizException(40001, "登录日期值格式非法(应为 YYYY-MM-DD)")`;空 value → 不加过滤 | ||
| 93 | +5. **SQL 拼装**: | ||
| 94 | + ```sql | ||
| 95 | + SELECT u.iIncrement, u.sUserName, s.sStaffName AS staffName, u.sUserNo, | ||
| 96 | + s.sDepartment AS department, u.sUserType, u.sLanguage, | ||
| 97 | + u.bDeleted, u.tLastLoginDate, u.sCreatedBy, u.tCreateDate | ||
| 98 | + FROM tUser u | ||
| 99 | + LEFT JOIN tStaff s ON s.iIncrement = u.iStaffId AND s.bDeleted = 0 | ||
| 100 | + WHERE <动态条件> | ||
| 101 | + ORDER BY u.iIncrement DESC | ||
| 102 | + LIMIT #{offset}, #{pageSize} | ||
| 103 | + ``` | ||
| 104 | + - 字符串列:`WHERE col LIKE CONCAT('%', #{value}, '%')`(包含)/ `NOT LIKE`(不包含)/ `= #{value}`(等于) | ||
| 105 | + - 布尔列:`WHERE u.bDeleted = #{boolValue}` | ||
| 106 | + - 日期列:`WHERE DATE(u.tLastLoginDate) = #{dateValue}` | ||
| 107 | +6. **总数查询**:另一条 `SELECT COUNT(1) FROM tUser u LEFT JOIN tStaff s ON ... WHERE <同条件>`。 | ||
| 108 | +7. **只读**:service 上 `@Transactional(readOnly = true)`。 | ||
| 109 | + | ||
| 110 | +## 边界与约束 | ||
| 111 | + | ||
| 112 | +- **field/match 非法或组合非法** → `40001` | ||
| 113 | +- **pageSize > 100** → `40002` | ||
| 114 | +- **登录日期 value 格式错** → `40001` | ||
| 115 | +- **value 空** → 不加过滤,返回全部(按 field/match 仍校验合法性) | ||
| 116 | +- **空结果** → `records=[]` + `total=0`;HTTP 200 / `code=0` | ||
| 117 | +- **JWT 伪造** → `20001` | ||
| 118 | +- **JWT 缺失** → permitAll stub | ||
| 119 | +- **`sPasswordHash` 不返回**(VO 不含字段) | ||
| 120 | + | ||
| 121 | +## 实现范围与边界抉择 | ||
| 122 | + | ||
| 123 | +1. **复用 USR-001/002 工程**:不新增 mapper;UserMapper 追加自定义 SQL 方法 + 新 VO 即可。 | ||
| 124 | +2. **field 枚举映射用 Java enum 或 Map 常量**:选 `Map<String, FieldDef>` 简化(无需新 enum 类);`FieldDef` 内含物理列名 + 类型 + 允许的 match 集合。 | ||
| 125 | +3. **不支持多字段同时查询**:spec 仅描述单 field × match × value;docs/05 同款。多字段后续 REQ 再补。 | ||
| 126 | +4. **不支持自定义排序**:固定 `ORDER BY u.iIncrement DESC`;docs/05 未要求 sort 参数。 | ||
| 127 | +5. **使用 MyBatis-Plus `IPage<T>`**:mapper 方法 `IPage<UserListVO> pageWithFilter(IPage<UserListVO> page, ...)`;service 把 IPage 转为响应 Map。 | ||
| 128 | + | ||
| 129 | +## 依赖的 schema 表 / 字段 | ||
| 130 | + | ||
| 131 | +读取: | ||
| 132 | +- `tUser`:iIncrement / sUserName / sUserNo / sUserType / sLanguage / bDeleted / tLastLoginDate / sCreatedBy / tCreateDate / iStaffId(用于 JOIN) | ||
| 133 | +- `tStaff`:iIncrement / sStaffName / sDepartment / bDeleted(用于 JOIN ON) | ||
| 134 | + | ||
| 135 | +依赖索引: | ||
| 136 | +- `tUser.uk_user_no` / `uk_user_name`(不直接命中 LIKE 但 ORDER BY iIncrement 走主键) | ||
| 137 | +- `tStaff.iIncrement` PK 兜底 JOIN | ||
| 138 | + | ||
| 139 | +## 依赖的接口 | ||
| 140 | + | ||
| 141 | +无。 | ||
| 142 | + | ||
| 143 | +## 验收标准 | ||
| 144 | + | ||
| 145 | +### 单元测试(追加到 `UserServiceImplTest`) | ||
| 146 | + | ||
| 147 | +- [x] `listWithDefaults_invokesMapperWithUserNameContainsEmpty` — 全空参数;mapper 入参 field=用户名 / match=包含 / value="" / page=1 / size=20 | ||
| 148 | +- [x] `listWithEmptyValue_skipsFilterCondition` | ||
| 149 | +- [x] `listWithInvalidField_throws40001` | ||
| 150 | +- [x] `listWithInvalidMatch_throws40001` | ||
| 151 | +- [x] `listWithIncompatibleFieldMatch_throws40001` — field=作废 + match=包含 | ||
| 152 | +- [x] `listWithPageSizeExceeds100_throws40002` | ||
| 153 | +- [x] `listWithInvalidLoginDateFormat_throws40001` — field=登录日期 / match=等于 / value="abc" | ||
| 154 | +- [x] `listWithBooleanFieldEqualsTrue_passesIntegerOne` — field=作废 / match=等于 / value="true" | ||
| 155 | +- [x] `listWithKeywordTrim` — value=" abc " → 传给 mapper 时 trim 为 "abc" | ||
| 156 | +- [x] `listReturnsEmptyRecords_whenMapperReturnsEmptyPage` | ||
| 157 | + | ||
| 158 | +### Mapper IT(追加到 `UserMapperIT`) | ||
| 159 | + | ||
| 160 | +- [x] `userMapper#pageWithFilter_filtersAndJoins` — JdbcTemplate 直插 staff + 2 user(一个有 iStaffId,一个无);调 `pageWithFilter` field=用户名 match=包含 value="";返回 records=[user1, user2],user1.staffName == "员工X",user2.staffName == null | ||
| 161 | + | ||
| 162 | +### 集成测试(`UserControllerIT`,追加 8 用例) | ||
| 163 | + | ||
| 164 | +- [x] `getDefaults_with_jwt_returns200_andList` | ||
| 165 | +- [x] `getKeywordContains_filtersByUsername` | ||
| 166 | +- [x] `getKeywordEquals_filtersExact` | ||
| 167 | +- [x] `getInvalidField_returns40001` | ||
| 168 | +- [x] `getPageSizeExceeds100_returns40002` | ||
| 169 | +- [x] `getNoMatch_returnsEmptyArray` | ||
| 170 | +- [x] `getWithoutJwt_permitAllStub_returns200` | ||
| 171 | +- [x] `getTamperedJwt_returns20001` | ||
| 172 | + | ||
| 173 | +### 工程验收 | ||
| 174 | + | ||
| 175 | +- [x] `mvn -B test` 全绿(110 + 新增 ≥ 19 = ≥ 129 用例) | ||
| 176 | +- [x] 响应 records 不含 sPasswordHash 字段 | ||
| 177 | +- [x] LEFT JOIN 在用户无 iStaffId 时 staffName/department 为 null |