From 8bf84c921d71963d9e1c902d13faa10255727b05 Mon Sep 17 00:00:00 2001 From: zichun Date: Fri, 15 May 2026 10:24:33 +0800 Subject: [PATCH] fix(usr): 修复 review round 1 must-fix REQ-USR-004 --- backend/src/main/java/com/xly/erp/module/usr/service/impl/UserListServiceImpl.java | 32 +++++++++++++++++++++++++++++--- backend/src/test/java/com/xly/erp/module/usr/service/UserListServiceImplTest.java | 40 ++++++++++++++++++++++++++++++++++++++++ docs/05-API接口契约.md | 5 +++-- 3 files changed, 72 insertions(+), 5 deletions(-) diff --git a/backend/src/main/java/com/xly/erp/module/usr/service/impl/UserListServiceImpl.java b/backend/src/main/java/com/xly/erp/module/usr/service/impl/UserListServiceImpl.java index 96ba212..e227566 100644 --- a/backend/src/main/java/com/xly/erp/module/usr/service/impl/UserListServiceImpl.java +++ b/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; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; +import java.time.format.DateTimeParseException; import java.util.List; import java.util.Map; import java.util.Set; @@ -32,6 +35,9 @@ public class UserListServiceImpl implements UserListService { static final Set MATCH_MODES = Set.of("contains", "notContains", "equals"); + /** spec § 业务规则 3:非字符串列(int/datetime)一律按 equals 处理。 */ + static final Set EQUALS_ONLY_FIELDS = Set.of("isDeleted", "lastLoginDate"); + static final Set USER_TYPES = Set.of("NORMAL", "SUPER_ADMIN"); static final Map QUERY_FIELD_TO_SQL = Map.ofEntries( @@ -74,8 +80,12 @@ public class UserListServiceImpl implements UserListService { throw new BizException(ErrorCode.INVALID_ENUM_PARAM, "queryField 不在白名单"); } // 只有 queryField + queryValue 都提供且非空才应用条件 - if (req.getQueryValue() != null && !req.getQueryValue().isEmpty()) { + if (req.getQueryValue() != null && !req.getQueryValue().isBlank()) { normalizedQueryValue = normalizeQueryValue(req.getQueryField(), req.getQueryValue()); + // spec § 业务规则 3:非字符串列一律强制 equals + if (EQUALS_ONLY_FIELDS.contains(req.getQueryField())) { + matchMode = "equals"; + } } else { sqlQueryColumn = null; // 缺 queryValue 跳过条件 } @@ -117,8 +127,10 @@ public class UserListServiceImpl implements UserListService { } /** - * 对 isDeleted(int 0/1)和 lastLoginDate(datetime)做规范化;其他字段返回原值。 - * 非法值抛 40001。 + * 对非字符串列做规范化(spec § 业务规则 3): + * - isDeleted: true/1 → "1";false/0 → "0";其他抛 40001 + * - lastLoginDate: 解析 ISO LOCAL_DATE_TIME 或 ISO LOCAL_DATE,统一格式为 'yyyy-MM-dd HH:mm:ss';非法抛 40001 + * - 其他列返回原值 */ private String normalizeQueryValue(String queryField, String raw) { if ("isDeleted".equals(queryField)) { @@ -127,6 +139,20 @@ public class UserListServiceImpl implements UserListService { throw new BizException(ErrorCode.BAD_REQUEST, "queryField=isDeleted 时 queryValue 必须为 true / false / 0 / 1"); } + if ("lastLoginDate".equals(queryField)) { + try { + LocalDateTime dt; + if (raw.contains("T") || raw.contains(" ")) { + dt = LocalDateTime.parse(raw.replace(' ', 'T')); + } else { + dt = java.time.LocalDate.parse(raw).atStartOfDay(); + } + return dt.format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")); + } catch (DateTimeParseException e) { + throw new BizException(ErrorCode.BAD_REQUEST, + "queryField=lastLoginDate 时 queryValue 必须为 ISO 日期或日期时间"); + } + } return raw; } } diff --git a/backend/src/test/java/com/xly/erp/module/usr/service/UserListServiceImplTest.java b/backend/src/test/java/com/xly/erp/module/usr/service/UserListServiceImplTest.java index 7fdd252..cca052d 100644 --- a/backend/src/test/java/com/xly/erp/module/usr/service/UserListServiceImplTest.java +++ b/backend/src/test/java/com/xly/erp/module/usr/service/UserListServiceImplTest.java @@ -194,6 +194,46 @@ class UserListServiceImplTest { } @Test + void list_queryByIsDeleted_matchModeContains_isForcedToEquals() { + // spec § 业务规则 3:isDeleted matchMode 强制 equals + UserQueryReq r = req(); + r.setQueryField("isDeleted"); + r.setMatchMode("contains"); + r.setQueryValue("true"); + PageResult result = service.list(r); + // 应该等同于 equals true → 仅 bob_deleted + assertEquals(1, result.getTotal()); + assertEquals(LoginTestSeeder.USER_DELETED, result.getRecords().get(0).getUsername()); + } + + @Test + void list_queryByLastLoginDate_matchModeContains_isForcedToEquals_andDateNormalized() { + // 给 alice 写一个明确的 lastLoginDate + jdbc.update("UPDATE sys_user SET tLastLoginDate='2026-05-15 10:00:00' WHERE sUsername=?", + LoginTestSeeder.USER_OK); + + UserQueryReq r = req(); + r.setQueryField("lastLoginDate"); + r.setMatchMode("contains"); + r.setQueryValue("2026-05-15 10:00:00"); + PageResult result = service.list(r); + assertEquals(1, result.getTotal()); + assertEquals(LoginTestSeeder.USER_OK, result.getRecords().get(0).getUsername()); + } + + @Test + void list_queryByLastLoginDate_invalidValue_throws40001() { + UserQueryReq r = req(); + r.setQueryField("lastLoginDate"); + r.setQueryValue("not-a-date"); + BizException e = assertThrows(BizException.class, () -> service.list(r)); + assertEquals(ErrorCode.BAD_REQUEST, e.getCode()); + } + + @org.springframework.beans.factory.annotation.Autowired + private org.springframework.jdbc.core.JdbcTemplate jdbc; + + @Test void list_responseDoesNotIncludePasswordField() { PageResult result = service.list(req()); UserListItemVo vo = result.getRecords().get(0); diff --git a/docs/05-API接口契约.md b/docs/05-API接口契约.md index 406e12b..6b0f52c 100644 --- a/docs/05-API接口契约.md +++ b/docs/05-API接口契约.md @@ -124,6 +124,7 @@ BasePath: `/api/v1` - **响应**: `PageResult`,`UserListItemVo { userId, username, employeeName, userCode, departmentName, userType, language, isDeleted, lastLoginDate, createdBy, createdDate }`(密码字段不返回) #### 错误码 -- `40001` — 分页参数越界或类型错误 -- `40003` — `queryField` / `matchMode` 不在白名单 +- `40001` — 分页参数越界或类型错误(page<1 / size 越界 / sortOrder 非 asc-desc / userType 非枚举 / lastLoginDate 入参非法日期 / isDeleted 入参非布尔) +- `40003` — `sortField` / `queryField` / `matchMode` 不在白名单 +- `40101` — 未携带或无效 Token - `40301` — 当前用户非超级管理员,无权调用 -- libgit2 0.22.2