diff --git a/src/main/java/com/campus/water/controller/GlobalExceptionHandler.java b/src/main/java/com/campus/water/controller/GlobalExceptionHandler.java index ba66090..14dd6b8 100644 --- a/src/main/java/com/campus/water/controller/GlobalExceptionHandler.java +++ b/src/main/java/com/campus/water/controller/GlobalExceptionHandler.java @@ -1,19 +1,35 @@ package com.campus.water.controller; +import com.campus.water.entity.BusinessException; import com.campus.water.util.ResultVO; -import org.springframework.security.access.AccessDeniedException; +import lombok.extern.slf4j.Slf4j; +import org.springframework.dao.DataIntegrityViolationException; +import org.springframework.dao.DuplicateKeyException; +import org.springframework.http.HttpStatus; +import org.springframework.http.converter.HttpMessageNotReadableException; +import org.springframework.validation.BindException; +import org.springframework.validation.FieldError; +import org.springframework.web.HttpMediaTypeNotSupportedException; +import org.springframework.web.HttpRequestMethodNotSupportedException; +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.bind.MethodArgumentNotValidException; +import org.springframework.web.multipart.MaxUploadSizeExceededException; +import org.springframework.web.servlet.NoHandlerFoundException; -import java.time.format.DateTimeParseException; -import java.util.Objects; +import java.io.IOException; +import java.nio.file.AccessDeniedException; +import java.util.List; +import java.util.NoSuchElementException; +import java.util.stream.Collectors; /** * 全局异常处理器 - 统一处理项目中所有控制器层异常 */ @RestControllerAdvice +@Slf4j public class GlobalExceptionHandler { /** @@ -26,6 +42,10 @@ public class GlobalExceptionHandler { if (msg.contains("AlertLevel") || msg.contains("AlertStatus")) { msg = "参数错误:告警级别可选值(info/warning/error/critical),告警状态可选值(pending/resolved/closed)"; } + // 设备ID格式错误特殊处理 + if (msg.contains("设备ID") || msg.contains("deviceId")) { + msg = "设备ID格式错误,正确格式为WM/WS开头+3位数字(如WM001、WS123)"; + } return ResultVO.error(400, "参数错误:" + msg); } @@ -36,8 +56,15 @@ public class GlobalExceptionHandler { public ResultVO handleTypeMismatch(MethodArgumentTypeMismatchException e) { String errorMsg; // 特殊处理时间格式错误(告警查询的时间参数) - if (e.getCause() instanceof DateTimeParseException) { + if (e.getCause() instanceof java.time.format.DateTimeParseException) { errorMsg = "时间参数格式错误,正确格式:yyyy-MM-dd HH:mm:ss(示例:2025-12-05 10:30:00)"; + } else if (e.getRequiredType() != null && e.getRequiredType().isEnum()) { + // 枚举类型转换错误处理 + errorMsg = String.format( + "参数[%s]枚举值错误,允许值:%s", + e.getName(), + getEnumValues(e.getRequiredType()) + ); } else { // 通用类型不匹配提示 errorMsg = String.format( @@ -51,21 +78,35 @@ public class GlobalExceptionHandler { } /** - * 处理权限不足异常(如非管理员/维修人员访问告警接口) + * 处理权限不足异常(如非管理员/维修人员访问受限接口) */ - @ExceptionHandler(AccessDeniedException.class) - public ResultVO handleAccessDenied(AccessDeniedException e) { - return ResultVO.error(403, "权限不足:仅管理员/维修人员可访问告警相关功能"); + @ExceptionHandler({AccessDeniedException.class, org.springframework.security.access.AccessDeniedException.class}) + public ResultVO handleAccessDenied(Exception e) { + String roleMsg = "仅超级管理员可操作"; + // 区分不同接口的权限提示 + if (e.getMessage().contains("AREA_ADMIN")) { + roleMsg = "仅区域管理员及以上权限可操作"; + } else if (e.getMessage().contains("REPAIRMAN")) { + roleMsg = "仅维修人员及管理员可操作"; + } + return ResultVO.error(403, "权限不足:" + roleMsg); } /** - * 处理通用运行时异常(兜底) + * 处理资源不存在异常(如查询不存在的设备/用户) */ - @ExceptionHandler(RuntimeException.class) - public ResultVO handleRuntimeException(RuntimeException e) { - // 生产环境建议添加日志记录,此处简化 - // log.error("服务器运行时异常", e); - return ResultVO.error(500, "服务器内部错误:" + e.getMessage()); + @ExceptionHandler(NoSuchElementException.class) + public ResultVO handleNoSuchElement(NoSuchElementException e) { + String msg = e.getMessage(); + // 标准化资源不存在提示 + if (msg.contains("设备")) { + return ResultVO.error(404, "设备不存在:" + msg.replace("No value present", "").trim()); + } else if (msg.contains("管理员") || msg.contains("Admin")) { + return ResultVO.error(404, "管理员不存在:" + msg.replace("No value present", "").trim()); + } else if (msg.contains("区域") || msg.contains("Area")) { + return ResultVO.error(404, "区域不存在:" + msg.replace("No value present", "").trim()); + } + return ResultVO.error(404, "请求的资源不存在:" + msg); } /** @@ -73,8 +114,176 @@ public class GlobalExceptionHandler { */ @ExceptionHandler(MethodArgumentNotValidException.class) public ResultVO handleMethodArgumentNotValid(MethodArgumentNotValidException e) { - // 获取第一个验证失败的字段和消息 - String errorMsg = Objects.requireNonNull(e.getBindingResult().getFieldError()).getDefaultMessage(); - return ResultVO.badRequest(errorMsg); // 返回400状态码和具体错误信息 + // 收集所有验证失败的字段和消息 + List errorMessages = e.getBindingResult().getFieldErrors().stream() + .map(error -> error.getField() + ":" + error.getDefaultMessage()) + .collect(Collectors.toList()); + return ResultVO.error(400, "参数验证失败:" + String.join(";", errorMessages)); + } + + /** + * 处理表单参数绑定异常(非@RequestBody的参数验证) + */ + @ExceptionHandler(BindException.class) + public ResultVO handleBindException(BindException e) { + FieldError firstError = e.getBindingResult().getFieldError(); + String errorMsg = firstError != null ? + firstError.getField() + ":" + firstError.getDefaultMessage() : + "参数绑定失败"; + return ResultVO.error(400, "表单参数错误:" + errorMsg); + } + + /** + * 处理缺失必填参数异常 + */ + @ExceptionHandler(MissingServletRequestParameterException.class) + public ResultVO handleMissingParam(MissingServletRequestParameterException e) { + return ResultVO.error(400, + String.format("缺少必填参数:%s(类型:%s)", + e.getParameterName(), + e.getParameterType())); + } + + /** + * 处理数据库唯一约束冲突异常 + */ + @ExceptionHandler(DuplicateKeyException.class) + public ResultVO handleDuplicateKey(DuplicateKeyException e) { + log.error("数据库唯一约束冲突", e); + String msg = "数据已存在,无法重复添加"; + // 针对设备ID冲突特殊处理 + if (e.getMessage().contains("device_id")) { + msg = "设备ID已存在,请更换设备ID后重试"; + } else if (e.getMessage().contains("admin_name")) { + msg = "管理员用户名已存在,请更换用户名"; + } + return ResultVO.error(409, msg); + } + + /** + * 处理数据库完整性约束异常(外键关联等) + */ + @ExceptionHandler(DataIntegrityViolationException.class) + public ResultVO handleDataIntegrityViolation(DataIntegrityViolationException e) { + log.error("数据库完整性约束异常", e); + String msg = "数据操作失败,可能存在关联数据"; + if (e.getMessage().contains("foreign key constraint")) { + msg = "无法删除,该数据已被其他记录关联引用"; + } else if (e.getMessage().contains("not null")) { + msg = "必填字段不能为空,请检查输入"; + } + return ResultVO.error(400, msg); + } + + /** + * 处理JSON解析异常(请求体格式错误) + */ + @ExceptionHandler(HttpMessageNotReadableException.class) + public ResultVO handleHttpMessageNotReadable(HttpMessageNotReadableException e) { + log.error("请求体解析失败", e); + String msg = "请求数据格式错误,请检查JSON格式是否正确"; + if (e.getMessage().contains("date-time")) { + msg = "日期时间格式错误,正确格式:yyyy-MM-dd HH:mm:ss"; + } + return ResultVO.error(400, msg); + } + + /** + * 处理不支持的HTTP方法异常 + */ + @ExceptionHandler(HttpRequestMethodNotSupportedException.class) + public ResultVO handleHttpRequestMethodNotSupported(HttpRequestMethodNotSupportedException e) { + return ResultVO.error(405, + String.format("不支持的请求方法:%s,支持的方法:%s", + e.getMethod(), + String.join(",", e.getSupportedMethods()))); + } + + /** + * 处理不支持的媒体类型异常 + */ + @ExceptionHandler(HttpMediaTypeNotSupportedException.class) + public ResultVO handleHttpMediaTypeNotSupported(HttpMediaTypeNotSupportedException e) { + return ResultVO.error(415, + String.format("不支持的媒体类型:%s,支持的类型:%s", + e.getContentType(), + e.getSupportedMediaTypes())); + } + + /** + * 处理文件上传大小超限异常 + */ + @ExceptionHandler(MaxUploadSizeExceededException.class) + public ResultVO handleMaxUploadSizeExceeded(MaxUploadSizeExceededException e) { + long maxSizeMB = e.getMaxUploadSize() / (1024 * 1024); + return ResultVO.error(413, + String.format("文件大小超限,最大支持:%dMB", maxSizeMB)); + } + + /** + * 处理IO异常(文件操作等) + */ + @ExceptionHandler(IOException.class) + public ResultVO handleIOException(IOException e) { + log.error("IO操作异常", e); + String msg = "文件操作失败:" + e.getMessage(); + if (e.getMessage().contains("Permission denied")) { + msg = "文件操作权限不足"; + } + return ResultVO.error(500, msg); + } + + /** + * 处理业务逻辑异常(自定义异常) + */ + @ExceptionHandler(BusinessException.class) + public ResultVO handleBusinessException(BusinessException e) { + // 业务异常自带状态码和消息 + return ResultVO.error(e.getCode(), e.getMessage()); + } + + /** + * 处理404异常 + */ + @ExceptionHandler(NoHandlerFoundException.class) + public ResultVO handleNoHandlerFound(NoHandlerFoundException e) { + return ResultVO.error(404, + String.format("请求的接口不存在:%s %s", + e.getHttpMethod(), + e.getRequestURL())); + } + + /** + * 处理通用运行时异常(兜底) + */ + @ExceptionHandler(RuntimeException.class) + public ResultVO handleRuntimeException(RuntimeException e) { + log.error("服务器运行时异常", e); + // 生产环境可根据异常类型返回更友好的提示 + String msg = "服务器内部错误:" + e.getMessage(); + // 对常见运行时异常进行特殊处理 + if (e instanceof NullPointerException) { + msg = "系统处理异常:数据为空"; + } else if (e instanceof IndexOutOfBoundsException) { + msg = "系统处理异常:数据索引越界"; + } + return ResultVO.error(500, msg); + } + + /** + * 工具方法:获取枚举类的所有值 + */ + private String getEnumValues(Class enumClass) { + if (!enumClass.isEnum()) { + return "未知"; + } + StringBuilder values = new StringBuilder(); + for (Object enumConstant : enumClass.getEnumConstants()) { + values.append(enumConstant).append(","); + } + if (values.length() > 0) { + values.deleteCharAt(values.length() - 1); + } + return values.toString(); } } \ No newline at end of file diff --git a/src/main/java/com/campus/water/entity/BusinessException.java b/src/main/java/com/campus/water/entity/BusinessException.java new file mode 100644 index 0000000..07e56fb --- /dev/null +++ b/src/main/java/com/campus/water/entity/BusinessException.java @@ -0,0 +1,55 @@ +package com.campus.water.entity; // 请确保这个包名与你的项目结构一致 + +import org.springframework.http.HttpStatus; + +/** + * 自定义业务异常 + *

+ * 用于封装业务层抛出的、需要明确反馈给用户的异常信息。 + * 例如:用户不存在、密码错误、工单状态不正确等。 + *

+ */ +public class BusinessException extends RuntimeException { + + private static final long serialVersionUID = 1L; + + /** + * 错误码,对应HTTP状态码 + */ + private int code; + + /** + * 错误消息 + */ + private String message; + + /** + * 构造函数 + * @param code 错误码 + * @param message 错误消息 + */ + public BusinessException(int code, String message) { + super(message); + this.code = code; + this.message = message; + } + + /** + * 构造函数,默认使用 400 Bad Request 状态码 + * @param message 错误消息 + */ + public BusinessException(String message) { + this(HttpStatus.BAD_REQUEST.value(), message); + } + + // --- Getters --- + + public int getCode() { + return code; + } + + @Override + public String getMessage() { + return message; + } +} \ No newline at end of file diff --git a/src/main/java/com/campus/water/mapper/AlertRepository.java b/src/main/java/com/campus/water/mapper/AlertRepository.java index 859f25d..ac58b22 100644 --- a/src/main/java/com/campus/water/mapper/AlertRepository.java +++ b/src/main/java/com/campus/water/mapper/AlertRepository.java @@ -58,4 +58,6 @@ public interface AlertRepository extends JpaRepository { List activeStatus, LocalDateTime timestamp ); + + List findByResolvedByAndStatus(String repairmanId, Alert.AlertStatus alertStatus); } \ No newline at end of file