diff --git a/backend/src/main/java/com/xly/erp/common/exception/BizException.java b/backend/src/main/java/com/xly/erp/common/exception/BizException.java new file mode 100644 index 0000000..87f9bb4 --- /dev/null +++ b/backend/src/main/java/com/xly/erp/common/exception/BizException.java @@ -0,0 +1,14 @@ +package com.xly.erp.common.exception; + +public class BizException extends RuntimeException { + private final int code; + + public BizException(int code, String msg) { + super(msg); + this.code = code; + } + + public int getCode() { + return code; + } +} 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 new file mode 100644 index 0000000..542158b --- /dev/null +++ b/backend/src/main/java/com/xly/erp/common/exception/GlobalExceptionHandler.java @@ -0,0 +1,37 @@ +package com.xly.erp.common.exception; + +import com.xly.erp.common.response.Result; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +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; + +@RestControllerAdvice +public class GlobalExceptionHandler { + + private static final Logger log = LoggerFactory.getLogger(GlobalExceptionHandler.class); + + @ExceptionHandler(BizException.class) + public Result handleBiz(BizException e) { + log.warn("BizException code={} msg={}", e.getCode(), e.getMessage()); + return Result.fail(e.getCode(), e.getMessage()); + } + + @ExceptionHandler(MethodArgumentNotValidException.class) + public Result handleValidation(MethodArgumentNotValidException e) { + FieldError fe = e.getBindingResult().getFieldError(); + String msg = fe == null + ? "参数校验失败" + : fe.getField() + ": " + fe.getDefaultMessage(); + log.warn("ValidationException {}", msg); + return Result.fail(40001, msg); + } + + @ExceptionHandler(Exception.class) + public Result handleAny(Exception e) { + log.error("Unhandled exception", e); + return Result.fail(50000, "系统繁忙"); + } +} diff --git a/backend/src/main/java/com/xly/erp/common/response/Result.java b/backend/src/main/java/com/xly/erp/common/response/Result.java new file mode 100644 index 0000000..19391ce --- /dev/null +++ b/backend/src/main/java/com/xly/erp/common/response/Result.java @@ -0,0 +1,52 @@ +package com.xly.erp.common.response; + +public class Result { + private int code; + private String msg; + private T data; + + public Result() { + } + + public Result(int code, String msg, T data) { + this.code = code; + this.msg = msg; + this.data = data; + } + + public static Result ok(T data) { + return new Result<>(0, "ok", data); + } + + public static Result ok() { + return new Result<>(0, "ok", null); + } + + public static Result fail(int code, String msg) { + return new Result<>(code, msg, null); + } + + public int getCode() { + return code; + } + + public void setCode(int code) { + this.code = code; + } + + public String getMsg() { + return msg; + } + + public void setMsg(String msg) { + this.msg = msg; + } + + public T getData() { + return data; + } + + public void setData(T data) { + this.data = data; + } +} diff --git a/backend/src/test/java/com/xly/erp/common/exception/GlobalExceptionHandlerTest.java b/backend/src/test/java/com/xly/erp/common/exception/GlobalExceptionHandlerTest.java new file mode 100644 index 0000000..c117597 --- /dev/null +++ b/backend/src/test/java/com/xly/erp/common/exception/GlobalExceptionHandlerTest.java @@ -0,0 +1,86 @@ +package com.xly.erp.common.exception; + +import com.xly.erp.common.response.Result; +import jakarta.validation.Valid; +import jakarta.validation.constraints.NotBlank; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.setup.MockMvcBuilders; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RestController; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +class GlobalExceptionHandlerTest { + + private MockMvc mockMvc; + + @BeforeEach + void setUp() { + mockMvc = MockMvcBuilders.standaloneSetup(new StubController()) + .setControllerAdvice(new GlobalExceptionHandler()) + .build(); + } + + @Test + void bizException_returnsResultWithBizCode() throws Exception { + mockMvc.perform(get("/__test/throw-biz")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.code").value(30001)) + .andExpect(jsonPath("$.msg").value("business x")); + } + + @Test + void validationException_returns40001WithFieldHint() throws Exception { + mockMvc.perform(post("/__test/throw-validate") + .contentType("application/json") + .content("{}")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.code").value(40001)) + .andExpect(jsonPath("$.msg", org.hamcrest.Matchers.containsString("name"))); + } + + @Test + void uncaughtException_returns50000() throws Exception { + mockMvc.perform(get("/__test/throw-runtime")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.code").value(50000)); + } + + @Test + void resultOkHelper_buildsZeroCode() { + Result r = Result.ok("hi"); + assertThat(r.getCode()).isZero(); + assertThat(r.getData()).isEqualTo("hi"); + } + + @RestController + static class StubController { + @GetMapping("/__test/throw-biz") + public Result throwBiz() { + throw new BizException(30001, "business x"); + } + + @PostMapping("/__test/throw-validate") + public Result throwValidate(@Valid @RequestBody StubBody body) { + return Result.ok(); + } + + @GetMapping("/__test/throw-runtime") + public Result throwRuntime() { + throw new IllegalStateException("boom"); + } + } + + static class StubBody { + @NotBlank + public String name; + } +}