Commit 8bf84c921d71963d9e1c902d13faa10255727b05
1 parent
d931bbcb
fix(usr): 修复 review round 1 must-fix REQ-USR-004
- isDeleted/lastLoginDate 强制 matchMode=equals + lastLoginDate 类型归一化 - queryValue 判空升级为 isBlank - docs/05 § REQ-USR-004 错误码补 sortField → 40003 + 40101
Showing
3 changed files
with
72 additions
and
5 deletions
backend/src/main/java/com/xly/erp/module/usr/service/impl/UserListServiceImpl.java
| @@ -11,6 +11,9 @@ import com.xly.erp.module.usr.vo.UserListItemVo; | @@ -11,6 +11,9 @@ import com.xly.erp.module.usr.vo.UserListItemVo; | ||
| 11 | import lombok.RequiredArgsConstructor; | 11 | import lombok.RequiredArgsConstructor; |
| 12 | import org.springframework.stereotype.Service; | 12 | import org.springframework.stereotype.Service; |
| 13 | 13 | ||
| 14 | +import java.time.LocalDateTime; | ||
| 15 | +import java.time.format.DateTimeFormatter; | ||
| 16 | +import java.time.format.DateTimeParseException; | ||
| 14 | import java.util.List; | 17 | import java.util.List; |
| 15 | import java.util.Map; | 18 | import java.util.Map; |
| 16 | import java.util.Set; | 19 | import java.util.Set; |
| @@ -32,6 +35,9 @@ public class UserListServiceImpl implements UserListService { | @@ -32,6 +35,9 @@ public class UserListServiceImpl implements UserListService { | ||
| 32 | 35 | ||
| 33 | static final Set<String> MATCH_MODES = Set.of("contains", "notContains", "equals"); | 36 | static final Set<String> MATCH_MODES = Set.of("contains", "notContains", "equals"); |
| 34 | 37 | ||
| 38 | + /** spec § 业务规则 3:非字符串列(int/datetime)一律按 equals 处理。 */ | ||
| 39 | + static final Set<String> EQUALS_ONLY_FIELDS = Set.of("isDeleted", "lastLoginDate"); | ||
| 40 | + | ||
| 35 | static final Set<String> USER_TYPES = Set.of("NORMAL", "SUPER_ADMIN"); | 41 | static final Set<String> USER_TYPES = Set.of("NORMAL", "SUPER_ADMIN"); |
| 36 | 42 | ||
| 37 | static final Map<String, String> QUERY_FIELD_TO_SQL = Map.ofEntries( | 43 | static final Map<String, String> QUERY_FIELD_TO_SQL = Map.ofEntries( |
| @@ -74,8 +80,12 @@ public class UserListServiceImpl implements UserListService { | @@ -74,8 +80,12 @@ public class UserListServiceImpl implements UserListService { | ||
| 74 | throw new BizException(ErrorCode.INVALID_ENUM_PARAM, "queryField 不在白名单"); | 80 | throw new BizException(ErrorCode.INVALID_ENUM_PARAM, "queryField 不在白名单"); |
| 75 | } | 81 | } |
| 76 | // 只有 queryField + queryValue 都提供且非空才应用条件 | 82 | // 只有 queryField + queryValue 都提供且非空才应用条件 |
| 77 | - if (req.getQueryValue() != null && !req.getQueryValue().isEmpty()) { | 83 | + if (req.getQueryValue() != null && !req.getQueryValue().isBlank()) { |
| 78 | normalizedQueryValue = normalizeQueryValue(req.getQueryField(), req.getQueryValue()); | 84 | normalizedQueryValue = normalizeQueryValue(req.getQueryField(), req.getQueryValue()); |
| 85 | + // spec § 业务规则 3:非字符串列一律强制 equals | ||
| 86 | + if (EQUALS_ONLY_FIELDS.contains(req.getQueryField())) { | ||
| 87 | + matchMode = "equals"; | ||
| 88 | + } | ||
| 79 | } else { | 89 | } else { |
| 80 | sqlQueryColumn = null; // 缺 queryValue 跳过条件 | 90 | sqlQueryColumn = null; // 缺 queryValue 跳过条件 |
| 81 | } | 91 | } |
| @@ -117,8 +127,10 @@ public class UserListServiceImpl implements UserListService { | @@ -117,8 +127,10 @@ public class UserListServiceImpl implements UserListService { | ||
| 117 | } | 127 | } |
| 118 | 128 | ||
| 119 | /** | 129 | /** |
| 120 | - * 对 isDeleted(int 0/1)和 lastLoginDate(datetime)做规范化;其他字段返回原值。 | ||
| 121 | - * 非法值抛 40001。 | 130 | + * 对非字符串列做规范化(spec § 业务规则 3): |
| 131 | + * - isDeleted: true/1 → "1";false/0 → "0";其他抛 40001 | ||
| 132 | + * - lastLoginDate: 解析 ISO LOCAL_DATE_TIME 或 ISO LOCAL_DATE,统一格式为 'yyyy-MM-dd HH:mm:ss';非法抛 40001 | ||
| 133 | + * - 其他列返回原值 | ||
| 122 | */ | 134 | */ |
| 123 | private String normalizeQueryValue(String queryField, String raw) { | 135 | private String normalizeQueryValue(String queryField, String raw) { |
| 124 | if ("isDeleted".equals(queryField)) { | 136 | if ("isDeleted".equals(queryField)) { |
| @@ -127,6 +139,20 @@ public class UserListServiceImpl implements UserListService { | @@ -127,6 +139,20 @@ public class UserListServiceImpl implements UserListService { | ||
| 127 | throw new BizException(ErrorCode.BAD_REQUEST, | 139 | throw new BizException(ErrorCode.BAD_REQUEST, |
| 128 | "queryField=isDeleted 时 queryValue 必须为 true / false / 0 / 1"); | 140 | "queryField=isDeleted 时 queryValue 必须为 true / false / 0 / 1"); |
| 129 | } | 141 | } |
| 142 | + if ("lastLoginDate".equals(queryField)) { | ||
| 143 | + try { | ||
| 144 | + LocalDateTime dt; | ||
| 145 | + if (raw.contains("T") || raw.contains(" ")) { | ||
| 146 | + dt = LocalDateTime.parse(raw.replace(' ', 'T')); | ||
| 147 | + } else { | ||
| 148 | + dt = java.time.LocalDate.parse(raw).atStartOfDay(); | ||
| 149 | + } | ||
| 150 | + return dt.format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")); | ||
| 151 | + } catch (DateTimeParseException e) { | ||
| 152 | + throw new BizException(ErrorCode.BAD_REQUEST, | ||
| 153 | + "queryField=lastLoginDate 时 queryValue 必须为 ISO 日期或日期时间"); | ||
| 154 | + } | ||
| 155 | + } | ||
| 130 | return raw; | 156 | return raw; |
| 131 | } | 157 | } |
| 132 | } | 158 | } |
backend/src/test/java/com/xly/erp/module/usr/service/UserListServiceImplTest.java
| @@ -194,6 +194,46 @@ class UserListServiceImplTest { | @@ -194,6 +194,46 @@ class UserListServiceImplTest { | ||
| 194 | } | 194 | } |
| 195 | 195 | ||
| 196 | @Test | 196 | @Test |
| 197 | + void list_queryByIsDeleted_matchModeContains_isForcedToEquals() { | ||
| 198 | + // spec § 业务规则 3:isDeleted matchMode 强制 equals | ||
| 199 | + UserQueryReq r = req(); | ||
| 200 | + r.setQueryField("isDeleted"); | ||
| 201 | + r.setMatchMode("contains"); | ||
| 202 | + r.setQueryValue("true"); | ||
| 203 | + PageResult<UserListItemVo> result = service.list(r); | ||
| 204 | + // 应该等同于 equals true → 仅 bob_deleted | ||
| 205 | + assertEquals(1, result.getTotal()); | ||
| 206 | + assertEquals(LoginTestSeeder.USER_DELETED, result.getRecords().get(0).getUsername()); | ||
| 207 | + } | ||
| 208 | + | ||
| 209 | + @Test | ||
| 210 | + void list_queryByLastLoginDate_matchModeContains_isForcedToEquals_andDateNormalized() { | ||
| 211 | + // 给 alice 写一个明确的 lastLoginDate | ||
| 212 | + jdbc.update("UPDATE sys_user SET tLastLoginDate='2026-05-15 10:00:00' WHERE sUsername=?", | ||
| 213 | + LoginTestSeeder.USER_OK); | ||
| 214 | + | ||
| 215 | + UserQueryReq r = req(); | ||
| 216 | + r.setQueryField("lastLoginDate"); | ||
| 217 | + r.setMatchMode("contains"); | ||
| 218 | + r.setQueryValue("2026-05-15 10:00:00"); | ||
| 219 | + PageResult<UserListItemVo> result = service.list(r); | ||
| 220 | + assertEquals(1, result.getTotal()); | ||
| 221 | + assertEquals(LoginTestSeeder.USER_OK, result.getRecords().get(0).getUsername()); | ||
| 222 | + } | ||
| 223 | + | ||
| 224 | + @Test | ||
| 225 | + void list_queryByLastLoginDate_invalidValue_throws40001() { | ||
| 226 | + UserQueryReq r = req(); | ||
| 227 | + r.setQueryField("lastLoginDate"); | ||
| 228 | + r.setQueryValue("not-a-date"); | ||
| 229 | + BizException e = assertThrows(BizException.class, () -> service.list(r)); | ||
| 230 | + assertEquals(ErrorCode.BAD_REQUEST, e.getCode()); | ||
| 231 | + } | ||
| 232 | + | ||
| 233 | + @org.springframework.beans.factory.annotation.Autowired | ||
| 234 | + private org.springframework.jdbc.core.JdbcTemplate jdbc; | ||
| 235 | + | ||
| 236 | + @Test | ||
| 197 | void list_responseDoesNotIncludePasswordField() { | 237 | void list_responseDoesNotIncludePasswordField() { |
| 198 | PageResult<UserListItemVo> result = service.list(req()); | 238 | PageResult<UserListItemVo> result = service.list(req()); |
| 199 | UserListItemVo vo = result.getRecords().get(0); | 239 | UserListItemVo vo = result.getRecords().get(0); |
docs/05-API接口契约.md
| @@ -124,6 +124,7 @@ BasePath: `/api/v1` | @@ -124,6 +124,7 @@ BasePath: `/api/v1` | ||
| 124 | - **响应**: `PageResult<UserListItemVo>`,`UserListItemVo { userId, username, employeeName, userCode, departmentName, userType, language, isDeleted, lastLoginDate, createdBy, createdDate }`(密码字段不返回) | 124 | - **响应**: `PageResult<UserListItemVo>`,`UserListItemVo { userId, username, employeeName, userCode, departmentName, userType, language, isDeleted, lastLoginDate, createdBy, createdDate }`(密码字段不返回) |
| 125 | 125 | ||
| 126 | #### 错误码 | 126 | #### 错误码 |
| 127 | -- `40001` — 分页参数越界或类型错误 | ||
| 128 | -- `40003` — `queryField` / `matchMode` 不在白名单 | 127 | +- `40001` — 分页参数越界或类型错误(page<1 / size 越界 / sortOrder 非 asc-desc / userType 非枚举 / lastLoginDate 入参非法日期 / isDeleted 入参非布尔) |
| 128 | +- `40003` — `sortField` / `queryField` / `matchMode` 不在白名单 | ||
| 129 | +- `40101` — 未携带或无效 Token | ||
| 129 | - `40301` — 当前用户非超级管理员,无权调用 | 130 | - `40301` — 当前用户非超级管理员,无权调用 |