Commit 8fd249c647e18df0dbf418e31fbc6743015930d8

Authored by zichun
1 parent 0e3b32b0

feat(usr): 全局异常处理器 + BusinessException REQ-USR-001

backend/src/main/java/com/xly/test4/common/exception/BusinessException.java 0 → 100644
  1 +package com.xly.test4.common.exception;
  2 +
  3 +import lombok.Getter;
  4 +
  5 +@Getter
  6 +public class BusinessException extends RuntimeException {
  7 +
  8 + private final int code;
  9 +
  10 + public BusinessException(int code, String message) {
  11 + super(message);
  12 + this.code = code;
  13 + }
  14 +}
backend/src/main/java/com/xly/test4/common/exception/GlobalExceptionHandler.java 0 → 100644
  1 +package com.xly.test4.common.exception;
  2 +
  3 +import com.xly.test4.common.response.Result;
  4 +import com.xly.test4.common.response.ResultCode;
  5 +import lombok.extern.slf4j.Slf4j;
  6 +import org.springframework.dao.DuplicateKeyException;
  7 +import org.springframework.security.access.AccessDeniedException;
  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 +@Slf4j
  14 +@RestControllerAdvice
  15 +public class GlobalExceptionHandler {
  16 +
  17 + @ExceptionHandler(BusinessException.class)
  18 + public Result<Object> handleBusiness(BusinessException e) {
  19 + return Result.fail(e.getCode(), e.getMessage());
  20 + }
  21 +
  22 + @ExceptionHandler(MethodArgumentNotValidException.class)
  23 + public Result<Object> handleValidation(MethodArgumentNotValidException e) {
  24 + FieldError first = e.getBindingResult().getFieldError();
  25 + String message = first != null ? first.getDefaultMessage() : "参数校验失败";
  26 + return Result.fail(ResultCode.PARAM_INVALID, message);
  27 + }
  28 +
  29 + @ExceptionHandler(AccessDeniedException.class)
  30 + public Result<Object> handleAccessDenied(AccessDeniedException e) {
  31 + return Result.fail(ResultCode.FORBIDDEN, "权限不足");
  32 + }
  33 +
  34 + @ExceptionHandler(DuplicateKeyException.class)
  35 + public Result<Object> handleDuplicateKey(DuplicateKeyException e) {
  36 + String causeMsg = e.getMostSpecificCause() != null
  37 + ? String.valueOf(e.getMostSpecificCause().getMessage())
  38 + : "";
  39 + if (causeMsg.contains("uk_tUser_sUserName")) {
  40 + return Result.fail(ResultCode.USER_NAME_DUPLICATE, "用户名已存在");
  41 + }
  42 + if (causeMsg.contains("uk_tUser_sUserCode")) {
  43 + return Result.fail(ResultCode.USER_CODE_DUPLICATE, "用户号已存在");
  44 + }
  45 + log.error("未识别的唯一索引冲突", e);
  46 + return Result.fail(ResultCode.INTERNAL_ERROR, "系统繁忙,请稍后重试");
  47 + }
  48 +
  49 + @ExceptionHandler(Exception.class)
  50 + public Result<Object> handleGeneric(Exception e) {
  51 + log.error("未预期异常", e);
  52 + return Result.fail(ResultCode.INTERNAL_ERROR, "系统繁忙,请稍后重试");
  53 + }
  54 +}
backend/src/test/java/com/xly/test4/common/exception/GlobalExceptionHandlerTest.java 0 → 100644
  1 +package com.xly.test4.common.exception;
  2 +
  3 +import com.xly.test4.common.response.Result;
  4 +import org.junit.jupiter.api.Test;
  5 +import org.springframework.core.MethodParameter;
  6 +import org.springframework.dao.DuplicateKeyException;
  7 +import org.springframework.security.access.AccessDeniedException;
  8 +import org.springframework.validation.BindingResult;
  9 +import org.springframework.validation.FieldError;
  10 +import org.springframework.validation.MapBindingResult;
  11 +import org.springframework.web.bind.MethodArgumentNotValidException;
  12 +
  13 +import java.util.HashMap;
  14 +
  15 +import java.sql.SQLIntegrityConstraintViolationException;
  16 +
  17 +import static org.assertj.core.api.Assertions.assertThat;
  18 +import static org.mockito.Mockito.mock;
  19 +
  20 +class GlobalExceptionHandlerTest {
  21 +
  22 + private final GlobalExceptionHandler handler = new GlobalExceptionHandler();
  23 +
  24 + @Test
  25 + void handleBusinessException_returnsFailResultWithCodeAndMessage() {
  26 + Result<Object> r = handler.handleBusiness(new BusinessException(40002, "用户名已存在"));
  27 +
  28 + assertThat(r.getCode()).isEqualTo(40002);
  29 + assertThat(r.getMessage()).isEqualTo("用户名已存在");
  30 + assertThat(r.getData()).isNull();
  31 + }
  32 +
  33 + @Test
  34 + void handleDuplicateKey_userNameIndex_returns40002() {
  35 + DuplicateKeyException e = new DuplicateKeyException(
  36 + "duplicate",
  37 + new SQLIntegrityConstraintViolationException(
  38 + "Duplicate entry 'foo' for key 'uk_tUser_sUserName'"));
  39 +
  40 + Result<Object> r = handler.handleDuplicateKey(e);
  41 + assertThat(r.getCode()).isEqualTo(40002);
  42 + }
  43 +
  44 + @Test
  45 + void handleDuplicateKey_userCodeIndex_returns40003() {
  46 + DuplicateKeyException e = new DuplicateKeyException(
  47 + "duplicate",
  48 + new SQLIntegrityConstraintViolationException(
  49 + "Duplicate entry 'foo' for key 'uk_tUser_sUserCode'"));
  50 +
  51 + Result<Object> r = handler.handleDuplicateKey(e);
  52 + assertThat(r.getCode()).isEqualTo(40003);
  53 + }
  54 +
  55 + @Test
  56 + void handleDuplicateKey_unknownIndex_returns50000() {
  57 + DuplicateKeyException e = new DuplicateKeyException(
  58 + "duplicate",
  59 + new SQLIntegrityConstraintViolationException(
  60 + "Duplicate entry 'foo' for key 'uk_other_table'"));
  61 +
  62 + Result<Object> r = handler.handleDuplicateKey(e);
  63 + assertThat(r.getCode()).isEqualTo(50000);
  64 + }
  65 +
  66 + @Test
  67 + void handleAccessDenied_returns40301() {
  68 + Result<Object> r = handler.handleAccessDenied(new AccessDeniedException("nope"));
  69 +
  70 + assertThat(r.getCode()).isEqualTo(40301);
  71 + assertThat(r.getMessage()).isEqualTo("权限不足");
  72 + }
  73 +
  74 + @Test
  75 + void handleValidation_returnsFirstFieldErrorMessage() {
  76 + BindingResult br = new MapBindingResult(new HashMap<>(), "dto");
  77 + br.addError(new FieldError("dto", "userName", "用户名不能为空"));
  78 + MethodParameter param = mock(MethodParameter.class);
  79 + MethodArgumentNotValidException e = new MethodArgumentNotValidException(param, br);
  80 +
  81 + Result<Object> r = handler.handleValidation(e);
  82 + assertThat(r.getCode()).isEqualTo(40001);
  83 + assertThat(r.getMessage()).isEqualTo("用户名不能为空");
  84 + }
  85 +
  86 + @Test
  87 + void handleGenericException_returns50000_noStackInResponse() {
  88 + Result<Object> r = handler.handleGeneric(new RuntimeException("boom"));
  89 +
  90 + assertThat(r.getCode()).isEqualTo(50000);
  91 + assertThat(r.getMessage()).isEqualTo("系统繁忙,请稍后重试");
  92 + // 响应体不带 stack/message 细节
  93 + assertThat(r.getMessage()).doesNotContain("boom");
  94 + }
  95 +}