From c6d8e4e344c4a6f17772e488eb9e370340d905ce Mon Sep 17 00:00:00 2001 From: zichun Date: Mon, 1 Jun 2026 14:25:59 +0800 Subject: [PATCH] feat(usr): 查询用户入参 UserQueryDTO 与输出 UserVO REQ-USR-003 --- backend/src/main/java/com/xly/erp/modules/usr/dto/UserQueryDTO.java | 77 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ backend/src/main/java/com/xly/erp/modules/usr/vo/UserVO.java | 152 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ backend/src/test/java/com/xly/erp/modules/usr/dto/UserQueryDTOValidationTest.java | 81 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ backend/src/test/java/com/xly/erp/modules/usr/vo/UserVOJsonTest.java | 56 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 366 insertions(+), 0 deletions(-) create mode 100644 backend/src/main/java/com/xly/erp/modules/usr/dto/UserQueryDTO.java create mode 100644 backend/src/main/java/com/xly/erp/modules/usr/vo/UserVO.java create mode 100644 backend/src/test/java/com/xly/erp/modules/usr/dto/UserQueryDTOValidationTest.java create mode 100644 backend/src/test/java/com/xly/erp/modules/usr/vo/UserVOJsonTest.java diff --git a/backend/src/main/java/com/xly/erp/modules/usr/dto/UserQueryDTO.java b/backend/src/main/java/com/xly/erp/modules/usr/dto/UserQueryDTO.java new file mode 100644 index 0000000..66f7057 --- /dev/null +++ b/backend/src/main/java/com/xly/erp/modules/usr/dto/UserQueryDTO.java @@ -0,0 +1,77 @@ +package com.xly.erp.modules.usr.dto; + +import jakarta.validation.constraints.Pattern; +import jakarta.validation.constraints.Size; + +/** + * 查询用户入参(docs/05 契约 / spec § 2.1)。REQ-USR-003 T2。 + * + *

query 参数绑定(非 JSON body):{@code queryField}/{@code matchType}/{@code queryValue}/ + * {@code pageNum}/{@code pageSize} 均为小驼峰,query 参数名直接同名绑定,全部可选。

+ * + *

校验口径:{@code queryField}/{@code matchType} 走 {@link Pattern}(null 跳过,默认值由 + * Service 兜底),越界 → 40001;{@code queryValue} {@link Size}(max=100),超长 → 40001。 + * {@code pageNum}/{@code pageSize} **不**加 @Min/@Max(避免 @Valid 失败被全局处理器统一转 + * 40001,与 spec 要求的 42201 冲突,spec § 8 D8)——范围在 Service 入口显式判定抛 42201。

+ */ +public class UserQueryDTO { + + /** 查询字段;null 时 Service 兜底「用户名」。越界 → 40001。 */ + @Pattern(regexp = "^(用户名|员工名|用户号|部门|用户类型|作废|登录日期|制单人)$", + message = "查询字段取值非法") + private String queryField; + + /** 匹配方式;null 时 Service 兜底「包含」。越界 → 40001。 */ + @Pattern(regexp = "^(包含|不包含|等于)$", message = "匹配方式取值非法") + private String matchType; + + /** 查询值;null / trim 后空 = 不施加条件。 */ + @Size(max = 100, message = "查询值长度不能超过 100") + private String queryValue; + + /** 页码,从 1 起;范围在 Service 入口判定(42201)。 */ + private Integer pageNum; + + /** 每页条数,1..100;范围在 Service 入口判定(42201)。 */ + private Integer pageSize; + + public String getQueryField() { + return queryField; + } + + public void setQueryField(String queryField) { + this.queryField = queryField; + } + + public String getMatchType() { + return matchType; + } + + public void setMatchType(String matchType) { + this.matchType = matchType; + } + + public String getQueryValue() { + return queryValue; + } + + public void setQueryValue(String queryValue) { + this.queryValue = queryValue; + } + + public Integer getPageNum() { + return pageNum; + } + + public void setPageNum(Integer pageNum) { + this.pageNum = pageNum; + } + + public Integer getPageSize() { + return pageSize; + } + + public void setPageSize(Integer pageSize) { + this.pageSize = pageSize; + } +} diff --git a/backend/src/main/java/com/xly/erp/modules/usr/vo/UserVO.java b/backend/src/main/java/com/xly/erp/modules/usr/vo/UserVO.java new file mode 100644 index 0000000..6923ad3 --- /dev/null +++ b/backend/src/main/java/com/xly/erp/modules/usr/vo/UserVO.java @@ -0,0 +1,152 @@ +package com.xly.erp.modules.usr.vo; + +import com.fasterxml.jackson.annotation.JsonProperty; +import java.io.Serializable; +import java.time.LocalDateTime; + +/** + * 查询用户输出(spec § 2.2 契约)。REQ-USR-003 T2。 + * + *

严格不含 {@code sPassword} 与租户列({@code sId}/{@code sBrandsId}/{@code sSubsidiaryId})。 + * 跨表字段 {@code employeeName}/{@code department} 来自 {@code usr_employee}(LEFT JOIN,可 null)。

+ * + *

带匈牙利前缀字段({@code sUserName} 等)的 getter 形如 {@code getSUserName} 会被 Jackson + * 推断为属性名 {@code SUserName},与契约 JSON 键不符;故对这些字段加 {@link JsonProperty} + * 锁定小驼峰键名(与 CreateUserDTO/UpdateUserDTO 同做法)。日期字段 {@code tLastLoginDate}/ + * {@code tCreateDate} 的 getter({@code getTLastLoginDate} 等)同样被 Jackson 推断为 + * {@code tlastLoginDate}(首段连续大写被小写化),与契约键不符,故一并加 @JsonProperty 锁键。 + * {@code employeeName}/{@code department}/{@code id} 为常规小驼峰,无需 @JsonProperty。

+ */ +public class UserVO implements Serializable { + + private static final long serialVersionUID = 1L; + + /** 用户主键 iIncrement。 */ + private Integer id; + + /** 用户名。 */ + @JsonProperty("sUserName") + private String sUserName; + + /** 员工名(usr_employee.sEmployeeName,LEFT JOIN 可 null)。 */ + private String employeeName; + + /** 用户号。 */ + @JsonProperty("sUserNo") + private String sUserNo; + + /** 部门(usr_employee.sDepartment,LEFT JOIN 可 null)。 */ + private String department; + + /** 用户类型。 */ + @JsonProperty("sUserType") + private String sUserType; + + /** 界面语言。 */ + @JsonProperty("sLanguage") + private String sLanguage; + + /** 作废标志:0 正常 / 1 已作废。 */ + @JsonProperty("iIsVoid") + private Integer iIsVoid; + + /** 最后登录时间(可 null)。 */ + @JsonProperty("tLastLoginDate") + private LocalDateTime tLastLoginDate; + + /** 制单人。 */ + @JsonProperty("sCreator") + private String sCreator; + + /** 创建时间。 */ + @JsonProperty("tCreateDate") + private LocalDateTime tCreateDate; + + public Integer getId() { + return id; + } + + public void setId(Integer id) { + this.id = id; + } + + public String getSUserName() { + return sUserName; + } + + public void setSUserName(String sUserName) { + this.sUserName = sUserName; + } + + public String getEmployeeName() { + return employeeName; + } + + public void setEmployeeName(String employeeName) { + this.employeeName = employeeName; + } + + public String getSUserNo() { + return sUserNo; + } + + public void setSUserNo(String sUserNo) { + this.sUserNo = sUserNo; + } + + public String getDepartment() { + return department; + } + + public void setDepartment(String department) { + this.department = department; + } + + public String getSUserType() { + return sUserType; + } + + public void setSUserType(String sUserType) { + this.sUserType = sUserType; + } + + public String getSLanguage() { + return sLanguage; + } + + public void setSLanguage(String sLanguage) { + this.sLanguage = sLanguage; + } + + public Integer getIIsVoid() { + return iIsVoid; + } + + public void setIIsVoid(Integer iIsVoid) { + this.iIsVoid = iIsVoid; + } + + public LocalDateTime getTLastLoginDate() { + return tLastLoginDate; + } + + public void setTLastLoginDate(LocalDateTime tLastLoginDate) { + this.tLastLoginDate = tLastLoginDate; + } + + public String getSCreator() { + return sCreator; + } + + public void setSCreator(String sCreator) { + this.sCreator = sCreator; + } + + public LocalDateTime getTCreateDate() { + return tCreateDate; + } + + public void setTCreateDate(LocalDateTime tCreateDate) { + this.tCreateDate = tCreateDate; + } +} diff --git a/backend/src/test/java/com/xly/erp/modules/usr/dto/UserQueryDTOValidationTest.java b/backend/src/test/java/com/xly/erp/modules/usr/dto/UserQueryDTOValidationTest.java new file mode 100644 index 0000000..e5b58ce --- /dev/null +++ b/backend/src/test/java/com/xly/erp/modules/usr/dto/UserQueryDTOValidationTest.java @@ -0,0 +1,81 @@ +package com.xly.erp.modules.usr.dto; + +import static org.assertj.core.api.Assertions.assertThat; + +import jakarta.validation.ConstraintViolation; +import jakarta.validation.Validation; +import jakarta.validation.Validator; +import jakarta.validation.ValidatorFactory; +import java.util.Set; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +/** + * REQ-USR-003 T2:UserQueryDTO Bean Validation 校验。 + * + *

queryField/matchType 走 @Pattern(null 跳过,默认值 Service 兜底);queryValue @Size(max=100); + * pageNum/pageSize 不加 @Min/@Max(范围在 Service 入口判定 42201,spec § 8 D8)。

+ */ +class UserQueryDTOValidationTest { + + private static ValidatorFactory factory; + private static Validator validator; + + @BeforeAll + static void setUp() { + factory = Validation.buildDefaultValidatorFactory(); + validator = factory.getValidator(); + } + + @AfterAll + static void tearDown() { + if (factory != null) { + factory.close(); + } + } + + @Test + void acceptsAllNullAsValid() { + UserQueryDTO dto = new UserQueryDTO(); + Set> violations = validator.validate(dto); + assertThat(violations).isEmpty(); + } + + @Test + void acceptsLegalEnums() { + UserQueryDTO dto = new UserQueryDTO(); + dto.setQueryField("登录日期"); + dto.setMatchType("不包含"); + dto.setQueryValue("2026-06-01"); + Set> violations = validator.validate(dto); + assertThat(violations).isEmpty(); + } + + @Test + void rejectsIllegalQueryField() { + UserQueryDTO dto = new UserQueryDTO(); + dto.setQueryField("身份证"); + Set> violations = validator.validate(dto); + assertThat(violations) + .anyMatch(v -> v.getPropertyPath().toString().equals("queryField")); + } + + @Test + void rejectsIllegalMatchType() { + UserQueryDTO dto = new UserQueryDTO(); + dto.setMatchType("大于"); + Set> violations = validator.validate(dto); + assertThat(violations) + .anyMatch(v -> v.getPropertyPath().toString().equals("matchType")); + } + + @Test + void rejectsTooLongQueryValue() { + UserQueryDTO dto = new UserQueryDTO(); + dto.setQueryValue("x".repeat(101)); + Set> violations = validator.validate(dto); + assertThat(violations) + .anyMatch(v -> v.getPropertyPath().toString().equals("queryValue")); + } +} diff --git a/backend/src/test/java/com/xly/erp/modules/usr/vo/UserVOJsonTest.java b/backend/src/test/java/com/xly/erp/modules/usr/vo/UserVOJsonTest.java new file mode 100644 index 0000000..8debcc9 --- /dev/null +++ b/backend/src/test/java/com/xly/erp/modules/usr/vo/UserVOJsonTest.java @@ -0,0 +1,56 @@ +package com.xly.erp.modules.usr.vo; + +import static org.assertj.core.api.Assertions.assertThat; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; +import java.time.LocalDateTime; +import org.junit.jupiter.api.Test; + +/** + * REQ-USR-003 T2:UserVO 序列化契约键名 + 不含密码 / 租户列。 + * + *

带匈牙利前缀字段加 @JsonProperty 锁键为小驼峰(sUserName 等); + * employeeName/department/id/日期为普通驼峰;严格无 sPassword/password/租户列。

+ */ +class UserVOJsonTest { + + // 注册 JavaTimeModule 模拟 Spring Boot 自动配置的 ObjectMapper(支持 LocalDateTime 序列化)。 + private final ObjectMapper objectMapper = new ObjectMapper().registerModule(new JavaTimeModule()); + + @Test + void serializesContractKeysNoPassword() throws Exception { + UserVO vo = new UserVO(); + vo.setId(7); + vo.setSUserName("alice"); + vo.setEmployeeName("爱丽丝"); + vo.setSUserNo("U007"); + vo.setDepartment("财务部"); + vo.setSUserType("超级管理员"); + vo.setSLanguage("中文"); + vo.setIIsVoid(0); + vo.setTLastLoginDate(LocalDateTime.of(2026, 6, 1, 12, 0, 0)); + vo.setSCreator("admin"); + vo.setTCreateDate(LocalDateTime.of(2026, 5, 1, 9, 0, 0)); + + String json = objectMapper.writeValueAsString(vo); + + assertThat(json) + .contains("\"id\"") + .contains("\"sUserName\"") + .contains("\"employeeName\"") + .contains("\"sUserNo\"") + .contains("\"department\"") + .contains("\"sUserType\"") + .contains("\"sLanguage\"") + .contains("\"iIsVoid\"") + .contains("\"tLastLoginDate\"") + .contains("\"sCreator\"") + .contains("\"tCreateDate\""); + // 匈牙利前缀字段 @JsonProperty 锁键生效:不得出现 Jackson 默认推断的大写首字母键名。 + assertThat(json).doesNotContain("\"SUserName\""); + // 严格不含密码 / 租户列。 + assertThat(json).doesNotContain("sPassword").doesNotContain("password"); + assertThat(json).doesNotContain("sBrandsId").doesNotContain("sSubsidiaryId"); + } +} -- libgit2 0.22.2