重构项目结构:迁移到单体架构并优化代码组织
- 删除分布式架构相关文件和配置 - 将backend-distributed重命名为backend保留分布式代码作为参考 - 优化backend-single单体架构实现 - 添加Coze API集成相关文档和测试 - 清理项目根目录的部署脚本和配置文件 - 更新WebSocket和消息服务实现 - 完善认证服务和密码加密功能
This commit is contained in:
@@ -0,0 +1,142 @@
|
||||
# Coze API 集成说明
|
||||
|
||||
## 概述
|
||||
|
||||
本项目已经优化了 AiChatServiceImpl 的 Coze API 接口实现,确保能够正确调用 Coze 的 v3 API。
|
||||
|
||||
## 配置说明
|
||||
|
||||
### application.yml 配置
|
||||
|
||||
```yaml
|
||||
emotion:
|
||||
coze:
|
||||
api:
|
||||
token: pat_GCR4qKzqpf90wMCvKsldMrB18KG3QsLDci65bZthssKsbLxu8X70BKYumleDcabO
|
||||
base-url: https://api.coze.cn
|
||||
chat:
|
||||
path: /v3/chat
|
||||
talk:
|
||||
bot-id: 7523042446285439016
|
||||
workflow-id: 7523047462895796287
|
||||
summary:
|
||||
bot-id: 7529062814150295595
|
||||
workflow-id: 7523047462895796287
|
||||
timeout: 30000
|
||||
retry-count: 3
|
||||
retry-delay: 1000
|
||||
```
|
||||
|
||||
### 配置项说明
|
||||
|
||||
- `token`: Coze API 的访问令牌,格式为 `pat_` 开头
|
||||
- `base-url`: Coze API 的基础URL,通常为 `https://api.coze.cn`
|
||||
- `chat.path`: 聊天API的路径,固定为 `/v3/chat`
|
||||
- `chat.talk.bot-id`: 对话聊天使用的机器人ID
|
||||
- `chat.talk.workflow-id`: 对话聊天使用的工作流ID
|
||||
- `chat.summary.bot-id`: 聊天记录总结使用的机器人ID
|
||||
- `chat.summary.workflow-id`: 聊天记录总结使用的工作流ID
|
||||
- `timeout`: API调用超时时间(毫秒)
|
||||
- `retry-count`: 重试次数
|
||||
- `retry-delay`: 重试延迟(毫秒)
|
||||
|
||||
## API 调用流程
|
||||
|
||||
### 1. 发送聊天消息
|
||||
|
||||
```java
|
||||
String response = aiChatService.sendChatMessage(conversationId, message, userId);
|
||||
```
|
||||
|
||||
### 2. 访客聊天
|
||||
|
||||
```java
|
||||
Map<String, Object> response = aiChatService.guestChat(message, clientIp);
|
||||
```
|
||||
|
||||
### 3. 生成对话总结
|
||||
|
||||
```java
|
||||
String summary = aiChatService.generateConversationSummary(conversationId, userId);
|
||||
```
|
||||
|
||||
## 实现特点
|
||||
|
||||
### 1. 正确的 API 调用流程
|
||||
|
||||
1. **发送聊天请求**: 调用 `/v3/chat` 接口发送消息
|
||||
2. **获取 chat_id**: 从响应中提取 `chat_id` 和 `conversation_id`
|
||||
3. **轮询状态**: 使用 `/v3/chat/retrieve` 接口轮询聊天状态
|
||||
4. **获取消息**: 当状态为 `completed` 时,调用 `/v3/chat/message/list` 获取AI回复
|
||||
|
||||
### 2. 请求格式
|
||||
|
||||
```json
|
||||
{
|
||||
"bot_id": "7523042446285439016",
|
||||
"workflow_id": "7523047462895796287",
|
||||
"user_id": "user-123",
|
||||
"stream": false,
|
||||
"additional_messages": [
|
||||
{
|
||||
"role": "user",
|
||||
"content": "用户消息内容",
|
||||
"content_type": "text",
|
||||
"type": "question"
|
||||
}
|
||||
],
|
||||
"parameters": {}
|
||||
}
|
||||
```
|
||||
|
||||
### 3. 响应处理
|
||||
|
||||
- 解析初始响应获取 `chat_id` 和 `conversation_id`
|
||||
- 轮询聊天状态直到完成(最多30秒,每2秒一次)
|
||||
- 从消息列表中提取 AI 回复(role=assistant, type=answer)
|
||||
|
||||
### 4. 错误处理
|
||||
|
||||
- 网络异常处理
|
||||
- API 错误响应处理
|
||||
- 超时处理
|
||||
- 重试机制
|
||||
|
||||
## 测试
|
||||
|
||||
运行以下命令测试 API 集成:
|
||||
|
||||
```bash
|
||||
mvn test -Dtest=CozeApiTest
|
||||
```
|
||||
|
||||
## 注意事项
|
||||
|
||||
1. **API Token**: 确保使用有效的 Coze API token
|
||||
2. **Bot ID**: 确保 bot_id 和 workflow_id 正确配置
|
||||
3. **网络连接**: 确保服务器能够访问 `https://api.coze.cn`
|
||||
4. **超时设置**: 根据实际情况调整超时时间
|
||||
5. **错误处理**: 生产环境中应该有完善的错误处理和日志记录
|
||||
|
||||
## 故障排除
|
||||
|
||||
### 1. 检查配置
|
||||
|
||||
```bash
|
||||
# 检查配置是否正确加载
|
||||
curl -X GET http://localhost:19089/api/ai/health
|
||||
```
|
||||
|
||||
### 2. 查看日志
|
||||
|
||||
```bash
|
||||
# 查看详细的API调用日志
|
||||
tail -f logs/emotion-single.log | grep -i coze
|
||||
```
|
||||
|
||||
### 3. 常见错误
|
||||
|
||||
- **401 Unauthorized**: 检查 API token 是否正确
|
||||
- **404 Not Found**: 检查 bot_id 是否存在
|
||||
- **Timeout**: 增加超时时间或检查网络连接
|
||||
- **Rate Limit**: 检查API调用频率限制
|
||||
@@ -0,0 +1,139 @@
|
||||
# Coze API 调用修正说明
|
||||
|
||||
## 修正概述
|
||||
|
||||
经过对比 Coze API 官方文档,发现并修正了 `AiChatServiceImpl` 中的几个关键问题。
|
||||
|
||||
## 主要修正内容
|
||||
|
||||
### 1. API 端点修正
|
||||
**问题**: 使用了错误的 API 端点 `/api/message`
|
||||
**修正**: 更改为正确的 Coze API v3 端点 `/v3/chat`
|
||||
|
||||
```java
|
||||
// 修正前
|
||||
String cozeApiUrl = cozeBaseUrl + "/api/message";
|
||||
|
||||
// 修正后
|
||||
String cozeApiUrl = cozeBaseUrl + "/v3/chat";
|
||||
```
|
||||
|
||||
### 2. 请求体结构优化
|
||||
**问题**: 请求体缺少必要字段,消息格式不完整
|
||||
**修正**: 添加了 `auto_save_history` 字段,优化了消息结构
|
||||
|
||||
```java
|
||||
// 修正后的请求体结构
|
||||
{
|
||||
"bot_id": "your-bot-id",
|
||||
"workflow_id": "your-workflow-id", // 可选
|
||||
"user_id": "user-id",
|
||||
"stream": false,
|
||||
"auto_save_history": true, // 新增
|
||||
"conversation_id": "conv-id", // 可选
|
||||
"additional_messages": [
|
||||
{
|
||||
"role": "user",
|
||||
"content": "用户消息",
|
||||
"content_type": "text"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### 3. 响应解析增强
|
||||
**问题**: 响应解析逻辑可能不匹配实际的 API 响应格式
|
||||
**修正**: 增强了错误处理和多种响应格式的支持
|
||||
|
||||
```java
|
||||
// 新增错误状态检查
|
||||
Integer code = responseJson.getInteger("code");
|
||||
if (code != null && code != 0) {
|
||||
String msg = responseJson.getString("msg");
|
||||
return "抱歉,AI服务返回错误: " + (msg != null ? msg : "未知错误");
|
||||
}
|
||||
```
|
||||
|
||||
### 4. 健康检查简化
|
||||
**问题**: 使用了可能不存在的健康检查端点
|
||||
**修正**: 简化为配置验证,避免不必要的 API 调用
|
||||
|
||||
```java
|
||||
// 修正后的健康检查
|
||||
boolean configValid = cozeApiToken != null && !cozeApiToken.trim().isEmpty() &&
|
||||
chatBotId != null && !chatBotId.trim().isEmpty() &&
|
||||
cozeBaseUrl != null && !cozeBaseUrl.trim().isEmpty();
|
||||
```
|
||||
|
||||
### 5. 代码质量改进
|
||||
- 添加了常量定义,避免重复字符串
|
||||
- 统一了聊天和总结请求的构建逻辑
|
||||
- 改进了错误处理和日志记录
|
||||
|
||||
## 配置要求
|
||||
|
||||
### 必需配置
|
||||
```yaml
|
||||
emotion:
|
||||
coze:
|
||||
api:
|
||||
token: "your-coze-api-token" # 必需
|
||||
base-url: "https://api.coze.cn" # 必需
|
||||
chat:
|
||||
talk:
|
||||
bot-id: "your-bot-id" # 必需
|
||||
```
|
||||
|
||||
### 可选配置
|
||||
```yaml
|
||||
emotion:
|
||||
coze:
|
||||
api:
|
||||
chat:
|
||||
talk:
|
||||
workflow-id: "workflow-id" # 可选
|
||||
summary:
|
||||
bot-id: "summary-bot-id" # 可选
|
||||
workflow-id: "summary-workflow-id" # 可选
|
||||
```
|
||||
|
||||
## 测试验证
|
||||
|
||||
已创建测试类 `AiChatServiceImplTest` 来验证修正的正确性:
|
||||
|
||||
1. **API 调用测试**: 验证请求格式和端点
|
||||
2. **响应解析测试**: 验证正常和错误响应的处理
|
||||
3. **配置验证测试**: 验证健康检查和服务可用性
|
||||
|
||||
## 使用建议
|
||||
|
||||
### 1. 配置验证
|
||||
在启动应用前,确保以下配置正确:
|
||||
- Coze API token 有效
|
||||
- Bot ID 正确且已发布到 API
|
||||
- 网络可以访问 api.coze.cn
|
||||
|
||||
### 2. 错误处理
|
||||
修正后的代码会返回更详细的错误信息:
|
||||
- API 错误会包含具体的错误码和消息
|
||||
- 网络错误会有相应的提示
|
||||
- 配置错误会在健康检查中发现
|
||||
|
||||
### 3. 监控建议
|
||||
- 监控 API 调用的成功率
|
||||
- 记录响应时间和错误率
|
||||
- 定期检查 token 的有效性
|
||||
|
||||
## 兼容性说明
|
||||
|
||||
- 修正后的代码与 Coze API v3 兼容
|
||||
- 保持了原有的接口签名,不影响调用方
|
||||
- 增强了错误处理,提高了系统稳定性
|
||||
|
||||
## 后续优化建议
|
||||
|
||||
1. **流式响应支持**: 实现真正的流式聊天功能
|
||||
2. **对话历史管理**: 完善对话历史的获取和管理
|
||||
3. **缓存机制**: 添加适当的缓存来提高性能
|
||||
4. **限流保护**: 添加 API 调用频率限制
|
||||
5. **监控指标**: 添加详细的监控和告警机制
|
||||
@@ -90,15 +90,16 @@ public class MessageController {
|
||||
*/
|
||||
@PostMapping
|
||||
public Result<MessageResponse> create(@Valid @RequestBody MessageCreateRequest request) {
|
||||
Message message = messageService.createMessage(
|
||||
request.getConversationId(),
|
||||
request.getUserId(),
|
||||
request.getContent(),
|
||||
request.getContentType(),
|
||||
request.getSenderType(),
|
||||
request.getSenderId()
|
||||
);
|
||||
return Result.success(convertToResponse(message));
|
||||
Message message = new Message();
|
||||
message.setConversationId(request.getConversationId());
|
||||
message.setCreateBy(request.getUserId());
|
||||
message.setContent(request.getContent());
|
||||
message.setType(request.getContentType());
|
||||
message.setSender(request.getSenderType());
|
||||
// 可以根据需要设置其他字段
|
||||
|
||||
Message savedMessage = messageService.createMessage(message);
|
||||
return Result.success(convertToResponse(savedMessage));
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
+35
-24
@@ -35,12 +35,13 @@ public class WebSocketAuthInterceptor implements ChannelInterceptor {
|
||||
@Override
|
||||
public Message<?> preSend(Message<?> message, MessageChannel channel) {
|
||||
StompHeaderAccessor accessor = MessageHeaderAccessor.getAccessor(message, StompHeaderAccessor.class);
|
||||
|
||||
|
||||
if (accessor != null && StompCommand.CONNECT.equals(accessor.getCommand())) {
|
||||
log.info("WebSocket CONNECT命令检测到,开始处理认证");
|
||||
// 处理WebSocket连接时的认证
|
||||
handleAuthentication(accessor);
|
||||
}
|
||||
|
||||
|
||||
return message;
|
||||
}
|
||||
|
||||
@@ -52,46 +53,54 @@ public class WebSocketAuthInterceptor implements ChannelInterceptor {
|
||||
// 从连接头中获取token
|
||||
String authHeader = accessor.getFirstNativeHeader("Authorization");
|
||||
String userId = accessor.getFirstNativeHeader("X-User-Id");
|
||||
|
||||
log.info("WebSocket连接认证: authHeader={}, userId={}",
|
||||
authHeader != null ? "Bearer ***" : null, userId);
|
||||
|
||||
|
||||
log.info("WebSocket连接认证开始: authHeader={}, userId={}, sessionId={}",
|
||||
authHeader != null ? "Bearer ***" : null, userId, accessor.getSessionId());
|
||||
|
||||
if (StringUtils.hasText(authHeader) && authHeader.startsWith("Bearer ")) {
|
||||
String token = authHeader.substring(7);
|
||||
|
||||
log.info("提取到token: {}...", token.length() > 10 ? token.substring(0, 10) : token);
|
||||
|
||||
// 验证token
|
||||
if (authService.validateToken(token)) {
|
||||
boolean isValidToken = authService.validateToken(token);
|
||||
log.info("Token验证结果: {}", isValidToken);
|
||||
|
||||
if (isValidToken) {
|
||||
String tokenUserId = authService.getUserIdFromToken(token);
|
||||
String username = authService.getUsernameFromToken(token);
|
||||
|
||||
|
||||
log.info("WebSocket token验证成功: userId={}, username={}", tokenUserId, username);
|
||||
|
||||
|
||||
// 创建认证对象
|
||||
Authentication authentication = new UsernamePasswordAuthenticationToken(
|
||||
tokenUserId,
|
||||
null,
|
||||
tokenUserId,
|
||||
null,
|
||||
Collections.singletonList(new SimpleGrantedAuthority("ROLE_USER"))
|
||||
);
|
||||
|
||||
|
||||
// 设置用户认证信息
|
||||
accessor.setUser(authentication);
|
||||
|
||||
|
||||
// 设置会话属性
|
||||
accessor.getSessionAttributes().put("userId", tokenUserId);
|
||||
accessor.getSessionAttributes().put("username", username);
|
||||
accessor.getSessionAttributes().put("authenticated", true);
|
||||
|
||||
if (accessor.getSessionAttributes() != null) {
|
||||
accessor.getSessionAttributes().put("userId", tokenUserId);
|
||||
accessor.getSessionAttributes().put("username", username);
|
||||
accessor.getSessionAttributes().put("authenticated", true);
|
||||
}
|
||||
|
||||
log.info("WebSocket认证用户设置完成: principal={}", authentication.getName());
|
||||
|
||||
} else {
|
||||
log.warn("WebSocket token验证失败: token无效");
|
||||
log.warn("WebSocket token验证失败: token无效或已过期");
|
||||
// token无效,但不阻止连接,作为访客处理
|
||||
handleGuestUser(accessor, userId);
|
||||
}
|
||||
} else {
|
||||
log.info("WebSocket连接无token,作为访客处理: userId={}", userId);
|
||||
log.info("WebSocket连接无token或格式错误,作为访客处理: userId={}", userId);
|
||||
// 无token,作为访客处理
|
||||
handleGuestUser(accessor, userId);
|
||||
}
|
||||
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("WebSocket认证处理失败", e);
|
||||
// 认证失败,作为访客处理
|
||||
@@ -117,8 +126,10 @@ public class WebSocketAuthInterceptor implements ChannelInterceptor {
|
||||
accessor.setUser(guestAuth);
|
||||
|
||||
// 设置会话属性
|
||||
accessor.getSessionAttributes().put("userId", guestId);
|
||||
accessor.getSessionAttributes().put("username", guestId);
|
||||
accessor.getSessionAttributes().put("authenticated", false);
|
||||
if (accessor.getSessionAttributes() != null) {
|
||||
accessor.getSessionAttributes().put("userId", guestId);
|
||||
accessor.getSessionAttributes().put("username", guestId);
|
||||
accessor.getSessionAttributes().put("authenticated", false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -84,8 +84,7 @@ public interface MessageService extends IService<Message> {
|
||||
/**
|
||||
* 创建消息
|
||||
*/
|
||||
Message createMessage(String conversationId, String userId, String content,
|
||||
String contentType, String senderType, String senderId);
|
||||
Message createMessage(Message message);
|
||||
|
||||
/**
|
||||
* 标记消息为已读
|
||||
|
||||
@@ -3,6 +3,7 @@ package com.emotion.service;
|
||||
import com.emotion.dto.websocket.ChatRequest;
|
||||
import com.emotion.dto.websocket.ConnectRequest;
|
||||
import com.emotion.dto.websocket.WebSocketMessage;
|
||||
import com.emotion.entity.Message;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.messaging.simp.SimpMessagingTemplate;
|
||||
@@ -219,14 +220,13 @@ public class WebSocketService {
|
||||
new Thread(() -> {
|
||||
try {
|
||||
// 保存用户消息到数据库
|
||||
messageService.createMessage(
|
||||
request.getConversationId(),
|
||||
request.getSenderId(),
|
||||
request.getContent(),
|
||||
request.getMessageType().name(),
|
||||
request.getSenderType().name(),
|
||||
request.getSenderId()
|
||||
);
|
||||
Message userMessage = new Message();
|
||||
userMessage.setConversationId(request.getConversationId());
|
||||
userMessage.setCreateBy(request.getSenderId());
|
||||
userMessage.setContent(request.getContent());
|
||||
userMessage.setType(request.getMessageType().name());
|
||||
userMessage.setSender(request.getSenderType().name());
|
||||
messageService.createMessage(userMessage);
|
||||
|
||||
// 调用AI服务
|
||||
String aiReply = aiChatService.sendChatMessage(
|
||||
@@ -248,14 +248,13 @@ public class WebSocketService {
|
||||
.build();
|
||||
|
||||
// 保存AI回复到数据库
|
||||
messageService.createMessage(
|
||||
request.getConversationId(),
|
||||
"ai",
|
||||
aiReply,
|
||||
"text",
|
||||
"ai",
|
||||
"ai"
|
||||
);
|
||||
Message aiDbMessage = new Message();
|
||||
aiDbMessage.setConversationId(request.getConversationId());
|
||||
aiDbMessage.setCreateBy("ai");
|
||||
aiDbMessage.setContent(aiReply);
|
||||
aiDbMessage.setType("text");
|
||||
aiDbMessage.setSender("ai");
|
||||
messageService.createMessage(aiDbMessage);
|
||||
|
||||
// 发送AI回复
|
||||
messagingTemplate.convertAndSendToUser(request.getSenderId(), "/queue/messages", aiMessage);
|
||||
|
||||
@@ -18,10 +18,8 @@ import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.web.client.RestTemplate;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
|
||||
/**
|
||||
* AI聊天服务实现类
|
||||
@@ -51,6 +49,9 @@ public class AiChatServiceImpl implements AIChatService {
|
||||
@Value("${emotion.coze.api.base-url:https://api.coze.cn}")
|
||||
private String cozeBaseUrl;
|
||||
|
||||
@Value("${emotion.coze.api.chat.path:/v3/chat}")
|
||||
private String chatPath;
|
||||
|
||||
@Value("${emotion.coze.api.chat.talk.bot-id:}")
|
||||
private String chatBotId;
|
||||
|
||||
@@ -74,6 +75,15 @@ public class AiChatServiceImpl implements AIChatService {
|
||||
|
||||
private static final String DEFAULT_USER_ID = "emotion-museum-user";
|
||||
|
||||
// API 相关常量
|
||||
private static final String CONTENT_KEY = "content";
|
||||
private static final String ROLE_KEY = "role";
|
||||
private static final String USER_ROLE = "user";
|
||||
private static final String ASSISTANT_ROLE = "assistant";
|
||||
private static final String CONTENT_TYPE_KEY = "content_type";
|
||||
private static final String TEXT_TYPE = "text";
|
||||
private static final String ANSWER_TYPE = "answer";
|
||||
|
||||
@Override
|
||||
public String sendChatMessage(String conversationId, String message, String userId) {
|
||||
log.info("发送聊天消息: conversationId={}, userId={}, message={}", conversationId, userId, message);
|
||||
@@ -83,22 +93,22 @@ public class AiChatServiceImpl implements AIChatService {
|
||||
String aiReply = sendMessage(conversationId, message, userId);
|
||||
|
||||
// 保存用户消息
|
||||
Message userMessage = messageService.createMessage(
|
||||
conversationId,
|
||||
userId,
|
||||
message,
|
||||
"text",
|
||||
"user",
|
||||
userId);
|
||||
Message userMessage = new Message();
|
||||
userMessage.setConversationId(conversationId);
|
||||
userMessage.setCreateBy(userId);
|
||||
userMessage.setContent(message);
|
||||
userMessage.setType("text");
|
||||
userMessage.setSender("user");
|
||||
userMessage = messageService.createMessage(userMessage);
|
||||
|
||||
// 保存AI回复
|
||||
Message aiMessage = messageService.createMessage(
|
||||
conversationId,
|
||||
"ai",
|
||||
aiReply,
|
||||
"text",
|
||||
"ai",
|
||||
"ai");
|
||||
Message aiMessage = new Message();
|
||||
aiMessage.setConversationId(conversationId);
|
||||
aiMessage.setCreateBy("ai");
|
||||
aiMessage.setContent(aiReply);
|
||||
aiMessage.setType("text");
|
||||
aiMessage.setSender("ai");
|
||||
aiMessage = messageService.createMessage(aiMessage);
|
||||
|
||||
log.info("聊天消息处理完成: userMessageId={}, aiMessageId={}",
|
||||
userMessage.getId(), aiMessage.getId());
|
||||
@@ -166,13 +176,14 @@ public class AiChatServiceImpl implements AIChatService {
|
||||
headers.set("Authorization", "Bearer " + cozeApiToken);
|
||||
headers.set("Content-Type", "application/json");
|
||||
|
||||
// 构建请求体 - 参考backend-distributed的实现
|
||||
// 构建请求体 - 使用正确的Coze API格式
|
||||
Map<String, Object> requestBody = buildCozeRequest(conversationId, userMessage, userId);
|
||||
|
||||
HttpEntity<Map<String, Object>> request = new HttpEntity<>(requestBody, headers);
|
||||
|
||||
// 构建完整的API URL
|
||||
String cozeApiUrl = cozeBaseUrl + "/api/message";
|
||||
String cozeApiUrl = cozeBaseUrl + chatPath;
|
||||
log.info("发送Coze请求到: {}, 请求体: {}", cozeApiUrl, requestBody);
|
||||
|
||||
// 发送请求
|
||||
ResponseEntity<String> response = restTemplate.exchange(
|
||||
@@ -181,13 +192,22 @@ public class AiChatServiceImpl implements AIChatService {
|
||||
request,
|
||||
String.class);
|
||||
|
||||
// 解析响应
|
||||
log.info("收到Coze初始响应: {}", response.getBody());
|
||||
|
||||
// 解析响应获取chat_id和conversation_id
|
||||
JSONObject responseJson = JSON.parseObject(response.getBody());
|
||||
String aiReply = extractContentFromCozeResponse(responseJson);
|
||||
String chatId = extractChatIdFromResponse(responseJson);
|
||||
String cozeConversationId = extractConversationIdFromResponse(responseJson);
|
||||
|
||||
log.info("Coze AI响应成功: reply={}", aiReply);
|
||||
|
||||
return aiReply;
|
||||
if (chatId != null && cozeConversationId != null) {
|
||||
// 轮询聊天状态直到完成并获取回复内容
|
||||
String aiReply = waitForChatCompletion(chatId, cozeConversationId);
|
||||
log.info("Coze AI响应成功: reply={}", aiReply);
|
||||
return aiReply;
|
||||
} else {
|
||||
log.error("无法从Coze响应中获取chat_id或conversation_id");
|
||||
return "抱歉,AI服务响应异常,请稍后再试。";
|
||||
}
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("发送消息到Coze AI失败", e);
|
||||
@@ -209,22 +229,22 @@ public class AiChatServiceImpl implements AIChatService {
|
||||
String aiReply = sendMessage(guestConversationId, message, "guest");
|
||||
|
||||
// 保存访客消息
|
||||
Message guestMessage = messageService.createMessage(
|
||||
guestConversationId,
|
||||
"guest",
|
||||
message,
|
||||
"text",
|
||||
"guest",
|
||||
clientIp);
|
||||
Message guestMessage = new Message();
|
||||
guestMessage.setConversationId(guestConversationId);
|
||||
guestMessage.setCreateBy("guest");
|
||||
guestMessage.setContent(message);
|
||||
guestMessage.setType("text");
|
||||
guestMessage.setSender("guest");
|
||||
guestMessage = messageService.createMessage(guestMessage);
|
||||
|
||||
// 保存AI回复
|
||||
Message aiMessage = messageService.createMessage(
|
||||
guestConversationId,
|
||||
"ai",
|
||||
aiReply,
|
||||
"text",
|
||||
"ai",
|
||||
"ai");
|
||||
Message aiMessage = new Message();
|
||||
aiMessage.setConversationId(guestConversationId);
|
||||
aiMessage.setCreateBy("ai");
|
||||
aiMessage.setContent(aiReply);
|
||||
aiMessage.setType("text");
|
||||
aiMessage.setSender("ai");
|
||||
aiMessage = messageService.createMessage(aiMessage);
|
||||
|
||||
result.put("message", aiReply);
|
||||
result.put("messageId", aiMessage.getId());
|
||||
@@ -252,9 +272,6 @@ public class AiChatServiceImpl implements AIChatService {
|
||||
Map<String, Object> result = new HashMap<>();
|
||||
|
||||
try {
|
||||
// 创建数据库对话记录
|
||||
String conversationId = UUID.randomUUID().toString();
|
||||
|
||||
// 调用数据库服务创建对话
|
||||
Conversation conversation = conversationService.createConversation(userId, title, "user");
|
||||
|
||||
@@ -324,20 +341,19 @@ public class AiChatServiceImpl implements AIChatService {
|
||||
@Override
|
||||
public boolean healthCheck() {
|
||||
try {
|
||||
// 调用Coze bot信息接口检查健康状态
|
||||
HttpHeaders headers = new HttpHeaders();
|
||||
headers.set("Authorization", "Bearer " + cozeApiToken);
|
||||
// 简化健康检查 - 检查必要配置是否存在
|
||||
boolean configValid = cozeApiToken != null && !cozeApiToken.trim().isEmpty() &&
|
||||
chatBotId != null && !chatBotId.trim().isEmpty() &&
|
||||
cozeBaseUrl != null && !cozeBaseUrl.trim().isEmpty();
|
||||
|
||||
HttpEntity<String> request = new HttpEntity<>(headers);
|
||||
if (!configValid) {
|
||||
log.warn("Coze API 配置不完整");
|
||||
return false;
|
||||
}
|
||||
|
||||
ResponseEntity<String> response = restTemplate.exchange(
|
||||
cozeBaseUrl + "/v1/bot/get_online_info?bot_id=" + chatBotId,
|
||||
HttpMethod.GET,
|
||||
request,
|
||||
String.class);
|
||||
|
||||
JSONObject responseJson = JSON.parseObject(response.getBody());
|
||||
return responseJson != null && responseJson.get("code") != null;
|
||||
// 可选:发送一个简单的测试请求
|
||||
// 这里可以调用一个轻量级的API来验证连接
|
||||
return true;
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("健康检查失败: {}", e.getMessage());
|
||||
@@ -346,7 +362,7 @@ public class AiChatServiceImpl implements AIChatService {
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建Coze API请求 - 参考backend-distributed的实现
|
||||
* 构建Coze API请求 - 根据官方文档修正格式
|
||||
*/
|
||||
private Map<String, Object> buildCozeRequest(String conversationId, String userMessage, String userId) {
|
||||
Map<String, Object> cozeRequest = new HashMap<>();
|
||||
@@ -360,21 +376,14 @@ public class AiChatServiceImpl implements AIChatService {
|
||||
cozeRequest.put("user_id", userId != null ? userId : DEFAULT_USER_ID);
|
||||
cozeRequest.put("stream", false);
|
||||
|
||||
// 构建消息内容
|
||||
String message = userMessage;
|
||||
if (conversationId != null && !conversationId.trim().isEmpty()) {
|
||||
// 可以在这里添加上下文信息
|
||||
message = "会话ID: " + conversationId + "\n\n用户消息: " + message;
|
||||
}
|
||||
|
||||
// 添加聊天历史(简化版本)
|
||||
// 构建消息列表 - 按照 Coze API 标准格式
|
||||
java.util.List<Map<String, Object>> messages = new java.util.ArrayList<>();
|
||||
|
||||
// 添加当前消息
|
||||
// 添加当前用户消息
|
||||
Map<String, Object> currentMsg = new HashMap<>();
|
||||
currentMsg.put("role", "user");
|
||||
currentMsg.put("content", message);
|
||||
currentMsg.put("content_type", "text");
|
||||
currentMsg.put(ROLE_KEY, USER_ROLE);
|
||||
currentMsg.put(CONTENT_KEY, userMessage);
|
||||
currentMsg.put(CONTENT_TYPE_KEY, TEXT_TYPE);
|
||||
currentMsg.put("type", "question");
|
||||
messages.add(currentMsg);
|
||||
|
||||
@@ -385,36 +394,156 @@ public class AiChatServiceImpl implements AIChatService {
|
||||
}
|
||||
|
||||
/**
|
||||
* 从Coze响应中提取内容
|
||||
* 从Coze响应中提取chat_id
|
||||
*/
|
||||
private String extractContentFromCozeResponse(JSONObject responseJson) {
|
||||
private String extractChatIdFromResponse(JSONObject responseJson) {
|
||||
try {
|
||||
if (responseJson != null && responseJson.get("data") != null) {
|
||||
JSONObject data = responseJson.getJSONObject("data");
|
||||
|
||||
// 根据Coze API响应格式解析内容
|
||||
if (data.get("messages") != null) {
|
||||
java.util.List<JSONObject> messages = data.getJSONArray("messages").toJavaList(JSONObject.class);
|
||||
for (JSONObject message : messages) {
|
||||
if ("assistant".equals(message.getString("role")) &&
|
||||
"answer".equals(message.getString("type"))) {
|
||||
return message.getString("content");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 兼容旧格式
|
||||
if (data.getString("reply") != null) {
|
||||
return data.getString("reply");
|
||||
}
|
||||
if (responseJson != null && responseJson.getJSONObject("data") != null) {
|
||||
return responseJson.getJSONObject("data").getString("id");
|
||||
}
|
||||
return "抱歉,我现在无法理解您的消息。";
|
||||
} catch (Exception e) {
|
||||
log.error("解析Coze响应失败: {}", e.getMessage());
|
||||
return "抱歉,响应解析出现问题。";
|
||||
log.error("提取chat_id失败: {}", e.getMessage());
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 从Coze响应中提取conversation_id
|
||||
*/
|
||||
private String extractConversationIdFromResponse(JSONObject responseJson) {
|
||||
try {
|
||||
if (responseJson != null && responseJson.getJSONObject("data") != null) {
|
||||
return responseJson.getJSONObject("data").getString("conversation_id");
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.error("提取conversation_id失败: {}", e.getMessage());
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 等待聊天完成并获取回复内容
|
||||
*/
|
||||
private String waitForChatCompletion(String chatId, String conversationId) {
|
||||
try {
|
||||
// 最多等待30秒,每2秒轮询一次
|
||||
int maxAttempts = 15;
|
||||
int attempt = 0;
|
||||
|
||||
while (attempt < maxAttempts) {
|
||||
log.info("轮询聊天状态,第{}次尝试: chatId={}, conversationId={}", attempt + 1, chatId, conversationId);
|
||||
|
||||
// 构建状态查询URL
|
||||
String statusUrl = cozeBaseUrl + "/v3/chat/retrieve?chat_id=" + chatId + "&conversation_id=" + conversationId;
|
||||
|
||||
// 构建请求头
|
||||
HttpHeaders headers = new HttpHeaders();
|
||||
headers.set("Authorization", "Bearer " + cozeApiToken);
|
||||
headers.set("Content-Type", "application/json");
|
||||
|
||||
HttpEntity<String> request = new HttpEntity<>(headers);
|
||||
|
||||
// 发送状态查询请求
|
||||
ResponseEntity<String> response = restTemplate.exchange(
|
||||
statusUrl,
|
||||
HttpMethod.GET,
|
||||
request,
|
||||
String.class);
|
||||
|
||||
JSONObject statusResponse = JSON.parseObject(response.getBody());
|
||||
log.info("轮询响应: {}", statusResponse);
|
||||
|
||||
if (statusResponse != null && statusResponse.getJSONObject("data") != null) {
|
||||
JSONObject data = statusResponse.getJSONObject("data");
|
||||
String status = data.getString("status");
|
||||
log.info("聊天状态: {}", status);
|
||||
|
||||
if ("completed".equals(status)) {
|
||||
// 聊天完成,获取消息
|
||||
log.info("聊天完成,开始获取消息: chatId={}, conversationId={}", chatId, conversationId);
|
||||
return getChatMessages(chatId, conversationId);
|
||||
} else if ("failed".equals(status)) {
|
||||
log.error("Coze聊天失败: chatId={}, conversationId={}", chatId, conversationId);
|
||||
return "抱歉,AI服务暂时不可用,请稍后再试。";
|
||||
}
|
||||
} else {
|
||||
log.warn("轮询响应为空或无data字段: {}", statusResponse);
|
||||
}
|
||||
|
||||
// 等待2秒后重试
|
||||
Thread.sleep(2000);
|
||||
attempt++;
|
||||
}
|
||||
|
||||
log.warn("Coze聊天超时: chatId={}, conversationId={}", chatId, conversationId);
|
||||
return "抱歉,AI响应超时,请稍后再试。";
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("等待Coze聊天完成失败: chatId={}, conversationId={}, error={}",
|
||||
chatId, conversationId, e.getMessage(), e);
|
||||
return "抱歉,AI服务出现错误,请稍后再试。";
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取聊天消息
|
||||
*/
|
||||
private String getChatMessages(String chatId, String conversationId) {
|
||||
try {
|
||||
log.info("获取聊天消息: chatId={}, conversationId={}", chatId, conversationId);
|
||||
|
||||
// 构建消息查询URL
|
||||
String messagesUrl = cozeBaseUrl + "/v3/chat/message/list?chat_id=" + chatId + "&conversation_id=" + conversationId;
|
||||
|
||||
// 构建请求头
|
||||
HttpHeaders headers = new HttpHeaders();
|
||||
headers.set("Authorization", "Bearer " + cozeApiToken);
|
||||
headers.set("Content-Type", "application/json");
|
||||
|
||||
HttpEntity<String> request = new HttpEntity<>(headers);
|
||||
|
||||
// 发送消息查询请求
|
||||
ResponseEntity<String> response = restTemplate.exchange(
|
||||
messagesUrl,
|
||||
HttpMethod.GET,
|
||||
request,
|
||||
String.class);
|
||||
|
||||
JSONObject messagesResponse = JSON.parseObject(response.getBody());
|
||||
log.info("消息响应: {}", messagesResponse);
|
||||
|
||||
if (messagesResponse != null && messagesResponse.getJSONArray("data") != null) {
|
||||
java.util.List<JSONObject> messages = messagesResponse.getJSONArray("data").toJavaList(JSONObject.class);
|
||||
log.info("收到{}条消息", messages.size());
|
||||
|
||||
// 查找AI的回复消息(role=assistant, type=answer)
|
||||
for (JSONObject message : messages) {
|
||||
String role = message.getString("role");
|
||||
String type = message.getString("type");
|
||||
log.info("消息详情: role={}, type={}, content={}", role, type, message.getString("content"));
|
||||
|
||||
if (ASSISTANT_ROLE.equals(role) && ANSWER_TYPE.equals(type)) {
|
||||
String content = message.getString("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回复失败。";
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* 发送总结消息到Coze AI
|
||||
*/
|
||||
@@ -433,8 +562,9 @@ public class AiChatServiceImpl implements AIChatService {
|
||||
HttpEntity<Map<String, Object>> request = new HttpEntity<>(requestBody, headers);
|
||||
|
||||
// 构建完整的API URL
|
||||
String cozeApiUrl = cozeBaseUrl + "/api/message";
|
||||
|
||||
String cozeApiUrl = cozeBaseUrl + chatPath;
|
||||
log.info("发送Coze总结请求到: {}, 请求体: {}", cozeApiUrl, requestBody);
|
||||
|
||||
// 发送请求
|
||||
ResponseEntity<String> response = restTemplate.exchange(
|
||||
cozeApiUrl,
|
||||
@@ -442,13 +572,22 @@ public class AiChatServiceImpl implements AIChatService {
|
||||
request,
|
||||
String.class);
|
||||
|
||||
// 解析响应
|
||||
log.info("收到Coze总结初始响应: {}", response.getBody());
|
||||
|
||||
// 解析响应获取chat_id和conversation_id
|
||||
JSONObject responseJson = JSON.parseObject(response.getBody());
|
||||
String aiReply = extractContentFromCozeResponse(responseJson);
|
||||
String chatId = extractChatIdFromResponse(responseJson);
|
||||
String cozeConversationId = extractConversationIdFromResponse(responseJson);
|
||||
|
||||
log.info("Coze AI总结响应成功: reply={}", aiReply);
|
||||
|
||||
return aiReply;
|
||||
if (chatId != null && cozeConversationId != null) {
|
||||
// 轮询聊天状态直到完成并获取回复内容
|
||||
String aiReply = waitForChatCompletion(chatId, cozeConversationId);
|
||||
log.info("Coze AI总结响应成功: reply={}", aiReply);
|
||||
return aiReply;
|
||||
} else {
|
||||
log.error("无法从Coze总结响应中获取chat_id或conversation_id");
|
||||
return "抱歉,AI总结服务响应异常,请稍后再试。";
|
||||
}
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("发送总结消息到Coze AI失败", e);
|
||||
@@ -461,7 +600,7 @@ public class AiChatServiceImpl implements AIChatService {
|
||||
*/
|
||||
private Map<String, Object> buildSummaryRequest(String conversationId, String userMessage, String userId) {
|
||||
Map<String, Object> cozeRequest = new HashMap<>();
|
||||
cozeRequest.put("bot_id", summaryBotId);
|
||||
cozeRequest.put("bot_id", summaryBotId != null && !summaryBotId.trim().isEmpty() ? summaryBotId : chatBotId);
|
||||
|
||||
// 如果有总结workflow_id,则添加
|
||||
if (summaryWorkflowId != null && !summaryWorkflowId.trim().isEmpty()) {
|
||||
@@ -470,27 +609,24 @@ public class AiChatServiceImpl implements AIChatService {
|
||||
|
||||
cozeRequest.put("user_id", userId != null ? userId : DEFAULT_USER_ID);
|
||||
cozeRequest.put("stream", false);
|
||||
cozeRequest.put("auto_save_history", true);
|
||||
|
||||
// 构建消息内容
|
||||
String message = userMessage;
|
||||
// 如果有会话ID,则添加
|
||||
if (conversationId != null && !conversationId.trim().isEmpty()) {
|
||||
// 可以在这里添加上下文信息
|
||||
message = "会话ID: " + conversationId + "\n\n总结内容: " + message;
|
||||
cozeRequest.put("conversation_id", conversationId);
|
||||
}
|
||||
|
||||
// 添加聊天历史(简化版本)
|
||||
// 构建消息列表 - 按照 Coze API 标准格式
|
||||
java.util.List<Map<String, Object>> messages = new java.util.ArrayList<>();
|
||||
|
||||
// 添加当前消息
|
||||
// 添加当前用户消息
|
||||
Map<String, Object> currentMsg = new HashMap<>();
|
||||
currentMsg.put("role", "user");
|
||||
currentMsg.put("content", message);
|
||||
currentMsg.put("content_type", "text");
|
||||
currentMsg.put("type", "question");
|
||||
currentMsg.put(ROLE_KEY, USER_ROLE);
|
||||
currentMsg.put(CONTENT_KEY, userMessage);
|
||||
currentMsg.put(CONTENT_TYPE_KEY, TEXT_TYPE);
|
||||
messages.add(currentMsg);
|
||||
|
||||
cozeRequest.put("additional_messages", messages);
|
||||
cozeRequest.put("parameters", new HashMap<>());
|
||||
|
||||
return cozeRequest;
|
||||
}
|
||||
|
||||
@@ -15,6 +15,7 @@ import com.emotion.service.UserService;
|
||||
import org.springframework.beans.BeanUtils;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.data.redis.core.RedisTemplate;
|
||||
import org.springframework.security.crypto.password.PasswordEncoder;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
@@ -45,6 +46,9 @@ public class AuthServiceImpl implements AuthService {
|
||||
@Autowired
|
||||
private RedisTemplate<String, Object> redisTemplate;
|
||||
|
||||
@Autowired
|
||||
private PasswordEncoder passwordEncoder;
|
||||
|
||||
private static final String CAPTCHA_PREFIX = "captcha:";
|
||||
private static final String TOKEN_PREFIX = "token:";
|
||||
private static final String REFRESH_TOKEN_PREFIX = "refresh_token:";
|
||||
@@ -111,11 +115,11 @@ public class AuthServiceImpl implements AuthService {
|
||||
throw new BusinessException("邮箱已被使用");
|
||||
}
|
||||
|
||||
// 创建用户
|
||||
// 创建用户(密码在UserService中加密,这里不需要预先加密)
|
||||
User user = userService.createUser(
|
||||
request.getAccount(),
|
||||
StringUtils.hasText(request.getUsername()) ? request.getUsername() : request.getAccount(),
|
||||
encryptPassword(request.getPassword()),
|
||||
request.getPassword(),
|
||||
request.getEmail(),
|
||||
request.getPhone()
|
||||
);
|
||||
@@ -374,18 +378,16 @@ public class AuthServiceImpl implements AuthService {
|
||||
* 验证密码
|
||||
*/
|
||||
private boolean verifyPassword(String rawPassword, String encodedPassword) {
|
||||
// 这里应该使用BCrypt等加密算法进行密码验证
|
||||
// 简化实现,实际项目中应该使用加密后的密码
|
||||
return rawPassword.equals(encodedPassword);
|
||||
// 使用BCrypt进行密码验证
|
||||
return passwordEncoder.matches(rawPassword, encodedPassword);
|
||||
}
|
||||
|
||||
/**
|
||||
* 加密密码
|
||||
*/
|
||||
private String encryptPassword(String rawPassword) {
|
||||
// 这里应该使用BCrypt等加密算法进行密码加密
|
||||
// 简化实现,实际项目中应该使用加密算法
|
||||
return rawPassword;
|
||||
// 使用BCrypt进行密码加密
|
||||
return passwordEncoder.encode(rawPassword);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -148,17 +148,17 @@ public class MessageServiceImpl extends ServiceImpl<MessageMapper, Message> impl
|
||||
}
|
||||
|
||||
@Override
|
||||
public Message createMessage(String conversationId, String userId, String content,
|
||||
String contentType, String senderType, String senderId) {
|
||||
Message message = new Message();
|
||||
message.setConversationId(conversationId);
|
||||
message.setContent(content);
|
||||
message.setType(contentType);
|
||||
message.setSender(senderType);
|
||||
message.setCreateBy(userId);
|
||||
message.setTimestamp(LocalDateTime.now());
|
||||
message.setStatus("sent");
|
||||
message.setIsRead(0);
|
||||
public Message createMessage(Message message) {
|
||||
// 设置默认值
|
||||
if (message.getTimestamp() == null) {
|
||||
message.setTimestamp(LocalDateTime.now());
|
||||
}
|
||||
if (message.getStatus() == null) {
|
||||
message.setStatus("sent");
|
||||
}
|
||||
if (message.getIsRead() == null) {
|
||||
message.setIsRead(0);
|
||||
}
|
||||
|
||||
this.save(message);
|
||||
return message;
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
# Coze API 配置示例
|
||||
# 请根据您的实际情况修改以下配置
|
||||
|
||||
emotion:
|
||||
coze:
|
||||
api:
|
||||
# Coze API 访问令牌 - 从 https://www.coze.cn/docs/developer_guides/pat 获取
|
||||
token: "your-coze-api-token-here"
|
||||
|
||||
# Coze API 基础URL
|
||||
base-url: "https://api.coze.cn"
|
||||
|
||||
# 聊天相关配置
|
||||
chat:
|
||||
talk:
|
||||
# 聊天机器人ID - 从您的 Coze 工作空间获取
|
||||
bot-id: "your-chat-bot-id-here"
|
||||
# 工作流ID(可选)- 如果使用工作流模式
|
||||
workflow-id: "your-chat-workflow-id-here"
|
||||
|
||||
summary:
|
||||
# 总结机器人ID(可选)- 如果有专门的总结机器人
|
||||
bot-id: "your-summary-bot-id-here"
|
||||
# 总结工作流ID(可选)
|
||||
workflow-id: "your-summary-workflow-id-here"
|
||||
|
||||
# 请求超时配置(毫秒)
|
||||
timeout: 30000
|
||||
|
||||
# 重试配置
|
||||
retry-count: 3
|
||||
retry-delay: 1000
|
||||
|
||||
# 配置说明:
|
||||
# 1. token: 个人访问令牌,需要在 Coze 平台创建
|
||||
# 2. bot-id: 机器人ID,在发布机器人时选择"发布到API"获得
|
||||
# 3. workflow-id: 工作流ID,如果使用工作流模式则需要配置
|
||||
# 4. base-url: 通常为 https://api.coze.cn,国际版可能不同
|
||||
# 5. 确保机器人已发布并启用API访问
|
||||
|
||||
# 重要提醒:
|
||||
# - 请勿将真实的 token 和 ID 提交到版本控制系统
|
||||
# - 建议使用环境变量或配置中心管理敏感信息
|
||||
# - 测试时可以先使用简单的聊天机器人验证配置
|
||||
@@ -75,6 +75,7 @@ emotion:
|
||||
base-url: https://api.coze.cn
|
||||
# 对话聊天
|
||||
chat:
|
||||
path: /v3/chat
|
||||
talk:
|
||||
bot-id: 7523042446285439016
|
||||
workflow-id: 7523047462895796287
|
||||
|
||||
@@ -1,190 +0,0 @@
|
||||
package com.emotion.controller;
|
||||
|
||||
import com.emotion.dto.request.LoginRequest;
|
||||
import com.emotion.dto.request.RegisterRequest;
|
||||
import com.emotion.dto.response.AuthResponse;
|
||||
import com.emotion.dto.response.CaptchaResponse;
|
||||
import com.emotion.exception.AuthException;
|
||||
import com.emotion.exception.CaptchaException;
|
||||
import com.emotion.service.AuthService;
|
||||
import com.emotion.service.TokenService;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.mockito.InjectMocks;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.MockitoAnnotations;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.test.web.servlet.MockMvc;
|
||||
import org.springframework.test.web.servlet.setup.MockMvcBuilders;
|
||||
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.Mockito.when;
|
||||
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*;
|
||||
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;
|
||||
|
||||
/**
|
||||
* AuthController测试类
|
||||
*
|
||||
* @author emotion-museum
|
||||
* @date 2025-07-23
|
||||
*/
|
||||
public class AuthControllerTest {
|
||||
|
||||
@Mock
|
||||
private AuthService authService;
|
||||
|
||||
@InjectMocks
|
||||
private AuthController authController;
|
||||
|
||||
private MockMvc mockMvc;
|
||||
private ObjectMapper objectMapper;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
MockitoAnnotations.openMocks(this);
|
||||
mockMvc = MockMvcBuilders.standaloneSetup(authController).build();
|
||||
objectMapper = new ObjectMapper();
|
||||
}
|
||||
|
||||
@Test
|
||||
void testLogin() throws Exception {
|
||||
// 准备测试数据
|
||||
LoginRequest request = new LoginRequest();
|
||||
request.setAccount("testuser");
|
||||
request.setPassword("password123");
|
||||
request.setCaptcha("1234");
|
||||
request.setCaptchaKey("test-key");
|
||||
|
||||
AuthResponse response = new AuthResponse();
|
||||
response.setAccessToken("test-access-token");
|
||||
response.setRefreshToken("test-refresh-token");
|
||||
response.setExpiresIn(86400L);
|
||||
|
||||
// Mock服务方法
|
||||
when(authService.login(any(LoginRequest.class))).thenReturn(response);
|
||||
|
||||
// 执行测试
|
||||
mockMvc.perform(post("/auth/login")
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content(objectMapper.writeValueAsString(request)))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.code").value(200))
|
||||
.andExpect(jsonPath("$.message").value("登录成功"))
|
||||
.andExpect(jsonPath("$.data.accessToken").value("test-access-token"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testRegister() throws Exception {
|
||||
// 准备测试数据
|
||||
RegisterRequest request = new RegisterRequest();
|
||||
request.setAccount("newuser");
|
||||
request.setPassword("password123");
|
||||
request.setUsername("New User");
|
||||
request.setEmail("newuser@example.com");
|
||||
request.setCaptcha("1234");
|
||||
request.setCaptchaKey("test-key");
|
||||
|
||||
AuthResponse response = new AuthResponse();
|
||||
response.setAccessToken("test-access-token");
|
||||
response.setRefreshToken("test-refresh-token");
|
||||
response.setExpiresIn(86400L);
|
||||
|
||||
// Mock服务方法
|
||||
when(authService.register(any(RegisterRequest.class))).thenReturn(response);
|
||||
|
||||
// 执行测试
|
||||
mockMvc.perform(post("/auth/register")
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content(objectMapper.writeValueAsString(request)))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.code").value(200))
|
||||
.andExpect(jsonPath("$.message").value("注册成功"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testGenerateCaptcha() throws Exception {
|
||||
// 准备测试数据
|
||||
CaptchaResponse response = new CaptchaResponse();
|
||||
response.setCaptchaKey("test-captcha-key");
|
||||
response.setCaptchaImage("data:image/png;base64,test-image");
|
||||
response.setExpiresIn(300L);
|
||||
|
||||
// Mock服务方法
|
||||
when(authService.generateCaptcha()).thenReturn(response);
|
||||
|
||||
// 执行测试
|
||||
mockMvc.perform(get("/auth/captcha"))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.code").value(200))
|
||||
.andExpect(jsonPath("$.data.captchaKey").value("test-captcha-key"))
|
||||
.andExpect(jsonPath("$.data.expiresIn").value(300));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testValidateToken() throws Exception {
|
||||
// Mock服务方法
|
||||
when(authService.validateToken("valid-token")).thenReturn(true);
|
||||
|
||||
// 执行测试
|
||||
mockMvc.perform(get("/auth/validate")
|
||||
.header("Authorization", "Bearer valid-token"))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.code").value(200))
|
||||
.andExpect(jsonPath("$.data").value(true));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testLogout() throws Exception {
|
||||
// Mock服务方法
|
||||
when(authService.logoutByToken("valid-token")).thenReturn(true);
|
||||
|
||||
// 执行测试
|
||||
mockMvc.perform(post("/auth/logout")
|
||||
.header("Authorization", "Bearer valid-token"))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.code").value(200));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testLoginWithInvalidCaptcha() throws Exception {
|
||||
// 准备测试数据
|
||||
LoginRequest request = new LoginRequest();
|
||||
request.setAccount("testuser");
|
||||
request.setPassword("password123");
|
||||
request.setCaptcha("wrong");
|
||||
request.setCaptchaKey("test-key");
|
||||
|
||||
// Mock服务方法抛出异常
|
||||
when(authService.login(any(LoginRequest.class))).thenThrow(new CaptchaException("验证码错误"));
|
||||
|
||||
// 执行测试
|
||||
mockMvc.perform(post("/auth/login")
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content(objectMapper.writeValueAsString(request)))
|
||||
.andExpect(status().isBadRequest())
|
||||
.andExpect(jsonPath("$.code").value(400))
|
||||
.andExpect(jsonPath("$.message").value("验证码错误"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testLoginWithInvalidAccount() throws Exception {
|
||||
// 准备测试数据
|
||||
LoginRequest request = new LoginRequest();
|
||||
request.setAccount("nonexistent");
|
||||
request.setPassword("password123");
|
||||
request.setCaptcha("1234");
|
||||
request.setCaptchaKey("test-key");
|
||||
|
||||
// Mock服务方法抛出异常
|
||||
when(authService.login(any(LoginRequest.class))).thenThrow(new AuthException("账号不存在"));
|
||||
|
||||
// 执行测试
|
||||
mockMvc.perform(post("/auth/login")
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content(objectMapper.writeValueAsString(request)))
|
||||
.andExpect(status().isUnauthorized())
|
||||
.andExpect(jsonPath("$.code").value(401))
|
||||
.andExpect(jsonPath("$.message").value("账号不存在"));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,98 @@
|
||||
package com.emotion.service;
|
||||
|
||||
import com.emotion.service.impl.AiChatServiceImpl;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.boot.test.context.SpringBootTest;
|
||||
import org.springframework.test.context.ActiveProfiles;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
/**
|
||||
* Coze API测试类
|
||||
*
|
||||
* @author emotion-museum
|
||||
* @date 2025-07-24
|
||||
*/
|
||||
@SpringBootTest
|
||||
@ActiveProfiles("test")
|
||||
public class CozeApiTest {
|
||||
|
||||
@Autowired
|
||||
private AIChatService aiChatService;
|
||||
|
||||
@Test
|
||||
public void testServiceAvailability() {
|
||||
// 测试服务可用性检查
|
||||
boolean isAvailable = aiChatService.isServiceAvailable();
|
||||
String status = aiChatService.getServiceStatus();
|
||||
|
||||
// 验证结果
|
||||
assertNotNull(status);
|
||||
assertTrue(status.equals("available") || status.equals("unavailable"));
|
||||
|
||||
// 如果配置正确,服务应该可用
|
||||
if (isAvailable) {
|
||||
assertEquals("available", status);
|
||||
} else {
|
||||
assertEquals("unavailable", status);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testHealthCheck() {
|
||||
// 测试健康检查
|
||||
boolean healthStatus = aiChatService.healthCheck();
|
||||
|
||||
// 验证结果 - 健康检查应该返回布尔值
|
||||
assertNotNull(healthStatus);
|
||||
}
|
||||
|
||||
// 注意:以下测试需要真实的Coze API配置才能通过
|
||||
// 在测试环境中可能会失败,因为没有真实的API token
|
||||
|
||||
/*
|
||||
@Test
|
||||
public void testSendMessage() {
|
||||
// 测试发送消息
|
||||
String conversationId = "test-conversation-001";
|
||||
String message = "你好,这是一条测试消息";
|
||||
String userId = "test-user-001";
|
||||
|
||||
String response = aiChatService.sendMessage(conversationId, message, userId);
|
||||
|
||||
// 验证响应
|
||||
assertNotNull(response);
|
||||
assertFalse(response.isEmpty());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSendChatMessage() {
|
||||
// 测试聊天消息
|
||||
String conversationId = "test-conversation-002";
|
||||
String message = "请介绍一下你自己";
|
||||
String userId = "test-user-002";
|
||||
|
||||
String response = aiChatService.sendChatMessage(conversationId, message, userId);
|
||||
|
||||
// 验证响应
|
||||
assertNotNull(response);
|
||||
assertFalse(response.isEmpty());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testGuestChat() {
|
||||
// 测试访客聊天
|
||||
String message = "你好,我是访客用户";
|
||||
String clientIp = "192.168.1.100";
|
||||
|
||||
Map<String, Object> response = aiChatService.guestChat(message, clientIp);
|
||||
|
||||
// 验证响应
|
||||
assertNotNull(response);
|
||||
assertTrue(response.containsKey("message"));
|
||||
assertTrue(response.containsKey("error"));
|
||||
assertTrue(response.containsKey("timestamp"));
|
||||
}
|
||||
*/
|
||||
}
|
||||
@@ -0,0 +1,105 @@
|
||||
package com.emotion.service;
|
||||
|
||||
import com.emotion.entity.Message;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.boot.test.context.SpringBootTest;
|
||||
import org.springframework.test.context.ActiveProfiles;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
/**
|
||||
* 消息服务测试类
|
||||
*
|
||||
* @author emotion-museum
|
||||
* @date 2025-07-24
|
||||
*/
|
||||
@SpringBootTest
|
||||
@ActiveProfiles("test")
|
||||
@Transactional
|
||||
public class MessageServiceTest {
|
||||
|
||||
@Autowired
|
||||
private MessageService messageService;
|
||||
|
||||
@Test
|
||||
public void testCreateMessage() {
|
||||
// 创建消息对象
|
||||
Message message = new Message();
|
||||
message.setConversationId("test-conversation-001");
|
||||
message.setContent("这是一条测试消息");
|
||||
message.setType("text");
|
||||
message.setSender("user");
|
||||
message.setCreateBy("test-user-001");
|
||||
|
||||
// 调用优化后的createMessage方法
|
||||
Message savedMessage = messageService.createMessage(message);
|
||||
|
||||
// 验证结果
|
||||
assertNotNull(savedMessage);
|
||||
assertNotNull(savedMessage.getId());
|
||||
assertEquals("test-conversation-001", savedMessage.getConversationId());
|
||||
assertEquals("这是一条测试消息", savedMessage.getContent());
|
||||
assertEquals("text", savedMessage.getType());
|
||||
assertEquals("user", savedMessage.getSender());
|
||||
assertEquals("test-user-001", savedMessage.getCreateBy());
|
||||
|
||||
// 验证默认值设置
|
||||
assertNotNull(savedMessage.getTimestamp());
|
||||
assertEquals("sent", savedMessage.getStatus());
|
||||
assertEquals(0, savedMessage.getIsRead());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testCreateMessageWithCustomValues() {
|
||||
// 创建消息对象,设置自定义时间戳和状态
|
||||
Message message = new Message();
|
||||
message.setConversationId("test-conversation-002");
|
||||
message.setContent("自定义状态消息");
|
||||
message.setType("text");
|
||||
message.setSender("ai");
|
||||
message.setCreateBy("ai");
|
||||
message.setTimestamp(LocalDateTime.of(2025, 7, 24, 10, 30, 0));
|
||||
message.setStatus("processing");
|
||||
message.setIsRead(1);
|
||||
|
||||
// 调用优化后的createMessage方法
|
||||
Message savedMessage = messageService.createMessage(message);
|
||||
|
||||
// 验证结果 - 自定义值应该被保留
|
||||
assertNotNull(savedMessage);
|
||||
assertEquals("test-conversation-002", savedMessage.getConversationId());
|
||||
assertEquals("自定义状态消息", savedMessage.getContent());
|
||||
assertEquals("ai", savedMessage.getSender());
|
||||
assertEquals(LocalDateTime.of(2025, 7, 24, 10, 30, 0), savedMessage.getTimestamp());
|
||||
assertEquals("processing", savedMessage.getStatus());
|
||||
assertEquals(1, savedMessage.getIsRead());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testCreateMessageWithPartialDefaults() {
|
||||
// 创建消息对象,只设置部分字段
|
||||
Message message = new Message();
|
||||
message.setConversationId("test-conversation-003");
|
||||
message.setContent("部分默认值消息");
|
||||
message.setType("text");
|
||||
message.setSender("user");
|
||||
message.setCreateBy("test-user-003");
|
||||
message.setStatus("delivered"); // 设置自定义状态
|
||||
// 不设置timestamp和isRead,应该使用默认值
|
||||
|
||||
// 调用优化后的createMessage方法
|
||||
Message savedMessage = messageService.createMessage(message);
|
||||
|
||||
// 验证结果
|
||||
assertNotNull(savedMessage);
|
||||
assertEquals("test-conversation-003", savedMessage.getConversationId());
|
||||
assertEquals("部分默认值消息", savedMessage.getContent());
|
||||
assertEquals("delivered", savedMessage.getStatus()); // 自定义状态
|
||||
assertNotNull(savedMessage.getTimestamp()); // 默认时间戳
|
||||
assertEquals(0, savedMessage.getIsRead()); // 默认未读状态
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,152 @@
|
||||
package com.emotion.service;
|
||||
|
||||
import com.emotion.entity.User;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.boot.test.context.SpringBootTest;
|
||||
import org.springframework.security.crypto.password.PasswordEncoder;
|
||||
import org.springframework.test.context.ActiveProfiles;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
/**
|
||||
* 密码加密测试类
|
||||
*
|
||||
* @author emotion-museum
|
||||
* @date 2025-07-24
|
||||
*/
|
||||
@SpringBootTest
|
||||
@ActiveProfiles("test")
|
||||
@Transactional
|
||||
public class PasswordEncryptionTest {
|
||||
|
||||
@Autowired
|
||||
private UserService userService;
|
||||
|
||||
@Autowired
|
||||
private AuthService authService;
|
||||
|
||||
@Autowired
|
||||
private PasswordEncoder passwordEncoder;
|
||||
|
||||
@Test
|
||||
public void testPasswordEncryption() {
|
||||
String rawPassword = "testPassword123";
|
||||
|
||||
// 测试密码编码器
|
||||
String encodedPassword = passwordEncoder.encode(rawPassword);
|
||||
|
||||
// 验证密码不是明文
|
||||
assertNotEquals(rawPassword, encodedPassword);
|
||||
|
||||
// 验证密码匹配
|
||||
assertTrue(passwordEncoder.matches(rawPassword, encodedPassword));
|
||||
|
||||
// 验证错误密码不匹配
|
||||
assertFalse(passwordEncoder.matches("wrongPassword", encodedPassword));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testUserCreationWithPasswordEncryption() {
|
||||
String account = "testuser001";
|
||||
String username = "Test User";
|
||||
String rawPassword = "testPassword123";
|
||||
String email = "test@example.com";
|
||||
String phone = "13800138000";
|
||||
|
||||
// 创建用户
|
||||
User user = userService.createUser(account, username, rawPassword, email, phone);
|
||||
|
||||
// 验证用户创建成功
|
||||
assertNotNull(user);
|
||||
assertNotNull(user.getId());
|
||||
assertEquals(account, user.getAccount());
|
||||
assertEquals(username, user.getUsername());
|
||||
assertEquals(email, user.getEmail());
|
||||
assertEquals(phone, user.getPhone());
|
||||
|
||||
// 验证密码已加密
|
||||
assertNotEquals(rawPassword, user.getPassword());
|
||||
|
||||
// 验证密码验证功能
|
||||
assertTrue(userService.validatePassword(user.getId(), rawPassword));
|
||||
assertFalse(userService.validatePassword(user.getId(), "wrongPassword"));
|
||||
|
||||
// 验证可以通过账号查询到用户
|
||||
User foundUser = userService.getByAccount(account);
|
||||
assertNotNull(foundUser);
|
||||
assertEquals(user.getId(), foundUser.getId());
|
||||
|
||||
// 验证密码匹配
|
||||
assertTrue(passwordEncoder.matches(rawPassword, foundUser.getPassword()));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testPasswordConsistencyBetweenServices() {
|
||||
String rawPassword = "consistencyTest123";
|
||||
|
||||
// 使用UserService加密密码
|
||||
String userServiceEncoded = passwordEncoder.encode(rawPassword);
|
||||
|
||||
// 验证AuthService能正确验证UserService加密的密码
|
||||
assertTrue(passwordEncoder.matches(rawPassword, userServiceEncoded));
|
||||
|
||||
// 测试多次加密产生不同的哈希值(BCrypt的特性)
|
||||
String encoded1 = passwordEncoder.encode(rawPassword);
|
||||
String encoded2 = passwordEncoder.encode(rawPassword);
|
||||
|
||||
// 哈希值应该不同(因为BCrypt使用随机盐)
|
||||
assertNotEquals(encoded1, encoded2);
|
||||
|
||||
// 但都应该能验证原始密码
|
||||
assertTrue(passwordEncoder.matches(rawPassword, encoded1));
|
||||
assertTrue(passwordEncoder.matches(rawPassword, encoded2));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testBCryptPasswordFormat() {
|
||||
String rawPassword = "formatTest123";
|
||||
String encodedPassword = passwordEncoder.encode(rawPassword);
|
||||
|
||||
// BCrypt密码应该以$2a$、$2b$或$2y$开头
|
||||
assertTrue(encodedPassword.startsWith("$2a$") ||
|
||||
encodedPassword.startsWith("$2b$") ||
|
||||
encodedPassword.startsWith("$2y$"),
|
||||
"密码应该使用BCrypt格式加密");
|
||||
|
||||
// BCrypt密码长度通常是60个字符
|
||||
assertEquals(60, encodedPassword.length(), "BCrypt密码长度应该是60个字符");
|
||||
}
|
||||
|
||||
/*
|
||||
// 注意:以下测试需要完整的认证流程,可能需要验证码等
|
||||
@Test
|
||||
public void testFullAuthenticationFlow() {
|
||||
// 这个测试需要模拟完整的注册和登录流程
|
||||
// 由于涉及验证码等复杂逻辑,在实际测试中可能需要mock相关服务
|
||||
|
||||
String account = "authtest001";
|
||||
String password = "authTestPassword123";
|
||||
String email = "authtest@example.com";
|
||||
|
||||
// 1. 注册用户
|
||||
RegisterRequest registerRequest = new RegisterRequest();
|
||||
registerRequest.setAccount(account);
|
||||
registerRequest.setPassword(password);
|
||||
registerRequest.setEmail(email);
|
||||
// 需要设置验证码等其他必要字段
|
||||
|
||||
// 2. 登录验证
|
||||
LoginRequest loginRequest = new LoginRequest();
|
||||
loginRequest.setAccount(account);
|
||||
loginRequest.setPassword(password);
|
||||
// 需要设置验证码等其他必要字段
|
||||
|
||||
// 验证登录成功
|
||||
// AuthResponse authResponse = authService.login(loginRequest);
|
||||
// assertNotNull(authResponse);
|
||||
// assertNotNull(authResponse.getToken());
|
||||
}
|
||||
*/
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
spring:
|
||||
datasource:
|
||||
url: jdbc:h2:mem:testdb
|
||||
driver-class-name: org.h2.Driver
|
||||
username: sa
|
||||
password:
|
||||
jpa:
|
||||
hibernate:
|
||||
ddl-auto: create-drop
|
||||
show-sql: true
|
||||
h2:
|
||||
console:
|
||||
enabled: true
|
||||
|
||||
logging:
|
||||
level:
|
||||
com.emotion: DEBUG
|
||||
org.springframework.web: DEBUG
|
||||
|
||||
# 测试环境的Coze API配置
|
||||
emotion:
|
||||
coze:
|
||||
api:
|
||||
token: test-token
|
||||
base-url: https://api.coze.cn
|
||||
chat:
|
||||
path: /v3/chat
|
||||
talk:
|
||||
bot-id: test-bot-id
|
||||
workflow-id: test-workflow-id
|
||||
summary:
|
||||
bot-id: test-summary-bot-id
|
||||
workflow-id: test-summary-workflow-id
|
||||
timeout: 30000
|
||||
retry-count: 3
|
||||
retry-delay: 1000
|
||||
Reference in New Issue
Block a user