Commit bc40a751e8d80607ed1123b41c838d3ce377cd69

Authored by zichun
1 parent 822b3bbf

feat(usr): CreateUserReq/Vo + Jackson 严格反序列化 REQ-USR-002

backend/src/main/java/com/xly/erp/common/exception/GlobalExceptionHandler.java
... ... @@ -5,6 +5,7 @@ import com.xly.erp.common.response.Result;
5 5 import jakarta.validation.ConstraintViolationException;
6 6 import lombok.extern.slf4j.Slf4j;
7 7 import org.springframework.http.ResponseEntity;
  8 +import org.springframework.http.converter.HttpMessageNotReadableException;
8 9 import org.springframework.web.bind.MethodArgumentNotValidException;
9 10 import org.springframework.web.bind.annotation.ExceptionHandler;
10 11 import org.springframework.web.bind.annotation.RestControllerAdvice;
... ... @@ -47,6 +48,14 @@ public class GlobalExceptionHandler {
47 48 .body(Result.fail(ErrorCode.BAD_REQUEST, e.getMessage()));
48 49 }
49 50  
  51 + @ExceptionHandler(HttpMessageNotReadableException.class)
  52 + public ResponseEntity<Result<Void>> handleNotReadable(HttpMessageNotReadableException e) {
  53 + log.warn("[HttpMessageNotReadable] {}", e.getMessage());
  54 + return ResponseEntity
  55 + .status(400)
  56 + .body(Result.fail(ErrorCode.BAD_REQUEST, "请求体格式不合法或包含未知字段"));
  57 + }
  58 +
50 59 @ExceptionHandler(Exception.class)
51 60 public ResponseEntity<Result<Void>> handleFallback(Exception e) {
52 61 log.error("[Unhandled] {}", e.getMessage(), e);
... ...
backend/src/main/java/com/xly/erp/module/usr/dto/CreateUserReq.java 0 → 100644
  1 +package com.xly.erp.module.usr.dto;
  2 +
  3 +import jakarta.validation.constraints.NotBlank;
  4 +import jakarta.validation.constraints.NotNull;
  5 +import jakarta.validation.constraints.Pattern;
  6 +import jakarta.validation.constraints.Size;
  7 +import lombok.Data;
  8 +
  9 +import java.util.List;
  10 +
  11 +@Data
  12 +public class CreateUserReq {
  13 +
  14 + @NotBlank
  15 + @Pattern(regexp = "^[A-Za-z0-9_]{3,20}$",
  16 + message = "用户名必须为 3-20 位字母数字下划线")
  17 + private String username;
  18 +
  19 + @NotBlank
  20 + @Size(max = 50)
  21 + private String userCode;
  22 +
  23 + @NotBlank
  24 + @Pattern(regexp = "NORMAL|SUPER_ADMIN",
  25 + message = "userType 必须为 NORMAL 或 SUPER_ADMIN")
  26 + private String userType;
  27 +
  28 + @NotBlank
  29 + @Pattern(regexp = "zh-CN|en-US|zh-TW",
  30 + message = "language 必须为 zh-CN / en-US / zh-TW")
  31 + private String language;
  32 +
  33 + @NotNull
  34 + private Boolean canEditDocument;
  35 +
  36 + /** 可选;非空则必须命中 sys_employee.iIncrement 且 iIsDeleted=0 */
  37 + private Integer employeeId;
  38 +
  39 + /** 可选;空数组 / null 都允许;非空时每个 ID 必须命中 sys_permission_category */
  40 + private List<Integer> permissionCategoryIds;
  41 +}
... ...
backend/src/main/java/com/xly/erp/module/usr/vo/CreateUserVo.java 0 → 100644
  1 +package com.xly.erp.module.usr.vo;
  2 +
  3 +import lombok.Builder;
  4 +import lombok.Data;
  5 +
  6 +@Data
  7 +@Builder
  8 +public class CreateUserVo {
  9 + private Integer userId;
  10 + private String username;
  11 + private String userCode;
  12 +}
... ...
backend/src/main/resources/application-test.yml
1 1 spring:
  2 + jackson:
  3 + deserialization:
  4 + fail-on-unknown-properties: true
2 5 datasource:
3 6 url: jdbc:mysql://${DB_HOST}:${DB_PORT}/${DB_SCHEMA}?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai&useSSL=false&allowPublicKeyRetrieval=true
4 7 username: ${DB_USER}
... ...
backend/src/main/resources/application.yml
... ... @@ -4,6 +4,9 @@ server:
4 4 spring:
5 5 application:
6 6 name: xly-erp-backend
  7 + jackson:
  8 + deserialization:
  9 + fail-on-unknown-properties: true
7 10 datasource:
8 11 driver-class-name: com.mysql.cj.jdbc.Driver
9 12 url: jdbc:mysql://${DB_HOST}:${DB_PORT}/${DB_SCHEMA}?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai&useSSL=false&allowPublicKeyRetrieval=true
... ...
backend/src/test/java/com/xly/erp/module/usr/dto/CreateUserReqValidationTest.java 0 → 100644
  1 +package com.xly.erp.module.usr.dto;
  2 +
  3 +import jakarta.validation.ConstraintViolation;
  4 +import jakarta.validation.Validation;
  5 +import jakarta.validation.Validator;
  6 +import org.junit.jupiter.api.Test;
  7 +
  8 +import java.util.List;
  9 +import java.util.Set;
  10 +
  11 +import static org.junit.jupiter.api.Assertions.*;
  12 +
  13 +class CreateUserReqValidationTest {
  14 +
  15 + private static final Validator VALIDATOR =
  16 + Validation.buildDefaultValidatorFactory().getValidator();
  17 +
  18 + private CreateUserReq build() {
  19 + CreateUserReq r = new CreateUserReq();
  20 + r.setUsername("alice2");
  21 + r.setUserCode("U010");
  22 + r.setUserType("NORMAL");
  23 + r.setLanguage("zh-CN");
  24 + r.setCanEditDocument(false);
  25 + return r;
  26 + }
  27 +
  28 + @Test
  29 + void allRequired_passes() {
  30 + Set<ConstraintViolation<CreateUserReq>> v = VALIDATOR.validate(build());
  31 + assertTrue(v.isEmpty());
  32 + }
  33 +
  34 + @Test
  35 + void blankUsername_fails() {
  36 + CreateUserReq r = build();
  37 + r.setUsername("");
  38 + assertFalse(VALIDATOR.validate(r).isEmpty());
  39 + }
  40 +
  41 + @Test
  42 + void usernameTooShort_fails() {
  43 + CreateUserReq r = build();
  44 + r.setUsername("ab");
  45 + assertFalse(VALIDATOR.validate(r).isEmpty());
  46 + }
  47 +
  48 + @Test
  49 + void usernameWithIllegalChar_fails() {
  50 + CreateUserReq r = build();
  51 + r.setUsername("al ice");
  52 + assertFalse(VALIDATOR.validate(r).isEmpty());
  53 + }
  54 +
  55 + @Test
  56 + void userCodeTooLong_fails() {
  57 + CreateUserReq r = build();
  58 + r.setUserCode("X".repeat(51));
  59 + assertFalse(VALIDATOR.validate(r).isEmpty());
  60 + }
  61 +
  62 + @Test
  63 + void userTypeNotEnum_fails() {
  64 + CreateUserReq r = build();
  65 + r.setUserType("ROOT");
  66 + assertFalse(VALIDATOR.validate(r).isEmpty());
  67 + }
  68 +
  69 + @Test
  70 + void languageNotEnum_fails() {
  71 + CreateUserReq r = build();
  72 + r.setLanguage("ja-JP");
  73 + assertFalse(VALIDATOR.validate(r).isEmpty());
  74 + }
  75 +
  76 + @Test
  77 + void canEditDocumentMissing_fails() {
  78 + CreateUserReq r = build();
  79 + r.setCanEditDocument(null);
  80 + assertFalse(VALIDATOR.validate(r).isEmpty());
  81 + }
  82 +
  83 + @Test
  84 + void employeeIdNull_isAllowed() {
  85 + CreateUserReq r = build();
  86 + r.setEmployeeId(null);
  87 + assertTrue(VALIDATOR.validate(r).isEmpty());
  88 + }
  89 +
  90 + @Test
  91 + void permissionCategoryIdsEmpty_isAllowed() {
  92 + CreateUserReq r = build();
  93 + r.setPermissionCategoryIds(List.of());
  94 + assertTrue(VALIDATOR.validate(r).isEmpty());
  95 + }
  96 +}
... ...