新增聊天历史表

This commit is contained in:
启航 2026-03-10 16:01:20 +08:00
parent fc0aa3d6da
commit 42eb1c21e2
11 changed files with 363 additions and 47 deletions

View File

@ -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();
}
}

View File

@ -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();

View File

@ -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();
}
}

View File

@ -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<>();

View File

@ -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);
}

View File

@ -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>

View File

@ -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聊天历史表';

View File

@ -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;
}

View File

@ -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);
}

View File

@ -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;
}
}

View File

@ -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;