# 情绪博物馆后端重构升级技术方案 ## 1. 升级概述 ### 1.1 升级目标 基于现有的Spring Boot 2.7.18单体架构,升级到Spring Boot 3.4.8版本,采用最新的技术栈和最佳实践,提升系统性能、安全性和可维护性。 ### 1.2 升级原则 - **向后兼容**: 保持现有API接口和业务逻辑不变 - **渐进升级**: 分阶段进行升级,确保系统稳定性 - **性能优先**: 充分利用Spring Boot 3.x的性能优化 - **安全增强**: 采用最新的安全特性和最佳实践 ## 2. 技术栈升级方案 ### 2.1 核心框架升级 #### 2.1.1 Spring Boot 3.4.8 ```xml org.springframework.boot spring-boot-starter-parent 3.4.8 21 21 21 ``` #### 2.1.2 核心依赖升级 ```xml org.springframework.boot spring-boot-starter-web org.springframework.boot spring-boot-starter-security org.springframework.boot spring-boot-starter-websocket org.springframework.boot spring-boot-starter-data-redis org.springframework.boot spring-boot-starter-webflux org.springframework.boot spring-boot-starter-web com.mysql mysql-connector-j runtime com.baomidou mybatis-plus-boot-starter 3.5.5 io.jsonwebtoken jjwt-api 0.12.3 io.jsonwebtoken jjwt-impl 0.12.3 runtime io.jsonwebtoken jjwt-jackson 0.12.3 runtime org.springdoc springdoc-openapi-starter-webmvc-ui 2.3.0 org.apache.commons commons-lang3 cn.hutool hutool-all 5.8.25 ``` ### 2.2 安全框架升级 #### 2.2.1 Spring Security 6.x配置 ```java @Configuration @EnableWebSecurity @EnableMethodSecurity(prePostEnabled = true) public class SecurityConfig { @Bean public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { http .csrf(csrf -> csrf.disable()) .cors(cors -> cors.configurationSource(corsConfigurationSource())) .sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS)) .authorizeHttpRequests(authz -> authz .requestMatchers("/auth/**", "/health/**", "/actuator/**", "/ws/**", "/ai/guest/**").permitAll() .requestMatchers("/swagger-ui/**", "/v3/api-docs/**").permitAll() .anyRequest().authenticated() ) .addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class); return http.build(); } @Bean public PasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(12); } } ``` #### 2.2.2 JWT升级到0.12.3 ```java @Component public class JwtUtil { private final SecretKey secretKey; private final JwtParser jwtParser; public JwtUtil(@Value("${emotion.jwt.secret}") String secret) { this.secretKey = Keys.hmacShaKeyFor(secret.getBytes(StandardCharsets.UTF_8)); this.jwtParser = Jwts.parser() .verifyWith(secretKey) .build(); } public String generateToken(String userId, String username) { return Jwts.builder() .subject(userId) .claim("username", username) .issuedAt(new Date()) .expiration(new Date(System.currentTimeMillis() + 86400000)) .signWith(secretKey) .compact(); } } ``` ### 2.3 异步处理优化 #### 2.3.1 异步配置升级 ```java @Configuration @EnableAsync public class AsyncConfig implements AsyncConfigurer { @Override public Executor getAsyncExecutor() { ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor(); executor.setCorePoolSize(20); executor.setMaxPoolSize(100); executor.setQueueCapacity(500); executor.setKeepAliveSeconds(60); executor.setThreadNamePrefix("emotion-async-"); executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy()); executor.initialize(); return executor; } @Bean("aiTaskExecutor") public Executor aiTaskExecutor() { ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor(); executor.setCorePoolSize(10); executor.setMaxPoolSize(50); executor.setQueueCapacity(200); executor.setThreadNamePrefix("ai-task-"); executor.initialize(); return executor; } } ``` ### 2.4 HTTP客户端配置 #### 2.4.1 WebClient配置 ```java @Configuration public class WebClientConfig { @Bean public WebClient.Builder webClientBuilder() { return WebClient.builder() .codecs(configurer -> configurer.defaultCodecs().maxInMemorySize(10 * 1024 * 1024)) .filter(ExchangeFilterFunction.ofRequestProcessor(clientRequest -> { log.debug("Request: {} {}", clientRequest.method(), clientRequest.url()); return Mono.just(clientRequest); })) .filter(ExchangeFilterFunction.ofResponseProcessor(clientResponse -> { log.debug("Response: {}", clientResponse.statusCode()); return Mono.just(clientResponse); })); } @Bean public RestTemplate restTemplate() { RestTemplate restTemplate = new RestTemplate(); // 设置超时时间 SimpleClientHttpRequestFactory factory = new SimpleClientHttpRequestFactory(); factory.setConnectTimeout(30000); factory.setReadTimeout(30000); restTemplate.setRequestFactory(factory); // 添加请求拦截器 restTemplate.getInterceptors().add(new ClientHttpRequestInterceptor() { @Override public ClientHttpResponse intercept(HttpRequest request, byte[] body, ClientHttpRequestExecution execution) throws IOException { log.debug("Request: {} {}", request.getMethod(), request.getURI()); ClientHttpResponse response = execution.execute(request, body); log.debug("Response: {}", response.getStatusCode()); return response; } }); return restTemplate; } } ``` ### 2.5 缓存优化 #### 2.5.1 Redis配置升级 ```java @Configuration @EnableCaching public class RedisConfig { @Bean public RedisTemplate redisTemplate(RedisConnectionFactory factory) { RedisTemplate template = new RedisTemplate<>(); template.setConnectionFactory(factory); Jackson2JsonRedisSerializer serializer = new Jackson2JsonRedisSerializer<>(Object.class); ObjectMapper mapper = new ObjectMapper(); mapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY); mapper.activateDefaultTyping(LaissezFaireSubTypeValidator.instance, ObjectMapper.DefaultTyping.NON_FINAL, JsonTypeInfo.As.PROPERTY); mapper.registerModule(new JavaTimeModule()); serializer.setObjectMapper(mapper); template.setKeySerializer(new StringRedisSerializer()); template.setValueSerializer(serializer); template.afterPropertiesSet(); return template; } @Bean public CacheManager cacheManager(RedisConnectionFactory factory) { RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig() .entryTtl(Duration.ofMinutes(30)) .serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(new StringRedisSerializer())) .serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(new GenericJackson2JsonRedisSerializer())); return RedisCacheManager.builder(factory) .cacheDefaults(config) .build(); } } ``` ## 3. 配置管理优化 ### 3.1 多环境配置 ```yaml # application.yml (主配置) spring: profiles: active: ${SPRING_PROFILES_ACTIVE:local} application: name: emotion-museum jackson: date-format: yyyy-MM-dd HH:mm:ss time-zone: GMT+8 serialization: write-dates-as-timestamps: false default-property-inclusion: non_null # application-local.yml (本地环境) server: port: 19089 servlet: context-path: /api spring: datasource: url: jdbc:mysql://localhost:3306/emotion_museum_local username: root password: password data: redis: host: localhost port: 6379 password: # application-prod.yml (生产环境) server: port: 19089 servlet: context-path: /api spring: datasource: url: jdbc:mysql://prod-db:3306/emotion_museum_prod username: ${MYSQL_USERNAME} password: ${MYSQL_PASSWORD} data: redis: host: ${REDIS_HOST} port: ${REDIS_PORT} password: ${REDIS_PASSWORD} # Coze API配置 emotion: coze: api-key: ${COZE_API_KEY} bot-id: ${COZE_BOT_ID} base-url: https://www.coze.cn/api timeout: 30000 max-retries: 3 ``` ### 3.2 数据库连接池优化 ```yaml spring: datasource: hikari: maximum-pool-size: 30 minimum-idle: 10 connection-timeout: 30000 idle-timeout: 600000 max-lifetime: 1800000 leak-detection-threshold: 60000 connection-test-query: SELECT 1 ``` ## 4. API文档升级 ### 4.1 SpringDoc OpenAPI 3配置 ```java @Configuration public class OpenApiConfig { @Bean public OpenAPI customOpenAPI() { return new OpenAPI() .info(new Info() .title("情绪博物馆API文档") .version("1.0.0") .description("情绪博物馆后端服务API文档") .contact(new Contact() .name("情绪博物馆团队") .email("support@emotion-museum.com"))) .addSecurityItem(new SecurityRequirement().addList("Bearer Authentication")) .components(new Components() .addSecuritySchemes("Bearer Authentication", new SecurityScheme() .type(SecurityScheme.Type.HTTP) .scheme("bearer") .bearerFormat("JWT"))); } } ``` ### 4.2 控制器文档注解 ```java @RestController @RequestMapping("/auth") @Tag(name = "认证管理", description = "用户认证相关接口") public class AuthController { @PostMapping("/login") @Operation(summary = "用户登录", description = "用户登录接口") @ApiResponses(value = { @ApiResponse(responseCode = "200", description = "登录成功"), @ApiResponse(responseCode = "400", description = "参数错误"), @ApiResponse(responseCode = "401", description = "认证失败") }) public Result login(@Valid @RequestBody LoginRequest request) { AuthResponse response = authService.login(request); return Result.success("登录成功", response); } } ``` ## 5. 监控和日志优化 ### 5.1 应用监控配置 ```yaml management: endpoints: web: exposure: include: health,info,metrics,prometheus endpoint: health: show-details: always metrics: enabled: true metrics: export: prometheus: enabled: true ``` ### 5.2 健康检查配置 ```java @Component public class CustomHealthIndicator implements HealthIndicator { @Autowired private RedisTemplate redisTemplate; @Autowired private DataSource dataSource; @Override public Health health() { try { // 检查Redis连接 redisTemplate.opsForValue().get("health_check"); // 检查数据库连接 try (Connection connection = dataSource.getConnection()) { try (Statement statement = connection.createStatement()) { statement.execute("SELECT 1"); } } return Health.up() .withDetail("redis", "UP") .withDetail("database", "UP") .build(); } catch (Exception e) { return Health.down() .withDetail("error", e.getMessage()) .build(); } } } ``` ## 6. 性能优化方案 ### 6.1 多级缓存设计 ```java @Service public class UserService { @Autowired private UserMapper userMapper; @Autowired private RedisTemplate redisTemplate; @Cacheable(value = "user", key = "#userId") public User getUserById(String userId) { return userMapper.selectById(userId); } @CacheEvict(value = "user", key = "#user.id") public void updateUser(User user) { userMapper.updateById(user); } @CacheEvict(value = "user", key = "#userId") public void deleteUser(String userId) { userMapper.deleteById(userId); } } ``` ### 6.2 Coze API调用实现 #### 6.2.1 Coze配置类 ```java @Configuration @ConfigurationProperties(prefix = "emotion.coze") @Data public class CozeConfig { private String apiKey; private String botId; private String baseUrl = "https://www.coze.cn/api"; private int timeout = 30000; private int maxRetries = 3; // 验证配置 @PostConstruct public void validateConfig() { if (StringUtils.isEmpty(apiKey)) { throw new IllegalStateException("Coze API Key不能为空"); } if (StringUtils.isEmpty(botId)) { throw new IllegalStateException("Coze Bot ID不能为空"); } log.info("Coze配置验证通过,Bot ID: {}", botId); } } ``` #### 6.2.2 Coze客户端实现 (使用WebClient) ```java @Service @Slf4j public class CozeClient { @Autowired private CozeConfig cozeConfig; @Autowired private WebClient.Builder webClientBuilder; private final WebClient webClient; public CozeClient(CozeConfig cozeConfig, WebClient.Builder webClientBuilder) { this.cozeConfig = cozeConfig; this.webClientBuilder = webClientBuilder; this.webClient = webClientBuilder .baseUrl(cozeConfig.getBaseUrl()) .defaultHeader("Authorization", "Bearer " + cozeConfig.getApiKey()) .defaultHeader("Content-Type", "application/json") .codecs(configurer -> configurer.defaultCodecs().maxInMemorySize(10 * 1024 * 1024)) .build(); } public Mono sendMessage(String message, String conversationId) { CozeRequest request = CozeRequest.builder() .botId(cozeConfig.getBotId()) .message(message) .conversationId(conversationId) .build(); return webClient.post() .uri("/chat/completions") .bodyValue(request) .retrieve() .bodyToMono(CozeResponse.class) .map(CozeResponse::getContent) .timeout(Duration.ofMillis(cozeConfig.getTimeout())) .retryWhen(Retry.backoff(cozeConfig.getMaxRetries(), Duration.ofSeconds(1))) .doOnError(error -> log.error("Coze API调用失败: {}", error.getMessage())) .doOnSuccess(response -> log.info("Coze API调用成功,响应长度: {}", response.length())); } public Mono generateSummary(String conversationId) { CozeSummaryRequest request = CozeSummaryRequest.builder() .botId(cozeConfig.getBotId()) .conversationId(conversationId) .build(); return webClient.post() .uri("/chat/summary") .bodyValue(request) .retrieve() .bodyToMono(CozeSummaryResponse.class) .map(CozeSummaryResponse::getSummary) .timeout(Duration.ofMillis(cozeConfig.getTimeout())) .retryWhen(Retry.backoff(cozeConfig.getMaxRetries(), Duration.ofSeconds(1))); } } ``` #### 6.2.2.1 替代方案:使用RestTemplate (同步调用) ```java @Service @Slf4j public class CozeRestTemplateClient { @Autowired private CozeConfig cozeConfig; @Autowired private RestTemplate restTemplate; public String sendMessage(String message, String conversationId) { CozeRequest request = CozeRequest.builder() .botId(cozeConfig.getBotId()) .message(message) .conversationId(conversationId) .build(); HttpHeaders headers = new HttpHeaders(); headers.setContentType(MediaType.APPLICATION_JSON); headers.setBearerAuth(cozeConfig.getApiKey()); HttpEntity entity = new HttpEntity<>(request, headers); try { ResponseEntity response = restTemplate.postForEntity( cozeConfig.getBaseUrl() + "/chat/completions", entity, CozeResponse.class ); if (response.getStatusCode() == HttpStatus.OK && response.getBody() != null) { log.info("Coze API调用成功,响应长度: {}", response.getBody().getContent().length()); return response.getBody().getContent(); } else { throw new RuntimeException("Coze API返回错误状态码: " + response.getStatusCode()); } } catch (Exception e) { log.error("Coze API调用失败: {}", e.getMessage()); throw new RuntimeException("Coze API调用失败", e); } } public String generateSummary(String conversationId) { CozeSummaryRequest request = CozeSummaryRequest.builder() .botId(cozeConfig.getBotId()) .conversationId(conversationId) .build(); HttpHeaders headers = new HttpHeaders(); headers.setContentType(MediaType.APPLICATION_JSON); headers.setBearerAuth(cozeConfig.getApiKey()); HttpEntity entity = new HttpEntity<>(request, headers); try { ResponseEntity response = restTemplate.postForEntity( cozeConfig.getBaseUrl() + "/chat/summary", entity, CozeSummaryResponse.class ); if (response.getStatusCode() == HttpStatus.OK && response.getBody() != null) { return response.getBody().getSummary(); } else { throw new RuntimeException("Coze API返回错误状态码: " + response.getStatusCode()); } } catch (Exception e) { log.error("Coze API调用失败: {}", e.getMessage()); throw new RuntimeException("Coze API调用失败", e); } } } ``` #### 6.2.3 AI服务异步化实现 ```java @Service @Slf4j public class AiChatService { @Autowired private CozeClient cozeClient; // 使用WebClient的异步客户端 @Autowired private CozeRestTemplateClient cozeRestTemplateClient; // 使用RestTemplate的同步客户端 @Autowired private MessageMapper messageMapper; // 方案1:使用WebClient异步调用 @Async("aiTaskExecutor") public CompletableFuture sendChatMessageAsync(String conversationId, String message, String userId) { try { // 调用Coze API (异步方式) String aiReply = cozeClient.sendMessage(message, conversationId) .block(); // 在异步方法中阻塞等待结果 if (aiReply != null) { // 异步保存消息 saveMessageAsync(conversationId, message, aiReply, userId); return CompletableFuture.completedFuture(aiReply); } else { throw new RuntimeException("Coze API返回空响应"); } } catch (Exception e) { log.error("AI调用失败", e); return CompletableFuture.failedFuture(e); } } // 方案2:使用RestTemplate同步调用 @Async("aiTaskExecutor") public CompletableFuture sendChatMessageSyncAsync(String conversationId, String message, String userId) { try { // 调用Coze API (同步方式) String aiReply = cozeRestTemplateClient.sendMessage(message, conversationId); if (aiReply != null) { // 异步保存消息 saveMessageAsync(conversationId, message, aiReply, userId); return CompletableFuture.completedFuture(aiReply); } else { throw new RuntimeException("Coze API返回空响应"); } } catch (Exception e) { log.error("AI调用失败", e); return CompletableFuture.failedFuture(e); } } @Async("aiTaskExecutor") public CompletableFuture generateSummaryAsync(String conversationId) { try { String summary = cozeClient.generateSummary(conversationId) .block(); return CompletableFuture.completedFuture(summary); } catch (Exception e) { log.error("生成对话总结失败", e); return CompletableFuture.failedFuture(e); } } @Async public void saveMessageAsync(String conversationId, String userMessage, String aiReply, String userId) { try { // 异步保存用户消息 Message message = new Message(); message.setConversationId(conversationId); message.setContent(userMessage); message.setSenderId(userId); message.setSenderType("USER"); message.setCreateTime(LocalDateTime.now()); messageMapper.insert(message); // 异步保存AI回复 Message aiMessage = new Message(); aiMessage.setConversationId(conversationId); aiMessage.setContent(aiReply); aiMessage.setSenderId("AI"); aiMessage.setSenderType("AI"); aiMessage.setCreateTime(LocalDateTime.now()); messageMapper.insert(aiMessage); log.info("消息保存成功,会话ID: {}", conversationId); } catch (Exception e) { log.error("保存消息失败", e); } } } ``` #### 6.2.4 Coze请求响应DTO ```java @Data @Builder @NoArgsConstructor @AllArgsConstructor public class CozeRequest { private String botId; private String message; private String conversationId; private Map parameters; } @Data @Builder @NoArgsConstructor @AllArgsConstructor public class CozeResponse { private String content; private String conversationId; private Long timestamp; private String status; } @Data @Builder @NoArgsConstructor @AllArgsConstructor public class CozeSummaryRequest { private String botId; private String conversationId; } @Data @Builder @NoArgsConstructor @AllArgsConstructor public class CozeSummaryResponse { private String summary; private String conversationId; private Long timestamp; } ``` ## 7. 测试策略升级 ### 7.1 单元测试升级 ```java @SpringBootTest @AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE) @Transactional @Rollback class UserServiceTest { @Autowired private UserService userService; @MockBean private UserMapper userMapper; @Test @DisplayName("测试用户注册") void testUserRegistration() { RegisterRequest request = new RegisterRequest(); request.setUsername("testuser"); request.setPassword("password123"); request.setEmail("test@example.com"); when(userMapper.selectByUsername("testuser")).thenReturn(null); when(userMapper.insert(any(User.class))).thenReturn(1); AuthResponse response = userService.register(request); assertNotNull(response); assertNotNull(response.getToken()); verify(userMapper).insert(any(User.class)); } } ``` ### 7.2 集成测试 ```java @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) @AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE) class AuthControllerIntegrationTest { @Autowired private TestRestTemplate restTemplate; @Test @DisplayName("测试用户登录接口") void testUserLogin() { LoginRequest request = new LoginRequest(); request.setUsername("testuser"); request.setPassword("password123"); request.setCaptcha("1234"); request.setCaptchaId("test-captcha-id"); ResponseEntity response = restTemplate.postForEntity( "/api/auth/login", request, Result.class); assertEquals(HttpStatus.OK, response.getStatusCode()); assertNotNull(response.getBody()); assertEquals(200, response.getBody().getCode()); } } ``` ## 8. 部署和运维优化 ### 8.1 Docker化部署 ```dockerfile # 使用OpenJDK 21作为基础镜像 FROM openjdk:21-jdk-slim # 设置工作目录 WORKDIR /app # 复制JAR文件 COPY target/emotion-museum-1.0.0.jar app.jar # 设置JVM参数 ENV JAVA_OPTS="-Xms512m -Xmx2g -XX:+UseG1GC -XX:+UseContainerSupport" # 暴露端口 EXPOSE 19089 # 健康检查 HEALTHCHECK --interval=30s --timeout=3s --start-period=60s --retries=3 \ CMD curl -f http://localhost:19089/api/health || exit 1 # 启动应用 ENTRYPOINT ["sh", "-c", "java $JAVA_OPTS -jar app.jar"] ``` ### 8.2 Docker Compose ```yaml version: '3.8' services: app: build: . ports: - "19089:19089" environment: - SPRING_PROFILES_ACTIVE=prod - MYSQL_HOST=mysql - REDIS_HOST=redis depends_on: - mysql - redis restart: unless-stopped mysql: image: mysql:8.2 environment: MYSQL_ROOT_PASSWORD: password MYSQL_DATABASE: emotion_museum volumes: - mysql_data:/var/lib/mysql ports: - "3306:3306" restart: unless-stopped redis: image: redis:8-alpine command: redis-server --requirepass password ports: - "6379:6379" volumes: - redis_data:/data restart: unless-stopped volumes: mysql_data: redis_data: ``` ## 9. 升级实施计划 ### 9.1 升级阶段规划 #### 第一阶段:基础升级 (1-2周) - [ ] 升级Spring Boot到3.4.8 - [ ] 升级Java到JDK 21 - [ ] 升级Spring Security到6.x - [ ] 升级MyBatis-Plus到3.5.5 - [ ] 升级JWT到0.12.3 - [ ] 修复编译错误和兼容性问题 #### 第二阶段:功能优化 (2-3周) - [ ] 优化异步处理配置 - [ ] 升级缓存策略 - [ ] 优化数据库连接池 - [ ] 升级WebSocket配置 - [ ] 优化安全配置 #### 第三阶段:性能优化 (1-2周) - [ ] 实现多级缓存 - [ ] 优化SQL查询 - [ ] 配置性能监控 - [ ] 优化日志配置 - [ ] 压力测试和调优 #### 第四阶段:测试和部署 (1-2周) - [ ] 完善单元测试 - [ ] 集成测试 - [ ] 性能测试 - [ ] 安全测试 - [ ] 生产环境部署 ### 9.2 成功标准 #### 9.2.1 功能标准 - [ ] 所有现有功能正常工作 - [ ] API接口响应正常 - [ ] WebSocket连接稳定 - [ ] 数据库操作正常 - [ ] 缓存功能正常 #### 9.2.2 性能标准 - [ ] 响应时间提升20%以上 - [ ] 并发处理能力提升50%以上 - [ ] 内存使用优化30%以上 - [ ] 启动时间缩短50%以上 #### 9.2.3 安全标准 - [ ] 通过安全扫描 - [ ] 符合OWASP安全标准 - [ ] 支持最新的安全特性 - [ ] 日志记录完整 ## 10. 总结 本技术升级方案基于Spring Boot 3.4.8版本,采用最新的技术栈和最佳实践,全面提升系统的性能、安全性和可维护性。通过分阶段的升级策略,确保系统稳定性和业务连续性。 ### 10.1 升级收益 1. **性能提升**: 利用Spring Boot 3.x的性能优化 2. **安全增强**: 采用最新的安全特性和最佳实践 3. **开发效率**: 使用最新的Java特性和工具 4. **维护性**: 更好的代码结构和文档 5. **扩展性**: 为未来的功能扩展奠定基础 ### 10.2 关键成功因素 1. **充分的测试覆盖** 2. **渐进式的升级策略** 3. **完善的监控和日志** 4. **团队的技术培训** 5. **详细的文档记录** 这个升级方案将为情绪博物馆后端服务带来显著的性能提升和技术现代化,为项目的长期发展提供强有力的技术支撑。