diff --git a/backend/iot-common/src/main/java/com/iot/common/exception/GlobalExceptionHandler.java b/backend/iot-common/src/main/java/com/iot/common/exception/GlobalExceptionHandler.java index b50f55c..0db6db7 100644 --- a/backend/iot-common/src/main/java/com/iot/common/exception/GlobalExceptionHandler.java +++ b/backend/iot-common/src/main/java/com/iot/common/exception/GlobalExceptionHandler.java @@ -3,14 +3,29 @@ package com.iot.common.exception; import com.iot.common.result.Result; import com.iot.common.result.ResultCode; import lombok.extern.slf4j.Slf4j; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.http.converter.HttpMessageNotReadableException; import org.springframework.validation.BindException; import org.springframework.validation.FieldError; +import org.springframework.web.HttpMediaTypeNotAcceptableException; +import org.springframework.web.HttpMediaTypeNotSupportedException; +import org.springframework.web.HttpRequestMethodNotSupportedException; import org.springframework.web.bind.MethodArgumentNotValidException; +import org.springframework.web.bind.MissingServletRequestParameterException; import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.RestControllerAdvice; +import org.springframework.web.method.annotation.MethodArgumentTypeMismatchException; +import org.springframework.web.multipart.MaxUploadSizeExceededException; /** * 全局异常处理器 + * + * 响应约定: + * 业务/参数类异常 -> HTTP 200 + body.code 表达语义(沿用历史约定,前端按 body.code 判定) + * HTTP 语义类异常 -> 真实 HTTP 4xx 状态码 + body.code 与状态码一致 + * (如 415 Unsupported Media Type,与 JwtAuthenticationEntryPoint 的 401 约定一致) + * 未预期异常 -> HTTP 200 + body.code 500 */ @Slf4j @RestControllerAdvice @@ -36,6 +51,85 @@ public class GlobalExceptionHandler { return Result.failed(ResultCode.PARAM_ERROR.getCode(), message); } + // ===== HTTP 语义类异常:返回真实 4xx 状态码 ===== + + /** + * 请求的 Content-Type 不被支持(如以 application/octet-stream 发送 JSON 请求体) + */ + @ExceptionHandler(HttpMediaTypeNotSupportedException.class) + public ResponseEntity> handleHttpMediaTypeNotSupported(HttpMediaTypeNotSupportedException e) { + String message = "不支持的请求媒体类型: " + e.getContentType(); + log.warn("请求媒体类型不支持: {}", message); + return ResponseEntity.status(HttpStatus.UNSUPPORTED_MEDIA_TYPE) + .body(Result.failed(ResultCode.UNSUPPORTED_MEDIA_TYPE.getCode(), message)); + } + + /** + * 请求的 Accept 头与可返回的类型不匹配 + */ + @ExceptionHandler(HttpMediaTypeNotAcceptableException.class) + public ResponseEntity> handleHttpMediaTypeNotAcceptable(HttpMediaTypeNotAcceptableException e) { + String message = "无法满足请求的 Accept 类型"; + log.warn("请求 Accept 类型不可接受: {}", message); + return ResponseEntity.status(HttpStatus.NOT_ACCEPTABLE) + .body(Result.failed(ResultCode.NOT_ACCEPTABLE.getCode(), message)); + } + + /** + * 请求方法不被支持 + */ + @ExceptionHandler(HttpRequestMethodNotSupportedException.class) + public ResponseEntity> handleMethodNotSupported(HttpRequestMethodNotSupportedException e) { + String message = "不支持的请求方法: " + e.getMethod(); + log.warn("请求方法不支持: {}", message); + return ResponseEntity.status(HttpStatus.METHOD_NOT_ALLOWED) + .body(Result.failed(ResultCode.METHOD_NOT_ALLOWED.getCode(), message)); + } + + /** + * 缺少必填请求参数 + */ + @ExceptionHandler(MissingServletRequestParameterException.class) + public ResponseEntity> handleMissingParameter(MissingServletRequestParameterException e) { + String message = "缺少必填参数: " + e.getParameterName(); + log.warn("缺少必填请求参数: {}", message); + return ResponseEntity.status(HttpStatus.BAD_REQUEST) + .body(Result.failed(ResultCode.PARAM_ERROR.getCode(), message)); + } + + /** + * 请求参数类型不匹配 + */ + @ExceptionHandler(MethodArgumentTypeMismatchException.class) + public ResponseEntity> handleTypeMismatch(MethodArgumentTypeMismatchException e) { + String message = "参数类型不匹配: " + e.getName(); + log.warn("请求参数类型不匹配: {}", message); + return ResponseEntity.status(HttpStatus.BAD_REQUEST) + .body(Result.failed(ResultCode.PARAM_ERROR.getCode(), message)); + } + + /** + * 请求体无法解析(JSON 格式错误等) + */ + @ExceptionHandler(HttpMessageNotReadableException.class) + public ResponseEntity> handleMessageNotReadable(HttpMessageNotReadableException e) { + String message = "请求体格式错误,无法解析"; + log.warn("请求体无法解析: {}", e.getMessage()); + return ResponseEntity.status(HttpStatus.BAD_REQUEST) + .body(Result.failed(ResultCode.PARAM_ERROR.getCode(), message)); + } + + /** + * 上传内容超过限制 + */ + @ExceptionHandler(MaxUploadSizeExceededException.class) + public ResponseEntity> handleMaxUploadSize(MaxUploadSizeExceededException e) { + String message = "上传内容超过大小限制"; + log.warn("上传内容超限: {}", message); + return ResponseEntity.status(HttpStatus.PAYLOAD_TOO_LARGE) + .body(Result.failed(ResultCode.PAYLOAD_TOO_LARGE.getCode(), message)); + } + /** * 处理参数绑定异常 */ diff --git a/backend/iot-common/src/main/java/com/iot/common/result/ResultCode.java b/backend/iot-common/src/main/java/com/iot/common/result/ResultCode.java index 444955f..b38b36d 100644 --- a/backend/iot-common/src/main/java/com/iot/common/result/ResultCode.java +++ b/backend/iot-common/src/main/java/com/iot/common/result/ResultCode.java @@ -43,6 +43,21 @@ public enum ResultCode { */ METHOD_NOT_ALLOWED(405, "方法不允许"), + /** + * 请求的媒体类型不被支持(Content-Type 不匹配) + */ + UNSUPPORTED_MEDIA_TYPE(415, "不支持的媒体类型"), + + /** + * 无法接受(Accept 头与可返回类型不匹配) + */ + NOT_ACCEPTABLE(406, "无法满足请求的 Accept 类型"), + + /** + * 请求体过大 + */ + PAYLOAD_TOO_LARGE(413, "上传内容过大"), + /** * 请求超时 */ diff --git a/backend/iot-common/src/test/java/com/iot/common/exception/GlobalExceptionHandlerHttpStatusTest.java b/backend/iot-common/src/test/java/com/iot/common/exception/GlobalExceptionHandlerHttpStatusTest.java new file mode 100644 index 0000000..20b2790 --- /dev/null +++ b/backend/iot-common/src/test/java/com/iot/common/exception/GlobalExceptionHandlerHttpStatusTest.java @@ -0,0 +1,40 @@ +package com.iot.common.exception; + +import com.iot.common.result.Result; +import org.junit.jupiter.api.Test; +import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.web.HttpMediaTypeNotSupportedException; +import org.springframework.web.HttpRequestMethodNotSupportedException; + +import java.util.List; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +/** + * HTTP 语义类异常的全局处理器单测: + * 确认真实 HTTP 状态码与 body.code 保持一致(415 / 405 等), + * 不再被兜底的 Exception handler 统一当成 500「系统繁忙」。 + */ +class GlobalExceptionHandlerHttpStatusTest { + + private final GlobalExceptionHandler handler = new GlobalExceptionHandler(); + + @Test + void unsupportedMediaType_shouldReturn415() { + HttpMediaTypeNotSupportedException ex = new HttpMediaTypeNotSupportedException( + "application/octet-stream", List.of(MediaType.APPLICATION_JSON)); + ResponseEntity> resp = handler.handleHttpMediaTypeNotSupported(ex); + assertEquals(HttpStatus.UNSUPPORTED_MEDIA_TYPE, resp.getStatusCode()); + assertEquals(415, resp.getBody().getCode()); + } + + @Test + void methodNotSupported_shouldReturn405() { + HttpRequestMethodNotSupportedException ex = new HttpRequestMethodNotSupportedException("TRACE"); + ResponseEntity> resp = handler.handleMethodNotSupported(ex); + assertEquals(HttpStatus.METHOD_NOT_ALLOWED, resp.getStatusCode()); + assertEquals(405, resp.getBody().getCode()); + } +}