Commit 1cd7a10c9fcf26e390a20637d3be3c3e14a2430c
1 parent
975afd36
feat(usr): 登录入参 LoginDTO 与公司下拉项 CompanyOptionVO REQ-USR-004
Showing
4 changed files
with
234 additions
and
0 deletions
backend/src/main/java/com/xly/erp/modules/usr/dto/LoginDTO.java
0 → 100644
| 1 | +package com.xly.erp.modules.usr.dto; | |
| 2 | + | |
| 3 | +import com.fasterxml.jackson.annotation.JsonProperty; | |
| 4 | +import jakarta.validation.constraints.NotBlank; | |
| 5 | +import jakarta.validation.constraints.NotNull; | |
| 6 | +import jakarta.validation.constraints.Size; | |
| 7 | + | |
| 8 | +/** | |
| 9 | + * 登录入参(docs/05 契约 / spec § 2.1)。REQ-USR-004 T2。 | |
| 10 | + * | |
| 11 | + * <p>三字段均必填,校验失败由全局处理器统一转 40001。{@code sUserName} 匈牙利前缀字段的 | |
| 12 | + * getter({@code getSUserName})会被 Jackson 推断为属性名 {@code SUserName},与契约 JSON | |
| 13 | + * 键不符,故加 {@link JsonProperty} 锁定键名(与 CreateUserDTO/UserVO 同做法); | |
| 14 | + * {@code password}/{@code companyId} 为常规小驼峰,无需注解。</p> | |
| 15 | + * | |
| 16 | + * <p>{@code password} 为登录明文,仅供服务端 BCrypt 比对,绝不落日志 / 回显。</p> | |
| 17 | + */ | |
| 18 | +public class LoginDTO { | |
| 19 | + | |
| 20 | + /** 登录用户名:必填,最长 50。 */ | |
| 21 | + @JsonProperty("sUserName") | |
| 22 | + @NotBlank(message = "用户名不能为空") | |
| 23 | + @Size(max = 50, message = "用户名长度不能超过 50") | |
| 24 | + private String sUserName; | |
| 25 | + | |
| 26 | + /** 登录密码明文:必填,最长 100;服务端 BCrypt 比对,绝不落日志。 */ | |
| 27 | + @NotBlank(message = "密码不能为空") | |
| 28 | + @Size(max = 100, message = "密码长度不能超过 100") | |
| 29 | + private String password; | |
| 30 | + | |
| 31 | + /** 版本下拉选中的 usr_company.iIncrement:必填,仅存在性校验,不参与认证绑定。 */ | |
| 32 | + @NotNull(message = "版本不能为空") | |
| 33 | + private Integer companyId; | |
| 34 | + | |
| 35 | + public String getSUserName() { | |
| 36 | + return sUserName; | |
| 37 | + } | |
| 38 | + | |
| 39 | + public void setSUserName(String sUserName) { | |
| 40 | + this.sUserName = sUserName; | |
| 41 | + } | |
| 42 | + | |
| 43 | + public String getPassword() { | |
| 44 | + return password; | |
| 45 | + } | |
| 46 | + | |
| 47 | + public void setPassword(String password) { | |
| 48 | + this.password = password; | |
| 49 | + } | |
| 50 | + | |
| 51 | + public Integer getCompanyId() { | |
| 52 | + return companyId; | |
| 53 | + } | |
| 54 | + | |
| 55 | + public void setCompanyId(Integer companyId) { | |
| 56 | + this.companyId = companyId; | |
| 57 | + } | |
| 58 | +} | ... | ... |
backend/src/main/java/com/xly/erp/modules/usr/vo/CompanyOptionVO.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 | + | |
| 6 | +/** | |
| 7 | + * 公司下拉项输出(spec § 2.3)。REQ-USR-004 T2。 | |
| 8 | + * | |
| 9 | + * <p>供登录页「版本」下拉展示。匈牙利前缀字段({@code sCompanyName}/{@code sVersion})的 | |
| 10 | + * getter 会被 Jackson 推断为大驼峰键,与契约不符,故加 {@link JsonProperty} 锁键; | |
| 11 | + * {@code id} 为常规小驼峰,无需注解。{@code sVersion} 可为 null。</p> | |
| 12 | + */ | |
| 13 | +public class CompanyOptionVO implements Serializable { | |
| 14 | + | |
| 15 | + private static final long serialVersionUID = 1L; | |
| 16 | + | |
| 17 | + /** 公司主键 iIncrement。 */ | |
| 18 | + private Integer id; | |
| 19 | + | |
| 20 | + /** 公司名称(usr_company.sCompanyName)。 */ | |
| 21 | + @JsonProperty("sCompanyName") | |
| 22 | + private String sCompanyName; | |
| 23 | + | |
| 24 | + /** 版本 / 账套标识(usr_company.sVersion,可 null)。 */ | |
| 25 | + @JsonProperty("sVersion") | |
| 26 | + private String sVersion; | |
| 27 | + | |
| 28 | + public Integer getId() { | |
| 29 | + return id; | |
| 30 | + } | |
| 31 | + | |
| 32 | + public void setId(Integer id) { | |
| 33 | + this.id = id; | |
| 34 | + } | |
| 35 | + | |
| 36 | + public String getSCompanyName() { | |
| 37 | + return sCompanyName; | |
| 38 | + } | |
| 39 | + | |
| 40 | + public void setSCompanyName(String sCompanyName) { | |
| 41 | + this.sCompanyName = sCompanyName; | |
| 42 | + } | |
| 43 | + | |
| 44 | + public String getSVersion() { | |
| 45 | + return sVersion; | |
| 46 | + } | |
| 47 | + | |
| 48 | + public void setSVersion(String sVersion) { | |
| 49 | + this.sVersion = sVersion; | |
| 50 | + } | |
| 51 | +} | ... | ... |
backend/src/test/java/com/xly/erp/modules/usr/dto/LoginDTOValidationTest.java
0 → 100644
| 1 | +package com.xly.erp.modules.usr.dto; | |
| 2 | + | |
| 3 | +import static org.assertj.core.api.Assertions.assertThat; | |
| 4 | + | |
| 5 | +import com.fasterxml.jackson.databind.ObjectMapper; | |
| 6 | +import jakarta.validation.ConstraintViolation; | |
| 7 | +import jakarta.validation.Validation; | |
| 8 | +import jakarta.validation.Validator; | |
| 9 | +import jakarta.validation.ValidatorFactory; | |
| 10 | +import java.util.Set; | |
| 11 | +import org.junit.jupiter.api.AfterAll; | |
| 12 | +import org.junit.jupiter.api.BeforeAll; | |
| 13 | +import org.junit.jupiter.api.Test; | |
| 14 | + | |
| 15 | +/** | |
| 16 | + * REQ-USR-004 T2:LoginDTO Bean Validation 与 JSON 反序列化键名。 | |
| 17 | + * | |
| 18 | + * <p>校验 {@code sUserName}/{@code password} 必填、长度上限,{@code companyId} 必填; | |
| 19 | + * 并验证 {@code @JsonProperty("sUserName")} 锁定反序列化键名(与 CreateUserDTO/UserVO 同做法)。</p> | |
| 20 | + */ | |
| 21 | +class LoginDTOValidationTest { | |
| 22 | + | |
| 23 | + private static ValidatorFactory factory; | |
| 24 | + private static Validator validator; | |
| 25 | + private final ObjectMapper objectMapper = new ObjectMapper(); | |
| 26 | + | |
| 27 | + @BeforeAll | |
| 28 | + static void initValidator() { | |
| 29 | + factory = Validation.buildDefaultValidatorFactory(); | |
| 30 | + validator = factory.getValidator(); | |
| 31 | + } | |
| 32 | + | |
| 33 | + @AfterAll | |
| 34 | + static void closeValidator() { | |
| 35 | + factory.close(); | |
| 36 | + } | |
| 37 | + | |
| 38 | + private LoginDTO valid() { | |
| 39 | + LoginDTO dto = new LoginDTO(); | |
| 40 | + dto.setSUserName("admin"); | |
| 41 | + dto.setPassword("666666"); | |
| 42 | + dto.setCompanyId(1); | |
| 43 | + return dto; | |
| 44 | + } | |
| 45 | + | |
| 46 | + @Test | |
| 47 | + void acceptsValidLogin() { | |
| 48 | + Set<ConstraintViolation<LoginDTO>> violations = validator.validate(valid()); | |
| 49 | + assertThat(violations).isEmpty(); | |
| 50 | + } | |
| 51 | + | |
| 52 | + @Test | |
| 53 | + void rejectsBlankUserName() { | |
| 54 | + LoginDTO dto = valid(); | |
| 55 | + dto.setSUserName(""); | |
| 56 | + assertThat(validator.validate(dto)).isNotEmpty(); | |
| 57 | + dto.setSUserName(null); | |
| 58 | + assertThat(validator.validate(dto)).isNotEmpty(); | |
| 59 | + } | |
| 60 | + | |
| 61 | + @Test | |
| 62 | + void rejectsBlankPassword() { | |
| 63 | + LoginDTO dto = valid(); | |
| 64 | + dto.setPassword(""); | |
| 65 | + assertThat(validator.validate(dto)).isNotEmpty(); | |
| 66 | + dto.setPassword(null); | |
| 67 | + assertThat(validator.validate(dto)).isNotEmpty(); | |
| 68 | + } | |
| 69 | + | |
| 70 | + @Test | |
| 71 | + void rejectsNullCompanyId() { | |
| 72 | + LoginDTO dto = valid(); | |
| 73 | + dto.setCompanyId(null); | |
| 74 | + assertThat(validator.validate(dto)).isNotEmpty(); | |
| 75 | + } | |
| 76 | + | |
| 77 | + @Test | |
| 78 | + void rejectsTooLongUserName() { | |
| 79 | + LoginDTO dto = valid(); | |
| 80 | + dto.setSUserName("a".repeat(51)); | |
| 81 | + assertThat(validator.validate(dto)).isNotEmpty(); | |
| 82 | + } | |
| 83 | + | |
| 84 | + @Test | |
| 85 | + void deserializesSUserNameJsonKey() throws Exception { | |
| 86 | + LoginDTO dto = objectMapper.readValue( | |
| 87 | + "{\"sUserName\":\"admin\",\"password\":\"x\",\"companyId\":1}", LoginDTO.class); | |
| 88 | + assertThat(dto.getSUserName()).isEqualTo("admin"); | |
| 89 | + assertThat(dto.getPassword()).isEqualTo("x"); | |
| 90 | + assertThat(dto.getCompanyId()).isEqualTo(1); | |
| 91 | + } | |
| 92 | +} | ... | ... |
backend/src/test/java/com/xly/erp/modules/usr/vo/CompanyOptionVOJsonTest.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 org.junit.jupiter.api.Test; | |
| 7 | + | |
| 8 | +/** | |
| 9 | + * REQ-USR-004 T2:CompanyOptionVO 序列化键名锁定。 | |
| 10 | + * | |
| 11 | + * <p>验证匈牙利前缀字段({@code sCompanyName}/{@code sVersion})经 {@code @JsonProperty} | |
| 12 | + * 序列化为契约小驼峰键,而非 Jackson 推断的大驼峰 {@code SCompanyName}。</p> | |
| 13 | + */ | |
| 14 | +class CompanyOptionVOJsonTest { | |
| 15 | + | |
| 16 | + private final ObjectMapper objectMapper = new ObjectMapper(); | |
| 17 | + | |
| 18 | + @Test | |
| 19 | + void serializesContractKeys() throws Exception { | |
| 20 | + CompanyOptionVO vo = new CompanyOptionVO(); | |
| 21 | + vo.setId(7); | |
| 22 | + vo.setSCompanyName("小羚羊总部"); | |
| 23 | + vo.setSVersion("企业版"); | |
| 24 | + | |
| 25 | + String json = objectMapper.writeValueAsString(vo); | |
| 26 | + | |
| 27 | + assertThat(json).contains("\"id\":7"); | |
| 28 | + assertThat(json).contains("\"sCompanyName\":\"小羚羊总部\""); | |
| 29 | + assertThat(json).contains("\"sVersion\":\"企业版\""); | |
| 30 | + assertThat(json).doesNotContain("SCompanyName"); | |
| 31 | + assertThat(json).doesNotContain("SVersion"); | |
| 32 | + } | |
| 33 | +} | ... | ... |