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.
 
 
 

311 lines
18 KiB

package com.mydreamjob.common.exception;
import com.mydreamjob.common.api.ApiResponse;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.validation.ConstraintViolationException;
import lombok.extern.slf4j.Slf4j;
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 java.util.HashMap;
import java.util.Map;
/**
* 全局异常处理器
*/
@Slf4j
@RestControllerAdvice
public class GlobalExceptionHandler {
@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 = resolveHttpStatus(errorCode);
return ResponseEntity
.status(status)
.body(ApiResponse.error(errorCode, e.getMessage(), getRequestId(request)));
}
/**
* 按业务错误码段映射 HTTP 状态码(与 API v0.2 草案一致):
* - FORBIDDEN(2002) → 403
* - 不存在类(5001/6001/7001/8002/4001/9102) → 404
* - 冲突/非法流转类(5003/7003/8001/8003/9101) → 409
* - 其余 → 400
*/
private HttpStatus resolveHttpStatus(int errorCode) {
// 权限 → 403
if (errorCode == ErrorCode.FORBIDDEN.getCode()) return HttpStatus.FORBIDDEN;
// 面试访问/越权 → 403
if (errorCode == ErrorCode.INTERVIEW_ACCESS_DENIED.getCode()
|| errorCode == ErrorCode.APPLICATION_OWNER_MISMATCH.getCode())
return HttpStatus.FORBIDDEN;
// 不存在 → 404
if (errorCode == ErrorCode.COMPANY_NOT_FOUND.getCode()
|| errorCode == ErrorCode.PROFILE_NOT_FOUND.getCode()
|| errorCode == ErrorCode.JOB_NOT_FOUND.getCode()
|| errorCode == ErrorCode.APPLICATION_NOT_FOUND.getCode()
|| errorCode == ErrorCode.OFFER_NOT_FOUND.getCode()
|| errorCode == ErrorCode.OFFER_COUNTER_OFFER_NOT_FOUND.getCode()
|| errorCode == ErrorCode.RESUME_VERSION_NOT_FOUND.getCode()
|| errorCode == ErrorCode.RESUME_EXPORT_TASK_NOT_FOUND.getCode()
|| errorCode == ErrorCode.USER_NOT_FOUND.getCode()
|| errorCode == ErrorCode.REVIEW_NOT_FOUND.getCode()
|| errorCode == ErrorCode.FEEDBACK_NOT_FOUND.getCode()
|| errorCode == ErrorCode.CONVERSATION_NOT_FOUND.getCode()
|| errorCode == ErrorCode.MESSAGE_NOT_FOUND.getCode()
|| errorCode == ErrorCode.APPEAL_NOT_FOUND.getCode()
|| errorCode == ErrorCode.DATA_EXPORT_TASK_NOT_FOUND.getCode()
|| errorCode == ErrorCode.REVIEW_REPLY_NOT_FOUND.getCode()
|| errorCode == ErrorCode.TALENT_INVITATION_NOT_FOUND.getCode()) return HttpStatus.NOT_FOUND;
// 冲突 → 409
if (errorCode == ErrorCode.COMPANY_ALREADY_EXISTS.getCode()
|| errorCode == ErrorCode.APPLICATION_DUPLICATE.getCode()
|| errorCode == ErrorCode.INVALID_APPLICATION_STATUS_TRANSITION.getCode()
|| errorCode == ErrorCode.INVALID_JOB_STATUS_TRANSITION.getCode()
|| errorCode == ErrorCode.INVALID_APPLICATION_STATUS_TRANSITION.getCode()
|| errorCode == ErrorCode.DUPLICATE_REVIEW.getCode()
|| errorCode == ErrorCode.DUPLICATE_FEEDBACK.getCode()
|| errorCode == ErrorCode.DUPLICATE_OFFER.getCode()
|| errorCode == ErrorCode.INVALID_OFFER_STATUS_TRANSITION.getCode()
|| errorCode == ErrorCode.OFFER_EXPIRED.getCode()
|| errorCode == ErrorCode.OFFER_NOT_EDITABLE.getCode()
|| errorCode == ErrorCode.OFFER_REMINDER_DUPLICATE.getCode()
|| errorCode == ErrorCode.OFFER_COUNTER_OFFER_EXISTS.getCode()
|| errorCode == ErrorCode.OFFER_COUNTER_OFFER_NOT_PENDING.getCode()
|| errorCode == ErrorCode.OFFER_COUNTER_OFFER_EXPIRED.getCode()
|| errorCode == ErrorCode.DEFAULT_RESUME_DELETE_NOT_ALLOWED.getCode()
|| errorCode == ErrorCode.RESUME_EXPORT_IN_PROGRESS.getCode()
|| errorCode == ErrorCode.DUPLICATE_DEFAULT_RESUME.getCode()
|| errorCode == ErrorCode.RESUME_EXPORT_NOT_READY.getCode()
|| errorCode == ErrorCode.MATCH_CLOSED.getCode()
|| errorCode == ErrorCode.MATCH_SCORES_LOCKED.getCode()
|| errorCode == ErrorCode.APPLICATION_NOT_MATCHED.getCode()
|| errorCode == ErrorCode.REVIEW_ALREADY_DECIDED.getCode()
// 面试冲突 → 409
|| errorCode == ErrorCode.INVALID_INTERVIEW_STATUS_TRANSITION.getCode()
|| errorCode == ErrorCode.INTERVIEW_COMPLETE_NOT_READY.getCode()
|| errorCode == ErrorCode.INTERVIEW_ALREADY_RESPONDED.getCode()
|| errorCode == ErrorCode.REVIEW_REPLY_ALREADY_EXISTS.getCode()
|| errorCode == ErrorCode.REVIEW_REPLY_INVALID_STATE.getCode()
|| errorCode == ErrorCode.REVIEW_REPLY_CONCURRENT_UPDATE.getCode()
|| errorCode == ErrorCode.REVIEW_REPLY_IDEMPOTENCY_CONFLICT.getCode()
|| errorCode == ErrorCode.MESSAGE_RECALL_WINDOW_EXPIRED.getCode()
|| errorCode == ErrorCode.MESSAGE_RECALL_CONCURRENT_UPDATE.getCode()
|| errorCode == ErrorCode.TALENT_INVITATION_CANDIDATE_UNAVAILABLE.getCode()
|| errorCode == ErrorCode.TALENT_INVITATION_ALREADY_ACTIVE.getCode()
|| errorCode == ErrorCode.TALENT_INVITATION_INVALID_STATE.getCode()
|| errorCode == ErrorCode.TALENT_INVITATION_EXPIRED.getCode()
|| errorCode == ErrorCode.TALENT_INVITATION_CONCURRENT_UPDATE.getCode()
|| errorCode == ErrorCode.TALENT_INVITATION_IDEMPOTENCY_CONFLICT.getCode())
return HttpStatus.CONFLICT;
// 越权 → 403(新增:企业-求职者建会话需匹配)
if (errorCode == ErrorCode.MATCH_REQUIRED_FOR_CONVERSATION.getCode())
return HttpStatus.FORBIDDEN;
// Offer 比较越权(包含非本人 Offer) → 403
if (errorCode == ErrorCode.OFFER_COMPARE_NOT_OWNER.getCode())
return HttpStatus.FORBIDDEN;
// 认证不存在 → 404
if (errorCode == ErrorCode.VERIFICATION_NOT_FOUND.getCode()) return HttpStatus.NOT_FOUND;
// 认证冲突 → 409
if (errorCode == ErrorCode.VERIFICATION_ALREADY_PENDING.getCode()
|| errorCode == ErrorCode.VERIFICATION_ALREADY_APPROVED.getCode()
|| errorCode == ErrorCode.INVALID_VERIFICATION_STATUS_TRANSITION.getCode())
return HttpStatus.CONFLICT;
// 权重并发冲突 → 409
if (errorCode == ErrorCode.REVIEW_WEIGHT_CONCURRENT_UPDATE.getCode())
return HttpStatus.CONFLICT;
// 隐私设置并发冲突 → 409
if (errorCode == ErrorCode.PRIVACY_SETTINGS_CONCURRENT_UPDATE.getCode())
return HttpStatus.CONFLICT;
// 账号注销状态冲突 → 409(9301-9306)
if (errorCode == ErrorCode.INVALID_DELETION_STATE.getCode()
|| errorCode == ErrorCode.ACCOUNT_DELETION_PENDING.getCode()
|| errorCode == ErrorCode.ACCOUNT_DELETED.getCode()
|| errorCode == ErrorCode.ACCOUNT_DELETION_CONCURRENT_UPDATE.getCode()
|| errorCode == ErrorCode.ACCOUNT_DELETION_CANCEL_DEADLINE_PASSED.getCode()
|| errorCode == ErrorCode.LAST_ADMIN_CANNOT_DELETE.getCode())
return HttpStatus.CONFLICT;
// 评分治理冲突 → 409
if (errorCode == ErrorCode.APPEAL_DUPLICATE.getCode()
|| errorCode == ErrorCode.APPEAL_ALREADY_DECIDED.getCode()
|| errorCode == ErrorCode.APPEAL_ROLLBACK_LIMIT_EXCEEDED.getCode()
|| errorCode == ErrorCode.APPEAL_INVALID_TRANSITION.getCode()
|| errorCode == ErrorCode.APPEAL_IDEMPOTENCY_CONFLICT.getCode())
return HttpStatus.CONFLICT;
// 个人数据导出 → 409(9402-9406,除 9401 已映射 404)
if (errorCode == ErrorCode.DATA_EXPORT_DUPLICATE.getCode()
|| errorCode == ErrorCode.DATA_EXPORT_NOT_READY.getCode()
|| errorCode == ErrorCode.DATA_EXPORT_EXPIRED.getCode()
|| errorCode == ErrorCode.DATA_EXPORT_FAILED.getCode()
|| errorCode == ErrorCode.DATA_EXPORT_FILE_NOT_FOUND.getCode())
return HttpStatus.CONFLICT;
// 结构化面试评分 → 404(9504 评分不存在)
if (errorCode == ErrorCode.INTERVIEW_SCORE_NOT_FOUND.getCode())
return HttpStatus.NOT_FOUND;
// 结构化面试评分 → 409(9501 并发 / 9502 未完成 / 9503 已提交)
if (errorCode == ErrorCode.INTERVIEW_SCORE_CONCURRENT_UPDATE.getCode()
|| errorCode == ErrorCode.INTERVIEW_NOT_COMPLETED.getCode()
|| errorCode == ErrorCode.INTERVIEW_SCORE_SUBMITTED.getCode())
return HttpStatus.CONFLICT;
// 举报与屏蔽:不存在 → 404
if (errorCode == ErrorCode.REPORT_NOT_FOUND.getCode()) return HttpStatus.NOT_FOUND;
// 举报与屏蔽:冲突/重复/非法流转 → 409
if (errorCode == ErrorCode.REPORT_DUPLICATE.getCode()
|| errorCode == ErrorCode.REPORT_ALREADY_DECIDED.getCode()
|| errorCode == ErrorCode.REPORT_INVALID_TRANSITION.getCode()
|| errorCode == ErrorCode.REPORT_IDEMPOTENCY_CONFLICT.getCode())
return HttpStatus.CONFLICT;
// 举报限流 → 429
if (errorCode == ErrorCode.REPORT_RATE_LIMITED.getCode())
return HttpStatus.TOO_MANY_REQUESTS;
// RIASEC 测评:不存在 → 404
if (errorCode == ErrorCode.ASSESSMENT_TYPE_NOT_FOUND.getCode()
|| errorCode == ErrorCode.ASSESSMENT_ATTEMPT_NOT_FOUND.getCode())
return HttpStatus.NOT_FOUND;
// RIASEC 测评:状态/并发/幂等冲突 → 409
if (errorCode == ErrorCode.ASSESSMENT_ATTEMPT_INVALID_STATE.getCode()
|| errorCode == ErrorCode.ASSESSMENT_CONCURRENT_UPDATE.getCode()
|| errorCode == ErrorCode.ASSESSMENT_DUPLICATE_ACTIVE.getCode()
|| errorCode == ErrorCode.ASSESSMENT_IDEMPOTENCY_CONFLICT.getCode())
return HttpStatus.CONFLICT;
// RIASEC 测评:答案/未完成 → 400
if (errorCode == ErrorCode.ASSESSMENT_ANSWER_INVALID.getCode()
|| errorCode == ErrorCode.ASSESSMENT_INCOMPLETE.getCode())
return HttpStatus.BAD_REQUEST;
// 四维能力测评:不存在 → 404
if (errorCode == ErrorCode.ABILITY_TYPE_NOT_FOUND.getCode()
|| errorCode == ErrorCode.ABILITY_ATTEMPT_NOT_FOUND.getCode()
|| errorCode == ErrorCode.ABILITY_RESULT_NOT_FOUND.getCode())
return HttpStatus.NOT_FOUND;
// 四维能力测评:状态/并发/幂等/冷却冲突 → 409
if (errorCode == ErrorCode.ABILITY_ATTEMPT_INVALID_STATE.getCode()
|| errorCode == ErrorCode.ABILITY_CONCURRENT_UPDATE.getCode()
|| errorCode == ErrorCode.ABILITY_DUPLICATE_ACTIVE.getCode()
|| errorCode == ErrorCode.ABILITY_IDEMPOTENCY_CONFLICT.getCode()
|| errorCode == ErrorCode.ABILITY_RETAKE_COOLDOWN.getCode())
return HttpStatus.CONFLICT;
// 四维能力测评:答案/未完成 → 400
if (errorCode == ErrorCode.ABILITY_ANSWER_INVALID.getCode()
|| errorCode == ErrorCode.ABILITY_INCOMPLETE.getCode())
return HttpStatus.BAD_REQUEST;
// 四维能力测评:重测次数限制 → 429
if (errorCode == ErrorCode.ABILITY_RETAKE_LIMIT.getCode())
return HttpStatus.TOO_MANY_REQUESTS;
// Offer 进阶契约 → 400(不可撤销、不可还价、比较数量超限)
if (errorCode == ErrorCode.OFFER_NOT_WITHDRAWABLE.getCode()
|| errorCode == ErrorCode.OFFER_NOT_NEGOTIABLE.getCode()
|| errorCode == ErrorCode.OFFER_COMPARE_TOO_MANY.getCode())
return HttpStatus.BAD_REQUEST;
// 风险概览与运营规则配置:不存在 → 404
if (errorCode == ErrorCode.RISK_RULE_NOT_FOUND.getCode()
|| errorCode == ErrorCode.RISK_EVENT_NOT_FOUND.getCode())
return HttpStatus.NOT_FOUND;
// 风险概览与运营规则配置:并发/非法流转 → 409
if (errorCode == ErrorCode.RISK_RULE_CONCURRENT_UPDATE.getCode()
|| errorCode == ErrorCode.RISK_EVENT_CONCURRENT_UPDATE.getCode()
|| errorCode == ErrorCode.RISK_INVALID_ACTION.getCode())
return HttpStatus.CONFLICT;
// 风险概览与运营规则配置:非法筛选 → 400(默认即 400,显式声明保证 9906)
if (errorCode == ErrorCode.RISK_INVALID_FILTER.getCode())
return HttpStatus.BAD_REQUEST;
if (errorCode == ErrorCode.TALENT_INVITATION_RATE_LIMITED.getCode())
return HttpStatus.TOO_MANY_REQUESTS;
return HttpStatus.BAD_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(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 request.getHeader("X-Request-Id");
}
}