Commit c6d8e4e344c4a6f17772e488eb9e370340d905ce

Authored by zichun
1 parent b8d7d588

feat(usr): 查询用户入参 UserQueryDTO 与输出 UserVO REQ-USR-003

backend/src/main/java/com/xly/erp/modules/usr/dto/UserQueryDTO.java 0 → 100644
  1 +package com.xly.erp.modules.usr.dto;
  2 +
  3 +import jakarta.validation.constraints.Pattern;
  4 +import jakarta.validation.constraints.Size;
  5 +
  6 +/**
  7 + * 查询用户入参(docs/05 契约 / spec § 2.1)。REQ-USR-003 T2。
  8 + *
  9 + * <p>query 参数绑定(非 JSON body):{@code queryField}/{@code matchType}/{@code queryValue}/
  10 + * {@code pageNum}/{@code pageSize} 均为小驼峰,query 参数名直接同名绑定,全部可选。</p>
  11 + *
  12 + * <p>校验口径:{@code queryField}/{@code matchType} 走 {@link Pattern}(null 跳过,默认值由
  13 + * Service 兜底),越界 → 40001;{@code queryValue} {@link Size}(max=100),超长 → 40001。
  14 + * {@code pageNum}/{@code pageSize} **不**加 @Min/@Max(避免 @Valid 失败被全局处理器统一转
  15 + * 40001,与 spec 要求的 42201 冲突,spec § 8 D8)——范围在 Service 入口显式判定抛 42201。</p>
  16 + */
  17 +public class UserQueryDTO {
  18 +
  19 + /** 查询字段;null 时 Service 兜底「用户名」。越界 → 40001。 */
  20 + @Pattern(regexp = "^(用户名|员工名|用户号|部门|用户类型|作废|登录日期|制单人)$",
  21 + message = "查询字段取值非法")
  22 + private String queryField;
  23 +
  24 + /** 匹配方式;null 时 Service 兜底「包含」。越界 → 40001。 */
  25 + @Pattern(regexp = "^(包含|不包含|等于)$", message = "匹配方式取值非法")
  26 + private String matchType;
  27 +
  28 + /** 查询值;null / trim 后空 = 不施加条件。 */
  29 + @Size(max = 100, message = "查询值长度不能超过 100")
  30 + private String queryValue;
  31 +
  32 + /** 页码,从 1 起;范围在 Service 入口判定(42201)。 */
  33 + private Integer pageNum;
  34 +
  35 + /** 每页条数,1..100;范围在 Service 入口判定(42201)。 */
  36 + private Integer pageSize;
  37 +
  38 + public String getQueryField() {
  39 + return queryField;
  40 + }
  41 +
  42 + public void setQueryField(String queryField) {
  43 + this.queryField = queryField;
  44 + }
  45 +
  46 + public String getMatchType() {
  47 + return matchType;
  48 + }
  49 +
  50 + public void setMatchType(String matchType) {
  51 + this.matchType = matchType;
  52 + }
  53 +
  54 + public String getQueryValue() {
  55 + return queryValue;
  56 + }
  57 +
  58 + public void setQueryValue(String queryValue) {
  59 + this.queryValue = queryValue;
  60 + }
  61 +
  62 + public Integer getPageNum() {
  63 + return pageNum;
  64 + }
  65 +
  66 + public void setPageNum(Integer pageNum) {
  67 + this.pageNum = pageNum;
  68 + }
  69 +
  70 + public Integer getPageSize() {
  71 + return pageSize;
  72 + }
  73 +
  74 + public void setPageSize(Integer pageSize) {
  75 + this.pageSize = pageSize;
  76 + }
  77 +}
backend/src/main/java/com/xly/erp/modules/usr/vo/UserVO.java 0 → 100644
  1 +package com.xly.erp.modules.usr.vo;
  2 +
  3 +import com.fasterxml.jackson.annotation.JsonProperty;
  4 +import java.io.Serializable;
  5 +import java.time.LocalDateTime;
  6 +
  7 +/**
  8 + * 查询用户输出(spec § 2.2 契约)。REQ-USR-003 T2。
  9 + *
  10 + * <p>严格不含 {@code sPassword} 与租户列({@code sId}/{@code sBrandsId}/{@code sSubsidiaryId})。
  11 + * 跨表字段 {@code employeeName}/{@code department} 来自 {@code usr_employee}(LEFT JOIN,可 null)。</p>
  12 + *
  13 + * <p>带匈牙利前缀字段({@code sUserName} 等)的 getter 形如 {@code getSUserName} 会被 Jackson
  14 + * 推断为属性名 {@code SUserName},与契约 JSON 键不符;故对这些字段加 {@link JsonProperty}
  15 + * 锁定小驼峰键名(与 CreateUserDTO/UpdateUserDTO 同做法)。日期字段 {@code tLastLoginDate}/
  16 + * {@code tCreateDate} 的 getter({@code getTLastLoginDate} 等)同样被 Jackson 推断为
  17 + * {@code tlastLoginDate}(首段连续大写被小写化),与契约键不符,故一并加 @JsonProperty 锁键。
  18 + * {@code employeeName}/{@code department}/{@code id} 为常规小驼峰,无需 @JsonProperty。</p>
  19 + */
  20 +public class UserVO implements Serializable {
  21 +
  22 + private static final long serialVersionUID = 1L;
  23 +
  24 + /** 用户主键 iIncrement。 */
  25 + private Integer id;
  26 +
  27 + /** 用户名。 */
  28 + @JsonProperty("sUserName")
  29 + private String sUserName;
  30 +
  31 + /** 员工名(usr_employee.sEmployeeName,LEFT JOIN 可 null)。 */
  32 + private String employeeName;
  33 +
  34 + /** 用户号。 */
  35 + @JsonProperty("sUserNo")
  36 + private String sUserNo;
  37 +
  38 + /** 部门(usr_employee.sDepartment,LEFT JOIN 可 null)。 */
  39 + private String department;
  40 +
  41 + /** 用户类型。 */
  42 + @JsonProperty("sUserType")
  43 + private String sUserType;
  44 +
  45 + /** 界面语言。 */
  46 + @JsonProperty("sLanguage")
  47 + private String sLanguage;
  48 +
  49 + /** 作废标志:0 正常 / 1 已作废。 */
  50 + @JsonProperty("iIsVoid")
  51 + private Integer iIsVoid;
  52 +
  53 + /** 最后登录时间(可 null)。 */
  54 + @JsonProperty("tLastLoginDate")
  55 + private LocalDateTime tLastLoginDate;
  56 +
  57 + /** 制单人。 */
  58 + @JsonProperty("sCreator")
  59 + private String sCreator;
  60 +
  61 + /** 创建时间。 */
  62 + @JsonProperty("tCreateDate")
  63 + private LocalDateTime tCreateDate;
  64 +
  65 + public Integer getId() {
  66 + return id;
  67 + }
  68 +
  69 + public void setId(Integer id) {
  70 + this.id = id;
  71 + }
  72 +
  73 + public String getSUserName() {
  74 + return sUserName;
  75 + }
  76 +
  77 + public void setSUserName(String sUserName) {
  78 + this.sUserName = sUserName;
  79 + }
  80 +
  81 + public String getEmployeeName() {
  82 + return employeeName;
  83 + }
  84 +
  85 + public void setEmployeeName(String employeeName) {
  86 + this.employeeName = employeeName;
  87 + }
  88 +
  89 + public String getSUserNo() {
  90 + return sUserNo;
  91 + }
  92 +
  93 + public void setSUserNo(String sUserNo) {
  94 + this.sUserNo = sUserNo;
  95 + }
  96 +
  97 + public String getDepartment() {
  98 + return department;
  99 + }
  100 +
  101 + public void setDepartment(String department) {
  102 + this.department = department;
  103 + }
  104 +
  105 + public String getSUserType() {
  106 + return sUserType;
  107 + }
  108 +
  109 + public void setSUserType(String sUserType) {
  110 + this.sUserType = sUserType;
  111 + }
  112 +
  113 + public String getSLanguage() {
  114 + return sLanguage;
  115 + }
  116 +
  117 + public void setSLanguage(String sLanguage) {
  118 + this.sLanguage = sLanguage;
  119 + }
  120 +
  121 + public Integer getIIsVoid() {
  122 + return iIsVoid;
  123 + }
  124 +
  125 + public void setIIsVoid(Integer iIsVoid) {
  126 + this.iIsVoid = iIsVoid;
  127 + }
  128 +
  129 + public LocalDateTime getTLastLoginDate() {
  130 + return tLastLoginDate;
  131 + }
  132 +
  133 + public void setTLastLoginDate(LocalDateTime tLastLoginDate) {
  134 + this.tLastLoginDate = tLastLoginDate;
  135 + }
  136 +
  137 + public String getSCreator() {
  138 + return sCreator;
  139 + }
  140 +
  141 + public void setSCreator(String sCreator) {
  142 + this.sCreator = sCreator;
  143 + }
  144 +
  145 + public LocalDateTime getTCreateDate() {
  146 + return tCreateDate;
  147 + }
  148 +
  149 + public void setTCreateDate(LocalDateTime tCreateDate) {
  150 + this.tCreateDate = tCreateDate;
  151 + }
  152 +}
backend/src/test/java/com/xly/erp/modules/usr/dto/UserQueryDTOValidationTest.java 0 → 100644
  1 +package com.xly.erp.modules.usr.dto;
  2 +
  3 +import static org.assertj.core.api.Assertions.assertThat;
  4 +
  5 +import jakarta.validation.ConstraintViolation;
  6 +import jakarta.validation.Validation;
  7 +import jakarta.validation.Validator;
  8 +import jakarta.validation.ValidatorFactory;
  9 +import java.util.Set;
  10 +import org.junit.jupiter.api.AfterAll;
  11 +import org.junit.jupiter.api.BeforeAll;
  12 +import org.junit.jupiter.api.Test;
  13 +
  14 +/**
  15 + * REQ-USR-003 T2:UserQueryDTO Bean Validation 校验。
  16 + *
  17 + * <p>queryField/matchType 走 @Pattern(null 跳过,默认值 Service 兜底);queryValue @Size(max=100);
  18 + * pageNum/pageSize 不加 @Min/@Max(范围在 Service 入口判定 42201,spec § 8 D8)。</p>
  19 + */
  20 +class UserQueryDTOValidationTest {
  21 +
  22 + private static ValidatorFactory factory;
  23 + private static Validator validator;
  24 +
  25 + @BeforeAll
  26 + static void setUp() {
  27 + factory = Validation.buildDefaultValidatorFactory();
  28 + validator = factory.getValidator();
  29 + }
  30 +
  31 + @AfterAll
  32 + static void tearDown() {
  33 + if (factory != null) {
  34 + factory.close();
  35 + }
  36 + }
  37 +
  38 + @Test
  39 + void acceptsAllNullAsValid() {
  40 + UserQueryDTO dto = new UserQueryDTO();
  41 + Set<ConstraintViolation<UserQueryDTO>> violations = validator.validate(dto);
  42 + assertThat(violations).isEmpty();
  43 + }
  44 +
  45 + @Test
  46 + void acceptsLegalEnums() {
  47 + UserQueryDTO dto = new UserQueryDTO();
  48 + dto.setQueryField("登录日期");
  49 + dto.setMatchType("不包含");
  50 + dto.setQueryValue("2026-06-01");
  51 + Set<ConstraintViolation<UserQueryDTO>> violations = validator.validate(dto);
  52 + assertThat(violations).isEmpty();
  53 + }
  54 +
  55 + @Test
  56 + void rejectsIllegalQueryField() {
  57 + UserQueryDTO dto = new UserQueryDTO();
  58 + dto.setQueryField("身份证");
  59 + Set<ConstraintViolation<UserQueryDTO>> violations = validator.validate(dto);
  60 + assertThat(violations)
  61 + .anyMatch(v -> v.getPropertyPath().toString().equals("queryField"));
  62 + }
  63 +
  64 + @Test
  65 + void rejectsIllegalMatchType() {
  66 + UserQueryDTO dto = new UserQueryDTO();
  67 + dto.setMatchType("大于");
  68 + Set<ConstraintViolation<UserQueryDTO>> violations = validator.validate(dto);
  69 + assertThat(violations)
  70 + .anyMatch(v -> v.getPropertyPath().toString().equals("matchType"));
  71 + }
  72 +
  73 + @Test
  74 + void rejectsTooLongQueryValue() {
  75 + UserQueryDTO dto = new UserQueryDTO();
  76 + dto.setQueryValue("x".repeat(101));
  77 + Set<ConstraintViolation<UserQueryDTO>> violations = validator.validate(dto);
  78 + assertThat(violations)
  79 + .anyMatch(v -> v.getPropertyPath().toString().equals("queryValue"));
  80 + }
  81 +}
backend/src/test/java/com/xly/erp/modules/usr/vo/UserVOJsonTest.java 0 → 100644
  1 +package com.xly.erp.modules.usr.vo;
  2 +
  3 +import static org.assertj.core.api.Assertions.assertThat;
  4 +
  5 +import com.fasterxml.jackson.databind.ObjectMapper;
  6 +import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
  7 +import java.time.LocalDateTime;
  8 +import org.junit.jupiter.api.Test;
  9 +
  10 +/**
  11 + * REQ-USR-003 T2:UserVO 序列化契约键名 + 不含密码 / 租户列。
  12 + *
  13 + * <p>带匈牙利前缀字段加 @JsonProperty 锁键为小驼峰(sUserName 等);
  14 + * employeeName/department/id/日期为普通驼峰;严格无 sPassword/password/租户列。</p>
  15 + */
  16 +class UserVOJsonTest {
  17 +
  18 + // 注册 JavaTimeModule 模拟 Spring Boot 自动配置的 ObjectMapper(支持 LocalDateTime 序列化)。
  19 + private final ObjectMapper objectMapper = new ObjectMapper().registerModule(new JavaTimeModule());
  20 +
  21 + @Test
  22 + void serializesContractKeysNoPassword() throws Exception {
  23 + UserVO vo = new UserVO();
  24 + vo.setId(7);
  25 + vo.setSUserName("alice");
  26 + vo.setEmployeeName("爱丽丝");
  27 + vo.setSUserNo("U007");
  28 + vo.setDepartment("财务部");
  29 + vo.setSUserType("超级管理员");
  30 + vo.setSLanguage("中文");
  31 + vo.setIIsVoid(0);
  32 + vo.setTLastLoginDate(LocalDateTime.of(2026, 6, 1, 12, 0, 0));
  33 + vo.setSCreator("admin");
  34 + vo.setTCreateDate(LocalDateTime.of(2026, 5, 1, 9, 0, 0));
  35 +
  36 + String json = objectMapper.writeValueAsString(vo);
  37 +
  38 + assertThat(json)
  39 + .contains("\"id\"")
  40 + .contains("\"sUserName\"")
  41 + .contains("\"employeeName\"")
  42 + .contains("\"sUserNo\"")
  43 + .contains("\"department\"")
  44 + .contains("\"sUserType\"")
  45 + .contains("\"sLanguage\"")
  46 + .contains("\"iIsVoid\"")
  47 + .contains("\"tLastLoginDate\"")
  48 + .contains("\"sCreator\"")
  49 + .contains("\"tCreateDate\"");
  50 + // 匈牙利前缀字段 @JsonProperty 锁键生效:不得出现 Jackson 默认推断的大写首字母键名。
  51 + assertThat(json).doesNotContain("\"SUserName\"");
  52 + // 严格不含密码 / 租户列。
  53 + assertThat(json).doesNotContain("sPassword").doesNotContain("password");
  54 + assertThat(json).doesNotContain("sBrandsId").doesNotContain("sSubsidiaryId");
  55 + }
  56 +}