From 06d82187ffe05fe2e3bc68c1280b531c68c1c882 Mon Sep 17 00:00:00 2001 From: JiaoTianBo Date: Mon, 30 Mar 2026 18:35:20 +0800 Subject: [PATCH] =?UTF-8?q?refactor(ai-chat):=20=E5=B0=86=E4=BC=9A?= =?UTF-8?q?=E8=AF=9DID=E7=B1=BB=E5=9E=8B=E7=BB=9F=E4=B8=80=E7=94=B1UUID?= =?UTF-8?q?=E6=94=B9=E4=B8=BA=E5=AD=97=E7=AC=A6=E4=B8=B2=E7=B1=BB=E5=9E=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 修改AiChat相关实体、VO及Mapper中sessionId字段类型为String - 调整AiChatController接口,支持字符串类型sessionId参数 - 修改AiChatService及实现类中相关方法的sessionId参数类型 - 更新业务逻辑中sessionId的处理,移除UUID转换操作 feat(vector-store): 添加文件访问URL字段及切片更新接口 - 在vector_store表及对应实体中新增file_url字段 - 增加AiDocument的fileUrl字段,保存文件访问链接 - 在DocumentProcessor处理切片时更新file_url字段 - 添加AiDocumentMapper中updateChunkFields接口及XML实现 feat(attachment): 知识库文件上传支持记录文件附件 - 新增FileAttachment实体及Mapper,保存上传文件元信息 - 在AiKnowledgeBaseServiceImpl实现文件上传后保存附件记录 - 上传接口返回文件URL并保存到文档和附件表中 --- docs/dev-ops/pgsql/sql/weform_run.sql | 1 + .../controller/ai/AiChatController.java | 5 ++-- .../domain/entity/AiChatMessage.java | 5 ++-- .../yinlihupo/domain/entity/AiDocument.java | 5 ++++ .../cn/yinlihupo/domain/vo/ChatSessionVO.java | 5 ++-- .../yinlihupo/mapper/AiChatHistoryMapper.java | 13 ++++---- .../cn/yinlihupo/mapper/AiDocumentMapper.java | 30 +++++++++++++++++++ .../yinlihupo/service/ai/AiChatService.java | 7 ++--- .../service/ai/impl/AiChatServiceImpl.java | 23 +++++++------- .../ai/impl/AiKnowledgeBaseServiceImpl.java | 24 +++++++++++++-- .../service/ai/rag/DocumentProcessor.java | 6 ++++ .../service/ai/rag/RagRetriever.java | 2 +- .../resources/mapper/AiDocumentMapper.xml | 18 +++++++++++ 13 files changed, 110 insertions(+), 34 deletions(-) diff --git a/docs/dev-ops/pgsql/sql/weform_run.sql b/docs/dev-ops/pgsql/sql/weform_run.sql index 137cc98..5d54dfa 100644 --- a/docs/dev-ops/pgsql/sql/weform_run.sql +++ b/docs/dev-ops/pgsql/sql/weform_run.sql @@ -41,6 +41,7 @@ CREATE TABLE vector_store ( file_type VARCHAR(50), file_size BIGINT, file_path VARCHAR(500), + file_url VARCHAR(500), -- 时间信息 (用于时间维度检索) doc_date DATE, diff --git a/src/main/java/cn/yinlihupo/controller/ai/AiChatController.java b/src/main/java/cn/yinlihupo/controller/ai/AiChatController.java index 4b34572..90f83e7 100644 --- a/src/main/java/cn/yinlihupo/controller/ai/AiChatController.java +++ b/src/main/java/cn/yinlihupo/controller/ai/AiChatController.java @@ -17,7 +17,6 @@ import org.springframework.web.bind.annotation.*; import org.springframework.web.servlet.mvc.method.annotation.SseEmitter; import java.util.List; -import java.util.UUID; /** * AI对话控制器 @@ -157,7 +156,7 @@ public class AiChatController { @GetMapping("/session/{sessionId}/messages") @Operation(summary = "获取会话历史", description = "获取指定会话的所有消息") public BaseResponse> getSessionMessages( - @PathVariable UUID sessionId) { + @PathVariable String sessionId) { Long userId = SecurityUtils.getCurrentUserId(); if (userId == null) { return ResultUtils.error("用户未登录"); @@ -180,7 +179,7 @@ public class AiChatController { */ @DeleteMapping("/session/{sessionId}") @Operation(summary = "删除会话", description = "删除指定的对话会话") - public BaseResponse deleteSession(@PathVariable UUID sessionId) { + public BaseResponse deleteSession(@PathVariable String sessionId) { Long userId = SecurityUtils.getCurrentUserId(); if (userId == null) { return ResultUtils.error("用户未登录"); diff --git a/src/main/java/cn/yinlihupo/domain/entity/AiChatMessage.java b/src/main/java/cn/yinlihupo/domain/entity/AiChatMessage.java index 50a3782..3779adf 100644 --- a/src/main/java/cn/yinlihupo/domain/entity/AiChatMessage.java +++ b/src/main/java/cn/yinlihupo/domain/entity/AiChatMessage.java @@ -6,7 +6,6 @@ import com.baomidou.mybatisplus.annotation.TableName; import lombok.Data; import java.time.LocalDateTime; -import java.util.UUID; /** * AI对话消息实体 @@ -20,9 +19,9 @@ public class AiChatMessage { private Long id; /** - * 会话ID + * 会话ID(UUID格式字符串) */ - private UUID sessionId; + private String sessionId; /** * 会话标题 diff --git a/src/main/java/cn/yinlihupo/domain/entity/AiDocument.java b/src/main/java/cn/yinlihupo/domain/entity/AiDocument.java index d8bf6b5..736efd5 100644 --- a/src/main/java/cn/yinlihupo/domain/entity/AiDocument.java +++ b/src/main/java/cn/yinlihupo/domain/entity/AiDocument.java @@ -101,6 +101,11 @@ public class AiDocument { */ private String filePath; + /** + * 文件访问URL + */ + private String fileUrl; + /** * 文档日期(如日报日期、照片拍摄日期) */ diff --git a/src/main/java/cn/yinlihupo/domain/vo/ChatSessionVO.java b/src/main/java/cn/yinlihupo/domain/vo/ChatSessionVO.java index f06185e..7baff4e 100644 --- a/src/main/java/cn/yinlihupo/domain/vo/ChatSessionVO.java +++ b/src/main/java/cn/yinlihupo/domain/vo/ChatSessionVO.java @@ -3,7 +3,6 @@ package cn.yinlihupo.domain.vo; import lombok.Data; import java.time.LocalDateTime; -import java.util.UUID; /** * 会话信息VO @@ -12,9 +11,9 @@ import java.util.UUID; public class ChatSessionVO { /** - * 会话ID + * 会话ID(UUID格式字符串) */ - private UUID sessionId; + private String sessionId; /** * 会话标题 diff --git a/src/main/java/cn/yinlihupo/mapper/AiChatHistoryMapper.java b/src/main/java/cn/yinlihupo/mapper/AiChatHistoryMapper.java index ef922d4..5ada192 100644 --- a/src/main/java/cn/yinlihupo/mapper/AiChatHistoryMapper.java +++ b/src/main/java/cn/yinlihupo/mapper/AiChatHistoryMapper.java @@ -8,7 +8,6 @@ import org.apache.ibatis.annotations.Mapper; import org.apache.ibatis.annotations.Param; import java.util.List; -import java.util.UUID; /** * AI对话历史Mapper @@ -32,7 +31,7 @@ public interface AiChatHistoryMapper extends BaseMapper { * @param sessionId 会话ID * @return 消息列表 */ - List selectSessionMessages(@Param("sessionId") UUID sessionId); + List selectSessionMessages(@Param("sessionId") String sessionId); /** * 获取会话最新消息序号 @@ -40,7 +39,7 @@ public interface AiChatHistoryMapper extends BaseMapper { * @param sessionId 会话ID * @return 最大序号 */ - Integer selectMaxMessageIndex(@Param("sessionId") UUID sessionId); + Integer selectMaxMessageIndex(@Param("sessionId") String sessionId); /** * 获取会话消息数量 @@ -48,7 +47,7 @@ public interface AiChatHistoryMapper extends BaseMapper { * @param sessionId 会话ID * @return 消息数量 */ - Integer selectMessageCount(@Param("sessionId") UUID sessionId); + Integer selectMessageCount(@Param("sessionId") String sessionId); /** * 获取会话最后一条消息时间 @@ -56,7 +55,7 @@ public interface AiChatHistoryMapper extends BaseMapper { * @param sessionId 会话ID * @return 最后消息时间 */ - String selectLastMessageTime(@Param("sessionId") UUID sessionId); + String selectLastMessageTime(@Param("sessionId") String sessionId); /** * 根据sessionId删除消息 @@ -64,7 +63,7 @@ public interface AiChatHistoryMapper extends BaseMapper { * @param sessionId 会话ID * @return 影响行数 */ - int deleteBySessionId(@Param("sessionId") UUID sessionId); + int deleteBySessionId(@Param("sessionId") String sessionId); /** * 获取会话的最近N条消息(用于上下文) @@ -73,6 +72,6 @@ public interface AiChatHistoryMapper extends BaseMapper { * @param limit 限制条数 * @return 消息列表 */ - List selectRecentMessages(@Param("sessionId") UUID sessionId, + List selectRecentMessages(@Param("sessionId") String sessionId, @Param("limit") Integer limit); } diff --git a/src/main/java/cn/yinlihupo/mapper/AiDocumentMapper.java b/src/main/java/cn/yinlihupo/mapper/AiDocumentMapper.java index e242c61..1536712 100644 --- a/src/main/java/cn/yinlihupo/mapper/AiDocumentMapper.java +++ b/src/main/java/cn/yinlihupo/mapper/AiDocumentMapper.java @@ -106,4 +106,34 @@ public interface AiDocumentMapper extends BaseMapper { * @return 分片详情 */ DocumentChunkVO selectChunkById(@Param("chunkId") String chunkId); + + /** + * 更新切片的独立字段 + * + * @param chunkId 切片ID + * @param parentId 父文档ID + * @param chunkIndex 分块序号 + * @param chunkTotal 总分块数 + * @param projectId 项目ID + * @param kbId 知识库ID + * @param sourceType 来源类型 + * @param sourceId 来源记录ID + * @param title 文档标题 + * @param docType 文档类型 + * @param fileType 文件类型 + * @param fileUrl 文件URL + * @return 影响行数 + */ + int updateChunkFields(@Param("chunkId") String chunkId, + @Param("parentId") String parentId, + @Param("chunkIndex") Integer chunkIndex, + @Param("chunkTotal") Integer chunkTotal, + @Param("projectId") Long projectId, + @Param("kbId") Long kbId, + @Param("sourceType") String sourceType, + @Param("sourceId") Long sourceId, + @Param("title") String title, + @Param("docType") String docType, + @Param("fileType") String fileType, + @Param("fileUrl") String fileUrl); } diff --git a/src/main/java/cn/yinlihupo/service/ai/AiChatService.java b/src/main/java/cn/yinlihupo/service/ai/AiChatService.java index 2ce9282..0ea5253 100644 --- a/src/main/java/cn/yinlihupo/service/ai/AiChatService.java +++ b/src/main/java/cn/yinlihupo/service/ai/AiChatService.java @@ -6,7 +6,6 @@ import cn.yinlihupo.domain.vo.ChatSessionVO; import org.springframework.web.servlet.mvc.method.annotation.SseEmitter; import java.util.List; -import java.util.UUID; /** * AI对话服务接口 @@ -51,7 +50,7 @@ public interface AiChatService { * @param userId 用户ID * @return 消息列表 */ - List getSessionMessages(UUID sessionId, Long userId); + List getSessionMessages(String sessionId, Long userId); /** * 删除会话 @@ -59,7 +58,7 @@ public interface AiChatService { * @param sessionId 会话ID * @param userId 用户ID */ - void deleteSession(UUID sessionId, Long userId); + void deleteSession(String sessionId, Long userId); /** * 验证用户是否有权限访问会话 @@ -68,5 +67,5 @@ public interface AiChatService { * @param userId 用户ID * @return 是否有权限 */ - boolean hasSessionAccess(UUID sessionId, Long userId); + boolean hasSessionAccess(String sessionId, Long userId); } diff --git a/src/main/java/cn/yinlihupo/service/ai/impl/AiChatServiceImpl.java b/src/main/java/cn/yinlihupo/service/ai/impl/AiChatServiceImpl.java index c1353c8..dd81ac1 100644 --- a/src/main/java/cn/yinlihupo/service/ai/impl/AiChatServiceImpl.java +++ b/src/main/java/cn/yinlihupo/service/ai/impl/AiChatServiceImpl.java @@ -78,7 +78,7 @@ public class AiChatServiceImpl implements AiChatService { createSession(userId, request.getProjectId(), request.getTimelineNodeId(), request.getMessage(), title); } else { // 验证会话权限 - if (!hasSessionAccess(UUID.fromString(sessionId), userId)) { + if (!hasSessionAccess(sessionId, userId)) { sendError(emitter, "无权访问该会话"); return; } @@ -88,7 +88,7 @@ public class AiChatServiceImpl implements AiChatService { // 发送开始消息 sendEvent(emitter, "start", Map.of( - "sessionId", finalSessionId.toString(), + "sessionId", finalSessionId, "isNewSession", isNewSession )); @@ -107,7 +107,7 @@ public class AiChatServiceImpl implements AiChatService { // 4. 构建Prompt String systemPrompt = buildSystemPrompt(request.getProjectId(), retrievedDocs); - List messages = buildMessages(UUID.fromString(finalSessionId) , request.getContextWindow(), + List messages = buildMessages(finalSessionId , request.getContextWindow(), systemPrompt, request.getMessage()); // 5. 流式调用LLM @@ -164,6 +164,7 @@ public class AiChatServiceImpl implements AiChatService { public ChatSessionVO createSession(Long userId, Long projectId, Long timelineNodeId, String firstMessage, String customTitle) { UUID sessionId = UUID.randomUUID(); + String sessionIdStr = sessionId.toString(); String title = customTitle; if (title == null || title.isEmpty()) { @@ -172,7 +173,7 @@ public class AiChatServiceImpl implements AiChatService { // 保存系统消息(作为会话创建标记) AiChatMessage message = new AiChatMessage(); - message.setSessionId(sessionId); + message.setSessionId(sessionIdStr); message.setSessionTitle(title); message.setUserId(userId); message.setProjectId(projectId); @@ -185,7 +186,7 @@ public class AiChatServiceImpl implements AiChatService { // 构建返回对象 ChatSessionVO vo = new ChatSessionVO(); - vo.setSessionId(sessionId); + vo.setSessionId(sessionIdStr); vo.setSessionTitle(title); vo.setProjectId(projectId); @@ -207,7 +208,7 @@ public class AiChatServiceImpl implements AiChatService { } @Override - public List getSessionMessages(UUID sessionId, Long userId) { + public List getSessionMessages(String sessionId, Long userId) { // 验证权限 if (!hasSessionAccess(sessionId, userId)) { throw new RuntimeException("无权访问该会话"); @@ -227,7 +228,7 @@ public class AiChatServiceImpl implements AiChatService { } @Override - public void deleteSession(UUID sessionId, Long userId) { + public void deleteSession(String sessionId, Long userId) { // 验证权限 if (!hasSessionAccess(sessionId, userId)) { throw new RuntimeException("无权删除该会话"); @@ -238,7 +239,7 @@ public class AiChatServiceImpl implements AiChatService { } @Override - public boolean hasSessionAccess(UUID sessionId, Long userId) { + public boolean hasSessionAccess(String sessionId, Long userId) { // 查询会话的第一条消息确认归属 // 简化实现:查询该session_id下是否有该用户的消息 // 实际应该查询所有消息中是否有该用户的记录 @@ -302,7 +303,7 @@ public class AiChatServiceImpl implements AiChatService { /** * 构建消息列表 */ - private List buildMessages(UUID sessionId, Integer contextWindow, + private List buildMessages(String sessionId, Integer contextWindow, String systemPrompt, String currentMessage) { List messages = new ArrayList<>(); @@ -335,11 +336,11 @@ public class AiChatServiceImpl implements AiChatService { Long timelineNodeId, String role, String content, String referencedDocIds) { // 获取当前最大序号 - Integer maxIndex = chatHistoryMapper.selectMaxMessageIndex(UUID.fromString(sessionId)); + Integer maxIndex = chatHistoryMapper.selectMaxMessageIndex(sessionId); int nextIndex = (maxIndex != null ? maxIndex : 0) + 1; AiChatMessage message = new AiChatMessage(); - message.setSessionId(UUID.fromString(sessionId)); + message.setSessionId(sessionId); message.setUserId(userId); message.setProjectId(projectId); message.setTimelineNodeId(timelineNodeId); diff --git a/src/main/java/cn/yinlihupo/service/ai/impl/AiKnowledgeBaseServiceImpl.java b/src/main/java/cn/yinlihupo/service/ai/impl/AiKnowledgeBaseServiceImpl.java index 46b5f0c..177315b 100644 --- a/src/main/java/cn/yinlihupo/service/ai/impl/AiKnowledgeBaseServiceImpl.java +++ b/src/main/java/cn/yinlihupo/service/ai/impl/AiKnowledgeBaseServiceImpl.java @@ -1,9 +1,11 @@ package cn.yinlihupo.service.ai.impl; import cn.yinlihupo.domain.entity.AiDocument; +import cn.yinlihupo.domain.entity.FileAttachment; import cn.yinlihupo.domain.vo.DocumentChunkVO; import cn.yinlihupo.domain.vo.KbDocumentVO; import cn.yinlihupo.mapper.AiDocumentMapper; +import cn.yinlihupo.mapper.FileAttachmentMapper; import cn.yinlihupo.service.ai.AiKnowledgeBaseService; import cn.yinlihupo.service.ai.rag.DocumentProcessor; import cn.yinlihupo.service.oss.MinioService; @@ -28,6 +30,7 @@ public class AiKnowledgeBaseServiceImpl implements AiKnowledgeBaseService { private final AiDocumentMapper documentMapper; private final DocumentProcessor documentProcessor; private final MinioService minioService; + private final FileAttachmentMapper fileAttachmentMapper; // 支持的文件类型 private static final List SUPPORTED_TYPES = List.of( @@ -46,15 +49,31 @@ public class AiKnowledgeBaseServiceImpl implements AiKnowledgeBaseService { String originalFilename = file.getOriginalFilename(); String fileExtension = getFileExtension(originalFilename); String filePath = String.format("kb/%d/%s.%s", projectId, docId, fileExtension); + String fileUrl; try { - minioService.uploadFile(filePath, file.getInputStream(), file.getContentType()); + fileUrl = minioService.uploadFile(filePath, file.getInputStream(), file.getContentType()); } catch (Exception e) { log.error("上传文件到MinIO失败: {}", e.getMessage(), e); throw new RuntimeException("文件上传失败: " + e.getMessage()); } - // 4. 保存文档元数据 + // 4. 保存到附件表 + FileAttachment attachment = new FileAttachment(); + attachment.setFileName(docId + "." + fileExtension); + attachment.setOriginalName(originalFilename); + attachment.setFilePath(filePath); + attachment.setFileUrl(fileUrl); + attachment.setFileType(file.getContentType()); + attachment.setFileSize(file.getSize()); + attachment.setStorageType("minio"); + attachment.setRelatedType("knowledge_base"); + attachment.setRelatedId(projectId); + attachment.setUploaderId(userId); + fileAttachmentMapper.insert(attachment); + log.info("文件附件记录保存成功, fileName: {}", originalFilename); + + // 5. 保存文档元数据到向量库 AiDocument doc = new AiDocument(); doc.setId(docId); // 设置标准UUID格式的ID doc.setProjectId(projectId); @@ -64,6 +83,7 @@ public class AiKnowledgeBaseServiceImpl implements AiKnowledgeBaseService { doc.setFileType(fileExtension); doc.setFileSize(file.getSize()); doc.setFilePath(filePath); + doc.setFileUrl(fileUrl); // 保存文件URL doc.setContent(""); doc.setStatus("pending"); // 待处理状态 doc.setChunkTotal(0); diff --git a/src/main/java/cn/yinlihupo/service/ai/rag/DocumentProcessor.java b/src/main/java/cn/yinlihupo/service/ai/rag/DocumentProcessor.java index cf5107b..f55e2bf 100644 --- a/src/main/java/cn/yinlihupo/service/ai/rag/DocumentProcessor.java +++ b/src/main/java/cn/yinlihupo/service/ai/rag/DocumentProcessor.java @@ -193,6 +193,7 @@ public class DocumentProcessor { metadata.put("title", doc.getTitle() != null ? doc.getTitle() : ""); metadata.put("doc_type", doc.getDocType() != null ? doc.getDocType() : ""); metadata.put("file_type", doc.getFileType() != null ? doc.getFileType() : ""); + metadata.put("file_url", doc.getFileUrl() != null ? doc.getFileUrl() : ""); // 文件访问URL // 分片信息 metadata.put("chunk_index", i); metadata.put("chunk_total", chunks.size()); @@ -204,6 +205,11 @@ public class DocumentProcessor { // 存储到向量库 vectorStore.add(List.of(vectorDoc)); + // 更新切片的独立字段(用于直接查询,避免依赖metadata) + documentMapper.updateChunkFields(chunkId, docId, i, chunks.size(), + doc.getProjectId(), doc.getKbId(), doc.getSourceType(), doc.getSourceId(), + doc.getTitle(), doc.getDocType(), doc.getFileType(), doc.getFileUrl()); + log.debug("存储切片: {}/{}, docId: {}, chunkId: {}", i + 1, chunks.size(), docId, chunkId); } diff --git a/src/main/java/cn/yinlihupo/service/ai/rag/RagRetriever.java b/src/main/java/cn/yinlihupo/service/ai/rag/RagRetriever.java index 2658bb2..b936e41 100644 --- a/src/main/java/cn/yinlihupo/service/ai/rag/RagRetriever.java +++ b/src/main/java/cn/yinlihupo/service/ai/rag/RagRetriever.java @@ -150,7 +150,7 @@ public class RagRetriever { * @param limit 限制条数 * @return 历史消息列表 */ - public List getChatHistory(UUID sessionId, int limit) { + public List getChatHistory(String sessionId, int limit) { try { return chatHistoryMapper.selectRecentMessages(sessionId, limit); } catch (Exception e) { diff --git a/src/main/resources/mapper/AiDocumentMapper.xml b/src/main/resources/mapper/AiDocumentMapper.xml index bc9f23e..07b49ab 100644 --- a/src/main/resources/mapper/AiDocumentMapper.xml +++ b/src/main/resources/mapper/AiDocumentMapper.xml @@ -134,4 +134,22 @@ LIMIT 1 + + + UPDATE vector_store + SET chunk_parent_id = #{parentId}, + chunk_index = #{chunkIndex}, + chunk_total = #{chunkTotal}, + project_id = #{projectId}, + kb_id = #{kbId}, + source_type = #{sourceType}, + source_id = #{sourceId}, + title = #{title}, + doc_type = #{docType}, + file_type = #{fileType}, + file_url = #{fileUrl}, + update_time = NOW() + WHERE id = #{chunkId} + +