本地 RAG 知识库
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 

313 lines
12 KiB

package com.wok.supportbot.service;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.wok.supportbot.dao.KnowledgeCategoryMapper;
import com.wok.supportbot.dao.KnowledgeDocumentMapper;
import com.wok.supportbot.dao.KnowledgeFolderMapper;
import com.wok.supportbot.entity.FolderNode;
import com.wok.supportbot.entity.KnowledgeCategory;
import com.wok.supportbot.entity.KnowledgeDocument;
import com.wok.supportbot.entity.KnowledgeFolder;
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.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
/**
* 知识库文档目录服务
* 管理分类下的目录树形结构,支持目录的增删改查与级联处理
*/
@Service
@Slf4j
public class FolderService {
@Autowired
private KnowledgeFolderMapper folderMapper;
@Autowired
private KnowledgeCategoryMapper categoryMapper;
@Autowired
private KnowledgeDocumentMapper documentMapper;
/**
* 获取目录树(按 parentId 拼装,parentId 为 null/0 的作为根节点)
*
* @param categoryId 分类ID(可选,为空时查询所有目录)
* @return 目录树根节点列表
*/
public List<FolderNode> getFolderTree(Long categoryId) {
QueryWrapper<KnowledgeFolder> wrapper = new QueryWrapper<>();
if (categoryId != null) {
wrapper.eq("category_id", categoryId);
}
wrapper.orderByAsc("sort_order");
List<KnowledgeFolder> folders = folderMapper.selectList(wrapper);
Map<Long, FolderNode> nodeMap = new LinkedHashMap<>();
List<FolderNode> rootNodes = new ArrayList<>();
for (KnowledgeFolder folder : folders) {
FolderNode node = FolderNode.builder()
.id(folder.getId())
.name(folder.getName())
.categoryId(folder.getCategoryId())
.parentId(folder.getParentId())
.sortOrder(folder.getSortOrder())
.documentCount(0)
.children(new ArrayList<>())
.build();
nodeMap.put(folder.getId(), node);
}
for (FolderNode node : nodeMap.values()) {
if (node.getParentId() == null || node.getParentId() == 0) {
rootNodes.add(node);
} else {
FolderNode parent = nodeMap.get(node.getParentId());
if (parent != null) {
parent.getChildren().add(node);
} else {
// 父目录缺失时降级为根节点,避免节点丢失
rootNodes.add(node);
}
}
}
return rootNodes;
}
/**
* 获取目录扁平列表
*
* @param categoryId 分类ID(可选)
* @return 目录列表
*/
public List<KnowledgeFolder> listFolders(Long categoryId) {
QueryWrapper<KnowledgeFolder> wrapper = new QueryWrapper<>();
if (categoryId != null) {
wrapper.eq("category_id", categoryId);
}
wrapper.orderByAsc("sort_order");
return folderMapper.selectList(wrapper);
}
/**
* 根据ID查询目录(逻辑删除的目录返回 null)
*
* @param id 目录ID
* @return 目录实体
*/
public KnowledgeFolder getFolderById(Long id) {
if (id == null || id <= 0) {
return null;
}
return folderMapper.selectById(id);
}
/**
* 创建目录
*
* @param name 目录名称
* @param categoryId 所属分类ID(必须存在且未删除)
* @param parentId 父目录ID(缺省/0 表示分类根目录)
* @param sortOrder 排序权重
* @return 创建完成的目录实体
*/
public KnowledgeFolder createFolder(String name, Long categoryId, Long parentId, Integer sortOrder) {
if (name == null || name.trim().isEmpty()) {
throw new RuntimeException("目录名称不能为空");
}
name = name.trim();
if (categoryId == null || categoryId <= 0) {
throw new RuntimeException("所属分类不存在");
}
KnowledgeCategory category = categoryMapper.selectById(categoryId);
if (category == null || category.isDelete()) {
throw new RuntimeException("所属分类不存在");
}
Long parent = (parentId == null || parentId == 0) ? 0L : parentId;
if (parent > 0) {
KnowledgeFolder parentFolder = folderMapper.selectById(parent);
if (parentFolder == null || parentFolder.isDelete()) {
throw new RuntimeException("父目录不存在");
}
if (parentFolder.getCategoryId() == null || !parentFolder.getCategoryId().equals(categoryId)) {
throw new RuntimeException("父目录不属于该分类");
}
}
// 同父同名查重
QueryWrapper<KnowledgeFolder> wrapper = new QueryWrapper<>();
wrapper.eq("category_id", categoryId);
wrapper.eq("parent_id", parent);
wrapper.eq("name", name);
if (folderMapper.selectCount(wrapper) > 0) {
throw new RuntimeException("同级目录下已存在同名目录");
}
KnowledgeFolder folder = KnowledgeFolder.builder()
.name(name)
.categoryId(categoryId)
.parentId(parent)
.sortOrder(sortOrder != null ? sortOrder : 0)
.build();
folderMapper.insert(folder);
log.info("创建目录: id={}, name={}, categoryId={}, parentId={}", folder.getId(), name, categoryId, parent);
return folder;
}
/**
* 重命名目录 / 调整排序
*
* @param id 目录ID
* @param name 新名称(可选)
* @param sortOrder 新排序权重(可选)
* @return 更新后的目录实体
*/
public KnowledgeFolder renameFolder(Long id, String name, Integer sortOrder) {
KnowledgeFolder folder = folderMapper.selectById(id);
if (folder == null) {
throw new RuntimeException("目录不存在");
}
if (name != null && !name.trim().isEmpty()) {
String newName = name.trim();
// 同父同名查重(排除自身)
QueryWrapper<KnowledgeFolder> wrapper = new QueryWrapper<>();
wrapper.eq("category_id", folder.getCategoryId());
wrapper.eq("parent_id", folder.getParentId());
wrapper.eq("name", newName);
wrapper.ne("id", id);
if (folderMapper.selectCount(wrapper) > 0) {
throw new RuntimeException("同级目录下已存在同名目录");
}
folder.setName(newName);
}
if (sortOrder != null) {
folder.setSortOrder(sortOrder);
}
folderMapper.updateById(folder);
return folder;
}
/**
* 删除目录(级联逻辑删除自身与所有子孙目录,并将其下文档 folder_id 置 0)
*
* @param id 目录ID
* @return 删除结果(删除的目录数 + 移动的文档数)
*/
@Transactional(rollbackFor = Exception.class)
public Map<String, Object> deleteFolder(Long id) {
KnowledgeFolder folder = folderMapper.selectById(id);
if (folder == null) {
throw new RuntimeException("目录不存在");
}
List<Long> ids = collectDescendantIds(id);
// 将该目录及其子孙目录下的文档 folder_id 置 0(不改变 categoryId)
KnowledgeDocument updateDoc = new KnowledgeDocument();
updateDoc.setFolderId(0L);
int movedCount = documentMapper.update(updateDoc,
new QueryWrapper<KnowledgeDocument>().in("folder_id", ids));
// 逻辑删除目录(含子孙)
folderMapper.deleteBatchIds(ids);
log.info("删除目录及子孙: id={}, 删除目录数={}, 移动文档数={}", id, ids.size(), movedCount);
return Map.of("deletedFolders", ids.size(), "movedDocuments", movedCount);
}
/**
* 清理指定分类下的所有目录及其子孙目录(用于删除分类时的级联处理)
*
* @param categoryId 分类ID
* @return 删除的目录数量
*/
@Transactional(rollbackFor = Exception.class)
public int deleteFoldersByCategoryId(Long categoryId) {
QueryWrapper<KnowledgeFolder> wrapper = new QueryWrapper<>();
wrapper.eq("category_id", categoryId);
List<KnowledgeFolder> folders = folderMapper.selectList(wrapper);
List<Long> ids = new ArrayList<>();
for (KnowledgeFolder folder : folders) {
ids.addAll(collectDescendantIds(folder.getId()));
}
// 去重(多个根目录的子孙理论上不重叠,防御性处理)
ids = ids.stream().distinct().collect(Collectors.toList());
if (ids.isEmpty()) {
return 0;
}
KnowledgeDocument updateDoc = new KnowledgeDocument();
updateDoc.setFolderId(0L);
documentMapper.update(updateDoc, new QueryWrapper<KnowledgeDocument>().in("folder_id", ids));
folderMapper.deleteBatchIds(ids);
log.info("清理分类目录: categoryId={}, 删除目录数={}", categoryId, ids.size());
return ids.size();
}
/**
* 从基础目录出发,逐段确保目录路径存在并返回最终目录ID
*
* @param categoryId 目标分类ID
* @param baseFolderId 基础目录ID(null/0 表示分类根目录)
* @param segments 目录段列表(不包含文件名)
* @return 最终目录ID
*/
public Long ensureFolderPath(Long categoryId, Long baseFolderId, List<String> segments) {
Long currentParentId = (baseFolderId == null || baseFolderId == 0) ? 0L : baseFolderId;
for (String segment : segments) {
currentParentId = getOrCreateFolder(categoryId, currentParentId, segment);
}
return currentParentId;
}
/**
* 在指定父目录下查找目录,不存在则创建,返回目录ID
*
* @param categoryId 分类ID
* @param parentId 父目录ID
* @param name 目录名称
* @return 目录ID
*/
private Long getOrCreateFolder(Long categoryId, Long parentId, String name) {
QueryWrapper<KnowledgeFolder> wrapper = new QueryWrapper<>();
wrapper.eq("category_id", categoryId);
wrapper.eq("parent_id", parentId != null ? parentId : 0L);
wrapper.eq("name", name);
List<KnowledgeFolder> existing = folderMapper.selectList(wrapper);
if (existing != null && !existing.isEmpty()) {
return existing.get(0).getId();
}
return createFolder(name, categoryId, parentId, null).getId();
}
/**
* 递归收集目录自身及所有子孙目录的ID
*
* @param rootId 根目录ID
* @return 目录ID列表(含根目录)
*/
private List<Long> collectDescendantIds(Long rootId) {
List<Long> all = new ArrayList<>();
all.add(rootId);
List<Long> current = new ArrayList<>();
current.add(rootId);
while (!current.isEmpty()) {
QueryWrapper<KnowledgeFolder> wrapper = new QueryWrapper<>();
wrapper.in("parent_id", current);
List<KnowledgeFolder> children = folderMapper.selectList(wrapper);
List<Long> next = new ArrayList<>();
for (KnowledgeFolder child : children) {
all.add(child.getId());
next.add(child.getId());
}
current = next;
}
return all;
}
}