feat: 项目初始化及当前全部内容提交
This commit is contained in:
@@ -0,0 +1,48 @@
|
||||
# AI服务Dockerfile
|
||||
FROM openjdk:17-jdk-alpine
|
||||
|
||||
# 设置工作目录
|
||||
WORKDIR /app
|
||||
|
||||
# 安装必要的工具
|
||||
RUN apk add --no-cache curl tzdata && \
|
||||
cp /usr/share/zoneinfo/Asia/Shanghai /etc/localtime && \
|
||||
echo "Asia/Shanghai" > /etc/timezone
|
||||
|
||||
# 复制Maven构建文件
|
||||
COPY pom.xml ./
|
||||
COPY emotion-common ./emotion-common
|
||||
COPY emotion-ai ./emotion-ai
|
||||
|
||||
# 安装Maven
|
||||
RUN apk add --no-cache maven
|
||||
|
||||
# 构建应用
|
||||
RUN mvn clean package -DskipTests -pl emotion-ai -am
|
||||
|
||||
# 创建运行用户
|
||||
RUN addgroup -g 1000 emotion && \
|
||||
adduser -D -s /bin/sh -u 1000 -G emotion emotion
|
||||
|
||||
# 复制jar文件
|
||||
RUN cp emotion-ai/target/emotion-ai-*.jar app.jar
|
||||
|
||||
# 设置文件权限
|
||||
RUN chown -R emotion:emotion /app
|
||||
|
||||
# 切换到非root用户
|
||||
USER emotion
|
||||
|
||||
# 健康检查
|
||||
HEALTHCHECK --interval=30s --timeout=10s --start-period=60s --retries=3 \
|
||||
CMD curl -f http://localhost:19002/actuator/health || exit 1
|
||||
|
||||
# 暴露端口
|
||||
EXPOSE 19002
|
||||
|
||||
# 启动命令
|
||||
ENTRYPOINT ["java", "-jar", \
|
||||
"-Xms512m", "-Xmx1024m", \
|
||||
"-Djava.security.egd=file:/dev/./urandom", \
|
||||
"-Dspring.profiles.active=local", \
|
||||
"app.jar"]
|
||||
@@ -0,0 +1,101 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
|
||||
<parent>
|
||||
<groupId>com.emotionmuseum</groupId>
|
||||
<artifactId>backend</artifactId>
|
||||
<version>1.0.0</version>
|
||||
</parent>
|
||||
|
||||
<artifactId>emotion-ai</artifactId>
|
||||
<name>emotion-ai</name>
|
||||
<description>AI对话服务</description>
|
||||
|
||||
<dependencies>
|
||||
<!-- 内部模块依赖 -->
|
||||
<dependency>
|
||||
<groupId>com.emotionmuseum</groupId>
|
||||
<artifactId>emotion-common</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- Spring Cloud Discovery -->
|
||||
<dependency>
|
||||
<groupId>org.springframework.cloud</groupId>
|
||||
<artifactId>spring-cloud-starter-bootstrap</artifactId>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>com.alibaba.cloud</groupId>
|
||||
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>com.alibaba.cloud</groupId>
|
||||
<artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- Spring Boot DevTools for automatic restart -->
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-devtools</artifactId>
|
||||
<scope>runtime</scope>
|
||||
<optional>true</optional>
|
||||
</dependency>
|
||||
|
||||
<!-- Spring Boot Web -->
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-web</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- OpenFeign -->
|
||||
<dependency>
|
||||
<groupId>org.springframework.cloud</groupId>
|
||||
<artifactId>spring-cloud-starter-openfeign</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- MySQL -->
|
||||
<dependency>
|
||||
<groupId>mysql</groupId>
|
||||
<artifactId>mysql-connector-java</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- Druid -->
|
||||
<dependency>
|
||||
<groupId>com.alibaba</groupId>
|
||||
<artifactId>druid-spring-boot-starter</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- 暂时移除Spring AI,使用原生HTTP客户端实现 -->
|
||||
|
||||
<!-- HTTP客户端 -->
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-webflux</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- 监控 -->
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-actuator</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- 监控指标 -->
|
||||
<dependency>
|
||||
<groupId>io.micrometer</groupId>
|
||||
<artifactId>micrometer-registry-prometheus</artifactId>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
<build>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-maven-plugin</artifactId>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
</project>
|
||||
@@ -0,0 +1,24 @@
|
||||
package com.emotionmuseum.ai;
|
||||
|
||||
import org.mybatis.spring.annotation.MapperScan;
|
||||
import org.springframework.boot.SpringApplication;
|
||||
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
||||
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
|
||||
import org.springframework.cloud.openfeign.EnableFeignClients;
|
||||
|
||||
/**
|
||||
* AI对话服务启动类
|
||||
*
|
||||
* @author emotion-museum
|
||||
* @since 2025-07-12
|
||||
*/
|
||||
@SpringBootApplication(scanBasePackages = {"com.emotionmuseum"})
|
||||
@EnableDiscoveryClient
|
||||
@EnableFeignClients
|
||||
@MapperScan("com.emotionmuseum.ai.mapper")
|
||||
public class AiApplication {
|
||||
|
||||
public static void main(String[] args) {
|
||||
SpringApplication.run(AiApplication.class, args);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
package com.emotionmuseum.ai.config;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.web.reactive.function.client.WebClient;
|
||||
|
||||
/**
|
||||
* AI配置类
|
||||
* 配置Coze平台HTTP客户端
|
||||
*
|
||||
* @author emotion-museum
|
||||
* @since 2025-07-12
|
||||
*/
|
||||
@Configuration
|
||||
public class AiConfig {
|
||||
|
||||
@Value("${coze.base-url:https://api.coze.cn}")
|
||||
private String baseUrl;
|
||||
|
||||
@Value("${coze.token}")
|
||||
private String token;
|
||||
|
||||
/**
|
||||
* 配置Coze API客户端
|
||||
*/
|
||||
@Bean
|
||||
public WebClient cozeWebClient() {
|
||||
return WebClient.builder()
|
||||
.baseUrl(baseUrl)
|
||||
.defaultHeader("Authorization", "Bearer " + token)
|
||||
.defaultHeader("Content-Type", "application/json")
|
||||
.build();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
package com.emotionmuseum.ai.config;
|
||||
|
||||
import lombok.Data;
|
||||
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
/**
|
||||
* 功能开关配置
|
||||
*
|
||||
* @author emotion-museum
|
||||
* @since 2025-07-13
|
||||
*/
|
||||
@Data
|
||||
@Component
|
||||
@ConfigurationProperties(prefix = "features")
|
||||
public class FeatureConfig {
|
||||
|
||||
/**
|
||||
* 情绪分析功能配置
|
||||
*/
|
||||
private EmotionAnalysis emotionAnalysis = new EmotionAnalysis();
|
||||
|
||||
/**
|
||||
* 聊天功能配置
|
||||
*/
|
||||
private Chat chat = new Chat();
|
||||
|
||||
@Data
|
||||
public static class EmotionAnalysis {
|
||||
/**
|
||||
* 是否启用情绪分析功能
|
||||
*/
|
||||
private boolean enabled = false;
|
||||
|
||||
/**
|
||||
* 是否自动进行情绪分析
|
||||
*/
|
||||
private boolean autoAnalyze = false;
|
||||
}
|
||||
|
||||
@Data
|
||||
public static class Chat {
|
||||
/**
|
||||
* 是否启用聊天功能
|
||||
*/
|
||||
private boolean enabled = true;
|
||||
|
||||
/**
|
||||
* 是否启用流式聊天
|
||||
*/
|
||||
private boolean stream = false;
|
||||
}
|
||||
}
|
||||
+234
@@ -0,0 +1,234 @@
|
||||
package com.emotionmuseum.ai.controller;
|
||||
|
||||
import com.emotionmuseum.ai.dto.*;
|
||||
import com.emotionmuseum.ai.entity.Conversation;
|
||||
import com.emotionmuseum.ai.entity.Message;
|
||||
import com.emotionmuseum.ai.service.AiChatService;
|
||||
import com.emotionmuseum.ai.service.ConversationDbService;
|
||||
import com.emotionmuseum.common.dto.PageQuery;
|
||||
import com.emotionmuseum.common.result.Result;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.Parameter;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.validation.annotation.Validated;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import jakarta.validation.Valid;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* AI聊天控制器
|
||||
*
|
||||
* @author emotion-museum
|
||||
* @since 2025-07-12
|
||||
*/
|
||||
@Slf4j
|
||||
@RestController
|
||||
@RequestMapping("/api/ai/chat")
|
||||
@RequiredArgsConstructor
|
||||
@Validated
|
||||
@Tag(name = "AI聊天", description = "AI聊天相关接口")
|
||||
public class AiChatController {
|
||||
|
||||
private final AiChatService aiChatService;
|
||||
private final ConversationDbService conversationDbService;
|
||||
|
||||
@Operation(summary = "创建会话")
|
||||
@PostMapping("/conversation/create")
|
||||
public Result<CreateConversationResponse> createConversation(
|
||||
@Valid @RequestBody CreateConversationRequest request) {
|
||||
log.info("收到创建会话请求: userId={}, title={}", request.getUserId(), request.getTitle());
|
||||
|
||||
CreateConversationResponse response = aiChatService.createConversation(request);
|
||||
return Result.success(response);
|
||||
}
|
||||
|
||||
@Operation(summary = "发送聊天消息")
|
||||
@PostMapping("/send")
|
||||
public Result<ChatResponse> sendMessage(@Valid @RequestBody ChatRequest request) {
|
||||
log.info("收到聊天请求: userId={}, message={}", request.getUserId(), request.getMessage());
|
||||
|
||||
ChatResponse response = aiChatService.chat(request);
|
||||
return Result.success(response);
|
||||
}
|
||||
|
||||
@Operation(summary = "情绪分析")
|
||||
@PostMapping("/emotion/analyze")
|
||||
public Result<EmotionAnalysisResponse> analyzeEmotion(@Valid @RequestBody EmotionAnalysisRequest request) {
|
||||
log.info("收到情绪分析请求: userId={}, text={}", request.getUserId(), request.getText());
|
||||
|
||||
EmotionAnalysisResponse response = aiChatService.analyzeEmotion(request);
|
||||
return Result.success(response);
|
||||
}
|
||||
|
||||
@Operation(summary = "流式聊天")
|
||||
@PostMapping("/stream")
|
||||
public Result<String> streamChat(@Valid @RequestBody ChatRequest request) {
|
||||
log.info("收到流式聊天请求: userId={}", request.getUserId());
|
||||
|
||||
String response = aiChatService.streamChat(request);
|
||||
return Result.success(response);
|
||||
}
|
||||
|
||||
@Operation(summary = "健康检查")
|
||||
@GetMapping("/health")
|
||||
public Result<Boolean> healthCheck() {
|
||||
log.info("AI服务健康检查");
|
||||
|
||||
boolean isHealthy = aiChatService.healthCheck();
|
||||
return Result.success(isHealthy);
|
||||
}
|
||||
|
||||
@Operation(summary = "获取AI服务信息")
|
||||
@GetMapping("/info")
|
||||
public Result<Object> getServiceInfo() {
|
||||
log.info("获取AI服务信息");
|
||||
|
||||
return Result.success("Emotion Museum AI Service - Powered by Spring AI & Coze");
|
||||
}
|
||||
|
||||
@Operation(summary = "获取用户会话列表")
|
||||
@GetMapping("/conversations/{userId}")
|
||||
public Result<List<Conversation>> getUserConversations(
|
||||
@Parameter(description = "用户ID") @PathVariable String userId,
|
||||
@Parameter(description = "页码") @RequestParam(defaultValue = "1") Integer pageNum,
|
||||
@Parameter(description = "页大小") @RequestParam(defaultValue = "20") Integer pageSize) {
|
||||
log.info("获取用户会话列表: userId={}, pageNum={}, pageSize={}", userId, pageNum, pageSize);
|
||||
|
||||
PageQuery pageQuery = new PageQuery();
|
||||
pageQuery.setPageNum(pageNum);
|
||||
pageQuery.setPageSize(pageSize);
|
||||
|
||||
List<Conversation> conversations = conversationDbService.getConversationsByUserId(userId, pageQuery);
|
||||
return Result.success(conversations);
|
||||
}
|
||||
|
||||
@Operation(summary = "获取会话详情")
|
||||
@GetMapping("/conversation/{conversationId}")
|
||||
public Result<Conversation> getConversation(@Parameter(description = "会话ID") @PathVariable String conversationId) {
|
||||
log.info("获取会话详情: conversationId={}", conversationId);
|
||||
|
||||
Conversation conversation = conversationDbService.getConversationById(conversationId);
|
||||
return Result.success(conversation);
|
||||
}
|
||||
|
||||
@Operation(summary = "获取会话消息列表")
|
||||
@GetMapping("/conversation/{conversationId}/messages")
|
||||
public Result<List<Message>> getConversationMessages(
|
||||
@Parameter(description = "会话ID") @PathVariable String conversationId,
|
||||
@Parameter(description = "页码") @RequestParam(defaultValue = "1") Integer pageNum,
|
||||
@Parameter(description = "页大小") @RequestParam(defaultValue = "50") Integer pageSize) {
|
||||
log.info("获取会话消息列表: conversationId={}, pageNum={}, pageSize={}", conversationId, pageNum, pageSize);
|
||||
|
||||
PageQuery pageQuery = new PageQuery();
|
||||
pageQuery.setPageNum(pageNum);
|
||||
pageQuery.setPageSize(pageSize);
|
||||
|
||||
List<Message> messages = conversationDbService.getMessagesByConversationId(conversationId, pageQuery);
|
||||
return Result.success(messages);
|
||||
}
|
||||
|
||||
@Operation(summary = "结束会话")
|
||||
@PutMapping("/conversation/{conversationId}/end")
|
||||
public Result<Void> endConversation(@Parameter(description = "会话ID") @PathVariable String conversationId) {
|
||||
log.info("结束会话: conversationId={}", conversationId);
|
||||
|
||||
boolean success = conversationDbService.updateConversationStatus(conversationId, "ended");
|
||||
return success ? Result.success() : Result.error("结束会话失败");
|
||||
}
|
||||
|
||||
@Operation(summary = "删除会话")
|
||||
@DeleteMapping("/conversation/{conversationId}")
|
||||
public Result<Void> deleteConversation(@Parameter(description = "会话ID") @PathVariable String conversationId) {
|
||||
log.info("删除会话: conversationId={}", conversationId);
|
||||
|
||||
boolean success = conversationDbService.deleteConversation(conversationId);
|
||||
return success ? Result.success() : Result.error("删除会话失败");
|
||||
}
|
||||
|
||||
@Operation(summary = "标记消息已读")
|
||||
@PutMapping("/message/{messageId}/read")
|
||||
public Result<Void> markMessageAsRead(@Parameter(description = "消息ID") @PathVariable String messageId) {
|
||||
log.info("标记消息已读: messageId={}", messageId);
|
||||
|
||||
boolean success = conversationDbService.markMessageAsRead(messageId);
|
||||
return success ? Result.success() : Result.error("标记消息已读失败");
|
||||
}
|
||||
|
||||
@Operation(summary = "标记会话所有消息已读")
|
||||
@PutMapping("/conversation/{conversationId}/read")
|
||||
public Result<Void> markConversationMessagesAsRead(
|
||||
@Parameter(description = "会话ID") @PathVariable String conversationId) {
|
||||
log.info("标记会话消息已读: conversationId={}", conversationId);
|
||||
|
||||
boolean success = conversationDbService.markConversationMessagesAsRead(conversationId);
|
||||
return success ? Result.success() : Result.error("标记会话消息已读失败");
|
||||
}
|
||||
|
||||
@Operation(summary = "获取拆分后的消息详情")
|
||||
@GetMapping("/messages/split")
|
||||
public Result<List<Message>> getSplitMessages(
|
||||
@Parameter(description = "消息ID列表,逗号分隔") @RequestParam String messageIds) {
|
||||
log.info("获取拆分消息详情: messageIds={}", messageIds);
|
||||
|
||||
String[] ids = messageIds.split(",");
|
||||
List<Message> messages = conversationDbService.getMessagesByIds(List.of(ids));
|
||||
return Result.success(messages);
|
||||
}
|
||||
|
||||
@Operation(summary = "测试消息拆分功能")
|
||||
@PostMapping("/test/split")
|
||||
public Result<ChatResponse> testMessageSplit(@Valid @RequestBody ChatRequest request) {
|
||||
log.info("测试消息拆分功能: userId={}, message={}", request.getUserId(), request.getMessage());
|
||||
|
||||
// 模拟一个包含\n\n的AI回复
|
||||
String mockAiReply = "这是第一段回复,介绍了基本功能。我可以帮助你进行日常对话。\n\n" +
|
||||
"这是第二段回复,详细说明了聊天功能。我能理解你的情感并给出合适的回应。\n\n" +
|
||||
"这是第三段回复,介绍了情感分析功能。我可以分析你的情绪状态并提供建议。";
|
||||
|
||||
// 创建或获取会话
|
||||
CreateConversationRequest convRequest = new CreateConversationRequest();
|
||||
convRequest.setUserId(request.getUserId());
|
||||
convRequest.setTitle("测试拆分消息");
|
||||
CreateConversationResponse conversation = aiChatService.createConversation(convRequest);
|
||||
|
||||
// 保存用户消息
|
||||
Message userMessage = new Message();
|
||||
userMessage.setConversationId(conversation.getConversationId());
|
||||
userMessage.setContent(request.getMessage());
|
||||
userMessage.setType("text");
|
||||
userMessage.setSender("user");
|
||||
userMessage.setTimestamp(java.time.LocalDateTime.now());
|
||||
userMessage.setStatus("sent");
|
||||
userMessage.setIsRead(0);
|
||||
Message savedUserMessage = conversationDbService.saveMessage(userMessage);
|
||||
|
||||
// 使用拆分逻辑保存AI回复
|
||||
List<Message> savedAiMessages = aiChatService.saveAiReplyMessages(
|
||||
conversation.getConversationId(), mockAiReply, null);
|
||||
|
||||
// 构建响应
|
||||
ChatResponse response = new ChatResponse();
|
||||
Message lastMessage = savedAiMessages.get(savedAiMessages.size() - 1);
|
||||
response.setMessageId(lastMessage.getId());
|
||||
response.setConversationId(conversation.getConversationId());
|
||||
response.setContent(mockAiReply);
|
||||
response.setTimestamp(lastMessage.getTimestamp());
|
||||
|
||||
// 设置多条消息信息
|
||||
if (savedAiMessages.size() > 1) {
|
||||
response.setMultipleMessages(true);
|
||||
response.setMessageCount(savedAiMessages.size());
|
||||
response.setMessageIds(savedAiMessages.stream()
|
||||
.map(Message::getId)
|
||||
.collect(java.util.stream.Collectors.toList()));
|
||||
} else {
|
||||
response.setMultipleMessages(false);
|
||||
response.setMessageCount(1);
|
||||
}
|
||||
|
||||
return Result.success(response);
|
||||
}
|
||||
}
|
||||
+209
@@ -0,0 +1,209 @@
|
||||
package com.emotionmuseum.ai.controller;
|
||||
|
||||
import com.emotionmuseum.ai.dto.*;
|
||||
import com.emotionmuseum.ai.service.GuestChatService;
|
||||
import com.emotionmuseum.common.result.Result;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.Parameter;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import org.springframework.web.context.request.RequestContextHolder;
|
||||
import org.springframework.web.context.request.ServletRequestAttributes;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 访客聊天控制器
|
||||
*
|
||||
* @author emotion-museum
|
||||
* @since 2025-07-13
|
||||
*/
|
||||
@Slf4j
|
||||
@RestController
|
||||
@RequestMapping("/api/ai/guest")
|
||||
@RequiredArgsConstructor
|
||||
@Tag(name = "访客聊天", description = "访客模式AI聊天接口")
|
||||
public class GuestChatController {
|
||||
|
||||
private final GuestChatService guestChatService;
|
||||
|
||||
@PostMapping("/chat")
|
||||
@Operation(summary = "访客聊天", description = "访客模式下发送消息并获取AI回复")
|
||||
public Result<GuestChatResponse> guestChat(@RequestBody GuestChatRequest request) {
|
||||
|
||||
// 自动获取客户端IP和User-Agent
|
||||
String clientIp = getClientIp();
|
||||
String userAgent = getUserAgent();
|
||||
|
||||
request.setClientIp(clientIp);
|
||||
request.setUserAgent(userAgent);
|
||||
|
||||
log.info("访客聊天请求: IP={}, Message={}", clientIp, request.getMessage());
|
||||
|
||||
return guestChatService.guestChat(request);
|
||||
}
|
||||
|
||||
@GetMapping("/conversations")
|
||||
@Operation(summary = "获取访客会话列表", description = "根据IP地址获取访客的历史会话列表")
|
||||
public Result<List<ConversationListResponse>> getGuestConversations(
|
||||
@Parameter(description = "页码") @RequestParam(defaultValue = "1") Integer pageNum,
|
||||
@Parameter(description = "页大小") @RequestParam(defaultValue = "20") Integer pageSize) {
|
||||
|
||||
String clientIp = getClientIp();
|
||||
log.info("获取访客会话列表: IP={}", clientIp);
|
||||
|
||||
return guestChatService.getGuestConversations(clientIp, pageNum, pageSize);
|
||||
}
|
||||
|
||||
@GetMapping("/conversation/{conversationId}/messages")
|
||||
@Operation(summary = "获取访客会话消息", description = "获取指定会话的消息列表")
|
||||
public Result<List<MessageListResponse>> getGuestConversationMessages(
|
||||
@Parameter(description = "会话ID") @PathVariable String conversationId,
|
||||
@Parameter(description = "页码") @RequestParam(defaultValue = "1") Integer pageNum,
|
||||
@Parameter(description = "页大小") @RequestParam(defaultValue = "50") Integer pageSize) {
|
||||
|
||||
String clientIp = getClientIp();
|
||||
log.info("获取访客会话消息: IP={}, ConversationId={}", clientIp, conversationId);
|
||||
|
||||
return guestChatService.getGuestConversationMessages(conversationId, clientIp, pageNum, pageSize);
|
||||
}
|
||||
|
||||
@PostMapping("/conversation/{conversationId}/end")
|
||||
@Operation(summary = "结束访客会话", description = "结束指定的访客会话")
|
||||
public Result<Void> endGuestConversation(
|
||||
@Parameter(description = "会话ID") @PathVariable String conversationId) {
|
||||
|
||||
String clientIp = getClientIp();
|
||||
log.info("结束访客会话: IP={}, ConversationId={}", clientIp, conversationId);
|
||||
|
||||
return guestChatService.endGuestConversation(conversationId, clientIp);
|
||||
}
|
||||
|
||||
@GetMapping("/user/info")
|
||||
@Operation(summary = "获取访客用户信息", description = "根据IP地址获取或创建访客用户信息")
|
||||
public Result<GuestUserInfo> getGuestUserInfo() {
|
||||
String clientIp = getClientIp();
|
||||
String userAgent = getUserAgent();
|
||||
|
||||
log.info("获取访客用户信息: IP={}", clientIp);
|
||||
|
||||
return guestChatService.getOrCreateGuestUser(clientIp, userAgent);
|
||||
}
|
||||
|
||||
@PostMapping("/emotion/analyze")
|
||||
@Operation(summary = "访客情绪分析", description = "分析访客输入文本的情绪")
|
||||
public Result<EmotionAnalysisResponse> analyzeGuestEmotion(
|
||||
@RequestBody EmotionAnalysisRequest request) {
|
||||
|
||||
String clientIp = getClientIp();
|
||||
log.info("访客情绪分析: IP={}, Text={}", clientIp, request.getText());
|
||||
|
||||
return guestChatService.analyzeGuestEmotion(request, clientIp);
|
||||
}
|
||||
|
||||
@GetMapping("/health")
|
||||
@Operation(summary = "访客服务健康检查", description = "检查访客聊天服务状态")
|
||||
public Result<Boolean> healthCheck() {
|
||||
return Result.success(true);
|
||||
}
|
||||
|
||||
@PostMapping("/test/split")
|
||||
@Operation(summary = "测试消息拆分功能", description = "测试AI回复消息的拆分功能")
|
||||
public Result<GuestChatResponse> testMessageSplit(@RequestBody GuestChatRequest request) {
|
||||
log.info("测试消息拆分功能: message={}", request.getMessage());
|
||||
|
||||
// 模拟包含不同换行符的AI回复进行测试
|
||||
String mockAiReply;
|
||||
if (request.getMessage().contains("双换行")) {
|
||||
mockAiReply = "这是第一段回复,介绍了基本功能。我可以帮助你进行日常对话。\n\n" +
|
||||
"这是第二段回复,详细说明了聊天功能。我能理解你的情感并给出合适的回应。\n\n" +
|
||||
"这是第三段回复,介绍了情感分析功能。我可以分析你的情绪状态并提供建议。";
|
||||
} else if (request.getMessage().contains("单换行")) {
|
||||
mockAiReply = "这是第一行回复,介绍基本功能。\n" +
|
||||
"这是第二行回复,说明聊天功能。\n" +
|
||||
"这是第三行回复,介绍情感分析。\n" +
|
||||
"这是第四行回复,提供使用建议。";
|
||||
} else {
|
||||
mockAiReply = "这是一个完整的回复,没有换行符,将作为单条消息处理。包含了所有功能介绍和使用说明。";
|
||||
}
|
||||
|
||||
// 创建模拟的访客聊天响应
|
||||
GuestChatResponse response = new GuestChatResponse();
|
||||
response.setGuestUserId("test_guest_user");
|
||||
response.setGuestNickname("测试用户");
|
||||
response.setConversationId("test_conversation_" + System.currentTimeMillis());
|
||||
response.setUserMessage(request.getMessage());
|
||||
response.setAiReply(mockAiReply);
|
||||
response.setTimestamp(LocalDateTime.now());
|
||||
response.setConversationStatus("active");
|
||||
response.setIsNewConversation(true);
|
||||
|
||||
log.info("测试拆分功能完成,AI回复长度: {}, 包含\\n\\n: {}, 包含\\n: {}",
|
||||
mockAiReply.length(),
|
||||
mockAiReply.contains("\n\n"),
|
||||
mockAiReply.contains("\n"));
|
||||
|
||||
return Result.success(response);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取客户端真实IP地址
|
||||
*/
|
||||
private String getClientIp() {
|
||||
try {
|
||||
ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder
|
||||
.getRequestAttributes();
|
||||
if (attributes == null) {
|
||||
return "127.0.0.1";
|
||||
}
|
||||
|
||||
var request = attributes.getRequest();
|
||||
String ip = request.getHeader("X-Forwarded-For");
|
||||
if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
|
||||
ip = request.getHeader("Proxy-Client-IP");
|
||||
}
|
||||
if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
|
||||
ip = request.getHeader("WL-Proxy-Client-IP");
|
||||
}
|
||||
if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
|
||||
ip = request.getHeader("HTTP_CLIENT_IP");
|
||||
}
|
||||
if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
|
||||
ip = request.getHeader("HTTP_X_FORWARDED_FOR");
|
||||
}
|
||||
if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
|
||||
ip = request.getRemoteAddr();
|
||||
}
|
||||
|
||||
// 处理多个IP的情况,取第一个
|
||||
if (ip != null && ip.contains(",")) {
|
||||
ip = ip.split(",")[0].trim();
|
||||
}
|
||||
|
||||
return ip;
|
||||
} catch (Exception e) {
|
||||
return "127.0.0.1";
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取用户代理信息
|
||||
*/
|
||||
private String getUserAgent() {
|
||||
try {
|
||||
ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder
|
||||
.getRequestAttributes();
|
||||
if (attributes == null) {
|
||||
return "Unknown";
|
||||
}
|
||||
|
||||
var request = attributes.getRequest();
|
||||
return request.getHeader("User-Agent");
|
||||
} catch (Exception e) {
|
||||
return "Unknown";
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
package com.emotionmuseum.ai.dto;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
import jakarta.validation.constraints.Size;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 聊天请求
|
||||
*
|
||||
* @author emotion-museum
|
||||
* @since 2025-07-12
|
||||
*/
|
||||
@Data
|
||||
@Schema(description = "聊天请求")
|
||||
public class ChatRequest {
|
||||
|
||||
@Schema(description = "用户ID", example = "user_123")
|
||||
@NotBlank(message = "用户ID不能为空")
|
||||
private String userId;
|
||||
|
||||
@Schema(description = "消息内容", example = "我今天感觉有点焦虑,不知道该怎么办")
|
||||
@NotBlank(message = "消息内容不能为空")
|
||||
@Size(max = 2000, message = "消息内容不能超过2000字符")
|
||||
private String message;
|
||||
|
||||
@Schema(description = "对话ID(可选)", example = "conv_123456")
|
||||
private String conversationId;
|
||||
|
||||
@Schema(description = "消息类型", example = "text")
|
||||
private String type = "text";
|
||||
|
||||
@Schema(description = "聊天历史(可选)")
|
||||
private List<ChatMessage> history;
|
||||
|
||||
@Schema(description = "是否需要情绪分析", example = "true")
|
||||
private Boolean needEmotionAnalysis = true;
|
||||
|
||||
@Schema(description = "上下文信息")
|
||||
private String context;
|
||||
|
||||
/**
|
||||
* 聊天消息
|
||||
*/
|
||||
@Data
|
||||
@Schema(description = "聊天消息")
|
||||
public static class ChatMessage {
|
||||
@Schema(description = "角色", example = "user")
|
||||
private String role; // user, assistant
|
||||
|
||||
@Schema(description = "消息内容")
|
||||
private String content;
|
||||
|
||||
@Schema(description = "时间戳")
|
||||
private Long timestamp;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
package com.emotionmuseum.ai.dto;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonFormat;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 聊天响应
|
||||
*
|
||||
* @author emotion-museum
|
||||
* @since 2025-07-12
|
||||
*/
|
||||
@Data
|
||||
@Schema(description = "聊天响应")
|
||||
public class ChatResponse {
|
||||
|
||||
@Schema(description = "消息ID")
|
||||
private String messageId;
|
||||
|
||||
@Schema(description = "对话ID")
|
||||
private String conversationId;
|
||||
|
||||
@Schema(description = "AI回复内容")
|
||||
private String content;
|
||||
|
||||
@Schema(description = "消息类型", example = "text")
|
||||
private String type = "text";
|
||||
|
||||
@Schema(description = "发送者", example = "assistant")
|
||||
private String sender = "assistant";
|
||||
|
||||
@Schema(description = "响应时间")
|
||||
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
|
||||
private LocalDateTime timestamp;
|
||||
|
||||
@Schema(description = "情绪分析结果")
|
||||
private EmotionAnalysisResponse emotionAnalysis;
|
||||
|
||||
@Schema(description = "使用情况")
|
||||
private Usage usage;
|
||||
|
||||
@Schema(description = "元数据")
|
||||
private Map<String, Object> metadata;
|
||||
|
||||
@Schema(description = "是否为多条消息")
|
||||
private Boolean multipleMessages = false;
|
||||
|
||||
@Schema(description = "消息数量")
|
||||
private Integer messageCount = 1;
|
||||
|
||||
@Schema(description = "所有消息ID列表(当拆分为多条消息时)")
|
||||
private List<String> messageIds;
|
||||
|
||||
/**
|
||||
* 使用情况
|
||||
*/
|
||||
@Data
|
||||
@Schema(description = "使用情况")
|
||||
public static class Usage {
|
||||
@Schema(description = "输入Token数")
|
||||
private Integer promptTokens;
|
||||
|
||||
@Schema(description = "输出Token数")
|
||||
private Integer completionTokens;
|
||||
|
||||
@Schema(description = "总Token数")
|
||||
private Integer totalTokens;
|
||||
}
|
||||
}
|
||||
+81
@@ -0,0 +1,81 @@
|
||||
package com.emotionmuseum.ai.dto;
|
||||
|
||||
import lombok.Data;
|
||||
import lombok.Builder;
|
||||
import lombok.NoArgsConstructor;
|
||||
import lombok.AllArgsConstructor;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
/**
|
||||
* 会话列表响应DTO
|
||||
*
|
||||
* @author emotion-museum
|
||||
* @since 2025-07-13
|
||||
*/
|
||||
@Data
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class ConversationListResponse {
|
||||
|
||||
/**
|
||||
* 会话ID
|
||||
*/
|
||||
private String conversationId;
|
||||
|
||||
/**
|
||||
* 会话标题
|
||||
*/
|
||||
private String title;
|
||||
|
||||
/**
|
||||
* 会话类型
|
||||
*/
|
||||
private String type;
|
||||
|
||||
/**
|
||||
* 会话状态
|
||||
*/
|
||||
private String status;
|
||||
|
||||
/**
|
||||
* 用户ID
|
||||
*/
|
||||
private String userId;
|
||||
|
||||
/**
|
||||
* 用户类型
|
||||
*/
|
||||
private String userType;
|
||||
|
||||
/**
|
||||
* 消息数量
|
||||
*/
|
||||
private Integer messageCount;
|
||||
|
||||
/**
|
||||
* 最后活跃时间
|
||||
*/
|
||||
private LocalDateTime lastActiveTime;
|
||||
|
||||
/**
|
||||
* 创建时间
|
||||
*/
|
||||
private LocalDateTime createTime;
|
||||
|
||||
/**
|
||||
* 主要情绪
|
||||
*/
|
||||
private String primaryEmotion;
|
||||
|
||||
/**
|
||||
* 情绪强度
|
||||
*/
|
||||
private Double emotionIntensity;
|
||||
|
||||
/**
|
||||
* Coze会话ID
|
||||
*/
|
||||
private String cozeConversationId;
|
||||
}
|
||||
+33
@@ -0,0 +1,33 @@
|
||||
package com.emotionmuseum.ai.dto;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
|
||||
/**
|
||||
* 创建会话请求
|
||||
*
|
||||
* @author emotion-museum
|
||||
* @since 2025-07-12
|
||||
*/
|
||||
@Data
|
||||
@Schema(description = "创建会话请求")
|
||||
public class CreateConversationRequest {
|
||||
|
||||
@Schema(description = "用户ID", example = "user_123")
|
||||
@NotBlank(message = "用户ID不能为空")
|
||||
private String userId;
|
||||
|
||||
@Schema(description = "会话标题", example = "今日心情分享")
|
||||
private String title;
|
||||
|
||||
@Schema(description = "会话类型", example = "emotion_chat")
|
||||
private String type = "emotion_chat";
|
||||
|
||||
@Schema(description = "初始消息", example = "你好,我想聊聊今天的心情")
|
||||
private String initialMessage;
|
||||
|
||||
@Schema(description = "上下文信息")
|
||||
private String context;
|
||||
}
|
||||
+48
@@ -0,0 +1,48 @@
|
||||
package com.emotionmuseum.ai.dto;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonFormat;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 创建会话响应
|
||||
*
|
||||
* @author emotion-museum
|
||||
* @since 2025-07-12
|
||||
*/
|
||||
@Data
|
||||
@Schema(description = "创建会话响应")
|
||||
public class CreateConversationResponse {
|
||||
|
||||
@Schema(description = "会话ID")
|
||||
private String conversationId;
|
||||
|
||||
@Schema(description = "用户ID")
|
||||
private String userId;
|
||||
|
||||
@Schema(description = "会话标题")
|
||||
private String title;
|
||||
|
||||
@Schema(description = "会话类型")
|
||||
private String type;
|
||||
|
||||
@Schema(description = "会话状态", example = "active")
|
||||
private String status = "active";
|
||||
|
||||
@Schema(description = "创建时间")
|
||||
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
|
||||
private LocalDateTime createTime;
|
||||
|
||||
@Schema(description = "更新时间")
|
||||
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
|
||||
private LocalDateTime updateTime;
|
||||
|
||||
@Schema(description = "Coze会话ID")
|
||||
private String cozeConversationId;
|
||||
|
||||
@Schema(description = "元数据")
|
||||
private Map<String, Object> metadata;
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
package com.emotionmuseum.ai.dto;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
import jakarta.validation.constraints.Size;
|
||||
|
||||
/**
|
||||
* 情绪分析请求
|
||||
*
|
||||
* @author emotion-museum
|
||||
* @since 2025-07-12
|
||||
*/
|
||||
@Data
|
||||
@Schema(description = "情绪分析请求")
|
||||
public class EmotionAnalysisRequest {
|
||||
|
||||
@Schema(description = "用户ID", example = "user_123")
|
||||
@NotBlank(message = "用户ID不能为空")
|
||||
private String userId;
|
||||
|
||||
@Schema(description = "待分析文本", example = "我今天感觉很沮丧,工作压力很大")
|
||||
@NotBlank(message = "待分析文本不能为空")
|
||||
@Size(max = 1000, message = "待分析文本不能超过1000字符")
|
||||
private String text;
|
||||
|
||||
@Schema(description = "分析类型", example = "detailed")
|
||||
private String analysisType = "detailed"; // simple, detailed
|
||||
|
||||
@Schema(description = "语言", example = "zh")
|
||||
private String language = "zh";
|
||||
|
||||
@Schema(description = "上下文信息")
|
||||
private String context;
|
||||
}
|
||||
+64
@@ -0,0 +1,64 @@
|
||||
package com.emotionmuseum.ai.dto;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonFormat;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 情绪分析响应
|
||||
*
|
||||
* @author emotion-museum
|
||||
* @since 2025-07-12
|
||||
*/
|
||||
@Data
|
||||
@Schema(description = "情绪分析响应")
|
||||
public class EmotionAnalysisResponse {
|
||||
|
||||
@Schema(description = "主要情绪", example = "焦虑")
|
||||
private String primaryEmotion;
|
||||
|
||||
@Schema(description = "情绪强度", example = "0.75")
|
||||
private Double intensity;
|
||||
|
||||
@Schema(description = "情绪极性", example = "negative")
|
||||
private String polarity; // positive, negative, neutral
|
||||
|
||||
@Schema(description = "置信度", example = "0.85")
|
||||
private Double confidence;
|
||||
|
||||
@Schema(description = "情绪分布")
|
||||
private List<EmotionScore> emotions;
|
||||
|
||||
@Schema(description = "关键词")
|
||||
private List<String> keywords;
|
||||
|
||||
@Schema(description = "建议")
|
||||
private String suggestion;
|
||||
|
||||
@Schema(description = "分析时间")
|
||||
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
|
||||
private LocalDateTime analysisTime;
|
||||
|
||||
@Schema(description = "额外信息")
|
||||
private Map<String, Object> metadata;
|
||||
|
||||
/**
|
||||
* 情绪得分
|
||||
*/
|
||||
@Data
|
||||
@Schema(description = "情绪得分")
|
||||
public static class EmotionScore {
|
||||
@Schema(description = "情绪名称")
|
||||
private String emotion;
|
||||
|
||||
@Schema(description = "得分")
|
||||
private Double score;
|
||||
|
||||
@Schema(description = "描述")
|
||||
private String description;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
package com.emotionmuseum.ai.dto;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
/**
|
||||
* 访客聊天请求DTO
|
||||
*
|
||||
* @author emotion-museum
|
||||
* @since 2025-07-13
|
||||
*/
|
||||
@Data
|
||||
public class GuestChatRequest {
|
||||
|
||||
/**
|
||||
* 消息内容
|
||||
*/
|
||||
private String message;
|
||||
|
||||
/**
|
||||
* 会话ID (可选,如果不提供则创建新会话)
|
||||
*/
|
||||
private String conversationId;
|
||||
|
||||
/**
|
||||
* 会话标题 (创建新会话时使用)
|
||||
*/
|
||||
private String title;
|
||||
|
||||
/**
|
||||
* 客户端IP地址
|
||||
*/
|
||||
private String clientIp;
|
||||
|
||||
/**
|
||||
* 用户代理信息
|
||||
*/
|
||||
private String userAgent;
|
||||
|
||||
/**
|
||||
* 消息类型 (默认为text)
|
||||
*/
|
||||
private String messageType = "text";
|
||||
|
||||
/**
|
||||
* 是否流式响应
|
||||
*/
|
||||
private Boolean stream = false;
|
||||
|
||||
/**
|
||||
* 附加上下文信息
|
||||
*/
|
||||
private String context;
|
||||
}
|
||||
@@ -0,0 +1,123 @@
|
||||
package com.emotionmuseum.ai.dto;
|
||||
|
||||
import lombok.Data;
|
||||
import lombok.Builder;
|
||||
import lombok.NoArgsConstructor;
|
||||
import lombok.AllArgsConstructor;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
/**
|
||||
* 访客聊天响应DTO
|
||||
*
|
||||
* @author emotion-museum
|
||||
* @since 2025-07-13
|
||||
*/
|
||||
@Data
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class GuestChatResponse {
|
||||
|
||||
/**
|
||||
* 访客用户ID
|
||||
*/
|
||||
private String guestUserId;
|
||||
|
||||
/**
|
||||
* 访客昵称
|
||||
*/
|
||||
private String guestNickname;
|
||||
|
||||
/**
|
||||
* 会话ID
|
||||
*/
|
||||
private String conversationId;
|
||||
|
||||
/**
|
||||
* 会话标题
|
||||
*/
|
||||
private String conversationTitle;
|
||||
|
||||
/**
|
||||
* 用户消息ID
|
||||
*/
|
||||
private String userMessageId;
|
||||
|
||||
/**
|
||||
* AI回复消息ID
|
||||
*/
|
||||
private String aiMessageId;
|
||||
|
||||
/**
|
||||
* 用户消息内容
|
||||
*/
|
||||
private String userMessage;
|
||||
|
||||
/**
|
||||
* AI回复内容
|
||||
*/
|
||||
private String aiReply;
|
||||
|
||||
/**
|
||||
* 消息时间戳
|
||||
*/
|
||||
private LocalDateTime timestamp;
|
||||
|
||||
/**
|
||||
* 会话状态
|
||||
*/
|
||||
private String conversationStatus;
|
||||
|
||||
/**
|
||||
* 是否为新会话
|
||||
*/
|
||||
private Boolean isNewConversation;
|
||||
|
||||
/**
|
||||
* Coze聊天ID
|
||||
*/
|
||||
private String cozeChatId;
|
||||
|
||||
/**
|
||||
* 情绪分析结果
|
||||
*/
|
||||
private EmotionAnalysisResult emotionAnalysis;
|
||||
|
||||
/**
|
||||
* Token使用情况
|
||||
*/
|
||||
private TokenUsage tokenUsage;
|
||||
|
||||
/**
|
||||
* 错误信息 (如果有)
|
||||
*/
|
||||
private String errorMessage;
|
||||
|
||||
/**
|
||||
* 情绪分析结果内部类
|
||||
*/
|
||||
@Data
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public static class EmotionAnalysisResult {
|
||||
private String primaryEmotion;
|
||||
private Double emotionScore;
|
||||
private Double confidence;
|
||||
private String emotionTrend;
|
||||
}
|
||||
|
||||
/**
|
||||
* Token使用情况内部类
|
||||
*/
|
||||
@Data
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public static class TokenUsage {
|
||||
private Integer promptTokens;
|
||||
private Integer completionTokens;
|
||||
private Integer totalTokens;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
package com.emotionmuseum.ai.dto;
|
||||
|
||||
import lombok.Data;
|
||||
import lombok.Builder;
|
||||
import lombok.NoArgsConstructor;
|
||||
import lombok.AllArgsConstructor;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
/**
|
||||
* 访客用户信息DTO
|
||||
*
|
||||
* @author emotion-museum
|
||||
* @since 2025-07-13
|
||||
*/
|
||||
@Data
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class GuestUserInfo {
|
||||
|
||||
/**
|
||||
* 访客用户ID (格式: guest_xxx)
|
||||
*/
|
||||
private String guestUserId;
|
||||
|
||||
/**
|
||||
* 客户端IP地址
|
||||
*/
|
||||
private String ipAddress;
|
||||
|
||||
/**
|
||||
* 用户代理信息
|
||||
*/
|
||||
private String userAgent;
|
||||
|
||||
/**
|
||||
* 访客昵称
|
||||
*/
|
||||
private String nickname;
|
||||
|
||||
/**
|
||||
* 访客头像
|
||||
*/
|
||||
private String avatar;
|
||||
|
||||
/**
|
||||
* 创建时间
|
||||
*/
|
||||
private LocalDateTime createTime;
|
||||
|
||||
/**
|
||||
* 最后活跃时间
|
||||
*/
|
||||
private LocalDateTime lastActiveTime;
|
||||
|
||||
/**
|
||||
* 是否为访客用户
|
||||
*/
|
||||
private Boolean isGuest;
|
||||
|
||||
/**
|
||||
* 会话数量
|
||||
*/
|
||||
private Integer conversationCount;
|
||||
|
||||
/**
|
||||
* 消息数量
|
||||
*/
|
||||
private Integer messageCount;
|
||||
}
|
||||
@@ -0,0 +1,97 @@
|
||||
package com.emotionmuseum.ai.dto;
|
||||
|
||||
import lombok.Data;
|
||||
import lombok.Builder;
|
||||
import lombok.NoArgsConstructor;
|
||||
import lombok.AllArgsConstructor;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
/**
|
||||
* 消息列表响应DTO
|
||||
*
|
||||
* @author emotion-museum
|
||||
* @since 2025-07-13
|
||||
*/
|
||||
@Data
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class MessageListResponse {
|
||||
|
||||
/**
|
||||
* 消息ID
|
||||
*/
|
||||
private String messageId;
|
||||
|
||||
/**
|
||||
* 会话ID
|
||||
*/
|
||||
private String conversationId;
|
||||
|
||||
/**
|
||||
* 消息内容
|
||||
*/
|
||||
private String content;
|
||||
|
||||
/**
|
||||
* 消息类型
|
||||
*/
|
||||
private String type;
|
||||
|
||||
/**
|
||||
* 发送者
|
||||
*/
|
||||
private String sender;
|
||||
|
||||
/**
|
||||
* 时间戳
|
||||
*/
|
||||
private LocalDateTime timestamp;
|
||||
|
||||
/**
|
||||
* 消息状态
|
||||
*/
|
||||
private String status;
|
||||
|
||||
/**
|
||||
* 情绪类型
|
||||
*/
|
||||
private String emotionType;
|
||||
|
||||
/**
|
||||
* 情绪分数
|
||||
*/
|
||||
private BigDecimal emotionScore;
|
||||
|
||||
/**
|
||||
* 情绪置信度
|
||||
*/
|
||||
private BigDecimal emotionConfidence;
|
||||
|
||||
/**
|
||||
* 是否已读
|
||||
*/
|
||||
private Integer isRead;
|
||||
|
||||
/**
|
||||
* Coze聊天ID
|
||||
*/
|
||||
private String cozeChatId;
|
||||
|
||||
/**
|
||||
* Coze消息ID
|
||||
*/
|
||||
private String cozeMessageId;
|
||||
|
||||
/**
|
||||
* 用户ID
|
||||
*/
|
||||
private String userId;
|
||||
|
||||
/**
|
||||
* 用户类型
|
||||
*/
|
||||
private String userType;
|
||||
}
|
||||
@@ -0,0 +1,185 @@
|
||||
package com.emotionmuseum.ai.entity;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.TableField;
|
||||
import com.baomidou.mybatisplus.annotation.TableLogic;
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
import com.baomidou.mybatisplus.extension.handlers.JacksonTypeHandler;
|
||||
import com.emotionmuseum.common.entity.BaseEntity;
|
||||
import com.fasterxml.jackson.annotation.JsonFormat;
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 对话实体
|
||||
*
|
||||
* @author emotion-museum
|
||||
* @since 2025-07-12
|
||||
*/
|
||||
@Data
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
@TableName(value = "conversation", autoResultMap = true)
|
||||
public class Conversation extends BaseEntity {
|
||||
|
||||
/**
|
||||
* 用户ID (注册用户ID或访客用户ID)
|
||||
*/
|
||||
@TableField("user_id")
|
||||
private String userId;
|
||||
|
||||
/**
|
||||
* 用户类型 (registered: 注册用户, guest: 访客用户)
|
||||
*/
|
||||
@TableField("user_type")
|
||||
private String userType;
|
||||
|
||||
/**
|
||||
* 对话标题
|
||||
*/
|
||||
@TableField("title")
|
||||
private String title;
|
||||
|
||||
/**
|
||||
* 会话类型
|
||||
*/
|
||||
@TableField("type")
|
||||
private String type;
|
||||
|
||||
/**
|
||||
* 会话状态 (active, ended, archived)
|
||||
*/
|
||||
@TableField("status")
|
||||
private String status;
|
||||
|
||||
/**
|
||||
* Coze会话ID
|
||||
*/
|
||||
@TableField("coze_conversation_id")
|
||||
private String cozeConversationId;
|
||||
|
||||
/**
|
||||
* Bot ID
|
||||
*/
|
||||
@TableField("bot_id")
|
||||
private String botId;
|
||||
|
||||
/**
|
||||
* Workflow ID
|
||||
*/
|
||||
@TableField("workflow_id")
|
||||
private String workflowId;
|
||||
|
||||
/**
|
||||
* 初始消息
|
||||
*/
|
||||
@TableField("initial_message")
|
||||
private String initialMessage;
|
||||
|
||||
/**
|
||||
* 上下文信息
|
||||
*/
|
||||
@TableField("context")
|
||||
private String context;
|
||||
|
||||
/**
|
||||
* 开始时间
|
||||
*/
|
||||
@TableField("start_time")
|
||||
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
|
||||
private LocalDateTime startTime;
|
||||
|
||||
/**
|
||||
* 结束时间
|
||||
*/
|
||||
@TableField("end_time")
|
||||
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
|
||||
private LocalDateTime endTime;
|
||||
|
||||
/**
|
||||
* 最后活跃时间
|
||||
*/
|
||||
@TableField("last_active_time")
|
||||
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
|
||||
private LocalDateTime lastActiveTime;
|
||||
|
||||
/**
|
||||
* 对话摘要
|
||||
*/
|
||||
@TableField("summary")
|
||||
private String summary;
|
||||
|
||||
/**
|
||||
* 标签
|
||||
*/
|
||||
@TableField(value = "tags", typeHandler = JacksonTypeHandler.class)
|
||||
private List<String> tags;
|
||||
|
||||
/**
|
||||
* 主要情绪
|
||||
*/
|
||||
@TableField("primary_emotion")
|
||||
private String primaryEmotion;
|
||||
|
||||
/**
|
||||
* 情绪强度
|
||||
*/
|
||||
@TableField("emotion_intensity")
|
||||
private BigDecimal emotionIntensity;
|
||||
|
||||
/**
|
||||
* 情绪趋势
|
||||
*/
|
||||
@TableField("emotion_trend")
|
||||
private String emotionTrend;
|
||||
|
||||
/**
|
||||
* 关键词
|
||||
*/
|
||||
@TableField(value = "keywords", typeHandler = JacksonTypeHandler.class)
|
||||
private List<String> keywords;
|
||||
|
||||
/**
|
||||
* AI洞察
|
||||
*/
|
||||
@TableField("ai_insights")
|
||||
private String aiInsights;
|
||||
|
||||
/**
|
||||
* 分析置信度
|
||||
*/
|
||||
@TableField("confidence")
|
||||
private BigDecimal confidence;
|
||||
|
||||
/**
|
||||
* 消息数量
|
||||
*/
|
||||
@TableField("message_count")
|
||||
private Integer messageCount;
|
||||
|
||||
/**
|
||||
* 总Token使用量
|
||||
*/
|
||||
@TableField("total_tokens")
|
||||
private Integer totalTokens;
|
||||
|
||||
/**
|
||||
* 总费用
|
||||
*/
|
||||
@TableField("total_cost")
|
||||
private BigDecimal totalCost;
|
||||
|
||||
/**
|
||||
* 客户端IP地址 (用于访客用户)
|
||||
*/
|
||||
@TableField("client_ip")
|
||||
private String clientIp;
|
||||
|
||||
/**
|
||||
* 用户代理信息
|
||||
*/
|
||||
@TableField("user_agent")
|
||||
private String userAgent;
|
||||
}
|
||||
@@ -0,0 +1,263 @@
|
||||
package com.emotionmuseum.ai.entity;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.TableField;
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
import com.baomidou.mybatisplus.extension.handlers.JacksonTypeHandler;
|
||||
import com.emotionmuseum.common.entity.BaseEntity;
|
||||
import com.fasterxml.jackson.annotation.JsonFormat;
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* Coze API调用记录实体
|
||||
*
|
||||
* @author emotion-museum
|
||||
* @since 2025-07-12
|
||||
*/
|
||||
@Data
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
@TableName(value = "coze_api_call", autoResultMap = true)
|
||||
public class CozeApiCall extends BaseEntity {
|
||||
|
||||
/**
|
||||
* 对话ID
|
||||
*/
|
||||
@TableField("conversation_id")
|
||||
private String conversationId;
|
||||
|
||||
/**
|
||||
* 消息ID
|
||||
*/
|
||||
@TableField("message_id")
|
||||
private String messageId;
|
||||
|
||||
/**
|
||||
* Coze聊天ID
|
||||
*/
|
||||
@TableField("coze_chat_id")
|
||||
private String cozeChatId;
|
||||
|
||||
/**
|
||||
* Coze对话ID
|
||||
*/
|
||||
@TableField("coze_conversation_id")
|
||||
private String cozeConversationId;
|
||||
|
||||
/**
|
||||
* Bot ID
|
||||
*/
|
||||
@TableField("bot_id")
|
||||
private String botId;
|
||||
|
||||
/**
|
||||
* Workflow ID
|
||||
*/
|
||||
@TableField("workflow_id")
|
||||
private String workflowId;
|
||||
|
||||
/**
|
||||
* 用户ID
|
||||
*/
|
||||
@TableField("user_id")
|
||||
private String userId;
|
||||
|
||||
/**
|
||||
* 请求类型:chat/stream/retrieve/messages
|
||||
*/
|
||||
@TableField("request_type")
|
||||
private String requestType;
|
||||
|
||||
/**
|
||||
* 用户消息内容
|
||||
*/
|
||||
@TableField("user_message")
|
||||
private String userMessage;
|
||||
|
||||
/**
|
||||
* 用户消息类型:text/image/file
|
||||
*/
|
||||
@TableField("user_message_type")
|
||||
private String userMessageType;
|
||||
|
||||
/**
|
||||
* AI回复内容
|
||||
*/
|
||||
@TableField("ai_reply")
|
||||
private String aiReply;
|
||||
|
||||
/**
|
||||
* AI回复类型:text/image/file
|
||||
*/
|
||||
@TableField("ai_reply_type")
|
||||
private String aiReplyType;
|
||||
|
||||
/**
|
||||
* 请求URL
|
||||
*/
|
||||
@TableField("request_url")
|
||||
private String requestUrl;
|
||||
|
||||
/**
|
||||
* 请求体
|
||||
*/
|
||||
@TableField(value = "request_body", typeHandler = JacksonTypeHandler.class)
|
||||
private Map<String, Object> requestBody;
|
||||
|
||||
/**
|
||||
* 请求头
|
||||
*/
|
||||
@TableField(value = "request_headers", typeHandler = JacksonTypeHandler.class)
|
||||
private Map<String, Object> requestHeaders;
|
||||
|
||||
/**
|
||||
* HTTP状态码
|
||||
*/
|
||||
@TableField("response_status")
|
||||
private Integer responseStatus;
|
||||
|
||||
/**
|
||||
* 响应体
|
||||
*/
|
||||
@TableField(value = "response_body", typeHandler = JacksonTypeHandler.class)
|
||||
private Map<String, Object> responseBody;
|
||||
|
||||
/**
|
||||
* 响应头
|
||||
*/
|
||||
@TableField(value = "response_headers", typeHandler = JacksonTypeHandler.class)
|
||||
private Map<String, Object> responseHeaders;
|
||||
|
||||
/**
|
||||
* 轮询次数
|
||||
*/
|
||||
@TableField("poll_count")
|
||||
private Integer pollCount;
|
||||
|
||||
/**
|
||||
* 轮询开始时间
|
||||
*/
|
||||
@TableField("poll_start_time")
|
||||
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
|
||||
private LocalDateTime pollStartTime;
|
||||
|
||||
/**
|
||||
* 轮询结束时间
|
||||
*/
|
||||
@TableField("poll_end_time")
|
||||
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
|
||||
private LocalDateTime pollEndTime;
|
||||
|
||||
/**
|
||||
* 最终状态:completed/failed/timeout
|
||||
*/
|
||||
@TableField("final_status")
|
||||
private String finalStatus;
|
||||
|
||||
/**
|
||||
* 调用状态:pending/success/failed/timeout
|
||||
*/
|
||||
@TableField("status")
|
||||
private String status;
|
||||
|
||||
/**
|
||||
* 开始时间
|
||||
*/
|
||||
@TableField("start_time")
|
||||
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
|
||||
private LocalDateTime startTime;
|
||||
|
||||
/**
|
||||
* 结束时间
|
||||
*/
|
||||
@TableField("end_time")
|
||||
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
|
||||
private LocalDateTime endTime;
|
||||
|
||||
/**
|
||||
* 耗时(毫秒)
|
||||
*/
|
||||
@TableField("duration_ms")
|
||||
private Integer durationMs;
|
||||
|
||||
/**
|
||||
* 输入Token数
|
||||
*/
|
||||
@TableField("prompt_tokens")
|
||||
private Integer promptTokens;
|
||||
|
||||
/**
|
||||
* 输出Token数
|
||||
*/
|
||||
@TableField("completion_tokens")
|
||||
private Integer completionTokens;
|
||||
|
||||
/**
|
||||
* 总Token数
|
||||
*/
|
||||
@TableField("total_tokens")
|
||||
private Integer totalTokens;
|
||||
|
||||
/**
|
||||
* 费用
|
||||
*/
|
||||
@TableField("cost")
|
||||
private BigDecimal cost;
|
||||
|
||||
/**
|
||||
* 函数调用记录
|
||||
*/
|
||||
@TableField(value = "function_calls", typeHandler = JacksonTypeHandler.class)
|
||||
private Map<String, Object> functionCalls;
|
||||
|
||||
/**
|
||||
* 函数调用结果
|
||||
*/
|
||||
@TableField(value = "function_results", typeHandler = JacksonTypeHandler.class)
|
||||
private Map<String, Object> functionResults;
|
||||
|
||||
/**
|
||||
* 错误代码
|
||||
*/
|
||||
@TableField("error_code")
|
||||
private String errorCode;
|
||||
|
||||
/**
|
||||
* 错误信息
|
||||
*/
|
||||
@TableField("error_message")
|
||||
private String errorMessage;
|
||||
|
||||
/**
|
||||
* 客户端IP
|
||||
*/
|
||||
@TableField("client_ip")
|
||||
private String clientIp;
|
||||
|
||||
/**
|
||||
* 用户代理
|
||||
*/
|
||||
@TableField("user_agent")
|
||||
private String userAgent;
|
||||
|
||||
/**
|
||||
* 会话ID
|
||||
*/
|
||||
@TableField("session_id")
|
||||
private String sessionId;
|
||||
|
||||
/**
|
||||
* 追踪ID
|
||||
*/
|
||||
@TableField("trace_id")
|
||||
private String traceId;
|
||||
|
||||
/**
|
||||
* 扩展元数据
|
||||
*/
|
||||
@TableField(value = "metadata", typeHandler = JacksonTypeHandler.class)
|
||||
private Map<String, Object> metadata;
|
||||
}
|
||||
@@ -0,0 +1,99 @@
|
||||
package com.emotionmuseum.ai.entity;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.TableField;
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
import com.baomidou.mybatisplus.extension.handlers.JacksonTypeHandler;
|
||||
import com.emotionmuseum.common.entity.BaseEntity;
|
||||
import com.fasterxml.jackson.annotation.JsonFormat;
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 情绪分析实体
|
||||
*
|
||||
* @author emotion-museum
|
||||
* @since 2025-07-12
|
||||
*/
|
||||
@Data
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
@TableName(value = "emotion_analysis", autoResultMap = true)
|
||||
public class EmotionAnalysis extends BaseEntity {
|
||||
|
||||
/**
|
||||
* 用户ID
|
||||
*/
|
||||
@TableField("user_id")
|
||||
private String userId;
|
||||
|
||||
/**
|
||||
* 消息ID
|
||||
*/
|
||||
@TableField("message_id")
|
||||
private String messageId;
|
||||
|
||||
/**
|
||||
* 分析文本
|
||||
*/
|
||||
@TableField("text")
|
||||
private String text;
|
||||
|
||||
/**
|
||||
* 主要情绪
|
||||
*/
|
||||
@TableField("primary_emotion")
|
||||
private String primaryEmotion;
|
||||
|
||||
/**
|
||||
* 情绪强度
|
||||
*/
|
||||
@TableField("intensity")
|
||||
private BigDecimal intensity;
|
||||
|
||||
/**
|
||||
* 情绪极性
|
||||
*/
|
||||
@TableField("polarity")
|
||||
private String polarity;
|
||||
|
||||
/**
|
||||
* 分析置信度
|
||||
*/
|
||||
@TableField("confidence")
|
||||
private BigDecimal confidence;
|
||||
|
||||
/**
|
||||
* 情绪详情
|
||||
*/
|
||||
@TableField(value = "emotions", typeHandler = JacksonTypeHandler.class)
|
||||
private List<Map<String, Object>> emotions;
|
||||
|
||||
/**
|
||||
* 关键词
|
||||
*/
|
||||
@TableField(value = "keywords", typeHandler = JacksonTypeHandler.class)
|
||||
private List<String> keywords;
|
||||
|
||||
/**
|
||||
* 建议
|
||||
*/
|
||||
@TableField("suggestion")
|
||||
private String suggestion;
|
||||
|
||||
/**
|
||||
* 分析时间
|
||||
*/
|
||||
@TableField("analysis_time")
|
||||
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
|
||||
private LocalDateTime analysisTime;
|
||||
|
||||
/**
|
||||
* 元数据
|
||||
*/
|
||||
@TableField(value = "metadata", typeHandler = JacksonTypeHandler.class)
|
||||
private Map<String, Object> metadata;
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
package com.emotionmuseum.ai.entity;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.TableField;
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
import com.emotionmuseum.common.entity.BaseEntity;
|
||||
import com.fasterxml.jackson.annotation.JsonFormat;
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
/**
|
||||
* 访客用户实体
|
||||
*
|
||||
* @author emotion-museum
|
||||
* @since 2025-07-13
|
||||
*/
|
||||
@Data
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
@TableName("guest_user")
|
||||
public class GuestUser extends BaseEntity {
|
||||
|
||||
/**
|
||||
* 访客用户ID (格式: guest_xxx)
|
||||
*/
|
||||
@TableField("guest_user_id")
|
||||
private String guestUserId;
|
||||
|
||||
/**
|
||||
* 客户端IP地址
|
||||
*/
|
||||
@TableField("ip_address")
|
||||
private String ipAddress;
|
||||
|
||||
/**
|
||||
* 用户代理信息
|
||||
*/
|
||||
@TableField("user_agent")
|
||||
private String userAgent;
|
||||
|
||||
/**
|
||||
* 访客昵称
|
||||
*/
|
||||
@TableField("nickname")
|
||||
private String nickname;
|
||||
|
||||
/**
|
||||
* 访客头像
|
||||
*/
|
||||
@TableField("avatar")
|
||||
private String avatar;
|
||||
|
||||
/**
|
||||
* 最后活跃时间
|
||||
*/
|
||||
@TableField("last_active_time")
|
||||
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
|
||||
private LocalDateTime lastActiveTime;
|
||||
|
||||
/**
|
||||
* 会话数量
|
||||
*/
|
||||
@TableField("conversation_count")
|
||||
private Integer conversationCount;
|
||||
|
||||
/**
|
||||
* 消息数量
|
||||
*/
|
||||
@TableField("message_count")
|
||||
private Integer messageCount;
|
||||
|
||||
/**
|
||||
* IP地址的地理位置信息
|
||||
*/
|
||||
@TableField("location")
|
||||
private String location;
|
||||
|
||||
/**
|
||||
* 设备信息
|
||||
*/
|
||||
@TableField("device_info")
|
||||
private String deviceInfo;
|
||||
}
|
||||
@@ -0,0 +1,164 @@
|
||||
package com.emotionmuseum.ai.entity;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.TableField;
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
import com.baomidou.mybatisplus.extension.handlers.JacksonTypeHandler;
|
||||
import com.emotionmuseum.common.entity.BaseEntity;
|
||||
import com.fasterxml.jackson.annotation.JsonFormat;
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 消息实体
|
||||
*
|
||||
* @author emotion-museum
|
||||
* @since 2025-07-12
|
||||
*/
|
||||
@Data
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
@TableName(value = "message", autoResultMap = true)
|
||||
public class Message extends BaseEntity {
|
||||
|
||||
/**
|
||||
* 对话ID
|
||||
*/
|
||||
@TableField("conversation_id")
|
||||
private String conversationId;
|
||||
|
||||
/**
|
||||
* 消息内容
|
||||
*/
|
||||
@TableField("content")
|
||||
private String content;
|
||||
|
||||
/**
|
||||
* 消息类型:text/voice/image/system
|
||||
*/
|
||||
@TableField("type")
|
||||
private String type;
|
||||
|
||||
/**
|
||||
* 发送者:user/ai
|
||||
*/
|
||||
@TableField("sender")
|
||||
private String sender;
|
||||
|
||||
/**
|
||||
* 时间戳
|
||||
*/
|
||||
@TableField("timestamp")
|
||||
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
|
||||
private LocalDateTime timestamp;
|
||||
|
||||
/**
|
||||
* Coze聊天ID
|
||||
*/
|
||||
@TableField("coze_chat_id")
|
||||
private String cozeChatId;
|
||||
|
||||
/**
|
||||
* Coze消息ID
|
||||
*/
|
||||
@TableField("coze_message_id")
|
||||
private String cozeMessageId;
|
||||
|
||||
/**
|
||||
* 消息状态:sending/sent/failed/processing
|
||||
*/
|
||||
@TableField("status")
|
||||
private String status;
|
||||
|
||||
/**
|
||||
* 错误信息
|
||||
*/
|
||||
@TableField("error_message")
|
||||
private String errorMessage;
|
||||
|
||||
/**
|
||||
* 情绪分数
|
||||
*/
|
||||
@TableField("emotion_score")
|
||||
private BigDecimal emotionScore;
|
||||
|
||||
/**
|
||||
* 情绪类型
|
||||
*/
|
||||
@TableField("emotion_type")
|
||||
private String emotionType;
|
||||
|
||||
/**
|
||||
* 情绪分析置信度
|
||||
*/
|
||||
@TableField("emotion_confidence")
|
||||
private BigDecimal emotionConfidence;
|
||||
|
||||
/**
|
||||
* 输入Token数
|
||||
*/
|
||||
@TableField("prompt_tokens")
|
||||
private Integer promptTokens;
|
||||
|
||||
/**
|
||||
* 输出Token数
|
||||
*/
|
||||
@TableField("completion_tokens")
|
||||
private Integer completionTokens;
|
||||
|
||||
/**
|
||||
* 总Token数
|
||||
*/
|
||||
@TableField("total_tokens")
|
||||
private Integer totalTokens;
|
||||
|
||||
/**
|
||||
* API调用费用
|
||||
*/
|
||||
@TableField("api_cost")
|
||||
private BigDecimal apiCost;
|
||||
|
||||
/**
|
||||
* 是否已读:0/1
|
||||
*/
|
||||
@TableField("is_read")
|
||||
private Integer isRead;
|
||||
|
||||
/**
|
||||
* 父消息ID(用于回复链)
|
||||
*/
|
||||
@TableField("parent_message_id")
|
||||
private String parentMessageId;
|
||||
|
||||
/**
|
||||
* 元数据
|
||||
*/
|
||||
@TableField(value = "metadata", typeHandler = JacksonTypeHandler.class)
|
||||
private Map<String, Object> metadata;
|
||||
|
||||
/**
|
||||
* Coze消息角色 (user/assistant/system)
|
||||
*/
|
||||
@TableField("coze_role")
|
||||
private String cozeRole;
|
||||
|
||||
/**
|
||||
* Coze消息内容类型 (text/image/file等)
|
||||
*/
|
||||
@TableField("coze_content_type")
|
||||
private String cozeContentType;
|
||||
|
||||
/**
|
||||
* 用户ID (注册用户或访客用户)
|
||||
*/
|
||||
@TableField("user_id")
|
||||
private String userId;
|
||||
|
||||
/**
|
||||
* 用户类型 (registered/guest)
|
||||
*/
|
||||
@TableField("user_type")
|
||||
private String userType;
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
package com.emotionmuseum.ai.mapper;
|
||||
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import com.emotionmuseum.ai.entity.Conversation;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
import org.apache.ibatis.annotations.Param;
|
||||
import org.apache.ibatis.annotations.Select;
|
||||
import org.apache.ibatis.annotations.Update;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 对话Mapper
|
||||
*
|
||||
* @author emotion-museum
|
||||
* @since 2025-07-12
|
||||
*/
|
||||
@Mapper
|
||||
public interface ConversationMapper extends BaseMapper<Conversation> {
|
||||
|
||||
/**
|
||||
* 根据用户ID查询对话列表
|
||||
*
|
||||
* @param userId 用户ID
|
||||
* @param limit 限制数量
|
||||
* @param offset 偏移量
|
||||
* @return 对话列表
|
||||
*/
|
||||
List<Conversation> selectByUserId(@Param("userId") String userId,
|
||||
@Param("limit") Integer limit,
|
||||
@Param("offset") Integer offset);
|
||||
|
||||
/**
|
||||
* 更新对话摘要
|
||||
*
|
||||
* @param conversationId 对话ID
|
||||
* @param summary 摘要
|
||||
* @param aiInsights AI洞察
|
||||
* @return 更新行数
|
||||
*/
|
||||
int updateSummary(@Param("conversationId") String conversationId,
|
||||
@Param("summary") String summary,
|
||||
@Param("aiInsights") String aiInsights);
|
||||
|
||||
/**
|
||||
* 更新对话情绪分析
|
||||
*
|
||||
* @param conversationId 对话ID
|
||||
* @param primaryEmotion 主要情绪
|
||||
* @param emotionIntensity 情绪强度
|
||||
* @param emotionTrend 情绪趋势
|
||||
* @param confidence 置信度
|
||||
* @return 更新行数
|
||||
*/
|
||||
int updateEmotionAnalysis(@Param("conversationId") String conversationId,
|
||||
@Param("primaryEmotion") String primaryEmotion,
|
||||
@Param("emotionIntensity") Double emotionIntensity,
|
||||
@Param("emotionTrend") String emotionTrend,
|
||||
@Param("confidence") Double confidence);
|
||||
|
||||
/**
|
||||
* 增加消息数量
|
||||
*
|
||||
* @param conversationId 对话ID
|
||||
* @return 更新行数
|
||||
*/
|
||||
int incrementMessageCount(@Param("conversationId") String conversationId);
|
||||
|
||||
/**
|
||||
* 根据用户ID查询活跃会话列表
|
||||
*
|
||||
* @param userId 用户ID
|
||||
* @return 会话列表
|
||||
*/
|
||||
@Select("SELECT * FROM conversation WHERE user_id = #{userId} AND status = 'active' AND is_deleted = 0 ORDER BY update_time DESC")
|
||||
List<Conversation> selectActiveConversationsByUserId(@Param("userId") String userId);
|
||||
|
||||
/**
|
||||
* 更新会话最后活跃时间和消息数量
|
||||
*
|
||||
* @param conversationId 会话ID
|
||||
* @param lastActiveTime 最后活跃时间
|
||||
* @param messageCount 消息数量
|
||||
* @return 更新行数
|
||||
*/
|
||||
@Update("UPDATE conversation SET last_active_time = #{lastActiveTime}, message_count = #{messageCount}, update_time = NOW() WHERE id = #{conversationId}")
|
||||
int updateLastActiveTime(@Param("conversationId") String conversationId,
|
||||
@Param("lastActiveTime") LocalDateTime lastActiveTime,
|
||||
@Param("messageCount") Integer messageCount);
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
package com.emotionmuseum.ai.mapper;
|
||||
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import com.emotionmuseum.ai.entity.CozeApiCall;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
import org.apache.ibatis.annotations.Param;
|
||||
import org.apache.ibatis.annotations.Update;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
/**
|
||||
* Coze API调用记录 Mapper 接口
|
||||
*
|
||||
* @author emotion-museum
|
||||
* @since 2025-07-12
|
||||
*/
|
||||
@Mapper
|
||||
public interface CozeApiCallMapper extends BaseMapper<CozeApiCall> {
|
||||
|
||||
/**
|
||||
* 更新API调用状态
|
||||
*/
|
||||
@Update("UPDATE coze_api_call SET status = #{status}, end_time = #{endTime}, update_time = #{updateTime}, response_body = #{responseBody} WHERE id = #{id}")
|
||||
int updateStatusById(@Param("id") String id,
|
||||
@Param("status") String status,
|
||||
@Param("endTime") LocalDateTime endTime,
|
||||
@Param("updateTime") LocalDateTime updateTime,
|
||||
@Param("responseBody") String responseBody);
|
||||
|
||||
/**
|
||||
* 更新API调用状态(带错误信息)
|
||||
*/
|
||||
@Update("UPDATE coze_api_call SET status = #{status}, end_time = #{endTime}, update_time = #{updateTime}, error_message = #{errorMessage} WHERE id = #{id}")
|
||||
int updateStatusWithErrorById(@Param("id") String id,
|
||||
@Param("status") String status,
|
||||
@Param("endTime") LocalDateTime endTime,
|
||||
@Param("updateTime") LocalDateTime updateTime,
|
||||
@Param("errorMessage") String errorMessage);
|
||||
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
package com.emotionmuseum.ai.mapper;
|
||||
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import com.emotionmuseum.ai.entity.GuestUser;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
import org.apache.ibatis.annotations.Param;
|
||||
import org.apache.ibatis.annotations.Select;
|
||||
import org.apache.ibatis.annotations.Update;
|
||||
|
||||
/**
|
||||
* 访客用户Mapper
|
||||
*
|
||||
* @author emotion-museum
|
||||
* @since 2025-07-13
|
||||
*/
|
||||
@Mapper
|
||||
public interface GuestUserMapper extends BaseMapper<GuestUser> {
|
||||
|
||||
/**
|
||||
* 根据IP地址查找访客用户
|
||||
*
|
||||
* @param ipAddress IP地址
|
||||
* @return 访客用户
|
||||
*/
|
||||
@Select("SELECT * FROM guest_user WHERE ip_address = #{ipAddress} AND is_deleted = 0 ORDER BY create_time DESC LIMIT 1")
|
||||
GuestUser findByIpAddress(@Param("ipAddress") String ipAddress);
|
||||
|
||||
/**
|
||||
* 根据访客用户ID查找
|
||||
*
|
||||
* @param guestUserId 访客用户ID
|
||||
* @return 访客用户
|
||||
*/
|
||||
@Select("SELECT * FROM guest_user WHERE guest_user_id = #{guestUserId} AND is_deleted = 0")
|
||||
GuestUser findByGuestUserId(@Param("guestUserId") String guestUserId);
|
||||
|
||||
/**
|
||||
* 更新最后活跃时间
|
||||
*
|
||||
* @param guestUserId 访客用户ID
|
||||
* @return 更新行数
|
||||
*/
|
||||
@Update("UPDATE guest_user SET last_active_time = NOW(), update_time = NOW() WHERE guest_user_id = #{guestUserId}")
|
||||
int updateLastActiveTime(@Param("guestUserId") String guestUserId);
|
||||
|
||||
/**
|
||||
* 增加会话数量
|
||||
*
|
||||
* @param guestUserId 访客用户ID
|
||||
* @return 更新行数
|
||||
*/
|
||||
@Update("UPDATE guest_user SET conversation_count = conversation_count + 1, update_time = NOW() WHERE guest_user_id = #{guestUserId}")
|
||||
int incrementConversationCount(@Param("guestUserId") String guestUserId);
|
||||
|
||||
/**
|
||||
* 增加消息数量
|
||||
*
|
||||
* @param guestUserId 访客用户ID
|
||||
* @param count 增加数量
|
||||
* @return 更新行数
|
||||
*/
|
||||
@Update("UPDATE guest_user SET message_count = message_count + #{count}, update_time = NOW() WHERE guest_user_id = #{guestUserId}")
|
||||
int incrementMessageCount(@Param("guestUserId") String guestUserId, @Param("count") int count);
|
||||
}
|
||||
@@ -0,0 +1,117 @@
|
||||
package com.emotionmuseum.ai.mapper;
|
||||
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import com.emotionmuseum.ai.entity.Message;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
import org.apache.ibatis.annotations.Param;
|
||||
import org.apache.ibatis.annotations.Select;
|
||||
import org.apache.ibatis.annotations.Update;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 消息Mapper
|
||||
*
|
||||
* @author emotion-museum
|
||||
* @since 2025-07-12
|
||||
*/
|
||||
@Mapper
|
||||
public interface MessageMapper extends BaseMapper<Message> {
|
||||
|
||||
/**
|
||||
* 根据对话ID查询消息列表
|
||||
*
|
||||
* @param conversationId 对话ID
|
||||
* @param limit 限制数量
|
||||
* @param offset 偏移量
|
||||
* @return 消息列表
|
||||
*/
|
||||
List<Message> selectByConversationId(@Param("conversationId") String conversationId,
|
||||
@Param("limit") Integer limit,
|
||||
@Param("offset") Integer offset);
|
||||
|
||||
/**
|
||||
* 标记消息为已读
|
||||
*
|
||||
* @param messageId 消息ID
|
||||
* @return 更新行数
|
||||
*/
|
||||
int markAsRead(@Param("messageId") String messageId);
|
||||
|
||||
/**
|
||||
* 批量标记消息为已读
|
||||
*
|
||||
* @param conversationId 对话ID
|
||||
* @return 更新行数
|
||||
*/
|
||||
int markAllAsRead(@Param("conversationId") String conversationId);
|
||||
|
||||
/**
|
||||
* 获取对话中的最新消息
|
||||
*
|
||||
* @param conversationId 对话ID
|
||||
* @param limit 限制数量
|
||||
* @return 消息列表
|
||||
*/
|
||||
List<Message> selectLatestMessages(@Param("conversationId") String conversationId,
|
||||
@Param("limit") Integer limit);
|
||||
|
||||
/**
|
||||
* 根据会话ID查询消息列表(带分页)
|
||||
*
|
||||
* @param conversationId 会话ID
|
||||
* @param limit 限制数量
|
||||
* @param offset 偏移量
|
||||
* @return 消息列表
|
||||
*/
|
||||
@Select("SELECT * FROM message WHERE conversation_id = #{conversationId} AND is_deleted = 0 ORDER BY timestamp ASC LIMIT #{limit} OFFSET #{offset}")
|
||||
List<Message> selectMessagesByConversationId(@Param("conversationId") String conversationId,
|
||||
@Param("limit") Integer limit,
|
||||
@Param("offset") Integer offset);
|
||||
|
||||
/**
|
||||
* 根据会话ID查询最新消息
|
||||
*
|
||||
* @param conversationId 会话ID
|
||||
* @param limit 限制数量
|
||||
* @return 消息列表
|
||||
*/
|
||||
@Select("SELECT * FROM message WHERE conversation_id = #{conversationId} AND is_deleted = 0 ORDER BY timestamp DESC LIMIT #{limit}")
|
||||
List<Message> selectLatestMessagesByConversationId(@Param("conversationId") String conversationId, @Param("limit") Integer limit);
|
||||
|
||||
/**
|
||||
* 统计会话消息数量
|
||||
*
|
||||
* @param conversationId 会话ID
|
||||
* @return 消息数量
|
||||
*/
|
||||
@Select("SELECT COUNT(*) FROM message WHERE conversation_id = #{conversationId} AND is_deleted = 0")
|
||||
Integer countMessagesByConversationId(@Param("conversationId") String conversationId);
|
||||
|
||||
/**
|
||||
* 标记消息为已读
|
||||
*
|
||||
* @param messageId 消息ID
|
||||
* @return 更新行数
|
||||
*/
|
||||
@Update("UPDATE message SET is_read = 1, update_time = NOW() WHERE id = #{messageId}")
|
||||
int markMessageAsRead(@Param("messageId") String messageId);
|
||||
|
||||
/**
|
||||
* 批量标记会话消息为已读
|
||||
*
|
||||
* @param conversationId 会话ID
|
||||
* @return 更新行数
|
||||
*/
|
||||
@Update("UPDATE message SET is_read = 1, update_time = NOW() WHERE conversation_id = #{conversationId} AND is_read = 0")
|
||||
int markConversationMessagesAsRead(@Param("conversationId") String conversationId);
|
||||
|
||||
/**
|
||||
* 查询未读消息数量
|
||||
*
|
||||
* @param conversationId 会话ID
|
||||
* @return 未读消息数量
|
||||
*/
|
||||
@Select("SELECT COUNT(*) FROM message WHERE conversation_id = #{conversationId} AND is_read = 0 AND is_deleted = 0")
|
||||
Integer countUnreadMessages(@Param("conversationId") String conversationId);
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
package com.emotionmuseum.ai.service;
|
||||
|
||||
import com.emotionmuseum.ai.dto.*;
|
||||
import com.emotionmuseum.ai.entity.Message;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* AI聊天服务接口
|
||||
*
|
||||
* @author emotion-museum
|
||||
* @since 2025-07-12
|
||||
*/
|
||||
public interface AiChatService {
|
||||
|
||||
/**
|
||||
* 创建会话
|
||||
*
|
||||
* @param request 创建会话请求
|
||||
* @return 创建会话响应
|
||||
*/
|
||||
CreateConversationResponse createConversation(CreateConversationRequest request);
|
||||
|
||||
/**
|
||||
* 发送聊天消息
|
||||
*
|
||||
* @param request 聊天请求
|
||||
* @return 聊天响应
|
||||
*/
|
||||
ChatResponse chat(ChatRequest request);
|
||||
|
||||
/**
|
||||
* 情绪分析
|
||||
*
|
||||
* @param request 情绪分析请求
|
||||
* @return 情绪分析响应
|
||||
*/
|
||||
EmotionAnalysisResponse analyzeEmotion(EmotionAnalysisRequest request);
|
||||
|
||||
/**
|
||||
* 流式聊天
|
||||
*
|
||||
* @param request 聊天请求
|
||||
* @return 流式响应
|
||||
*/
|
||||
String streamChat(ChatRequest request);
|
||||
|
||||
/**
|
||||
* 健康检查
|
||||
*
|
||||
* @return 是否健康
|
||||
*/
|
||||
boolean healthCheck();
|
||||
|
||||
/**
|
||||
* 保存AI回复消息(支持拆分多条消息)
|
||||
*
|
||||
* @param conversationId 会话ID
|
||||
* @param aiContent AI回复内容
|
||||
* @param cozeChatId Coze聊天ID
|
||||
* @return 保存的消息列表
|
||||
*/
|
||||
List<Message> saveAiReplyMessages(String conversationId, String aiContent, String cozeChatId);
|
||||
}
|
||||
+207
@@ -0,0 +1,207 @@
|
||||
package com.emotionmuseum.ai.service;
|
||||
|
||||
import com.emotionmuseum.ai.entity.Conversation;
|
||||
import com.emotionmuseum.ai.entity.Message;
|
||||
import com.emotionmuseum.ai.entity.CozeApiCall;
|
||||
import com.emotionmuseum.common.dto.PageQuery;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 会话数据库服务接口
|
||||
*
|
||||
* @author emotion-museum
|
||||
* @since 2025-07-12
|
||||
*/
|
||||
public interface ConversationDbService {
|
||||
|
||||
/**
|
||||
* 保存会话
|
||||
*
|
||||
* @param conversation 会话信息
|
||||
* @return 保存的会话
|
||||
*/
|
||||
Conversation saveConversation(Conversation conversation);
|
||||
|
||||
/**
|
||||
* 根据ID查询会话
|
||||
*
|
||||
* @param conversationId 会话ID
|
||||
* @return 会话信息
|
||||
*/
|
||||
Conversation getConversationById(String conversationId);
|
||||
|
||||
/**
|
||||
* 根据用户ID查询会话列表
|
||||
*
|
||||
* @param userId 用户ID
|
||||
* @param pageQuery 分页查询
|
||||
* @return 会话列表
|
||||
*/
|
||||
List<Conversation> getConversationsByUserId(String userId, PageQuery pageQuery);
|
||||
|
||||
/**
|
||||
* 根据用户ID查询活跃会话列表
|
||||
*
|
||||
* @param userId 用户ID
|
||||
* @return 活跃会话列表
|
||||
*/
|
||||
List<Conversation> getActiveConversationsByUserId(String userId);
|
||||
|
||||
/**
|
||||
* 更新会话状态
|
||||
*
|
||||
* @param conversationId 会话ID
|
||||
* @param status 状态
|
||||
* @return 是否成功
|
||||
*/
|
||||
boolean updateConversationStatus(String conversationId, String status);
|
||||
|
||||
/**
|
||||
* 更新会话活跃时间
|
||||
*
|
||||
* @param conversationId 会话ID
|
||||
* @return 是否成功
|
||||
*/
|
||||
boolean updateConversationActiveTime(String conversationId);
|
||||
|
||||
/**
|
||||
* 保存消息
|
||||
*
|
||||
* @param message 消息信息
|
||||
* @return 保存的消息
|
||||
*/
|
||||
Message saveMessage(Message message);
|
||||
|
||||
/**
|
||||
* 根据会话ID查询消息列表
|
||||
*
|
||||
* @param conversationId 会话ID
|
||||
* @param pageQuery 分页查询
|
||||
* @return 消息列表
|
||||
*/
|
||||
List<Message> getMessagesByConversationId(String conversationId, PageQuery pageQuery);
|
||||
|
||||
/**
|
||||
* 根据会话ID查询最新消息
|
||||
*
|
||||
* @param conversationId 会话ID
|
||||
* @param limit 限制数量
|
||||
* @return 消息列表
|
||||
*/
|
||||
List<Message> getLatestMessages(String conversationId, Integer limit);
|
||||
|
||||
/**
|
||||
* 标记消息为已读
|
||||
*
|
||||
* @param messageId 消息ID
|
||||
* @return 是否成功
|
||||
*/
|
||||
boolean markMessageAsRead(String messageId);
|
||||
|
||||
/**
|
||||
* 标记会话所有消息为已读
|
||||
*
|
||||
* @param conversationId 会话ID
|
||||
* @return 是否成功
|
||||
*/
|
||||
boolean markConversationMessagesAsRead(String conversationId);
|
||||
|
||||
/**
|
||||
* 统计会话消息数量
|
||||
*
|
||||
* @param conversationId 会话ID
|
||||
* @return 消息数量
|
||||
*/
|
||||
Integer getMessageCount(String conversationId);
|
||||
|
||||
/**
|
||||
* 统计未读消息数量
|
||||
*
|
||||
* @param conversationId 会话ID
|
||||
* @return 未读消息数量
|
||||
*/
|
||||
Integer getUnreadMessageCount(String conversationId);
|
||||
|
||||
/**
|
||||
* 删除会话
|
||||
*
|
||||
* @param conversationId 会话ID
|
||||
* @return 是否成功
|
||||
*/
|
||||
boolean deleteConversation(String conversationId);
|
||||
|
||||
/**
|
||||
* 根据Coze对话ID查询会话
|
||||
*
|
||||
* @param cozeConversationId Coze对话ID
|
||||
* @return 会话信息
|
||||
*/
|
||||
Conversation getConversationByCozeId(String cozeConversationId);
|
||||
|
||||
/**
|
||||
* 更新会话的Coze相关信息
|
||||
*
|
||||
* @param conversationId 会话ID
|
||||
* @param cozeConversationId Coze对话ID
|
||||
* @param botId Bot ID
|
||||
* @param workflowId Workflow ID
|
||||
* @return 是否成功
|
||||
*/
|
||||
boolean updateConversationCozeInfo(String conversationId, String cozeConversationId, String botId,
|
||||
String workflowId);
|
||||
|
||||
/**
|
||||
* 更新消息的Coze相关信息
|
||||
*
|
||||
* @param messageId 消息ID
|
||||
* @param cozeChatId Coze聊天ID
|
||||
* @param cozeMessageId Coze消息ID
|
||||
* @param status 状态
|
||||
* @return 是否成功
|
||||
*/
|
||||
boolean updateMessageCozeInfo(String messageId, String cozeChatId, String cozeMessageId, String status);
|
||||
|
||||
/**
|
||||
* 保存Coze API调用记录
|
||||
*
|
||||
* @param cozeApiCall API调用记录
|
||||
* @return 保存的记录
|
||||
*/
|
||||
CozeApiCall saveCozeApiCall(CozeApiCall cozeApiCall);
|
||||
|
||||
/**
|
||||
* 更新Coze API调用记录状态
|
||||
*
|
||||
* @param callId 调用记录ID
|
||||
* @param status 状态
|
||||
* @param responseBody 响应体
|
||||
* @param errorMessage 错误信息
|
||||
* @return 是否成功
|
||||
*/
|
||||
boolean updateCozeApiCallStatus(String callId, String status, Object responseBody, String errorMessage);
|
||||
|
||||
/**
|
||||
* 根据ID获取Coze API调用记录
|
||||
*
|
||||
* @param callId 调用记录ID
|
||||
* @return API调用记录
|
||||
*/
|
||||
CozeApiCall getCozeApiCallById(String callId);
|
||||
|
||||
/**
|
||||
* 更新Coze API调用记录
|
||||
*
|
||||
* @param cozeApiCall API调用记录
|
||||
* @return 是否成功
|
||||
*/
|
||||
boolean updateCozeApiCall(CozeApiCall cozeApiCall);
|
||||
|
||||
/**
|
||||
* 根据ID列表获取消息
|
||||
*
|
||||
* @param messageIds 消息ID列表
|
||||
* @return 消息列表
|
||||
*/
|
||||
List<Message> getMessagesByIds(List<String> messageIds);
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
package com.emotionmuseum.ai.service;
|
||||
|
||||
import com.emotionmuseum.ai.dto.*;
|
||||
import com.emotionmuseum.common.result.Result;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 访客聊天服务接口
|
||||
*
|
||||
* @author emotion-museum
|
||||
* @since 2025-07-13
|
||||
*/
|
||||
public interface GuestChatService {
|
||||
|
||||
/**
|
||||
* 访客聊天
|
||||
*
|
||||
* @param request 聊天请求
|
||||
* @return 聊天响应
|
||||
*/
|
||||
Result<GuestChatResponse> guestChat(GuestChatRequest request);
|
||||
|
||||
/**
|
||||
* 获取访客会话列表
|
||||
*
|
||||
* @param clientIp 客户端IP
|
||||
* @param pageNum 页码
|
||||
* @param pageSize 页大小
|
||||
* @return 会话列表
|
||||
*/
|
||||
Result<List<ConversationListResponse>> getGuestConversations(String clientIp, Integer pageNum, Integer pageSize);
|
||||
|
||||
/**
|
||||
* 获取访客会话消息列表
|
||||
*
|
||||
* @param conversationId 会话ID
|
||||
* @param clientIp 客户端IP
|
||||
* @param pageNum 页码
|
||||
* @param pageSize 页大小
|
||||
* @return 消息列表
|
||||
*/
|
||||
Result<List<MessageListResponse>> getGuestConversationMessages(String conversationId, String clientIp, Integer pageNum, Integer pageSize);
|
||||
|
||||
/**
|
||||
* 结束访客会话
|
||||
*
|
||||
* @param conversationId 会话ID
|
||||
* @param clientIp 客户端IP
|
||||
* @return 操作结果
|
||||
*/
|
||||
Result<Void> endGuestConversation(String conversationId, String clientIp);
|
||||
|
||||
/**
|
||||
* 获取或创建访客用户
|
||||
*
|
||||
* @param clientIp 客户端IP
|
||||
* @param userAgent 用户代理
|
||||
* @return 访客用户信息
|
||||
*/
|
||||
Result<GuestUserInfo> getOrCreateGuestUser(String clientIp, String userAgent);
|
||||
|
||||
/**
|
||||
* 访客情绪分析
|
||||
*
|
||||
* @param request 情绪分析请求
|
||||
* @param clientIp 客户端IP
|
||||
* @return 情绪分析结果
|
||||
*/
|
||||
Result<EmotionAnalysisResponse> analyzeGuestEmotion(EmotionAnalysisRequest request, String clientIp);
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
package com.emotionmuseum.ai.service;
|
||||
|
||||
import com.emotionmuseum.ai.dto.GuestUserInfo;
|
||||
|
||||
/**
|
||||
* 访客用户服务接口
|
||||
*
|
||||
* @author emotion-museum
|
||||
* @since 2025-07-13
|
||||
*/
|
||||
public interface GuestUserService {
|
||||
|
||||
/**
|
||||
* 根据IP地址获取或创建访客用户
|
||||
*
|
||||
* @param ipAddress 客户端IP地址
|
||||
* @param userAgent 用户代理信息
|
||||
* @return 访客用户信息
|
||||
*/
|
||||
GuestUserInfo getOrCreateGuestUser(String ipAddress, String userAgent);
|
||||
|
||||
/**
|
||||
* 根据访客ID获取访客用户信息
|
||||
*
|
||||
* @param guestUserId 访客用户ID
|
||||
* @return 访客用户信息
|
||||
*/
|
||||
GuestUserInfo getGuestUserById(String guestUserId);
|
||||
|
||||
/**
|
||||
* 更新访客用户最后活跃时间
|
||||
*
|
||||
* @param guestUserId 访客用户ID
|
||||
*/
|
||||
void updateLastActiveTime(String guestUserId);
|
||||
|
||||
/**
|
||||
* 检查是否为访客用户ID
|
||||
*
|
||||
* @param userId 用户ID
|
||||
* @return 是否为访客用户
|
||||
*/
|
||||
boolean isGuestUser(String userId);
|
||||
|
||||
/**
|
||||
* 生成访客用户ID
|
||||
*
|
||||
* @param ipAddress IP地址
|
||||
* @return 访客用户ID
|
||||
*/
|
||||
String generateGuestUserId(String ipAddress);
|
||||
}
|
||||
+800
@@ -0,0 +1,800 @@
|
||||
package com.emotionmuseum.ai.service.impl;
|
||||
|
||||
import com.emotionmuseum.ai.config.FeatureConfig;
|
||||
import com.emotionmuseum.ai.dto.*;
|
||||
import com.emotionmuseum.ai.entity.Conversation;
|
||||
import com.emotionmuseum.ai.entity.Message;
|
||||
import com.emotionmuseum.ai.entity.CozeApiCall;
|
||||
import com.emotionmuseum.ai.service.AiChatService;
|
||||
import com.emotionmuseum.ai.service.ConversationDbService;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.web.reactive.function.client.WebClient;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.*;
|
||||
|
||||
/**
|
||||
* AI聊天服务实现类
|
||||
*
|
||||
* @author emotion-museum
|
||||
* @since 2025-07-12
|
||||
*/
|
||||
@Slf4j
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class AiChatServiceImpl implements AiChatService {
|
||||
|
||||
private final WebClient cozeWebClient;
|
||||
private final ConversationDbService conversationDbService;
|
||||
private final FeatureConfig featureConfig;
|
||||
|
||||
@Value("${coze.bot-id}")
|
||||
private String botId;
|
||||
|
||||
@Value("${coze.workflow-id:}")
|
||||
private String workflowId;
|
||||
|
||||
@Value("${coze.user-id:emotion-museum-user}")
|
||||
private String defaultUserId;
|
||||
|
||||
@Override
|
||||
public CreateConversationResponse createConversation(CreateConversationRequest request) {
|
||||
log.info("创建会话请求: userId={}, title={}", request.getUserId(), request.getTitle());
|
||||
|
||||
try {
|
||||
// 处理用户类型
|
||||
String userId = request.getUserId();
|
||||
String userType = userId != null && userId.startsWith("guest_") ? "guest" : "registered";
|
||||
|
||||
// 调用Coze API创建会话
|
||||
Map<String, Object> cozeResponse = cozeWebClient.post()
|
||||
.uri("/v1/conversation/create")
|
||||
.retrieve()
|
||||
.bodyToMono(Map.class)
|
||||
.block();
|
||||
|
||||
// 创建会话实体
|
||||
Conversation conversation = new Conversation();
|
||||
conversation.setUserId(userId);
|
||||
conversation.setUserType(userType);
|
||||
conversation.setTitle(request.getTitle() != null ? request.getTitle() : "新会话");
|
||||
conversation.setType(request.getType());
|
||||
conversation.setStatus("active");
|
||||
conversation.setInitialMessage(request.getInitialMessage());
|
||||
conversation.setContext(request.getContext());
|
||||
conversation.setStartTime(LocalDateTime.now());
|
||||
conversation.setLastActiveTime(LocalDateTime.now());
|
||||
conversation.setMessageCount(0);
|
||||
conversation.setBotId(botId);
|
||||
conversation.setWorkflowId(workflowId);
|
||||
|
||||
// 设置客户端信息(访客模式下会有这些信息)
|
||||
// 这些字段在CreateConversationRequest中可能不存在,暂时跳过
|
||||
|
||||
// 解析Coze响应获取会话ID
|
||||
if (cozeResponse != null && cozeResponse.get("data") != null) {
|
||||
Map<String, Object> data = (Map<String, Object>) cozeResponse.get("data");
|
||||
if (data.get("id") != null) {
|
||||
conversation.setCozeConversationId(data.get("id").toString());
|
||||
}
|
||||
}
|
||||
|
||||
// 保存到数据库
|
||||
Conversation savedConversation = conversationDbService.saveConversation(conversation);
|
||||
|
||||
// 构建响应
|
||||
CreateConversationResponse response = new CreateConversationResponse();
|
||||
response.setConversationId(savedConversation.getId());
|
||||
response.setUserId(savedConversation.getUserId());
|
||||
response.setTitle(savedConversation.getTitle());
|
||||
response.setType(savedConversation.getType());
|
||||
response.setStatus(savedConversation.getStatus());
|
||||
response.setCozeConversationId(savedConversation.getCozeConversationId());
|
||||
response.setCreateTime(savedConversation.getCreateTime());
|
||||
response.setUpdateTime(savedConversation.getUpdateTime());
|
||||
|
||||
log.info("会话创建成功: conversationId={}, cozeConversationId={}",
|
||||
response.getConversationId(), response.getCozeConversationId());
|
||||
return response;
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("创建会话失败: userId={}, error={}", request.getUserId(), e.getMessage(), e);
|
||||
throw new RuntimeException("创建会话失败: " + e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public com.emotionmuseum.ai.dto.ChatResponse chat(ChatRequest request) {
|
||||
log.info("处理聊天请求: userId={}, message={}", request.getUserId(), request.getMessage());
|
||||
|
||||
try {
|
||||
// 构建Coze API请求
|
||||
Map<String, Object> cozeRequest = buildCozeRequest(request);
|
||||
|
||||
// 保存用户消息到数据库
|
||||
Message userMessage = new Message();
|
||||
userMessage.setConversationId(request.getConversationId());
|
||||
userMessage.setContent(request.getMessage());
|
||||
userMessage.setType(request.getType() != null ? request.getType() : "text");
|
||||
userMessage.setSender("user");
|
||||
userMessage.setTimestamp(LocalDateTime.now());
|
||||
userMessage.setStatus("sent");
|
||||
userMessage.setIsRead(1);
|
||||
Message savedUserMessage = conversationDbService.saveMessage(userMessage);
|
||||
|
||||
// 创建API调用记录
|
||||
CozeApiCall apiCall = new CozeApiCall();
|
||||
apiCall.setConversationId(request.getConversationId());
|
||||
apiCall.setMessageId(savedUserMessage.getId());
|
||||
apiCall.setBotId(botId);
|
||||
apiCall.setWorkflowId(workflowId);
|
||||
apiCall.setUserId(request.getUserId());
|
||||
apiCall.setRequestType("chat");
|
||||
apiCall.setRequestUrl("/v3/chat");
|
||||
apiCall.setRequestBody((Map<String, Object>) cozeRequest);
|
||||
// 保存用户消息内容
|
||||
apiCall.setUserMessage(request.getMessage());
|
||||
apiCall.setUserMessageType("text");
|
||||
// 设置客户端信息
|
||||
apiCall.setClientIp(getClientIpFromRequest());
|
||||
apiCall.setUserAgent(getUserAgentFromRequest());
|
||||
apiCall.setSessionId(generateSessionId(request));
|
||||
apiCall.setTraceId(generateTraceId());
|
||||
apiCall.setStatus("pending");
|
||||
apiCall.setStartTime(LocalDateTime.now());
|
||||
CozeApiCall savedApiCall = conversationDbService.saveCozeApiCall(apiCall);
|
||||
|
||||
// 调用Coze API
|
||||
log.info("发送Coze请求: {}", cozeRequest);
|
||||
Map<String, Object> cozeResponse = cozeWebClient.post()
|
||||
.uri("/v3/chat")
|
||||
.bodyValue(cozeRequest)
|
||||
.retrieve()
|
||||
.bodyToMono(Map.class)
|
||||
.block();
|
||||
|
||||
log.info("收到Coze初始响应: {}", cozeResponse);
|
||||
|
||||
// 解析Coze响应并获取AI回复
|
||||
String aiContent = "抱歉,我现在无法理解您的消息。";
|
||||
String cozeChatId = null;
|
||||
String cozeConversationId = null;
|
||||
|
||||
if (cozeResponse != null && cozeResponse.get("data") != null) {
|
||||
Map<String, Object> data = (Map<String, Object>) cozeResponse.get("data");
|
||||
cozeChatId = (String) data.get("id");
|
||||
cozeConversationId = (String) data.get("conversation_id");
|
||||
|
||||
// 更新API调用记录
|
||||
conversationDbService.updateCozeApiCallStatus(savedApiCall.getId(), "success", cozeResponse, null);
|
||||
|
||||
if (cozeChatId != null && cozeConversationId != null) {
|
||||
// 更新会话的Coze信息
|
||||
conversationDbService.updateConversationCozeInfo(
|
||||
request.getConversationId(), cozeConversationId, botId, workflowId);
|
||||
|
||||
// 轮询聊天状态直到完成并获取回复内容
|
||||
ChatCompletionResult result = waitForChatCompletionWithResult(cozeChatId, cozeConversationId);
|
||||
aiContent = result.getContent();
|
||||
|
||||
// 更新API调用记录
|
||||
updateCozeApiCallWithResult(savedApiCall.getId(), result, aiContent);
|
||||
}
|
||||
} else {
|
||||
// 更新API调用记录为失败
|
||||
conversationDbService.updateCozeApiCallStatus(savedApiCall.getId(), "failed", null,
|
||||
"No valid response from Coze API");
|
||||
}
|
||||
|
||||
// 保存AI回复消息到数据库(支持拆分多条消息)
|
||||
List<Message> savedAiMessages = saveAiReplyMessages(request.getConversationId(), aiContent, cozeChatId);
|
||||
Message savedAiMessage = savedAiMessages.get(savedAiMessages.size() - 1); // 获取最后一条消息作为主要回复
|
||||
|
||||
// 构建响应
|
||||
com.emotionmuseum.ai.dto.ChatResponse response = new com.emotionmuseum.ai.dto.ChatResponse();
|
||||
response.setMessageId(savedAiMessage.getId());
|
||||
response.setConversationId(request.getConversationId());
|
||||
response.setContent(aiContent);
|
||||
response.setTimestamp(savedAiMessage.getTimestamp());
|
||||
|
||||
// 添加多条消息信息
|
||||
if (savedAiMessages.size() > 1) {
|
||||
response.setMultipleMessages(true);
|
||||
response.setMessageCount(savedAiMessages.size());
|
||||
response.setMessageIds(savedAiMessages.stream()
|
||||
.map(Message::getId)
|
||||
.collect(java.util.stream.Collectors.toList()));
|
||||
log.info("AI回复已拆分为{}条消息: conversationId={}, messageIds={}",
|
||||
savedAiMessages.size(), request.getConversationId(), response.getMessageIds());
|
||||
} else {
|
||||
response.setMultipleMessages(false);
|
||||
response.setMessageCount(1);
|
||||
}
|
||||
|
||||
// 如果需要情绪分析且功能已启用
|
||||
if (Boolean.TRUE.equals(request.getNeedEmotionAnalysis()) &&
|
||||
featureConfig.getEmotionAnalysis().isEnabled()) {
|
||||
try {
|
||||
EmotionAnalysisRequest emotionRequest = new EmotionAnalysisRequest();
|
||||
emotionRequest.setUserId(request.getUserId());
|
||||
emotionRequest.setText(request.getMessage());
|
||||
response.setEmotionAnalysis(analyzeEmotion(emotionRequest));
|
||||
log.debug("情绪分析完成: userId={}", request.getUserId());
|
||||
} catch (Exception e) {
|
||||
log.warn("情绪分析失败,跳过: userId={}, error={}", request.getUserId(), e.getMessage());
|
||||
// 情绪分析失败不影响聊天功能
|
||||
}
|
||||
} else if (Boolean.TRUE.equals(request.getNeedEmotionAnalysis())) {
|
||||
log.debug("情绪分析功能已禁用,跳过分析: userId={}", request.getUserId());
|
||||
}
|
||||
|
||||
log.info("聊天响应生成成功: messageId={}", response.getMessageId());
|
||||
return response;
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("聊天处理失败: userId={}, error={}", request.getUserId(), e.getMessage(), e);
|
||||
throw new RuntimeException("聊天处理失败: " + e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public EmotionAnalysisResponse analyzeEmotion(EmotionAnalysisRequest request) {
|
||||
log.info("处理情绪分析请求: userId={}, text={}", request.getUserId(), request.getText());
|
||||
|
||||
// 检查情绪分析功能是否启用
|
||||
if (!featureConfig.getEmotionAnalysis().isEnabled()) {
|
||||
log.warn("情绪分析功能已禁用: userId={}", request.getUserId());
|
||||
throw new RuntimeException("情绪分析功能暂时不可用");
|
||||
}
|
||||
|
||||
try {
|
||||
// 构建情绪分析请求
|
||||
Map<String, Object> cozeRequest = new HashMap<>();
|
||||
cozeRequest.put("bot_id", botId);
|
||||
cozeRequest.put("user_id", request.getUserId() != null ? request.getUserId() : defaultUserId);
|
||||
cozeRequest.put("stream", false);
|
||||
|
||||
String prompt = buildEmotionAnalysisPrompt(request.getText());
|
||||
List<Map<String, Object>> messages = new ArrayList<>();
|
||||
Map<String, Object> message = new HashMap<>();
|
||||
message.put("role", "user");
|
||||
message.put("content", prompt);
|
||||
message.put("content_type", "text");
|
||||
messages.add(message);
|
||||
cozeRequest.put("additional_messages", messages);
|
||||
|
||||
// 调用Coze API
|
||||
Map<String, Object> cozeResponse = cozeWebClient.post()
|
||||
.uri("/v3/chat")
|
||||
.bodyValue(cozeRequest)
|
||||
.retrieve()
|
||||
.bodyToMono(Map.class)
|
||||
.block();
|
||||
|
||||
// 解析AI返回的情绪分析结果
|
||||
String result = "";
|
||||
if (cozeResponse != null && cozeResponse.get("data") != null) {
|
||||
Map<String, Object> data = (Map<String, Object>) cozeResponse.get("data");
|
||||
result = extractContentFromCozeResponse(data);
|
||||
}
|
||||
|
||||
return parseEmotionAnalysisResult(result);
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("情绪分析失败: userId={}, error={}", request.getUserId(), e.getMessage(), e);
|
||||
throw new RuntimeException("情绪分析失败: " + e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public String streamChat(ChatRequest request) {
|
||||
log.info("处理流式聊天请求: userId={}", request.getUserId());
|
||||
|
||||
try {
|
||||
// 构建流式请求
|
||||
Map<String, Object> cozeRequest = buildCozeRequest(request);
|
||||
cozeRequest.put("stream", true); // 启用流式响应
|
||||
|
||||
log.debug("发送流式Coze请求: {}", cozeRequest);
|
||||
|
||||
// 调用Coze流式API
|
||||
String streamResponse = cozeWebClient.post()
|
||||
.uri("/v3/chat")
|
||||
.bodyValue(cozeRequest)
|
||||
.retrieve()
|
||||
.bodyToMono(String.class)
|
||||
.block();
|
||||
|
||||
log.debug("收到流式Coze响应: {}", streamResponse);
|
||||
|
||||
// 解析流式响应并提取最终内容
|
||||
String finalContent = parseStreamResponse(streamResponse);
|
||||
|
||||
return finalContent != null ? finalContent : "抱歉,流式聊天暂时无法处理您的请求。";
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("流式聊天失败: userId={}, error={}", request.getUserId(), e.getMessage(), e);
|
||||
// 降级到普通聊天
|
||||
try {
|
||||
com.emotionmuseum.ai.dto.ChatResponse response = chat(request);
|
||||
return response.getContent();
|
||||
} catch (Exception fallbackError) {
|
||||
log.error("降级聊天也失败: {}", fallbackError.getMessage());
|
||||
return "抱歉,聊天服务暂时不可用,请稍后再试。";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean healthCheck() {
|
||||
try {
|
||||
// 调用Coze bot信息接口检查健康状态
|
||||
Map<String, Object> response = cozeWebClient.get()
|
||||
.uri("/v1/bot/get_online_info?bot_id=" + botId)
|
||||
.retrieve()
|
||||
.bodyToMono(Map.class)
|
||||
.block();
|
||||
|
||||
return response != null && response.get("code") != null;
|
||||
} catch (Exception e) {
|
||||
log.error("健康检查失败: {}", e.getMessage());
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建Coze API请求
|
||||
*/
|
||||
private Map<String, Object> buildCozeRequest(ChatRequest request) {
|
||||
Map<String, Object> cozeRequest = new HashMap<>();
|
||||
cozeRequest.put("bot_id", botId);
|
||||
|
||||
// 如果有workflow_id,则添加
|
||||
if (workflowId != null && !workflowId.trim().isEmpty()) {
|
||||
cozeRequest.put("workflow_id", workflowId);
|
||||
}
|
||||
|
||||
cozeRequest.put("user_id", request.getUserId() != null ? request.getUserId() : defaultUserId);
|
||||
cozeRequest.put("stream", false);
|
||||
|
||||
// 构建消息内容
|
||||
String message = request.getMessage();
|
||||
if (request.getContext() != null && !request.getContext().trim().isEmpty()) {
|
||||
message = "上下文: " + request.getContext() + "\n\n用户消息: " + message;
|
||||
}
|
||||
|
||||
// 添加聊天历史
|
||||
List<Map<String, Object>> messages = new ArrayList<>();
|
||||
if (request.getHistory() != null && !request.getHistory().isEmpty()) {
|
||||
for (ChatRequest.ChatMessage historyMsg : request.getHistory()) {
|
||||
Map<String, Object> msg = new HashMap<>();
|
||||
msg.put("role", historyMsg.getRole());
|
||||
msg.put("content", historyMsg.getContent());
|
||||
msg.put("content_type", "text");
|
||||
msg.put("type", "user".equals(historyMsg.getRole()) ? "question" : "answer");
|
||||
messages.add(msg);
|
||||
}
|
||||
}
|
||||
|
||||
// 添加当前消息
|
||||
Map<String, Object> currentMsg = new HashMap<>();
|
||||
currentMsg.put("role", "user");
|
||||
currentMsg.put("content", message);
|
||||
currentMsg.put("content_type", "text");
|
||||
currentMsg.put("type", "question");
|
||||
messages.add(currentMsg);
|
||||
|
||||
cozeRequest.put("additional_messages", messages);
|
||||
cozeRequest.put("parameters", new HashMap<>());
|
||||
|
||||
return cozeRequest;
|
||||
}
|
||||
|
||||
/**
|
||||
* 聊天完成结果类
|
||||
*/
|
||||
private static class ChatCompletionResult {
|
||||
private final boolean success;
|
||||
private final String content;
|
||||
private final Map<String, Object> finalResponse;
|
||||
private final String errorMessage;
|
||||
|
||||
public ChatCompletionResult(boolean success, String content, Map<String, Object> finalResponse,
|
||||
String errorMessage) {
|
||||
this.success = success;
|
||||
this.content = content;
|
||||
this.finalResponse = finalResponse;
|
||||
this.errorMessage = errorMessage;
|
||||
}
|
||||
|
||||
public boolean isSuccess() {
|
||||
return success;
|
||||
}
|
||||
|
||||
public String getContent() {
|
||||
return content;
|
||||
}
|
||||
|
||||
public Map<String, Object> getFinalResponse() {
|
||||
return finalResponse;
|
||||
}
|
||||
|
||||
public String getErrorMessage() {
|
||||
return errorMessage;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 等待聊天完成并获取回复内容(带详细结果)
|
||||
*/
|
||||
private ChatCompletionResult waitForChatCompletionWithResult(String chatId, String conversationId) {
|
||||
try {
|
||||
// 最多等待30秒,每2秒轮询一次
|
||||
int maxAttempts = 15;
|
||||
int attempt = 0;
|
||||
|
||||
while (attempt < maxAttempts) {
|
||||
// 检查聊天状态
|
||||
log.info("轮询聊天状态,第{}次尝试: chatId={}, conversationId={}", attempt + 1, chatId, conversationId);
|
||||
Map<String, Object> statusResponse = cozeWebClient.get()
|
||||
.uri("/v3/chat/retrieve?chat_id={chatId}&conversation_id={conversationId}",
|
||||
chatId, conversationId)
|
||||
.retrieve()
|
||||
.bodyToMono(Map.class)
|
||||
.block();
|
||||
|
||||
log.info("轮询响应: {}", statusResponse);
|
||||
|
||||
if (statusResponse != null && statusResponse.get("data") != null) {
|
||||
Map<String, Object> data = (Map<String, Object>) statusResponse.get("data");
|
||||
String status = (String) data.get("status");
|
||||
log.info("聊天状态: {}", status);
|
||||
|
||||
if ("completed".equals(status)) {
|
||||
// 聊天完成,获取消息
|
||||
log.info("聊天完成,开始获取消息: chatId={}, conversationId={}", chatId, conversationId);
|
||||
String content = getChatMessages(chatId, conversationId);
|
||||
return new ChatCompletionResult(true, content, statusResponse, null);
|
||||
} else if ("failed".equals(status)) {
|
||||
log.error("Coze聊天失败: chatId={}, conversationId={}", chatId, conversationId);
|
||||
return new ChatCompletionResult(false, "抱歉,AI服务暂时不可用,请稍后再试。", statusResponse, "Chat failed");
|
||||
}
|
||||
} else {
|
||||
log.warn("轮询响应为空或无data字段: {}", statusResponse);
|
||||
}
|
||||
|
||||
// 等待2秒后重试
|
||||
Thread.sleep(2000);
|
||||
attempt++;
|
||||
}
|
||||
|
||||
log.warn("Coze聊天超时: chatId={}, conversationId={}", chatId, conversationId);
|
||||
return new ChatCompletionResult(false, "抱歉,AI响应超时,请稍后再试。", null, "Timeout");
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("等待Coze聊天完成失败: chatId={}, conversationId={}, error={}",
|
||||
chatId, conversationId, e.getMessage(), e);
|
||||
return new ChatCompletionResult(false, "抱歉,AI服务出现错误,请稍后再试。", null, e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 等待聊天完成并获取回复内容
|
||||
*/
|
||||
private String waitForChatCompletion(String chatId, String conversationId) {
|
||||
ChatCompletionResult result = waitForChatCompletionWithResult(chatId, conversationId);
|
||||
return result.getContent();
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新Coze API调用记录
|
||||
*/
|
||||
private void updateCozeApiCallWithResult(String apiCallId, ChatCompletionResult result, String aiReply) {
|
||||
try {
|
||||
CozeApiCall updateRecord = new CozeApiCall();
|
||||
updateRecord.setId(apiCallId);
|
||||
updateRecord.setEndTime(LocalDateTime.now());
|
||||
updateRecord.setAiReply(aiReply);
|
||||
updateRecord.setAiReplyType("text");
|
||||
|
||||
if (result.isSuccess()) {
|
||||
updateRecord.setStatus("success");
|
||||
updateRecord.setFinalStatus("completed");
|
||||
|
||||
// 提取token使用情况
|
||||
Map<String, Object> finalResponse = result.getFinalResponse();
|
||||
if (finalResponse != null && finalResponse.get("data") != null) {
|
||||
Map<String, Object> data = (Map<String, Object>) finalResponse.get("data");
|
||||
Map<String, Object> usage = (Map<String, Object>) data.get("usage");
|
||||
if (usage != null) {
|
||||
updateRecord.setPromptTokens((Integer) usage.get("input_count"));
|
||||
updateRecord.setCompletionTokens((Integer) usage.get("output_count"));
|
||||
updateRecord.setTotalTokens((Integer) usage.get("token_count"));
|
||||
}
|
||||
}
|
||||
} else {
|
||||
updateRecord.setStatus("failed");
|
||||
updateRecord.setFinalStatus("failed");
|
||||
updateRecord.setErrorMessage(result.getErrorMessage());
|
||||
}
|
||||
|
||||
// 保存最终响应
|
||||
updateRecord.setResponseBody(result.getFinalResponse());
|
||||
|
||||
// 计算耗时
|
||||
CozeApiCall originalRecord = conversationDbService.getCozeApiCallById(apiCallId);
|
||||
if (originalRecord != null && originalRecord.getStartTime() != null) {
|
||||
long duration = java.time.Duration.between(originalRecord.getStartTime(), updateRecord.getEndTime())
|
||||
.toMillis();
|
||||
updateRecord.setDurationMs((int) duration);
|
||||
}
|
||||
|
||||
conversationDbService.updateCozeApiCall(updateRecord);
|
||||
log.info("更新API调用记录成功: apiCallId={}, status={}, aiReply={}",
|
||||
apiCallId, updateRecord.getStatus(),
|
||||
aiReply != null ? aiReply.substring(0, Math.min(50, aiReply.length())) + "..." : "null");
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("更新API调用记录失败: apiCallId={}, error={}", apiCallId, e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取客户端IP
|
||||
*/
|
||||
private String getClientIpFromRequest() {
|
||||
// 这里可以从RequestContextHolder获取,暂时返回默认值
|
||||
return "127.0.0.1";
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取用户代理
|
||||
*/
|
||||
private String getUserAgentFromRequest() {
|
||||
// 这里可以从RequestContextHolder获取,暂时返回默认值
|
||||
return "EmotionMuseum-Client";
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成会话ID
|
||||
*/
|
||||
private String generateSessionId(ChatRequest request) {
|
||||
return "session_" + request.getUserId() + "_" + System.currentTimeMillis();
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成追踪ID
|
||||
*/
|
||||
private String generateTraceId() {
|
||||
return "trace_" + System.currentTimeMillis() + "_" + (int) (Math.random() * 10000);
|
||||
}
|
||||
|
||||
/**
|
||||
* 保存AI回复消息(支持拆分多条消息)
|
||||
* 当AI回复中包含\n\n或\n时,将消息拆分成多条,模拟真实对话
|
||||
*/
|
||||
public List<Message> saveAiReplyMessages(String conversationId, String aiContent, String cozeChatId) {
|
||||
List<Message> savedMessages = new ArrayList<>();
|
||||
|
||||
if (aiContent == null || aiContent.trim().isEmpty()) {
|
||||
log.warn("AI回复内容为空,跳过保存");
|
||||
return savedMessages;
|
||||
}
|
||||
|
||||
// 优先按\n\n拆分,如果没有\n\n则按\n拆分
|
||||
String[] messageParts;
|
||||
String splitPattern;
|
||||
|
||||
if (aiContent.contains("\n\n")) {
|
||||
messageParts = aiContent.split("\\n\\n");
|
||||
splitPattern = "\\n\\n";
|
||||
log.info("AI回复包含\\n\\n,按双换行拆分为{}条消息: conversationId={}", messageParts.length, conversationId);
|
||||
} else if (aiContent.contains("\n")) {
|
||||
messageParts = aiContent.split("\\n");
|
||||
splitPattern = "\\n";
|
||||
log.info("AI回复包含\\n,按单换行拆分为{}条消息: conversationId={}", messageParts.length, conversationId);
|
||||
} else {
|
||||
// 没有换行符,作为单条消息处理
|
||||
messageParts = new String[] { aiContent };
|
||||
splitPattern = "none";
|
||||
log.info("AI回复无换行符,作为单条消息处理: conversationId={}", conversationId);
|
||||
}
|
||||
|
||||
for (int i = 0; i < messageParts.length; i++) {
|
||||
String part = messageParts[i].trim();
|
||||
if (part.isEmpty()) {
|
||||
continue; // 跳过空白部分
|
||||
}
|
||||
|
||||
Message aiMessage = new Message();
|
||||
aiMessage.setConversationId(conversationId);
|
||||
aiMessage.setContent(part);
|
||||
aiMessage.setType("text");
|
||||
aiMessage.setSender("assistant");
|
||||
aiMessage.setTimestamp(LocalDateTime.now().plusSeconds(i)); // 每条消息间隔1秒,模拟真实对话
|
||||
aiMessage.setStatus("sent");
|
||||
aiMessage.setCozeChatId(cozeChatId);
|
||||
aiMessage.setIsRead(0);
|
||||
|
||||
// 为拆分的消息添加序号标识和拆分模式
|
||||
if (messageParts.length > 1) {
|
||||
String splitInfo = "none".equals(splitPattern) ? "" : " (按" + splitPattern + "拆分)";
|
||||
aiMessage.setRemarks("分段消息 " + (i + 1) + "/" + messageParts.length + splitInfo);
|
||||
}
|
||||
|
||||
Message savedMessage = conversationDbService.saveMessage(aiMessage);
|
||||
savedMessages.add(savedMessage);
|
||||
|
||||
log.info("保存AI回复消息片段 {}/{}: messageId={}, content={}",
|
||||
i + 1, messageParts.length, savedMessage.getId(),
|
||||
part.length() > 50 ? part.substring(0, 50) + "..." : part);
|
||||
}
|
||||
|
||||
return savedMessages;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取聊天消息
|
||||
*/
|
||||
private String getChatMessages(String chatId, String conversationId) {
|
||||
try {
|
||||
log.info("获取聊天消息: chatId={}, conversationId={}", chatId, conversationId);
|
||||
Map<String, Object> messagesResponse = cozeWebClient.get()
|
||||
.uri("/v3/chat/message/list?chat_id={chatId}&conversation_id={conversationId}",
|
||||
chatId, conversationId)
|
||||
.retrieve()
|
||||
.bodyToMono(Map.class)
|
||||
.block();
|
||||
|
||||
log.info("消息响应: {}", messagesResponse);
|
||||
|
||||
if (messagesResponse != null && messagesResponse.get("data") != null) {
|
||||
List<Map<String, Object>> messages = (List<Map<String, Object>>) messagesResponse.get("data");
|
||||
log.info("收到{}条消息", messages.size());
|
||||
|
||||
// 查找AI的回复消息(role=assistant, type=answer)
|
||||
for (Map<String, Object> message : messages) {
|
||||
String role = (String) message.get("role");
|
||||
String type = (String) message.get("type");
|
||||
log.info("消息详情: role={}, type={}, content={}", role, type, message.get("content"));
|
||||
|
||||
if ("assistant".equals(role) && "answer".equals(type)) {
|
||||
String content = (String) message.get("content");
|
||||
log.info("找到AI回复: {}", content);
|
||||
return content;
|
||||
}
|
||||
}
|
||||
log.warn("未找到AI回复消息");
|
||||
} else {
|
||||
log.warn("消息响应为空或无data字段");
|
||||
}
|
||||
|
||||
return "抱歉,未能获取到AI回复。";
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("获取Coze聊天消息失败: chatId={}, conversationId={}, error={}",
|
||||
chatId, conversationId, e.getMessage(), e);
|
||||
return "抱歉,获取AI回复失败。";
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析流式响应
|
||||
*/
|
||||
private String parseStreamResponse(String streamResponse) {
|
||||
if (streamResponse == null || streamResponse.trim().isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
// 流式响应通常是多行JSON,每行一个事件
|
||||
String[] lines = streamResponse.split("\n");
|
||||
StringBuilder finalContent = new StringBuilder();
|
||||
|
||||
for (String line : lines) {
|
||||
line = line.trim();
|
||||
if (line.isEmpty() || !line.startsWith("{")) {
|
||||
continue;
|
||||
}
|
||||
|
||||
try {
|
||||
// 这里应该根据Coze实际的流式响应格式来解析
|
||||
// 暂时简单处理,实际使用时需要根据API文档调整
|
||||
if (line.contains("\"content\"")) {
|
||||
// 提取content字段
|
||||
int contentStart = line.indexOf("\"content\":\"") + 11;
|
||||
int contentEnd = line.indexOf("\"", contentStart);
|
||||
if (contentStart > 10 && contentEnd > contentStart) {
|
||||
String content = line.substring(contentStart, contentEnd);
|
||||
finalContent.append(content);
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.debug("解析流式响应行失败: {}", line, e);
|
||||
}
|
||||
}
|
||||
|
||||
return finalContent.length() > 0 ? finalContent.toString() : null;
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("解析流式响应失败: {}", e.getMessage(), e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 从Coze响应中提取内容
|
||||
*/
|
||||
private String extractContentFromCozeResponse(Map<String, Object> data) {
|
||||
try {
|
||||
// 根据Coze API响应格式解析内容
|
||||
if (data.get("messages") != null) {
|
||||
List<Map<String, Object>> messages = (List<Map<String, Object>>) data.get("messages");
|
||||
for (Map<String, Object> message : messages) {
|
||||
if ("assistant".equals(message.get("role")) && "answer".equals(message.get("type"))) {
|
||||
return (String) message.get("content");
|
||||
}
|
||||
}
|
||||
}
|
||||
return "抱歉,我现在无法理解您的消息。";
|
||||
} catch (Exception e) {
|
||||
log.error("解析Coze响应失败: {}", e.getMessage());
|
||||
return "抱歉,响应解析出现问题。";
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建情绪分析提示词
|
||||
*/
|
||||
private String buildEmotionAnalysisPrompt(String text) {
|
||||
return String.format("""
|
||||
请对以下文本进行情绪分析,并以JSON格式返回结果:
|
||||
|
||||
文本: "%s"
|
||||
|
||||
请返回以下格式的JSON:
|
||||
{
|
||||
"primaryEmotion": "主要情绪",
|
||||
"intensity": 0.0-1.0的强度值,
|
||||
"polarity": "positive/negative/neutral",
|
||||
"confidence": 0.0-1.0的置信度,
|
||||
"emotions": [
|
||||
{"emotion": "情绪名称", "score": 得分, "description": "描述"}
|
||||
],
|
||||
"keywords": ["关键词1", "关键词2"],
|
||||
"suggestion": "建议"
|
||||
}
|
||||
""", text);
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析情绪分析结果
|
||||
*/
|
||||
private EmotionAnalysisResponse parseEmotionAnalysisResult(String result) {
|
||||
// 这里应该解析AI返回的JSON结果
|
||||
// 为了简化,先返回一个模拟结果
|
||||
EmotionAnalysisResponse response = new EmotionAnalysisResponse();
|
||||
response.setPrimaryEmotion("中性");
|
||||
response.setIntensity(0.5);
|
||||
response.setPolarity("neutral");
|
||||
response.setConfidence(0.8);
|
||||
response.setAnalysisTime(LocalDateTime.now());
|
||||
|
||||
List<EmotionAnalysisResponse.EmotionScore> emotions = new ArrayList<>();
|
||||
EmotionAnalysisResponse.EmotionScore score = new EmotionAnalysisResponse.EmotionScore();
|
||||
score.setEmotion("中性");
|
||||
score.setScore(0.5);
|
||||
score.setDescription("情绪相对平稳");
|
||||
emotions.add(score);
|
||||
response.setEmotions(emotions);
|
||||
|
||||
response.setKeywords(List.of("情绪", "分析"));
|
||||
response.setSuggestion("保持当前的情绪状态");
|
||||
|
||||
return response;
|
||||
}
|
||||
}
|
||||
+318
@@ -0,0 +1,318 @@
|
||||
package com.emotionmuseum.ai.service.impl;
|
||||
|
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
|
||||
import com.baomidou.mybatisplus.core.conditions.update.UpdateWrapper;
|
||||
import com.emotionmuseum.ai.entity.Conversation;
|
||||
import com.emotionmuseum.ai.entity.Message;
|
||||
import com.emotionmuseum.ai.entity.CozeApiCall;
|
||||
import com.emotionmuseum.ai.mapper.ConversationMapper;
|
||||
import com.emotionmuseum.ai.mapper.MessageMapper;
|
||||
import com.emotionmuseum.ai.mapper.CozeApiCallMapper;
|
||||
import com.emotionmuseum.ai.service.ConversationDbService;
|
||||
import com.emotionmuseum.common.dto.PageQuery;
|
||||
import com.emotionmuseum.common.entity.BaseEntity;
|
||||
import com.emotionmuseum.common.util.SnowflakeIdGenerator;
|
||||
import cn.hutool.core.util.IdUtil;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 会话数据库服务实现类
|
||||
*
|
||||
* @author emotion-museum
|
||||
* @since 2025-07-12
|
||||
*/
|
||||
@Slf4j
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class ConversationDbServiceImpl implements ConversationDbService {
|
||||
|
||||
private final ConversationMapper conversationMapper;
|
||||
private final MessageMapper messageMapper;
|
||||
private final CozeApiCallMapper cozeApiCallMapper;
|
||||
private final SnowflakeIdGenerator snowflakeIdGenerator;
|
||||
private final ObjectMapper objectMapper;
|
||||
|
||||
@Override
|
||||
@Transactional
|
||||
public Conversation saveConversation(Conversation conversation) {
|
||||
log.info("保存会话: conversationId={}, userId={}", conversation.getId(), conversation.getUserId());
|
||||
|
||||
// 手动设置ID,确保不为空
|
||||
if (conversation.getId() == null || conversation.getId().isEmpty()) {
|
||||
conversation.setId(String.valueOf(snowflakeIdGenerator.nextId()));
|
||||
}
|
||||
|
||||
if (conversation.getStartTime() == null) {
|
||||
conversation.setStartTime(LocalDateTime.now());
|
||||
}
|
||||
if (conversation.getStatus() == null) {
|
||||
conversation.setStatus("active");
|
||||
}
|
||||
if (conversation.getMessageCount() == null) {
|
||||
conversation.setMessageCount(0);
|
||||
}
|
||||
if (conversation.getCreateTime() == null) {
|
||||
conversation.setCreateTime(LocalDateTime.now());
|
||||
}
|
||||
if (conversation.getUpdateTime() == null) {
|
||||
conversation.setUpdateTime(LocalDateTime.now());
|
||||
}
|
||||
|
||||
conversationMapper.insert(conversation);
|
||||
return conversation;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Conversation getConversationById(String conversationId) {
|
||||
log.debug("查询会话: conversationId={}", conversationId);
|
||||
return conversationMapper.selectById(conversationId);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<Conversation> getConversationsByUserId(String userId, PageQuery pageQuery) {
|
||||
log.debug("查询用户会话列表: userId={}, pageNum={}, pageSize={}",
|
||||
userId, pageQuery.getPageNum(), pageQuery.getPageSize());
|
||||
|
||||
LambdaQueryWrapper<Conversation> wrapper = new LambdaQueryWrapper<>();
|
||||
wrapper.eq(Conversation::getUserId, userId)
|
||||
.orderByDesc(Conversation::getUpdateTime);
|
||||
|
||||
// 简单分页实现
|
||||
int offset = (pageQuery.getPageNum() - 1) * pageQuery.getPageSize();
|
||||
wrapper.last("LIMIT " + pageQuery.getPageSize() + " OFFSET " + offset);
|
||||
|
||||
return conversationMapper.selectList(wrapper);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<Conversation> getActiveConversationsByUserId(String userId) {
|
||||
log.debug("查询用户活跃会话: userId={}", userId);
|
||||
return conversationMapper.selectActiveConversationsByUserId(userId);
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional
|
||||
public boolean updateConversationStatus(String conversationId, String status) {
|
||||
log.info("更新会话状态: conversationId={}, status={}", conversationId, status);
|
||||
|
||||
LambdaUpdateWrapper<Conversation> wrapper = new LambdaUpdateWrapper<>();
|
||||
wrapper.eq(Conversation::getId, conversationId)
|
||||
.set(Conversation::getStatus, status)
|
||||
.set(Conversation::getUpdateTime, LocalDateTime.now());
|
||||
|
||||
if ("ended".equals(status)) {
|
||||
wrapper.set(Conversation::getEndTime, LocalDateTime.now());
|
||||
}
|
||||
|
||||
return conversationMapper.update(null, wrapper) > 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional
|
||||
public boolean updateConversationActiveTime(String conversationId) {
|
||||
log.debug("更新会话活跃时间: conversationId={}", conversationId);
|
||||
|
||||
// 获取当前消息数量
|
||||
Integer messageCount = getMessageCount(conversationId);
|
||||
|
||||
return conversationMapper.updateLastActiveTime(conversationId, LocalDateTime.now(), messageCount) > 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional
|
||||
public Message saveMessage(Message message) {
|
||||
log.info("保存消息: conversationId={}, sender={}, type={}",
|
||||
message.getConversationId(), message.getSender(), message.getType());
|
||||
|
||||
// 设置消息ID
|
||||
if (message.getId() == null || message.getId().isEmpty()) {
|
||||
message.setId(String.valueOf(snowflakeIdGenerator.nextId()));
|
||||
}
|
||||
|
||||
if (message.getTimestamp() == null) {
|
||||
message.setTimestamp(LocalDateTime.now());
|
||||
}
|
||||
if (message.getIsRead() == null) {
|
||||
message.setIsRead(0);
|
||||
}
|
||||
|
||||
// 手动设置通用字段
|
||||
LocalDateTime now = LocalDateTime.now();
|
||||
if (message.getCreateTime() == null) {
|
||||
message.setCreateTime(now);
|
||||
}
|
||||
if (message.getUpdateTime() == null) {
|
||||
message.setUpdateTime(now);
|
||||
}
|
||||
|
||||
messageMapper.insert(message);
|
||||
|
||||
// 更新会话活跃时间
|
||||
updateConversationActiveTime(message.getConversationId());
|
||||
|
||||
return message;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<Message> getMessagesByConversationId(String conversationId, PageQuery pageQuery) {
|
||||
log.debug("查询会话消息: conversationId={}, pageNum={}, pageSize={}",
|
||||
conversationId, pageQuery.getPageNum(), pageQuery.getPageSize());
|
||||
|
||||
int offset = (pageQuery.getPageNum() - 1) * pageQuery.getPageSize();
|
||||
return messageMapper.selectMessagesByConversationId(conversationId, pageQuery.getPageSize(), offset);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<Message> getLatestMessages(String conversationId, Integer limit) {
|
||||
log.debug("查询最新消息: conversationId={}, limit={}", conversationId, limit);
|
||||
return messageMapper.selectLatestMessagesByConversationId(conversationId, limit);
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional
|
||||
public boolean markMessageAsRead(String messageId) {
|
||||
log.debug("标记消息已读: messageId={}", messageId);
|
||||
return messageMapper.markMessageAsRead(messageId) > 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional
|
||||
public boolean markConversationMessagesAsRead(String conversationId) {
|
||||
log.info("标记会话消息已读: conversationId={}", conversationId);
|
||||
return messageMapper.markConversationMessagesAsRead(conversationId) > 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Integer getMessageCount(String conversationId) {
|
||||
return messageMapper.countMessagesByConversationId(conversationId);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Integer getUnreadMessageCount(String conversationId) {
|
||||
return messageMapper.countUnreadMessages(conversationId);
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional
|
||||
public boolean deleteConversation(String conversationId) {
|
||||
log.info("删除会话: conversationId={}", conversationId);
|
||||
|
||||
// 软删除会话
|
||||
LambdaUpdateWrapper<Conversation> conversationWrapper = new LambdaUpdateWrapper<>();
|
||||
conversationWrapper.eq(Conversation::getId, conversationId)
|
||||
.setSql("is_deleted = 1")
|
||||
.set(Conversation::getUpdateTime, LocalDateTime.now());
|
||||
|
||||
// 软删除相关消息
|
||||
LambdaUpdateWrapper<Message> messageWrapper = new LambdaUpdateWrapper<>();
|
||||
messageWrapper.eq(Message::getConversationId, conversationId)
|
||||
.setSql("is_deleted = 1")
|
||||
.set(Message::getUpdateTime, LocalDateTime.now());
|
||||
|
||||
boolean conversationDeleted = conversationMapper.update(null, conversationWrapper) > 0;
|
||||
boolean messagesDeleted = messageMapper.update(null, messageWrapper) >= 0; // 可能没有消息
|
||||
|
||||
return conversationDeleted && messagesDeleted;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Conversation getConversationByCozeId(String cozeConversationId) {
|
||||
LambdaQueryWrapper<Conversation> wrapper = new LambdaQueryWrapper<>();
|
||||
wrapper.eq(Conversation::getCozeConversationId, cozeConversationId)
|
||||
.last("AND is_deleted = 0");
|
||||
return conversationMapper.selectOne(wrapper);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean updateConversationCozeInfo(String conversationId, String cozeConversationId, String botId,
|
||||
String workflowId) {
|
||||
LambdaUpdateWrapper<Conversation> wrapper = new LambdaUpdateWrapper<>();
|
||||
wrapper.eq(Conversation::getId, conversationId)
|
||||
.set(Conversation::getCozeConversationId, cozeConversationId)
|
||||
.set(Conversation::getBotId, botId)
|
||||
.set(Conversation::getWorkflowId, workflowId)
|
||||
.set(Conversation::getUpdateTime, LocalDateTime.now());
|
||||
return conversationMapper.update(null, wrapper) > 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean updateMessageCozeInfo(String messageId, String cozeChatId, String cozeMessageId, String status) {
|
||||
LambdaUpdateWrapper<Message> wrapper = new LambdaUpdateWrapper<>();
|
||||
wrapper.eq(Message::getId, messageId)
|
||||
.set(Message::getCozeChatId, cozeChatId)
|
||||
.set(Message::getCozeMessageId, cozeMessageId)
|
||||
.set(Message::getStatus, status)
|
||||
.set(Message::getUpdateTime, LocalDateTime.now());
|
||||
return messageMapper.update(null, wrapper) > 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional
|
||||
public CozeApiCall saveCozeApiCall(CozeApiCall cozeApiCall) {
|
||||
if (cozeApiCall.getId() == null) {
|
||||
cozeApiCall.setId(IdUtil.fastSimpleUUID());
|
||||
}
|
||||
|
||||
// 手动设置通用字段
|
||||
LocalDateTime now = LocalDateTime.now();
|
||||
if (cozeApiCall.getCreateTime() == null) {
|
||||
cozeApiCall.setCreateTime(now);
|
||||
}
|
||||
if (cozeApiCall.getUpdateTime() == null) {
|
||||
cozeApiCall.setUpdateTime(now);
|
||||
}
|
||||
|
||||
cozeApiCallMapper.insert(cozeApiCall);
|
||||
return cozeApiCall;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean updateCozeApiCallStatus(String callId, String status, Object responseBody, String errorMessage) {
|
||||
LocalDateTime now = LocalDateTime.now();
|
||||
|
||||
if (errorMessage != null) {
|
||||
// 有错误信息时使用错误更新方法
|
||||
return cozeApiCallMapper.updateStatusWithErrorById(callId, status, now, now, errorMessage) > 0;
|
||||
} else {
|
||||
// 正常响应时使用响应更新方法,将对象序列化为JSON字符串
|
||||
String responseBodyStr = null;
|
||||
if (responseBody != null) {
|
||||
try {
|
||||
responseBodyStr = objectMapper.writeValueAsString(responseBody);
|
||||
} catch (Exception e) {
|
||||
log.error("序列化响应体失败: {}", e.getMessage());
|
||||
responseBodyStr = responseBody.toString();
|
||||
}
|
||||
}
|
||||
return cozeApiCallMapper.updateStatusById(callId, status, now, now, responseBodyStr) > 0;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public CozeApiCall getCozeApiCallById(String callId) {
|
||||
return cozeApiCallMapper.selectById(callId);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean updateCozeApiCall(CozeApiCall cozeApiCall) {
|
||||
cozeApiCall.setUpdateTime(LocalDateTime.now());
|
||||
return cozeApiCallMapper.updateById(cozeApiCall) > 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<Message> getMessagesByIds(List<String> messageIds) {
|
||||
if (messageIds == null || messageIds.isEmpty()) {
|
||||
return new ArrayList<>();
|
||||
}
|
||||
return messageMapper.selectBatchIds(messageIds);
|
||||
}
|
||||
}
|
||||
+298
@@ -0,0 +1,298 @@
|
||||
package com.emotionmuseum.ai.service.impl;
|
||||
|
||||
import com.emotionmuseum.ai.config.FeatureConfig;
|
||||
import com.emotionmuseum.ai.dto.*;
|
||||
import com.emotionmuseum.ai.entity.Conversation;
|
||||
import com.emotionmuseum.ai.entity.Message;
|
||||
import com.emotionmuseum.ai.service.AiChatService;
|
||||
import com.emotionmuseum.ai.service.ConversationDbService;
|
||||
import com.emotionmuseum.ai.service.GuestChatService;
|
||||
import com.emotionmuseum.ai.service.GuestUserService;
|
||||
import com.emotionmuseum.common.result.Result;
|
||||
import com.emotionmuseum.common.dto.PageQuery;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* 访客聊天服务实现
|
||||
*
|
||||
* @author emotion-museum
|
||||
* @since 2025-07-13
|
||||
*/
|
||||
@Slf4j
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class GuestChatServiceImpl implements GuestChatService {
|
||||
|
||||
private final GuestUserService guestUserService;
|
||||
private final AiChatService aiChatService;
|
||||
private final ConversationDbService conversationDbService;
|
||||
private final FeatureConfig featureConfig;
|
||||
|
||||
@Override
|
||||
public Result<GuestChatResponse> guestChat(GuestChatRequest request) {
|
||||
log.info("处理访客聊天请求: IP={}, Message={}", request.getClientIp(), request.getMessage());
|
||||
|
||||
try {
|
||||
// 1. 获取或创建访客用户
|
||||
GuestUserInfo guestUser = guestUserService.getOrCreateGuestUser(
|
||||
request.getClientIp(), request.getUserAgent());
|
||||
|
||||
// 2. 处理会话
|
||||
String conversationId = request.getConversationId();
|
||||
boolean isNewConversation = false;
|
||||
|
||||
if (!StringUtils.hasText(conversationId)) {
|
||||
// 创建新会话
|
||||
CreateConversationRequest createRequest = new CreateConversationRequest();
|
||||
createRequest.setUserId(guestUser.getGuestUserId());
|
||||
createRequest.setTitle(request.getTitle() != null ? request.getTitle() : "访客会话");
|
||||
createRequest.setType("guest_chat");
|
||||
|
||||
CreateConversationResponse createResponse = aiChatService.createConversation(createRequest);
|
||||
conversationId = createResponse.getConversationId().toString();
|
||||
isNewConversation = true;
|
||||
|
||||
log.info("为访客用户创建新会话: guestUserId={}, conversationId={}",
|
||||
guestUser.getGuestUserId(), conversationId);
|
||||
}
|
||||
|
||||
// 3. 发送消息
|
||||
ChatRequest chatRequest = new ChatRequest();
|
||||
chatRequest.setUserId(guestUser.getGuestUserId());
|
||||
chatRequest.setConversationId(conversationId);
|
||||
chatRequest.setMessage(request.getMessage());
|
||||
chatRequest.setType(request.getMessageType());
|
||||
chatRequest.setNeedEmotionAnalysis(true);
|
||||
|
||||
ChatResponse chatResponse = aiChatService.chat(chatRequest);
|
||||
|
||||
// 4. 更新访客用户统计
|
||||
try {
|
||||
((GuestUserServiceImpl) guestUserService).incrementMessageCount(guestUser.getGuestUserId(), 2); // 用户消息+AI回复
|
||||
guestUserService.updateLastActiveTime(guestUser.getGuestUserId());
|
||||
} catch (Exception e) {
|
||||
log.warn("更新访客用户统计失败: {}", e.getMessage());
|
||||
}
|
||||
|
||||
// 5. 构建响应
|
||||
GuestChatResponse response = GuestChatResponse.builder()
|
||||
.guestUserId(guestUser.getGuestUserId())
|
||||
.guestNickname(guestUser.getNickname())
|
||||
.conversationId(conversationId)
|
||||
.conversationTitle(request.getTitle())
|
||||
.userMessageId(chatResponse.getMessageId())
|
||||
.aiMessageId(chatResponse.getMessageId())
|
||||
.userMessage(request.getMessage())
|
||||
.aiReply(chatResponse.getContent())
|
||||
.timestamp(chatResponse.getTimestamp())
|
||||
.conversationStatus("active")
|
||||
.isNewConversation(isNewConversation)
|
||||
.build();
|
||||
|
||||
// 6. 添加情绪分析结果(如果有)
|
||||
if (chatResponse.getEmotionAnalysis() != null) {
|
||||
response.setEmotionAnalysis(GuestChatResponse.EmotionAnalysisResult.builder()
|
||||
.primaryEmotion(chatResponse.getEmotionAnalysis().getPrimaryEmotion())
|
||||
.emotionScore(chatResponse.getEmotionAnalysis().getIntensity())
|
||||
.confidence(chatResponse.getEmotionAnalysis().getConfidence())
|
||||
.emotionTrend("stable")
|
||||
.build());
|
||||
}
|
||||
|
||||
// 7. 添加Token使用情况(如果有)
|
||||
if (chatResponse.getUsage() != null) {
|
||||
response.setTokenUsage(GuestChatResponse.TokenUsage.builder()
|
||||
.promptTokens(chatResponse.getUsage().getPromptTokens())
|
||||
.completionTokens(chatResponse.getUsage().getCompletionTokens())
|
||||
.totalTokens(chatResponse.getUsage().getTotalTokens())
|
||||
.build());
|
||||
}
|
||||
|
||||
log.info("访客聊天处理成功: guestUserId={}, conversationId={}",
|
||||
guestUser.getGuestUserId(), conversationId);
|
||||
|
||||
return Result.success(response);
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("访客聊天处理失败", e);
|
||||
return Result.error("聊天处理失败: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Result<List<ConversationListResponse>> getGuestConversations(String clientIp, Integer pageNum,
|
||||
Integer pageSize) {
|
||||
try {
|
||||
// 根据IP获取访客用户
|
||||
GuestUserInfo guestUser = guestUserService.getOrCreateGuestUser(clientIp, null);
|
||||
|
||||
// 获取访客的会话列表
|
||||
PageQuery pageQuery = new PageQuery();
|
||||
pageQuery.setPageNum(pageNum);
|
||||
pageQuery.setPageSize(pageSize);
|
||||
|
||||
List<Conversation> conversations = conversationDbService.getConversationsByUserId(
|
||||
guestUser.getGuestUserId(), pageQuery);
|
||||
|
||||
List<ConversationListResponse> responseList = conversations.stream()
|
||||
.map(this::convertToConversationListResponse)
|
||||
.collect(Collectors.toList());
|
||||
|
||||
return Result.success(responseList);
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("获取访客会话列表失败", e);
|
||||
return Result.error("获取会话列表失败: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Result<List<MessageListResponse>> getGuestConversationMessages(String conversationId, String clientIp,
|
||||
Integer pageNum, Integer pageSize) {
|
||||
try {
|
||||
// 验证会话是否属于该访客用户
|
||||
Conversation conversation = conversationDbService.getConversationById(conversationId);
|
||||
if (conversation == null) {
|
||||
return Result.error("会话不存在");
|
||||
}
|
||||
|
||||
// 验证IP是否匹配
|
||||
if (!clientIp.equals(conversation.getClientIp())) {
|
||||
log.warn("访客IP不匹配: 请求IP={}, 会话IP={}", clientIp, conversation.getClientIp());
|
||||
return Result.error("无权访问该会话");
|
||||
}
|
||||
|
||||
// 获取消息列表
|
||||
PageQuery pageQuery = new PageQuery();
|
||||
pageQuery.setPageNum(pageNum);
|
||||
pageQuery.setPageSize(pageSize);
|
||||
|
||||
List<Message> messages = conversationDbService.getMessagesByConversationId(
|
||||
conversationId, pageQuery);
|
||||
|
||||
List<MessageListResponse> responseList = messages.stream()
|
||||
.map(this::convertToMessageListResponse)
|
||||
.collect(Collectors.toList());
|
||||
|
||||
return Result.success(responseList);
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("获取访客会话消息失败", e);
|
||||
return Result.error("获取会话消息失败: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Result<Void> endGuestConversation(String conversationId, String clientIp) {
|
||||
try {
|
||||
// 验证会话是否属于该访客用户
|
||||
Conversation conversation = conversationDbService.getConversationById(conversationId);
|
||||
if (conversation == null) {
|
||||
return Result.error("会话不存在");
|
||||
}
|
||||
|
||||
// 验证IP是否匹配
|
||||
if (!clientIp.equals(conversation.getClientIp())) {
|
||||
return Result.error("无权操作该会话");
|
||||
}
|
||||
|
||||
// 结束会话
|
||||
conversationDbService.updateConversationStatus(conversationId, "ended");
|
||||
|
||||
return Result.success();
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("结束访客会话失败", e);
|
||||
return Result.error("结束会话失败: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Result<GuestUserInfo> getOrCreateGuestUser(String clientIp, String userAgent) {
|
||||
try {
|
||||
GuestUserInfo guestUser = guestUserService.getOrCreateGuestUser(clientIp, userAgent);
|
||||
return Result.success(guestUser);
|
||||
} catch (Exception e) {
|
||||
log.error("获取访客用户信息失败", e);
|
||||
return Result.error("获取用户信息失败: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Result<EmotionAnalysisResponse> analyzeGuestEmotion(EmotionAnalysisRequest request, String clientIp) {
|
||||
// 检查情绪分析功能是否启用
|
||||
if (!featureConfig.getEmotionAnalysis().isEnabled()) {
|
||||
log.warn("访客情绪分析功能已禁用: IP={}", clientIp);
|
||||
return Result.error("情绪分析功能暂时不可用");
|
||||
}
|
||||
|
||||
try {
|
||||
// 获取访客用户信息
|
||||
GuestUserInfo guestUser = guestUserService.getOrCreateGuestUser(clientIp, null);
|
||||
|
||||
// 设置用户ID为访客用户ID
|
||||
request.setUserId(guestUser.getGuestUserId());
|
||||
|
||||
// 调用情绪分析服务
|
||||
EmotionAnalysisResponse response = aiChatService.analyzeEmotion(request);
|
||||
|
||||
return Result.success(response);
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("访客情绪分析失败", e);
|
||||
return Result.error("情绪分析失败: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 转换为会话列表响应
|
||||
*/
|
||||
private ConversationListResponse convertToConversationListResponse(Conversation conversation) {
|
||||
return ConversationListResponse.builder()
|
||||
.conversationId(conversation.getId())
|
||||
.title(conversation.getTitle())
|
||||
.type(conversation.getType())
|
||||
.status(conversation.getStatus())
|
||||
.userId(conversation.getUserId())
|
||||
.userType(conversation.getUserType())
|
||||
.messageCount(conversation.getMessageCount())
|
||||
.lastActiveTime(conversation.getLastActiveTime())
|
||||
.createTime(conversation.getCreateTime())
|
||||
.primaryEmotion(conversation.getPrimaryEmotion())
|
||||
.emotionIntensity(
|
||||
conversation.getEmotionIntensity() != null ? conversation.getEmotionIntensity().doubleValue()
|
||||
: null)
|
||||
.cozeConversationId(conversation.getCozeConversationId())
|
||||
.build();
|
||||
}
|
||||
|
||||
/**
|
||||
* 转换为消息列表响应
|
||||
*/
|
||||
private MessageListResponse convertToMessageListResponse(Message message) {
|
||||
return MessageListResponse.builder()
|
||||
.messageId(message.getId())
|
||||
.conversationId(message.getConversationId())
|
||||
.content(message.getContent())
|
||||
.type(message.getType())
|
||||
.sender(message.getSender())
|
||||
.timestamp(message.getTimestamp())
|
||||
.status(message.getStatus())
|
||||
.emotionType(message.getEmotionType())
|
||||
.emotionScore(message.getEmotionScore())
|
||||
.emotionConfidence(message.getEmotionConfidence())
|
||||
.isRead(message.getIsRead())
|
||||
.cozeChatId(message.getCozeChatId())
|
||||
.cozeMessageId(message.getCozeMessageId())
|
||||
.userId(message.getUserId())
|
||||
.userType(message.getUserType())
|
||||
.build();
|
||||
}
|
||||
}
|
||||
+180
@@ -0,0 +1,180 @@
|
||||
package com.emotionmuseum.ai.service.impl;
|
||||
|
||||
import com.emotionmuseum.ai.dto.GuestUserInfo;
|
||||
import com.emotionmuseum.ai.entity.GuestUser;
|
||||
import com.emotionmuseum.ai.mapper.GuestUserMapper;
|
||||
import com.emotionmuseum.ai.service.GuestUserService;
|
||||
import com.emotionmuseum.common.util.SnowflakeIdGenerator;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
import java.security.MessageDigest;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.Random;
|
||||
|
||||
/**
|
||||
* 访客用户服务实现
|
||||
*
|
||||
* @author emotion-museum
|
||||
* @since 2025-07-13
|
||||
*/
|
||||
@Slf4j
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class GuestUserServiceImpl implements GuestUserService {
|
||||
|
||||
private final GuestUserMapper guestUserMapper;
|
||||
private final SnowflakeIdGenerator snowflakeIdGenerator;
|
||||
private final Random random = new Random();
|
||||
|
||||
@Override
|
||||
public GuestUserInfo getOrCreateGuestUser(String ipAddress, String userAgent) {
|
||||
log.info("获取或创建访客用户, IP: {}, UserAgent: {}", ipAddress, userAgent);
|
||||
|
||||
// 先尝试根据IP查找现有访客用户
|
||||
GuestUser existingUser = guestUserMapper.findByIpAddress(ipAddress);
|
||||
|
||||
if (existingUser != null) {
|
||||
// 更新最后活跃时间
|
||||
updateLastActiveTime(existingUser.getGuestUserId());
|
||||
log.info("找到现有访客用户: {}", existingUser.getGuestUserId());
|
||||
return convertToDto(existingUser);
|
||||
}
|
||||
|
||||
// 创建新的访客用户
|
||||
String guestUserId = generateGuestUserId(ipAddress);
|
||||
GuestUser newUser = new GuestUser();
|
||||
// 手动设置ID,确保不为空
|
||||
newUser.setId(String.valueOf(snowflakeIdGenerator.nextId()));
|
||||
newUser.setGuestUserId(guestUserId);
|
||||
newUser.setIpAddress(ipAddress);
|
||||
newUser.setUserAgent(userAgent);
|
||||
newUser.setNickname(generateGuestNickname());
|
||||
newUser.setAvatar(generateGuestAvatar());
|
||||
newUser.setLastActiveTime(LocalDateTime.now());
|
||||
newUser.setConversationCount(0);
|
||||
newUser.setMessageCount(0);
|
||||
newUser.setCreateBy("system");
|
||||
newUser.setUpdateBy("system");
|
||||
|
||||
try {
|
||||
guestUserMapper.insert(newUser);
|
||||
log.info("创建新访客用户成功: {}", guestUserId);
|
||||
return convertToDto(newUser);
|
||||
} catch (Exception e) {
|
||||
log.error("创建访客用户失败", e);
|
||||
throw new RuntimeException("创建访客用户失败: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public GuestUserInfo getGuestUserById(String guestUserId) {
|
||||
if (!isGuestUser(guestUserId)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
GuestUser guestUser = guestUserMapper.findByGuestUserId(guestUserId);
|
||||
return guestUser != null ? convertToDto(guestUser) : null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void updateLastActiveTime(String guestUserId) {
|
||||
if (isGuestUser(guestUserId)) {
|
||||
guestUserMapper.updateLastActiveTime(guestUserId);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isGuestUser(String userId) {
|
||||
return StringUtils.hasText(userId) && userId.startsWith("guest_");
|
||||
}
|
||||
|
||||
@Override
|
||||
public String generateGuestUserId(String ipAddress) {
|
||||
try {
|
||||
// 使用IP地址和时间戳生成唯一ID
|
||||
String input = ipAddress + "_" + System.currentTimeMillis() + "_" + random.nextInt(10000);
|
||||
MessageDigest md = MessageDigest.getInstance("MD5");
|
||||
byte[] digest = md.digest(input.getBytes());
|
||||
|
||||
StringBuilder sb = new StringBuilder();
|
||||
for (byte b : digest) {
|
||||
sb.append(String.format("%02x", b));
|
||||
}
|
||||
|
||||
return "guest_" + sb.toString().substring(0, 16);
|
||||
} catch (Exception e) {
|
||||
log.error("生成访客用户ID失败", e);
|
||||
// 降级方案
|
||||
return "guest_" + System.currentTimeMillis() + "_" + random.nextInt(10000);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成访客昵称
|
||||
*/
|
||||
private String generateGuestNickname() {
|
||||
String[] adjectives = { "神秘的", "友善的", "智慧的", "温暖的", "勇敢的", "优雅的", "活泼的", "宁静的" };
|
||||
String[] nouns = { "访客", "旅行者", "探索者", "朋友", "伙伴", "客人", "用户", "来访者" };
|
||||
|
||||
String adjective = adjectives[random.nextInt(adjectives.length)];
|
||||
String noun = nouns[random.nextInt(nouns.length)];
|
||||
int number = random.nextInt(9999) + 1;
|
||||
|
||||
return adjective + noun + number;
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成访客头像
|
||||
*/
|
||||
private String generateGuestAvatar() {
|
||||
// 使用默认头像或随机头像
|
||||
String[] avatars = {
|
||||
"/images/avatars/guest1.png",
|
||||
"/images/avatars/guest2.png",
|
||||
"/images/avatars/guest3.png",
|
||||
"/images/avatars/guest4.png",
|
||||
"/images/avatars/guest5.png"
|
||||
};
|
||||
|
||||
return avatars[random.nextInt(avatars.length)];
|
||||
}
|
||||
|
||||
/**
|
||||
* 转换为DTO
|
||||
*/
|
||||
private GuestUserInfo convertToDto(GuestUser guestUser) {
|
||||
return GuestUserInfo.builder()
|
||||
.guestUserId(guestUser.getGuestUserId())
|
||||
.ipAddress(guestUser.getIpAddress())
|
||||
.userAgent(guestUser.getUserAgent())
|
||||
.nickname(guestUser.getNickname())
|
||||
.avatar(guestUser.getAvatar())
|
||||
.createTime(guestUser.getCreateTime())
|
||||
.lastActiveTime(guestUser.getLastActiveTime())
|
||||
.isGuest(true)
|
||||
.conversationCount(guestUser.getConversationCount())
|
||||
.messageCount(guestUser.getMessageCount())
|
||||
.build();
|
||||
}
|
||||
|
||||
/**
|
||||
* 增加会话数量
|
||||
*/
|
||||
public void incrementConversationCount(String guestUserId) {
|
||||
if (isGuestUser(guestUserId)) {
|
||||
guestUserMapper.incrementConversationCount(guestUserId);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 增加消息数量
|
||||
*/
|
||||
public void incrementMessageCount(String guestUserId, int count) {
|
||||
if (isGuestUser(guestUserId)) {
|
||||
guestUserMapper.incrementMessageCount(guestUserId, count);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,89 @@
|
||||
# AI服务 Docker环境配置
|
||||
server:
|
||||
port: 9002
|
||||
|
||||
spring:
|
||||
application:
|
||||
name: emotion-ai
|
||||
profiles:
|
||||
active: docker
|
||||
cloud:
|
||||
nacos:
|
||||
discovery:
|
||||
server-addr: ${NACOS_SERVER_ADDR:nacos:8848}
|
||||
namespace: public
|
||||
group: DEFAULT_GROUP
|
||||
config:
|
||||
server-addr: ${NACOS_SERVER_ADDR:nacos:8848}
|
||||
file-extension: yml
|
||||
namespace: public
|
||||
group: DEFAULT_GROUP
|
||||
datasource:
|
||||
url: jdbc:mysql://${MYSQL_HOST:mysql}:${MYSQL_PORT:3306}/emotion_museum?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=false&serverTimezone=GMT%2B8&allowPublicKeyRetrieval=true
|
||||
username: root
|
||||
password: 123456
|
||||
driver-class-name: com.mysql.cj.jdbc.Driver
|
||||
hikari:
|
||||
pool-name: EmotionAiHikariCP
|
||||
minimum-idle: 5
|
||||
maximum-pool-size: 20
|
||||
auto-commit: true
|
||||
idle-timeout: 30000
|
||||
max-lifetime: 1800000
|
||||
connection-timeout: 30000
|
||||
data:
|
||||
redis:
|
||||
host: ${REDIS_HOST:redis}
|
||||
port: ${REDIS_PORT:6379}
|
||||
password:
|
||||
database: 1
|
||||
timeout: 6000ms
|
||||
lettuce:
|
||||
pool:
|
||||
max-active: 8
|
||||
max-wait: -1ms
|
||||
max-idle: 8
|
||||
min-idle: 0
|
||||
|
||||
# MyBatis Plus配置
|
||||
mybatis-plus:
|
||||
configuration:
|
||||
map-underscore-to-camel-case: true
|
||||
cache-enabled: false
|
||||
call-setters-on-nulls: true
|
||||
jdbc-type-for-null: 'null'
|
||||
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
|
||||
global-config:
|
||||
db-config:
|
||||
id-type: assign_uuid
|
||||
logic-delete-field: isDeleted
|
||||
logic-delete-value: 1
|
||||
logic-not-delete-value: 0
|
||||
banner: false
|
||||
|
||||
# Coze API配置
|
||||
coze:
|
||||
api:
|
||||
base-url: https://api.coze.cn
|
||||
token: ${COZE_API_TOKEN:your-coze-api-token}
|
||||
bot-id: 7523042446285439016
|
||||
workflow-id: 7523047462895796287
|
||||
timeout: 30000
|
||||
|
||||
# 日志配置
|
||||
logging:
|
||||
level:
|
||||
com.emotionmuseum: DEBUG
|
||||
com.emotionmuseum.ai.mapper: DEBUG
|
||||
pattern:
|
||||
console: "%d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level [%logger{50}] - %msg%n"
|
||||
|
||||
# 管理端点
|
||||
management:
|
||||
endpoints:
|
||||
web:
|
||||
exposure:
|
||||
include: health,info,metrics,prometheus
|
||||
endpoint:
|
||||
health:
|
||||
show-details: always
|
||||
@@ -0,0 +1,89 @@
|
||||
server:
|
||||
port: 19002
|
||||
|
||||
spring:
|
||||
application:
|
||||
name: emotion-ai
|
||||
|
||||
# 数据源配置
|
||||
datasource:
|
||||
driver-class-name: com.mysql.cj.jdbc.Driver
|
||||
url: jdbc:mysql://localhost:3306/emotion_museum?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=false&serverTimezone=GMT%2B8&allowPublicKeyRetrieval=true
|
||||
username: root
|
||||
password: 123456
|
||||
|
||||
# Redis配置
|
||||
data:
|
||||
redis:
|
||||
host: localhost
|
||||
port: 6379
|
||||
password:
|
||||
database: 1
|
||||
timeout: 10000ms
|
||||
lettuce:
|
||||
pool:
|
||||
max-active: 8
|
||||
max-wait: -1ms
|
||||
max-idle: 8
|
||||
min-idle: 0
|
||||
|
||||
# MyBatis Plus配置
|
||||
mybatis-plus:
|
||||
configuration:
|
||||
map-underscore-to-camel-case: true
|
||||
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
|
||||
global-config:
|
||||
db-config:
|
||||
logic-delete-field: isDeleted
|
||||
logic-delete-value: 1
|
||||
logic-not-delete-value: 0
|
||||
|
||||
# Nacos配置
|
||||
spring:
|
||||
cloud:
|
||||
nacos:
|
||||
discovery:
|
||||
server-addr: localhost:8848
|
||||
namespace:
|
||||
group: DEFAULT_GROUP
|
||||
enabled: true
|
||||
register-enabled: true
|
||||
heart-beat-interval: 5000
|
||||
heart-beat-timeout: 15000
|
||||
ip-delete-timeout: 30000
|
||||
config:
|
||||
server-addr: localhost:8848
|
||||
namespace:
|
||||
group: DEFAULT_GROUP
|
||||
file-extension: yml
|
||||
enabled: false
|
||||
|
||||
# Coze API配置
|
||||
coze:
|
||||
api:
|
||||
base-url: https://api.coze.cn
|
||||
token: ${COZE_API_TOKEN:your_coze_api_token}
|
||||
bot-id: 7523042446285439016
|
||||
workflow-id: 7523047462895796287
|
||||
timeout: 30000
|
||||
|
||||
# 日志配置
|
||||
logging:
|
||||
level:
|
||||
com.emotionmuseum: debug
|
||||
org.springframework.web: debug
|
||||
pattern:
|
||||
console: "%d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level %logger{50} - %msg%n"
|
||||
file: "%d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level %logger{50} - %msg%n"
|
||||
file:
|
||||
name: logs/emotion-ai-local.log
|
||||
|
||||
# 管理端点配置
|
||||
management:
|
||||
endpoints:
|
||||
web:
|
||||
exposure:
|
||||
include: health,info,metrics
|
||||
endpoint:
|
||||
health:
|
||||
show-details: always
|
||||
@@ -0,0 +1,95 @@
|
||||
server:
|
||||
port: 9002
|
||||
|
||||
spring:
|
||||
application:
|
||||
name: emotion-ai
|
||||
main:
|
||||
allow-bean-definition-overriding: true
|
||||
cloud:
|
||||
nacos:
|
||||
discovery:
|
||||
server-addr: ${NACOS_SERVER_ADDR:localhost:8848}
|
||||
namespace: public
|
||||
group: DEFAULT_GROUP
|
||||
enabled: true
|
||||
ip: ${SERVER_IP:localhost}
|
||||
metadata:
|
||||
version: 1.0.0
|
||||
environment: prod
|
||||
config:
|
||||
enabled: false
|
||||
datasource:
|
||||
url: jdbc:mysql://${MYSQL_HOST:localhost}:${MYSQL_PORT:3306}/${MYSQL_DATABASE:emotion_museum}?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=false&serverTimezone=GMT%2B8&allowPublicKeyRetrieval=true
|
||||
username: ${MYSQL_USERNAME:root}
|
||||
password: ${MYSQL_PASSWORD:123456}
|
||||
driver-class-name: com.mysql.cj.jdbc.Driver
|
||||
hikari:
|
||||
pool-name: EmotionAiHikariCP
|
||||
minimum-idle: 5
|
||||
maximum-pool-size: 20
|
||||
auto-commit: true
|
||||
idle-timeout: 30000
|
||||
max-lifetime: 1800000
|
||||
connection-timeout: 30000
|
||||
data:
|
||||
redis:
|
||||
host: ${REDIS_HOST:localhost}
|
||||
port: ${REDIS_PORT:6379}
|
||||
database: 0
|
||||
timeout: 3000ms
|
||||
lettuce:
|
||||
pool:
|
||||
max-active: 20
|
||||
max-idle: 10
|
||||
min-idle: 5
|
||||
max-wait: 3000ms
|
||||
|
||||
# MyBatis Plus配置
|
||||
mybatis-plus:
|
||||
configuration:
|
||||
map-underscore-to-camel-case: true
|
||||
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
|
||||
global-config:
|
||||
db-config:
|
||||
id-type: input
|
||||
logic-delete-field: is_deleted
|
||||
logic-delete-value: 1
|
||||
logic-not-delete-value: 0
|
||||
mapper-locations: classpath*:mapper/*.xml
|
||||
|
||||
# Coze平台配置
|
||||
coze:
|
||||
base-url: ${COZE_BASE_URL:https://api.coze.cn}
|
||||
api-key: ${COZE_API_KEY:your-coze-api-key}
|
||||
bot-id: ${COZE_BOT_ID:7523042446285439016}
|
||||
workflow-id: ${COZE_WORKFLOW_ID:7523047462895796287}
|
||||
user-id: ${COZE_USER_ID:emotion-museum-user}
|
||||
token: pat_GCR4qKzqpf90wMCvKsldMrB18KG3QsLDci65bZthssKsbLxu8X70BKYumleDcabO
|
||||
timeout: 60
|
||||
max-retries: 3
|
||||
stream: false
|
||||
model:
|
||||
temperature: 0.7
|
||||
max-tokens: 1000
|
||||
top-p: 0.9
|
||||
frequency-penalty: 0.0
|
||||
presence-penalty: 0.0
|
||||
|
||||
# 日志配置
|
||||
logging:
|
||||
level:
|
||||
com.emotionmuseum: INFO
|
||||
com.baomidou.mybatisplus: DEBUG
|
||||
pattern:
|
||||
console: "%d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level %logger{36} - %msg%n"
|
||||
|
||||
# 管理端点
|
||||
management:
|
||||
endpoints:
|
||||
web:
|
||||
exposure:
|
||||
include: health,info,metrics
|
||||
endpoint:
|
||||
health:
|
||||
show-details: always
|
||||
@@ -0,0 +1,112 @@
|
||||
server:
|
||||
port: 19002
|
||||
|
||||
spring:
|
||||
application:
|
||||
name: emotion-ai
|
||||
profiles:
|
||||
active: dev
|
||||
data:
|
||||
redis:
|
||||
host: localhost
|
||||
port: 6379
|
||||
database: 0
|
||||
timeout: 3000ms
|
||||
lettuce:
|
||||
pool:
|
||||
max-active: 20
|
||||
max-idle: 10
|
||||
min-idle: 5
|
||||
max-wait: 3000ms
|
||||
# 本地数据库配置(备用)
|
||||
datasource:
|
||||
driver-class-name: com.mysql.cj.jdbc.Driver
|
||||
url: jdbc:mysql://localhost:3306/emotion_museum?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8
|
||||
username: ${DB_USERNAME:root}
|
||||
password: ${DB_PASSWORD:123456}
|
||||
hikari:
|
||||
minimum-idle: 5
|
||||
maximum-pool-size: 20
|
||||
auto-commit: true
|
||||
idle-timeout: 30000
|
||||
pool-name: EmotionAiHikariCP
|
||||
max-lifetime: 1800000
|
||||
connection-timeout: 30000
|
||||
connection-test-query: SELECT 1
|
||||
cloud:
|
||||
nacos:
|
||||
discovery:
|
||||
server-addr: localhost:8848
|
||||
namespace: emotion-dev
|
||||
group: DEFAULT_GROUP
|
||||
enabled: false
|
||||
config:
|
||||
enabled: false
|
||||
|
||||
|
||||
|
||||
# Coze平台配置
|
||||
coze:
|
||||
base-url: ${COZE_BASE_URL:https://api.coze.cn}
|
||||
api-key: ${COZE_API_KEY:your-coze-api-key}
|
||||
bot-id: ${COZE_BOT_ID:7523042446285439016}
|
||||
workflow-id: ${COZE_WORKFLOW_ID:7523047462895796287}
|
||||
user-id: ${COZE_USER_ID:emotion-museum-user}
|
||||
token: pat_GCR4qKzqpf90wMCvKsldMrB18KG3QsLDci65bZthssKsbLxu8X70BKYumleDcabO
|
||||
timeout: 60
|
||||
max-retries: 3
|
||||
stream: false
|
||||
model:
|
||||
temperature: 0.7
|
||||
max-tokens: 1000
|
||||
top-p: 0.9
|
||||
frequency-penalty: 0.0
|
||||
presence-penalty: 0.0
|
||||
|
||||
# 功能开关配置
|
||||
features:
|
||||
emotion-analysis:
|
||||
enabled: ${EMOTION_ANALYSIS_ENABLED:false} # 暂时禁用情绪分析
|
||||
auto-analyze: false # 禁用自动情绪分析
|
||||
chat:
|
||||
enabled: true
|
||||
stream: false
|
||||
|
||||
# MyBatis Plus配置
|
||||
mybatis-plus:
|
||||
configuration:
|
||||
map-underscore-to-camel-case: true
|
||||
cache-enabled: false
|
||||
call-setters-on-nulls: true
|
||||
jdbc-type-for-null: 'null'
|
||||
global-config:
|
||||
db-config:
|
||||
id-type: ASSIGN_UUID
|
||||
logic-delete-field: is_deleted
|
||||
logic-delete-value: 1
|
||||
logic-not-delete-value: 0
|
||||
mapper-locations: classpath*:/mapper/**/*.xml
|
||||
|
||||
# 监控配置
|
||||
management:
|
||||
endpoints:
|
||||
web:
|
||||
exposure:
|
||||
include: health,info,metrics,prometheus
|
||||
endpoint:
|
||||
health:
|
||||
show-details: always
|
||||
metrics:
|
||||
export:
|
||||
prometheus:
|
||||
enabled: true
|
||||
|
||||
# 日志配置
|
||||
logging:
|
||||
level:
|
||||
com.emotionmuseum: debug
|
||||
com.baomidou.mybatisplus: debug
|
||||
com.emotionmuseum.common.handler.MetaObjectHandler: debug
|
||||
com.emotionmuseum.common.interceptor.UserContextInterceptor: debug
|
||||
pattern:
|
||||
console: "%d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level [%logger{50}] - %msg%n"
|
||||
@@ -0,0 +1,30 @@
|
||||
package com.emotionmuseum.ai.service;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.boot.test.context.SpringBootTest;
|
||||
|
||||
/**
|
||||
* 消息拆分功能测试
|
||||
*/
|
||||
@SpringBootTest
|
||||
public class MessageSplitTest {
|
||||
|
||||
@Test
|
||||
public void testMessageSplit() {
|
||||
// 测试消息拆分逻辑
|
||||
String aiContent = "这是第一段内容,介绍了基本功能。\n\n这是第二段内容,详细说明了聊天功能。\n\n这是第三段内容,介绍了情感分析功能。";
|
||||
|
||||
// 按\n\n拆分消息
|
||||
String[] messageParts = aiContent.split("\\n\\n");
|
||||
|
||||
System.out.println("原始消息: " + aiContent);
|
||||
System.out.println("拆分后的消息数量: " + messageParts.length);
|
||||
|
||||
for (int i = 0; i < messageParts.length; i++) {
|
||||
String part = messageParts[i].trim();
|
||||
if (!part.isEmpty()) {
|
||||
System.out.println("消息片段 " + (i + 1) + ": " + part);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,89 @@
|
||||
# AI服务 Docker环境配置
|
||||
server:
|
||||
port: 9002
|
||||
|
||||
spring:
|
||||
application:
|
||||
name: emotion-ai
|
||||
profiles:
|
||||
active: docker
|
||||
cloud:
|
||||
nacos:
|
||||
discovery:
|
||||
server-addr: ${NACOS_SERVER_ADDR:nacos:8848}
|
||||
namespace: public
|
||||
group: DEFAULT_GROUP
|
||||
config:
|
||||
server-addr: ${NACOS_SERVER_ADDR:nacos:8848}
|
||||
file-extension: yml
|
||||
namespace: public
|
||||
group: DEFAULT_GROUP
|
||||
datasource:
|
||||
url: jdbc:mysql://${MYSQL_HOST:mysql}:${MYSQL_PORT:3306}/emotion_museum?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=false&serverTimezone=GMT%2B8&allowPublicKeyRetrieval=true
|
||||
username: root
|
||||
password: 123456
|
||||
driver-class-name: com.mysql.cj.jdbc.Driver
|
||||
hikari:
|
||||
pool-name: EmotionAiHikariCP
|
||||
minimum-idle: 5
|
||||
maximum-pool-size: 20
|
||||
auto-commit: true
|
||||
idle-timeout: 30000
|
||||
max-lifetime: 1800000
|
||||
connection-timeout: 30000
|
||||
data:
|
||||
redis:
|
||||
host: ${REDIS_HOST:redis}
|
||||
port: ${REDIS_PORT:6379}
|
||||
password:
|
||||
database: 1
|
||||
timeout: 6000ms
|
||||
lettuce:
|
||||
pool:
|
||||
max-active: 8
|
||||
max-wait: -1ms
|
||||
max-idle: 8
|
||||
min-idle: 0
|
||||
|
||||
# MyBatis Plus配置
|
||||
mybatis-plus:
|
||||
configuration:
|
||||
map-underscore-to-camel-case: true
|
||||
cache-enabled: false
|
||||
call-setters-on-nulls: true
|
||||
jdbc-type-for-null: 'null'
|
||||
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
|
||||
global-config:
|
||||
db-config:
|
||||
id-type: assign_uuid
|
||||
logic-delete-field: isDeleted
|
||||
logic-delete-value: 1
|
||||
logic-not-delete-value: 0
|
||||
banner: false
|
||||
|
||||
# Coze API配置
|
||||
coze:
|
||||
api:
|
||||
base-url: https://api.coze.cn
|
||||
token: ${COZE_API_TOKEN:your-coze-api-token}
|
||||
bot-id: 7523042446285439016
|
||||
workflow-id: 7523047462895796287
|
||||
timeout: 30000
|
||||
|
||||
# 日志配置
|
||||
logging:
|
||||
level:
|
||||
com.emotionmuseum: DEBUG
|
||||
com.emotionmuseum.ai.mapper: DEBUG
|
||||
pattern:
|
||||
console: "%d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level [%logger{50}] - %msg%n"
|
||||
|
||||
# 管理端点
|
||||
management:
|
||||
endpoints:
|
||||
web:
|
||||
exposure:
|
||||
include: health,info,metrics,prometheus
|
||||
endpoint:
|
||||
health:
|
||||
show-details: always
|
||||
@@ -0,0 +1,89 @@
|
||||
server:
|
||||
port: 19002
|
||||
|
||||
spring:
|
||||
application:
|
||||
name: emotion-ai
|
||||
|
||||
# 数据源配置
|
||||
datasource:
|
||||
driver-class-name: com.mysql.cj.jdbc.Driver
|
||||
url: jdbc:mysql://localhost:3306/emotion_museum?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=false&serverTimezone=GMT%2B8&allowPublicKeyRetrieval=true
|
||||
username: root
|
||||
password: 123456
|
||||
|
||||
# Redis配置
|
||||
data:
|
||||
redis:
|
||||
host: localhost
|
||||
port: 6379
|
||||
password:
|
||||
database: 1
|
||||
timeout: 10000ms
|
||||
lettuce:
|
||||
pool:
|
||||
max-active: 8
|
||||
max-wait: -1ms
|
||||
max-idle: 8
|
||||
min-idle: 0
|
||||
|
||||
# MyBatis Plus配置
|
||||
mybatis-plus:
|
||||
configuration:
|
||||
map-underscore-to-camel-case: true
|
||||
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
|
||||
global-config:
|
||||
db-config:
|
||||
logic-delete-field: isDeleted
|
||||
logic-delete-value: 1
|
||||
logic-not-delete-value: 0
|
||||
|
||||
# Nacos配置
|
||||
spring:
|
||||
cloud:
|
||||
nacos:
|
||||
discovery:
|
||||
server-addr: localhost:8848
|
||||
namespace:
|
||||
group: DEFAULT_GROUP
|
||||
enabled: true
|
||||
register-enabled: true
|
||||
heart-beat-interval: 5000
|
||||
heart-beat-timeout: 15000
|
||||
ip-delete-timeout: 30000
|
||||
config:
|
||||
server-addr: localhost:8848
|
||||
namespace:
|
||||
group: DEFAULT_GROUP
|
||||
file-extension: yml
|
||||
enabled: false
|
||||
|
||||
# Coze API配置
|
||||
coze:
|
||||
api:
|
||||
base-url: https://api.coze.cn
|
||||
token: ${COZE_API_TOKEN:your_coze_api_token}
|
||||
bot-id: 7523042446285439016
|
||||
workflow-id: 7523047462895796287
|
||||
timeout: 30000
|
||||
|
||||
# 日志配置
|
||||
logging:
|
||||
level:
|
||||
com.emotionmuseum: debug
|
||||
org.springframework.web: debug
|
||||
pattern:
|
||||
console: "%d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level %logger{50} - %msg%n"
|
||||
file: "%d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level %logger{50} - %msg%n"
|
||||
file:
|
||||
name: logs/emotion-ai-local.log
|
||||
|
||||
# 管理端点配置
|
||||
management:
|
||||
endpoints:
|
||||
web:
|
||||
exposure:
|
||||
include: health,info,metrics
|
||||
endpoint:
|
||||
health:
|
||||
show-details: always
|
||||
@@ -0,0 +1,95 @@
|
||||
server:
|
||||
port: 9002
|
||||
|
||||
spring:
|
||||
application:
|
||||
name: emotion-ai
|
||||
main:
|
||||
allow-bean-definition-overriding: true
|
||||
cloud:
|
||||
nacos:
|
||||
discovery:
|
||||
server-addr: ${NACOS_SERVER_ADDR:localhost:8848}
|
||||
namespace: public
|
||||
group: DEFAULT_GROUP
|
||||
enabled: true
|
||||
ip: ${SERVER_IP:localhost}
|
||||
metadata:
|
||||
version: 1.0.0
|
||||
environment: prod
|
||||
config:
|
||||
enabled: false
|
||||
datasource:
|
||||
url: jdbc:mysql://${MYSQL_HOST:localhost}:${MYSQL_PORT:3306}/${MYSQL_DATABASE:emotion_museum}?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=false&serverTimezone=GMT%2B8&allowPublicKeyRetrieval=true
|
||||
username: ${MYSQL_USERNAME:root}
|
||||
password: ${MYSQL_PASSWORD:123456}
|
||||
driver-class-name: com.mysql.cj.jdbc.Driver
|
||||
hikari:
|
||||
pool-name: EmotionAiHikariCP
|
||||
minimum-idle: 5
|
||||
maximum-pool-size: 20
|
||||
auto-commit: true
|
||||
idle-timeout: 30000
|
||||
max-lifetime: 1800000
|
||||
connection-timeout: 30000
|
||||
data:
|
||||
redis:
|
||||
host: ${REDIS_HOST:localhost}
|
||||
port: ${REDIS_PORT:6379}
|
||||
database: 0
|
||||
timeout: 3000ms
|
||||
lettuce:
|
||||
pool:
|
||||
max-active: 20
|
||||
max-idle: 10
|
||||
min-idle: 5
|
||||
max-wait: 3000ms
|
||||
|
||||
# MyBatis Plus配置
|
||||
mybatis-plus:
|
||||
configuration:
|
||||
map-underscore-to-camel-case: true
|
||||
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
|
||||
global-config:
|
||||
db-config:
|
||||
id-type: input
|
||||
logic-delete-field: is_deleted
|
||||
logic-delete-value: 1
|
||||
logic-not-delete-value: 0
|
||||
mapper-locations: classpath*:mapper/*.xml
|
||||
|
||||
# Coze平台配置
|
||||
coze:
|
||||
base-url: ${COZE_BASE_URL:https://api.coze.cn}
|
||||
api-key: ${COZE_API_KEY:your-coze-api-key}
|
||||
bot-id: ${COZE_BOT_ID:7523042446285439016}
|
||||
workflow-id: ${COZE_WORKFLOW_ID:7523047462895796287}
|
||||
user-id: ${COZE_USER_ID:emotion-museum-user}
|
||||
token: pat_GCR4qKzqpf90wMCvKsldMrB18KG3QsLDci65bZthssKsbLxu8X70BKYumleDcabO
|
||||
timeout: 60
|
||||
max-retries: 3
|
||||
stream: false
|
||||
model:
|
||||
temperature: 0.7
|
||||
max-tokens: 1000
|
||||
top-p: 0.9
|
||||
frequency-penalty: 0.0
|
||||
presence-penalty: 0.0
|
||||
|
||||
# 日志配置
|
||||
logging:
|
||||
level:
|
||||
com.emotionmuseum: INFO
|
||||
com.baomidou.mybatisplus: DEBUG
|
||||
pattern:
|
||||
console: "%d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level %logger{36} - %msg%n"
|
||||
|
||||
# 管理端点
|
||||
management:
|
||||
endpoints:
|
||||
web:
|
||||
exposure:
|
||||
include: health,info,metrics
|
||||
endpoint:
|
||||
health:
|
||||
show-details: always
|
||||
@@ -0,0 +1,112 @@
|
||||
server:
|
||||
port: 19002
|
||||
|
||||
spring:
|
||||
application:
|
||||
name: emotion-ai
|
||||
profiles:
|
||||
active: dev
|
||||
data:
|
||||
redis:
|
||||
host: localhost
|
||||
port: 6379
|
||||
database: 0
|
||||
timeout: 3000ms
|
||||
lettuce:
|
||||
pool:
|
||||
max-active: 20
|
||||
max-idle: 10
|
||||
min-idle: 5
|
||||
max-wait: 3000ms
|
||||
# 本地数据库配置(备用)
|
||||
datasource:
|
||||
driver-class-name: com.mysql.cj.jdbc.Driver
|
||||
url: jdbc:mysql://localhost:3306/emotion_museum?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8
|
||||
username: ${DB_USERNAME:root}
|
||||
password: ${DB_PASSWORD:123456}
|
||||
hikari:
|
||||
minimum-idle: 5
|
||||
maximum-pool-size: 20
|
||||
auto-commit: true
|
||||
idle-timeout: 30000
|
||||
pool-name: EmotionAiHikariCP
|
||||
max-lifetime: 1800000
|
||||
connection-timeout: 30000
|
||||
connection-test-query: SELECT 1
|
||||
cloud:
|
||||
nacos:
|
||||
discovery:
|
||||
server-addr: localhost:8848
|
||||
namespace: emotion-dev
|
||||
group: DEFAULT_GROUP
|
||||
enabled: false
|
||||
config:
|
||||
enabled: false
|
||||
|
||||
|
||||
|
||||
# Coze平台配置
|
||||
coze:
|
||||
base-url: ${COZE_BASE_URL:https://api.coze.cn}
|
||||
api-key: ${COZE_API_KEY:your-coze-api-key}
|
||||
bot-id: ${COZE_BOT_ID:7523042446285439016}
|
||||
workflow-id: ${COZE_WORKFLOW_ID:7523047462895796287}
|
||||
user-id: ${COZE_USER_ID:emotion-museum-user}
|
||||
token: pat_GCR4qKzqpf90wMCvKsldMrB18KG3QsLDci65bZthssKsbLxu8X70BKYumleDcabO
|
||||
timeout: 60
|
||||
max-retries: 3
|
||||
stream: false
|
||||
model:
|
||||
temperature: 0.7
|
||||
max-tokens: 1000
|
||||
top-p: 0.9
|
||||
frequency-penalty: 0.0
|
||||
presence-penalty: 0.0
|
||||
|
||||
# 功能开关配置
|
||||
features:
|
||||
emotion-analysis:
|
||||
enabled: ${EMOTION_ANALYSIS_ENABLED:false} # 暂时禁用情绪分析
|
||||
auto-analyze: false # 禁用自动情绪分析
|
||||
chat:
|
||||
enabled: true
|
||||
stream: false
|
||||
|
||||
# MyBatis Plus配置
|
||||
mybatis-plus:
|
||||
configuration:
|
||||
map-underscore-to-camel-case: true
|
||||
cache-enabled: false
|
||||
call-setters-on-nulls: true
|
||||
jdbc-type-for-null: 'null'
|
||||
global-config:
|
||||
db-config:
|
||||
id-type: ASSIGN_UUID
|
||||
logic-delete-field: is_deleted
|
||||
logic-delete-value: 1
|
||||
logic-not-delete-value: 0
|
||||
mapper-locations: classpath*:/mapper/**/*.xml
|
||||
|
||||
# 监控配置
|
||||
management:
|
||||
endpoints:
|
||||
web:
|
||||
exposure:
|
||||
include: health,info,metrics,prometheus
|
||||
endpoint:
|
||||
health:
|
||||
show-details: always
|
||||
metrics:
|
||||
export:
|
||||
prometheus:
|
||||
enabled: true
|
||||
|
||||
# 日志配置
|
||||
logging:
|
||||
level:
|
||||
com.emotionmuseum: debug
|
||||
com.baomidou.mybatisplus: debug
|
||||
com.emotionmuseum.common.handler.MetaObjectHandler: debug
|
||||
com.emotionmuseum.common.interceptor.UserContextInterceptor: debug
|
||||
pattern:
|
||||
console: "%d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level [%logger{50}] - %msg%n"
|
||||
Binary file not shown.
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
Binary file not shown.
BIN
Binary file not shown.
Binary file not shown.
BIN
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,3 @@
|
||||
artifactId=emotion-ai
|
||||
groupId=com.emotionmuseum
|
||||
version=1.0.0
|
||||
+47
@@ -0,0 +1,47 @@
|
||||
com/emotionmuseum/ai/dto/CreateConversationResponse.class
|
||||
com/emotionmuseum/ai/dto/CreateConversationRequest.class
|
||||
com/emotionmuseum/ai/dto/GuestChatResponse$EmotionAnalysisResult$EmotionAnalysisResultBuilder.class
|
||||
com/emotionmuseum/ai/service/ConversationDbService.class
|
||||
com/emotionmuseum/ai/config/FeatureConfig.class
|
||||
com/emotionmuseum/ai/entity/GuestUser.class
|
||||
com/emotionmuseum/ai/service/GuestUserService.class
|
||||
com/emotionmuseum/ai/config/FeatureConfig$Chat.class
|
||||
com/emotionmuseum/ai/dto/MessageListResponse$MessageListResponseBuilder.class
|
||||
com/emotionmuseum/ai/service/GuestChatService.class
|
||||
com/emotionmuseum/ai/controller/GuestChatController.class
|
||||
com/emotionmuseum/ai/dto/ChatResponse$Usage.class
|
||||
com/emotionmuseum/ai/dto/GuestChatResponse.class
|
||||
com/emotionmuseum/ai/config/AiConfig.class
|
||||
com/emotionmuseum/ai/dto/EmotionAnalysisResponse.class
|
||||
com/emotionmuseum/ai/entity/Conversation.class
|
||||
com/emotionmuseum/ai/dto/GuestUserInfo.class
|
||||
com/emotionmuseum/ai/service/impl/GuestUserServiceImpl.class
|
||||
com/emotionmuseum/ai/dto/EmotionAnalysisRequest.class
|
||||
com/emotionmuseum/ai/dto/GuestUserInfo$GuestUserInfoBuilder.class
|
||||
com/emotionmuseum/ai/dto/ConversationListResponse.class
|
||||
com/emotionmuseum/ai/dto/GuestChatResponse$TokenUsage$TokenUsageBuilder.class
|
||||
com/emotionmuseum/ai/service/impl/ConversationDbServiceImpl.class
|
||||
com/emotionmuseum/ai/service/impl/AiChatServiceImpl.class
|
||||
com/emotionmuseum/ai/controller/AiChatController.class
|
||||
com/emotionmuseum/ai/dto/ChatResponse.class
|
||||
com/emotionmuseum/ai/dto/ChatRequest.class
|
||||
com/emotionmuseum/ai/config/FeatureConfig$EmotionAnalysis.class
|
||||
com/emotionmuseum/ai/dto/EmotionAnalysisResponse$EmotionScore.class
|
||||
com/emotionmuseum/ai/dto/GuestChatResponse$EmotionAnalysisResult.class
|
||||
com/emotionmuseum/ai/entity/Message.class
|
||||
com/emotionmuseum/ai/dto/MessageListResponse.class
|
||||
com/emotionmuseum/ai/entity/CozeApiCall.class
|
||||
com/emotionmuseum/ai/service/impl/GuestChatServiceImpl.class
|
||||
com/emotionmuseum/ai/service/impl/AiChatServiceImpl$ChatCompletionResult.class
|
||||
com/emotionmuseum/ai/service/AiChatService.class
|
||||
com/emotionmuseum/ai/dto/ConversationListResponse$ConversationListResponseBuilder.class
|
||||
com/emotionmuseum/ai/mapper/GuestUserMapper.class
|
||||
com/emotionmuseum/ai/dto/GuestChatResponse$GuestChatResponseBuilder.class
|
||||
com/emotionmuseum/ai/mapper/CozeApiCallMapper.class
|
||||
com/emotionmuseum/ai/entity/EmotionAnalysis.class
|
||||
com/emotionmuseum/ai/mapper/MessageMapper.class
|
||||
com/emotionmuseum/ai/mapper/ConversationMapper.class
|
||||
com/emotionmuseum/ai/AiApplication.class
|
||||
com/emotionmuseum/ai/dto/GuestChatRequest.class
|
||||
com/emotionmuseum/ai/dto/ChatRequest$ChatMessage.class
|
||||
com/emotionmuseum/ai/dto/GuestChatResponse$TokenUsage.class
|
||||
+33
@@ -0,0 +1,33 @@
|
||||
/Users/huazhongmin/peanut/AppleDevelop/EmotionMuseum/backend/emotion-ai/src/main/java/com/emotionmuseum/ai/dto/CreateConversationRequest.java
|
||||
/Users/huazhongmin/peanut/AppleDevelop/EmotionMuseum/backend/emotion-ai/src/main/java/com/emotionmuseum/ai/entity/Conversation.java
|
||||
/Users/huazhongmin/peanut/AppleDevelop/EmotionMuseum/backend/emotion-ai/src/main/java/com/emotionmuseum/ai/dto/ChatResponse.java
|
||||
/Users/huazhongmin/peanut/AppleDevelop/EmotionMuseum/backend/emotion-ai/src/main/java/com/emotionmuseum/ai/service/ConversationDbService.java
|
||||
/Users/huazhongmin/peanut/AppleDevelop/EmotionMuseum/backend/emotion-ai/src/main/java/com/emotionmuseum/ai/service/impl/ConversationDbServiceImpl.java
|
||||
/Users/huazhongmin/peanut/AppleDevelop/EmotionMuseum/backend/emotion-ai/src/main/java/com/emotionmuseum/ai/controller/AiChatController.java
|
||||
/Users/huazhongmin/peanut/AppleDevelop/EmotionMuseum/backend/emotion-ai/src/main/java/com/emotionmuseum/ai/service/impl/GuestChatServiceImpl.java
|
||||
/Users/huazhongmin/peanut/AppleDevelop/EmotionMuseum/backend/emotion-ai/src/main/java/com/emotionmuseum/ai/entity/GuestUser.java
|
||||
/Users/huazhongmin/peanut/AppleDevelop/EmotionMuseum/backend/emotion-ai/src/main/java/com/emotionmuseum/ai/entity/Message.java
|
||||
/Users/huazhongmin/peanut/AppleDevelop/EmotionMuseum/backend/emotion-ai/src/main/java/com/emotionmuseum/ai/mapper/GuestUserMapper.java
|
||||
/Users/huazhongmin/peanut/AppleDevelop/EmotionMuseum/backend/emotion-ai/src/main/java/com/emotionmuseum/ai/dto/MessageListResponse.java
|
||||
/Users/huazhongmin/peanut/AppleDevelop/EmotionMuseum/backend/emotion-ai/src/main/java/com/emotionmuseum/ai/service/impl/GuestUserServiceImpl.java
|
||||
/Users/huazhongmin/peanut/AppleDevelop/EmotionMuseum/backend/emotion-ai/src/main/java/com/emotionmuseum/ai/service/GuestUserService.java
|
||||
/Users/huazhongmin/peanut/AppleDevelop/EmotionMuseum/backend/emotion-ai/src/main/java/com/emotionmuseum/ai/dto/ChatRequest.java
|
||||
/Users/huazhongmin/peanut/AppleDevelop/EmotionMuseum/backend/emotion-ai/src/main/java/com/emotionmuseum/ai/dto/EmotionAnalysisRequest.java
|
||||
/Users/huazhongmin/peanut/AppleDevelop/EmotionMuseum/backend/emotion-ai/src/main/java/com/emotionmuseum/ai/dto/GuestChatResponse.java
|
||||
/Users/huazhongmin/peanut/AppleDevelop/EmotionMuseum/backend/emotion-ai/src/main/java/com/emotionmuseum/ai/dto/CreateConversationResponse.java
|
||||
/Users/huazhongmin/peanut/AppleDevelop/EmotionMuseum/backend/emotion-ai/src/main/java/com/emotionmuseum/ai/service/AiChatService.java
|
||||
/Users/huazhongmin/peanut/AppleDevelop/EmotionMuseum/backend/emotion-ai/src/main/java/com/emotionmuseum/ai/mapper/MessageMapper.java
|
||||
/Users/huazhongmin/peanut/AppleDevelop/EmotionMuseum/backend/emotion-ai/src/main/java/com/emotionmuseum/ai/service/impl/AiChatServiceImpl.java
|
||||
/Users/huazhongmin/peanut/AppleDevelop/EmotionMuseum/backend/emotion-ai/src/main/java/com/emotionmuseum/ai/dto/GuestChatRequest.java
|
||||
/Users/huazhongmin/peanut/AppleDevelop/EmotionMuseum/backend/emotion-ai/src/main/java/com/emotionmuseum/ai/mapper/CozeApiCallMapper.java
|
||||
/Users/huazhongmin/peanut/AppleDevelop/EmotionMuseum/backend/emotion-ai/src/main/java/com/emotionmuseum/ai/entity/CozeApiCall.java
|
||||
/Users/huazhongmin/peanut/AppleDevelop/EmotionMuseum/backend/emotion-ai/src/main/java/com/emotionmuseum/ai/AiApplication.java
|
||||
/Users/huazhongmin/peanut/AppleDevelop/EmotionMuseum/backend/emotion-ai/src/main/java/com/emotionmuseum/ai/entity/EmotionAnalysis.java
|
||||
/Users/huazhongmin/peanut/AppleDevelop/EmotionMuseum/backend/emotion-ai/src/main/java/com/emotionmuseum/ai/dto/ConversationListResponse.java
|
||||
/Users/huazhongmin/peanut/AppleDevelop/EmotionMuseum/backend/emotion-ai/src/main/java/com/emotionmuseum/ai/dto/GuestUserInfo.java
|
||||
/Users/huazhongmin/peanut/AppleDevelop/EmotionMuseum/backend/emotion-ai/src/main/java/com/emotionmuseum/ai/dto/EmotionAnalysisResponse.java
|
||||
/Users/huazhongmin/peanut/AppleDevelop/EmotionMuseum/backend/emotion-ai/src/main/java/com/emotionmuseum/ai/mapper/ConversationMapper.java
|
||||
/Users/huazhongmin/peanut/AppleDevelop/EmotionMuseum/backend/emotion-ai/src/main/java/com/emotionmuseum/ai/controller/GuestChatController.java
|
||||
/Users/huazhongmin/peanut/AppleDevelop/EmotionMuseum/backend/emotion-ai/src/main/java/com/emotionmuseum/ai/config/AiConfig.java
|
||||
/Users/huazhongmin/peanut/AppleDevelop/EmotionMuseum/backend/emotion-ai/src/main/java/com/emotionmuseum/ai/service/GuestChatService.java
|
||||
/Users/huazhongmin/peanut/AppleDevelop/EmotionMuseum/backend/emotion-ai/src/main/java/com/emotionmuseum/ai/config/FeatureConfig.java
|
||||
+1
@@ -0,0 +1 @@
|
||||
com/emotionmuseum/ai/service/MessageSplitTest.class
|
||||
+1
@@ -0,0 +1 @@
|
||||
/Users/huazhongmin/peanut/AppleDevelop/EmotionMuseum/backend/emotion-ai/src/test/java/com/emotionmuseum/ai/service/MessageSplitTest.java
|
||||
BIN
Binary file not shown.
Reference in New Issue
Block a user