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);
}
}