对话逻辑修复

This commit is contained in:
2025-07-25 17:48:02 +08:00
parent a4c6140ed5
commit f576de68da
31 changed files with 2129 additions and 588 deletions
+7 -5
View File
@@ -8,8 +8,10 @@ type: "always_apply"
4.前端所有接口的访问尽可能走网关调用;
5.为不同(local,dev,prod)环境创建单独的配置文件,可以在部署时通过参数选择要使用的配置文件;
6.Controller层不允许写业务代码,业务代码写在service层中;
7.除了特殊情况下的异常,一般情况下不需要try-catch,由全局异常处理机制处理
8.所有Controller层接口定义要完整,入参使用request封装请求,出参是response封装出参,使用项目已有的Result做接口返回
9.所有对项目的变更要遵循当前的项目现有规范
10.禁止在新增的Controller层的路由前面添加/api
11.与当前用户相关的接口,禁止直接传递用户id,需要后端根据当前登录用户,接口调用的token获取当前登录的用户信息
7.service层必须是接口Service和实现类ServiceImpl的方式来实现;
8.除了特殊情况下的异常,一般情况下不需要try-catch,由全局异常处理机制处理
9.所有Controller层接口定义要完整,入参使用request封装请求,出参是response封装出参,使用项目已有的Result做接口返回
10.所有对项目的变更要遵循当前的项目现有规范
11.禁止在新增的Controller层的路由前面添加/api
12.与当前用户相关的接口,禁止直接传递用户id,需要后端根据当前登录用户,接口调用的token获取当前登录的用户信息
13.在优化代码时必须要确保不能破坏已经实现的业务逻辑
+1 -1
View File
File diff suppressed because one or more lines are too long
+1
View File
@@ -7,6 +7,7 @@
<sourceOutputDir name="target/generated-sources/annotations" />
<sourceTestOutputDir name="target/generated-test-sources/test-annotations" />
<outputRelativeToContentRoot value="true" />
<module name="emotion-single" />
</profile>
<profile name="Annotation profile for backend" enabled="true">
<sourceOutputDir name="target/generated-sources/annotations" />
+2
View File
@@ -1,6 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="Encoding">
<file url="file://$PROJECT_DIR$/backend-single/src/main/java" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/backend-single/src/main/resources" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/backend/emotion-ai/src/main/java" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/backend/emotion-ai/src/main/resources" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/backend/emotion-auth/src/main/java" charset="UTF-8" />
+1
View File
@@ -6,6 +6,7 @@
<list>
<option value="$PROJECT_DIR$/server/pom.xml" />
<option value="$PROJECT_DIR$/backend/pom.xml" />
<option value="$PROJECT_DIR$/backend-single/pom.xml" />
</list>
</option>
</component>
@@ -3,7 +3,6 @@ package com.emotion.controller;
import com.emotion.common.Result;
import com.emotion.dto.request.AiChatRequest;
import com.emotion.dto.request.AiSummaryRequest;
import com.emotion.dto.request.ChatStatsRequest;
import com.emotion.dto.request.GuestChatRequest;
import com.emotion.dto.request.ConversationCreateRequest;
import com.emotion.dto.response.AiChatResponse;
@@ -14,7 +13,7 @@ import com.emotion.dto.response.GuestChatResponse;
import com.emotion.dto.response.GuestUserInfoResponse;
import com.emotion.dto.response.ConversationResponse;
import com.emotion.entity.Conversation;
import com.emotion.service.AIChatService;
import com.emotion.service.AiChatService;
import com.emotion.service.MessageService;
import com.emotion.service.ConversationService;
import lombok.extern.slf4j.Slf4j;
@@ -25,7 +24,6 @@ import org.springframework.web.bind.annotation.*;
import javax.servlet.http.HttpServletRequest;
import javax.validation.Valid;
import java.time.format.DateTimeFormatter;
import java.util.HashMap;
import java.util.Map;
/**
@@ -40,7 +38,7 @@ import java.util.Map;
public class AiChatController {
@Autowired
private AIChatService aiChatService;
private AiChatService aiChatService;
@Autowired
private MessageService messageService;
@@ -1,7 +1,7 @@
package com.emotion.controller;
import com.emotion.common.Result;
import com.emotion.service.AIChatService;
import com.emotion.service.AiChatService;
import com.emotion.util.CurrentUserUtil;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
@@ -24,7 +24,7 @@ import java.util.Map;
public class EmotionSummaryController {
@Autowired
private AIChatService aiChatService;
private AiChatService aiChatService;
@Operation(summary = "生成用户当天的情绪记录总结", description = "基于用户当天的聊天记录生成情绪分析和记录")
@PostMapping("/generate")
@@ -1,22 +1,19 @@
package com.emotion.controller;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.emotion.common.PageResult;
import com.emotion.common.Result;
import com.emotion.dto.request.PageRequest;
import com.emotion.dto.request.MessageCreateRequest;
import com.emotion.dto.request.MessagePageRequest;
import com.emotion.dto.request.MessageSearchRequest;
import com.emotion.dto.request.MessageRecentRequest;
import com.emotion.dto.response.MessageResponse;
import com.emotion.entity.Message;
import com.emotion.service.MessageService;
import com.emotion.util.CurrentUserUtil;
import org.springframework.beans.BeanUtils;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import javax.validation.Valid;
import java.time.format.DateTimeFormatter;
import java.util.List;
import java.util.stream.Collectors;
/**
* 消息控制器
@@ -26,52 +23,31 @@ import java.util.stream.Collectors;
*/
@RestController
@RequestMapping("/message")
@Slf4j
public class MessageController {
@Autowired
private MessageService messageService;
private static final DateTimeFormatter DATE_TIME_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
/**
* 分页查询消息
* 创建消息
*/
@GetMapping("/page")
public Result<PageResult<MessageResponse>> getPage(@Valid PageRequest request) {
IPage<Message> page = messageService.getPage(request);
List<MessageResponse> responses = page.getRecords().stream()
.map(this::convertToResponse)
.collect(Collectors.toList());
@PostMapping
public Result<MessageResponse> create(@Valid @RequestBody MessageCreateRequest request) {
log.info("创建消息: conversationId={}", request.getConversationId());
PageResult<MessageResponse> pageResult = new PageResult<>();
pageResult.setCurrent(page.getCurrent());
pageResult.setSize(page.getSize());
pageResult.setTotal(page.getTotal());
pageResult.setPages(page.getPages());
pageResult.setRecords(responses);
try {
MessageResponse response = messageService.createMessageFromRequest(request);
log.info("创建消息成功: messageId={}", response.getId());
return Result.success(response);
return Result.success(pageResult);
}
/**
* 根据会话ID分页查询消息
*/
@GetMapping("/conversation/{conversationId}/page")
public Result<PageResult<MessageResponse>> getPageByConversationId(@PathVariable String conversationId,
@Valid PageRequest request) {
IPage<Message> page = messageService.getPageByConversationId(request, conversationId);
List<MessageResponse> responses = page.getRecords().stream()
.map(this::convertToResponse)
.collect(Collectors.toList());
PageResult<MessageResponse> pageResult = new PageResult<>();
pageResult.setCurrent(page.getCurrent());
pageResult.setSize(page.getSize());
pageResult.setTotal(page.getTotal());
pageResult.setPages(page.getPages());
pageResult.setRecords(responses);
return Result.success(pageResult);
} catch (IllegalStateException e) {
log.error("用户未认证: {}", e.getMessage());
return Result.error(401, "用户未登录或认证失败");
} catch (Exception e) {
log.error("创建消息失败", e);
return Result.error(500, "创建消息失败,请稍后重试");
}
}
/**
@@ -79,139 +55,90 @@ public class MessageController {
*/
@GetMapping("/{id}")
public Result<MessageResponse> getById(@PathVariable String id) {
Message message = messageService.getById(id);
if (message == null) {
return Result.notFound("消息不存在");
log.info("获取消息详情: id={}", id);
try {
MessageResponse response = messageService.getMessageById(id);
if (response == null) {
return Result.error(404, "消息不存在");
}
return Result.success(response);
} catch (Exception e) {
log.error("获取消息详情失败", e);
return Result.error(500, "获取消息详情失败,请稍后重试");
}
return Result.success(convertToResponse(message));
}
/**
* 创建消息
*/
@PostMapping
public Result<MessageResponse> create(@Valid @RequestBody MessageCreateRequest request) {
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));
}
/**
* 根据会话ID查询消息
*/
@GetMapping("/conversation/{conversationId}")
public Result<List<MessageResponse>> getByConversationId(@PathVariable String conversationId) {
List<Message> messages = messageService.getByConversationId(conversationId);
List<MessageResponse> responses = messages.stream()
.map(this::convertToResponse)
.collect(Collectors.toList());
return Result.success(responses);
}
/**
* 统计会话消息数量
*/
@GetMapping("/conversation/{conversationId}/count")
public Result<Long> countByConversationId(@PathVariable String conversationId) {
Long count = messageService.countByConversationId(conversationId);
return Result.success(count);
}
/**
* 根据用户ID分页查询消息
*/
@GetMapping("/user/page")
public Result<PageResult<MessageResponse>> getPageByUserId(@Valid PageRequest request) {
public Result<PageResult<MessageResponse>> getPageByUserId(
@RequestParam(defaultValue = "1") Long current,
@RequestParam(defaultValue = "20") Long size) {
log.info("获取用户消息分页: current={}, size={}", current, size);
try {
// 从上下文中获取当前用户ID
String userId = CurrentUserUtil.requireCurrentUserId();
IPage<Message> page = messageService.getByUserIdWithPage(userId, Math.toIntExact(request.getCurrent()),
Math.toIntExact(request.getSize()));
List<MessageResponse> responses = page.getRecords().stream()
.map(this::convertToResponse)
.collect(Collectors.toList());
PageResult<MessageResponse> pageResult = new PageResult<>();
pageResult.setCurrent(page.getCurrent());
pageResult.setSize(page.getSize());
pageResult.setTotal(page.getTotal());
pageResult.setPages(page.getPages());
pageResult.setRecords(responses);
// 构建请求对象
MessagePageRequest request = new MessagePageRequest();
request.setCurrent(current);
request.setSize(size);
PageResult<MessageResponse> pageResult = messageService.getUserMessagesWithPage(request);
log.info("获取用户消息分页成功: total={}", pageResult.getTotal());
return Result.success(pageResult);
} catch (IllegalStateException e) {
return Result.error(e.getMessage());
log.error("用户未认证: {}", e.getMessage());
return Result.error(401, "用户未登录或认证失败");
} catch (Exception e) {
log.error("获取用户消息失败", e);
return Result.error(500, "获取消息失败,请稍后重试");
}
}
/**
* 根据用户ID和关键词搜索消息
*/
@GetMapping("/user/search")
public Result<List<MessageResponse>> searchByUserId(
@RequestParam String keyword,
@RequestParam(defaultValue = "50") Integer limit) {
@PostMapping("/user/search")
public Result<List<MessageResponse>> searchByUserId(@Valid @RequestBody MessageSearchRequest request) {
log.info("搜索用户消息: keyword={}, limit={}", request.getKeyword(), request.getLimit());
try {
// 从上下文中获取当前用户ID
String userId = CurrentUserUtil.requireCurrentUserId();
List<Message> messages = messageService.searchByUserIdAndKeyword(userId, keyword, limit);
List<MessageResponse> responses = messages.stream()
.map(this::convertToResponse)
.collect(Collectors.toList());
List<MessageResponse> responses = messageService.searchUserMessages(request);
log.info("搜索用户消息成功: {} 条消息", responses.size());
return Result.success(responses);
} catch (IllegalStateException e) {
return Result.error(e.getMessage());
log.error("用户未认证: {}", e.getMessage());
return Result.error(401, "用户未登录或认证失败");
} catch (Exception e) {
log.error("搜索用户消息失败", e);
return Result.error(500, "搜索失败,请稍后重试");
}
}
/**
* 获取用户最近的聊天记录
*/
@GetMapping("/user/recent")
public Result<List<MessageResponse>> getRecentMessages(
@RequestParam(defaultValue = "10") Integer limit) {
@PostMapping("/user/recent")
public Result<List<MessageResponse>> getRecentMessages(@Valid @RequestBody MessageRecentRequest request) {
log.info("获取用户最近消息: limit={}", request.getLimit());
try {
// 从上下文中获取当前用户ID
String userId = CurrentUserUtil.requireCurrentUserId();
List<Message> messages = messageService.getRecentByUserId(userId, limit);
List<MessageResponse> responses = messages.stream()
.map(this::convertToResponse)
.collect(Collectors.toList());
List<MessageResponse> responses = messageService.getUserRecentMessages(request);
log.info("获取用户最近消息成功: {} 条消息", responses.size());
return Result.success(responses);
} catch (IllegalStateException e) {
return Result.error(e.getMessage());
log.error("用户未认证: {}", e.getMessage());
return Result.error(401, "用户未登录或认证失败");
} catch (Exception e) {
log.error("获取最近消息失败", e);
return Result.error(500, "获取最近消息失败,请稍后重试");
}
}
/**
* 转换为响应对象
*/
private MessageResponse convertToResponse(Message message) {
MessageResponse response = new MessageResponse();
BeanUtils.copyProperties(message, response);
response.setId(message.getId());
if (message.getCreateTime() != null) {
response.setCreateTime(message.getCreateTime().format(DATE_TIME_FORMATTER));
}
if (message.getUpdateTime() != null) {
response.setUpdateTime(message.getUpdateTime().format(DATE_TIME_FORMATTER));
}
return response;
}
}
@@ -1,12 +1,11 @@
package com.emotion.controller;
import com.emotion.service.AIChatService;
import com.emotion.service.AiChatService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.messaging.handler.annotation.MessageMapping;
import org.springframework.messaging.handler.annotation.Payload;
import org.springframework.messaging.handler.annotation.SendTo;
import org.springframework.messaging.simp.SimpMessageHeaderAccessor;
import org.springframework.messaging.simp.SimpMessagingTemplate;
import org.springframework.stereotype.Controller;
@@ -31,7 +30,7 @@ public class WebSocketController {
private SimpMessagingTemplate messagingTemplate;
@Autowired
private AIChatService aiChatService;
private AiChatService aiChatService;
// 已移除旧的WebSocket消息处理方法,使用新的ChatWebSocketController
@@ -0,0 +1,56 @@
package com.emotion.dto.request;
import lombok.Data;
import lombok.EqualsAndHashCode;
import javax.validation.constraints.Min;
import javax.validation.constraints.Max;
/**
* 消息分页查询请求类
*
* @author emotion-museum
* @date 2025-07-25
*/
@Data
@EqualsAndHashCode(callSuper = true)
public class MessagePageRequest extends BaseRequest {
/**
* 当前页码
*/
@Min(value = 1, message = "页码不能小于1")
private Long current = 1L;
/**
* 每页大小
*/
@Min(value = 1, message = "每页大小不能小于1")
@Max(value = 100, message = "每页大小不能超过100")
private Long size = 20L;
/**
* 会话ID(可选,用于查询特定会话的消息)
*/
private String conversationId;
/**
* 消息类型(可选)
*/
private String type;
/**
* 发送者类型(可选)
*/
private String sender;
/**
* 开始时间(可选,格式:yyyy-MM-dd HH:mm:ss
*/
private String startTime;
/**
* 结束时间(可选,格式:yyyy-MM-dd HH:mm:ss
*/
private String endTime;
}
@@ -0,0 +1,40 @@
package com.emotion.dto.request;
import lombok.Data;
import lombok.EqualsAndHashCode;
import javax.validation.constraints.Min;
import javax.validation.constraints.Max;
/**
* 获取最近消息请求类
*
* @author emotion-museum
* @date 2025-07-25
*/
@Data
@EqualsAndHashCode(callSuper = true)
public class MessageRecentRequest extends BaseRequest {
/**
* 限制返回数量
*/
@Min(value = 1, message = "限制数量不能小于1")
@Max(value = 50, message = "限制数量不能超过50")
private Integer limit = 10;
/**
* 会话ID(可选,用于获取特定会话的最近消息)
*/
private String conversationId;
/**
* 消息类型(可选)
*/
private String type;
/**
* 发送者类型(可选)
*/
private String sender;
}
@@ -0,0 +1,57 @@
package com.emotion.dto.request;
import lombok.Data;
import lombok.EqualsAndHashCode;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.Min;
import javax.validation.constraints.Max;
/**
* 消息搜索请求类
*
* @author emotion-museum
* @date 2025-07-25
*/
@Data
@EqualsAndHashCode(callSuper = true)
public class MessageSearchRequest extends BaseRequest {
/**
* 搜索关键词
*/
@NotBlank(message = "搜索关键词不能为空")
private String keyword;
/**
* 限制返回数量
*/
@Min(value = 1, message = "限制数量不能小于1")
@Max(value = 100, message = "限制数量不能超过100")
private Integer limit = 50;
/**
* 会话ID(可选,用于在特定会话中搜索)
*/
private String conversationId;
/**
* 消息类型(可选)
*/
private String type;
/**
* 发送者类型(可选)
*/
private String sender;
/**
* 开始时间(可选,格式:yyyy-MM-dd HH:mm:ss
*/
private String startTime;
/**
* 结束时间(可选,格式:yyyy-MM-dd HH:mm:ss
*/
private String endTime;
}
@@ -52,9 +52,11 @@ public class AuthInterceptor implements HandlerInterceptor {
return false;
}
// 将用户ID存储到请求属性中,供后续使用
// 将用户信息存储到请求属性中,供后续使用
String userId = authService.getUserIdFromToken(token);
String username = authService.getUsernameFromToken(token);
request.setAttribute("userId", userId);
request.setAttribute("username", username);
request.setAttribute("token", token);
return true;
@@ -76,25 +76,31 @@ public class UserContextInterceptor implements HandlerInterceptor {
* @return 用户ID
*/
private String getUserIdFromRequest(HttpServletRequest request) {
// 1. 从请求头获取
// 1. 优先从请求属性获取(AuthInterceptor设置的)
Object userIdAttr = request.getAttribute("userId");
if (userIdAttr != null && StringUtils.hasText(userIdAttr.toString())) {
return userIdAttr.toString();
}
// 2. 从请求头获取
String userId = request.getHeader("X-User-Id");
if (StringUtils.hasText(userId)) {
return userId;
}
// 2. 从请求参数获取
// 3. 从请求参数获取
userId = request.getParameter("userId");
if (StringUtils.hasText(userId)) {
return userId;
}
// 3. 从Session获取
// 4. 从Session获取
Object sessionUserId = request.getSession().getAttribute("userId");
if (sessionUserId != null) {
return sessionUserId.toString();
}
// 4. 生成访客ID
// 5. 生成访客ID
return "guest_" + System.currentTimeMillis();
}
@@ -105,19 +111,25 @@ public class UserContextInterceptor implements HandlerInterceptor {
* @return 用户名
*/
private String getUsernameFromRequest(HttpServletRequest request) {
// 1. 从请求头获取
// 1. 优先从请求属性获取(AuthInterceptor设置的)
Object usernameAttr = request.getAttribute("username");
if (usernameAttr != null && StringUtils.hasText(usernameAttr.toString())) {
return usernameAttr.toString();
}
// 2. 从请求头获取
String username = request.getHeader("X-Username");
if (StringUtils.hasText(username)) {
return username;
}
// 2. 从请求参数获取
// 3. 从请求参数获取
username = request.getParameter("username");
if (StringUtils.hasText(username)) {
return username;
}
// 3. 从Session获取
// 4. 从Session获取
Object sessionUsername = request.getSession().getAttribute("username");
if (sessionUsername != null) {
return sessionUsername.toString();
@@ -8,13 +8,19 @@ import java.util.Map;
* @author emotion-museum
* @date 2025-07-24
*/
public interface AIChatService {
public interface AiChatService {
/**
* 发送聊天消息
*/
String sendChatMessage(String conversationId, String message, String userId);
/**
* 发送聊天消息仅获取AI回复不保存用户消息
* 用于WebSocket场景避免重复保存用户消息
*/
String sendChatMessageForWebSocket(String conversationId, String message, String userId);
/**
* 生成对话总结
*/
@@ -3,6 +3,12 @@ package com.emotion.service;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.service.IService;
import com.emotion.common.BasePageRequest;
import com.emotion.common.PageResult;
import com.emotion.dto.request.MessagePageRequest;
import com.emotion.dto.request.MessageSearchRequest;
import com.emotion.dto.request.MessageRecentRequest;
import com.emotion.dto.request.MessageCreateRequest;
import com.emotion.dto.response.MessageResponse;
import com.emotion.entity.Message;
import java.time.LocalDateTime;
@@ -110,4 +116,29 @@ public interface MessageService extends IService<Message> {
* 标记消息为已读
*/
boolean markAsRead(String messageId);
/**
* 获取用户消息分页(新接口)
*/
PageResult<MessageResponse> getUserMessagesWithPage(MessagePageRequest request);
/**
* 搜索用户消息(新接口)
*/
List<MessageResponse> searchUserMessages(MessageSearchRequest request);
/**
* 获取用户最近消息(新接口)
*/
List<MessageResponse> getUserRecentMessages(MessageRecentRequest request);
/**
* 根据请求创建消息(新接口)
*/
MessageResponse createMessageFromRequest(MessageCreateRequest request);
/**
* 根据ID获取消息响应(新接口)
*/
MessageResponse getMessageById(String id);
}
@@ -2,411 +2,39 @@ 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 com.emotion.entity.Conversation;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.messaging.simp.SimpMessagingTemplate;
import org.springframework.stereotype.Service;
import java.security.Principal;
import java.time.LocalDateTime;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
/**
* WebSocket服务
* WebSocket服务接口
*
* @author emotion-museum
* @date 2025-07-23
* @date 2025-07-25
*/
@Slf4j
@Service
public class WebSocketService {
@Autowired
private SimpMessagingTemplate messagingTemplate;
@Autowired
private AIChatService aiChatService;
@Autowired
private MessageService messageService;
@Autowired
private ConversationService conversationService;
// 在线用户管理
private final ConcurrentHashMap<String, String> onlineUsers = new ConcurrentHashMap<>();
public interface WebSocketService {
/**
* 处理聊天消息
*/
public void handleChatMessage(ChatRequest request, String sessionId, Principal principal) {
try {
log.info("处理聊天消息: request={}, sessionId={}, principal={}", request, sessionId, principal);
// 验证请求参数
if (request.getContent() == null || request.getContent().trim().isEmpty()) {
sendErrorMessage(request.getSenderId(), "消息内容不能为空");
return;
}
// 确定用户身份和类型
String userId = request.getSenderId();
WebSocketMessage.SenderType senderType = WebSocketMessage.SenderType.GUEST;
if (principal != null) {
userId = principal.getName();
// 如果用户ID不是以guest_开头,说明是认证用户
if (!userId.startsWith("guest_")) {
senderType = WebSocketMessage.SenderType.USER;
}
}
// 更新请求中的用户信息
request.setSenderId(userId);
request.setSenderType(senderType == WebSocketMessage.SenderType.USER ? ChatRequest.SenderType.USER
: ChatRequest.SenderType.GUEST);
log.info("确定用户身份: userId={}, senderType={}", userId, senderType);
// 构建用户消息
WebSocketMessage userMessage = WebSocketMessage.builder()
.messageId(UUID.randomUUID().toString())
.conversationId(request.getConversationId())
.type(WebSocketMessage.MessageType.TEXT)
.content(request.getContent())
.senderId(userId)
.senderType(senderType)
.status(WebSocketMessage.MessageStatus.SENT)
.createTime(LocalDateTime.now())
.build();
// 发送用户消息到会话
if (request.getConversationId() != null) {
messagingTemplate.convertAndSend("/topic/conversation/" + request.getConversationId(), userMessage);
}
// 发送给用户私有队列
messagingTemplate.convertAndSendToUser(request.getSenderId(), "/queue/messages", userMessage);
// 发送AI思考状态
sendAiThinkingMessage(request.getSenderId(), request.getConversationId());
// 异步调用AI服务
processAiResponse(request);
} catch (Exception e) {
log.error("处理聊天消息失败", e);
sendErrorMessage(request.getSenderId(), "消息处理失败,请稍后重试");
}
}
void handleChatMessage(ChatRequest request, String sessionId, Principal principal);
/**
* 处理用户连接
*/
public void handleUserConnect(ConnectRequest request, String sessionId, Principal principal) {
try {
String userId = request.getUserId();
boolean isAuthenticated = false;
// 优先从Principal获取认证用户信息
if (principal != null) {
userId = principal.getName();
// 检查是否是认证用户(不是访客)
isAuthenticated = !userId.startsWith("guest_");
}
// 如果还没有userId,生成访客ID
if (userId == null) {
userId = "guest_" + sessionId;
}
log.info("用户连接WebSocket: userId={}, sessionId={}, authenticated={}",
userId, sessionId, isAuthenticated);
// 记录在线用户
onlineUsers.put(sessionId, userId);
// 发送连接成功消息
WebSocketMessage connectMessage = WebSocketMessage.builder()
.messageId(UUID.randomUUID().toString())
.type(WebSocketMessage.MessageType.CONNECTION)
.content("连接成功")
.senderId("system")
.senderType(WebSocketMessage.SenderType.SYSTEM)
.status(WebSocketMessage.MessageStatus.SENT)
.createTime(LocalDateTime.now())
.build();
messagingTemplate.convertAndSendToUser(userId, "/queue/messages", connectMessage);
} catch (Exception e) {
log.error("处理用户连接失败", e);
}
}
void handleUserConnect(ConnectRequest request, String sessionId, Principal principal);
/**
* 处理用户断开连接
*/
public void handleUserDisconnect(String sessionId, Principal principal) {
try {
String userId = onlineUsers.remove(sessionId);
log.info("用户断开WebSocket连接: userId={}, sessionId={}", userId, sessionId);
} catch (Exception e) {
log.error("处理用户断开连接失败", e);
}
}
void handleUserDisconnect(String sessionId, Principal principal);
/**
* 处理心跳消息
*/
public void handleHeartbeat(String sessionId, Principal principal) {
try {
String userId = onlineUsers.get(sessionId);
if (userId == null && principal != null) {
userId = principal.getName();
}
// 发送心跳响应
WebSocketMessage heartbeatMessage = WebSocketMessage.builder()
.messageId(UUID.randomUUID().toString())
.type(WebSocketMessage.MessageType.HEARTBEAT)
.content("pong")
.senderId("system")
.senderType(WebSocketMessage.SenderType.SYSTEM)
.status(WebSocketMessage.MessageStatus.SENT)
.createTime(LocalDateTime.now())
.build();
if (userId != null) {
messagingTemplate.convertAndSendToUser(userId, "/queue/messages", heartbeatMessage);
}
} catch (Exception e) {
log.error("处理心跳消息失败", e);
}
}
/**
* 发送AI思考状态消息
*/
private void sendAiThinkingMessage(String userId, String conversationId) {
WebSocketMessage thinkingMessage = WebSocketMessage.builder()
.messageId(UUID.randomUUID().toString())
.conversationId(conversationId)
.type(WebSocketMessage.MessageType.AI_THINKING)
.content("AI正在思考中...")
.senderId("ai")
.senderType(WebSocketMessage.SenderType.AI)
.status(WebSocketMessage.MessageStatus.SENT)
.createTime(LocalDateTime.now())
.build();
messagingTemplate.convertAndSendToUser(userId, "/queue/messages", thinkingMessage);
if (conversationId != null) {
messagingTemplate.convertAndSend("/topic/conversation/" + conversationId, thinkingMessage);
}
}
/**
* 异步处理AI响应
*/
private void processAiResponse(ChatRequest request) {
// 使用线程池异步处理AI响应
new Thread(() -> {
try {
String userId = request.getSenderId();
String conversationId = request.getConversationId();
// 如果没有会话ID,创建新会话
if (conversationId == null || conversationId.trim().isEmpty()) {
conversationId = createNewConversation(userId, request);
request.setConversationId(conversationId);
}
// 确保会话存在并更新活跃时间
ensureConversationExists(conversationId, userId, request);
// 保存用户消息到数据库
Message userMessage = new Message();
userMessage.setConversationId(conversationId);
userMessage.setUserId(userId);
userMessage
.setUserType(request.getSenderType() == ChatRequest.SenderType.USER ? "registered" : "guest");
userMessage.setContent(request.getContent());
userMessage.setType("text");
userMessage.setSender("user");
userMessage.setCozeRole("user");
userMessage.setCozeContentType("text");
messageService.createMessage(userMessage);
// 调用AI服务
String aiReply = aiChatService.sendChatMessage(
conversationId,
request.getContent(),
userId
);
// 如果AI回复包含换行符,分割成多条消息
String[] replyParts = aiReply.split("\\n\\n|\\n");
for (String part : replyParts) {
if (part.trim().isEmpty())
continue;
// 构建AI回复消息
WebSocketMessage aiMessage = WebSocketMessage.builder()
.messageId(UUID.randomUUID().toString())
.conversationId(conversationId)
.type(WebSocketMessage.MessageType.TEXT)
.content(part.trim())
.senderId("ai")
.senderType(WebSocketMessage.SenderType.AI)
.status(WebSocketMessage.MessageStatus.SENT)
.createTime(LocalDateTime.now())
.build();
// 保存AI回复到数据库
Message aiDbMessage = new Message();
aiDbMessage.setConversationId(conversationId);
aiDbMessage.setUserId(userId);
aiDbMessage.setUserType(
request.getSenderType() == ChatRequest.SenderType.USER ? "registered" : "guest");
aiDbMessage.setContent(part.trim());
aiDbMessage.setType("text");
aiDbMessage.setSender("ai");
aiDbMessage.setCozeRole("assistant");
aiDbMessage.setCozeContentType("text");
messageService.createMessage(aiDbMessage);
// 发送AI回复
messagingTemplate.convertAndSendToUser(userId, "/queue/messages", aiMessage);
if (conversationId != null) {
messagingTemplate.convertAndSend("/topic/conversation/" + conversationId, aiMessage);
}
// 添加短暂延迟,模拟自然对话
Thread.sleep(500);
}
// 更新会话的最后活跃时间和消息数量
updateConversationActivity(conversationId);
} catch (Exception e) {
log.error("AI响应处理失败", e);
sendErrorMessage(request.getSenderId(), "AI服务暂时不可用,请稍后重试");
}
}).start();
}
/**
* 发送错误消息
*/
private void sendErrorMessage(String userId, String errorContent) {
WebSocketMessage errorMessage = WebSocketMessage.builder()
.messageId(UUID.randomUUID().toString())
.type(WebSocketMessage.MessageType.ERROR)
.content(errorContent)
.senderId("system")
.senderType(WebSocketMessage.SenderType.SYSTEM)
.status(WebSocketMessage.MessageStatus.SENT)
.createTime(LocalDateTime.now())
.build();
messagingTemplate.convertAndSendToUser(userId, "/queue/messages", errorMessage);
}
void handleHeartbeat(String sessionId, Principal principal);
/**
* 获取在线用户数量
*/
public int getOnlineUserCount() {
return onlineUsers.size();
}
/**
* 创建新会话
*/
private String createNewConversation(String userId, ChatRequest request) {
try {
String conversationId = "conv_" + System.currentTimeMillis() + "_" + UUID.randomUUID().toString().substring(0, 8);
Conversation conversation = Conversation.builder()
.userId(userId)
.userType(request.getSenderType() == ChatRequest.SenderType.USER ? "registered" : "guest")
.title("新对话")
.type("chat")
.conversationStatus("active")
.startTime(LocalDateTime.now())
.lastActiveTime(LocalDateTime.now())
.messageCount(0)
.build();
// 设置ID
conversation.setId(conversationId);
conversationService.save(conversation);
log.info("创建新会话: conversationId={}, userId={}", conversationId, userId);
return conversationId;
} catch (Exception e) {
log.error("创建新会话失败: userId={}", userId, e);
throw new RuntimeException("创建会话失败", e);
}
}
/**
* 确保会话存在并更新活跃时间
*/
private void ensureConversationExists(String conversationId, String userId, ChatRequest request) {
try {
Conversation conversation = conversationService.getById(conversationId);
if (conversation == null) {
// 如果会话不存在,创建一个
conversation = Conversation.builder()
.userId(userId)
.userType(request.getSenderType() == ChatRequest.SenderType.USER ? "registered" : "guest")
.title("对话")
.type("chat")
.conversationStatus("active")
.startTime(LocalDateTime.now())
.lastActiveTime(LocalDateTime.now())
.messageCount(0)
.build();
// 设置ID
conversation.setId(conversationId);
conversationService.save(conversation);
log.info("创建会话: conversationId={}, userId={}", conversationId, userId);
} else {
// 更新最后活跃时间
conversation.setLastActiveTime(LocalDateTime.now());
conversationService.updateById(conversation);
}
} catch (Exception e) {
log.error("确保会话存在失败: conversationId={}, userId={}", conversationId, userId, e);
}
}
/**
* 更新会话活跃状态
*/
private void updateConversationActivity(String conversationId) {
try {
Conversation conversation = conversationService.getById(conversationId);
if (conversation != null) {
conversation.setLastActiveTime(LocalDateTime.now());
conversation.setMessageCount((conversation.getMessageCount() != null ? conversation.getMessageCount() : 0) + 1);
conversationService.updateById(conversation);
}
} catch (Exception e) {
log.error("更新会话活跃状态失败: conversationId={}", conversationId, e);
}
}
int getOnlineUserCount();
}
@@ -7,7 +7,7 @@ import com.emotion.entity.Conversation;
import com.emotion.entity.CozeApiCall;
import com.emotion.entity.EmotionRecord;
import com.emotion.entity.EmotionAnalysis;
import com.emotion.service.AIChatService;
import com.emotion.service.AiChatService;
import com.emotion.service.MessageService;
import com.emotion.service.ConversationService;
import com.emotion.service.CozeApiCallService;
@@ -34,7 +34,6 @@ import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.UUID;
import java.util.stream.Collectors;
/**
* AI聊天服务实现类
@@ -44,7 +43,7 @@ import java.util.stream.Collectors;
*/
@Slf4j
@Service
public class AiChatServiceImpl implements AIChatService {
public class AiChatServiceImpl implements AiChatService {
@Autowired
private RestTemplate restTemplate;
@@ -138,6 +137,34 @@ public class AiChatServiceImpl implements AIChatService {
} catch (Exception e) {
log.error("发送聊天消息失败", e);
return "抱歉,AI服务暂时不可用,请稍后再试。";
}
}
@Override
public String sendChatMessageForWebSocket(String conversationId, String message, String userId) {
log.info("WebSocket发送聊天消息: conversationId={}, userId={}, message={}", conversationId, userId, message);
try {
// 调用Coze API
String aiReply = sendMessage(conversationId, message, userId);
// 注意:不保存用户消息,因为WebSocket处理器已经保存了
// 只保存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("WebSocket聊天消息处理完成: aiMessageId={}", aiMessage.getId());
return aiReply;
} catch (Exception e) {
log.error("WebSocket发送聊天消息失败", e);
return "抱歉,我暂时无法回复,请稍后再试。";
}
}
@@ -1051,6 +1078,39 @@ public class AiChatServiceImpl implements AIChatService {
.build();
}
/**
* 根据主要情绪确定情绪极性
*/
private String determinePolarity(String primaryEmotion) {
if (primaryEmotion == null || primaryEmotion.trim().isEmpty()) {
return "neutral";
}
String emotion = primaryEmotion.toLowerCase().trim();
// 积极情绪
if (emotion.contains("快乐") || emotion.contains("高兴") || emotion.contains("喜悦") ||
emotion.contains("兴奋") || emotion.contains("满足") || emotion.contains("感激") ||
emotion.contains("") || emotion.contains("希望") || emotion.contains("自信") ||
emotion.contains("平静") || emotion.contains("放松") || emotion.contains("开心") ||
emotion.contains("幸福") || emotion.contains("乐观") || emotion.contains("满意")) {
return "positive";
}
// 消极情绪
if (emotion.contains("悲伤") || emotion.contains("愤怒") || emotion.contains("恐惧") ||
emotion.contains("焦虑") || emotion.contains("沮丧") || emotion.contains("失望") ||
emotion.contains("孤独") || emotion.contains("痛苦") || emotion.contains("绝望") ||
emotion.contains("愧疚") || emotion.contains("羞耻") || emotion.contains("嫉妒") ||
emotion.contains("厌恶") || emotion.contains("烦躁") || emotion.contains("压抑") ||
emotion.contains("无助") || emotion.contains("困惑") || emotion.contains("担心")) {
return "negative";
}
// 默认为中性
return "neutral";
}
/**
* 从AI回复中提取JSON字符串
*/
@@ -5,14 +5,25 @@ import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.emotion.common.BasePageRequest;
import com.emotion.common.PageResult;
import com.emotion.dto.request.MessagePageRequest;
import com.emotion.dto.request.MessageSearchRequest;
import com.emotion.dto.request.MessageRecentRequest;
import com.emotion.dto.request.MessageCreateRequest;
import com.emotion.dto.response.MessageResponse;
import com.emotion.entity.Message;
import com.emotion.mapper.MessageMapper;
import com.emotion.service.MessageService;
import com.emotion.util.CurrentUserUtil;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.BeanUtils;
import org.springframework.stereotype.Service;
import org.springframework.util.StringUtils;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.List;
import java.util.stream.Collectors;
/**
* 消息服务实现类
@@ -20,9 +31,12 @@ import java.util.List;
* @author emotion-museum
* @date 2025-07-24
*/
@Slf4j
@Service
public class MessageServiceImpl extends ServiceImpl<MessageMapper, Message> implements MessageService {
private static final DateTimeFormatter DATE_TIME_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
@Override
public IPage<Message> getPage(BasePageRequest request) {
Page<Message> page = new Page<>(request.getCurrent(), request.getSize());
@@ -201,4 +215,124 @@ public class MessageServiceImpl extends ServiceImpl<MessageMapper, Message> impl
// 获取用户最近的消息,按时间倒序
return this.baseMapper.getRecentByUserId(userId, limit);
}
@Override
public PageResult<MessageResponse> getUserMessagesWithPage(MessagePageRequest request) {
log.info("获取用户消息分页: current={}, size={}", request.getCurrent(), request.getSize());
// 从上下文中获取当前用户ID
String userId = CurrentUserUtil.requireCurrentUserId();
log.info("当前用户ID: {}", userId);
// 调用原有的分页查询方法
IPage<Message> page = getByUserIdWithPage(userId, Math.toIntExact(request.getCurrent()),
Math.toIntExact(request.getSize()));
log.info("查询结果: total={}, records={}", page.getTotal(), page.getRecords().size());
// 转换为响应对象
List<MessageResponse> responses = page.getRecords().stream()
.map(this::convertToResponse)
.collect(Collectors.toList());
// 构建分页结果
PageResult<MessageResponse> pageResult = new PageResult<>();
pageResult.setCurrent(page.getCurrent());
pageResult.setSize(page.getSize());
pageResult.setTotal(page.getTotal());
pageResult.setPages(page.getPages());
pageResult.setRecords(responses);
return pageResult;
}
@Override
public List<MessageResponse> searchUserMessages(MessageSearchRequest request) {
log.info("搜索用户消息: keyword={}, limit={}", request.getKeyword(), request.getLimit());
// 从上下文中获取当前用户ID
String userId = CurrentUserUtil.requireCurrentUserId();
log.info("当前用户ID: {}", userId);
// 调用原有的搜索方法
List<Message> messages = searchByUserIdAndKeyword(userId, request.getKeyword(), request.getLimit());
log.info("搜索结果: {} 条消息", messages.size());
// 转换为响应对象
return messages.stream()
.map(this::convertToResponse)
.collect(Collectors.toList());
}
@Override
public List<MessageResponse> getUserRecentMessages(MessageRecentRequest request) {
log.info("获取用户最近消息: limit={}", request.getLimit());
// 从上下文中获取当前用户ID
String userId = CurrentUserUtil.requireCurrentUserId();
log.info("当前用户ID: {}", userId);
// 调用原有的获取最近消息方法
List<Message> messages = getRecentByUserId(userId, request.getLimit());
log.info("查询结果: {} 条最近消息", messages.size());
// 转换为响应对象
return messages.stream()
.map(this::convertToResponse)
.collect(Collectors.toList());
}
@Override
public MessageResponse createMessageFromRequest(MessageCreateRequest request) {
log.info("根据请求创建消息: conversationId={}", request.getConversationId());
// 从上下文中获取当前用户ID
String userId = CurrentUserUtil.requireCurrentUserId();
log.info("当前用户ID: {}", userId);
// 构建消息对象
Message message = new Message();
message.setConversationId(request.getConversationId());
message.setCreateBy(userId);
message.setContent(request.getContent());
message.setType(request.getContentType());
message.setSender(request.getSenderType());
// 调用原有的创建方法
Message savedMessage = createMessage(message);
log.info("创建消息成功: messageId={}", savedMessage.getId());
// 转换为响应对象
return convertToResponse(savedMessage);
}
@Override
public MessageResponse getMessageById(String id) {
log.info("根据ID获取消息: id={}", id);
Message message = getById(id);
if (message == null) {
log.warn("消息不存在: id={}", id);
return null;
}
// 转换为响应对象
return convertToResponse(message);
}
/**
* 转换为响应对象
*/
private MessageResponse convertToResponse(Message message) {
MessageResponse response = new MessageResponse();
BeanUtils.copyProperties(message, response);
response.setId(message.getId());
if (message.getCreateTime() != null) {
response.setCreateTime(message.getCreateTime().format(DATE_TIME_FORMATTER));
}
if (message.getUpdateTime() != null) {
response.setUpdateTime(message.getUpdateTime().format(DATE_TIME_FORMATTER));
}
return response;
}
}
@@ -0,0 +1,399 @@
package com.emotion.service.impl;
import com.emotion.dto.websocket.ChatRequest;
import com.emotion.dto.websocket.ConnectRequest;
import com.emotion.dto.websocket.WebSocketMessage;
import com.emotion.entity.Message;
import com.emotion.entity.Conversation;
import com.emotion.service.WebSocketService;
import com.emotion.service.AiChatService;
import com.emotion.service.MessageService;
import com.emotion.service.ConversationService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.messaging.simp.SimpMessagingTemplate;
import org.springframework.stereotype.Service;
import java.security.Principal;
import java.time.LocalDateTime;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
/**
* WebSocket服务实现类
*
* @author emotion-museum
* @date 2025-07-25
*/
@Slf4j
@Service
public class WebSocketServiceImpl implements WebSocketService {
@Autowired
private SimpMessagingTemplate messagingTemplate;
@Autowired
private AiChatService aiChatService;
@Autowired
private MessageService messageService;
@Autowired
private ConversationService conversationService;
// 在线用户管理
private final ConcurrentHashMap<String, String> onlineUsers = new ConcurrentHashMap<>();
/**
* 处理聊天消息
*/
@Override
public void handleChatMessage(ChatRequest request, String sessionId, Principal principal) {
try {
log.info("处理聊天消息: request={}, sessionId={}, principal={}", request, sessionId, principal);
// 验证请求参数
if (request.getContent() == null || request.getContent().trim().isEmpty()) {
sendErrorMessage(request.getSenderId(), "消息内容不能为空");
return;
}
// 确定用户身份和类型
String userId = request.getSenderId();
WebSocketMessage.SenderType senderType = WebSocketMessage.SenderType.GUEST;
if (principal != null) {
userId = principal.getName();
// 如果用户ID不是以guest_开头,说明是认证用户
if (!userId.startsWith("guest_")) {
senderType = WebSocketMessage.SenderType.USER;
}
}
// 更新请求中的用户信息
request.setSenderId(userId);
request.setSenderType(senderType == WebSocketMessage.SenderType.USER ? ChatRequest.SenderType.USER
: ChatRequest.SenderType.GUEST);
log.info("确定用户身份: userId={}, senderType={}", userId, senderType);
// 构建用户消息
WebSocketMessage userMessage = WebSocketMessage.builder()
.messageId(UUID.randomUUID().toString())
.conversationId(request.getConversationId())
.type(WebSocketMessage.MessageType.TEXT)
.content(request.getContent())
.senderId(userId)
.senderType(senderType)
.status(WebSocketMessage.MessageStatus.SENT)
.createTime(LocalDateTime.now())
.build();
// 发送用户消息到会话
if (request.getConversationId() != null) {
messagingTemplate.convertAndSend("/topic/conversation/" + request.getConversationId(), userMessage);
}
// 发送给用户私有队列
messagingTemplate.convertAndSendToUser(request.getSenderId(), "/queue/messages", userMessage);
// 发送AI思考状态
sendAiThinkingMessage(request.getSenderId(), request.getConversationId());
// 异步调用AI服务
processAiResponse(request);
} catch (Exception e) {
log.error("处理聊天消息失败", e);
sendErrorMessage(request.getSenderId(), "消息处理失败,请稍后重试");
}
}
/**
* 处理用户连接
*/
@Override
public void handleUserConnect(ConnectRequest request, String sessionId, Principal principal) {
try {
String userId = request.getUserId();
boolean isAuthenticated = false;
// 优先从Principal获取认证用户信息
if (principal != null) {
userId = principal.getName();
// 检查是否是认证用户(不是访客)
isAuthenticated = !userId.startsWith("guest_");
}
// 如果还没有userId,生成访客ID
if (userId == null) {
userId = "guest_" + sessionId;
}
log.info("用户连接WebSocket: userId={}, sessionId={}, authenticated={}",
userId, sessionId, isAuthenticated);
// 记录在线用户
onlineUsers.put(sessionId, userId);
// 发送连接成功消息
WebSocketMessage connectMessage = WebSocketMessage.builder()
.messageId(UUID.randomUUID().toString())
.type(WebSocketMessage.MessageType.CONNECTION)
.content("连接成功")
.senderId("system")
.senderType(WebSocketMessage.SenderType.SYSTEM)
.status(WebSocketMessage.MessageStatus.SENT)
.createTime(LocalDateTime.now())
.build();
messagingTemplate.convertAndSendToUser(userId, "/queue/messages", connectMessage);
} catch (Exception e) {
log.error("处理用户连接失败", e);
}
}
/**
* 处理用户断开连接
*/
@Override
public void handleUserDisconnect(String sessionId, Principal principal) {
try {
String userId = onlineUsers.remove(sessionId);
log.info("用户断开WebSocket连接: userId={}, sessionId={}", userId, sessionId);
} catch (Exception e) {
log.error("处理用户断开连接失败", e);
}
}
/**
* 处理心跳消息
*/
@Override
public void handleHeartbeat(String sessionId, Principal principal) {
try {
String userId = onlineUsers.get(sessionId);
if (userId == null && principal != null) {
userId = principal.getName();
}
// 发送心跳响应
WebSocketMessage heartbeatMessage = WebSocketMessage.builder()
.messageId(UUID.randomUUID().toString())
.type(WebSocketMessage.MessageType.HEARTBEAT)
.content("pong")
.senderId("system")
.senderType(WebSocketMessage.SenderType.SYSTEM)
.status(WebSocketMessage.MessageStatus.SENT)
.createTime(LocalDateTime.now())
.build();
if (userId != null) {
messagingTemplate.convertAndSendToUser(userId, "/queue/messages", heartbeatMessage);
}
} catch (Exception e) {
log.error("处理心跳消息失败", e);
}
}
/**
* 发送AI思考状态消息
*/
private void sendAiThinkingMessage(String userId, String conversationId) {
WebSocketMessage thinkingMessage = WebSocketMessage.builder()
.messageId(UUID.randomUUID().toString())
.conversationId(conversationId)
.type(WebSocketMessage.MessageType.AI_THINKING)
.content("AI正在思考中...")
.senderId("ai")
.senderType(WebSocketMessage.SenderType.AI)
.status(WebSocketMessage.MessageStatus.SENT)
.createTime(LocalDateTime.now())
.build();
messagingTemplate.convertAndSendToUser(userId, "/queue/messages", thinkingMessage);
if (conversationId != null) {
messagingTemplate.convertAndSend("/topic/conversation/" + conversationId, thinkingMessage);
}
}
/**
* 异步处理AI响应
*/
private void processAiResponse(ChatRequest request) {
// 使用线程池异步处理AI响应
new Thread(() -> {
try {
String userId = request.getSenderId();
String conversationId = request.getConversationId();
// 如果没有会话ID,创建新会话
if (conversationId == null || conversationId.trim().isEmpty()) {
conversationId = createNewConversation(userId, request);
request.setConversationId(conversationId);
}
// 确保会话存在并更新活跃时间
ensureConversationExists(conversationId, userId, request);
// 保存用户消息到数据库
Message userMessage = new Message();
userMessage.setConversationId(conversationId);
userMessage.setUserId(userId);
userMessage
.setUserType(request.getSenderType() == ChatRequest.SenderType.USER ? "registered" : "guest");
userMessage.setContent(request.getContent());
userMessage.setType("text");
userMessage.setSender("user");
userMessage.setCozeRole("user");
userMessage.setCozeContentType("text");
messageService.createMessage(userMessage);
// 调用AI服务(WebSocket专用方法,不重复保存用户消息)
String aiReply = aiChatService.sendChatMessageForWebSocket(
conversationId,
request.getContent(),
userId
);
// 构建AI回复消息(不分割,保持完整性)
WebSocketMessage aiMessage = WebSocketMessage.builder()
.messageId(UUID.randomUUID().toString())
.conversationId(conversationId)
.type(WebSocketMessage.MessageType.TEXT)
.content(aiReply)
.senderId("ai")
.senderType(WebSocketMessage.SenderType.AI)
.status(WebSocketMessage.MessageStatus.SENT)
.createTime(LocalDateTime.now())
.build();
// AI回复已经在sendChatMessageForWebSocket中保存了,这里不需要重复保存
// 发送AI回复
messagingTemplate.convertAndSendToUser(userId, "/queue/messages", aiMessage);
if (conversationId != null) {
messagingTemplate.convertAndSend("/topic/conversation/" + conversationId, aiMessage);
}
// 更新会话的最后活跃时间和消息数量
updateConversationActivity(conversationId);
} catch (Exception e) {
log.error("AI响应处理失败", e);
sendErrorMessage(request.getSenderId(), "AI服务暂时不可用,请稍后重试");
}
}).start();
}
/**
* 发送错误消息
*/
private void sendErrorMessage(String userId, String errorContent) {
WebSocketMessage errorMessage = WebSocketMessage.builder()
.messageId(UUID.randomUUID().toString())
.type(WebSocketMessage.MessageType.ERROR)
.content(errorContent)
.senderId("system")
.senderType(WebSocketMessage.SenderType.SYSTEM)
.status(WebSocketMessage.MessageStatus.SENT)
.createTime(LocalDateTime.now())
.build();
messagingTemplate.convertAndSendToUser(userId, "/queue/messages", errorMessage);
}
/**
* 获取在线用户数量
*/
@Override
public int getOnlineUserCount() {
return onlineUsers.size();
}
/**
* 创建新会话
*/
private String createNewConversation(String userId, ChatRequest request) {
try {
String conversationId = "conv_" + System.currentTimeMillis() + "_" + UUID.randomUUID().toString().substring(0, 8);
Conversation conversation = Conversation.builder()
.userId(userId)
.userType(request.getSenderType() == ChatRequest.SenderType.USER ? "registered" : "guest")
.title("新对话")
.type("chat")
.conversationStatus("active")
.startTime(LocalDateTime.now())
.lastActiveTime(LocalDateTime.now())
.messageCount(0)
.build();
// 设置ID
conversation.setId(conversationId);
conversationService.save(conversation);
log.info("创建新会话: conversationId={}, userId={}", conversationId, userId);
return conversationId;
} catch (Exception e) {
log.error("创建新会话失败: userId={}", userId, e);
throw new RuntimeException("创建会话失败", e);
}
}
/**
* 确保会话存在并更新活跃时间
*/
private void ensureConversationExists(String conversationId, String userId, ChatRequest request) {
try {
Conversation conversation = conversationService.getById(conversationId);
if (conversation == null) {
// 如果会话不存在,创建一个
conversation = Conversation.builder()
.userId(userId)
.userType(request.getSenderType() == ChatRequest.SenderType.USER ? "registered" : "guest")
.title("对话")
.type("chat")
.conversationStatus("active")
.startTime(LocalDateTime.now())
.lastActiveTime(LocalDateTime.now())
.messageCount(0)
.build();
// 设置ID
conversation.setId(conversationId);
conversationService.save(conversation);
log.info("创建会话: conversationId={}, userId={}", conversationId, userId);
} else {
// 更新最后活跃时间
conversation.setLastActiveTime(LocalDateTime.now());
conversationService.updateById(conversation);
}
} catch (Exception e) {
log.error("确保会话存在失败: conversationId={}, userId={}", conversationId, userId, e);
}
}
/**
* 更新会话活跃状态
*/
private void updateConversationActivity(String conversationId) {
try {
Conversation conversation = conversationService.getById(conversationId);
if (conversation != null) {
conversation.setLastActiveTime(LocalDateTime.now());
conversation.setMessageCount((conversation.getMessageCount() != null ? conversation.getMessageCount() : 0) + 1);
conversationService.updateById(conversation);
}
} catch (Exception e) {
log.error("更新会话活跃状态失败: conversationId={}", conversationId, e);
}
}
}
+87
View File
@@ -0,0 +1,87 @@
# 聊天记录加载问题修复总结
## 问题描述
用户在聊天页面右上角点击聊天记录按钮时,无法正确加载聊天记录。
## 问题根源分析
通过代码分析发现问题的根本原因:
### 1. 拦截器配置问题
- **WebMvcConfig**: 配置了JwtAuthInterceptor,但只拦截`/api/**`路径
- **WebConfig**: 配置了AuthInterceptor和UserContextInterceptor,拦截`/**`路径
- **MessageController**: 路径是`/message`,不是`/api/message`
### 2. 用户上下文设置问题
- **AuthInterceptor**: 验证token并将用户信息存储到请求属性中
- **UserContextInterceptor**: 负责设置UserContextHolder,但没有从请求属性中获取用户信息
- **CurrentUserUtil**: 从UserContextHolder中获取用户ID,但UserContextHolder中没有正确的用户信息
## 修复方案
### 1. 前端错误处理改进
**文件**: `web/src/views/Chat/index.vue`
-`loadHistoryMessages``searchHistoryMessages`方法中添加详细的日志输出
- 改进错误处理,区分不同类型的错误(401认证失败、500服务器错误等)
**文件**: `web/src/services/api.ts`
- 改进响应拦截器的错误处理
- 为业务错误码401添加特殊处理
- 增加详细的错误日志输出
### 2. 后端认证链修复
**文件**: `backend-single/src/main/java/com/emotion/interceptor/AuthInterceptor.java`
- 添加从token中获取username并存储到请求属性中
**文件**: `backend-single/src/main/java/com/emotion/interceptor/UserContextInterceptor.java`
- 修改`getUserIdFromRequest`方法,优先从请求属性中获取用户ID
- 修改`getUsernameFromRequest`方法,优先从请求属性中获取用户名
### 3. 后端错误处理改进
**文件**: `backend-single/src/main/java/com/emotion/controller/MessageController.java`
- 在所有用户消息相关的API中添加详细的日志输出
- 改进异常处理,区分认证失败和其他错误
- 返回更友好的错误信息
## 修复后的认证流程
1. **请求到达**: 用户发送带有Authorization头的请求到`/message/user/page`
2. **AuthInterceptor处理**:
- 验证JWT token
- 从token中提取userId和username
- 将用户信息存储到请求属性中
3. **UserContextInterceptor处理**:
- 从请求属性中获取用户信息
- 设置到UserContextHolder中
4. **Controller处理**:
- 通过CurrentUserUtil.requireCurrentUserId()获取用户ID
- 查询用户的聊天记录
- 返回结果
## 测试验证
### 1. 使用测试工具
- 打开`test-chat-history-api.html`
- 输入有效的JWT token
- 测试各个API接口
### 2. 前端测试
- 登录用户账号
- 点击聊天页面右上角的聊天记录按钮
- 检查浏览器控制台的日志输出
- 验证聊天记录是否正确加载
### 3. 后端日志检查
- 查看后端日志中的用户认证和上下文设置信息
- 确认用户ID正确传递到Controller层
## 预期结果
- 聊天记录弹窗能够正确打开
- 用户的历史消息能够正确加载和显示
- 搜索功能正常工作
- 分页功能正常工作
## 注意事项
1. 确保前端localStorage中有有效的token
2. 确保后端服务正常运行
3. 如果仍有问题,检查数据库中是否有用户的消息记录
4. 可以使用提供的测试工具进行API级别的调试
+147
View File
@@ -0,0 +1,147 @@
# 聊天记录调试指南
## 问题总结
1. **Controller层优化完成**
- 移除了业务逻辑,业务代码移到Service层
- 使用专门的Request和Response DTO
- 统一使用Result返回格式
2. **前端调用问题修复**
- 添加了缺失的`useUserStore`导入
- 修正了API调用方法(搜索和最近消息改为POST)
## 调试步骤
### 1. 检查前端基础环境
在浏览器控制台运行:
```javascript
// 检查token
console.log('Token:', localStorage.getItem('token'))
// 检查用户信息
console.log('User store:', window.Vue?.config?.globalProperties?.$stores?.user)
// 检查API基础配置
console.log('API Base URL:', 'http://localhost:8080') // 根据实际情况修改
```
### 2. 手动测试API调用
在浏览器控制台运行:
```javascript
// 测试获取用户消息分页
fetch('/message/user/page?current=1&size=5', {
headers: {
'Authorization': `Bearer ${localStorage.getItem('token')}`,
'Content-Type': 'application/json'
}
})
.then(res => res.json())
.then(data => console.log('分页API结果:', data))
.catch(err => console.error('分页API错误:', err))
// 测试搜索消息
fetch('/message/user/search', {
method: 'POST',
headers: {
'Authorization': `Bearer ${localStorage.getItem('token')}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({ keyword: '测试', limit: 5 })
})
.then(res => res.json())
.then(data => console.log('搜索API结果:', data))
.catch(err => console.error('搜索API错误:', err))
// 测试获取最近消息
fetch('/message/user/recent', {
method: 'POST',
headers: {
'Authorization': `Bearer ${localStorage.getItem('token')}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({ limit: 5 })
})
.then(res => res.json())
.then(data => console.log('最近消息API结果:', data))
.catch(err => console.error('最近消息API错误:', err))
```
### 3. 检查聊天记录按钮事件
在聊天页面控制台运行:
```javascript
// 查找聊天记录按钮
const historyButton = document.querySelector('.header-right .action-btn')
console.log('聊天记录按钮:', historyButton)
// 检查点击事件
if (historyButton) {
historyButton.addEventListener('click', () => {
console.log('聊天记录按钮被点击')
})
}
// 检查抽屉状态
const drawer = document.querySelector('.history-drawer')
console.log('聊天记录抽屉:', drawer)
```
### 4. 检查Vue组件状态
在聊天页面控制台运行:
```javascript
// 检查Vue实例
const app = document.querySelector('#app').__vue__
console.log('Vue app:', app)
// 检查聊天记录相关的响应式数据
console.log('showHistory:', app?.setupState?.showHistory?.value)
console.log('historyMessages:', app?.setupState?.historyMessages?.value)
console.log('historyLoading:', app?.setupState?.historyLoading?.value)
```
## 常见问题排查
### 1. Token问题
- 检查localStorage中是否有token
- 检查token是否过期
- 检查token格式是否正确
### 2. 网络请求问题
- 检查浏览器Network面板
- 检查是否有CORS错误
- 检查API路径是否正确
### 3. 后端认证问题
- 检查后端日志中的认证信息
- 检查UserContextHolder是否正确设置
- 检查CurrentUserUtil.requireCurrentUserId()是否抛出异常
### 4. 前端组件问题
- 检查Vue组件是否正确挂载
- 检查响应式数据是否正确初始化
- 检查事件绑定是否正确
## 修复后的API接口
### 后端接口
- `GET /message/user/page` - 获取用户消息分页
- `POST /message/user/search` - 搜索用户消息
- `POST /message/user/recent` - 获取用户最近消息
### 前端调用
```javascript
// 获取分页消息
messageApi.getUserMessages(1, 20)
// 搜索消息
messageApi.searchUserMessages('关键词', 50)
// 获取最近消息
messageApi.getRecentMessages(10)
```
## 预期结果
- 点击聊天记录按钮后,抽屉正常打开
- 控制台显示API调用日志
- 聊天记录正确加载和显示
- 搜索功能正常工作
+226
View File
@@ -0,0 +1,226 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>聊天记录API测试</title>
<style>
body {
font-family: Arial, sans-serif;
max-width: 800px;
margin: 0 auto;
padding: 20px;
}
.test-section {
margin: 20px 0;
padding: 15px;
border: 1px solid #ddd;
border-radius: 5px;
}
.test-button {
background: #1890ff;
color: white;
border: none;
padding: 8px 16px;
border-radius: 4px;
cursor: pointer;
margin: 5px;
}
.test-button:hover {
background: #40a9ff;
}
.result {
background: #f5f5f5;
padding: 10px;
border-radius: 4px;
margin-top: 10px;
white-space: pre-wrap;
font-family: monospace;
}
.error {
background: #fff2f0;
border: 1px solid #ffccc7;
color: #ff4d4f;
}
.success {
background: #f6ffed;
border: 1px solid #b7eb8f;
color: #52c41a;
}
.input-group {
margin: 10px 0;
}
.input-group label {
display: inline-block;
width: 100px;
font-weight: bold;
}
.input-group input {
padding: 5px;
border: 1px solid #ddd;
border-radius: 4px;
width: 200px;
}
</style>
</head>
<body>
<h1>聊天记录API测试工具</h1>
<div class="test-section">
<h3>认证信息</h3>
<div class="input-group">
<label>Token:</label>
<input type="text" id="tokenInput" placeholder="请输入JWT Token">
</div>
<div class="input-group">
<label>API Base:</label>
<input type="text" id="apiBaseInput" value="http://localhost:8080" placeholder="API基础URL">
</div>
<button class="test-button" onclick="loadTokenFromStorage()">从localStorage加载Token</button>
</div>
<div class="test-section">
<h3>API测试</h3>
<button class="test-button" onclick="testGetUserMessages()">测试获取用户消息分页</button>
<button class="test-button" onclick="testSearchMessages()">测试搜索消息</button>
<button class="test-button" onclick="testGetRecentMessages()">测试获取最近消息</button>
<button class="test-button" onclick="testCurrentUser()">测试获取当前用户信息</button>
<div class="input-group">
<label>搜索关键词:</label>
<input type="text" id="searchKeyword" value="测试" placeholder="搜索关键词">
</div>
</div>
<div class="test-section">
<h3>测试结果</h3>
<div id="testResult" class="result">等待测试...</div>
</div>
<script>
function getToken() {
return document.getElementById('tokenInput').value.trim();
}
function getApiBase() {
return document.getElementById('apiBaseInput').value.trim();
}
function loadTokenFromStorage() {
const token = localStorage.getItem('token');
if (token) {
document.getElementById('tokenInput').value = token;
showResult('Token已从localStorage加载', 'success');
} else {
showResult('localStorage中没有找到token', 'error');
}
}
function showResult(message, type = 'success') {
const resultDiv = document.getElementById('testResult');
resultDiv.textContent = message;
resultDiv.className = `result ${type}`;
}
async function makeRequest(url, options = {}) {
const token = getToken();
const apiBase = getApiBase();
if (!token) {
throw new Error('请先输入Token');
}
const fullUrl = `${apiBase}${url}`;
const headers = {
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json',
...options.headers
};
console.log('发送请求:', { url: fullUrl, headers });
const response = await fetch(fullUrl, {
...options,
headers
});
const data = await response.json();
console.log('响应数据:', { status: response.status, data });
return { status: response.status, data };
}
async function testGetUserMessages() {
try {
showResult('正在测试获取用户消息分页...', 'success');
const result = await makeRequest('/message/user/page?current=1&size=10');
const message = `状态码: ${result.status}
响应数据: ${JSON.stringify(result.data, null, 2)}`;
showResult(message, result.status === 200 ? 'success' : 'error');
} catch (error) {
showResult(`错误: ${error.message}`, 'error');
}
}
async function testSearchMessages() {
try {
const keyword = document.getElementById('searchKeyword').value.trim();
if (!keyword) {
showResult('请输入搜索关键词', 'error');
return;
}
showResult('正在测试搜索消息...', 'success');
const result = await makeRequest(`/message/user/search?keyword=${encodeURIComponent(keyword)}&limit=10`);
const message = `状态码: ${result.status}
响应数据: ${JSON.stringify(result.data, null, 2)}`;
showResult(message, result.status === 200 ? 'success' : 'error');
} catch (error) {
showResult(`错误: ${error.message}`, 'error');
}
}
async function testGetRecentMessages() {
try {
showResult('正在测试获取最近消息...', 'success');
const result = await makeRequest('/message/user/recent?limit=10');
const message = `状态码: ${result.status}
响应数据: ${JSON.stringify(result.data, null, 2)}`;
showResult(message, result.status === 200 ? 'success' : 'error');
} catch (error) {
showResult(`错误: ${error.message}`, 'error');
}
}
async function testCurrentUser() {
try {
showResult('正在测试获取当前用户信息...', 'success');
const result = await makeRequest('/user/current');
const message = `状态码: ${result.status}
响应数据: ${JSON.stringify(result.data, null, 2)}`;
showResult(message, result.status === 200 ? 'success' : 'error');
} catch (error) {
showResult(`错误: ${error.message}`, 'error');
}
}
// 页面加载时自动尝试加载token
window.onload = function() {
loadTokenFromStorage();
};
</script>
</body>
</html>
+238
View File
@@ -0,0 +1,238 @@
// 完整的聊天记录功能测试脚本
// 在聊天页面的浏览器控制台中运行
(function() {
console.log('=== 聊天记录功能完整测试 ===');
// 1. 检查基础环境
function checkBasicEnvironment() {
console.log('\n1. 检查基础环境:');
const token = localStorage.getItem('token');
console.log('- Token存在:', !!token);
if (!token) {
console.error('❌ 没有找到token,请先登录');
return false;
}
console.log('- 当前页面:', window.location.pathname);
console.log('- Token长度:', token.length);
return true;
}
// 2. 测试API调用
async function testAPIEndpoints() {
console.log('\n2. 测试API端点:');
const token = localStorage.getItem('token');
const baseURL = window.location.origin; // 使用当前域名
const headers = {
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json'
};
// 测试分页API
try {
console.log('- 测试分页API...');
const pageResponse = await fetch(`${baseURL}/message/user/page?current=1&size=5`, {
method: 'GET',
headers
});
console.log(' 状态码:', pageResponse.status);
const pageData = await pageResponse.json();
console.log(' 响应数据:', pageData);
if (pageResponse.ok && pageData.code === 200) {
console.log(' ✅ 分页API成功');
} else {
console.log(' ❌ 分页API失败:', pageData.message);
}
} catch (error) {
console.log(' ❌ 分页API错误:', error.message);
}
// 测试搜索API
try {
console.log('- 测试搜索API...');
const searchResponse = await fetch(`${baseURL}/message/user/search`, {
method: 'POST',
headers,
body: JSON.stringify({ keyword: '测试', limit: 5 })
});
console.log(' 状态码:', searchResponse.status);
const searchData = await searchResponse.json();
console.log(' 响应数据:', searchData);
if (searchResponse.ok && searchData.code === 200) {
console.log(' ✅ 搜索API成功');
} else {
console.log(' ❌ 搜索API失败:', searchData.message);
}
} catch (error) {
console.log(' ❌ 搜索API错误:', error.message);
}
// 测试最近消息API
try {
console.log('- 测试最近消息API...');
const recentResponse = await fetch(`${baseURL}/message/user/recent`, {
method: 'POST',
headers,
body: JSON.stringify({ limit: 5 })
});
console.log(' 状态码:', recentResponse.status);
const recentData = await recentResponse.json();
console.log(' 响应数据:', recentData);
if (recentResponse.ok && recentData.code === 200) {
console.log(' ✅ 最近消息API成功');
} else {
console.log(' ❌ 最近消息API失败:', recentData.message);
}
} catch (error) {
console.log(' ❌ 最近消息API错误:', error.message);
}
}
// 3. 检查前端组件
function checkFrontendComponents() {
console.log('\n3. 检查前端组件:');
// 检查聊天记录按钮
const historyButton = document.querySelector('.header-right .action-btn');
console.log('- 聊天记录按钮:', !!historyButton);
if (!historyButton) {
console.log(' ❌ 未找到聊天记录按钮');
return false;
}
// 检查Vue实例
const app = document.querySelector('#app');
const vueInstance = app?.__vue__ || app?._vnode?.component?.ctx;
console.log('- Vue实例:', !!vueInstance);
if (vueInstance) {
// 检查响应式数据
const setupState = vueInstance.setupState || vueInstance.$data;
console.log('- showHistory:', setupState?.showHistory?.value);
console.log('- historyLoading:', setupState?.historyLoading?.value);
console.log('- historyMessages长度:', setupState?.historyMessages?.value?.length || 0);
}
return true;
}
// 4. 模拟点击测试
function simulateClickTest() {
console.log('\n4. 模拟点击测试:');
const historyButton = document.querySelector('.header-right .action-btn');
if (!historyButton) {
console.log('❌ 无法进行点击测试,按钮不存在');
return;
}
console.log('- 模拟点击聊天记录按钮...');
// 添加事件监听器来监控点击
let clickDetected = false;
const clickHandler = () => {
clickDetected = true;
console.log(' ✅ 检测到点击事件');
};
historyButton.addEventListener('click', clickHandler, { once: true });
// 模拟点击
historyButton.click();
// 检查抽屉是否打开
setTimeout(() => {
const drawer = document.querySelector('.ant-drawer');
const drawerVisible = drawer && !drawer.classList.contains('ant-drawer-hidden');
console.log('- 抽屉是否可见:', drawerVisible);
if (clickDetected && drawerVisible) {
console.log(' ✅ 聊天记录功能正常');
} else {
console.log(' ❌ 聊天记录功能异常');
if (!clickDetected) console.log(' - 点击事件未触发');
if (!drawerVisible) console.log(' - 抽屉未显示');
}
// 清理事件监听器
historyButton.removeEventListener('click', clickHandler);
}, 1000);
}
// 5. 检查网络请求
function monitorNetworkRequests() {
console.log('\n5. 监控网络请求:');
// 重写fetch来监控请求
const originalFetch = window.fetch;
const requests = [];
window.fetch = function(...args) {
const url = args[0];
if (typeof url === 'string' && url.includes('/message/user/')) {
console.log('- 检测到消息API请求:', url);
requests.push({ url, timestamp: Date.now() });
}
return originalFetch.apply(this, args);
};
// 5秒后恢复原始fetch并报告结果
setTimeout(() => {
window.fetch = originalFetch;
console.log('- 监控期间的API请求数量:', requests.length);
requests.forEach(req => {
console.log(` ${new Date(req.timestamp).toLocaleTimeString()}: ${req.url}`);
});
}, 5000);
}
// 主测试函数
async function runCompleteTest() {
try {
// 检查基础环境
if (!checkBasicEnvironment()) {
return;
}
// 开始监控网络请求
monitorNetworkRequests();
// 测试API端点
await testAPIEndpoints();
// 检查前端组件
checkFrontendComponents();
// 模拟点击测试
simulateClickTest();
console.log('\n=== 测试完成 ===');
console.log('请查看上述结果,如果API测试成功但前端功能异常,请检查:');
console.log('1. Vue组件是否正确挂载');
console.log('2. 事件绑定是否正确');
console.log('3. 响应式数据是否正确更新');
console.log('4. 是否有JavaScript错误');
} catch (error) {
console.error('测试过程中发生错误:', error);
}
}
// 运行测试
runCompleteTest();
})();
+69
View File
@@ -0,0 +1,69 @@
# 重复消息问题修复测试
## 问题描述
用户在聊天页面发送一条消息时,数据库中保存了两条相同内容的用户消息:
1. 第一条:通过WebSocket处理器保存,包含完整的用户信息
2. 第二条:通过REST API保存,缺少部分用户信息
## 根本原因
前端`sendMessage`方法中存在双重保存机制:
1. WebSocket发送 - 后端WebSocket处理器会保存消息
2. REST API调用 - 前端额外调用`chatApi.createMessage`保存消息
## 修复方案
1. **前端修复**:移除前端`chat.ts``sendMessage`方法里的`chatApi.createMessage`调用,只保留WebSocket发送
2. **后端修复**:创建专门的WebSocket方法`sendChatMessageForWebSocket`,避免重复保存用户消息
## 修复内容
### 前端修改
文件:`web/src/stores/chat.ts`
- 移除了第69-82行的REST API调用
- 添加了注释说明修复原因
- 保留WebSocket发送逻辑
### 后端修改
文件:`backend-single/src/main/java/com/emotion/service/AiChatService.java`
- 新增`sendChatMessageForWebSocket`方法接口
文件:`backend-single/src/main/java/com/emotion/service/impl/AiChatServiceImpl.java`
- 实现`sendChatMessageForWebSocket`方法,只保存AI回复,不保存用户消息
文件:`backend-single/src/main/java/com/emotion/service/WebSocketService.java`
- 修改WebSocket处理器调用新的`sendChatMessageForWebSocket`方法
## 测试步骤
1. 启动后端服务
2. 启动前端服务
3. 登录用户账号
4. 发送一条测试消息
5. 检查数据库中是否只有一条用户消息记录
## 预期结果
- 数据库中只保存一条用户消息
- 消息包含完整的用户信息(user_id, user_type, coze_role等)
- AI回复正常工作
- 前端显示正常
## 验证SQL
```sql
-- 查看最新的消息记录
SELECT id, conversation_id, content, sender, user_id, user_type, coze_role, create_by, create_time
FROM message
WHERE conversation_id = '你的会话ID'
ORDER BY create_time DESC
LIMIT 10;
-- 检查是否还有重复消息
SELECT content, COUNT(*) as count
FROM message
WHERE conversation_id = '你的会话ID'
AND sender = 'user'
AND create_time > '2025-07-25 16:00:00'
GROUP BY content
HAVING COUNT(*) > 1;
```
## 注意事项
- 此修复只影响新发送的消息
- 历史重复消息需要手动清理
- 确保WebSocket连接正常工作
+130
View File
@@ -0,0 +1,130 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>消息API测试</title>
<style>
body { font-family: Arial, sans-serif; max-width: 800px; margin: 0 auto; padding: 20px; }
.test-section { margin: 20px 0; padding: 15px; border: 1px solid #ddd; border-radius: 5px; }
.test-button { background: #1890ff; color: white; border: none; padding: 8px 16px; border-radius: 4px; cursor: pointer; margin: 5px; }
.result { background: #f5f5f5; padding: 10px; border-radius: 4px; margin-top: 10px; white-space: pre-wrap; font-family: monospace; }
.error { background: #fff2f0; border: 1px solid #ffccc7; color: #ff4d4f; }
.success { background: #f6ffed; border: 1px solid #b7eb8f; color: #52c41a; }
</style>
</head>
<body>
<h1>消息API测试</h1>
<div class="test-section">
<h3>Token设置</h3>
<input type="text" id="tokenInput" placeholder="请输入JWT Token" style="width: 500px; padding: 5px;">
<button class="test-button" onclick="loadToken()">从localStorage加载</button>
</div>
<div class="test-section">
<h3>API测试</h3>
<button class="test-button" onclick="testGetUserMessages()">测试分页查询</button>
<button class="test-button" onclick="testSearchMessages()">测试搜索消息</button>
<button class="test-button" onclick="testRecentMessages()">测试最近消息</button>
</div>
<div class="test-section">
<h3>测试结果</h3>
<div id="result" class="result">等待测试...</div>
</div>
<script>
function loadToken() {
const token = localStorage.getItem('token');
if (token) {
document.getElementById('tokenInput').value = token;
showResult('Token已加载', 'success');
} else {
showResult('localStorage中没有token', 'error');
}
}
function showResult(message, type = 'success') {
const resultDiv = document.getElementById('result');
resultDiv.textContent = message;
resultDiv.className = `result ${type}`;
}
async function makeRequest(url, options = {}) {
const token = document.getElementById('tokenInput').value.trim();
if (!token) {
throw new Error('请先输入Token');
}
const headers = {
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json',
...options.headers
};
const response = await fetch(url, { ...options, headers });
const data = await response.json();
return { status: response.status, data };
}
async function testGetUserMessages() {
try {
showResult('正在测试分页查询...', 'success');
const result = await makeRequest('/message/user/page?current=1&size=5');
const message = `分页查询结果:
状态码: ${result.status}
响应: ${JSON.stringify(result.data, null, 2)}`;
showResult(message, result.status === 200 ? 'success' : 'error');
} catch (error) {
showResult(`分页查询错误: ${error.message}`, 'error');
}
}
async function testSearchMessages() {
try {
showResult('正在测试搜索消息...', 'success');
const result = await makeRequest('/message/user/search', {
method: 'POST',
body: JSON.stringify({ keyword: '测试', limit: 5 })
});
const message = `搜索消息结果:
状态码: ${result.status}
响应: ${JSON.stringify(result.data, null, 2)}`;
showResult(message, result.status === 200 ? 'success' : 'error');
} catch (error) {
showResult(`搜索消息错误: ${error.message}`, 'error');
}
}
async function testRecentMessages() {
try {
showResult('正在测试最近消息...', 'success');
const result = await makeRequest('/message/user/recent', {
method: 'POST',
body: JSON.stringify({ limit: 5 })
});
const message = `最近消息结果:
状态码: ${result.status}
响应: ${JSON.stringify(result.data, null, 2)}`;
showResult(message, result.status === 200 ? 'success' : 'error');
} catch (error) {
showResult(`最近消息错误: ${error.message}`, 'error');
}
}
// 页面加载时自动加载token
window.onload = loadToken;
</script>
</body>
</html>
+174
View File
@@ -0,0 +1,174 @@
// 聊天记录修复验证脚本
// 在浏览器控制台中运行此脚本来验证修复效果
(function() {
console.log('=== 聊天记录修复验证脚本 ===');
// 检查基础环境
function checkEnvironment() {
console.log('\n1. 检查基础环境:');
// 检查token
const token = localStorage.getItem('token');
console.log('- Token存在:', !!token);
if (token) {
console.log('- Token长度:', token.length);
console.log('- Token前缀:', token.substring(0, 20) + '...');
}
// 检查当前页面
console.log('- 当前页面:', window.location.pathname);
// 检查API基础URL
const apiBase = 'http://localhost:8080'; // 根据实际情况修改
console.log('- API基础URL:', apiBase);
return { token, apiBase };
}
// 测试API调用
async function testAPI(url, token, apiBase) {
try {
console.log(`\n测试API: ${url}`);
const response = await fetch(`${apiBase}${url}`, {
method: 'GET',
headers: {
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json'
}
});
console.log('- 响应状态:', response.status);
console.log('- 响应状态文本:', response.statusText);
const data = await response.json();
console.log('- 响应数据:', data);
if (response.ok && data.code === 200) {
console.log('✅ API调用成功');
return { success: true, data: data.data };
} else {
console.log('❌ API调用失败:', data.message || '未知错误');
return { success: false, error: data.message || '未知错误' };
}
} catch (error) {
console.log('❌ 网络错误:', error.message);
return { success: false, error: error.message };
}
}
// 测试所有聊天记录相关API
async function testChatHistoryAPIs() {
console.log('\n2. 测试聊天记录API:');
const { token, apiBase } = checkEnvironment();
if (!token) {
console.log('❌ 没有找到token,请先登录');
return;
}
// 测试获取用户消息分页
const pageResult = await testAPI('/message/user/page?current=1&size=5', token, apiBase);
// 测试搜索消息
const searchResult = await testAPI('/message/user/search?keyword=测试&limit=5', token, apiBase);
// 测试获取最近消息
const recentResult = await testAPI('/message/user/recent?limit=5', token, apiBase);
// 测试获取当前用户信息
const userResult = await testAPI('/user/current', token, apiBase);
// 汇总结果
console.log('\n3. 测试结果汇总:');
console.log('- 分页查询:', pageResult.success ? '✅ 成功' : '❌ 失败');
console.log('- 搜索功能:', searchResult.success ? '✅ 成功' : '❌ 失败');
console.log('- 最近消息:', recentResult.success ? '✅ 成功' : '❌ 失败');
console.log('- 用户信息:', userResult.success ? '✅ 成功' : '❌ 失败');
// 如果有成功的结果,显示数据统计
if (pageResult.success && pageResult.data) {
console.log('\n4. 数据统计:');
console.log('- 总消息数:', pageResult.data.total || 0);
console.log('- 当前页消息数:', pageResult.data.records ? pageResult.data.records.length : 0);
if (pageResult.data.records && pageResult.data.records.length > 0) {
const firstMessage = pageResult.data.records[0];
console.log('- 最新消息预览:', {
id: firstMessage.id,
content: firstMessage.content ? firstMessage.content.substring(0, 50) + '...' : '',
sender: firstMessage.sender,
createTime: firstMessage.createTime
});
}
}
return {
pageResult,
searchResult,
recentResult,
userResult
};
}
// 测试前端聊天记录功能
function testFrontendChatHistory() {
console.log('\n5. 测试前端聊天记录功能:');
// 检查是否在聊天页面
if (!window.location.pathname.includes('/chat')) {
console.log('❌ 当前不在聊天页面,请先进入聊天页面');
return;
}
// 查找聊天记录按钮
const historyButton = document.querySelector('.header-right .action-btn');
if (historyButton) {
console.log('✅ 找到聊天记录按钮');
// 模拟点击
console.log('- 模拟点击聊天记录按钮...');
historyButton.click();
// 检查抽屉是否打开
setTimeout(() => {
const drawer = document.querySelector('.history-drawer');
if (drawer && drawer.style.display !== 'none') {
console.log('✅ 聊天记录抽屉已打开');
} else {
console.log('❌ 聊天记录抽屉未打开');
}
}, 1000);
} else {
console.log('❌ 未找到聊天记录按钮');
}
}
// 主函数
async function main() {
try {
// 测试API
const apiResults = await testChatHistoryAPIs();
// 测试前端功能
testFrontendChatHistory();
console.log('\n=== 验证完成 ===');
console.log('如果API测试都成功,但前端仍有问题,请检查:');
console.log('1. 浏览器控制台是否有JavaScript错误');
console.log('2. 网络请求是否正常发送');
console.log('3. 前端代码是否正确处理API响应');
} catch (error) {
console.error('验证过程中发生错误:', error);
}
}
// 运行验证
main();
})();
+73
View File
@@ -0,0 +1,73 @@
-- 验证重复消息修复效果的SQL脚本
-- 1. 查看最新的消息记录(修复后应该只有一条用户消息)
SELECT
id,
conversation_id,
content,
sender,
user_id,
user_type,
coze_role,
create_by,
create_time,
update_time
FROM message
WHERE create_time > '2025-07-25 16:15:00' -- 修复后的时间
ORDER BY create_time DESC
LIMIT 20;
-- 2. 检查是否还有重复的用户消息(修复后应该返回0条记录)
SELECT
content,
conversation_id,
sender,
COUNT(*) as duplicate_count,
GROUP_CONCAT(id) as message_ids,
MIN(create_time) as first_time,
MAX(create_time) as last_time
FROM message
WHERE sender = 'user'
AND create_time > '2025-07-25 16:15:00' -- 修复后的时间
GROUP BY content, conversation_id, sender
HAVING COUNT(*) > 1;
-- 3. 查看特定会话的消息流(验证消息顺序正常)
-- 请将 'YOUR_CONVERSATION_ID' 替换为实际的会话ID
SELECT
id,
content,
sender,
user_id,
user_type,
create_time,
CASE
WHEN user_id IS NOT NULL AND user_type IS NOT NULL THEN 'WebSocket保存'
WHEN user_id IS NULL AND create_by != 'system' THEN 'REST API保存'
ELSE '其他方式保存'
END as save_method
FROM message
WHERE conversation_id = 'YOUR_CONVERSATION_ID'
ORDER BY create_time ASC;
-- 4. 统计修复前后的消息数量对比
SELECT
DATE(create_time) as date,
sender,
COUNT(*) as message_count
FROM message
WHERE create_time >= '2025-07-25 00:00:00'
GROUP BY DATE(create_time), sender
ORDER BY date DESC, sender;
-- 5. 查找可能的重复消息模式
SELECT
content,
sender,
COUNT(*) as count,
GROUP_CONCAT(DISTINCT user_id) as user_ids,
GROUP_CONCAT(DISTINCT create_by) as create_bys
FROM message
WHERE create_time > '2025-07-25 16:00:00'
GROUP BY content, sender
HAVING COUNT(*) > 1;
+23 -4
View File
@@ -38,8 +38,21 @@ api.interceptors.response.use(
// 检查业务状态码
if (data.code !== 200) {
console.error('API Error:', data.message)
return Promise.reject(new Error(data.message))
console.error('API Business Error:', {
code: data.code,
message: data.message,
url: response.config.url
})
// 对于认证错误,特殊处理
if (data.code === 401) {
localStorage.removeItem('token')
window.location.href = '/login'
}
const error = new Error(data.message || '请求失败')
error.response = response
return Promise.reject(error)
}
return response
@@ -49,6 +62,12 @@ api.interceptors.response.use(
if (error.response) {
const { status, data } = error.response
console.error('HTTP Error:', {
status,
url: error.config?.url,
message: data?.message || error.message
})
switch (status) {
case 401:
// 未授权,清除token并跳转到登录页
@@ -121,11 +140,11 @@ export const messageApi = {
// 搜索用户消息
searchUserMessages: (keyword: string, limit: number = 50) =>
request.get(`/message/user/search`, { params: { keyword, limit } }),
request.post(`/message/user/search`, { keyword, limit }),
// 获取用户最近的聊天记录
getRecentMessages: (limit: number = 10) =>
request.get(`/message/user/recent`, { params: { limit } }),
request.post(`/message/user/recent`, { limit }),
// 获取消息详情
getMessageById: (id: string) =>
+6 -17
View File
@@ -40,7 +40,7 @@ export const useChatStore = defineStore('chat', () => {
}
}
// 发送消息:WebSocket推送+数据库保存
// 发送消息:仅通过WebSocket推送,后端统一处理保存
const sendMessage = async (content: string) => {
if (!wsConnected.value) {
console.error('WebSocket未连接,无法发送消息')
@@ -52,7 +52,7 @@ export const useChatStore = defineStore('chat', () => {
return
}
// 添加用户消息
// 添加用户消息到前端显示
const userMessage = addMessage({
content,
type: 'user',
@@ -60,28 +60,17 @@ export const useChatStore = defineStore('chat', () => {
})
try {
// WebSocket推送
// 仅通过WebSocket推送,后端会统一处理消息保存
webSocketService.sendChatMessage(content, currentSession.value?.id)
// 更新消息状态为已发送
updateMessageStatus(userMessage.id, 'sent')
// 数据库保存
if (currentSession.value?.id && userStore.user?.id) {
await chatApi.createMessage({
conversationId: currentSession.value.id,
userId: userStore.user.id,
content,
contentType: 'TEXT',
senderType: 'USER',
senderId: userStore.user.id
})
// 注意:移除了重复的REST API调用,避免重复保存消息
// 后端WebSocket处理器会统一保存用户消息到数据库
// 更新消息状态为已送达
updateMessageStatus(userMessage.id, 'delivered')
}
} catch (error) {
console.error('消息发送或保存失败:', error)
console.error('消息发送失败:', error)
// 更新消息状态为失败
updateMessageStatus(userMessage.id, 'failed', '发送失败')
+41 -7
View File
@@ -80,7 +80,7 @@
:class="{ 'user-message': message.type === 'user' }"
>
<div class="message-bubble">
<div v-if="message.type === 'ai'" class="message-avatar">
<div v-if="message.type === 'ai' || message.sender === 'ai'" class="message-avatar">
<a-avatar :src="kaikaiAvatar" :size="32" />
</div>
<div class="message-content">
@@ -285,12 +285,13 @@
SearchOutlined,
HeartOutlined,
} from '@ant-design/icons-vue'
import { useChatStore } from '@/stores'
import { useChatStore, useUserStore } from '@/stores'
import { formatTime } from '@/utils'
import { messageApi, emotionSummaryApi } from '@/services/api'
import type { Dayjs } from 'dayjs'
const chatStore = useChatStore()
const userStore = useUserStore()
// 响应式数据
const messageInput = ref('')
@@ -443,9 +444,18 @@
try {
historyLoading.value = true
console.log('开始加载历史记录:', {
page,
pageSize: historyPagination.value.pageSize,
token: !!localStorage.getItem('token'),
userInfo: userStore.userInfo
})
// 调用API获取用户消息(后端会从token中获取用户信息)
const pageData = await messageApi.getUserMessages(page, historyPagination.value.pageSize)
console.log('API返回数据:', pageData)
if (page === 1) {
historyMessages.value = pageData.records || []
} else {
@@ -458,10 +468,20 @@
total: pageData.total || 0
}
console.log('历史记录加载成功:', historyMessages.value.length, '条')
console.log('历史记录加载成功:', {
total: historyMessages.value.length,
pagination: historyPagination.value
})
} catch (error) {
console.error('加载历史记录时发生错误:', error)
// 显示用户友好的错误信息
if (error.response?.status === 401) {
console.log('认证失败,可能需要重新登录')
} else if (error.response?.status === 500) {
console.log('服务器错误,请稍后重试')
}
} finally {
historyLoading.value = false
}
@@ -477,13 +497,27 @@
try {
historyLoading.value = true
console.log('开始搜索历史记录:', {
keyword: searchKeyword.value,
token: !!localStorage.getItem('token')
})
// 调用API搜索用户消息(后端会从token中获取用户信息)
const messages = await messageApi.searchUserMessages(searchKeyword.value, 100)
historyMessages.value = messages || []
console.log('搜索历史记录成功:', historyMessages.value.length, '条')
console.log('搜索历史记录成功:', {
keyword: searchKeyword.value,
total: historyMessages.value.length
})
} catch (error) {
console.error('搜索历史记录时发生错误:', error)
// 显示用户友好的错误信息
if (error.response?.status === 401) {
console.log('认证失败,搜索功能需要登录')
}
} finally {
historyLoading.value = false
}
@@ -509,15 +543,15 @@
id: msg.id,
content: msg.content,
sender: msg.sender === 'user' ? 'user' : 'ai',
timestamp: new Date(msg.createTime).getTime(),
type: 'text'
type: msg.sender === 'user' ? 'user' : 'ai', // 修复:type字段用于CSS类判断
timestamp: new Date(msg.createTime).getTime()
}))
// 按时间顺序排列(最新的在最后)
chatMessages.sort((a, b) => a.timestamp - b.timestamp)
// 添加到消息列表
messages.value.push(...chatMessages)
chatStore.messages.push(...chatMessages)
console.log('加载最近聊天记录成功:', chatMessages.length, '条')
}