You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
161 lines
7.9 KiB
161 lines
7.9 KiB
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;
|
|
|
|
/**
|
|
* 全局异常处理器
|
|
*
|
|
* <p>R01-B/B2:业务错误码 → HTTP 状态的映射已收束到 {@link ErrorHttpStatusRegistry},
|
|
* 本类只负责调用它,不再持有任何映射表或条件链。</p>
|
|
*/
|
|
@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<ApiResponse<SalaryPolicyConflictDetails>> handleSalaryPolicyConflict(
|
|
SalaryPolicyVersionConflictException ex, HttpServletRequest request) {
|
|
ErrorCode code = ErrorCode.SALARY_POLICY_VERSION_CONFLICT;
|
|
ApiResponse<SalaryPolicyConflictDetails> 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<ApiResponse<Void>> 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<ApiResponse<Void>> 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<ApiResponse<Void>> handleValidationException(
|
|
MethodArgumentNotValidException ex, HttpServletRequest request) {
|
|
Map<String, String> 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<ApiResponse<Void>> 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<ApiResponse<Void>> 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<ApiResponse<Void>> 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<ApiResponse<Void>> 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<ApiResponse<Void>> 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<ApiResponse<Void>> 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<ApiResponse<Void>> 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);
|
|
}
|
|
}
|
|
|