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 | +} |