package com.mydreamjob.common.exception; import com.mydreamjob.common.api.ApiResponse; import com.mydreamjob.salarycalculator.SalaryPolicyVersionConflictException; import com.mydreamjob.common.logging.RequestIdAccessor; import jakarta.servlet.http.HttpServletRequest; import jakarta.validation.ConstraintViolationException; import lombok.extern.slf4j.Slf4j; import lombok.RequiredArgsConstructor; import org.springframework.context.annotation.Import; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.http.converter.HttpMessageNotReadableException; import org.springframework.security.access.AccessDeniedException; import org.springframework.security.authentication.BadCredentialsException; import org.springframework.security.core.AuthenticationException; import org.springframework.validation.FieldError; 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.servlet.resource.NoResourceFoundException; import java.util.HashMap; import java.util.Map; /** * 全局异常处理器 * *

R01-B/B2:业务错误码 → HTTP 状态的映射已收束到 {@link ErrorHttpStatusRegistry}, * 本类只负责调用它,不再持有任何映射表或条件链。

*/ @Slf4j @RestControllerAdvice @RequiredArgsConstructor @Import(ErrorHttpStatusRegistry.class) public class GlobalExceptionHandler { private final RequestIdAccessor requestIdAccessor; private final ErrorHttpStatusRegistry errorHttpStatusRegistry; public record SalaryPolicyConflictDetails(String latestPolicyVersion, java.time.LocalDate policyEffectiveDate) {} @ExceptionHandler(SalaryPolicyVersionConflictException.class) public ResponseEntity> handleSalaryPolicyConflict( SalaryPolicyVersionConflictException ex, HttpServletRequest request) { ErrorCode code = ErrorCode.SALARY_POLICY_VERSION_CONFLICT; ApiResponse body = ApiResponse.error( code.getCode(), code.getMessage(), getRequestId(request)); body.setData(new SalaryPolicyConflictDetails( ex.getLatestPolicyVersion(), ex.getLatestPolicyEffectiveDate())); return ResponseEntity.status(errorHttpStatusRegistry.resolve(code.getCode())).body(body); } @ExceptionHandler(ConstraintViolationException.class) public ResponseEntity> handleConstraintViolation( ConstraintViolationException ex, HttpServletRequest request) { log.warn("Constraint violation: path={}, message={}", request.getRequestURI(), ex.getMessage()); return ResponseEntity .status(HttpStatus.BAD_REQUEST) .body(ApiResponse.badRequest(ex.getMessage(), getRequestId(request))); } @ExceptionHandler(BusinessException.class) public ResponseEntity> handleBusinessException( BusinessException e, HttpServletRequest request) { int errorCode = e.getErrorCode(); log.warn("BusinessException: code={}, message={}, path={}", errorCode, e.getMessage(), request.getRequestURI()); HttpStatus status = errorHttpStatusRegistry.resolve(errorCode); return ResponseEntity .status(status) .body(ApiResponse.error(errorCode, e.getMessage(), getRequestId(request))); } @ExceptionHandler(MethodArgumentNotValidException.class) public ResponseEntity> handleValidationException( MethodArgumentNotValidException ex, HttpServletRequest request) { Map errors = new HashMap<>(); ex.getBindingResult().getAllErrors().forEach((error) -> { String fieldName = ((FieldError) error).getField(); String errorMessage = error.getDefaultMessage(); errors.put(fieldName, errorMessage); }); log.warn("Validation failed: path={}, errors={}", request.getRequestURI(), errors); return ResponseEntity .status(HttpStatus.BAD_REQUEST) .body(ApiResponse.badRequest("validation failed: " + errors, getRequestId(request))); } @ExceptionHandler(HttpMessageNotReadableException.class) public ResponseEntity> handleMessageNotReadable( HttpMessageNotReadableException ex, HttpServletRequest request) { log.warn("Message not readable: path={}", request.getRequestURI(), ex); return ResponseEntity .status(HttpStatus.BAD_REQUEST) .body(ApiResponse.badRequest("request body is invalid", getRequestId(request))); } @ExceptionHandler(MissingServletRequestParameterException.class) public ResponseEntity> handleMissingParam( MissingServletRequestParameterException ex, HttpServletRequest request) { log.warn("Missing parameter: {} path={}", ex.getParameterName(), request.getRequestURI()); return ResponseEntity .status(HttpStatus.BAD_REQUEST) .body(ApiResponse.badRequest("missing parameter: " + ex.getParameterName(), getRequestId(request))); } @ExceptionHandler(MethodArgumentTypeMismatchException.class) public ResponseEntity> handleTypeMismatch( MethodArgumentTypeMismatchException ex, HttpServletRequest request) { log.warn("Type mismatch: {} path={}", ex.getName(), request.getRequestURI()); return ResponseEntity .status(HttpStatus.BAD_REQUEST) .body(ApiResponse.error(ErrorCode.BAD_REQUEST.getCode(), "bad request", getRequestId(request))); } @ExceptionHandler(AuthenticationException.class) public ResponseEntity> handleAuthentication( AuthenticationException ex, HttpServletRequest request) { log.warn("Authentication failed: path={}, message={}", request.getRequestURI(), ex.getMessage()); return ResponseEntity .status(HttpStatus.UNAUTHORIZED) .body(ApiResponse.unauthorized(ex.getMessage(), getRequestId(request))); } @ExceptionHandler(AccessDeniedException.class) public ResponseEntity> handleAccessDenied( AccessDeniedException ex, HttpServletRequest request) { log.warn("Access denied: path={}", request.getRequestURI()); return ResponseEntity .status(HttpStatus.FORBIDDEN) .body(ApiResponse.forbidden("access denied", getRequestId(request))); } @ExceptionHandler(NoResourceFoundException.class) public ResponseEntity> handleNoResourceFound( NoResourceFoundException ex, HttpServletRequest request) { // S03-C 整改:业务端口 /actuator/** 等不存在资源统一返回 404。 // 只记录固定文案与归一化 requestId,不回显异常 message、资源路径或堆栈。 log.warn("No resource found: requestId={}", getRequestId(request)); return ResponseEntity .status(HttpStatus.NOT_FOUND) .body(ApiResponse.error(404, "not found", getRequestId(request))); } @ExceptionHandler(Exception.class) public ResponseEntity> handleGenericException( Exception ex, HttpServletRequest request) { log.error("Unexpected error: path={}", request.getRequestURI(), ex); return ResponseEntity .status(HttpStatus.INTERNAL_SERVER_ERROR) .body(ApiResponse.internalError(getRequestId(request))); } private String getRequestId(HttpServletRequest request) { return requestIdAccessor.get(request); } }