package com.yaoyuan.jiscuss.exception; import com.yaoyuan.jiscuss.response.ApiResponse; import com.yaoyuan.jiscuss.response.ResponseCode; import jakarta.validation.ConstraintViolationException; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.security.access.AccessDeniedException; import org.springframework.web.bind.MethodArgumentNotValidException; import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.RestControllerAdvice; import java.util.stream.Collectors; /** * Global exception handler. * *

Catches {@link BaseException}, validation errors, and unexpected exceptions so they * are returned as structured JSON instead of Spring's default error page. */ @RestControllerAdvice public class GlobalExceptionHandler { private static final Logger log = LoggerFactory.getLogger(GlobalExceptionHandler.class); /** Business logic exceptions (already typed with a ResponseCode). */ @ExceptionHandler(BaseException.class) public ResponseEntity> handleBaseException(BaseException ex) { log.warn("Business exception: code={}, msg={}", ex.getCode().getCode(), ex.getCode().getMsg()); return ResponseEntity .status(HttpStatus.BAD_REQUEST) .body(ApiResponse.fail(ex.getCode())); } /** @Valid / @Validated failures on @RequestBody. */ @ExceptionHandler(MethodArgumentNotValidException.class) public ResponseEntity> handleValidationException(MethodArgumentNotValidException ex) { String message = ex.getBindingResult().getFieldErrors().stream() .map(fe -> fe.getField() + ": " + fe.getDefaultMessage()) .collect(Collectors.joining("; ")); log.warn("Validation failed: {}", message); return ResponseEntity .status(HttpStatus.BAD_REQUEST) .body(ApiResponse.error(message)); } /** @Validated failures on @RequestParam / @PathVariable. */ @ExceptionHandler(ConstraintViolationException.class) public ResponseEntity> handleConstraintViolation(ConstraintViolationException ex) { String message = ex.getConstraintViolations().stream() .map(cv -> cv.getPropertyPath() + ": " + cv.getMessage()) .collect(Collectors.joining("; ")); log.warn("Constraint violation: {}", message); return ResponseEntity .status(HttpStatus.BAD_REQUEST) .body(ApiResponse.error(message)); } /** * Spring Security access-denied exceptions re-thrown by method security. * Note: exceptions raised before method invocation are handled by {@code CustomAccessDeniedHandler}. */ @ExceptionHandler(AccessDeniedException.class) public ResponseEntity> handleAccessDenied(AccessDeniedException ex) { return ResponseEntity .status(HttpStatus.FORBIDDEN) .body(ApiResponse.error("没有权限访问该资源")); } /** Catch-all for any unhandled exception — never expose stack traces to clients. */ @ExceptionHandler(Exception.class) public ResponseEntity> handleGeneral(Exception ex) { log.error("Unexpected error", ex); return ResponseEntity .status(HttpStatus.INTERNAL_SERVER_ERROR) .body(ApiResponse.fail(ResponseCode.SERVICE_ERROR)); } }