From bc40a751e8d80607ed1123b41c838d3ce377cd69 Mon Sep 17 00:00:00 2001 From: zichun Date: Fri, 15 May 2026 09:19:18 +0800 Subject: [PATCH] feat(usr): CreateUserReq/Vo + Jackson 严格反序列化 REQ-USR-002 --- backend/src/main/java/com/xly/erp/common/exception/GlobalExceptionHandler.java | 9 +++++++++ backend/src/main/java/com/xly/erp/module/usr/dto/CreateUserReq.java | 41 +++++++++++++++++++++++++++++++++++++++++ backend/src/main/java/com/xly/erp/module/usr/vo/CreateUserVo.java | 12 ++++++++++++ backend/src/main/resources/application-test.yml | 3 +++ backend/src/main/resources/application.yml | 3 +++ backend/src/test/java/com/xly/erp/module/usr/dto/CreateUserReqValidationTest.java | 96 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 6 files changed, 164 insertions(+), 0 deletions(-) create mode 100644 backend/src/main/java/com/xly/erp/module/usr/dto/CreateUserReq.java create mode 100644 backend/src/main/java/com/xly/erp/module/usr/vo/CreateUserVo.java create mode 100644 backend/src/test/java/com/xly/erp/module/usr/dto/CreateUserReqValidationTest.java diff --git a/backend/src/main/java/com/xly/erp/common/exception/GlobalExceptionHandler.java b/backend/src/main/java/com/xly/erp/common/exception/GlobalExceptionHandler.java index 39902a6..7523765 100644 --- a/backend/src/main/java/com/xly/erp/common/exception/GlobalExceptionHandler.java +++ b/backend/src/main/java/com/xly/erp/common/exception/GlobalExceptionHandler.java @@ -5,6 +5,7 @@ import com.xly.erp.common.response.Result; import jakarta.validation.ConstraintViolationException; import lombok.extern.slf4j.Slf4j; import org.springframework.http.ResponseEntity; +import org.springframework.http.converter.HttpMessageNotReadableException; import org.springframework.web.bind.MethodArgumentNotValidException; import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.RestControllerAdvice; @@ -47,6 +48,14 @@ public class GlobalExceptionHandler { .body(Result.fail(ErrorCode.BAD_REQUEST, e.getMessage())); } + @ExceptionHandler(HttpMessageNotReadableException.class) + public ResponseEntity> handleNotReadable(HttpMessageNotReadableException e) { + log.warn("[HttpMessageNotReadable] {}", e.getMessage()); + return ResponseEntity + .status(400) + .body(Result.fail(ErrorCode.BAD_REQUEST, "请求体格式不合法或包含未知字段")); + } + @ExceptionHandler(Exception.class) public ResponseEntity> handleFallback(Exception e) { log.error("[Unhandled] {}", e.getMessage(), e); diff --git a/backend/src/main/java/com/xly/erp/module/usr/dto/CreateUserReq.java b/backend/src/main/java/com/xly/erp/module/usr/dto/CreateUserReq.java new file mode 100644 index 0000000..6322e88 --- /dev/null +++ b/backend/src/main/java/com/xly/erp/module/usr/dto/CreateUserReq.java @@ -0,0 +1,41 @@ +package com.xly.erp.module.usr.dto; + +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Pattern; +import jakarta.validation.constraints.Size; +import lombok.Data; + +import java.util.List; + +@Data +public class CreateUserReq { + + @NotBlank + @Pattern(regexp = "^[A-Za-z0-9_]{3,20}$", + message = "用户名必须为 3-20 位字母数字下划线") + private String username; + + @NotBlank + @Size(max = 50) + private String userCode; + + @NotBlank + @Pattern(regexp = "NORMAL|SUPER_ADMIN", + message = "userType 必须为 NORMAL 或 SUPER_ADMIN") + private String userType; + + @NotBlank + @Pattern(regexp = "zh-CN|en-US|zh-TW", + message = "language 必须为 zh-CN / en-US / zh-TW") + private String language; + + @NotNull + private Boolean canEditDocument; + + /** 可选;非空则必须命中 sys_employee.iIncrement 且 iIsDeleted=0 */ + private Integer employeeId; + + /** 可选;空数组 / null 都允许;非空时每个 ID 必须命中 sys_permission_category */ + private List permissionCategoryIds; +} diff --git a/backend/src/main/java/com/xly/erp/module/usr/vo/CreateUserVo.java b/backend/src/main/java/com/xly/erp/module/usr/vo/CreateUserVo.java new file mode 100644 index 0000000..2365edc --- /dev/null +++ b/backend/src/main/java/com/xly/erp/module/usr/vo/CreateUserVo.java @@ -0,0 +1,12 @@ +package com.xly.erp.module.usr.vo; + +import lombok.Builder; +import lombok.Data; + +@Data +@Builder +public class CreateUserVo { + private Integer userId; + private String username; + private String userCode; +} diff --git a/backend/src/main/resources/application-test.yml b/backend/src/main/resources/application-test.yml index 69e0d31..7917780 100644 --- a/backend/src/main/resources/application-test.yml +++ b/backend/src/main/resources/application-test.yml @@ -1,4 +1,7 @@ spring: + jackson: + deserialization: + fail-on-unknown-properties: true datasource: url: jdbc:mysql://${DB_HOST}:${DB_PORT}/${DB_SCHEMA}?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai&useSSL=false&allowPublicKeyRetrieval=true username: ${DB_USER} diff --git a/backend/src/main/resources/application.yml b/backend/src/main/resources/application.yml index d08ab3b..1660d18 100644 --- a/backend/src/main/resources/application.yml +++ b/backend/src/main/resources/application.yml @@ -4,6 +4,9 @@ server: spring: application: name: xly-erp-backend + jackson: + deserialization: + fail-on-unknown-properties: true datasource: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://${DB_HOST}:${DB_PORT}/${DB_SCHEMA}?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai&useSSL=false&allowPublicKeyRetrieval=true diff --git a/backend/src/test/java/com/xly/erp/module/usr/dto/CreateUserReqValidationTest.java b/backend/src/test/java/com/xly/erp/module/usr/dto/CreateUserReqValidationTest.java new file mode 100644 index 0000000..7b11be2 --- /dev/null +++ b/backend/src/test/java/com/xly/erp/module/usr/dto/CreateUserReqValidationTest.java @@ -0,0 +1,96 @@ +package com.xly.erp.module.usr.dto; + +import jakarta.validation.ConstraintViolation; +import jakarta.validation.Validation; +import jakarta.validation.Validator; +import org.junit.jupiter.api.Test; + +import java.util.List; +import java.util.Set; + +import static org.junit.jupiter.api.Assertions.*; + +class CreateUserReqValidationTest { + + private static final Validator VALIDATOR = + Validation.buildDefaultValidatorFactory().getValidator(); + + private CreateUserReq build() { + CreateUserReq r = new CreateUserReq(); + r.setUsername("alice2"); + r.setUserCode("U010"); + r.setUserType("NORMAL"); + r.setLanguage("zh-CN"); + r.setCanEditDocument(false); + return r; + } + + @Test + void allRequired_passes() { + Set> v = VALIDATOR.validate(build()); + assertTrue(v.isEmpty()); + } + + @Test + void blankUsername_fails() { + CreateUserReq r = build(); + r.setUsername(""); + assertFalse(VALIDATOR.validate(r).isEmpty()); + } + + @Test + void usernameTooShort_fails() { + CreateUserReq r = build(); + r.setUsername("ab"); + assertFalse(VALIDATOR.validate(r).isEmpty()); + } + + @Test + void usernameWithIllegalChar_fails() { + CreateUserReq r = build(); + r.setUsername("al ice"); + assertFalse(VALIDATOR.validate(r).isEmpty()); + } + + @Test + void userCodeTooLong_fails() { + CreateUserReq r = build(); + r.setUserCode("X".repeat(51)); + assertFalse(VALIDATOR.validate(r).isEmpty()); + } + + @Test + void userTypeNotEnum_fails() { + CreateUserReq r = build(); + r.setUserType("ROOT"); + assertFalse(VALIDATOR.validate(r).isEmpty()); + } + + @Test + void languageNotEnum_fails() { + CreateUserReq r = build(); + r.setLanguage("ja-JP"); + assertFalse(VALIDATOR.validate(r).isEmpty()); + } + + @Test + void canEditDocumentMissing_fails() { + CreateUserReq r = build(); + r.setCanEditDocument(null); + assertFalse(VALIDATOR.validate(r).isEmpty()); + } + + @Test + void employeeIdNull_isAllowed() { + CreateUserReq r = build(); + r.setEmployeeId(null); + assertTrue(VALIDATOR.validate(r).isEmpty()); + } + + @Test + void permissionCategoryIdsEmpty_isAllowed() { + CreateUserReq r = build(); + r.setPermissionCategoryIds(List.of()); + assertTrue(VALIDATOR.validate(r).isEmpty()); + } +} -- libgit2 0.22.2