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
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;
|
|
}
|
|
}
|