From f576de68dac226100ea19b7b4c9d05f12d260abf Mon Sep 17 00:00:00 2001 From: huazhongmin Date: Fri, 25 Jul 2025 17:48:02 +0800 Subject: [PATCH] =?UTF-8?q?=E5=AF=B9=E8=AF=9D=E9=80=BB=E8=BE=91=E4=BF=AE?= =?UTF-8?q?=E5=A4=8D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .augment/rules/rules.md | 12 +- .idea/AugmentWebviewStateStore.xml | 2 +- .idea/compiler.xml | 1 + .idea/encodings.xml | 2 + .idea/misc.xml | 1 + .../emotion/controller/AiChatController.java | 6 +- .../controller/EmotionSummaryController.java | 4 +- .../emotion/controller/MessageController.java | 209 +++------ .../controller/WebSocketController.java | 5 +- .../dto/request/MessagePageRequest.java | 56 +++ .../dto/request/MessageRecentRequest.java | 40 ++ .../dto/request/MessageSearchRequest.java | 57 +++ .../emotion/interceptor/AuthInterceptor.java | 4 +- .../interceptor/UserContextInterceptor.java | 42 +- ...{AIChatService.java => AiChatService.java} | 8 +- .../com/emotion/service/MessageService.java | 31 ++ .../com/emotion/service/WebSocketService.java | 388 +---------------- .../service/impl/AiChatServiceImpl.java | 66 ++- .../service/impl/MessageServiceImpl.java | 134 ++++++ .../service/impl/WebSocketServiceImpl.java | 399 ++++++++++++++++++ chat-history-fix-summary.md | 87 ++++ debug-chat-history.md | 147 +++++++ test-chat-history-api.html | 226 ++++++++++ test-chat-history-complete.js | 238 +++++++++++ test-duplicate-message-fix.md | 69 +++ test-message-api.html | 130 ++++++ verify-chat-history-fix.js | 174 ++++++++ verify-fix.sql | 73 ++++ web/src/services/api.ts | 35 +- web/src/stores/chat.ts | 23 +- web/src/views/Chat/index.vue | 48 ++- 31 files changed, 2129 insertions(+), 588 deletions(-) create mode 100644 backend-single/src/main/java/com/emotion/dto/request/MessagePageRequest.java create mode 100644 backend-single/src/main/java/com/emotion/dto/request/MessageRecentRequest.java create mode 100644 backend-single/src/main/java/com/emotion/dto/request/MessageSearchRequest.java rename backend-single/src/main/java/com/emotion/service/{AIChatService.java => AiChatService.java} (83%) create mode 100644 backend-single/src/main/java/com/emotion/service/impl/WebSocketServiceImpl.java create mode 100644 chat-history-fix-summary.md create mode 100644 debug-chat-history.md create mode 100644 test-chat-history-api.html create mode 100644 test-chat-history-complete.js create mode 100644 test-duplicate-message-fix.md create mode 100644 test-message-api.html create mode 100644 verify-chat-history-fix.js create mode 100644 verify-fix.sql diff --git a/.augment/rules/rules.md b/.augment/rules/rules.md index 8e122c6..40250d5 100644 --- a/.augment/rules/rules.md +++ b/.augment/rules/rules.md @@ -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获取当前登录的用户信息 \ No newline at end of file +7.service层必须是接口Service和实现类ServiceImpl的方式来实现; +8.除了特殊情况下的异常,一般情况下不需要try-catch,由全局异常处理机制处理 +9.所有Controller层接口定义要完整,入参使用request封装请求,出参是response封装出参,使用项目已有的Result做接口返回 +10.所有对项目的变更要遵循当前的项目现有规范 +11.禁止在新增的Controller层的路由前面添加/api +12.与当前用户相关的接口,禁止直接传递用户id,需要后端根据当前登录用户,接口调用的token获取当前登录的用户信息 +13.在优化代码时必须要确保不能破坏已经实现的业务逻辑 \ No newline at end of file diff --git a/.idea/AugmentWebviewStateStore.xml b/.idea/AugmentWebviewStateStore.xml index 49d2460..4c66126 100644 --- a/.idea/AugmentWebviewStateStore.xml +++ b/.idea/AugmentWebviewStateStore.xml @@ -3,7 +3,7 @@ diff --git a/.idea/compiler.xml b/.idea/compiler.xml index 60e9baa..eeca3cb 100644 --- a/.idea/compiler.xml +++ b/.idea/compiler.xml @@ -7,6 +7,7 @@ + diff --git a/.idea/encodings.xml b/.idea/encodings.xml index c9d7e2c..c8f350f 100644 --- a/.idea/encodings.xml +++ b/.idea/encodings.xml @@ -1,6 +1,8 @@ + + diff --git a/.idea/misc.xml b/.idea/misc.xml index 53b0c32..3c06d23 100644 --- a/.idea/misc.xml +++ b/.idea/misc.xml @@ -6,6 +6,7 @@ diff --git a/backend-single/src/main/java/com/emotion/controller/AiChatController.java b/backend-single/src/main/java/com/emotion/controller/AiChatController.java index 09c27db..cc987fc 100644 --- a/backend-single/src/main/java/com/emotion/controller/AiChatController.java +++ b/backend-single/src/main/java/com/emotion/controller/AiChatController.java @@ -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; diff --git a/backend-single/src/main/java/com/emotion/controller/EmotionSummaryController.java b/backend-single/src/main/java/com/emotion/controller/EmotionSummaryController.java index 64ba69e..cea6a1f 100644 --- a/backend-single/src/main/java/com/emotion/controller/EmotionSummaryController.java +++ b/backend-single/src/main/java/com/emotion/controller/EmotionSummaryController.java @@ -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") diff --git a/backend-single/src/main/java/com/emotion/controller/MessageController.java b/backend-single/src/main/java/com/emotion/controller/MessageController.java index 14daeb5..63a4668 100644 --- a/backend-single/src/main/java/com/emotion/controller/MessageController.java +++ b/backend-single/src/main/java/com/emotion/controller/MessageController.java @@ -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> getPage(@Valid PageRequest request) { - IPage page = messageService.getPage(request); - List responses = page.getRecords().stream() - .map(this::convertToResponse) - .collect(Collectors.toList()); + @PostMapping + public Result create(@Valid @RequestBody MessageCreateRequest request) { + log.info("创建消息: conversationId={}", request.getConversationId()); - PageResult 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> getPageByConversationId(@PathVariable String conversationId, - @Valid PageRequest request) { - IPage page = messageService.getPageByConversationId(request, conversationId); - List responses = page.getRecords().stream() - .map(this::convertToResponse) - .collect(Collectors.toList()); - - PageResult 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 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 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> getByConversationId(@PathVariable String conversationId) { - List messages = messageService.getByConversationId(conversationId); - List responses = messages.stream() - .map(this::convertToResponse) - .collect(Collectors.toList()); - return Result.success(responses); - } - - /** - * 统计会话消息数量 - */ - @GetMapping("/conversation/{conversationId}/count") - public Result countByConversationId(@PathVariable String conversationId) { - Long count = messageService.countByConversationId(conversationId); - return Result.success(count); } /** * 根据用户ID分页查询消息 */ @GetMapping("/user/page") - public Result> getPageByUserId(@Valid PageRequest request) { + public Result> getPageByUserId( + @RequestParam(defaultValue = "1") Long current, + @RequestParam(defaultValue = "20") Long size) { + log.info("获取用户消息分页: current={}, size={}", current, size); try { - // 从上下文中获取当前用户ID - String userId = CurrentUserUtil.requireCurrentUserId(); - - IPage page = messageService.getByUserIdWithPage(userId, Math.toIntExact(request.getCurrent()), - Math.toIntExact(request.getSize())); - List responses = page.getRecords().stream() - .map(this::convertToResponse) - .collect(Collectors.toList()); - - PageResult 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 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> searchByUserId( - @RequestParam String keyword, - @RequestParam(defaultValue = "50") Integer limit) { + @PostMapping("/user/search") + public Result> searchByUserId(@Valid @RequestBody MessageSearchRequest request) { + log.info("搜索用户消息: keyword={}, limit={}", request.getKeyword(), request.getLimit()); try { - // 从上下文中获取当前用户ID - String userId = CurrentUserUtil.requireCurrentUserId(); - - List messages = messageService.searchByUserIdAndKeyword(userId, keyword, limit); - List responses = messages.stream() - .map(this::convertToResponse) - .collect(Collectors.toList()); + List 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> getRecentMessages( - @RequestParam(defaultValue = "10") Integer limit) { + @PostMapping("/user/recent") + public Result> getRecentMessages(@Valid @RequestBody MessageRecentRequest request) { + log.info("获取用户最近消息: limit={}", request.getLimit()); try { - // 从上下文中获取当前用户ID - String userId = CurrentUserUtil.requireCurrentUserId(); - - List messages = messageService.getRecentByUserId(userId, limit); - List responses = messages.stream() - .map(this::convertToResponse) - .collect(Collectors.toList()); + List 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; - } + } \ No newline at end of file diff --git a/backend-single/src/main/java/com/emotion/controller/WebSocketController.java b/backend-single/src/main/java/com/emotion/controller/WebSocketController.java index 46ea8a7..c1aabc3 100644 --- a/backend-single/src/main/java/com/emotion/controller/WebSocketController.java +++ b/backend-single/src/main/java/com/emotion/controller/WebSocketController.java @@ -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 diff --git a/backend-single/src/main/java/com/emotion/dto/request/MessagePageRequest.java b/backend-single/src/main/java/com/emotion/dto/request/MessagePageRequest.java new file mode 100644 index 0000000..7c8d182 --- /dev/null +++ b/backend-single/src/main/java/com/emotion/dto/request/MessagePageRequest.java @@ -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; +} diff --git a/backend-single/src/main/java/com/emotion/dto/request/MessageRecentRequest.java b/backend-single/src/main/java/com/emotion/dto/request/MessageRecentRequest.java new file mode 100644 index 0000000..5858e24 --- /dev/null +++ b/backend-single/src/main/java/com/emotion/dto/request/MessageRecentRequest.java @@ -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; +} diff --git a/backend-single/src/main/java/com/emotion/dto/request/MessageSearchRequest.java b/backend-single/src/main/java/com/emotion/dto/request/MessageSearchRequest.java new file mode 100644 index 0000000..fcf87e7 --- /dev/null +++ b/backend-single/src/main/java/com/emotion/dto/request/MessageSearchRequest.java @@ -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; +} diff --git a/backend-single/src/main/java/com/emotion/interceptor/AuthInterceptor.java b/backend-single/src/main/java/com/emotion/interceptor/AuthInterceptor.java index 5ca6b2f..773358e 100644 --- a/backend-single/src/main/java/com/emotion/interceptor/AuthInterceptor.java +++ b/backend-single/src/main/java/com/emotion/interceptor/AuthInterceptor.java @@ -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; diff --git a/backend-single/src/main/java/com/emotion/interceptor/UserContextInterceptor.java b/backend-single/src/main/java/com/emotion/interceptor/UserContextInterceptor.java index 60a424e..b3540d4 100644 --- a/backend-single/src/main/java/com/emotion/interceptor/UserContextInterceptor.java +++ b/backend-single/src/main/java/com/emotion/interceptor/UserContextInterceptor.java @@ -71,58 +71,70 @@ public class UserContextInterceptor implements HandlerInterceptor { /** * 从请求中获取用户ID - * + * * @param request HTTP请求 * @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(); } /** * 从请求中获取用户名 - * + * * @param request HTTP请求 * @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(); } - + return "guest"; } diff --git a/backend-single/src/main/java/com/emotion/service/AIChatService.java b/backend-single/src/main/java/com/emotion/service/AiChatService.java similarity index 83% rename from backend-single/src/main/java/com/emotion/service/AIChatService.java rename to backend-single/src/main/java/com/emotion/service/AiChatService.java index ff370c5..70bceac 100644 --- a/backend-single/src/main/java/com/emotion/service/AIChatService.java +++ b/backend-single/src/main/java/com/emotion/service/AiChatService.java @@ -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); + /** * 生成对话总结 */ diff --git a/backend-single/src/main/java/com/emotion/service/MessageService.java b/backend-single/src/main/java/com/emotion/service/MessageService.java index aec91c7..990adc9 100644 --- a/backend-single/src/main/java/com/emotion/service/MessageService.java +++ b/backend-single/src/main/java/com/emotion/service/MessageService.java @@ -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 { * 标记消息为已读 */ boolean markAsRead(String messageId); + + /** + * 获取用户消息分页(新接口) + */ + PageResult getUserMessagesWithPage(MessagePageRequest request); + + /** + * 搜索用户消息(新接口) + */ + List searchUserMessages(MessageSearchRequest request); + + /** + * 获取用户最近消息(新接口) + */ + List getUserRecentMessages(MessageRecentRequest request); + + /** + * 根据请求创建消息(新接口) + */ + MessageResponse createMessageFromRequest(MessageCreateRequest request); + + /** + * 根据ID获取消息响应(新接口) + */ + MessageResponse getMessageById(String id); } diff --git a/backend-single/src/main/java/com/emotion/service/WebSocketService.java b/backend-single/src/main/java/com/emotion/service/WebSocketService.java index 13a250d..de6d679 100644 --- a/backend-single/src/main/java/com/emotion/service/WebSocketService.java +++ b/backend-single/src/main/java/com/emotion/service/WebSocketService.java @@ -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 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(); } diff --git a/backend-single/src/main/java/com/emotion/service/impl/AiChatServiceImpl.java b/backend-single/src/main/java/com/emotion/service/impl/AiChatServiceImpl.java index 1041ca8..136a259 100644 --- a/backend-single/src/main/java/com/emotion/service/impl/AiChatServiceImpl.java +++ b/backend-single/src/main/java/com/emotion/service/impl/AiChatServiceImpl.java @@ -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字符串 */ diff --git a/backend-single/src/main/java/com/emotion/service/impl/MessageServiceImpl.java b/backend-single/src/main/java/com/emotion/service/impl/MessageServiceImpl.java index ffe9925..4823c66 100644 --- a/backend-single/src/main/java/com/emotion/service/impl/MessageServiceImpl.java +++ b/backend-single/src/main/java/com/emotion/service/impl/MessageServiceImpl.java @@ -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 implements MessageService { + private static final DateTimeFormatter DATE_TIME_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"); + @Override public IPage getPage(BasePageRequest request) { Page page = new Page<>(request.getCurrent(), request.getSize()); @@ -201,4 +215,124 @@ public class MessageServiceImpl extends ServiceImpl impl // 获取用户最近的消息,按时间倒序 return this.baseMapper.getRecentByUserId(userId, limit); } + + @Override + public PageResult getUserMessagesWithPage(MessagePageRequest request) { + log.info("获取用户消息分页: current={}, size={}", request.getCurrent(), request.getSize()); + + // 从上下文中获取当前用户ID + String userId = CurrentUserUtil.requireCurrentUserId(); + log.info("当前用户ID: {}", userId); + + // 调用原有的分页查询方法 + IPage page = getByUserIdWithPage(userId, Math.toIntExact(request.getCurrent()), + Math.toIntExact(request.getSize())); + + log.info("查询结果: total={}, records={}", page.getTotal(), page.getRecords().size()); + + // 转换为响应对象 + List responses = page.getRecords().stream() + .map(this::convertToResponse) + .collect(Collectors.toList()); + + // 构建分页结果 + PageResult 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 searchUserMessages(MessageSearchRequest request) { + log.info("搜索用户消息: keyword={}, limit={}", request.getKeyword(), request.getLimit()); + + // 从上下文中获取当前用户ID + String userId = CurrentUserUtil.requireCurrentUserId(); + log.info("当前用户ID: {}", userId); + + // 调用原有的搜索方法 + List messages = searchByUserIdAndKeyword(userId, request.getKeyword(), request.getLimit()); + log.info("搜索结果: {} 条消息", messages.size()); + + // 转换为响应对象 + return messages.stream() + .map(this::convertToResponse) + .collect(Collectors.toList()); + } + + @Override + public List getUserRecentMessages(MessageRecentRequest request) { + log.info("获取用户最近消息: limit={}", request.getLimit()); + + // 从上下文中获取当前用户ID + String userId = CurrentUserUtil.requireCurrentUserId(); + log.info("当前用户ID: {}", userId); + + // 调用原有的获取最近消息方法 + List 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; + } } \ No newline at end of file diff --git a/backend-single/src/main/java/com/emotion/service/impl/WebSocketServiceImpl.java b/backend-single/src/main/java/com/emotion/service/impl/WebSocketServiceImpl.java new file mode 100644 index 0000000..f428a09 --- /dev/null +++ b/backend-single/src/main/java/com/emotion/service/impl/WebSocketServiceImpl.java @@ -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 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); + } + } +} diff --git a/chat-history-fix-summary.md b/chat-history-fix-summary.md new file mode 100644 index 0000000..64d2e44 --- /dev/null +++ b/chat-history-fix-summary.md @@ -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级别的调试 diff --git a/debug-chat-history.md b/debug-chat-history.md new file mode 100644 index 0000000..5741e17 --- /dev/null +++ b/debug-chat-history.md @@ -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调用日志 +- 聊天记录正确加载和显示 +- 搜索功能正常工作 diff --git a/test-chat-history-api.html b/test-chat-history-api.html new file mode 100644 index 0000000..1bb8380 --- /dev/null +++ b/test-chat-history-api.html @@ -0,0 +1,226 @@ + + + + + + 聊天记录API测试 + + + +

聊天记录API测试工具

+ +
+

认证信息

+
+ + +
+
+ + +
+ +
+ +
+

API测试

+ + + + + +
+ + +
+
+ +
+

测试结果

+
等待测试...
+
+ + + + diff --git a/test-chat-history-complete.js b/test-chat-history-complete.js new file mode 100644 index 0000000..8fd5004 --- /dev/null +++ b/test-chat-history-complete.js @@ -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(); + +})(); diff --git a/test-duplicate-message-fix.md b/test-duplicate-message-fix.md new file mode 100644 index 0000000..e8dfe86 --- /dev/null +++ b/test-duplicate-message-fix.md @@ -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连接正常工作 diff --git a/test-message-api.html b/test-message-api.html new file mode 100644 index 0000000..7bf0158 --- /dev/null +++ b/test-message-api.html @@ -0,0 +1,130 @@ + + + + + + 消息API测试 + + + +

消息API测试

+ +
+

Token设置

+ + +
+ +
+

API测试

+ + + +
+ +
+

测试结果

+
等待测试...
+
+ + + + diff --git a/verify-chat-history-fix.js b/verify-chat-history-fix.js new file mode 100644 index 0000000..9dc2c04 --- /dev/null +++ b/verify-chat-history-fix.js @@ -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(); + +})(); diff --git a/verify-fix.sql b/verify-fix.sql new file mode 100644 index 0000000..87396a2 --- /dev/null +++ b/verify-fix.sql @@ -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; diff --git a/web/src/services/api.ts b/web/src/services/api.ts index 9fd982f..a3abd59 100644 --- a/web/src/services/api.ts +++ b/web/src/services/api.ts @@ -35,20 +35,39 @@ api.interceptors.request.use( api.interceptors.response.use( (response: AxiosResponse) => { const { data } = response - + // 检查业务状态码 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 }, (error) => { // 处理HTTP错误 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并跳转到登录页 @@ -72,7 +91,7 @@ api.interceptors.response.use( } else { console.error('Request setup error:', error.message) } - + return Promise.reject(error) } ) @@ -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) => diff --git a/web/src/stores/chat.ts b/web/src/stores/chat.ts index f1fe427..1a13bdf 100644 --- a/web/src/stores/chat.ts +++ b/web/src/stores/chat.ts @@ -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', '发送失败') diff --git a/web/src/views/Chat/index.vue b/web/src/views/Chat/index.vue index b7eec02..ea7309e 100644 --- a/web/src/views/Chat/index.vue +++ b/web/src/views/Chat/index.vue @@ -80,7 +80,7 @@ :class="{ 'user-message': message.type === 'user' }" >
-
+
@@ -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, '条') }