Browse Source
feat(document): 新增知识库文档目录(文件夹)功能
feat(document): 新增知识库文档目录(文件夹)功能
- 新增 KnowledgeFolder 实体/Mapper、FolderService、FolderController,支持目录树增删改查 - 文档新增 folderId 字段,列表支持按目录筛选与服务端排序 - 新增文件夹批量上传(按相对路径自动创建子目录) - 文档列表页新增左侧目录树面板,上传页新增目录选择与文件夹上传Spring-AI-1.1.2
17 changed files with 1485 additions and 102 deletions
-
2frontend/components.d.ts
-
5frontend/src/api/document.ts
-
17frontend/src/api/folder.ts
-
5frontend/src/api/upload.ts
-
15frontend/src/types/models.ts
-
404frontend/src/views/DocList.vue
-
169frontend/src/views/DocUpload.vue
-
59src/main/java/com/wok/supportbot/config/DatabaseInitConfig.java
-
59src/main/java/com/wok/supportbot/controller/DocumentController.java
-
136src/main/java/com/wok/supportbot/controller/FolderController.java
-
12src/main/java/com/wok/supportbot/dao/KnowledgeFolderMapper.java
-
63src/main/java/com/wok/supportbot/entity/FolderNode.java
-
7src/main/java/com/wok/supportbot/entity/KnowledgeDocument.java
-
80src/main/java/com/wok/supportbot/entity/KnowledgeFolder.java
-
205src/main/java/com/wok/supportbot/service/DocumentService.java
-
313src/main/java/com/wok/supportbot/service/FolderService.java
-
36src/main/resources/init-database.sql
@ -0,0 +1,17 @@ |
|||||
|
import request from './request' |
||||
|
import type { ApiResponse } from '@/types/api' |
||||
|
|
||||
|
/** 获取目录树(可按分类过滤) */ |
||||
|
export function getFolderTree(categoryId?: string): Promise<ApiResponse> { return request.get(`/folder/tree${categoryId ? `?categoryId=${categoryId}` : ''}`).then(r => r.data) } |
||||
|
|
||||
|
/** 获取目录列表(可按分类过滤) */ |
||||
|
export function getFolderList(categoryId?: string): Promise<ApiResponse> { return request.get(`/folder/list${categoryId ? `?categoryId=${categoryId}` : ''}`).then(r => r.data) } |
||||
|
|
||||
|
/** 创建目录 */ |
||||
|
export function createFolder(data: any): Promise<ApiResponse> { return request.post('/folder', data).then(r => r.data) } |
||||
|
|
||||
|
/** 更新目录 */ |
||||
|
export function updateFolder(id: string, data: any): Promise<ApiResponse> { return request.put(`/folder/${id}`, data).then(r => r.data) } |
||||
|
|
||||
|
/** 删除目录 */ |
||||
|
export function deleteFolder(id: string): Promise<ApiResponse> { return request.delete(`/folder/${id}`).then(r => r.data) } |
||||
@ -0,0 +1,136 @@ |
|||||
|
package com.wok.supportbot.controller; |
||||
|
|
||||
|
import com.wok.supportbot.entity.FolderNode; |
||||
|
import com.wok.supportbot.entity.KnowledgeFolder; |
||||
|
import com.wok.supportbot.service.FolderService; |
||||
|
import org.springframework.beans.factory.annotation.Autowired; |
||||
|
import org.springframework.http.ResponseEntity; |
||||
|
import org.springframework.security.access.prepost.PreAuthorize; |
||||
|
import org.springframework.web.bind.annotation.*; |
||||
|
|
||||
|
import java.util.List; |
||||
|
import java.util.Map; |
||||
|
|
||||
|
/** |
||||
|
* 知识库文档目录控制器 |
||||
|
* 提供目录的树形查询、扁平列表、创建、重命名、删除等功能 |
||||
|
*/ |
||||
|
@RestController |
||||
|
public class FolderController { |
||||
|
|
||||
|
@Autowired |
||||
|
private FolderService folderService; |
||||
|
|
||||
|
/** |
||||
|
* 获取目录树 |
||||
|
*/ |
||||
|
@GetMapping("/folder/tree") |
||||
|
@PreAuthorize("hasAnyRole('admin','kb_operator')") |
||||
|
public ResponseEntity<Map<String, Object>> getFolderTree(@RequestParam(required = false) Long categoryId) { |
||||
|
try { |
||||
|
List<FolderNode> tree = folderService.getFolderTree(categoryId); |
||||
|
return ResponseEntity.ok(Map.of( |
||||
|
"success", true, |
||||
|
"data", tree |
||||
|
)); |
||||
|
} catch (Exception e) { |
||||
|
return ResponseEntity.status(500).body(Map.of( |
||||
|
"success", false, |
||||
|
"message", "获取目录树失败:" + e.getMessage() |
||||
|
)); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 获取目录扁平列表 |
||||
|
*/ |
||||
|
@GetMapping("/folder/list") |
||||
|
@PreAuthorize("hasAnyRole('admin','kb_operator')") |
||||
|
public ResponseEntity<Map<String, Object>> listFolders(@RequestParam(required = false) Long categoryId) { |
||||
|
try { |
||||
|
List<KnowledgeFolder> list = folderService.listFolders(categoryId); |
||||
|
return ResponseEntity.ok(Map.of( |
||||
|
"success", true, |
||||
|
"data", list |
||||
|
)); |
||||
|
} catch (Exception e) { |
||||
|
return ResponseEntity.status(500).body(Map.of( |
||||
|
"success", false, |
||||
|
"message", "获取目录列表失败:" + e.getMessage() |
||||
|
)); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 创建目录 |
||||
|
*/ |
||||
|
@PostMapping("/folder") |
||||
|
@PreAuthorize("hasAnyRole('admin','kb_operator')") |
||||
|
public ResponseEntity<Map<String, Object>> createFolder(@RequestBody Map<String, Object> body) { |
||||
|
try { |
||||
|
String name = (String) body.get("name"); |
||||
|
Long categoryId = body.get("categoryId") != null ? Long.valueOf(body.get("categoryId").toString()) : null; |
||||
|
Long parentId = body.get("parentId") != null ? Long.valueOf(body.get("parentId").toString()) : null; |
||||
|
Integer sortOrder = body.get("sortOrder") != null ? Integer.valueOf(body.get("sortOrder").toString()) : null; |
||||
|
|
||||
|
KnowledgeFolder folder = folderService.createFolder(name, categoryId, parentId, sortOrder); |
||||
|
return ResponseEntity.ok(Map.of( |
||||
|
"success", true, |
||||
|
"message", "目录创建成功", |
||||
|
"data", folder |
||||
|
)); |
||||
|
} catch (Exception e) { |
||||
|
return ResponseEntity.status(500).body(Map.of( |
||||
|
"success", false, |
||||
|
"message", "创建目录失败:" + e.getMessage() |
||||
|
)); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 重命名目录 / 调整排序 |
||||
|
*/ |
||||
|
@PutMapping("/folder/{id}") |
||||
|
@PreAuthorize("hasAnyRole('admin','kb_operator')") |
||||
|
public ResponseEntity<Map<String, Object>> renameFolder( |
||||
|
@PathVariable Long id, |
||||
|
@RequestBody Map<String, Object> body) { |
||||
|
try { |
||||
|
String name = (String) body.get("name"); |
||||
|
Integer sortOrder = body.get("sortOrder") != null ? Integer.valueOf(body.get("sortOrder").toString()) : null; |
||||
|
|
||||
|
KnowledgeFolder folder = folderService.renameFolder(id, name, sortOrder); |
||||
|
return ResponseEntity.ok(Map.of( |
||||
|
"success", true, |
||||
|
"message", "目录更新成功", |
||||
|
"data", folder |
||||
|
)); |
||||
|
} catch (Exception e) { |
||||
|
return ResponseEntity.status(500).body(Map.of( |
||||
|
"success", false, |
||||
|
"message", "更新目录失败:" + e.getMessage() |
||||
|
)); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 删除目录(级联删除子孙目录,并将其下文档移到分类根) |
||||
|
*/ |
||||
|
@DeleteMapping("/folder/{id}") |
||||
|
@PreAuthorize("hasAnyRole('admin','kb_operator')") |
||||
|
public ResponseEntity<Map<String, Object>> deleteFolder(@PathVariable Long id) { |
||||
|
try { |
||||
|
Map<String, Object> result = folderService.deleteFolder(id); |
||||
|
return ResponseEntity.ok(Map.of( |
||||
|
"success", true, |
||||
|
"message", "目录删除成功", |
||||
|
"data", result |
||||
|
)); |
||||
|
} catch (Exception e) { |
||||
|
return ResponseEntity.status(500).body(Map.of( |
||||
|
"success", false, |
||||
|
"message", "删除目录失败:" + e.getMessage() |
||||
|
)); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,12 @@ |
|||||
|
package com.wok.supportbot.dao; |
||||
|
|
||||
|
import com.baomidou.mybatisplus.core.mapper.BaseMapper; |
||||
|
import com.wok.supportbot.entity.KnowledgeFolder; |
||||
|
import org.apache.ibatis.annotations.Mapper; |
||||
|
|
||||
|
/** |
||||
|
* 知识库文档目录 Mapper |
||||
|
*/ |
||||
|
@Mapper |
||||
|
public interface KnowledgeFolderMapper extends BaseMapper<KnowledgeFolder> { |
||||
|
} |
||||
@ -0,0 +1,63 @@ |
|||||
|
package com.wok.supportbot.entity; |
||||
|
|
||||
|
import com.fasterxml.jackson.databind.annotation.JsonSerialize; |
||||
|
import com.fasterxml.jackson.databind.ser.std.ToStringSerializer; |
||||
|
import lombok.AllArgsConstructor; |
||||
|
import lombok.Builder; |
||||
|
import lombok.Data; |
||||
|
import lombok.NoArgsConstructor; |
||||
|
|
||||
|
import java.io.Serial; |
||||
|
import java.io.Serializable; |
||||
|
import java.util.List; |
||||
|
|
||||
|
/** |
||||
|
* 目录树节点 - 用于返回树形结构 |
||||
|
*/ |
||||
|
@Data |
||||
|
@Builder |
||||
|
@AllArgsConstructor |
||||
|
@NoArgsConstructor |
||||
|
public class FolderNode implements Serializable { |
||||
|
|
||||
|
@Serial |
||||
|
private static final long serialVersionUID = 1L; |
||||
|
|
||||
|
/** |
||||
|
* 目录ID |
||||
|
*/ |
||||
|
@JsonSerialize(using = ToStringSerializer.class) |
||||
|
private Long id; |
||||
|
|
||||
|
/** |
||||
|
* 目录名称 |
||||
|
*/ |
||||
|
private String name; |
||||
|
|
||||
|
/** |
||||
|
* 所属分类ID |
||||
|
*/ |
||||
|
@JsonSerialize(using = ToStringSerializer.class) |
||||
|
private Long categoryId; |
||||
|
|
||||
|
/** |
||||
|
* 父目录ID |
||||
|
*/ |
||||
|
@JsonSerialize(using = ToStringSerializer.class) |
||||
|
private Long parentId; |
||||
|
|
||||
|
/** |
||||
|
* 排序权重 |
||||
|
*/ |
||||
|
private Integer sortOrder; |
||||
|
|
||||
|
/** |
||||
|
* 目录内文档数量 |
||||
|
*/ |
||||
|
private Integer documentCount; |
||||
|
|
||||
|
/** |
||||
|
* 子目录列表 |
||||
|
*/ |
||||
|
private List<FolderNode> children; |
||||
|
} |
||||
@ -0,0 +1,80 @@ |
|||||
|
package com.wok.supportbot.entity; |
||||
|
|
||||
|
import com.baomidou.mybatisplus.annotation.*; |
||||
|
import com.fasterxml.jackson.databind.annotation.JsonSerialize; |
||||
|
import com.fasterxml.jackson.databind.ser.std.ToStringSerializer; |
||||
|
import lombok.AllArgsConstructor; |
||||
|
import lombok.Builder; |
||||
|
import lombok.Data; |
||||
|
import lombok.NoArgsConstructor; |
||||
|
|
||||
|
import java.io.Serial; |
||||
|
import java.io.Serializable; |
||||
|
import java.util.Date; |
||||
|
|
||||
|
/** |
||||
|
* 知识库文档目录表 - 支持分类下的目录树形结构 |
||||
|
*/ |
||||
|
@Data |
||||
|
@Builder |
||||
|
@AllArgsConstructor |
||||
|
@NoArgsConstructor |
||||
|
@TableName("knowledge_folder") |
||||
|
public class KnowledgeFolder implements Serializable { |
||||
|
|
||||
|
@Serial |
||||
|
@TableField(exist = false) |
||||
|
private static final long serialVersionUID = 1L; |
||||
|
|
||||
|
/** |
||||
|
* 目录ID |
||||
|
*/ |
||||
|
@TableId(value = "id", type = IdType.ASSIGN_ID) |
||||
|
@JsonSerialize(using = ToStringSerializer.class) |
||||
|
private Long id; |
||||
|
|
||||
|
/** |
||||
|
* 所属分类ID |
||||
|
*/ |
||||
|
@TableField("category_id") |
||||
|
@JsonSerialize(using = ToStringSerializer.class) |
||||
|
private Long categoryId; |
||||
|
|
||||
|
/** |
||||
|
* 父目录ID,0=该分类下的根目录 |
||||
|
*/ |
||||
|
@TableField("parent_id") |
||||
|
@JsonSerialize(using = ToStringSerializer.class) |
||||
|
private Long parentId; |
||||
|
|
||||
|
/** |
||||
|
* 目录名称 |
||||
|
*/ |
||||
|
@TableField("name") |
||||
|
private String name; |
||||
|
|
||||
|
/** |
||||
|
* 排序权重(越大越靠前) |
||||
|
*/ |
||||
|
@TableField("sort_order") |
||||
|
private Integer sortOrder; |
||||
|
|
||||
|
/** |
||||
|
* 创建时间 |
||||
|
*/ |
||||
|
@TableField(value = "create_time", fill = FieldFill.INSERT) |
||||
|
private Date createTime; |
||||
|
|
||||
|
/** |
||||
|
* 更新时间 |
||||
|
*/ |
||||
|
@TableField(value = "update_time", fill = FieldFill.INSERT_UPDATE) |
||||
|
private Date updateTime; |
||||
|
|
||||
|
/** |
||||
|
* 逻辑删除标志 - false:未删除, true:已删除(逻辑删除) |
||||
|
*/ |
||||
|
@TableField("is_delete") |
||||
|
@TableLogic |
||||
|
private boolean isDelete; |
||||
|
} |
||||
@ -0,0 +1,313 @@ |
|||||
|
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; |
||||
|
} |
||||
|
} |
||||
Write
Preview
Loading…
Cancel
Save
Reference in new issue