对话逻辑修复
This commit is contained in:
@@ -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.在优化代码时必须要确保不能破坏已经实现的业务逻辑
|
||||
Generated
+1
-1
File diff suppressed because one or more lines are too long
Generated
+1
@@ -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" />
|
||||
|
||||
Generated
+2
@@ -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" />
|
||||
|
||||
Generated
+1
@@ -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;
|
||||
|
||||
@@ -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";
|
||||
}
|
||||
|
||||
|
||||
+7
-1
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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级别的调试
|
||||
@@ -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调用日志
|
||||
- 聊天记录正确加载和显示
|
||||
- 搜索功能正常工作
|
||||
@@ -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>
|
||||
@@ -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();
|
||||
|
||||
})();
|
||||
@@ -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连接正常工作
|
||||
@@ -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>
|
||||
@@ -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();
|
||||
|
||||
})();
|
||||
@@ -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;
|
||||
+27
-8
@@ -35,20 +35,39 @@ api.interceptors.request.use(
|
||||
api.interceptors.response.use(
|
||||
(response: AxiosResponse<ApiResponse>) => {
|
||||
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) =>
|
||||
|
||||
+6
-17
@@ -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', '发送失败')
|
||||
|
||||
@@ -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, '条')
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user