2991692032 4 weeks ago
parent 63ffb1bb2a
commit 9cc5739e58

@ -11,4 +11,9 @@
</profile>
</annotationProcessing>
</component>
<component name="JavacSettings">
<option name="ADDITIONAL_OPTIONS_OVERRIDE">
<module name="unilife-server" options="-parameters" />
</option>
</component>
</project>

@ -0,0 +1,137 @@
# Spring AI ChatMemory MySQL 集成说明
## 概述
本项目已成功集成Spring AI的ChatMemory功能使用MySQL作为持久化存储实现了真正的AI会话记忆功能。
## 主要改进
### 1. 删除的错误实现
- ❌ 删除了 `AiChatSession.java` - 自定义会话实体与Spring AI不兼容
- ❌ 删除了 `AiChatMessage.java` - 自定义消息实体与Spring AI不兼容
- ❌ 删除了基于TODO注释的伪实现
### 2. 新增的正确实现
- ✅ 添加了 `spring-ai-starter-model-chat-memory-repository-jdbc` 依赖
- ✅ 配置了基于MySQL的JdbcChatMemoryRepository
- ✅ 集成了MessageChatMemoryAdvisor实现自动会话记忆
- ✅ 使用Spring AI标准的ChatMemory接口
## 核心功能
### 会话记忆机制
- **自动记忆**: 每次对话自动保存到MySQL数据库
- **上下文保持**: 支持最多20条消息的上下文窗口
- **会话隔离**: 不同conversationId的会话完全隔离
- **持久化存储**: 重启服务后会话记忆不丢失
### 数据库表结构
```sql
CREATE TABLE SPRING_AI_CHAT_MEMORY (
conversation_id VARCHAR(36) NOT NULL, -- 会话ID
content TEXT NOT NULL, -- 消息内容
type VARCHAR(10) NOT NULL, -- 消息类型USER/ASSISTANT/SYSTEM/TOOL
timestamp TIMESTAMP NOT NULL, -- 时间戳
INDEX idx_conversation_id_timestamp (conversation_id, timestamp)
);
```
## API使用说明
### 1. 发送消息(带会话记忆)
```http
POST /ai/chat?prompt=你好&sessionId=my-session-001
```
- 自动将用户消息和AI回复保存到数据库
- 后续对话会自动加载历史上下文
### 2. 测试记忆功能
```http
GET /ai/test/memory?conversationId=test-session&message=我的名字是张三
GET /ai/test/memory?conversationId=test-session&message=我的名字是什么?
```
- 第二次询问AI会记住之前说过的名字
### 3. 查看会话历史
```http
GET /ai/test/history/test-session
```
### 4. 清空会话记忆
```http
DELETE /ai/test/memory/test-session
```
## 配置说明
### application.yml 关键配置
```yaml
spring:
ai:
chat:
memory:
repository:
jdbc:
initialize-schema: always # 自动初始化表结构
schema: classpath:schema-mysql.sql
```
### AiConfig.java 关键配置
```java
@Bean
public ChatMemory chatMemory(ChatMemoryRepository chatMemoryRepository) {
return MessageWindowChatMemory.builder()
.chatMemoryRepository(chatMemoryRepository)
.maxMessages(20) // 保留最近20条消息
.build();
}
@Bean
public ChatClient chatClient(OpenAiChatModel model, ChatMemory chatMemory) {
return ChatClient.builder(model)
.defaultAdvisors(
new SimpleLoggerAdvisor(),
MessageChatMemoryAdvisor.builder(chatMemory).build() // 自动记忆
)
.build();
}
```
## 使用最佳实践
### 1. 会话ID管理
- 前端生成唯一的sessionId`session_${timestamp}_${random}`
- 同一会话的所有消息使用相同的sessionId
- 新建会话时生成新的sessionId
### 2. 会话记忆策略
- 系统自动保留最近20条消息作为上下文
- 超过20条消息时旧消息会被自动清理仅从内存中数据库保留完整历史
- 可根据需要调整`maxMessages`参数
### 3. 数据库维护
- 定期清理过期的会话数据
- 监控`SPRING_AI_CHAT_MEMORY`表的大小
- 考虑为长期存储添加分区策略
## 注意事项
1. **会话列表功能限制**: Spring AI ChatMemory专注于消息存储不提供会话元数据管理。如需要会话列表、标题管理等功能建议维护单独的会话管理表。
2. **性能优化**: 对于高并发场景,考虑:
- 数据库连接池配置
- 消息内容压缩
- 定期清理策略
3. **错误处理**: 当数据库连接失败时ChatMemory会降级为内存模式重启后会话记忆将丢失。
## 测试验证
启动应用后,可以通过以下步骤验证功能:
1. 访问 Swagger UI: `http://localhost:8087/doc.html`
2. 找到 "AI测试接口" 分组
3. 使用测试接口验证会话记忆功能
4. 检查MySQL数据库中的`SPRING_AI_CHAT_MEMORY`表
这样就完成了Spring AI ChatMemory与MySQL的完整集成实现了真正的AI会话记忆功能

@ -0,0 +1,250 @@
# AI聊天会话历史功能设置指南
## 概述
本文档详细说明如何在您的UniLife项目中设置和使用AI聊天会话历史功能。该功能结合了Spring AI的ChatMemory会话记忆和MySQL数据库存储实现了完整的会话历史管理。
## 功能特性
### 🔥 核心功能
- **双重存储架构**: Spring AI ChatMemory短期记忆+ MySQL长期历史
- **自动同步**: ChatMemory消息自动同步到MySQL历史表
- **会话管理**: 支持会话列表、标题管理、删除等操作
- **匿名支持**: 支持匿名会话和用户会话
- **性能优化**: 历史表查询优化,支持分页
- **兼容性**: 完全兼容现有的Spring AI ChatMemory功能
### 📊 数据库表结构
1. **SPRING_AI_CHAT_MEMORY** - Spring AI原生记忆表
2. **ai_chat_sessions** - 会话元数据管理表
3. **ai_chat_messages_history** - 消息历史详情表
## 设置步骤
### 1. 数据库初始化
运行以下SQL脚本来创建必要的表结构
```sql
-- Spring AI Chat Memory表自动创建
CREATE TABLE IF NOT EXISTS SPRING_AI_CHAT_MEMORY (
conversation_id VARCHAR(36) NOT NULL COMMENT '会话ID',
content TEXT NOT NULL COMMENT '消息内容',
type VARCHAR(10) NOT NULL COMMENT '消息类型USER、ASSISTANT、SYSTEM、TOOL',
`timestamp` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '消息时间戳',
INDEX idx_conversation_id_timestamp (conversation_id, `timestamp`),
CONSTRAINT chk_message_type CHECK (type IN ('USER', 'ASSISTANT', 'SYSTEM', 'TOOL'))
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
-- 会话历史管理表
CREATE TABLE IF NOT EXISTS ai_chat_sessions (
id VARCHAR(64) PRIMARY KEY COMMENT '会话ID前端生成',
user_id BIGINT NULL COMMENT '用户ID可选支持匿名会话',
title VARCHAR(200) NOT NULL DEFAULT '新对话' COMMENT '会话标题',
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
last_message_time TIMESTAMP NULL COMMENT '最后消息时间',
message_count INT NOT NULL DEFAULT 0 COMMENT '消息总数',
INDEX idx_user_id (user_id),
INDEX idx_created_at (created_at),
INDEX idx_updated_at (updated_at)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
-- 会话消息历史详情表
CREATE TABLE IF NOT EXISTS ai_chat_messages_history (
id BIGINT AUTO_INCREMENT PRIMARY KEY COMMENT '消息历史ID',
session_id VARCHAR(64) NOT NULL COMMENT '会话ID',
conversation_id VARCHAR(36) NOT NULL COMMENT 'Spring AI会话ID',
role ENUM('user', 'assistant', 'system', 'tool') NOT NULL COMMENT '消息角色',
content TEXT NOT NULL COMMENT '消息内容',
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
INDEX idx_session_id (session_id),
INDEX idx_conversation_id (conversation_id),
INDEX idx_created_at (created_at),
FOREIGN KEY (session_id) REFERENCES ai_chat_sessions(id) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
```
### 2. 配置验证
确保`application.yml`中包含以下配置:
```yaml
spring:
ai:
chat:
memory:
repository:
jdbc:
initialize-schema: always
schema: classpath:schema-mysql.sql
datasource:
url: jdbc:mysql://localhost:3306/UniLife?allowPublicKeyRetrieval=true&useSSL=false&serverTimezone=UTC&characterEncoding=UTF-8
username: root
password: 123456
driver-class-name: com.mysql.cj.jdbc.Driver
```
### 3. Maven依赖检查
确保`pom.xml`中包含Spring AI JDBC ChatMemory依赖
```xml
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-starter-model-chat-memory-repository-jdbc</artifactId>
</dependency>
```
## 使用指南
### API接口说明
#### 1. 发送消息(带会话记忆)
```http
GET /ai/chat?prompt=你好&sessionId=session_001
```
#### 2. 获取会话列表
```http
GET /ai/sessions?page=1&size=20
```
#### 3. 获取会话消息历史
```http
GET /ai/sessions/{sessionId}/messages?page=1&size=50
```
#### 4. 创建会话
```http
POST /ai/sessions
{
"sessionId": "session_123456789",
"title": "关于Spring Boot的讨论"
}
```
#### 5. 更新会话标题
```http
PUT /ai/sessions/{sessionId}
{
"title": "新的会话标题"
}
```
#### 6. 删除会话
```http
DELETE /ai/sessions/{sessionId}
```
### 测试接口
系统提供了专门的测试接口来验证功能:
#### 1. 测试会话记忆
```http
GET /ai/test/memory?conversationId=test-001&message=我的名字是张三
GET /ai/test/memory?conversationId=test-001&message=我的名字是什么?
```
#### 2. 查看会话历史
```http
GET /ai/test/history/test-001
```
#### 3. 查看Spring AI原始记忆
```http
GET /ai/test/raw-memory/test-001
```
#### 4. 同步记忆到历史表
```http
POST /ai/test/sync/test-001
```
## 架构说明
### 数据流程
1. **用户发送消息** → ChatClient处理 → Spring AI ChatMemory自动存储
2. **消息处理完成** → 自动同步到MySQL历史表
3. **查询历史** → 优先从历史表查询(性能更好)
4. **会话管理** → 独立的会话元数据管理
### 关键组件
- **AiChatSessionHistoryService**: 会话历史管理服务
- **AiChatSessionMapper**: 会话数据访问层
- **AiChatMessageHistoryMapper**: 消息历史数据访问层
- **AiServiceImpl**: 整合Spring AI和历史管理的主服务
## 最佳实践
### 1. 会话ID管理
- 使用格式:`session_${timestamp}_${random}`
- 前端生成,确保唯一性
- 示例:`session_1703827200000_abc123`
### 2. 性能优化
- 定期清理过期会话数据
- 监控`SPRING_AI_CHAT_MEMORY`表大小
- 使用分页查询大量历史数据
### 3. 错误处理
- 数据库连接失败时ChatMemory会降级为内存模式
- 同步失败不影响主要聊天功能
- 提供手动同步接口
## 监控和维护
### 1. 数据库监控
```sql
-- 查看会话统计
SELECT COUNT(*) as total_sessions FROM ai_chat_sessions;
-- 查看消息统计
SELECT COUNT(*) as total_messages FROM ai_chat_messages_history;
-- 查看Spring AI记忆表大小
SELECT COUNT(*) as memory_records FROM SPRING_AI_CHAT_MEMORY;
```
### 2. 清理过期数据
```sql
-- 删除30天前的会话
DELETE FROM ai_chat_sessions
WHERE updated_at < DATE_SUB(NOW(), INTERVAL 30 DAY);
```
## 故障排除
### 常见问题
1. **表不存在错误**
- 检查`schema-mysql.sql`是否正确
- 确认`initialize-schema: always`配置
2. **同步失败**
- 检查数据库连接
- 查看日志中的错误信息
3. **会话列表为空**
- 确认会话已创建:`POST /ai/test/session`
- 检查数据库中`ai_chat_sessions`表
### 调试命令
```bash
# 查看应用日志
tail -f logs/unilife.log | grep -i "chat\|memory\|session"
# 检查数据库连接
mysql -u root -p -e "USE UniLife; SHOW TABLES LIKE '%chat%';"
```
## 总结
该实现提供了一个完整的AI聊天会话历史管理解决方案结合了Spring AI的强大记忆功能和MySQL的持久化存储。通过这种双重存储架构您既享受了Spring AI的智能记忆管理又获得了完整的会话历史功能。
启动应用后访问Swagger UI`http://localhost:8087/doc.html`查看所有可用的API接口并进行测试。

@ -0,0 +1,72 @@
# 简化的AI聊天会话历史方案
## 现状分析
您的项目已经有了完整的Spring AI ChatMemory + MySQL实现
- ✅ Spring AI自动将消息存储到 `SPRING_AI_CHAT_MEMORY`
- ✅ 自动会话记忆功能20条消息窗口
- ✅ 消息持久化到MySQL
## 缺失的功能
Spring AI ChatMemory 专注于消息存储,但缺少:
- ❌ 会话列表管理
- ❌ 会话标题管理
- ❌ 会话创建时间等元数据
## 建议的简化方案
### 1. 保留的表结构
只需要一个会话管理表:
```sql
-- 会话元数据管理表补充Spring AI ChatMemory
CREATE TABLE IF NOT EXISTS ai_chat_sessions (
id VARCHAR(64) PRIMARY KEY COMMENT '会话ID',
user_id BIGINT NULL COMMENT '用户ID可选',
title VARCHAR(200) NOT NULL DEFAULT '新对话' COMMENT '会话标题',
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
INDEX idx_user_id (user_id),
INDEX idx_created_at (created_at)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
```
### 2. 删除冗余的表
可以删除:
- `ai_chat_messages_history` 表(与 SPRING_AI_CHAT_MEMORY 重复)
### 3. 简化的服务层
只需要会话元数据管理,消息历史直接从 `SPRING_AI_CHAT_MEMORY` 查询:
```java
// 获取会话消息历史 - 直接查询Spring AI表
@Override
public Result<AiMessageHistoryVO> getSessionMessages(String sessionId, Integer page, Integer size) {
// 直接从Spring AI ChatMemory获取
List<Message> messages = chatMemory.get(sessionId);
// 转换为VO并返回
// ... 转换逻辑
}
```
### 4. 保留的核心功能
- 会话列表管理
- 会话标题管理
- 会话创建/删除
- 从 SPRING_AI_CHAT_MEMORY 表直接查询消息历史
## 实际需要的修改
1. **删除冗余表**: 移除 `ai_chat_messages_history`
2. **简化服务**: 移除消息同步逻辑直接使用Spring AI ChatMemory
3. **保留会话管理**: 只管理会话元数据
## 结论
您的担心是对的大部分功能确实是冗余的。Spring AI ChatMemory已经提供了强大的消息存储和记忆功能我们只需要补充会话元数据管理即可。

@ -189,6 +189,12 @@
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-starter-model-openai</artifactId>
</dependency>
<!-- Spring AI JDBC Chat Memory Repository for MySQL -->
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-starter-model-chat-memory-repository-jdbc</artifactId>
</dependency>
</dependencies>
<build>
@ -202,6 +208,7 @@
<source>${java.version}</source>
<target>${java.version}</target>
<encoding>UTF-8</encoding>
<parameters>true</parameters>
</configuration>
</plugin>
@ -210,6 +217,15 @@
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<version>${spring-boot.version}</version>
<configuration>
<!-- 确保Lombok注解处理器不会干扰参数名保留 -->
<excludes>
<exclude>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</exclude>
</excludes>
</configuration>
<executions>
<execution>
<goals>

@ -53,6 +53,10 @@ public class Result<T>{
return new Result<>(code, message, null);
}
public static <T> Result<T> error(String message) {
return new Result<>(500, message, null);
}
public static <T> Result<T> error(T data,String message){
return new Result<>(200,message,null);
}

@ -1,20 +1,40 @@
package com.unilife.config;
import org.springframework.ai.chat.client.ChatClient;
import org.springframework.ai.chat.client.advisor.MessageChatMemoryAdvisor;
import org.springframework.ai.chat.client.advisor.SimpleLoggerAdvisor;
import org.springframework.ai.chat.memory.ChatMemory;
import org.springframework.ai.chat.memory.ChatMemoryRepository;
import org.springframework.ai.chat.memory.MessageWindowChatMemory;
import org.springframework.ai.openai.OpenAiChatModel;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class AiConfig {
/**
* ChatMemory使JDBC
*/
@Bean
public ChatMemory chatMemory(ChatMemoryRepository chatMemoryRepository) {
return MessageWindowChatMemory.builder()
.chatMemoryRepository(chatMemoryRepository)
.maxMessages(20) // 保留最近20条消息作为上下文
.build();
}
/**
* ChatClientChat Memory
*/
@Bean
public ChatClient chatClient(OpenAiChatModel model) {
public ChatClient chatClient(OpenAiChatModel model, ChatMemory chatMemory) {
return ChatClient.builder(model)
.defaultAdvisors(new SimpleLoggerAdvisor())
.defaultAdvisors(
new SimpleLoggerAdvisor(),
MessageChatMemoryAdvisor.builder(chatMemory).build()
)
.build();
}
}

@ -24,7 +24,6 @@ public class WebMvcConfig implements WebMvcConfigurer {
"/users/register",
"/users/code",
"/users/login/code",
"/ai/**",
// 静态资源访问
"/api/files/**",

@ -0,0 +1,70 @@
package com.unilife.mapper;
import com.unilife.model.entity.AiChatSession;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import java.time.LocalDateTime;
import java.util.List;
/**
* AIMapper
*/
@Mapper
public interface AiChatSessionMapper {
/**
*
*/
int insert(AiChatSession session);
/**
* ID
*/
AiChatSession selectById(@Param("id") String id);
/**
* ID
*/
List<AiChatSession> selectByUserId(@Param("userId") Long userId,
@Param("offset") int offset,
@Param("limit") int limit);
/**
* ID
*/
List<AiChatSession> selectAnonymousSessions(@Param("offset") int offset,
@Param("limit") int limit);
/**
*
*/
long countByUserId(@Param("userId") Long userId);
/**
*
*/
long countAnonymousSessions();
/**
*
*/
int updateTitle(@Param("id") String id, @Param("title") String title);
/**
*
*/
int updateMessageInfo(@Param("id") String id,
@Param("lastMessageTime") LocalDateTime lastMessageTime,
@Param("messageCount") Integer messageCount);
/**
*
*/
int deleteById(@Param("id") String id);
/**
*
*/
int deleteExpiredSessions(@Param("expireTime") LocalDateTime expireTime);
}

@ -1,37 +0,0 @@
package com.unilife.model.entity;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.time.LocalDateTime;
@Data
@AllArgsConstructor
@NoArgsConstructor
public class AiChatMessage {
/**
* ID
*/
private String id;
/**
* ID
*/
private String sessionId;
/**
* (user, assistant, system)
*/
private String role;
/**
*
*/
private String content;
/**
*
*/
private LocalDateTime createdAt;
}

@ -6,6 +6,10 @@ import lombok.NoArgsConstructor;
import java.time.LocalDateTime;
/**
* AI
* Spring AI ChatMemory
*/
@Data
@AllArgsConstructor
@NoArgsConstructor
@ -16,7 +20,7 @@ public class AiChatSession {
private String id;
/**
* ID
* ID
*/
private Long userId;

@ -27,14 +27,5 @@ public class AiSessionVO {
*
*/
private String updatedAt;
/**
*
*/
private String lastMessageTime;
/**
*
*/
private Integer messageCount;
}

@ -0,0 +1,52 @@
package com.unilife.service;
import com.unilife.common.result.Result;
import com.unilife.model.entity.AiChatSession;
import com.unilife.model.vo.AiSessionListVO;
/**
* AI
* Spring AI ChatMemory
*/
public interface AiChatSessionHistoryService {
/**
*
* @param sessionId ID
* @param userId ID
* @param title
* @return
*/
Result<AiChatSession> createOrUpdateSession(String sessionId, Long userId, String title);
/**
*
* @param userId IDnull
* @param page
* @param size
* @return
*/
Result<AiSessionListVO> getSessionList(Long userId, Integer page, Integer size);
/**
*
* @param sessionId ID
* @return
*/
Result<AiChatSession> getSessionDetail(String sessionId);
/**
*
* @param sessionId ID
* @param title
* @return
*/
Result<Void> updateSessionTitle(String sessionId, String title);
/**
* Spring AI ChatMemory
* @param sessionId ID
* @return
*/
Result<Void> deleteSession(String sessionId);
}

@ -0,0 +1,169 @@
package com.unilife.service.impl;
import com.unilife.common.result.Result;
import com.unilife.mapper.AiChatSessionMapper;
import com.unilife.model.entity.AiChatSession;
import com.unilife.model.vo.AiSessionListVO;
import com.unilife.model.vo.AiSessionVO;
import com.unilife.service.AiChatSessionHistoryService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.List;
import java.util.stream.Collectors;
/**
* AI
* Spring AI ChatMemory
*/
@Service
@Slf4j
public class AiChatSessionHistoryServiceImpl implements AiChatSessionHistoryService {
@Autowired
private AiChatSessionMapper sessionMapper;
private static final DateTimeFormatter FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
@Override
@Transactional
public Result<AiChatSession> createOrUpdateSession(String sessionId, Long userId, String title) {
log.info("创建或更新会话: sessionId={}, userId={}, title={}", sessionId, userId, title);
try {
// 检查会话是否已存在
AiChatSession existingSession = sessionMapper.selectById(sessionId);
if (existingSession != null) {
// 更新现有会话
if (title != null && !title.equals(existingSession.getTitle())) {
sessionMapper.updateTitle(sessionId, title);
existingSession.setTitle(title);
existingSession.setUpdatedAt(LocalDateTime.now());
}
return Result.success(existingSession);
} else {
// 创建新会话
AiChatSession newSession = new AiChatSession();
newSession.setId(sessionId);
newSession.setUserId(userId);
newSession.setTitle(title != null ? title : "新对话");
newSession.setCreatedAt(LocalDateTime.now());
newSession.setUpdatedAt(LocalDateTime.now());
sessionMapper.insert(newSession);
log.info("成功创建新会话: {}", sessionId);
return Result.success(newSession);
}
} catch (Exception e) {
log.error("创建或更新会话失败: {}", e.getMessage(), e);
return Result.error("会话操作失败");
}
}
@Override
public Result<AiSessionListVO> getSessionList(Long userId, Integer page, Integer size) {
log.info("获取会话列表: userId={}, page={}, size={}", userId, page, size);
try {
int offset = (page - 1) * size;
List<AiChatSession> sessions;
long total;
if (userId != null) {
// 查询用户会话
sessions = sessionMapper.selectByUserId(userId, offset, size);
total = sessionMapper.countByUserId(userId);
} else {
// 查询匿名会话
sessions = sessionMapper.selectAnonymousSessions(offset, size);
total = sessionMapper.countAnonymousSessions();
}
// 转换为VO
List<AiSessionVO> sessionVOs = sessions.stream()
.map(this::convertToSessionVO)
.collect(Collectors.toList());
AiSessionListVO result = new AiSessionListVO();
result.setSessions(sessionVOs);
result.setTotal(total);
return Result.success(result);
} catch (Exception e) {
log.error("获取会话列表失败: {}", e.getMessage(), e);
return Result.error("获取会话列表失败");
}
}
@Override
public Result<AiChatSession> getSessionDetail(String sessionId) {
log.info("获取会话详细信息: {}", sessionId);
try {
AiChatSession session = sessionMapper.selectById(sessionId);
if (session == null) {
return Result.error("会话不存在");
}
return Result.success(session);
} catch (Exception e) {
log.error("获取会话详细信息失败: {}", e.getMessage(), e);
return Result.error("获取会话信息失败");
}
}
@Override
@Transactional
public Result<Void> updateSessionTitle(String sessionId, String title) {
log.info("更新会话标题: sessionId={}, title={}", sessionId, title);
try {
int updated = sessionMapper.updateTitle(sessionId, title);
if (updated > 0) {
return Result.success();
} else {
return Result.error("会话不存在或更新失败");
}
} catch (Exception e) {
log.error("更新会话标题失败: {}", e.getMessage(), e);
return Result.error("更新会话标题失败");
}
}
@Override
@Transactional
public Result<Void> deleteSession(String sessionId) {
log.info("删除会话: {}", sessionId);
try {
// 只删除会话元数据Spring AI ChatMemory中的消息由AiServiceImpl处理
int deleted = sessionMapper.deleteById(sessionId);
if (deleted > 0) {
log.info("成功删除会话: {}", sessionId);
return Result.success();
} else {
return Result.error("会话不存在");
}
} catch (Exception e) {
log.error("删除会话失败: {}", e.getMessage(), e);
return Result.error("删除会话失败");
}
}
/**
* SessionVO
*/
private AiSessionVO convertToSessionVO(AiChatSession session) {
AiSessionVO vo = new AiSessionVO();
vo.setId(session.getId());
vo.setTitle(session.getTitle());
vo.setCreatedAt(session.getCreatedAt() != null ? session.getCreatedAt().format(FORMATTER) : null);
vo.setUpdatedAt(session.getUpdatedAt() != null ? session.getUpdatedAt().format(FORMATTER) : null);
return vo;
}
}

@ -8,13 +8,20 @@ import com.unilife.model.vo.AiCreateSessionVO;
import com.unilife.model.vo.AiMessageHistoryVO;
import com.unilife.model.vo.AiSessionListVO;
import com.unilife.service.AiService;
import com.unilife.service.AiChatSessionHistoryService;
import com.unilife.utils.BaseContext;
import lombok.extern.slf4j.Slf4j;
import org.springframework.ai.chat.client.ChatClient;
import org.springframework.ai.chat.memory.ChatMemory;
import org.springframework.ai.chat.messages.Message;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import reactor.core.publisher.Flux;
import java.time.format.DateTimeFormatter;
import java.util.ArrayList;
import java.util.List;
import java.util.stream.Collectors;
@Service
@Slf4j
@ -22,77 +29,149 @@ public class AiServiceImpl implements AiService {
@Autowired
private ChatClient chatClient;
@Autowired
private ChatMemory chatMemory;
@Autowired
private AiChatSessionHistoryService sessionHistoryService;
@Override
public Flux<String> sendMessage(AiSendMessageDTO sendMessageDTO) {
log.info("发送消息给AI: {}", sendMessageDTO.getMessage());
log.info("发送消息给AI: {}, 会话ID: {}", sendMessageDTO.getMessage(), sendMessageDTO.getSessionId());
String sessionId = sendMessageDTO.getSessionId();
if (sessionId == null || sessionId.trim().isEmpty()) {
// 如果没有提供会话ID生成一个新的
sessionId = "session_" + System.currentTimeMillis();
log.info("生成新的会话ID: {}", sessionId);
}
// 确保会话元数据存在
sessionHistoryService.createOrUpdateSession(sessionId, BaseContext.getId(), "新对话");
// 使用ChatClient的流式响应
return chatClient.prompt(sendMessageDTO.getMessage())
// 使用ChatClient的流式响应Spring AI会自动处理记忆
return chatClient.prompt()
.user(sendMessageDTO.getMessage())
.advisors(a -> a.param(ChatMemory.CONVERSATION_ID, sendMessageDTO.getSessionId()))
.stream()
.content();
}
@Override
public Result<AiSessionListVO> getSessionList(Integer page, Integer size) {
log.info("获取会话列表,页码: {}, 每页大小: {}", page, size);
log.info("获取会话列表: page={}, size={}", page, size);
// TODO: 实现从数据库获取会话列表的逻辑
AiSessionListVO sessionList = new AiSessionListVO();
sessionList.setSessions(new ArrayList<>());
sessionList.setTotal(0L);
return Result.success(sessionList);
// 使用会话历史服务获取会话列表(支持匿名会话)
return sessionHistoryService.getSessionList(null, page, size);
}
@Override
public Result<AiMessageHistoryVO> getSessionMessages(String sessionId, Integer page, Integer size) {
log.info("获取会话消息历史会话ID: {}, 页码: {}, 每页大小: {}", sessionId, page, size);
// TODO: 实现从数据库获取消息历史的逻辑
AiMessageHistoryVO messageHistory = new AiMessageHistoryVO();
messageHistory.setMessages(new ArrayList<>());
messageHistory.setTotal(0L);
log.info("获取会话消息历史会话ID: {}", sessionId);
return Result.success(messageHistory);
try {
// 直接从Spring AI ChatMemory获取消息历史
List<Message> messages = chatMemory.get(sessionId);
AiMessageHistoryVO messageHistory = new AiMessageHistoryVO();
if (messages != null && !messages.isEmpty()) {
// 转换Message为VO对象
List<com.unilife.model.vo.AiMessageVO> messageVOs = messages.stream()
.map(message -> {
com.unilife.model.vo.AiMessageVO vo = new com.unilife.model.vo.AiMessageVO();
vo.setId(String.valueOf(System.currentTimeMillis() + Math.random()));
vo.setRole(message.getMessageType().getValue().toLowerCase());
vo.setContent(message.getText());
vo.setTimestamp(java.time.LocalDateTime.now().format(DateTimeFormatter.ISO_LOCAL_DATE_TIME));
return vo;
})
.collect(Collectors.toList());
messageHistory.setMessages(messageVOs);
messageHistory.setTotal((long) messageVOs.size());
} else {
messageHistory.setMessages(new ArrayList<>());
messageHistory.setTotal(0L);
}
return Result.success(messageHistory);
} catch (Exception e) {
log.error("获取会话消息历史失败: {}", e.getMessage(), e);
return Result.error("获取消息历史失败");
}
}
@Override
public Result<AiCreateSessionVO> createSession(AiCreateSessionDTO createSessionDTO) {
log.info("创建聊天会话: {}", createSessionDTO.getSessionId());
// TODO: 实现在数据库中创建会话的逻辑
AiCreateSessionVO response = new AiCreateSessionVO();
response.setSessionId(createSessionDTO.getSessionId());
response.setTitle(createSessionDTO.getTitle() != null ? createSessionDTO.getTitle() : "新对话");
return Result.success(response);
try {
// 使用会话历史服务创建会话元数据
Result<?> result = sessionHistoryService.createOrUpdateSession(
createSessionDTO.getSessionId(),
BaseContext.getId(), // 暂时支持匿名会话
createSessionDTO.getTitle()
);
if (result.getCode() == 200) {
AiCreateSessionVO response = new AiCreateSessionVO();
response.setSessionId(createSessionDTO.getSessionId());
response.setTitle(createSessionDTO.getTitle() != null ? createSessionDTO.getTitle() : "新对话");
return Result.success(response);
} else {
return Result.error(result.getMessage());
}
} catch (Exception e) {
log.error("创建会话失败: {}", e.getMessage(), e);
return Result.error("创建会话失败");
}
}
@Override
public Result<Void> updateSessionTitle(String sessionId, AiUpdateSessionDTO updateSessionDTO) {
log.info("更新会话标题会话ID: {}, 新标题: {}", sessionId, updateSessionDTO.getTitle());
log.info("更新会话标题: sessionId={}, title={}", sessionId, updateSessionDTO.getTitle());
// TODO: 实现在数据库中更新会话标题的逻辑
return Result.success();
// 使用会话历史服务更新标题
return sessionHistoryService.updateSessionTitle(sessionId, updateSessionDTO.getTitle());
}
@Override
public Result<Void> clearSessionMessages(String sessionId) {
log.info("清空会话消息会话ID: {}", sessionId);
// TODO: 实现在数据库中清空会话消息的逻辑
return Result.success();
try {
// 清空Spring AI ChatMemory中的消息
chatMemory.clear(sessionId);
log.info("成功清空会话 {} 的消息", sessionId);
return Result.success();
} catch (Exception e) {
log.error("清空会话消息失败: {}", e.getMessage(), e);
return Result.error("清空会话消息失败");
}
}
@Override
public Result<Void> deleteSession(String sessionId) {
log.info("删除会话会话ID: {}", sessionId);
// TODO: 实现在数据库中删除会话的逻辑
return Result.success();
try {
// 删除Spring AI ChatMemory中的消息
chatMemory.clear(sessionId);
// 删除会话元数据
Result<Void> result = sessionHistoryService.deleteSession(sessionId);
if (result.getCode() == 200) {
log.info("成功删除会话 {}", sessionId);
return Result.success();
} else {
return result;
}
} catch (Exception e) {
log.error("删除会话失败: {}", e.getMessage(), e);
return Result.error("删除会话失败");
}
}
}

@ -12,6 +12,12 @@ spring:
options:
model: text-embedding-v3
dimensions: 1024
chat:
memory:
repository:
jdbc:
initialize-schema: always # 自动初始化表结构
schema: classpath:schema-mysql.sql
datasource:
url: jdbc:mysql://localhost:3306/UniLife?allowPublicKeyRetrieval=true&useSSL=false&serverTimezone=UTC&characterEncoding=UTF-8
username: root

@ -0,0 +1,87 @@
<?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="com.unilife.mapper.AiChatSessionMapper">
<!-- 结果映射 -->
<resultMap id="AiChatSessionResultMap" type="com.unilife.model.entity.AiChatSession">
<id column="id" property="id"/>
<result column="user_id" property="userId"/>
<result column="title" property="title"/>
<result column="created_at" property="createdAt"/>
<result column="updated_at" property="updatedAt"/>
</resultMap>
<!-- 插入会话 -->
<insert id="insert" parameterType="com.unilife.model.entity.AiChatSession">
INSERT INTO ai_chat_sessions (
id, user_id, title, created_at, updated_at
) VALUES (
#{id}, #{userId}, #{title}, #{createdAt}, #{updatedAt}
)
</insert>
<!-- 根据ID查询会话 -->
<select id="selectById" resultMap="AiChatSessionResultMap">
SELECT id, user_id, title, created_at, updated_at
FROM ai_chat_sessions
WHERE id = #{id}
</select>
<!-- 根据用户ID分页查询会话列表 -->
<select id="selectByUserId" resultMap="AiChatSessionResultMap">
SELECT id, user_id, title, created_at, updated_at
FROM ai_chat_sessions
WHERE user_id = #{userId}
ORDER BY updated_at DESC
LIMIT #{offset}, #{limit}
</select>
<!-- 查询匿名会话列表 -->
<select id="selectAnonymousSessions" resultMap="AiChatSessionResultMap">
SELECT id, user_id, title, created_at, updated_at
FROM ai_chat_sessions
WHERE user_id IS NULL
ORDER BY updated_at DESC
LIMIT #{offset}, #{limit}
</select>
<!-- 统计用户会话总数 -->
<select id="countByUserId" resultType="long">
SELECT COUNT(*)
FROM ai_chat_sessions
WHERE user_id = #{userId}
</select>
<!-- 统计匿名会话总数 -->
<select id="countAnonymousSessions" resultType="long">
SELECT COUNT(*)
FROM ai_chat_sessions
WHERE user_id IS NULL
</select>
<!-- 更新会话标题 -->
<update id="updateTitle">
UPDATE ai_chat_sessions
SET title = #{title}, updated_at = CURRENT_TIMESTAMP
WHERE id = #{id}
</update>
<!-- 更新会话的最后消息时间和消息数量(简化方案中保留方法但不使用) -->
<update id="updateMessageInfo">
UPDATE ai_chat_sessions
SET updated_at = CURRENT_TIMESTAMP
WHERE id = #{id}
</update>
<!-- 删除会话 -->
<delete id="deleteById">
DELETE FROM ai_chat_sessions WHERE id = #{id}
</delete>
<!-- 批量删除过期会话 -->
<delete id="deleteExpiredSessions">
DELETE FROM ai_chat_sessions
WHERE updated_at &lt; #{expireTime}
</delete>
</mapper>

@ -0,0 +1,24 @@
-- Spring AI Chat Memory MySQL Schema
-- 此表用于存储AI聊天会话的消息记忆
CREATE TABLE IF NOT EXISTS SPRING_AI_CHAT_MEMORY (
conversation_id VARCHAR(36) NOT NULL COMMENT '会话ID',
content TEXT NOT NULL COMMENT '消息内容',
type VARCHAR(10) NOT NULL COMMENT '消息类型USER、ASSISTANT、SYSTEM、TOOL',
`timestamp` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '消息时间戳',
INDEX idx_conversation_id_timestamp (conversation_id, `timestamp`),
CONSTRAINT chk_message_type CHECK (type IN ('USER', 'ASSISTANT', 'SYSTEM', 'TOOL'))
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='Spring AI聊天记忆表';
-- 会话元数据管理表补充Spring AI ChatMemory功能
-- 只管理会话的元数据信息消息存储由Spring AI ChatMemory处理
CREATE TABLE IF NOT EXISTS ai_chat_sessions (
id VARCHAR(64) PRIMARY KEY COMMENT '会话ID前端生成',
user_id BIGINT NULL COMMENT '用户ID可选支持匿名会话',
title VARCHAR(200) NOT NULL DEFAULT '新对话' COMMENT '会话标题',
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
INDEX idx_user_id (user_id),
INDEX idx_created_at (created_at),
INDEX idx_updated_at (updated_at)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='AI聊天会话元数据表';
Loading…
Cancel
Save