diff --git a/backend/src/main/java/com/xly/test4/common/exception/BusinessException.java b/backend/src/main/java/com/xly/test4/common/exception/BusinessException.java new file mode 100644 index 0000000..fbbcc11 --- /dev/null +++ b/backend/src/main/java/com/xly/test4/common/exception/BusinessException.java @@ -0,0 +1,14 @@ +package com.xly.test4.common.exception; + +import lombok.Getter; + +@Getter +public class BusinessException extends RuntimeException { + + private final int code; + + public BusinessException(int code, String message) { + super(message); + this.code = code; + } +} diff --git a/backend/src/main/java/com/xly/test4/common/exception/GlobalExceptionHandler.java b/backend/src/main/java/com/xly/test4/common/exception/GlobalExceptionHandler.java new file mode 100644 index 0000000..69c976f --- /dev/null +++ b/backend/src/main/java/com/xly/test4/common/exception/GlobalExceptionHandler.java @@ -0,0 +1,54 @@ +package com.xly.test4.common.exception; + +import com.xly.test4.common.response.Result; +import com.xly.test4.common.response.ResultCode; +import lombok.extern.slf4j.Slf4j; +import org.springframework.dao.DuplicateKeyException; +import org.springframework.security.access.AccessDeniedException; +import org.springframework.validation.FieldError; +import org.springframework.web.bind.MethodArgumentNotValidException; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.bind.annotation.RestControllerAdvice; + +@Slf4j +@RestControllerAdvice +public class GlobalExceptionHandler { + + @ExceptionHandler(BusinessException.class) + public Result handleBusiness(BusinessException e) { + return Result.fail(e.getCode(), e.getMessage()); + } + + @ExceptionHandler(MethodArgumentNotValidException.class) + public Result handleValidation(MethodArgumentNotValidException e) { + FieldError first = e.getBindingResult().getFieldError(); + String message = first != null ? first.getDefaultMessage() : "参数校验失败"; + return Result.fail(ResultCode.PARAM_INVALID, message); + } + + @ExceptionHandler(AccessDeniedException.class) + public Result handleAccessDenied(AccessDeniedException e) { + return Result.fail(ResultCode.FORBIDDEN, "权限不足"); + } + + @ExceptionHandler(DuplicateKeyException.class) + public Result handleDuplicateKey(DuplicateKeyException e) { + String causeMsg = e.getMostSpecificCause() != null + ? String.valueOf(e.getMostSpecificCause().getMessage()) + : ""; + if (causeMsg.contains("uk_tUser_sUserName")) { + return Result.fail(ResultCode.USER_NAME_DUPLICATE, "用户名已存在"); + } + if (causeMsg.contains("uk_tUser_sUserCode")) { + return Result.fail(ResultCode.USER_CODE_DUPLICATE, "用户号已存在"); + } + log.error("未识别的唯一索引冲突", e); + return Result.fail(ResultCode.INTERNAL_ERROR, "系统繁忙,请稍后重试"); + } + + @ExceptionHandler(Exception.class) + public Result handleGeneric(Exception e) { + log.error("未预期异常", e); + return Result.fail(ResultCode.INTERNAL_ERROR, "系统繁忙,请稍后重试"); + } +} diff --git a/backend/src/test/java/com/xly/test4/common/exception/GlobalExceptionHandlerTest.java b/backend/src/test/java/com/xly/test4/common/exception/GlobalExceptionHandlerTest.java new file mode 100644 index 0000000..7f2a115 --- /dev/null +++ b/backend/src/test/java/com/xly/test4/common/exception/GlobalExceptionHandlerTest.java @@ -0,0 +1,95 @@ +package com.xly.test4.common.exception; + +import com.xly.test4.common.response.Result; +import org.junit.jupiter.api.Test; +import org.springframework.core.MethodParameter; +import org.springframework.dao.DuplicateKeyException; +import org.springframework.security.access.AccessDeniedException; +import org.springframework.validation.BindingResult; +import org.springframework.validation.FieldError; +import org.springframework.validation.MapBindingResult; +import org.springframework.web.bind.MethodArgumentNotValidException; + +import java.util.HashMap; + +import java.sql.SQLIntegrityConstraintViolationException; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.mock; + +class GlobalExceptionHandlerTest { + + private final GlobalExceptionHandler handler = new GlobalExceptionHandler(); + + @Test + void handleBusinessException_returnsFailResultWithCodeAndMessage() { + Result r = handler.handleBusiness(new BusinessException(40002, "用户名已存在")); + + assertThat(r.getCode()).isEqualTo(40002); + assertThat(r.getMessage()).isEqualTo("用户名已存在"); + assertThat(r.getData()).isNull(); + } + + @Test + void handleDuplicateKey_userNameIndex_returns40002() { + DuplicateKeyException e = new DuplicateKeyException( + "duplicate", + new SQLIntegrityConstraintViolationException( + "Duplicate entry 'foo' for key 'uk_tUser_sUserName'")); + + Result r = handler.handleDuplicateKey(e); + assertThat(r.getCode()).isEqualTo(40002); + } + + @Test + void handleDuplicateKey_userCodeIndex_returns40003() { + DuplicateKeyException e = new DuplicateKeyException( + "duplicate", + new SQLIntegrityConstraintViolationException( + "Duplicate entry 'foo' for key 'uk_tUser_sUserCode'")); + + Result r = handler.handleDuplicateKey(e); + assertThat(r.getCode()).isEqualTo(40003); + } + + @Test + void handleDuplicateKey_unknownIndex_returns50000() { + DuplicateKeyException e = new DuplicateKeyException( + "duplicate", + new SQLIntegrityConstraintViolationException( + "Duplicate entry 'foo' for key 'uk_other_table'")); + + Result r = handler.handleDuplicateKey(e); + assertThat(r.getCode()).isEqualTo(50000); + } + + @Test + void handleAccessDenied_returns40301() { + Result r = handler.handleAccessDenied(new AccessDeniedException("nope")); + + assertThat(r.getCode()).isEqualTo(40301); + assertThat(r.getMessage()).isEqualTo("权限不足"); + } + + @Test + void handleValidation_returnsFirstFieldErrorMessage() { + BindingResult br = new MapBindingResult(new HashMap<>(), "dto"); + br.addError(new FieldError("dto", "userName", "用户名不能为空")); + MethodParameter param = mock(MethodParameter.class); + MethodArgumentNotValidException e = new MethodArgumentNotValidException(param, br); + + Result r = handler.handleValidation(e); + assertThat(r.getCode()).isEqualTo(40001); + assertThat(r.getMessage()).isEqualTo("用户名不能为空"); + } + + @Test + void handleGenericException_returns50000_noStackInResponse() { + Result r = handler.handleGeneric(new RuntimeException("boom")); + + assertThat(r.getCode()).isEqualTo(50000); + assertThat(r.getMessage()).isEqualTo("系统繁忙,请稍后重试"); + // 响应体不带 stack/message 细节 + assertThat(r.getMessage()).doesNotContain("boom"); + } +}