新增聊天历史表
This commit is contained in:
parent
fc0aa3d6da
commit
42eb1c21e2
|
|
@ -1,29 +0,0 @@
|
|||
package cn.qihangerp.erp.config;
|
||||
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import cn.qihangerp.erp.serviceImpl.ConversationHistoryManager;
|
||||
import cn.qihangerp.erp.serviceImpl.SessionManager;
|
||||
|
||||
/**
|
||||
* AI相关配置类
|
||||
*/
|
||||
@Configuration
|
||||
public class AiConfig {
|
||||
|
||||
/**
|
||||
* 会话管理服务Bean
|
||||
*/
|
||||
@Bean
|
||||
public SessionManager sessionManager() {
|
||||
return new SessionManager();
|
||||
}
|
||||
|
||||
/**
|
||||
* 对话历史管理服务Bean
|
||||
*/
|
||||
@Bean
|
||||
public ConversationHistoryManager conversationHistoryManager() {
|
||||
return new ConversationHistoryManager();
|
||||
}
|
||||
}
|
||||
|
|
@ -123,7 +123,7 @@ public class SseController {
|
|||
log.info("用户 {} 的会话ID: {}", userId, sessionId);
|
||||
|
||||
// 添加用户消息到对话历史
|
||||
conversationHistoryManager.addMessage(sessionId, "user", message);
|
||||
conversationHistoryManager.addMessage(userId, sessionId, "user", message);
|
||||
}
|
||||
|
||||
// 获取对话历史
|
||||
|
|
@ -138,7 +138,7 @@ public class SseController {
|
|||
|
||||
// 如果有会话ID,添加AI回复到对话历史
|
||||
if (sessionId != null) {
|
||||
conversationHistoryManager.addMessage(sessionId, "assistant", response);
|
||||
conversationHistoryManager.addMessage(userId, sessionId, "assistant", response);
|
||||
}
|
||||
|
||||
// 检查响应是否已经是JSON格式(以{开头)
|
||||
|
|
@ -160,6 +160,16 @@ public class SseController {
|
|||
return "消息发送成功";
|
||||
} catch (Exception e) {
|
||||
log.error("消息处理失败: {}", e.getMessage());
|
||||
try {
|
||||
// 发送错误信息到前端
|
||||
String errorMessage = e.getMessage();
|
||||
String jsonError = String.format("{\"error\": \"%s\"}", errorMessage.replace("\"", "\\\"").replace("\n", "\\n"));
|
||||
emitter.send(SseEmitter.event()
|
||||
.name("error")
|
||||
.data(jsonError));
|
||||
} catch (IOException ex) {
|
||||
log.error("发送错误信息失败: {}", ex.getMessage());
|
||||
}
|
||||
emitters.remove(clientId);
|
||||
clientUserIdMap.remove(clientId);
|
||||
return "消息发送失败: " + e.getMessage();
|
||||
|
|
|
|||
|
|
@ -1,17 +1,22 @@
|
|||
package cn.qihangerp.erp.serviceImpl;
|
||||
|
||||
import cn.qihangerp.service.IAiConversationHistoryService;
|
||||
import cn.qihangerp.model.entity.AiConversationHistory;
|
||||
import lombok.AllArgsConstructor;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.atomic.AtomicLong;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* 对话历史管理服务,用于保存和管理用户的对话历史
|
||||
*/
|
||||
@Component
|
||||
@AllArgsConstructor
|
||||
public class ConversationHistoryManager {
|
||||
private static final Map<String, List<Message>> sessionHistoryMap = new ConcurrentHashMap<>();
|
||||
private static final AtomicLong messageIdCounter = new AtomicLong(0);
|
||||
|
||||
private final IAiConversationHistoryService aiConversationHistoryService;
|
||||
|
||||
/**
|
||||
* 消息实体类
|
||||
|
|
@ -22,8 +27,15 @@ public class ConversationHistoryManager {
|
|||
private String content;
|
||||
private long timestamp;
|
||||
|
||||
public Message(long id, String role, String content, long timestamp) {
|
||||
this.id = id;
|
||||
this.role = role;
|
||||
this.content = content;
|
||||
this.timestamp = timestamp;
|
||||
}
|
||||
|
||||
public Message(String role, String content) {
|
||||
this.id = messageIdCounter.incrementAndGet();
|
||||
this.id = 0;
|
||||
this.role = role;
|
||||
this.content = content;
|
||||
this.timestamp = System.currentTimeMillis();
|
||||
|
|
@ -56,8 +68,22 @@ public class ConversationHistoryManager {
|
|||
if (sessionId == null) {
|
||||
return;
|
||||
}
|
||||
sessionHistoryMap.computeIfAbsent(sessionId, k -> new ArrayList<>())
|
||||
.add(new Message(role, content));
|
||||
// 这里简化处理,暂时不传入userId,实际使用时需要从会话中获取
|
||||
aiConversationHistoryService.saveMessage(0L, sessionId, role, content);
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加消息到对话历史(带用户ID)
|
||||
* @param userId 用户ID
|
||||
* @param sessionId 会话ID
|
||||
* @param role 角色
|
||||
* @param content 消息内容
|
||||
*/
|
||||
public void addMessage(Long userId, String sessionId, String role, String content) {
|
||||
if (sessionId == null) {
|
||||
return;
|
||||
}
|
||||
aiConversationHistoryService.saveMessage(userId, sessionId, role, content);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -69,7 +95,10 @@ public class ConversationHistoryManager {
|
|||
if (sessionId == null) {
|
||||
return new ArrayList<>();
|
||||
}
|
||||
return sessionHistoryMap.getOrDefault(sessionId, new ArrayList<>());
|
||||
List<AiConversationHistory> historyList = aiConversationHistoryService.getBySessionId(sessionId);
|
||||
return historyList.stream()
|
||||
.map(history -> new Message(history.getId(), history.getRole(), history.getContent(), history.getTimestamp()))
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -82,9 +111,13 @@ public class ConversationHistoryManager {
|
|||
if (sessionId == null) {
|
||||
return new ArrayList<>();
|
||||
}
|
||||
List<Message> history = sessionHistoryMap.getOrDefault(sessionId, new ArrayList<>());
|
||||
int startIndex = Math.max(0, history.size() - limit);
|
||||
return history.subList(startIndex, history.size());
|
||||
List<AiConversationHistory> historyList = aiConversationHistoryService.getRecentBySessionId(sessionId, limit);
|
||||
// 反转列表,使时间戳从早到晚排序
|
||||
List<Message> messages = historyList.stream()
|
||||
.map(history -> new Message(history.getId(), history.getRole(), history.getContent(), history.getTimestamp()))
|
||||
.collect(Collectors.toList());
|
||||
java.util.Collections.reverse(messages);
|
||||
return messages;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -93,7 +126,7 @@ public class ConversationHistoryManager {
|
|||
*/
|
||||
public void clearConversationHistory(String sessionId) {
|
||||
if (sessionId != null) {
|
||||
sessionHistoryMap.remove(sessionId);
|
||||
aiConversationHistoryService.deleteBySessionId(sessionId);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -106,7 +139,7 @@ public class ConversationHistoryManager {
|
|||
if (sessionId == null) {
|
||||
return 0;
|
||||
}
|
||||
List<Message> history = sessionHistoryMap.get(sessionId);
|
||||
return history != null ? history.size() : 0;
|
||||
List<AiConversationHistory> historyList = aiConversationHistoryService.getBySessionId(sessionId);
|
||||
return historyList.size();
|
||||
}
|
||||
}
|
||||
|
|
@ -3,10 +3,12 @@ package cn.qihangerp.erp.serviceImpl;
|
|||
import java.util.Map;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.UUID;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
/**
|
||||
* 会话管理服务,用于管理用户的会话ID
|
||||
*/
|
||||
@Component
|
||||
public class SessionManager {
|
||||
private static final Map<Long, String> userIdSessionMap = new ConcurrentHashMap<>();
|
||||
private static final Map<String, Long> sessionUserIdMap = new ConcurrentHashMap<>();
|
||||
|
|
|
|||
|
|
@ -0,0 +1,43 @@
|
|||
package cn.qihangerp.mapper;
|
||||
|
||||
import cn.qihangerp.model.entity.AiConversationHistory;
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
import org.apache.ibatis.annotations.Param;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* AI聊天历史Mapper
|
||||
*/
|
||||
@Mapper
|
||||
public interface AiConversationHistoryMapper extends BaseMapper<AiConversationHistory> {
|
||||
/**
|
||||
* 根据会话ID获取聊天历史
|
||||
* @param sessionId 会话ID
|
||||
* @return 聊天历史列表
|
||||
*/
|
||||
List<AiConversationHistory> selectBySessionId(@Param("sessionId") String sessionId);
|
||||
|
||||
/**
|
||||
* 根据会话ID获取最近的聊天历史
|
||||
* @param sessionId 会话ID
|
||||
* @param limit 限制数量
|
||||
* @return 最近的聊天历史列表
|
||||
*/
|
||||
List<AiConversationHistory> selectRecentBySessionId(@Param("sessionId") String sessionId, @Param("limit") int limit);
|
||||
|
||||
/**
|
||||
* 根据用户ID获取会话ID列表
|
||||
* @param userId 用户ID
|
||||
* @return 会话ID列表
|
||||
*/
|
||||
List<String> selectSessionIdsByUserId(@Param("userId") Long userId);
|
||||
|
||||
/**
|
||||
* 根据会话ID删除聊天历史
|
||||
* @param sessionId 会话ID
|
||||
* @return 删除的记录数
|
||||
*/
|
||||
int deleteBySessionId(@Param("sessionId") String sessionId);
|
||||
}
|
||||
|
|
@ -0,0 +1,50 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE mapper
|
||||
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
||||
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
|
||||
<mapper namespace="cn.qihangerp.mapper.AiConversationHistoryMapper">
|
||||
|
||||
<resultMap id="BaseResultMap" type="cn.qihangerp.model.entity.AiConversationHistory">
|
||||
<id property="id" column="id" jdbcType="BIGINT"/>
|
||||
<result property="userId" column="user_id" jdbcType="BIGINT"/>
|
||||
<result property="sessionId" column="session_id" jdbcType="VARCHAR"/>
|
||||
<result property="role" column="role" jdbcType="VARCHAR"/>
|
||||
<result property="content" column="content" jdbcType="LONGVARCHAR"/>
|
||||
<result property="timestamp" column="timestamp" jdbcType="BIGINT"/>
|
||||
<result property="createTime" column="create_time" jdbcType="TIMESTAMP"/>
|
||||
<result property="updateTime" column="update_time" jdbcType="TIMESTAMP"/>
|
||||
</resultMap>
|
||||
|
||||
<sql id="Base_Column_List">
|
||||
id, user_id, session_id, role, content, timestamp, create_time, update_time
|
||||
</sql>
|
||||
|
||||
<select id="selectBySessionId" parameterType="String" resultMap="BaseResultMap">
|
||||
select
|
||||
<include refid="Base_Column_List"/>
|
||||
from ai_conversation_history
|
||||
where session_id = #{sessionId}
|
||||
order by timestamp asc
|
||||
</select>
|
||||
|
||||
<select id="selectRecentBySessionId" parameterType="map" resultMap="BaseResultMap">
|
||||
select
|
||||
<include refid="Base_Column_List"/>
|
||||
from ai_conversation_history
|
||||
where session_id = #{sessionId}
|
||||
order by timestamp desc
|
||||
limit #{limit}
|
||||
</select>
|
||||
|
||||
<select id="selectSessionIdsByUserId" parameterType="Long" resultType="String">
|
||||
select distinct session_id
|
||||
from ai_conversation_history
|
||||
where user_id = #{userId}
|
||||
order by create_time desc
|
||||
</select>
|
||||
|
||||
<delete id="deleteBySessionId" parameterType="String">
|
||||
delete from ai_conversation_history
|
||||
where session_id = #{sessionId}
|
||||
</delete>
|
||||
</mapper>
|
||||
|
|
@ -0,0 +1,14 @@
|
|||
CREATE TABLE `ai_conversation_history` (
|
||||
`id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '主键id,自增',
|
||||
`user_id` bigint(20) NOT NULL COMMENT '用户id',
|
||||
`session_id` varchar(36) NOT NULL COMMENT '会话id',
|
||||
`role` varchar(20) NOT NULL COMMENT '角色:user或assistant',
|
||||
`content` text NOT NULL COMMENT '消息内容',
|
||||
`timestamp` bigint(20) NOT NULL COMMENT '消息时间戳',
|
||||
`create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
|
||||
`update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
|
||||
PRIMARY KEY (`id`),
|
||||
KEY `idx_session_id` (`session_id`),
|
||||
KEY `idx_user_id` (`user_id`),
|
||||
KEY `idx_timestamp` (`timestamp`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='AI聊天历史表';
|
||||
|
|
@ -0,0 +1,62 @@
|
|||
package cn.qihangerp.model.entity;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.IdType;
|
||||
import com.baomidou.mybatisplus.annotation.TableField;
|
||||
import com.baomidou.mybatisplus.annotation.TableId;
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
import lombok.Data;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.Date;
|
||||
|
||||
/**
|
||||
* AI聊天历史表
|
||||
* @TableName ai_conversation_history
|
||||
*/
|
||||
@TableName(value = "ai_conversation_history")
|
||||
@Data
|
||||
public class AiConversationHistory implements Serializable {
|
||||
/**
|
||||
* 主键id,自增
|
||||
*/
|
||||
@TableId(type = IdType.AUTO)
|
||||
private Long id;
|
||||
|
||||
/**
|
||||
* 用户id
|
||||
*/
|
||||
private Long userId;
|
||||
|
||||
/**
|
||||
* 会话id
|
||||
*/
|
||||
private String sessionId;
|
||||
|
||||
/**
|
||||
* 角色:user或assistant
|
||||
*/
|
||||
private String role;
|
||||
|
||||
/**
|
||||
* 消息内容
|
||||
*/
|
||||
private String content;
|
||||
|
||||
/**
|
||||
* 消息时间戳
|
||||
*/
|
||||
private Long timestamp;
|
||||
|
||||
/**
|
||||
* 创建时间
|
||||
*/
|
||||
private Date createTime;
|
||||
|
||||
/**
|
||||
* 更新时间
|
||||
*/
|
||||
private Date updateTime;
|
||||
|
||||
@TableField(exist = false)
|
||||
private static final long serialVersionUID = 1L;
|
||||
}
|
||||
|
|
@ -0,0 +1,50 @@
|
|||
package cn.qihangerp.service;
|
||||
|
||||
import cn.qihangerp.model.entity.AiConversationHistory;
|
||||
import com.baomidou.mybatisplus.extension.service.IService;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* AI聊天历史Service
|
||||
*/
|
||||
public interface IAiConversationHistoryService extends IService<AiConversationHistory> {
|
||||
/**
|
||||
* 根据会话ID获取聊天历史
|
||||
* @param sessionId 会话ID
|
||||
* @return 聊天历史列表
|
||||
*/
|
||||
List<AiConversationHistory> getBySessionId(String sessionId);
|
||||
|
||||
/**
|
||||
* 根据会话ID获取最近的聊天历史
|
||||
* @param sessionId 会话ID
|
||||
* @param limit 限制数量
|
||||
* @return 最近的聊天历史列表
|
||||
*/
|
||||
List<AiConversationHistory> getRecentBySessionId(String sessionId, int limit);
|
||||
|
||||
/**
|
||||
* 根据用户ID获取会话ID列表
|
||||
* @param userId 用户ID
|
||||
* @return 会话ID列表
|
||||
*/
|
||||
List<String> getSessionIdsByUserId(Long userId);
|
||||
|
||||
/**
|
||||
* 根据会话ID删除聊天历史
|
||||
* @param sessionId 会话ID
|
||||
* @return 删除的记录数
|
||||
*/
|
||||
int deleteBySessionId(String sessionId);
|
||||
|
||||
/**
|
||||
* 保存聊天消息
|
||||
* @param userId 用户ID
|
||||
* @param sessionId 会话ID
|
||||
* @param role 角色
|
||||
* @param content 内容
|
||||
* @return 保存的消息
|
||||
*/
|
||||
AiConversationHistory saveMessage(Long userId, String sessionId, String role, String content);
|
||||
}
|
||||
|
|
@ -0,0 +1,55 @@
|
|||
package cn.qihangerp.service.impl;
|
||||
|
||||
import cn.qihangerp.mapper.AiConversationHistoryMapper;
|
||||
import cn.qihangerp.model.entity.AiConversationHistory;
|
||||
import cn.qihangerp.service.IAiConversationHistoryService;
|
||||
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
|
||||
import lombok.AllArgsConstructor;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* AI聊天历史Service实现
|
||||
*/
|
||||
@Service
|
||||
@AllArgsConstructor
|
||||
public class AiConversationHistoryServiceImpl extends ServiceImpl<AiConversationHistoryMapper, AiConversationHistory> implements IAiConversationHistoryService {
|
||||
|
||||
private final AiConversationHistoryMapper aiConversationHistoryMapper;
|
||||
|
||||
@Override
|
||||
public List<AiConversationHistory> getBySessionId(String sessionId) {
|
||||
return aiConversationHistoryMapper.selectBySessionId(sessionId);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<AiConversationHistory> getRecentBySessionId(String sessionId, int limit) {
|
||||
return aiConversationHistoryMapper.selectRecentBySessionId(sessionId, limit);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<String> getSessionIdsByUserId(Long userId) {
|
||||
return aiConversationHistoryMapper.selectSessionIdsByUserId(userId);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int deleteBySessionId(String sessionId) {
|
||||
return aiConversationHistoryMapper.deleteBySessionId(sessionId);
|
||||
}
|
||||
|
||||
@Override
|
||||
public AiConversationHistory saveMessage(Long userId, String sessionId, String role, String content) {
|
||||
AiConversationHistory history = new AiConversationHistory();
|
||||
history.setUserId(userId);
|
||||
history.setSessionId(sessionId);
|
||||
history.setRole(role);
|
||||
history.setContent(content);
|
||||
history.setTimestamp(System.currentTimeMillis());
|
||||
history.setCreateTime(new Date());
|
||||
history.setUpdateTime(new Date());
|
||||
save(history);
|
||||
return history;
|
||||
}
|
||||
}
|
||||
|
|
@ -269,7 +269,33 @@ export default {
|
|||
console.log('收到心跳:', event.data);
|
||||
});
|
||||
|
||||
// 监听错误
|
||||
// 监听后端发送的错误信息
|
||||
this.sse.addEventListener('error', (event) => {
|
||||
console.log('收到错误信息:', event.data);
|
||||
try {
|
||||
// 解析错误信息
|
||||
const errorData = JSON.parse(event.data);
|
||||
if (errorData.error) {
|
||||
// 移除正在思考的消息
|
||||
if (this.isLoading) {
|
||||
this.messages = this.messages.filter(msg => !msg.isLoading);
|
||||
this.isLoading = false;
|
||||
}
|
||||
// 显示错误信息
|
||||
this.messages.push({
|
||||
content: `错误: ${errorData.error}`,
|
||||
time: this.formatTime(new Date()),
|
||||
isMe: false,
|
||||
avatar: ''
|
||||
});
|
||||
this.scrollToBottom();
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('解析错误信息失败:', e);
|
||||
}
|
||||
});
|
||||
|
||||
// 监听连接错误
|
||||
this.sse.onerror = (error) => {
|
||||
console.error('SSE连接错误:', error);
|
||||
this.isSseConnected = false;
|
||||
|
|
|
|||
Loading…
Reference in New Issue