1035 lines
30 KiB
Markdown
1035 lines
30 KiB
Markdown
# 情绪博物馆后端重构升级技术方案
|
|
|
|
## 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
|
|
<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 核心依赖升级
|
|
```xml
|
|
<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配置
|
|
```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<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 多环境配置
|
|
```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<AuthResponse> 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<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 多级缓存设计
|
|
```java
|
|
@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配置类
|
|
```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<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 (同步调用)
|
|
```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<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服务异步化实现
|
|
```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<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
|
|
```java
|
|
@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 单元测试升级
|
|
```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<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化部署
|
|
```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. **详细的文档记录**
|
|
|
|
这个升级方案将为情绪博物馆后端服务带来显著的性能提升和技术现代化,为项目的长期发展提供强有力的技术支撑。
|