cc 3 tygodni temu
rodzic
commit
af76ce6f98

+ 77 - 1
ai-server/src/main/java/com/zsjz/ai/common/config/WebConfig.java

@@ -1,9 +1,26 @@
 package com.zsjz.ai.common.config;
 
+import com.fasterxml.jackson.annotation.JsonAutoDetect;
+import com.fasterxml.jackson.annotation.JsonTypeInfo;
+import com.fasterxml.jackson.annotation.PropertyAccessor;
 import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.SerializationFeature;
+import com.fasterxml.jackson.databind.jsontype.impl.LaissezFaireSubTypeValidator;
+import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
 import com.zsjz.ai.common.utils.Json;
+import org.springframework.cache.CacheManager;
 import org.springframework.context.annotation.Bean;
 import org.springframework.context.annotation.Configuration;
+import org.springframework.data.redis.cache.RedisCacheConfiguration;
+import org.springframework.data.redis.cache.RedisCacheManager;
+import org.springframework.data.redis.connection.RedisConnectionFactory;
+import org.springframework.data.redis.core.RedisTemplate;
+import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer;
+import org.springframework.data.redis.serializer.RedisSerializationContext;
+import org.springframework.data.redis.serializer.RedisSerializer;
+import org.springframework.data.redis.serializer.StringRedisSerializer;
+
+import java.time.Duration;
 
 @Configuration
 public class WebConfig {
@@ -22,8 +39,67 @@ public class WebConfig {
 
     @Bean
     public ObjectMapper objectMapper() {
-      return Json.objectMapper();
+        return Json.objectMapper();
+    }
+
+    @Bean
+    public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) {
+        RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();
+        redisTemplate.setConnectionFactory(redisConnectionFactory);
+        // 用Jackson2JsonRedisSerializer来序列化和反序列化redis的value值
+        redisTemplate.setValueSerializer(serializer());
+
+        StringRedisSerializer stringRedisSerializer = new StringRedisSerializer();
+        // 使用StringRedisSerializer来序列化和反序列化redis的key值
+        redisTemplate.setKeySerializer(stringRedisSerializer);
+
+        // hash的key也采用String的序列化方式
+        redisTemplate.setHashKeySerializer(stringRedisSerializer);
+        // hash的value序列化方式采用jackson
+        redisTemplate.setHashValueSerializer(serializer());
+        redisTemplate.afterPropertiesSet();
+        return redisTemplate;
     }
 
 
+    @Bean
+    public CacheManager cacheManager(RedisConnectionFactory factory) {
+        RedisSerializer<String> redisSerializer = new StringRedisSerializer();
+        // 配置序列化(解决乱码的问题)
+        RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig()
+                // 缓存有效期
+                .entryTtl(Duration.ZERO)
+                // 使用StringRedisSerializer来序列化和反序列化redis的key值
+                .serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(redisSerializer))
+                // 使用Jackson2JsonRedisSerializer来序列化和反序列化redis的value值
+                .serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(serializer()))
+                // 禁用空值
+                .disableCachingNullValues();
+
+        return RedisCacheManager.builder(factory)
+                .cacheDefaults(config)
+                .build();
+    }
+
+    /**
+     * 配置Jackson2JsonRedisSerializer序列化策略
+     *
+     */
+    private GenericJackson2JsonRedisSerializer serializer() {
+        // 使用Jackson2JsonRedisSerializer来序列化和反序列化redis的value值
+        ObjectMapper objectMapper = new ObjectMapper();
+        // 指定要序列化的域,field,get和set,以及修饰符范围,ANY是都有包括private和public
+        objectMapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
+        // 指定序列化输入的类型,类必须是非final修饰的,final修饰的类,比如String,Integer等会跑出异常
+        objectMapper.activateDefaultTyping(LaissezFaireSubTypeValidator.instance, ObjectMapper.DefaultTyping.NON_FINAL);
+        ObjectMapper om = new ObjectMapper();
+        om.registerModule(new JavaTimeModule());
+        om.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);
+        om.activateDefaultTyping(
+                om.getPolymorphicTypeValidator(),
+                ObjectMapper.DefaultTyping.NON_FINAL,
+                JsonTypeInfo.As.PROPERTY);
+        return  new GenericJackson2JsonRedisSerializer(om);
+    }
+
 }

+ 85 - 28
ai-server/src/main/java/com/zsjz/ai/common/exception/GlobalExceptionHandler.java

@@ -1,16 +1,23 @@
 package com.zsjz.ai.common.exception;
 
-import cn.dev33.satoken.exception.NotLoginException;
-import cn.dev33.satoken.exception.NotRoleException;
 import com.zsjz.ai.common.config.Result;
+import jakarta.validation.ConstraintViolationException;
 import lombok.extern.slf4j.Slf4j;
 import org.springframework.context.support.DefaultMessageSourceResolvable;
 import org.springframework.http.HttpStatus;
 import org.springframework.http.ResponseEntity;
 import org.springframework.http.converter.HttpMessageNotReadableException;
+import org.springframework.validation.BindException;
+import org.springframework.web.HttpMediaTypeNotSupportedException;
+import org.springframework.web.HttpRequestMethodNotSupportedException;
 import org.springframework.web.bind.MethodArgumentNotValidException;
 import org.springframework.web.bind.annotation.ExceptionHandler;
+import org.springframework.web.bind.annotation.PathVariable;
 import org.springframework.web.bind.annotation.RestControllerAdvice;
+import org.springframework.web.method.annotation.MethodArgumentTypeMismatchException;
+import org.springframework.web.multipart.MaxUploadSizeExceededException;
+import org.springframework.web.servlet.NoHandlerFoundException;
+import org.springframework.web.servlet.resource.NoResourceFoundException;
 
 /**
  * 全局异常处理器:统一错误响应格式为 Result。
@@ -29,23 +36,6 @@ public class GlobalExceptionHandler {
                 .body(Result.failure(e.getCode(), e.getMessage()));
     }
 
-    /**
-     * sa-token 未登录:返回 401
-     */
-    @ExceptionHandler(NotLoginException.class)
-    public ResponseEntity<Result<Void>> handleNotLogin(NotLoginException e) {
-        return ResponseEntity.status(HttpStatus.UNAUTHORIZED)
-                .body(Result.failure(401, "未登录或登录已过期"));
-    }
-
-    /**
-     * sa-token 角色不足:返回 403
-     */
-    @ExceptionHandler(NotRoleException.class)
-    public ResponseEntity<Result<Void>> handleNotRole(NotRoleException e) {
-        return ResponseEntity.status(HttpStatus.FORBIDDEN)
-                .body(Result.failure(403, "无权限执行该操作"));
-    }
 
     /**
      * 参数校验异常:@Valid 校验失败
@@ -61,15 +51,6 @@ public class GlobalExceptionHandler {
         return ResponseEntity.badRequest().body(Result.failure(400, message));
     }
 
-    /**
-     * 请求体解析异常:JSON 格式错误
-     */
-    @ExceptionHandler(HttpMessageNotReadableException.class)
-    public ResponseEntity<Result<Void>> handleHttpMessageNotReadable(HttpMessageNotReadableException e) {
-        log.warn("请求体解析失败: {}", e.getMessage());
-        return ResponseEntity.badRequest().body(Result.failure(400, "请求体格式错误"));
-    }
-
     /**
      * 兜底异常:记录完整日志,不向客户端泄露内部细节
      */
@@ -80,6 +61,82 @@ public class GlobalExceptionHandler {
                 .body(Result.failure(500, "系统异常,请稍后重试"));
     }
 
+    @ExceptionHandler(BindException.class)
+    public ResponseEntity<Result<Void>> handleBind(BindException e) {
+        String message = e.getFieldErrors().stream()
+                .map(fe -> fe.getField() + ": " + fe.getDefaultMessage())
+                .findFirst().orElse("参数绑定失败");
+        log.info("参数绑定失败: {}", message);
+        return ResponseEntity.status(HttpStatus.BAD_REQUEST)
+                .body(Result.failure(400, message));
+    }
+
+    @ExceptionHandler(ConstraintViolationException.class)
+    public ResponseEntity<Result<Void>> handleConstraint(ConstraintViolationException e) {
+        log.info("约束校验失败: {}", e.getMessage());
+        return ResponseEntity.status(HttpStatus.BAD_REQUEST)
+                .body(Result.failure(400, e.getMessage()));
+    }
+
+
+    @ExceptionHandler(HttpMessageNotReadableException.class)
+    public ResponseEntity<Result<Void>> handleNotReadable(
+            org.springframework.http.converter.HttpMessageNotReadableException e) {
+        log.info("请求体解析失败: {}", e.getMessage());
+        return ResponseEntity.status(HttpStatus.BAD_REQUEST)
+                .body(Result.failure(400, "请求体格式错误"));
+    }
+
+    @ExceptionHandler(NoHandlerFoundException.class)
+    public ResponseEntity<Result<Void>> handleNoHandler(NoHandlerFoundException e) {
+        log.info("接口不存在: {} {}", e.getHttpMethod(), e.getRequestURL());
+        return ResponseEntity.status(HttpStatus.NOT_FOUND)
+                .body(Result.failure(404, "接口不存在"));
+    }
+
+    @ExceptionHandler(NoResourceFoundException.class)
+    public ResponseEntity<Result<Void>> handleNoResource(NoResourceFoundException e) {
+        log.info("资源不存在: {}", e.getMessage());
+        return ResponseEntity.status(HttpStatus.NOT_FOUND)
+                .body(Result.failure(404, "资源不存在"));
+    }
+
+    @ExceptionHandler(HttpRequestMethodNotSupportedException.class)
+    public ResponseEntity<Result<Void>> handleMethodNotAllowed(
+            HttpRequestMethodNotSupportedException e) {
+        log.info("请求方法不支持: method={}", e.getMethod());
+        return ResponseEntity.status(HttpStatus.METHOD_NOT_ALLOWED)
+                .body(Result.failure(405, "请求方法不支持"));
+    }
+
+    @ExceptionHandler(HttpMediaTypeNotSupportedException.class)
+    public ResponseEntity<Result<Void>> handleMediaTypeNotSupported(
+            HttpMediaTypeNotSupportedException e) {
+        log.info("不支持的媒体类型: {}", e.getContentType());
+        return ResponseEntity.status(HttpStatus.UNSUPPORTED_MEDIA_TYPE)
+                .body(Result.failure(415, "不支持的Content-Type"));
+    }
+
+    @ExceptionHandler(MaxUploadSizeExceededException.class)
+    public ResponseEntity<Result<Void>> handleMaxUploadSize(MaxUploadSizeExceededException e) {
+        return ResponseEntity.status(HttpStatus.BAD_REQUEST)
+                .body(Result.failure(400, "上传文件大小超过限制"));
+    }
+
+    @ExceptionHandler(MethodArgumentTypeMismatchException.class)
+    public ResponseEntity<Result<Void>> handleTypeMismatch(MethodArgumentTypeMismatchException e) {
+        // 路径变量转换失败视为资源不存在(如超长/非法 ID 无法匹配任何资源),复用前端"资源不存在"识别逻辑
+        boolean pathVariable = e.getParameter().hasParameterAnnotation(PathVariable.class);
+        if (pathVariable) {
+            log.info("路径参数无效: name={}, value={}", e.getName(), e.getValue());
+            return ResponseEntity.status(HttpStatus.NOT_FOUND)
+                    .body(Result.failure(404, "资源不存在"));
+        }
+        log.info("请求参数格式错误: name={}, value={}", e.getName(), e.getValue());
+        return ResponseEntity.status(HttpStatus.BAD_REQUEST)
+                .body(Result.failure(400, "请求参数格式错误"));
+    }
+
     /**
      * 业务码映射为 HTTP 状态码,非法值按 400 处理
      */