From 9cc5739e583c90a99d75c6fb196138695d030ea0 Mon Sep 17 00:00:00 2001 From: 2991692032 Date: Fri, 30 May 2025 21:27:10 +0800 Subject: [PATCH] tmp --- .idea/compiler.xml | 5 + unilife-server/AI_MEMORY_INTEGRATION.md | 137 ++++++++++ unilife-server/CHAT_HISTORY_SETUP.md | 250 ++++++++++++++++++ unilife-server/SIMPLIFIED_CHAT_HISTORY.md | 72 +++++ unilife-server/pom.xml | 16 ++ .../com/unilife/common/result/Result.java | 4 + .../java/com/unilife/config/AiConfig.java | 30 ++- .../java/com/unilife/config/WebMvcConfig.java | 1 - .../unilife/mapper/AiChatSessionMapper.java | 70 +++++ .../unilife/model/entity/AiChatMessage.java | 37 --- .../unilife/model/entity/AiChatSession.java | 6 +- .../com/unilife/model/vo/AiSessionVO.java | 11 +- .../service/AiChatSessionHistoryService.java | 52 ++++ .../impl/AiChatSessionHistoryServiceImpl.java | 169 ++++++++++++ .../unilife/service/impl/AiServiceImpl.java | 145 +++++++--- .../src/main/resources/application.yml | 6 + .../resources/mappers/AiChatSessionMapper.xml | 87 ++++++ .../src/main/resources/schema-mysql.sql | 24 ++ 18 files changed, 1035 insertions(+), 87 deletions(-) create mode 100644 unilife-server/AI_MEMORY_INTEGRATION.md create mode 100644 unilife-server/CHAT_HISTORY_SETUP.md create mode 100644 unilife-server/SIMPLIFIED_CHAT_HISTORY.md create mode 100644 unilife-server/src/main/java/com/unilife/mapper/AiChatSessionMapper.java delete mode 100644 unilife-server/src/main/java/com/unilife/model/entity/AiChatMessage.java create mode 100644 unilife-server/src/main/java/com/unilife/service/AiChatSessionHistoryService.java create mode 100644 unilife-server/src/main/java/com/unilife/service/impl/AiChatSessionHistoryServiceImpl.java create mode 100644 unilife-server/src/main/resources/mappers/AiChatSessionMapper.xml create mode 100644 unilife-server/src/main/resources/schema-mysql.sql diff --git a/.idea/compiler.xml b/.idea/compiler.xml index 5e89e00..665003a 100644 --- a/.idea/compiler.xml +++ b/.idea/compiler.xml @@ -11,4 +11,9 @@ + + + \ No newline at end of file diff --git a/unilife-server/AI_MEMORY_INTEGRATION.md b/unilife-server/AI_MEMORY_INTEGRATION.md new file mode 100644 index 0000000..3682b3d --- /dev/null +++ b/unilife-server/AI_MEMORY_INTEGRATION.md @@ -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会话记忆功能! \ No newline at end of file diff --git a/unilife-server/CHAT_HISTORY_SETUP.md b/unilife-server/CHAT_HISTORY_SETUP.md new file mode 100644 index 0000000..52460ea --- /dev/null +++ b/unilife-server/CHAT_HISTORY_SETUP.md @@ -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 + + org.springframework.ai + spring-ai-starter-model-chat-memory-repository-jdbc + +``` + +## 使用指南 + +### 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接口并进行测试。 \ No newline at end of file diff --git a/unilife-server/SIMPLIFIED_CHAT_HISTORY.md b/unilife-server/SIMPLIFIED_CHAT_HISTORY.md new file mode 100644 index 0000000..92520cf --- /dev/null +++ b/unilife-server/SIMPLIFIED_CHAT_HISTORY.md @@ -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 getSessionMessages(String sessionId, Integer page, Integer size) { + // 直接从Spring AI ChatMemory获取 + List messages = chatMemory.get(sessionId); + + // 转换为VO并返回 + // ... 转换逻辑 +} +``` + +### 4. 保留的核心功能 + +- 会话列表管理 +- 会话标题管理 +- 会话创建/删除 +- 从 SPRING_AI_CHAT_MEMORY 表直接查询消息历史 + +## 实际需要的修改 + +1. **删除冗余表**: 移除 `ai_chat_messages_history` +2. **简化服务**: 移除消息同步逻辑,直接使用Spring AI ChatMemory +3. **保留会话管理**: 只管理会话元数据 + +## 结论 + +您的担心是对的,大部分功能确实是冗余的。Spring AI ChatMemory已经提供了强大的消息存储和记忆功能,我们只需要补充会话元数据管理即可。 \ No newline at end of file diff --git a/unilife-server/pom.xml b/unilife-server/pom.xml index 77a66aa..b66ab5e 100644 --- a/unilife-server/pom.xml +++ b/unilife-server/pom.xml @@ -189,6 +189,12 @@ org.springframework.ai spring-ai-starter-model-openai + + + + org.springframework.ai + spring-ai-starter-model-chat-memory-repository-jdbc + @@ -202,6 +208,7 @@ ${java.version} ${java.version} UTF-8 + true @@ -210,6 +217,15 @@ org.springframework.boot spring-boot-maven-plugin ${spring-boot.version} + + + + + org.projectlombok + lombok + + + diff --git a/unilife-server/src/main/java/com/unilife/common/result/Result.java b/unilife-server/src/main/java/com/unilife/common/result/Result.java index 1e91ff2..cc21d68 100644 --- a/unilife-server/src/main/java/com/unilife/common/result/Result.java +++ b/unilife-server/src/main/java/com/unilife/common/result/Result.java @@ -53,6 +53,10 @@ public class Result{ return new Result<>(code, message, null); } + public static Result error(String message) { + return new Result<>(500, message, null); + } + public static Result error(T data,String message){ return new Result<>(200,message,null); } diff --git a/unilife-server/src/main/java/com/unilife/config/AiConfig.java b/unilife-server/src/main/java/com/unilife/config/AiConfig.java index 5444222..afc43a4 100644 --- a/unilife-server/src/main/java/com/unilife/config/AiConfig.java +++ b/unilife-server/src/main/java/com/unilife/config/AiConfig.java @@ -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(); + } + + /** + * 配置ChatClient,集成Chat 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(); } - - - } diff --git a/unilife-server/src/main/java/com/unilife/config/WebMvcConfig.java b/unilife-server/src/main/java/com/unilife/config/WebMvcConfig.java index 96b6916..2f3c8d9 100644 --- a/unilife-server/src/main/java/com/unilife/config/WebMvcConfig.java +++ b/unilife-server/src/main/java/com/unilife/config/WebMvcConfig.java @@ -24,7 +24,6 @@ public class WebMvcConfig implements WebMvcConfigurer { "/users/register", "/users/code", "/users/login/code", - "/ai/**", // 静态资源访问 "/api/files/**", diff --git a/unilife-server/src/main/java/com/unilife/mapper/AiChatSessionMapper.java b/unilife-server/src/main/java/com/unilife/mapper/AiChatSessionMapper.java new file mode 100644 index 0000000..fcc5f88 --- /dev/null +++ b/unilife-server/src/main/java/com/unilife/mapper/AiChatSessionMapper.java @@ -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; + +/** + * AI聊天会话Mapper + */ +@Mapper +public interface AiChatSessionMapper { + + /** + * 插入会话 + */ + int insert(AiChatSession session); + + /** + * 根据ID查询会话 + */ + AiChatSession selectById(@Param("id") String id); + + /** + * 根据用户ID分页查询会话列表 + */ + List selectByUserId(@Param("userId") Long userId, + @Param("offset") int offset, + @Param("limit") int limit); + + /** + * 查询匿名会话列表(用户ID为空) + */ + List 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); +} \ No newline at end of file diff --git a/unilife-server/src/main/java/com/unilife/model/entity/AiChatMessage.java b/unilife-server/src/main/java/com/unilife/model/entity/AiChatMessage.java deleted file mode 100644 index a492337..0000000 --- a/unilife-server/src/main/java/com/unilife/model/entity/AiChatMessage.java +++ /dev/null @@ -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; -} \ No newline at end of file diff --git a/unilife-server/src/main/java/com/unilife/model/entity/AiChatSession.java b/unilife-server/src/main/java/com/unilife/model/entity/AiChatSession.java index 1ffe0e2..a4826e5 100644 --- a/unilife-server/src/main/java/com/unilife/model/entity/AiChatSession.java +++ b/unilife-server/src/main/java/com/unilife/model/entity/AiChatSession.java @@ -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; diff --git a/unilife-server/src/main/java/com/unilife/model/vo/AiSessionVO.java b/unilife-server/src/main/java/com/unilife/model/vo/AiSessionVO.java index ceb8b1b..62317de 100644 --- a/unilife-server/src/main/java/com/unilife/model/vo/AiSessionVO.java +++ b/unilife-server/src/main/java/com/unilife/model/vo/AiSessionVO.java @@ -27,14 +27,5 @@ public class AiSessionVO { * 更新时间 */ private String updatedAt; - - /** - * 最后消息时间 - */ - private String lastMessageTime; - - /** - * 消息总数 - */ - private Integer messageCount; + } \ No newline at end of file diff --git a/unilife-server/src/main/java/com/unilife/service/AiChatSessionHistoryService.java b/unilife-server/src/main/java/com/unilife/service/AiChatSessionHistoryService.java new file mode 100644 index 0000000..f73fd6f --- /dev/null +++ b/unilife-server/src/main/java/com/unilife/service/AiChatSessionHistoryService.java @@ -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 createOrUpdateSession(String sessionId, Long userId, String title); + + /** + * 获取会话列表(支持用户会话和匿名会话) + * @param userId 用户ID(为null时查询匿名会话) + * @param page 页码 + * @param size 每页大小 + * @return 会话列表 + */ + Result getSessionList(Long userId, Integer page, Integer size); + + /** + * 获取会话详细信息 + * @param sessionId 会话ID + * @return 会话信息 + */ + Result getSessionDetail(String sessionId); + + /** + * 更新会话标题 + * @param sessionId 会话ID + * @param title 新标题 + * @return 更新结果 + */ + Result updateSessionTitle(String sessionId, String title); + + /** + * 删除会话(会话元数据和Spring AI ChatMemory中的消息) + * @param sessionId 会话ID + * @return 删除结果 + */ + Result deleteSession(String sessionId); +} \ No newline at end of file diff --git a/unilife-server/src/main/java/com/unilife/service/impl/AiChatSessionHistoryServiceImpl.java b/unilife-server/src/main/java/com/unilife/service/impl/AiChatSessionHistoryServiceImpl.java new file mode 100644 index 0000000..1931bfd --- /dev/null +++ b/unilife-server/src/main/java/com/unilife/service/impl/AiChatSessionHistoryServiceImpl.java @@ -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 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 getSessionList(Long userId, Integer page, Integer size) { + log.info("获取会话列表: userId={}, page={}, size={}", userId, page, size); + + try { + int offset = (page - 1) * size; + + List 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 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 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 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 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; + } +} \ No newline at end of file diff --git a/unilife-server/src/main/java/com/unilife/service/impl/AiServiceImpl.java b/unilife-server/src/main/java/com/unilife/service/impl/AiServiceImpl.java index fc97c86..7d31900 100644 --- a/unilife-server/src/main/java/com/unilife/service/impl/AiServiceImpl.java +++ b/unilife-server/src/main/java/com/unilife/service/impl/AiServiceImpl.java @@ -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 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 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 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 messages = chatMemory.get(sessionId); + + AiMessageHistoryVO messageHistory = new AiMessageHistoryVO(); + + if (messages != null && !messages.isEmpty()) { + // 转换Message为VO对象 + List 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 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 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 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 deleteSession(String sessionId) { log.info("删除会话,会话ID: {}", sessionId); - // TODO: 实现在数据库中删除会话的逻辑 - - return Result.success(); + try { + // 删除Spring AI ChatMemory中的消息 + chatMemory.clear(sessionId); + + // 删除会话元数据 + Result 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("删除会话失败"); + } } } diff --git a/unilife-server/src/main/resources/application.yml b/unilife-server/src/main/resources/application.yml index 8bbb2f9..a3951be 100644 --- a/unilife-server/src/main/resources/application.yml +++ b/unilife-server/src/main/resources/application.yml @@ -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 diff --git a/unilife-server/src/main/resources/mappers/AiChatSessionMapper.xml b/unilife-server/src/main/resources/mappers/AiChatSessionMapper.xml new file mode 100644 index 0000000..afb5af7 --- /dev/null +++ b/unilife-server/src/main/resources/mappers/AiChatSessionMapper.xml @@ -0,0 +1,87 @@ + + + + + + + + + + + + + + + + INSERT INTO ai_chat_sessions ( + id, user_id, title, created_at, updated_at + ) VALUES ( + #{id}, #{userId}, #{title}, #{createdAt}, #{updatedAt} + ) + + + + + + + + + + + + + + + + + + + + UPDATE ai_chat_sessions + SET title = #{title}, updated_at = CURRENT_TIMESTAMP + WHERE id = #{id} + + + + + UPDATE ai_chat_sessions + SET updated_at = CURRENT_TIMESTAMP + WHERE id = #{id} + + + + + DELETE FROM ai_chat_sessions WHERE id = #{id} + + + + + DELETE FROM ai_chat_sessions + WHERE updated_at < #{expireTime} + + + \ No newline at end of file diff --git a/unilife-server/src/main/resources/schema-mysql.sql b/unilife-server/src/main/resources/schema-mysql.sql new file mode 100644 index 0000000..0e4325f --- /dev/null +++ b/unilife-server/src/main/resources/schema-mysql.sql @@ -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聊天会话元数据表'; \ No newline at end of file