Files
happy-life-star/server/后端重构升级技术方案.md
T

30 KiB

情绪博物馆后端重构升级技术方案

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

<parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <version>3.4.8</version>
    <relativePath/>
</parent>

<properties>
    <java.version>21</java.version>
    <maven.compiler.source>21</maven.compiler.source>
    <maven.compiler.target>21</maven.compiler.target>
</properties>

2.1.2 核心依赖升级

<dependencies>
    <!-- Spring Boot Starters -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-security</artifactId>
    </dependency>
    
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-websocket</artifactId>
    </dependency>
    
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-data-redis</artifactId>
    </dependency>
    
    <!-- HTTP客户端 (用于调用Coze API) -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-webflux</artifactId>
    </dependency>
    
    <!-- RestTemplate配置 -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    
    <!-- 数据库相关 -->
    <dependency>
        <groupId>com.mysql</groupId>
        <artifactId>mysql-connector-j</artifactId>
        <scope>runtime</scope>
    </dependency>
    
    <dependency>
        <groupId>com.baomidou</groupId>
        <artifactId>mybatis-plus-boot-starter</artifactId>
        <version>3.5.5</version>
    </dependency>
    
    <!-- JWT -->
    <dependency>
        <groupId>io.jsonwebtoken</groupId>
        <artifactId>jjwt-api</artifactId>
        <version>0.12.3</version>
    </dependency>
    <dependency>
        <groupId>io.jsonwebtoken</groupId>
        <artifactId>jjwt-impl</artifactId>
        <version>0.12.3</version>
        <scope>runtime</scope>
    </dependency>
    <dependency>
        <groupId>io.jsonwebtoken</groupId>
        <artifactId>jjwt-jackson</artifactId>
        <version>0.12.3</version>
        <scope>runtime</scope>
    </dependency>
    
    <!-- API文档 -->
    <dependency>
        <groupId>org.springdoc</groupId>
        <artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>
        <version>2.3.0</version>
    </dependency>
    
    <!-- 工具类 -->
    <dependency>
        <groupId>org.apache.commons</groupId>
        <artifactId>commons-lang3</artifactId>
    </dependency>
    
    <dependency>
        <groupId>cn.hutool</groupId>
        <artifactId>hutool-all</artifactId>
        <version>5.8.25</version>
    </dependency>
</dependencies>

2.2 安全框架升级

2.2.1 Spring Security 6.x配置

@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

@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 异步配置升级

@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配置

@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配置升级

@Configuration
@EnableCaching
public class RedisConfig {
    
    @Bean
    public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory) {
        RedisTemplate<String, Object> template = new RedisTemplate<>();
        template.setConnectionFactory(factory);
        
        Jackson2JsonRedisSerializer<Object> 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 多环境配置

# 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 数据库连接池优化

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配置

@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 控制器文档注解

@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<AuthResponse> login(@Valid @RequestBody LoginRequest request) {
        AuthResponse response = authService.login(request);
        return Result.success("登录成功", response);
    }
}

5. 监控和日志优化

5.1 应用监控配置

management:
  endpoints:
    web:
      exposure:
        include: health,info,metrics,prometheus
  endpoint:
    health:
      show-details: always
    metrics:
      enabled: true
  metrics:
    export:
      prometheus:
        enabled: true

5.2 健康检查配置

@Component
public class CustomHealthIndicator implements HealthIndicator {
    
    @Autowired
    private RedisTemplate<String, Object> 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 多级缓存设计

@Service
public class UserService {
    
    @Autowired
    private UserMapper userMapper;
    
    @Autowired
    private RedisTemplate<String, Object> 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配置类

@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)

@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<String> 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<String> 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 (同步调用)

@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<CozeRequest> entity = new HttpEntity<>(request, headers);
        
        try {
            ResponseEntity<CozeResponse> 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<CozeSummaryRequest> entity = new HttpEntity<>(request, headers);
        
        try {
            ResponseEntity<CozeSummaryResponse> 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服务异步化实现

@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<String> 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<String> 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<String> 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

@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class CozeRequest {
    private String botId;
    private String message;
    private String conversationId;
    private Map<String, Object> 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 单元测试升级

@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 集成测试

@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<Result> 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化部署

# 使用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

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. 详细的文档记录

这个升级方案将为情绪博物馆后端服务带来显著的性能提升和技术现代化,为项目的长期发展提供强有力的技术支撑。