package com.tailbet.config; import com.tailbet.common.enums.ErrorCodeEnum; import com.tailbet.common.exception.BusinessException; import com.tailbet.model.vo.R; import jakarta.servlet.http.HttpServletRequest; import jakarta.validation.ConstraintViolation; import jakarta.validation.ConstraintViolationException; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.context.MessageSource; import org.springframework.http.converter.HttpMessageNotReadableException; import org.springframework.util.StringUtils; import org.springframework.validation.BindException; 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.Locale; import java.util.stream.Collectors; /** * 全局异常处理器 *

* 统一捕获并处理Controller层抛出的各类异常,将异常转换为统一的Result响应格式。 * 处理优先级:具体异常 > 通用异常 > 兜底异常。 * 支持国际化:根据请求头 Accept-Language 自动返回对应语言的错误信息。 *

* * @author socialapp团队 * @since 1.0.0 */ @Slf4j @RestControllerAdvice @RequiredArgsConstructor public class GlobalExceptionHandler { /** * 国际化消息源,用于将消息 key 解析为对应语言的文本 */ private final MessageSource messageSource; /** * 处理业务异常(最高优先级) * * @param e 业务异常 * @param request HTTP请求 * @return 统一错误响应 */ @ExceptionHandler(BusinessException.class) @SuppressWarnings({"unchecked", "rawtypes"}) public R handleBusinessException(BusinessException e, HttpServletRequest request) { // 业务异常的 message 可能是 key,也可能是业务自定义文本,统一走 resolveMessage 处理 String message = e.getMessage(); log.warn("【业务异常】URI: {}, 错误码: {}, 消息: {}", request.getRequestURI(), e.getCode(), message); if (e.getData() != null) { R r = R.fail(e.getCode(), message); r.setData(e.getData()); return r; } return R.fail(e.getCode(), message); } /** * 处理参数校验异常(@Valid / @RequestBody) * * @param e 方法参数校验异常 * @param request HTTP请求 * @return 统一错误响应 */ @ExceptionHandler(MethodArgumentNotValidException.class) public R handleMethodArgumentNotValid(MethodArgumentNotValidException e, HttpServletRequest request) { String message = e.getBindingResult().getFieldErrors().stream() .map(FieldError::getDefaultMessage) .collect(Collectors.joining(", ")); log.warn("【参数校验失败】URI: {}, 错误: {}", request.getRequestURI(), message); // 参数校验失败的 message 来自注解,直接使用,不走国际化 key 解析 return R.fail(ErrorCodeEnum.PARAM_VALIDATE_ERROR, message); } /** * 处理参数绑定异常(@ModelAttribute) * * @param e 参数绑定异常 * @param request HTTP请求 * @return 统一错误响应 */ @ExceptionHandler(BindException.class) public R handleBindException(BindException e, HttpServletRequest request) { String message = e.getFieldErrors().stream() .map(error -> error.getField() + ": " + error.getDefaultMessage()) .collect(Collectors.joining(", ")); log.warn("【参数绑定失败】URI: {}, 错误: {}", request.getRequestURI(), message); return R.fail(ErrorCodeEnum.PARAM_VALIDATE_ERROR, message); } /** * 处理约束校验异常(@Validated 方法级别) * * @param e 约束校验异常 * @param request HTTP请求 * @return 统一错误响应 */ @ExceptionHandler(ConstraintViolationException.class) public R handleConstraintViolation(ConstraintViolationException e, HttpServletRequest request) { String message = e.getConstraintViolations().stream() .map(ConstraintViolation::getMessage) .collect(Collectors.joining(", ")); log.warn("【约束校验失败】URI: {}, 错误: {}", request.getRequestURI(), message); return R.fail(ErrorCodeEnum.PARAM_VALIDATE_ERROR, message); } /** * 处理缺少请求参数异常 * * @param e 缺少参数异常 * @param request HTTP请求 * @return 统一错误响应 */ @ExceptionHandler(MissingServletRequestParameterException.class) public R handleMissingParam(MissingServletRequestParameterException e, HttpServletRequest request) { // 缺少参数的提示信息拼接参数名,不走国际化 key String message = e.getMessage() + ": " + e.getParameterName(); log.warn("【缺少参数】URI: {}, 错误: {}", request.getRequestURI(), message); return R.fail(ErrorCodeEnum.PARAM_ERROR, message); } /** * 处理请求体不可读异常(请求体缺失、格式错误、缺少必填字段等) * * @param e 请求体不可读异常 * @param request HTTP请求 * @return 统一错误响应 */ @ExceptionHandler(HttpMessageNotReadableException.class) public R handleHttpMessageNotReadable(HttpMessageNotReadableException e, HttpServletRequest request) { // 请求体格式错误,使用国际化后的错误码消息 String message = e.getMessage(); log.warn("【请求体不可读】URI: {}, 错误: {}", request.getRequestURI(), e.getMessage()); return R.fail(ErrorCodeEnum.PARAM_FORMAT_ERROR, message); } /** * 处理参数类型不匹配异常 * * @param e 参数类型不匹配异常 * @param request HTTP请求 * @return 统一错误响应 */ @ExceptionHandler(MethodArgumentTypeMismatchException.class) public R handleTypeMismatch(MethodArgumentTypeMismatchException e, HttpServletRequest request) { String message = String.format("参数[%s]类型错误,期望类型: %s", e.getName(), e.getRequiredType() != null ? e.getRequiredType().getSimpleName() : "未知"); log.warn("【参数类型错误】URI: {}, 错误: {}", request.getRequestURI(), message); return R.fail(ErrorCodeEnum.PARAM_FORMAT_ERROR, message); } /** * 处理非法参数异常 * * @param e 非法参数异常 * @param request HTTP请求 * @return 统一错误响应 */ @ExceptionHandler(IllegalArgumentException.class) public R handleIllegalArgument(IllegalArgumentException e, HttpServletRequest request) { log.warn("【非法参数】URI: {}, 错误: {}", request.getRequestURI(), e.getMessage()); return R.fail(ErrorCodeEnum.PARAM_ERROR, e.getMessage()); } /** * 处理非法状态异常 * * @param e 非法状态异常 * @param request HTTP请求 * @return 统一错误响应 */ @ExceptionHandler(IllegalStateException.class) public R handleIllegalState(IllegalStateException e, HttpServletRequest request) { log.warn("【非法状态】URI: {}, 错误: {}", request.getRequestURI(), e.getMessage()); return R.fail(ErrorCodeEnum.OPERATION_NOT_ALLOWED, e.getMessage()); } /** * 兜底异常处理器(最低优先级) *

* 捕获所有未被前面处理器捕获的异常,避免异常信息直接暴露给客户端。 *

* * @param e 异常对象 * @param request HTTP请求 * @return 统一错误响应 */ @ExceptionHandler(Exception.class) public R handleException(Exception e, HttpServletRequest request) { log.error("【系统异常】URI: {}, 错误类型: {}, 消息: {}", request.getRequestURI(), e.getClass().getName(), e.getMessage(), e); // 兜底异常使用国际化后的系统繁忙提示 return R.fail(ErrorCodeEnum.INTERNAL_ERROR.getCode(), e.getMessage()); } }