refactor(ai-chat): 将会话ID类型统一由UUID改为字符串类型
- 修改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并保存到文档和附件表中
This commit is contained in:
@@ -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,
|
||||
|
||||
@@ -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<List<ChatMessageVO>> 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<Void> deleteSession(@PathVariable UUID sessionId) {
|
||||
public BaseResponse<Void> deleteSession(@PathVariable String sessionId) {
|
||||
Long userId = SecurityUtils.getCurrentUserId();
|
||||
if (userId == null) {
|
||||
return ResultUtils.error("用户未登录");
|
||||
|
||||
@@ -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;
|
||||
|
||||
/**
|
||||
* 会话标题
|
||||
|
||||
@@ -101,6 +101,11 @@ public class AiDocument {
|
||||
*/
|
||||
private String filePath;
|
||||
|
||||
/**
|
||||
* 文件访问URL
|
||||
*/
|
||||
private String fileUrl;
|
||||
|
||||
/**
|
||||
* 文档日期(如日报日期、照片拍摄日期)
|
||||
*/
|
||||
|
||||
@@ -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;
|
||||
|
||||
/**
|
||||
* 会话标题
|
||||
|
||||
@@ -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<AiChatMessage> {
|
||||
* @param sessionId 会话ID
|
||||
* @return 消息列表
|
||||
*/
|
||||
List<ChatMessageVO> selectSessionMessages(@Param("sessionId") UUID sessionId);
|
||||
List<ChatMessageVO> selectSessionMessages(@Param("sessionId") String sessionId);
|
||||
|
||||
/**
|
||||
* 获取会话最新消息序号
|
||||
@@ -40,7 +39,7 @@ public interface AiChatHistoryMapper extends BaseMapper<AiChatMessage> {
|
||||
* @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<AiChatMessage> {
|
||||
* @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<AiChatMessage> {
|
||||
* @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<AiChatMessage> {
|
||||
* @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<AiChatMessage> {
|
||||
* @param limit 限制条数
|
||||
* @return 消息列表
|
||||
*/
|
||||
List<AiChatMessage> selectRecentMessages(@Param("sessionId") UUID sessionId,
|
||||
List<AiChatMessage> selectRecentMessages(@Param("sessionId") String sessionId,
|
||||
@Param("limit") Integer limit);
|
||||
}
|
||||
|
||||
@@ -106,4 +106,34 @@ public interface AiDocumentMapper extends BaseMapper<AiDocument> {
|
||||
* @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);
|
||||
}
|
||||
|
||||
@@ -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<ChatMessageVO> getSessionMessages(UUID sessionId, Long userId);
|
||||
List<ChatMessageVO> 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);
|
||||
}
|
||||
|
||||
@@ -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<Message> messages = buildMessages(UUID.fromString(finalSessionId) , request.getContextWindow(),
|
||||
List<Message> 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<ChatMessageVO> getSessionMessages(UUID sessionId, Long userId) {
|
||||
public List<ChatMessageVO> 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<Message> buildMessages(UUID sessionId, Integer contextWindow,
|
||||
private List<Message> buildMessages(String sessionId, Integer contextWindow,
|
||||
String systemPrompt, String currentMessage) {
|
||||
List<Message> 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);
|
||||
|
||||
@@ -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<String> 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);
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -150,7 +150,7 @@ public class RagRetriever {
|
||||
* @param limit 限制条数
|
||||
* @return 历史消息列表
|
||||
*/
|
||||
public List<AiChatMessage> getChatHistory(UUID sessionId, int limit) {
|
||||
public List<AiChatMessage> getChatHistory(String sessionId, int limit) {
|
||||
try {
|
||||
return chatHistoryMapper.selectRecentMessages(sessionId, limit);
|
||||
} catch (Exception e) {
|
||||
|
||||
@@ -134,4 +134,22 @@
|
||||
LIMIT 1
|
||||
</select>
|
||||
|
||||
<!-- 更新切片的独立字段 -->
|
||||
<update id="updateChunkFields">
|
||||
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}
|
||||
</update>
|
||||
|
||||
</mapper>
|
||||
|
||||
Reference in New Issue
Block a user