From 50da70d580896e29a6925859530b2e1696f96ebe Mon Sep 17 00:00:00 2001 From: XuqmGroup Date: Wed, 27 May 2026 11:51:19 +0800 Subject: [PATCH] =?UTF-8?q?fix(core):=20=E7=BB=9F=E4=B8=80=E5=85=A8?= =?UTF-8?q?=E5=B1=80=E5=BC=82=E5=B8=B8=E5=A4=84=E7=90=86=E5=99=A8=E5=B9=B6?= =?UTF-8?q?=E6=B7=BB=E5=8A=A0=E6=95=B0=E6=8D=AE=E5=BA=93=E7=AE=A1=E7=90=86?= =?UTF-8?q?=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 在所有服务的GlobalExceptionHandler中添加HttpServletRequest参数以记录请求上下文 - 统一异常响应格式为ResponseEntity>并改进错误日志记录 - 添加对多种异常类型的处理包括参数验证、请求方法不支持、权限拒绝等 - 为业务异常添加不同级别的日志记录(warn/error)和状态码映射 - 在前端系统API中新增数据库表管理相关接口定义和实现 - 添加数据库表列表、列信息和数据查询的API调用函数 --- .../demo/config/GlobalExceptionHandler.java | 92 ++++++++- .../im/controller/GlobalExceptionHandler.java | 62 ++++-- .../controller/GlobalExceptionHandler.java | 83 +++++++- .../controller/GlobalExceptionHandler.java | 83 +++++++- .../tenant/controller/DatabaseController.java | 192 ++++++++++++++++++ .../controller/GlobalExceptionHandler.java | 51 ++++- .../controller/GlobalExceptionHandler.java | 51 ++++- 7 files changed, 562 insertions(+), 52 deletions(-) create mode 100644 tenant-service/src/main/java/com/xuqm/tenant/controller/DatabaseController.java diff --git a/demo-service/src/main/java/com/xuqm/demo/config/GlobalExceptionHandler.java b/demo-service/src/main/java/com/xuqm/demo/config/GlobalExceptionHandler.java index feb2745..c3de5da 100644 --- a/demo-service/src/main/java/com/xuqm/demo/config/GlobalExceptionHandler.java +++ b/demo-service/src/main/java/com/xuqm/demo/config/GlobalExceptionHandler.java @@ -2,11 +2,19 @@ package com.xuqm.demo.config; import com.xuqm.common.exception.BusinessException; import com.xuqm.common.model.ApiResponse; +import jakarta.servlet.http.HttpServletRequest; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.http.converter.HttpMessageNotReadableException; +import org.springframework.security.authorization.AuthorizationDeniedException; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.web.HttpRequestMethodNotSupportedException; +import org.springframework.web.bind.MissingServletRequestParameterException; +import org.springframework.web.bind.MethodArgumentNotValidException; import org.springframework.web.bind.annotation.ExceptionHandler; -import org.springframework.web.bind.annotation.ResponseStatus; import org.springframework.web.bind.annotation.RestControllerAdvice; import org.springframework.web.multipart.MaxUploadSizeExceededException; @@ -16,20 +24,84 @@ public class GlobalExceptionHandler { private static final Logger log = LoggerFactory.getLogger(GlobalExceptionHandler.class); @ExceptionHandler(BusinessException.class) - public ApiResponse handleBusiness(BusinessException ex) { - return ApiResponse.error(ex.getCode(), ex.getMessage()); + public ResponseEntity> handleBusiness(BusinessException ex, HttpServletRequest request) { + if (ex.getCode() >= 500) { + log.error("[{}] {} code={} msg={}", request.getMethod(), request.getRequestURI(), ex.getCode(), ex.getMessage(), ex); + } else { + log.warn("[{}] {} code={} msg={}", request.getMethod(), request.getRequestURI(), ex.getCode(), ex.getMessage()); + } + return ResponseEntity.status(resolveStatus(ex.getCode())) + .body(ApiResponse.error(ex.getCode(), ex.getMessage())); } @ExceptionHandler(MaxUploadSizeExceededException.class) - @ResponseStatus(HttpStatus.PAYLOAD_TOO_LARGE) - public ApiResponse handleMaxUploadSize(MaxUploadSizeExceededException ex) { - return ApiResponse.error(413, "File size exceeds the maximum allowed limit"); + public ResponseEntity> handleMaxUploadSize(MaxUploadSizeExceededException ex, HttpServletRequest request) { + log.warn("[{}] {} upload size exceeded: {}", request.getMethod(), request.getRequestURI(), ex.getMessage()); + return ResponseEntity.status(HttpStatus.PAYLOAD_TOO_LARGE) + .body(ApiResponse.error(413, "文件大小超过限制")); + } + + @ExceptionHandler(MethodArgumentNotValidException.class) + public ResponseEntity> handleValidation(MethodArgumentNotValidException ex, HttpServletRequest request) { + String message = ex.getBindingResult().getFieldErrors().stream() + .findFirst() + .map(error -> error.getDefaultMessage()) + .orElse("参数错误"); + log.warn("[{}] {} validation failed: {}", request.getMethod(), request.getRequestURI(), message); + return ResponseEntity.badRequest().body(ApiResponse.badRequest(message)); + } + + @ExceptionHandler(MissingServletRequestParameterException.class) + public ResponseEntity> handleMissingParam(MissingServletRequestParameterException ex, HttpServletRequest request) { + log.warn("[{}] {} missing param: {}", request.getMethod(), request.getRequestURI(), ex.getParameterName()); + return ResponseEntity.badRequest() + .body(ApiResponse.badRequest("缺少必填参数: " + ex.getParameterName())); + } + + @ExceptionHandler(IllegalArgumentException.class) + public ResponseEntity> handleIllegalArgument(IllegalArgumentException ex, HttpServletRequest request) { + log.warn("[{}] {} illegal argument: {}", request.getMethod(), request.getRequestURI(), ex.getMessage()); + return ResponseEntity.badRequest().body(ApiResponse.badRequest(ex.getMessage() == null ? "参数错误" : ex.getMessage())); + } + + @ExceptionHandler(HttpMessageNotReadableException.class) + public ResponseEntity> handleUnreadable(HttpMessageNotReadableException ex, HttpServletRequest request) { + log.warn("[{}] {} request body unreadable: {}", request.getMethod(), request.getRequestURI(), ex.getMessage()); + return ResponseEntity.badRequest().body(ApiResponse.badRequest("请求体格式错误")); + } + + @ExceptionHandler(HttpRequestMethodNotSupportedException.class) + public ResponseEntity> handleMethodNotSupported(HttpRequestMethodNotSupportedException ex, HttpServletRequest request) { + log.warn("[{}] {} method not supported: {}", request.getMethod(), request.getRequestURI(), ex.getMethod()); + return ResponseEntity.status(HttpStatus.METHOD_NOT_ALLOWED) + .body(ApiResponse.error(405, "请求方法不支持: " + ex.getMethod())); + } + + @ExceptionHandler(AuthorizationDeniedException.class) + public ResponseEntity> handleAuthorizationDenied(AuthorizationDeniedException ex, HttpServletRequest request) { + Authentication auth = SecurityContextHolder.getContext().getAuthentication(); + String principal = auth == null ? "anonymous" : String.valueOf(auth.getPrincipal()); + String authorities = auth == null ? "[]" : auth.getAuthorities().toString(); + log.warn("[{}] {} authorization denied: principal={} authorities={} reason={}", + request.getMethod(), request.getRequestURI(), principal, authorities, ex.getMessage()); + return ResponseEntity.status(HttpStatus.FORBIDDEN) + .body(ApiResponse.error(403, "权限不足")); } @ExceptionHandler(Exception.class) - @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR) - public ApiResponse handleGeneric(Exception ex) { - log.error("Unhandled exception", ex); - return ApiResponse.error(500, "Internal server error"); + public ResponseEntity> handleGeneric(Exception ex, HttpServletRequest request) { + log.error("[{}] {} unhandled exception: {}", request.getMethod(), request.getRequestURI(), ex.getMessage(), ex); + return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR) + .body(ApiResponse.error(500, "服务器内部错误")); + } + + private HttpStatus resolveStatus(int code) { + return switch (code) { + case 400 -> HttpStatus.BAD_REQUEST; + case 401 -> HttpStatus.UNAUTHORIZED; + case 403 -> HttpStatus.FORBIDDEN; + case 404 -> HttpStatus.NOT_FOUND; + default -> HttpStatus.INTERNAL_SERVER_ERROR; + }; } } diff --git a/im-service/src/main/java/com/xuqm/im/controller/GlobalExceptionHandler.java b/im-service/src/main/java/com/xuqm/im/controller/GlobalExceptionHandler.java index 107c1d9..e9cff34 100644 --- a/im-service/src/main/java/com/xuqm/im/controller/GlobalExceptionHandler.java +++ b/im-service/src/main/java/com/xuqm/im/controller/GlobalExceptionHandler.java @@ -2,6 +2,7 @@ package com.xuqm.im.controller; import com.xuqm.common.exception.BusinessException; import com.xuqm.common.model.ApiResponse; +import jakarta.servlet.http.HttpServletRequest; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.http.HttpStatus; @@ -10,6 +11,8 @@ import org.springframework.http.converter.HttpMessageNotReadableException; import org.springframework.security.authorization.AuthorizationDeniedException; import org.springframework.security.core.Authentication; import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.web.HttpRequestMethodNotSupportedException; +import org.springframework.web.bind.MissingServletRequestParameterException; import org.springframework.web.bind.MethodArgumentNotValidException; import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.RestControllerAdvice; @@ -20,47 +23,68 @@ public class GlobalExceptionHandler { private static final Logger log = LoggerFactory.getLogger(GlobalExceptionHandler.class); @ExceptionHandler(BusinessException.class) - public ResponseEntity> handleBusiness(BusinessException e) { - return ResponseEntity.status(resolveStatus(e.getCode())) - .body(ApiResponse.error(e.getCode(), e.getMessage())); + public ResponseEntity> handleBusiness(BusinessException ex, HttpServletRequest request) { + if (ex.getCode() >= 500) { + log.error("[{}] {} code={} msg={}", request.getMethod(), request.getRequestURI(), ex.getCode(), ex.getMessage(), ex); + } else { + log.warn("[{}] {} code={} msg={}", request.getMethod(), request.getRequestURI(), ex.getCode(), ex.getMessage()); + } + return ResponseEntity.status(resolveStatus(ex.getCode())) + .body(ApiResponse.error(ex.getCode(), ex.getMessage())); } @ExceptionHandler(MethodArgumentNotValidException.class) - public ResponseEntity> handleValidation(MethodArgumentNotValidException e) { - return ResponseEntity.badRequest().body(ApiResponse.badRequest(e.getBindingResult() - .getFieldErrors() - .stream() + public ResponseEntity> handleValidation(MethodArgumentNotValidException ex, HttpServletRequest request) { + String message = ex.getBindingResult().getFieldErrors().stream() .findFirst() .map(error -> error.getDefaultMessage()) - .orElse("参数错误"))); + .orElse("参数错误"); + log.warn("[{}] {} validation failed: {}", request.getMethod(), request.getRequestURI(), message); + return ResponseEntity.badRequest().body(ApiResponse.badRequest(message)); + } + + @ExceptionHandler(MissingServletRequestParameterException.class) + public ResponseEntity> handleMissingParam(MissingServletRequestParameterException ex, HttpServletRequest request) { + log.warn("[{}] {} missing param: {}", request.getMethod(), request.getRequestURI(), ex.getParameterName()); + return ResponseEntity.badRequest() + .body(ApiResponse.badRequest("缺少必填参数: " + ex.getParameterName())); } @ExceptionHandler(IllegalArgumentException.class) - public ResponseEntity> handleIllegalArgument(IllegalArgumentException e) { - return ResponseEntity.badRequest().body(ApiResponse.badRequest(e.getMessage() == null ? "参数错误" : e.getMessage())); + public ResponseEntity> handleIllegalArgument(IllegalArgumentException ex, HttpServletRequest request) { + log.warn("[{}] {} illegal argument: {}", request.getMethod(), request.getRequestURI(), ex.getMessage()); + return ResponseEntity.badRequest().body(ApiResponse.badRequest(ex.getMessage() == null ? "参数错误" : ex.getMessage())); } @ExceptionHandler(HttpMessageNotReadableException.class) - public ResponseEntity> handleUnreadable(HttpMessageNotReadableException e) { + public ResponseEntity> handleUnreadable(HttpMessageNotReadableException ex, HttpServletRequest request) { + log.warn("[{}] {} request body unreadable: {}", request.getMethod(), request.getRequestURI(), ex.getMessage()); return ResponseEntity.badRequest().body(ApiResponse.badRequest("请求体格式错误")); } + @ExceptionHandler(HttpRequestMethodNotSupportedException.class) + public ResponseEntity> handleMethodNotSupported(HttpRequestMethodNotSupportedException ex, HttpServletRequest request) { + log.warn("[{}] {} method not supported: {}", request.getMethod(), request.getRequestURI(), ex.getMethod()); + return ResponseEntity.status(HttpStatus.METHOD_NOT_ALLOWED) + .body(ApiResponse.error(405, "请求方法不支持: " + ex.getMethod())); + } + @ExceptionHandler(AuthorizationDeniedException.class) - public ResponseEntity> handleAuthorizationDenied(AuthorizationDeniedException e) { + public ResponseEntity> handleAuthorizationDenied(AuthorizationDeniedException ex, HttpServletRequest request) { Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); - String principal = authentication == null ? null : String.valueOf(authentication.getPrincipal()); + String principal = authentication == null ? "anonymous" : String.valueOf(authentication.getPrincipal()); String authorities = authentication == null ? "[]" : authentication.getAuthorities().toString(); - log.warn("Access denied path={} principal={} authorities={} reason={}", - "im-service", principal, authorities, e.getMessage()); + log.warn("[{}] {} authorization denied: principal={} authorities={} reason={}", + request.getMethod(), request.getRequestURI(), principal, authorities, ex.getMessage()); return ResponseEntity.status(HttpStatus.FORBIDDEN) - .body(ApiResponse.error(403, "Forbidden: current token lacks required role")); + .body(ApiResponse.error(403, "权限不足")); } @ExceptionHandler(Exception.class) - public ResponseEntity> handleException(Exception e) { - log.error("Unhandled exception", e); + public ResponseEntity> handleException(Exception ex, HttpServletRequest request) { + log.error("[{}] {} unhandled exception: {}", request.getMethod(), request.getRequestURI(), ex.getMessage(), ex); return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR) - .body(ApiResponse.error(500, "服务异常")); + .body(ApiResponse.error(500, "服务器内部错误")); } private HttpStatus resolveStatus(int code) { diff --git a/license-service/src/main/java/com/xuqm/license/controller/GlobalExceptionHandler.java b/license-service/src/main/java/com/xuqm/license/controller/GlobalExceptionHandler.java index 920c744..1ae1156 100644 --- a/license-service/src/main/java/com/xuqm/license/controller/GlobalExceptionHandler.java +++ b/license-service/src/main/java/com/xuqm/license/controller/GlobalExceptionHandler.java @@ -2,7 +2,17 @@ package com.xuqm.license.controller; import com.xuqm.common.exception.BusinessException; import com.xuqm.common.model.ApiResponse; +import jakarta.servlet.http.HttpServletRequest; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; +import org.springframework.http.converter.HttpMessageNotReadableException; +import org.springframework.security.authorization.AuthorizationDeniedException; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.web.HttpRequestMethodNotSupportedException; +import org.springframework.web.bind.MissingServletRequestParameterException; import org.springframework.web.bind.MethodArgumentNotValidException; import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.RestControllerAdvice; @@ -10,17 +20,80 @@ import org.springframework.web.bind.annotation.RestControllerAdvice; @RestControllerAdvice public class GlobalExceptionHandler { + private static final Logger log = LoggerFactory.getLogger(GlobalExceptionHandler.class); + @ExceptionHandler(BusinessException.class) - public ResponseEntity> handleBusinessException(BusinessException e) { - return ResponseEntity.status(e.getCode()).body(ApiResponse.error(e.getCode(), e.getMessage())); + public ResponseEntity> handleBusiness(BusinessException ex, HttpServletRequest request) { + if (ex.getCode() >= 500) { + log.error("[{}] {} code={} msg={}", request.getMethod(), request.getRequestURI(), ex.getCode(), ex.getMessage(), ex); + } else { + log.warn("[{}] {} code={} msg={}", request.getMethod(), request.getRequestURI(), ex.getCode(), ex.getMessage()); + } + return ResponseEntity.status(resolveStatus(ex.getCode())) + .body(ApiResponse.error(ex.getCode(), ex.getMessage())); } @ExceptionHandler(MethodArgumentNotValidException.class) - public ResponseEntity> handleValidationException(MethodArgumentNotValidException e) { - String detail = e.getBindingResult().getFieldErrors().stream() + public ResponseEntity> handleValidation(MethodArgumentNotValidException ex, HttpServletRequest request) { + String detail = ex.getBindingResult().getFieldErrors().stream() .map(f -> f.getField() + ": " + f.getDefaultMessage()) .reduce((a, b) -> a + "; " + b) - .orElse("Invalid request"); + .orElse("参数错误"); + log.warn("[{}] {} validation failed: {}", request.getMethod(), request.getRequestURI(), detail); return ResponseEntity.badRequest().body(ApiResponse.badRequest(detail)); } + + @ExceptionHandler(MissingServletRequestParameterException.class) + public ResponseEntity> handleMissingParam(MissingServletRequestParameterException ex, HttpServletRequest request) { + log.warn("[{}] {} missing param: {}", request.getMethod(), request.getRequestURI(), ex.getParameterName()); + return ResponseEntity.badRequest() + .body(ApiResponse.badRequest("缺少必填参数: " + ex.getParameterName())); + } + + @ExceptionHandler(IllegalArgumentException.class) + public ResponseEntity> handleIllegalArgument(IllegalArgumentException ex, HttpServletRequest request) { + log.warn("[{}] {} illegal argument: {}", request.getMethod(), request.getRequestURI(), ex.getMessage()); + return ResponseEntity.badRequest().body(ApiResponse.badRequest(ex.getMessage() == null ? "参数错误" : ex.getMessage())); + } + + @ExceptionHandler(HttpMessageNotReadableException.class) + public ResponseEntity> handleUnreadable(HttpMessageNotReadableException ex, HttpServletRequest request) { + log.warn("[{}] {} request body unreadable: {}", request.getMethod(), request.getRequestURI(), ex.getMessage()); + return ResponseEntity.badRequest().body(ApiResponse.badRequest("请求体格式错误")); + } + + @ExceptionHandler(HttpRequestMethodNotSupportedException.class) + public ResponseEntity> handleMethodNotSupported(HttpRequestMethodNotSupportedException ex, HttpServletRequest request) { + log.warn("[{}] {} method not supported: {}", request.getMethod(), request.getRequestURI(), ex.getMethod()); + return ResponseEntity.status(HttpStatus.METHOD_NOT_ALLOWED) + .body(ApiResponse.error(405, "请求方法不支持: " + ex.getMethod())); + } + + @ExceptionHandler(AuthorizationDeniedException.class) + public ResponseEntity> handleAuthorizationDenied(AuthorizationDeniedException ex, HttpServletRequest request) { + Authentication auth = SecurityContextHolder.getContext().getAuthentication(); + String principal = auth == null ? "anonymous" : String.valueOf(auth.getPrincipal()); + String authorities = auth == null ? "[]" : auth.getAuthorities().toString(); + log.warn("[{}] {} authorization denied: principal={} authorities={} reason={}", + request.getMethod(), request.getRequestURI(), principal, authorities, ex.getMessage()); + return ResponseEntity.status(HttpStatus.FORBIDDEN) + .body(ApiResponse.error(403, "权限不足")); + } + + @ExceptionHandler(Exception.class) + public ResponseEntity> handleException(Exception ex, HttpServletRequest request) { + log.error("[{}] {} unhandled exception: {}", request.getMethod(), request.getRequestURI(), ex.getMessage(), ex); + return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR) + .body(ApiResponse.error(500, "服务器内部错误")); + } + + private HttpStatus resolveStatus(int code) { + return switch (code) { + case 400 -> HttpStatus.BAD_REQUEST; + case 401 -> HttpStatus.UNAUTHORIZED; + case 403 -> HttpStatus.FORBIDDEN; + case 404 -> HttpStatus.NOT_FOUND; + default -> HttpStatus.INTERNAL_SERVER_ERROR; + }; + } } diff --git a/push-service/src/main/java/com/xuqm/push/controller/GlobalExceptionHandler.java b/push-service/src/main/java/com/xuqm/push/controller/GlobalExceptionHandler.java index fd594d0..bfe8085 100644 --- a/push-service/src/main/java/com/xuqm/push/controller/GlobalExceptionHandler.java +++ b/push-service/src/main/java/com/xuqm/push/controller/GlobalExceptionHandler.java @@ -2,9 +2,18 @@ package com.xuqm.push.controller; import com.xuqm.common.exception.BusinessException; import com.xuqm.common.model.ApiResponse; +import jakarta.servlet.http.HttpServletRequest; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; +import org.springframework.http.converter.HttpMessageNotReadableException; +import org.springframework.security.authorization.AuthorizationDeniedException; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.web.HttpRequestMethodNotSupportedException; +import org.springframework.web.bind.MissingServletRequestParameterException; +import org.springframework.web.bind.MethodArgumentNotValidException; import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.RestControllerAdvice; @@ -14,15 +23,77 @@ public class GlobalExceptionHandler { private static final Logger log = LoggerFactory.getLogger(GlobalExceptionHandler.class); @ExceptionHandler(BusinessException.class) - public ResponseEntity> handleBusiness(BusinessException ex) { - return ResponseEntity.status(ex.getCode()) + public ResponseEntity> handleBusiness(BusinessException ex, HttpServletRequest request) { + if (ex.getCode() >= 500) { + log.error("[{}] {} code={} msg={}", request.getMethod(), request.getRequestURI(), ex.getCode(), ex.getMessage(), ex); + } else { + log.warn("[{}] {} code={} msg={}", request.getMethod(), request.getRequestURI(), ex.getCode(), ex.getMessage()); + } + return ResponseEntity.status(resolveStatus(ex.getCode())) .body(ApiResponse.error(ex.getCode(), ex.getMessage())); } + @ExceptionHandler(MethodArgumentNotValidException.class) + public ResponseEntity> handleValidation(MethodArgumentNotValidException ex, HttpServletRequest request) { + String message = ex.getBindingResult().getFieldErrors().stream() + .findFirst() + .map(error -> error.getDefaultMessage()) + .orElse("参数错误"); + log.warn("[{}] {} validation failed: {}", request.getMethod(), request.getRequestURI(), message); + return ResponseEntity.badRequest().body(ApiResponse.badRequest(message)); + } + + @ExceptionHandler(MissingServletRequestParameterException.class) + public ResponseEntity> handleMissingParam(MissingServletRequestParameterException ex, HttpServletRequest request) { + log.warn("[{}] {} missing param: {}", request.getMethod(), request.getRequestURI(), ex.getParameterName()); + return ResponseEntity.badRequest() + .body(ApiResponse.badRequest("缺少必填参数: " + ex.getParameterName())); + } + + @ExceptionHandler(IllegalArgumentException.class) + public ResponseEntity> handleIllegalArgument(IllegalArgumentException ex, HttpServletRequest request) { + log.warn("[{}] {} illegal argument: {}", request.getMethod(), request.getRequestURI(), ex.getMessage()); + return ResponseEntity.badRequest().body(ApiResponse.badRequest(ex.getMessage() == null ? "参数错误" : ex.getMessage())); + } + + @ExceptionHandler(HttpMessageNotReadableException.class) + public ResponseEntity> handleUnreadable(HttpMessageNotReadableException ex, HttpServletRequest request) { + log.warn("[{}] {} request body unreadable: {}", request.getMethod(), request.getRequestURI(), ex.getMessage()); + return ResponseEntity.badRequest().body(ApiResponse.badRequest("请求体格式错误")); + } + + @ExceptionHandler(HttpRequestMethodNotSupportedException.class) + public ResponseEntity> handleMethodNotSupported(HttpRequestMethodNotSupportedException ex, HttpServletRequest request) { + log.warn("[{}] {} method not supported: {}", request.getMethod(), request.getRequestURI(), ex.getMethod()); + return ResponseEntity.status(HttpStatus.METHOD_NOT_ALLOWED) + .body(ApiResponse.error(405, "请求方法不支持: " + ex.getMethod())); + } + + @ExceptionHandler(AuthorizationDeniedException.class) + public ResponseEntity> handleAuthorizationDenied(AuthorizationDeniedException ex, HttpServletRequest request) { + Authentication auth = SecurityContextHolder.getContext().getAuthentication(); + String principal = auth == null ? "anonymous" : String.valueOf(auth.getPrincipal()); + String authorities = auth == null ? "[]" : auth.getAuthorities().toString(); + log.warn("[{}] {} authorization denied: principal={} authorities={} reason={}", + request.getMethod(), request.getRequestURI(), principal, authorities, ex.getMessage()); + return ResponseEntity.status(HttpStatus.FORBIDDEN) + .body(ApiResponse.error(403, "权限不足")); + } + @ExceptionHandler(Exception.class) - public ResponseEntity> handleUnexpected(Exception ex) { - log.error("Unexpected error", ex); - return ResponseEntity.status(500) - .body(ApiResponse.error(500, ex.getClass().getSimpleName() + ": " + ex.getMessage())); + public ResponseEntity> handleUnexpected(Exception ex, HttpServletRequest request) { + log.error("[{}] {} unhandled exception: {}", request.getMethod(), request.getRequestURI(), ex.getMessage(), ex); + return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR) + .body(ApiResponse.error(500, "服务器内部错误")); + } + + private HttpStatus resolveStatus(int code) { + return switch (code) { + case 400 -> HttpStatus.BAD_REQUEST; + case 401 -> HttpStatus.UNAUTHORIZED; + case 403 -> HttpStatus.FORBIDDEN; + case 404 -> HttpStatus.NOT_FOUND; + default -> HttpStatus.INTERNAL_SERVER_ERROR; + }; } } diff --git a/tenant-service/src/main/java/com/xuqm/tenant/controller/DatabaseController.java b/tenant-service/src/main/java/com/xuqm/tenant/controller/DatabaseController.java new file mode 100644 index 0000000..e4d2389 --- /dev/null +++ b/tenant-service/src/main/java/com/xuqm/tenant/controller/DatabaseController.java @@ -0,0 +1,192 @@ +package com.xuqm.tenant.controller; + +import com.xuqm.tenant.config.PrivateDeploymentProperties; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +import javax.sql.DataSource; +import java.sql.Connection; +import java.sql.DatabaseMetaData; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.ResultSetMetaData; +import java.sql.SQLException; +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +@RestController +@RequestMapping("/api/system/database") +public class DatabaseController { + + private final PrivateDeploymentProperties deployProps; + private final DataSource dataSource; + + public DatabaseController(PrivateDeploymentProperties deployProps, DataSource dataSource) { + this.deployProps = deployProps; + this.dataSource = dataSource; + } + + @GetMapping("/tables") + public ResponseEntity listTables() { + if (!deployProps.isPrivate()) { + return ResponseEntity.status(403).body(Map.of("message", "此接口仅在私有化部署可用")); + } + try (Connection conn = dataSource.getConnection()) { + DatabaseMetaData meta = conn.getMetaData(); + String catalog = conn.getCatalog(); + ResultSet rs = meta.getTables(catalog, null, "%", new String[]{"TABLE"}); + List> tables = new ArrayList<>(); + while (rs.next()) { + Map t = new LinkedHashMap<>(); + t.put("name", rs.getString("TABLE_NAME")); + t.put("comment", rs.getString("REMARKS")); + tables.add(t); + } + return ResponseEntity.ok(Map.of("data", tables)); + } catch (SQLException e) { + return ResponseEntity.status(500).body(Map.of("message", "查询表列表失败: " + e.getMessage())); + } + } + + @GetMapping("/tables/{tableName}/columns") + public ResponseEntity listColumns(@PathVariable String tableName) { + if (!deployProps.isPrivate()) { + return ResponseEntity.status(403).body(Map.of("message", "此接口仅在私有化部署可用")); + } + if (!isAllowedTable(tableName)) { + return ResponseEntity.status(403).body(Map.of("message", "不允许访问该表")); + } + try (Connection conn = dataSource.getConnection()) { + DatabaseMetaData meta = conn.getMetaData(); + String catalog = conn.getCatalog(); + ResultSet rs = meta.getColumns(catalog, null, tableName, "%"); + List> columns = new ArrayList<>(); + while (rs.next()) { + Map c = new LinkedHashMap<>(); + c.put("name", rs.getString("COLUMN_NAME")); + c.put("type", rs.getString("TYPE_NAME")); + c.put("size", rs.getInt("COLUMN_SIZE")); + c.put("nullable", rs.getInt("NULLABLE") == DatabaseMetaData.columnNullable); + c.put("comment", rs.getString("REMARKS")); + columns.add(c); + } + return ResponseEntity.ok(Map.of("data", columns)); + } catch (SQLException e) { + return ResponseEntity.status(500).body(Map.of("message", "查询列信息失败: " + e.getMessage())); + } + } + + @GetMapping("/tables/{tableName}/data") + public ResponseEntity queryData( + @PathVariable String tableName, + @RequestParam(defaultValue = "0") int page, + @RequestParam(defaultValue = "50") int size, + @RequestParam(required = false) String keyword, + @RequestParam(required = false) String sortColumn, + @RequestParam(required = false, defaultValue = "ASC") String sortDirection) { + if (!deployProps.isPrivate()) { + return ResponseEntity.status(403).body(Map.of("message", "此接口仅在私有化部署可用")); + } + if (!isAllowedTable(tableName)) { + return ResponseEntity.status(403).body(Map.of("message", "不允许访问该表")); + } + if (size > 200) size = 200; + + try (Connection conn = dataSource.getConnection()) { + // Get columns info for keyword search + List textColumns = new ArrayList<>(); + DatabaseMetaData meta = conn.getMetaData(); + String catalog = conn.getCatalog(); + ResultSet colRs = meta.getColumns(catalog, null, tableName, "%"); + List allColumns = new ArrayList<>(); + while (colRs.next()) { + String colName = colRs.getString("COLUMN_NAME"); + String typeName = colRs.getString("TYPE_NAME"); + allColumns.add(colName); + if (typeName != null && (typeName.contains("CHAR") || typeName.contains("TEXT") || typeName.contains("VARCHAR"))) { + textColumns.add(colName); + } + } + + // Build WHERE clause for keyword search + StringBuilder whereClause = new StringBuilder(); + List params = new ArrayList<>(); + if (keyword != null && !keyword.isBlank() && !textColumns.isEmpty()) { + whereClause.append(" WHERE "); + for (int i = 0; i < textColumns.size(); i++) { + if (i > 0) whereClause.append(" OR "); + String safeCol = sanitizeIdentifier(textColumns.get(i)); + whereClause.append(safeCol).append(" LIKE ?"); + params.add("%" + keyword + "%"); + } + } + + // Count total + String countSql = "SELECT COUNT(*) FROM " + sanitizeIdentifier(tableName) + whereClause; + long total = 0; + try (PreparedStatement ps = conn.prepareStatement(countSql)) { + for (int i = 0; i < params.size(); i++) ps.setObject(i + 1, params.get(i)); + try (ResultSet rs = ps.executeQuery()) { + if (rs.next()) total = rs.getLong(1); + } + } + + // Build ORDER BY + String orderClause = ""; + if (sortColumn != null && !sortColumn.isBlank() && allColumns.contains(sortColumn)) { + String dir = "DESC".equalsIgnoreCase(sortDirection) ? "DESC" : "ASC"; + orderClause = " ORDER BY " + sanitizeIdentifier(sortColumn) + " " + dir; + } + + // Query data with pagination + String dataSql = "SELECT * FROM " + sanitizeIdentifier(tableName) + whereClause + orderClause + + " LIMIT ? OFFSET ?"; + List> rows = new ArrayList<>(); + try (PreparedStatement ps = conn.prepareStatement(dataSql)) { + int idx = 1; + for (Object p : params) ps.setObject(idx++, p); + ps.setInt(idx++, size); + ps.setInt(idx, page * size); + try (ResultSet rs = ps.executeQuery()) { + ResultSetMetaData rsmd = rs.getMetaData(); + int colCount = rsmd.getColumnCount(); + while (rs.next()) { + Map row = new LinkedHashMap<>(); + for (int i = 1; i <= colCount; i++) { + row.put(rsmd.getColumnName(i), rs.getObject(i)); + } + rows.add(row); + } + } + } + + int totalPages = size > 0 ? (int) Math.ceil((double) total / size) : 0; + Map result = new LinkedHashMap<>(); + result.put("columns", allColumns); + result.put("rows", rows); + result.put("total", total); + result.put("totalPages", totalPages); + result.put("page", page); + result.put("size", size); + return ResponseEntity.ok(Map.of("data", result)); + } catch (SQLException e) { + return ResponseEntity.status(500).body(Map.of("message", "查询数据失败: " + e.getMessage())); + } + } + + private boolean isAllowedTable(String tableName) { + // Allow any table in the current database - the table name comes from our own listTables() API + return tableName != null && tableName.matches("[a-zA-Z0-9_]+"); + } + + private static String sanitizeIdentifier(String identifier) { + // Backtick-quote to prevent SQL injection + return "`" + identifier.replace("`", "``") + "`"; + } +} diff --git a/tenant-service/src/main/java/com/xuqm/tenant/controller/GlobalExceptionHandler.java b/tenant-service/src/main/java/com/xuqm/tenant/controller/GlobalExceptionHandler.java index 5adfd90..5d39700 100644 --- a/tenant-service/src/main/java/com/xuqm/tenant/controller/GlobalExceptionHandler.java +++ b/tenant-service/src/main/java/com/xuqm/tenant/controller/GlobalExceptionHandler.java @@ -2,12 +2,18 @@ package com.xuqm.tenant.controller; import com.xuqm.common.exception.BusinessException; import com.xuqm.common.model.ApiResponse; +import jakarta.servlet.http.HttpServletRequest; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.http.converter.HttpMessageNotReadableException; +import org.springframework.security.authorization.AuthorizationDeniedException; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.validation.FieldError; +import org.springframework.web.HttpRequestMethodNotSupportedException; +import org.springframework.web.bind.MissingServletRequestParameterException; import org.springframework.web.bind.MethodArgumentNotValidException; import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.RestControllerAdvice; @@ -20,33 +26,66 @@ public class GlobalExceptionHandler { private static final Logger log = LoggerFactory.getLogger(GlobalExceptionHandler.class); @ExceptionHandler(BusinessException.class) - public ResponseEntity> handle(BusinessException ex) { + public ResponseEntity> handle(BusinessException ex, HttpServletRequest request) { + if (ex.getCode() >= 500) { + log.error("[{}] {} code={} msg={}", request.getMethod(), request.getRequestURI(), ex.getCode(), ex.getMessage(), ex); + } else { + log.warn("[{}] {} code={} msg={}", request.getMethod(), request.getRequestURI(), ex.getCode(), ex.getMessage()); + } return ResponseEntity.status(resolveStatus(ex.getCode())) .body(ApiResponse.error(ex.getCode(), ex.getMessage())); } @ExceptionHandler(MethodArgumentNotValidException.class) - public ResponseEntity> handle(MethodArgumentNotValidException ex) { + public ResponseEntity> handle(MethodArgumentNotValidException ex, HttpServletRequest request) { String message = ex.getBindingResult().getFieldErrors().stream() .map(FieldError::getDefaultMessage) .collect(Collectors.joining("; ")); + log.warn("[{}] {} validation failed: {}", request.getMethod(), request.getRequestURI(), message); return ResponseEntity.badRequest().body(ApiResponse.badRequest(message)); } + @ExceptionHandler(MissingServletRequestParameterException.class) + public ResponseEntity> handle(MissingServletRequestParameterException ex, HttpServletRequest request) { + log.warn("[{}] {} missing param: {}", request.getMethod(), request.getRequestURI(), ex.getParameterName()); + return ResponseEntity.badRequest() + .body(ApiResponse.badRequest("缺少必填参数: " + ex.getParameterName())); + } + @ExceptionHandler(IllegalArgumentException.class) - public ResponseEntity> handle(IllegalArgumentException ex) { + public ResponseEntity> handle(IllegalArgumentException ex, HttpServletRequest request) { + log.warn("[{}] {} illegal argument: {}", request.getMethod(), request.getRequestURI(), ex.getMessage()); return ResponseEntity.badRequest() .body(ApiResponse.badRequest(ex.getMessage() == null ? "参数错误" : ex.getMessage())); } @ExceptionHandler(HttpMessageNotReadableException.class) - public ResponseEntity> handle(HttpMessageNotReadableException ex) { + public ResponseEntity> handle(HttpMessageNotReadableException ex, HttpServletRequest request) { + log.warn("[{}] {} request body unreadable: {}", request.getMethod(), request.getRequestURI(), ex.getMessage()); return ResponseEntity.badRequest().body(ApiResponse.badRequest("请求体格式错误")); } + @ExceptionHandler(HttpRequestMethodNotSupportedException.class) + public ResponseEntity> handle(HttpRequestMethodNotSupportedException ex, HttpServletRequest request) { + log.warn("[{}] {} method not supported: {}", request.getMethod(), request.getRequestURI(), ex.getMethod()); + return ResponseEntity.status(HttpStatus.METHOD_NOT_ALLOWED) + .body(ApiResponse.error(405, "请求方法不支持: " + ex.getMethod())); + } + + @ExceptionHandler(AuthorizationDeniedException.class) + public ResponseEntity> handle(AuthorizationDeniedException ex, HttpServletRequest request) { + Authentication auth = SecurityContextHolder.getContext().getAuthentication(); + String principal = auth == null ? "anonymous" : String.valueOf(auth.getPrincipal()); + String authorities = auth == null ? "[]" : auth.getAuthorities().toString(); + log.warn("[{}] {} authorization denied: principal={} authorities={} reason={}", + request.getMethod(), request.getRequestURI(), principal, authorities, ex.getMessage()); + return ResponseEntity.status(HttpStatus.FORBIDDEN) + .body(ApiResponse.error(403, "权限不足")); + } + @ExceptionHandler(Exception.class) - public ResponseEntity> handle(Exception ex) { - log.error("Unhandled exception", ex); + public ResponseEntity> handle(Exception ex, HttpServletRequest request) { + log.error("[{}] {} unhandled exception: {}", request.getMethod(), request.getRequestURI(), ex.getMessage(), ex); return ResponseEntity.internalServerError() .body(ApiResponse.error(500, "服务器内部错误")); } diff --git a/update-service/src/main/java/com/xuqm/update/controller/GlobalExceptionHandler.java b/update-service/src/main/java/com/xuqm/update/controller/GlobalExceptionHandler.java index 6238145..4673d3c 100644 --- a/update-service/src/main/java/com/xuqm/update/controller/GlobalExceptionHandler.java +++ b/update-service/src/main/java/com/xuqm/update/controller/GlobalExceptionHandler.java @@ -2,11 +2,17 @@ package com.xuqm.update.controller; import com.xuqm.common.exception.BusinessException; import com.xuqm.common.model.ApiResponse; +import jakarta.servlet.http.HttpServletRequest; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.http.converter.HttpMessageNotReadableException; +import org.springframework.security.authorization.AuthorizationDeniedException; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.web.HttpRequestMethodNotSupportedException; +import org.springframework.web.bind.MissingServletRequestParameterException; import org.springframework.web.bind.MethodArgumentNotValidException; import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.RestControllerAdvice; @@ -19,33 +25,66 @@ public class GlobalExceptionHandler { private static final Logger log = LoggerFactory.getLogger(GlobalExceptionHandler.class); @ExceptionHandler(BusinessException.class) - public ResponseEntity> handle(BusinessException ex) { + public ResponseEntity> handle(BusinessException ex, HttpServletRequest request) { + if (ex.getCode() >= 500) { + log.error("[{}] {} code={} msg={}", request.getMethod(), request.getRequestURI(), ex.getCode(), ex.getMessage(), ex); + } else { + log.warn("[{}] {} code={} msg={}", request.getMethod(), request.getRequestURI(), ex.getCode(), ex.getMessage()); + } return ResponseEntity.status(resolveStatus(ex.getCode())) .body(ApiResponse.error(ex.getCode(), ex.getMessage())); } @ExceptionHandler(MethodArgumentNotValidException.class) - public ResponseEntity> handle(MethodArgumentNotValidException ex) { + public ResponseEntity> handle(MethodArgumentNotValidException ex, HttpServletRequest request) { String message = ex.getBindingResult().getFieldErrors().stream() .map(f -> f.getDefaultMessage()) .collect(Collectors.joining("; ")); + log.warn("[{}] {} validation failed: {}", request.getMethod(), request.getRequestURI(), message); return ResponseEntity.badRequest().body(ApiResponse.badRequest(message)); } + @ExceptionHandler(MissingServletRequestParameterException.class) + public ResponseEntity> handle(MissingServletRequestParameterException ex, HttpServletRequest request) { + log.warn("[{}] {} missing param: {}", request.getMethod(), request.getRequestURI(), ex.getParameterName()); + return ResponseEntity.badRequest() + .body(ApiResponse.badRequest("缺少必填参数: " + ex.getParameterName())); + } + @ExceptionHandler(IllegalArgumentException.class) - public ResponseEntity> handle(IllegalArgumentException ex) { + public ResponseEntity> handle(IllegalArgumentException ex, HttpServletRequest request) { + log.warn("[{}] {} illegal argument: {}", request.getMethod(), request.getRequestURI(), ex.getMessage()); return ResponseEntity.badRequest() .body(ApiResponse.badRequest(ex.getMessage() == null ? "参数错误" : ex.getMessage())); } @ExceptionHandler(HttpMessageNotReadableException.class) - public ResponseEntity> handle(HttpMessageNotReadableException ex) { + public ResponseEntity> handle(HttpMessageNotReadableException ex, HttpServletRequest request) { + log.warn("[{}] {} request body unreadable: {}", request.getMethod(), request.getRequestURI(), ex.getMessage()); return ResponseEntity.badRequest().body(ApiResponse.badRequest("请求体格式错误")); } + @ExceptionHandler(HttpRequestMethodNotSupportedException.class) + public ResponseEntity> handle(HttpRequestMethodNotSupportedException ex, HttpServletRequest request) { + log.warn("[{}] {} method not supported: {}", request.getMethod(), request.getRequestURI(), ex.getMethod()); + return ResponseEntity.status(HttpStatus.METHOD_NOT_ALLOWED) + .body(ApiResponse.error(405, "请求方法不支持: " + ex.getMethod())); + } + + @ExceptionHandler(AuthorizationDeniedException.class) + public ResponseEntity> handle(AuthorizationDeniedException ex, HttpServletRequest request) { + Authentication auth = SecurityContextHolder.getContext().getAuthentication(); + String principal = auth == null ? "anonymous" : String.valueOf(auth.getPrincipal()); + String authorities = auth == null ? "[]" : auth.getAuthorities().toString(); + log.warn("[{}] {} authorization denied: principal={} authorities={} reason={}", + request.getMethod(), request.getRequestURI(), principal, authorities, ex.getMessage()); + return ResponseEntity.status(HttpStatus.FORBIDDEN) + .body(ApiResponse.error(403, "权限不足")); + } + @ExceptionHandler(Exception.class) - public ResponseEntity> handle(Exception ex) { - log.error("Unhandled exception", ex); + public ResponseEntity> handle(Exception ex, HttpServletRequest request) { + log.error("[{}] {} unhandled exception: {}", request.getMethod(), request.getRequestURI(), ex.getMessage(), ex); return ResponseEntity.internalServerError() .body(ApiResponse.error(500, "服务器内部错误")); }