Commit eb40632c250cba2c84bd145d765ad8ee1ae63e44

Authored by zichun
1 parent 7b98b08b

feat(usr): 统一响应体/错误码枚举/全局异常处理 REQ-USR-001

backend/src/main/java/com/xly/erp/common/exception/BusinessException.java 0 → 100644
  1 +package com.xly.erp.common.exception;
  2 +
  3 +import com.xly.erp.common.response.ResultCode;
  4 +
  5 +/**
  6 + * 业务异常(docs/04 § 1.5 SSoT)。
  7 + *
  8 + * <p>REQ-USR-001 T2。业务错误统一抛本异常,由 {@link GlobalExceptionHandler} 捕获转 Result。</p>
  9 + */
  10 +public class BusinessException extends RuntimeException {
  11 +
  12 + private static final long serialVersionUID = 1L;
  13 +
  14 + private final ResultCode resultCode;
  15 +
  16 + public BusinessException(ResultCode resultCode) {
  17 + super(resultCode.getMessage());
  18 + this.resultCode = resultCode;
  19 + }
  20 +
  21 + public BusinessException(ResultCode resultCode, String message) {
  22 + super(message);
  23 + this.resultCode = resultCode;
  24 + }
  25 +
  26 + public ResultCode getResultCode() {
  27 + return resultCode;
  28 + }
  29 +}
... ...
backend/src/main/java/com/xly/erp/common/exception/GlobalExceptionHandler.java 0 → 100644
  1 +package com.xly.erp.common.exception;
  2 +
  3 +import com.xly.erp.common.response.Result;
  4 +import com.xly.erp.common.response.ResultCode;
  5 +import org.slf4j.Logger;
  6 +import org.slf4j.LoggerFactory;
  7 +import org.springframework.dao.DuplicateKeyException;
  8 +import org.springframework.validation.FieldError;
  9 +import org.springframework.web.bind.MethodArgumentNotValidException;
  10 +import org.springframework.web.bind.annotation.ExceptionHandler;
  11 +import org.springframework.web.bind.annotation.RestControllerAdvice;
  12 +
  13 +/**
  14 + * 全局异常处理(docs/04 § 1.5 SSoT)。
  15 + *
  16 + * <p>REQ-USR-001 T2:把 BusinessException / 参数校验失败 / 唯一键冲突 / 未捕获异常
  17 + * 统一转为 {@link Result},失败响应不抛栈到前端。</p>
  18 + */
  19 +@RestControllerAdvice
  20 +public class GlobalExceptionHandler {
  21 +
  22 + private static final Logger LOG = LoggerFactory.getLogger(GlobalExceptionHandler.class);
  23 +
  24 + /**
  25 + * 业务异常 → 对应错误码。
  26 + */
  27 + @ExceptionHandler(BusinessException.class)
  28 + public <T> Result<T> handleBusinessException(BusinessException ex) {
  29 + return Result.fail(ex.getResultCode(), ex.getMessage());
  30 + }
  31 +
  32 + /**
  33 + * Bean Validation(@Valid)失败 → 40001,message 取首个字段错误提示。
  34 + */
  35 + @ExceptionHandler(MethodArgumentNotValidException.class)
  36 + public <T> Result<T> handleValidationException(MethodArgumentNotValidException ex) {
  37 + FieldError fieldError = ex.getBindingResult().getFieldError();
  38 + String message = fieldError != null
  39 + ? fieldError.getField() + ": " + fieldError.getDefaultMessage()
  40 + : ResultCode.PARAM_INVALID.getMessage();
  41 + return Result.fail(ResultCode.PARAM_INVALID, message);
  42 + }
  43 +
  44 + /**
  45 + * 唯一键冲突(并发兜底)→ 40901 用户名已存在。
  46 + */
  47 + @ExceptionHandler(DuplicateKeyException.class)
  48 + public <T> Result<T> handleDuplicateKeyException(DuplicateKeyException ex) {
  49 + return Result.fail(ResultCode.USERNAME_EXISTS, ResultCode.USERNAME_EXISTS.getMessage());
  50 + }
  51 +
  52 + /**
  53 + * 兜底:未捕获异常记录 ERROR 日志(含栈),对外只返回通用错误码,不泄露内部细节。
  54 + */
  55 + @ExceptionHandler(Exception.class)
  56 + public <T> Result<T> handleException(Exception ex) {
  57 + LOG.error("系统异常", ex);
  58 + return Result.fail(ResultCode.SYSTEM_ERROR, ResultCode.SYSTEM_ERROR.getMessage());
  59 + }
  60 +}
... ...
backend/src/main/java/com/xly/erp/common/response/Result.java 0 → 100644
  1 +package com.xly.erp.common.response;
  2 +
  3 +import java.io.Serializable;
  4 +
  5 +/**
  6 + * 统一响应体(docs/04 § 1.4 SSoT)。
  7 + *
  8 + * <p>REQ-USR-001 T2。所有接口返回 {@code Result<T>}:{@code code=0} 成功,非 0 为错误码。</p>
  9 + *
  10 + * @param <T> 业务数据类型
  11 + */
  12 +public class Result<T> implements Serializable {
  13 +
  14 + private static final long serialVersionUID = 1L;
  15 +
  16 + private int code;
  17 + private String message;
  18 + private T data;
  19 +
  20 + public Result() {
  21 + }
  22 +
  23 + public Result(int code, String message, T data) {
  24 + this.code = code;
  25 + this.message = message;
  26 + this.data = data;
  27 + }
  28 +
  29 + /**
  30 + * 成功且携带数据。
  31 + */
  32 + public static <T> Result<T> success(T data) {
  33 + return new Result<>(ResultCode.SUCCESS.getCode(), ResultCode.SUCCESS.getMessage(), data);
  34 + }
  35 +
  36 + /**
  37 + * 成功且不携带数据。
  38 + */
  39 + public static <T> Result<T> success() {
  40 + return new Result<>(ResultCode.SUCCESS.getCode(), ResultCode.SUCCESS.getMessage(), null);
  41 + }
  42 +
  43 + /**
  44 + * 失败,携带错误码与可读提示。
  45 + */
  46 + public static <T> Result<T> fail(ResultCode code, String message) {
  47 + return new Result<>(code.getCode(), message, null);
  48 + }
  49 +
  50 + public int getCode() {
  51 + return code;
  52 + }
  53 +
  54 + public void setCode(int code) {
  55 + this.code = code;
  56 + }
  57 +
  58 + public String getMessage() {
  59 + return message;
  60 + }
  61 +
  62 + public void setMessage(String message) {
  63 + this.message = message;
  64 + }
  65 +
  66 + public T getData() {
  67 + return data;
  68 + }
  69 +
  70 + public void setData(T data) {
  71 + this.data = data;
  72 + }
  73 +}
... ...
backend/src/main/java/com/xly/erp/common/response/ResultCode.java 0 → 100644
  1 +package com.xly.erp.common.response;
  2 +
  3 +/**
  4 + * 统一错误码枚举(docs/05 / spec § 6 SSoT)。
  5 + *
  6 + * <p>REQ-USR-001 T2:一次性建好本枚举,含本 REQ 使用的 0/40001/40301/40901,
  7 + * 并预留后续 REQ 复用的 40101/40302/40401/42201,避免后续重复修改公共文件。</p>
  8 + */
  9 +public enum ResultCode {
  10 +
  11 + /** 成功。 */
  12 + SUCCESS(0, "success"),
  13 + /** 参数校验失败(字段格式 / 必填 / 枚举越界 / 关联 id 不存在)。 */
  14 + PARAM_INVALID(40001, "参数校验失败"),
  15 + /** 认证失败(用户名或密码错误;预留 REQ-USR-004)。 */
  16 + UNAUTHORIZED(40101, "认证失败"),
  17 + /** 无权限(非管理员调用)。 */
  18 + FORBIDDEN(40301, "无权限"),
  19 + /** 账号已禁用(预留 REQ-USR-004)。 */
  20 + ACCOUNT_DISABLED(40302, "账号已禁用"),
  21 + /** 资源不存在(预留 REQ-USR-002)。 */
  22 + NOT_FOUND(40401, "资源不存在"),
  23 + /** 用户名已存在(sUserName 全局唯一冲突)。 */
  24 + USERNAME_EXISTS(40901, "用户名已存在"),
  25 + /** 分页参数非法(预留 REQ-USR-003)。 */
  26 + PAGE_PARAM_INVALID(42201, "分页参数非法"),
  27 + /** 系统内部错误(兜底)。 */
  28 + SYSTEM_ERROR(50000, "系统繁忙,请稍后重试");
  29 +
  30 + private final int code;
  31 + private final String message;
  32 +
  33 + ResultCode(int code, String message) {
  34 + this.code = code;
  35 + this.message = message;
  36 + }
  37 +
  38 + public int getCode() {
  39 + return code;
  40 + }
  41 +
  42 + public String getMessage() {
  43 + return message;
  44 + }
  45 +}
... ...
backend/src/test/java/com/xly/erp/common/exception/GlobalExceptionHandlerTest.java 0 → 100644
  1 +package com.xly.erp.common.exception;
  2 +
  3 +import static org.assertj.core.api.Assertions.assertThat;
  4 +
  5 +import com.xly.erp.common.response.Result;
  6 +import com.xly.erp.common.response.ResultCode;
  7 +import org.junit.jupiter.api.Test;
  8 +
  9 +/**
  10 + * REQ-USR-001 T2:全局异常处理器把 BusinessException 转为统一 Result。
  11 + */
  12 +class GlobalExceptionHandlerTest {
  13 +
  14 + private final GlobalExceptionHandler handler = new GlobalExceptionHandler();
  15 +
  16 + @Test
  17 + void businessExceptionMapsToResult() {
  18 + BusinessException ex = new BusinessException(ResultCode.USERNAME_EXISTS, "用户名已存在");
  19 + Result<Void> r = handler.handleBusinessException(ex);
  20 + assertThat(r.getCode()).isEqualTo(40901);
  21 + assertThat(r.getMessage()).isEqualTo("用户名已存在");
  22 + }
  23 +}
... ...
backend/src/test/java/com/xly/erp/common/response/ResultTest.java 0 → 100644
  1 +package com.xly.erp.common.response;
  2 +
  3 +import static org.assertj.core.api.Assertions.assertThat;
  4 +
  5 +import org.junit.jupiter.api.Test;
  6 +
  7 +/**
  8 + * REQ-USR-001 T2:统一响应体 Result 行为校验。
  9 + */
  10 +class ResultTest {
  11 +
  12 + @Test
  13 + void successCarriesCodeZeroAndData() {
  14 + Result<String> r = Result.success("hello");
  15 + assertThat(r.getCode()).isEqualTo(0);
  16 + assertThat(r.getData()).isEqualTo("hello");
  17 + }
  18 +
  19 + @Test
  20 + void failCarriesBusinessCodeAndMessage() {
  21 + Result<Void> r = Result.fail(ResultCode.USERNAME_EXISTS, "用户名已存在");
  22 + assertThat(r.getCode()).isEqualTo(40901);
  23 + assertThat(r.getMessage()).isEqualTo("用户名已存在");
  24 + assertThat(r.getData()).isNull();
  25 + }
  26 +}
... ...