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 getFolderTree(Long categoryId) { QueryWrapper wrapper = new QueryWrapper<>(); if (categoryId != null) { wrapper.eq("category_id", categoryId); } wrapper.orderByAsc("sort_order"); List folders = folderMapper.selectList(wrapper); Map nodeMap = new LinkedHashMap<>(); List 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 listFolders(Long categoryId) { QueryWrapper 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 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 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 deleteFolder(Long id) { KnowledgeFolder folder = folderMapper.selectById(id); if (folder == null) { throw new RuntimeException("目录不存在"); } List ids = collectDescendantIds(id); // 将该目录及其子孙目录下的文档 folder_id 置 0(不改变 categoryId) KnowledgeDocument updateDoc = new KnowledgeDocument(); updateDoc.setFolderId(0L); int movedCount = documentMapper.update(updateDoc, new QueryWrapper().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 wrapper = new QueryWrapper<>(); wrapper.eq("category_id", categoryId); List folders = folderMapper.selectList(wrapper); List 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().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 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 wrapper = new QueryWrapper<>(); wrapper.eq("category_id", categoryId); wrapper.eq("parent_id", parentId != null ? parentId : 0L); wrapper.eq("name", name); List 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 collectDescendantIds(Long rootId) { List all = new ArrayList<>(); all.add(rootId); List current = new ArrayList<>(); current.add(rootId); while (!current.isEmpty()) { QueryWrapper wrapper = new QueryWrapper<>(); wrapper.in("parent_id", current); List children = folderMapper.selectList(wrapper); List next = new ArrayList<>(); for (KnowledgeFolder child : children) { all.add(child.getId()); next.add(child.getId()); } current = next; } return all; } }