feat(ai): 新增AI对话与知识库功能模块
- 集成Fastjson2依赖优化JSON处理性能 - 配置专用文档处理异步线程池,提升任务并发处理能力 - 实现基于Spring AI的PgVectorStore向量存储配置 - 新增AI对话控制器,支持SSE流式对话及会话管理接口 - 新增AI知识库控制器,支持文件上传、文档管理及重新索引功能 - 定义AI对话和知识库相关的数据传输对象DTO与视图对象VO - 建立AI对话消息和文档向量的数据库实体与MyBatis Mapper - 实现AI对话服务接口及其具体业务逻辑,包括会话管理和RAG检索 - 完善安全校验和错误处理,确保接口调用的用户权限和参数有效性 - 提供对话消息流式响应机制,支持实时传输用户互动内容和引用文档信息
This commit is contained in:
@@ -0,0 +1,211 @@
|
||||
package cn.yinlihupo.service.ai.impl;
|
||||
|
||||
import cn.yinlihupo.domain.entity.AiDocument;
|
||||
import cn.yinlihupo.domain.vo.KbDocumentVO;
|
||||
import cn.yinlihupo.mapper.AiDocumentMapper;
|
||||
import cn.yinlihupo.service.ai.AiKnowledgeBaseService;
|
||||
import cn.yinlihupo.service.ai.rag.DocumentProcessor;
|
||||
import cn.yinlihupo.service.oss.MinioService;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.scheduling.annotation.Async;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
|
||||
/**
|
||||
* AI知识库服务实现
|
||||
*/
|
||||
@Slf4j
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class AiKnowledgeBaseServiceImpl implements AiKnowledgeBaseService {
|
||||
|
||||
private final AiDocumentMapper documentMapper;
|
||||
private final DocumentProcessor documentProcessor;
|
||||
private final MinioService minioService;
|
||||
|
||||
// 支持的文件类型
|
||||
private static final List<String> SUPPORTED_TYPES = List.of(
|
||||
"pdf", "doc", "docx", "txt", "md", "json", "csv"
|
||||
);
|
||||
|
||||
@Override
|
||||
public KbDocumentVO uploadFile(Long projectId, MultipartFile file, Long userId) {
|
||||
// 1. 验证文件
|
||||
validateFile(file);
|
||||
|
||||
// 2. 生成文档UUID
|
||||
UUID docId = UUID.randomUUID();
|
||||
|
||||
// 3. 上传文件到MinIO
|
||||
String originalFilename = file.getOriginalFilename();
|
||||
String fileExtension = getFileExtension(originalFilename);
|
||||
String filePath = String.format("kb/%d/%s.%s", projectId, docId, fileExtension);
|
||||
|
||||
try {
|
||||
minioService.uploadFile(filePath, file.getInputStream(), file.getContentType());
|
||||
} catch (Exception e) {
|
||||
log.error("上传文件到MinIO失败: {}", e.getMessage(), e);
|
||||
throw new RuntimeException("文件上传失败: " + e.getMessage());
|
||||
}
|
||||
|
||||
// 4. 保存文档元数据
|
||||
AiDocument doc = new AiDocument();
|
||||
doc.setDocId(docId);
|
||||
doc.setProjectId(projectId);
|
||||
doc.setSourceType("upload");
|
||||
doc.setTitle(originalFilename);
|
||||
doc.setDocType(detectDocType(fileExtension));
|
||||
doc.setFileType(fileExtension);
|
||||
doc.setFileSize(file.getSize());
|
||||
doc.setFilePath(filePath);
|
||||
doc.setStatus("pending"); // 待处理状态
|
||||
doc.setChunkTotal(0);
|
||||
doc.setCreateBy(userId);
|
||||
doc.setCreateTime(LocalDateTime.now());
|
||||
doc.setDeleted(0);
|
||||
|
||||
documentMapper.insert(doc);
|
||||
|
||||
// 5. 异步处理文档(解析、切片、向量化)
|
||||
documentProcessor.processDocumentAsync(doc.getId());
|
||||
|
||||
log.info("文件上传成功: {}, docId: {}", originalFilename, docId);
|
||||
|
||||
// 6. 返回VO
|
||||
return convertToVO(doc);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<KbDocumentVO> getProjectDocuments(Long projectId) {
|
||||
return documentMapper.selectProjectDocuments(projectId);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void deleteDocument(UUID docId, Long userId) {
|
||||
// 1. 查询文档
|
||||
AiDocument doc = documentMapper.selectByDocId(docId);
|
||||
if (doc == null) {
|
||||
throw new RuntimeException("文档不存在");
|
||||
}
|
||||
|
||||
// 2. 删除MinIO中的文件
|
||||
try {
|
||||
minioService.deleteFile(doc.getFilePath());
|
||||
} catch (Exception e) {
|
||||
log.error("删除MinIO文件失败: {}, 错误: {}", doc.getFilePath(), e.getMessage());
|
||||
// 继续删除数据库记录
|
||||
}
|
||||
|
||||
// 3. 删除向量库中的向量(简化处理,实际可能需要更复杂的逻辑)
|
||||
documentProcessor.deleteDocumentVectors(docId);
|
||||
|
||||
// 4. 删除数据库记录
|
||||
documentMapper.deleteByDocId(docId);
|
||||
|
||||
log.info("文档删除成功: {}, userId: {}", docId, userId);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void reindexDocument(UUID docId, Long userId) {
|
||||
// 1. 查询文档
|
||||
AiDocument doc = documentMapper.selectByDocId(docId);
|
||||
if (doc == null) {
|
||||
throw new RuntimeException("文档不存在");
|
||||
}
|
||||
|
||||
// 2. 更新状态为处理中
|
||||
doc.setStatus("processing");
|
||||
documentMapper.updateById(doc);
|
||||
|
||||
// 3. 删除旧的向量
|
||||
documentProcessor.deleteDocumentVectors(docId);
|
||||
|
||||
// 4. 重新处理
|
||||
documentProcessor.processDocumentAsync(doc.getId());
|
||||
|
||||
log.info("文档重新索引: {}, userId: {}", docId, userId);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void processDocument(Long docId) {
|
||||
documentProcessor.processDocument(docId);
|
||||
}
|
||||
|
||||
@Override
|
||||
@Async
|
||||
public void processDocumentAsync(Long docId) {
|
||||
documentProcessor.processDocument(docId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证文件
|
||||
*/
|
||||
private void validateFile(MultipartFile file) {
|
||||
if (file == null || file.isEmpty()) {
|
||||
throw new RuntimeException("文件不能为空");
|
||||
}
|
||||
|
||||
String filename = file.getOriginalFilename();
|
||||
if (filename == null || filename.isEmpty()) {
|
||||
throw new RuntimeException("文件名不能为空");
|
||||
}
|
||||
|
||||
String extension = getFileExtension(filename);
|
||||
if (!SUPPORTED_TYPES.contains(extension.toLowerCase())) {
|
||||
throw new RuntimeException("不支持的文件类型: " + extension);
|
||||
}
|
||||
|
||||
// 文件大小限制(50MB)
|
||||
long maxSize = 50 * 1024 * 1024;
|
||||
if (file.getSize() > maxSize) {
|
||||
throw new RuntimeException("文件大小超过限制(最大50MB)");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取文件扩展名
|
||||
*/
|
||||
private String getFileExtension(String filename) {
|
||||
if (filename == null || filename.lastIndexOf('.') == -1) {
|
||||
return "";
|
||||
}
|
||||
return filename.substring(filename.lastIndexOf('.') + 1).toLowerCase();
|
||||
}
|
||||
|
||||
/**
|
||||
* 检测文档类型
|
||||
*/
|
||||
private String detectDocType(String extension) {
|
||||
return switch (extension.toLowerCase()) {
|
||||
case "pdf" -> "report";
|
||||
case "doc", "docx" -> "document";
|
||||
case "txt", "md" -> "text";
|
||||
case "json", "csv" -> "data";
|
||||
default -> "other";
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 转换为VO
|
||||
*/
|
||||
private KbDocumentVO convertToVO(AiDocument doc) {
|
||||
KbDocumentVO vo = new KbDocumentVO();
|
||||
vo.setId(doc.getId());
|
||||
vo.setDocId(doc.getDocId());
|
||||
vo.setTitle(doc.getTitle());
|
||||
vo.setDocType(doc.getDocType());
|
||||
vo.setFileType(doc.getFileType());
|
||||
vo.setFileSize(doc.getFileSize());
|
||||
vo.setFilePath(doc.getFilePath());
|
||||
vo.setSourceType(doc.getSourceType());
|
||||
vo.setChunkCount(doc.getChunkTotal());
|
||||
vo.setStatus(doc.getStatus());
|
||||
vo.setCreateTime(doc.getCreateTime());
|
||||
return vo;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user