From c23a9da8cbd4294632a8ef1bb88d3a353717e29a Mon Sep 17 00:00:00 2001 From: Jerry Yan <792602257@qq.com> Date: Sat, 31 Jan 2026 18:36:40 +0800 Subject: [PATCH] =?UTF-8?q?feat(knowledge):=20=E6=B7=BB=E5=8A=A0=E7=9F=A5?= =?UTF-8?q?=E8=AF=86=E5=BA=93=E7=9B=AE=E5=BD=95=E7=AE=A1=E7=90=86=E5=8A=9F?= =?UTF-8?q?=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 在知识条目表中新增relative_path字段用于存储条目相对路径 - 创建知识条目目录表用于管理知识库中的目录结构 - 实现目录的增删查接口和相应的应用服务逻辑 - 在前端知识库详情页面集成目录显示和操作功能 - 添加目录创建删除等相关的API接口和DTO定义 - 更新数据库初始化脚本包含新的目录表结构 --- .../KnowledgeDirectoryApplicationService.java | 142 ++++++++++++++++++ .../knowledge/KnowledgeItemDirectory.java | 29 ++++ .../mapper/KnowledgeItemDirectoryMapper.java | 9 ++ .../KnowledgeItemDirectoryRepository.java | 18 +++ .../repository/KnowledgeItemRepository.java | 4 + .../KnowledgeItemDirectoryRepositoryImpl.java | 96 ++++++++++++ .../impl/KnowledgeItemRepositoryImpl.java | 39 +++++ .../converter/KnowledgeConverter.java | 6 + .../dto/CreateKnowledgeDirectoryRequest.java | 20 +++ .../dto/KnowledgeDirectoryQuery.java | 20 +++ .../dto/KnowledgeDirectoryResponse.java | 20 +++ .../rest/KnowledgeDirectoryController.java | 43 ++++++ .../Detail/KnowledgeSetDetail.tsx | 100 +++++++++--- .../knowledge-management.api.ts | 16 ++ .../knowledge-management.model.ts | 9 ++ scripts/db/data-management-init.sql | 20 ++- 16 files changed, 571 insertions(+), 20 deletions(-) create mode 100644 backend/services/data-management-service/src/main/java/com/datamate/datamanagement/application/KnowledgeDirectoryApplicationService.java create mode 100644 backend/services/data-management-service/src/main/java/com/datamate/datamanagement/domain/model/knowledge/KnowledgeItemDirectory.java create mode 100644 backend/services/data-management-service/src/main/java/com/datamate/datamanagement/infrastructure/persistence/mapper/KnowledgeItemDirectoryMapper.java create mode 100644 backend/services/data-management-service/src/main/java/com/datamate/datamanagement/infrastructure/persistence/repository/KnowledgeItemDirectoryRepository.java create mode 100644 backend/services/data-management-service/src/main/java/com/datamate/datamanagement/infrastructure/persistence/repository/impl/KnowledgeItemDirectoryRepositoryImpl.java create mode 100644 backend/services/data-management-service/src/main/java/com/datamate/datamanagement/interfaces/dto/CreateKnowledgeDirectoryRequest.java create mode 100644 backend/services/data-management-service/src/main/java/com/datamate/datamanagement/interfaces/dto/KnowledgeDirectoryQuery.java create mode 100644 backend/services/data-management-service/src/main/java/com/datamate/datamanagement/interfaces/dto/KnowledgeDirectoryResponse.java create mode 100644 backend/services/data-management-service/src/main/java/com/datamate/datamanagement/interfaces/rest/KnowledgeDirectoryController.java diff --git a/backend/services/data-management-service/src/main/java/com/datamate/datamanagement/application/KnowledgeDirectoryApplicationService.java b/backend/services/data-management-service/src/main/java/com/datamate/datamanagement/application/KnowledgeDirectoryApplicationService.java new file mode 100644 index 0000000..84e3e56 --- /dev/null +++ b/backend/services/data-management-service/src/main/java/com/datamate/datamanagement/application/KnowledgeDirectoryApplicationService.java @@ -0,0 +1,142 @@ +package com.datamate.datamanagement.application; + +import com.datamate.common.infrastructure.exception.BusinessAssert; +import com.datamate.common.infrastructure.exception.CommonErrorCode; +import com.datamate.datamanagement.common.enums.KnowledgeStatusType; +import com.datamate.datamanagement.domain.model.knowledge.KnowledgeItemDirectory; +import com.datamate.datamanagement.domain.model.knowledge.KnowledgeSet; +import com.datamate.datamanagement.infrastructure.exception.DataManagementErrorCode; +import com.datamate.datamanagement.infrastructure.persistence.repository.KnowledgeItemDirectoryRepository; +import com.datamate.datamanagement.infrastructure.persistence.repository.KnowledgeItemRepository; +import com.datamate.datamanagement.infrastructure.persistence.repository.KnowledgeSetRepository; +import com.datamate.datamanagement.interfaces.dto.CreateKnowledgeDirectoryRequest; +import com.datamate.datamanagement.interfaces.dto.KnowledgeDirectoryQuery; +import lombok.RequiredArgsConstructor; +import org.apache.commons.lang3.StringUtils; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.List; +import java.util.UUID; + +/** + * 知识条目目录应用服务 + */ +@Service +@Transactional +@RequiredArgsConstructor +public class KnowledgeDirectoryApplicationService { + private static final String PATH_SEPARATOR = "/"; + private static final String INVALID_PATH_SEGMENT = ".."; + + private final KnowledgeItemDirectoryRepository knowledgeItemDirectoryRepository; + private final KnowledgeItemRepository knowledgeItemRepository; + private final KnowledgeSetRepository knowledgeSetRepository; + + @Transactional(readOnly = true) + public List getKnowledgeDirectories(String setId, KnowledgeDirectoryQuery query) { + BusinessAssert.notNull(query, CommonErrorCode.PARAM_ERROR); + query.setSetId(setId); + return knowledgeItemDirectoryRepository.findByCriteria(query); + } + + public KnowledgeItemDirectory createKnowledgeDirectory(String setId, CreateKnowledgeDirectoryRequest request) { + BusinessAssert.notNull(request, CommonErrorCode.PARAM_ERROR); + KnowledgeSet knowledgeSet = requireKnowledgeSet(setId); + BusinessAssert.isTrue(!isReadOnlyStatus(knowledgeSet.getStatus()), + DataManagementErrorCode.KNOWLEDGE_SET_STATUS_ERROR); + + String directoryName = normalizeDirectoryName(request.getDirectoryName()); + validateDirectoryName(directoryName); + + String parentPrefix = normalizeRelativePathPrefix(request.getParentPrefix()); + String relativePath = normalizeRelativePathValue(parentPrefix + directoryName); + validateRelativePath(relativePath); + + BusinessAssert.isTrue(!knowledgeItemRepository.existsBySetIdAndRelativePath(setId, relativePath), + CommonErrorCode.PARAM_ERROR); + + KnowledgeItemDirectory existing = knowledgeItemDirectoryRepository.findBySetIdAndPath(setId, relativePath); + if (existing != null) { + return existing; + } + + KnowledgeItemDirectory directory = new KnowledgeItemDirectory(); + directory.setId(UUID.randomUUID().toString()); + directory.setSetId(setId); + directory.setName(directoryName); + directory.setRelativePath(relativePath); + knowledgeItemDirectoryRepository.save(directory); + return directory; + } + + public void deleteKnowledgeDirectory(String setId, String relativePath) { + KnowledgeSet knowledgeSet = requireKnowledgeSet(setId); + BusinessAssert.isTrue(!isReadOnlyStatus(knowledgeSet.getStatus()), + DataManagementErrorCode.KNOWLEDGE_SET_STATUS_ERROR); + + String normalized = normalizeRelativePathValue(relativePath); + validateRelativePath(normalized); + + knowledgeItemRepository.removeByRelativePathPrefix(setId, normalized); + knowledgeItemDirectoryRepository.removeByRelativePathPrefix(setId, normalized); + } + + private KnowledgeSet requireKnowledgeSet(String setId) { + KnowledgeSet knowledgeSet = knowledgeSetRepository.getById(setId); + BusinessAssert.notNull(knowledgeSet, DataManagementErrorCode.KNOWLEDGE_SET_NOT_FOUND); + return knowledgeSet; + } + + private boolean isReadOnlyStatus(KnowledgeStatusType status) { + return status == KnowledgeStatusType.ARCHIVED || status == KnowledgeStatusType.DEPRECATED; + } + + private String normalizeDirectoryName(String name) { + return StringUtils.trimToEmpty(name); + } + + private void validateDirectoryName(String name) { + BusinessAssert.isTrue(StringUtils.isNotBlank(name), CommonErrorCode.PARAM_ERROR); + BusinessAssert.isTrue(!name.contains(PATH_SEPARATOR), CommonErrorCode.PARAM_ERROR); + BusinessAssert.isTrue(!name.contains("\\"), CommonErrorCode.PARAM_ERROR); + BusinessAssert.isTrue(!name.contains(INVALID_PATH_SEGMENT), CommonErrorCode.PARAM_ERROR); + } + + private void validateRelativePath(String relativePath) { + BusinessAssert.isTrue(StringUtils.isNotBlank(relativePath), CommonErrorCode.PARAM_ERROR); + BusinessAssert.isTrue(!relativePath.contains(INVALID_PATH_SEGMENT), CommonErrorCode.PARAM_ERROR); + } + + private String normalizeRelativePathPrefix(String prefix) { + if (StringUtils.isBlank(prefix)) { + return ""; + } + String normalized = prefix.replace("\\", PATH_SEPARATOR).trim(); + while (normalized.startsWith(PATH_SEPARATOR)) { + normalized = normalized.substring(1); + } + while (normalized.endsWith(PATH_SEPARATOR)) { + normalized = normalized.substring(0, normalized.length() - 1); + } + if (StringUtils.isBlank(normalized)) { + return ""; + } + validateRelativePath(normalized); + return normalized + PATH_SEPARATOR; + } + + private String normalizeRelativePathValue(String relativePath) { + if (StringUtils.isBlank(relativePath)) { + return ""; + } + String normalized = relativePath.replace("\\", PATH_SEPARATOR).trim(); + while (normalized.startsWith(PATH_SEPARATOR)) { + normalized = normalized.substring(1); + } + while (normalized.endsWith(PATH_SEPARATOR)) { + normalized = normalized.substring(0, normalized.length() - 1); + } + return normalized; + } +} diff --git a/backend/services/data-management-service/src/main/java/com/datamate/datamanagement/domain/model/knowledge/KnowledgeItemDirectory.java b/backend/services/data-management-service/src/main/java/com/datamate/datamanagement/domain/model/knowledge/KnowledgeItemDirectory.java new file mode 100644 index 0000000..3fa858c --- /dev/null +++ b/backend/services/data-management-service/src/main/java/com/datamate/datamanagement/domain/model/knowledge/KnowledgeItemDirectory.java @@ -0,0 +1,29 @@ +package com.datamate.datamanagement.domain.model.knowledge; + +import com.baomidou.mybatisplus.annotation.TableName; +import com.datamate.common.domain.model.base.BaseEntity; +import lombok.Getter; +import lombok.Setter; + +/** + * 知识条目目录实体(与数据库表 t_dm_knowledge_item_directories 对齐) + */ +@Getter +@Setter +@TableName(value = "t_dm_knowledge_item_directories", autoResultMap = true) +public class KnowledgeItemDirectory extends BaseEntity { + /** + * 所属知识集ID + */ + private String setId; + + /** + * 目录名称 + */ + private String name; + + /** + * 目录相对路径 + */ + private String relativePath; +} diff --git a/backend/services/data-management-service/src/main/java/com/datamate/datamanagement/infrastructure/persistence/mapper/KnowledgeItemDirectoryMapper.java b/backend/services/data-management-service/src/main/java/com/datamate/datamanagement/infrastructure/persistence/mapper/KnowledgeItemDirectoryMapper.java new file mode 100644 index 0000000..108077d --- /dev/null +++ b/backend/services/data-management-service/src/main/java/com/datamate/datamanagement/infrastructure/persistence/mapper/KnowledgeItemDirectoryMapper.java @@ -0,0 +1,9 @@ +package com.datamate.datamanagement.infrastructure.persistence.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.datamate.datamanagement.domain.model.knowledge.KnowledgeItemDirectory; +import org.apache.ibatis.annotations.Mapper; + +@Mapper +public interface KnowledgeItemDirectoryMapper extends BaseMapper { +} diff --git a/backend/services/data-management-service/src/main/java/com/datamate/datamanagement/infrastructure/persistence/repository/KnowledgeItemDirectoryRepository.java b/backend/services/data-management-service/src/main/java/com/datamate/datamanagement/infrastructure/persistence/repository/KnowledgeItemDirectoryRepository.java new file mode 100644 index 0000000..ade6739 --- /dev/null +++ b/backend/services/data-management-service/src/main/java/com/datamate/datamanagement/infrastructure/persistence/repository/KnowledgeItemDirectoryRepository.java @@ -0,0 +1,18 @@ +package com.datamate.datamanagement.infrastructure.persistence.repository; + +import com.baomidou.mybatisplus.extension.repository.IRepository; +import com.datamate.datamanagement.domain.model.knowledge.KnowledgeItemDirectory; +import com.datamate.datamanagement.interfaces.dto.KnowledgeDirectoryQuery; + +import java.util.List; + +/** + * 知识条目目录仓储接口 + */ +public interface KnowledgeItemDirectoryRepository extends IRepository { + List findByCriteria(KnowledgeDirectoryQuery query); + + KnowledgeItemDirectory findBySetIdAndPath(String setId, String relativePath); + + int removeByRelativePathPrefix(String setId, String relativePath); +} diff --git a/backend/services/data-management-service/src/main/java/com/datamate/datamanagement/infrastructure/persistence/repository/KnowledgeItemRepository.java b/backend/services/data-management-service/src/main/java/com/datamate/datamanagement/infrastructure/persistence/repository/KnowledgeItemRepository.java index fc8188d..6acad30 100644 --- a/backend/services/data-management-service/src/main/java/com/datamate/datamanagement/infrastructure/persistence/repository/KnowledgeItemRepository.java +++ b/backend/services/data-management-service/src/main/java/com/datamate/datamanagement/infrastructure/persistence/repository/KnowledgeItemRepository.java @@ -26,4 +26,8 @@ public interface KnowledgeItemRepository extends IRepository { IPage searchFileItems(IPage page, String keyword); Long sumDatasetFileSize(); + + boolean existsBySetIdAndRelativePath(String setId, String relativePath); + + int removeByRelativePathPrefix(String setId, String relativePath); } diff --git a/backend/services/data-management-service/src/main/java/com/datamate/datamanagement/infrastructure/persistence/repository/impl/KnowledgeItemDirectoryRepositoryImpl.java b/backend/services/data-management-service/src/main/java/com/datamate/datamanagement/infrastructure/persistence/repository/impl/KnowledgeItemDirectoryRepositoryImpl.java new file mode 100644 index 0000000..8d17009 --- /dev/null +++ b/backend/services/data-management-service/src/main/java/com/datamate/datamanagement/infrastructure/persistence/repository/impl/KnowledgeItemDirectoryRepositoryImpl.java @@ -0,0 +1,96 @@ +package com.datamate.datamanagement.infrastructure.persistence.repository.impl; + +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.extension.repository.CrudRepository; +import com.datamate.datamanagement.domain.model.knowledge.KnowledgeItemDirectory; +import com.datamate.datamanagement.infrastructure.persistence.mapper.KnowledgeItemDirectoryMapper; +import com.datamate.datamanagement.infrastructure.persistence.repository.KnowledgeItemDirectoryRepository; +import com.datamate.datamanagement.interfaces.dto.KnowledgeDirectoryQuery; +import lombok.RequiredArgsConstructor; +import org.apache.commons.lang3.StringUtils; +import org.springframework.stereotype.Repository; + +import java.util.List; + +/** + * 知识条目目录仓储实现类 + */ +@Repository +@RequiredArgsConstructor +public class KnowledgeItemDirectoryRepositoryImpl + extends CrudRepository + implements KnowledgeItemDirectoryRepository { + + private static final String PATH_SEPARATOR = "/"; + private final KnowledgeItemDirectoryMapper knowledgeItemDirectoryMapper; + + @Override + public List findByCriteria(KnowledgeDirectoryQuery query) { + String relativePath = normalizeRelativePathPrefix(query.getRelativePath()); + LambdaQueryWrapper wrapper = new LambdaQueryWrapper() + .eq(StringUtils.isNotBlank(query.getSetId()), KnowledgeItemDirectory::getSetId, query.getSetId()) + .likeRight(StringUtils.isNotBlank(relativePath), KnowledgeItemDirectory::getRelativePath, relativePath); + + if (StringUtils.isNotBlank(query.getKeyword())) { + wrapper.and(w -> w.like(KnowledgeItemDirectory::getName, query.getKeyword()) + .or() + .like(KnowledgeItemDirectory::getRelativePath, query.getKeyword())); + } + + wrapper.orderByAsc(KnowledgeItemDirectory::getRelativePath); + return knowledgeItemDirectoryMapper.selectList(wrapper); + } + + @Override + public KnowledgeItemDirectory findBySetIdAndPath(String setId, String relativePath) { + return knowledgeItemDirectoryMapper.selectOne(new LambdaQueryWrapper() + .eq(KnowledgeItemDirectory::getSetId, setId) + .eq(KnowledgeItemDirectory::getRelativePath, relativePath)); + } + + @Override + public int removeByRelativePathPrefix(String setId, String relativePath) { + String normalized = normalizeRelativePathValue(relativePath); + if (StringUtils.isBlank(normalized)) { + return 0; + } + String prefix = normalizeRelativePathPrefix(normalized); + LambdaQueryWrapper wrapper = new LambdaQueryWrapper() + .eq(KnowledgeItemDirectory::getSetId, setId) + .and(w -> w.eq(KnowledgeItemDirectory::getRelativePath, normalized) + .or() + .likeRight(KnowledgeItemDirectory::getRelativePath, prefix)); + return knowledgeItemDirectoryMapper.delete(wrapper); + } + + private String normalizeRelativePathPrefix(String relativePath) { + if (StringUtils.isBlank(relativePath)) { + return ""; + } + String normalized = relativePath.replace("\\", PATH_SEPARATOR).trim(); + while (normalized.startsWith(PATH_SEPARATOR)) { + normalized = normalized.substring(1); + } + if (StringUtils.isBlank(normalized)) { + return ""; + } + if (!normalized.endsWith(PATH_SEPARATOR)) { + normalized = normalized + PATH_SEPARATOR; + } + return normalized; + } + + private String normalizeRelativePathValue(String relativePath) { + if (StringUtils.isBlank(relativePath)) { + return ""; + } + String normalized = relativePath.replace("\\", PATH_SEPARATOR).trim(); + while (normalized.startsWith(PATH_SEPARATOR)) { + normalized = normalized.substring(1); + } + while (normalized.endsWith(PATH_SEPARATOR)) { + normalized = normalized.substring(0, normalized.length() - 1); + } + return normalized; + } +} diff --git a/backend/services/data-management-service/src/main/java/com/datamate/datamanagement/infrastructure/persistence/repository/impl/KnowledgeItemRepositoryImpl.java b/backend/services/data-management-service/src/main/java/com/datamate/datamanagement/infrastructure/persistence/repository/impl/KnowledgeItemRepositoryImpl.java index df5e07c..e3aebbd 100644 --- a/backend/services/data-management-service/src/main/java/com/datamate/datamanagement/infrastructure/persistence/repository/impl/KnowledgeItemRepositoryImpl.java +++ b/backend/services/data-management-service/src/main/java/com/datamate/datamanagement/infrastructure/persistence/repository/impl/KnowledgeItemRepositoryImpl.java @@ -83,6 +83,31 @@ public class KnowledgeItemRepositoryImpl extends CrudRepository() + .eq(KnowledgeItem::getSetId, setId) + .eq(KnowledgeItem::getRelativePath, relativePath)) > 0; + } + + @Override + public int removeByRelativePathPrefix(String setId, String relativePath) { + String normalized = normalizeRelativePathValue(relativePath); + if (StringUtils.isBlank(setId) || StringUtils.isBlank(normalized)) { + return 0; + } + String prefix = normalizeRelativePathPrefix(normalized); + LambdaQueryWrapper wrapper = new LambdaQueryWrapper() + .eq(KnowledgeItem::getSetId, setId) + .and(w -> w.eq(KnowledgeItem::getRelativePath, normalized) + .or() + .likeRight(KnowledgeItem::getRelativePath, prefix)); + return knowledgeItemMapper.delete(wrapper); + } + private String normalizeRelativePathPrefix(String relativePath) { if (StringUtils.isBlank(relativePath)) { return ""; @@ -99,4 +124,18 @@ public class KnowledgeItemRepositoryImpl extends CrudRepository convertItemResponses(List items); + + KnowledgeDirectoryResponse convertToResponse(KnowledgeItemDirectory directory); + + List convertDirectoryResponses(List directories); } diff --git a/backend/services/data-management-service/src/main/java/com/datamate/datamanagement/interfaces/dto/CreateKnowledgeDirectoryRequest.java b/backend/services/data-management-service/src/main/java/com/datamate/datamanagement/interfaces/dto/CreateKnowledgeDirectoryRequest.java new file mode 100644 index 0000000..10c0ce2 --- /dev/null +++ b/backend/services/data-management-service/src/main/java/com/datamate/datamanagement/interfaces/dto/CreateKnowledgeDirectoryRequest.java @@ -0,0 +1,20 @@ +package com.datamate.datamanagement.interfaces.dto; + +import jakarta.validation.constraints.NotBlank; +import lombok.Getter; +import lombok.Setter; + +/** + * 创建知识条目目录请求 + */ +@Getter +@Setter +public class CreateKnowledgeDirectoryRequest { + + /** 父级前缀路径,例如 "docs/",为空表示知识集根目录 */ + private String parentPrefix; + + /** 新建目录名称 */ + @NotBlank + private String directoryName; +} diff --git a/backend/services/data-management-service/src/main/java/com/datamate/datamanagement/interfaces/dto/KnowledgeDirectoryQuery.java b/backend/services/data-management-service/src/main/java/com/datamate/datamanagement/interfaces/dto/KnowledgeDirectoryQuery.java new file mode 100644 index 0000000..734fe0a --- /dev/null +++ b/backend/services/data-management-service/src/main/java/com/datamate/datamanagement/interfaces/dto/KnowledgeDirectoryQuery.java @@ -0,0 +1,20 @@ +package com.datamate.datamanagement.interfaces.dto; + +import lombok.Getter; +import lombok.Setter; + +/** + * 知识条目目录查询参数 + */ +@Getter +@Setter +public class KnowledgeDirectoryQuery { + /** 所属知识集ID */ + private String setId; + + /** 目录相对路径前缀 */ + private String relativePath; + + /** 搜索关键字 */ + private String keyword; +} diff --git a/backend/services/data-management-service/src/main/java/com/datamate/datamanagement/interfaces/dto/KnowledgeDirectoryResponse.java b/backend/services/data-management-service/src/main/java/com/datamate/datamanagement/interfaces/dto/KnowledgeDirectoryResponse.java new file mode 100644 index 0000000..fd3219a --- /dev/null +++ b/backend/services/data-management-service/src/main/java/com/datamate/datamanagement/interfaces/dto/KnowledgeDirectoryResponse.java @@ -0,0 +1,20 @@ +package com.datamate.datamanagement.interfaces.dto; + +import lombok.Getter; +import lombok.Setter; + +import java.time.LocalDateTime; + +/** + * 知识条目目录响应 + */ +@Getter +@Setter +public class KnowledgeDirectoryResponse { + private String id; + private String setId; + private String name; + private String relativePath; + private LocalDateTime createdAt; + private LocalDateTime updatedAt; +} diff --git a/backend/services/data-management-service/src/main/java/com/datamate/datamanagement/interfaces/rest/KnowledgeDirectoryController.java b/backend/services/data-management-service/src/main/java/com/datamate/datamanagement/interfaces/rest/KnowledgeDirectoryController.java new file mode 100644 index 0000000..213d6d5 --- /dev/null +++ b/backend/services/data-management-service/src/main/java/com/datamate/datamanagement/interfaces/rest/KnowledgeDirectoryController.java @@ -0,0 +1,43 @@ +package com.datamate.datamanagement.interfaces.rest; + +import com.datamate.datamanagement.application.KnowledgeDirectoryApplicationService; +import com.datamate.datamanagement.domain.model.knowledge.KnowledgeItemDirectory; +import com.datamate.datamanagement.interfaces.converter.KnowledgeConverter; +import com.datamate.datamanagement.interfaces.dto.CreateKnowledgeDirectoryRequest; +import com.datamate.datamanagement.interfaces.dto.KnowledgeDirectoryQuery; +import com.datamate.datamanagement.interfaces.dto.KnowledgeDirectoryResponse; +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import org.springframework.web.bind.annotation.*; + +import java.util.List; + +/** + * 知识条目目录 REST 控制器 + */ +@RestController +@RequiredArgsConstructor +@RequestMapping("/data-management/knowledge-sets/{setId}/directories") +public class KnowledgeDirectoryController { + private final KnowledgeDirectoryApplicationService knowledgeDirectoryApplicationService; + + @GetMapping + public List getKnowledgeDirectories(@PathVariable("setId") String setId, + KnowledgeDirectoryQuery query) { + List directories = knowledgeDirectoryApplicationService.getKnowledgeDirectories(setId, query); + return KnowledgeConverter.INSTANCE.convertDirectoryResponses(directories); + } + + @PostMapping + public KnowledgeDirectoryResponse createKnowledgeDirectory(@PathVariable("setId") String setId, + @RequestBody @Valid CreateKnowledgeDirectoryRequest request) { + KnowledgeItemDirectory directory = knowledgeDirectoryApplicationService.createKnowledgeDirectory(setId, request); + return KnowledgeConverter.INSTANCE.convertToResponse(directory); + } + + @DeleteMapping + public void deleteKnowledgeDirectory(@PathVariable("setId") String setId, + @RequestParam("relativePath") String relativePath) { + knowledgeDirectoryApplicationService.deleteKnowledgeDirectory(setId, relativePath); + } +} diff --git a/frontend/src/pages/KnowledgeManagement/Detail/KnowledgeSetDetail.tsx b/frontend/src/pages/KnowledgeManagement/Detail/KnowledgeSetDetail.tsx index 923e6a6..d52a367 100644 --- a/frontend/src/pages/KnowledgeManagement/Detail/KnowledgeSetDetail.tsx +++ b/frontend/src/pages/KnowledgeManagement/Detail/KnowledgeSetDetail.tsx @@ -16,11 +16,13 @@ import { useNavigate, useParams } from "react-router"; import DetailHeader from "@/components/DetailHeader"; import { SearchControls } from "@/components/SearchControls"; import { + createKnowledgeDirectoryUsingPost, + deleteKnowledgeDirectoryUsingDelete, deleteKnowledgeItemByIdUsingDelete, - deleteKnowledgeItemsByIdsUsingPost, deleteKnowledgeSetByIdUsingDelete, downloadKnowledgeItemFileUsingGet, exportKnowledgeItemsUsingGet, + queryKnowledgeDirectoriesUsingGet, queryKnowledgeItemsUsingGet, queryKnowledgeSetByIdUsingGet, } from "../knowledge-management.api"; @@ -33,6 +35,7 @@ import { } from "../knowledge-management.const"; import { KnowledgeItem, + KnowledgeDirectory, KnowledgeSet, KnowledgeContentType, KnowledgeSourceType, @@ -108,6 +111,8 @@ const KnowledgeSetDetail = () => { const [fileKeyword, setFileKeyword] = useState(""); const [itemsLoading, setItemsLoading] = useState(false); const [allItems, setAllItems] = useState([]); + const [directoriesLoading, setDirectoriesLoading] = useState(false); + const [allDirectories, setAllDirectories] = useState([]); const [filePagination, setFilePagination] = useState({ current: 1, pageSize: 10, @@ -157,6 +162,29 @@ const KnowledgeSetDetail = () => { } }, [fileKeyword, filePrefix, id, message]); + const fetchDirectories = useCallback(async () => { + if (!id) { + setAllDirectories([]); + return; + } + setDirectoriesLoading(true); + try { + const currentPrefix = normalizePrefix(filePrefix); + const keyword = fileKeyword.trim(); + const { data } = await queryKnowledgeDirectoriesUsingGet(id, { + ...(currentPrefix ? { relativePath: currentPrefix } : {}), + ...(keyword ? { keyword } : {}), + }); + const directories = Array.isArray(data) ? data : []; + setAllDirectories(directories); + } catch (error) { + console.error("加载知识目录失败", error); + message.error("知识目录加载失败"); + } finally { + setDirectoriesLoading(false); + } + }, [fileKeyword, filePrefix, id, message]); + useEffect(() => { fetchKnowledgeSet(); }, [fetchKnowledgeSet]); @@ -164,8 +192,9 @@ const KnowledgeSetDetail = () => { useEffect(() => { if (id) { fetchItems(); + fetchDirectories(); } - }, [id, fetchItems]); + }, [id, fetchItems, fetchDirectories]); useEffect(() => { setFilePagination((prev) => ({ ...prev, current: 1 })); @@ -213,6 +242,11 @@ const KnowledgeSetDetail = () => { return normalizePath(rawPath).replace(/^\/+/, ""); }, []); + const resolveDirectoryRelativePath = useCallback((directory: KnowledgeDirectory) => { + const rawPath = directory.relativePath || ""; + return normalizePath(rawPath).replace(/^\/+/, ""); + }, []); + const resolveDisplayName = useCallback( (record: KnowledgeItemView) => { const relativePath = resolveItemRelativePath(record); @@ -395,21 +429,12 @@ const KnowledgeSetDetail = () => { return; } const currentPrefix = normalizePrefix(filePrefix); - const directoryPrefix = normalizePrefix(`${currentPrefix}${directoryName}`); - const targetIds = allItems - .filter((item) => { - const fullPath = resolveItemRelativePath(item); - return fullPath.startsWith(directoryPrefix); - }) - .map((item) => item.id); - if (targetIds.length === 0) { - message.info("该文件夹为空"); - return; - } + const directoryPath = normalizePrefix(`${currentPrefix}${directoryName}`).replace(/\/$/, ""); try { - await deleteKnowledgeItemsByIdsUsingPost(id, { ids: targetIds }); - message.success(`已删除 ${targetIds.length} 个条目`); + await deleteKnowledgeDirectoryUsingDelete(id, directoryPath); + message.success("文件夹已删除"); fetchItems(); + fetchDirectories(); } catch (error) { console.error("删除文件夹失败", error); message.error("文件夹删除失败"); @@ -458,6 +483,21 @@ const KnowledgeSetDetail = () => { }); }); + allDirectories.forEach((directory) => { + const fullPath = resolveDirectoryRelativePath(directory); + if (!fullPath) { + return; + } + const segments = splitRelativePath(fullPath, normalizedPrefix); + if (segments.length === 0) { + return; + } + const leafName = segments[0]; + if (!folderMap.has(leafName)) { + folderMap.set(leafName, { name: leafName, fileCount: 0 }); + } + }); + const folderItems: KnowledgeItemRow[] = Array.from(folderMap.values()).map((entry) => ({ id: `directory-${normalizedPrefix}${entry.name}`, setId: id || "", @@ -486,7 +526,15 @@ const KnowledgeSetDetail = () => { const combined = [...folderItems, ...fileItems]; return { rows: combined, total: combined.length }; - }, [allItems, id, normalizedPrefix, resolveDisplayName, resolveItemRelativePath]); + }, [ + allDirectories, + allItems, + id, + normalizedPrefix, + resolveDisplayName, + resolveDirectoryRelativePath, + resolveItemRelativePath, + ]); const pageCurrent = filePagination.current; const pageSize = filePagination.pageSize; @@ -786,9 +834,23 @@ const KnowledgeSetDetail = () => { message.warning("请输入合法的文件夹名称"); return Promise.reject(); } + if (!id) { + return Promise.reject(); + } const currentPrefix = normalizePrefix(filePrefix); - const nextPrefix = normalizePrefix(`${currentPrefix}${dirName}`); - setFilePrefix(nextPrefix); + try { + await createKnowledgeDirectoryUsingPost(id, { + parentPrefix: currentPrefix, + directoryName: dirName, + }); + message.success("文件夹已创建"); + const nextPrefix = normalizePrefix(`${currentPrefix}${dirName}`); + setFilePrefix(nextPrefix); + } catch (error) { + console.error("创建文件夹失败", error); + message.error("创建文件夹失败"); + return Promise.reject(); + } }, }); }} @@ -846,7 +908,7 @@ const KnowledgeSetDetail = () => { ) : ( ) { + return get(`/api/data-management/knowledge-sets/${setId}/directories`, params); +} + +// 创建知识条目目录 +export function createKnowledgeDirectoryUsingPost(setId: string, data: Record) { + return post(`/api/data-management/knowledge-sets/${setId}/directories`, data); +} + +// 删除知识条目目录 +export function deleteKnowledgeDirectoryUsingDelete(setId: string, relativePath: string) { + const query = new URLSearchParams({ relativePath }).toString(); + return del(`/api/data-management/knowledge-sets/${setId}/directories?${query}`); +} + // 知识条目文件搜索 export function searchKnowledgeItemsUsingGet(params?: Record) { return get("/api/data-management/knowledge-items/search", params); diff --git a/frontend/src/pages/KnowledgeManagement/knowledge-management.model.ts b/frontend/src/pages/KnowledgeManagement/knowledge-management.model.ts index b4770c7..30781d2 100644 --- a/frontend/src/pages/KnowledgeManagement/knowledge-management.model.ts +++ b/frontend/src/pages/KnowledgeManagement/knowledge-management.model.ts @@ -69,6 +69,15 @@ export interface KnowledgeItem { updatedBy?: string; } +export interface KnowledgeDirectory { + id: string; + setId: string; + name: string; + relativePath: string; + createdAt?: string; + updatedAt?: string; +} + export interface KnowledgeManagementStatistics { totalKnowledgeSets: number; totalFiles: number; diff --git a/scripts/db/data-management-init.sql b/scripts/db/data-management-init.sql index ed8d40a..664fd50 100644 --- a/scripts/db/data-management-init.sql +++ b/scripts/db/data-management-init.sql @@ -158,6 +158,7 @@ CREATE TABLE IF NOT EXISTS t_dm_knowledge_items ( sensitivity VARCHAR(50) COMMENT '敏感级别', source_dataset_id VARCHAR(36) COMMENT '来源数据集ID', source_file_id VARCHAR(36) COMMENT '来源文件ID', + relative_path VARCHAR(1000) COMMENT '条目相对路径', tags JSON COMMENT '标签列表', metadata JSON COMMENT '扩展元数据', created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', @@ -174,9 +175,26 @@ CREATE TABLE IF NOT EXISTS t_dm_knowledge_items ( INDEX idx_dm_ki_valid_from (valid_from), INDEX idx_dm_ki_valid_to (valid_to), INDEX idx_dm_ki_source_dataset (source_dataset_id), - INDEX idx_dm_ki_source_file (source_file_id) + INDEX idx_dm_ki_source_file (source_file_id), + INDEX idx_dm_ki_relative_path (relative_path) ) COMMENT='知识条目表(UUID 主键)'; +-- 知识条目目录表 +CREATE TABLE IF NOT EXISTS t_dm_knowledge_item_directories ( + id VARCHAR(36) PRIMARY KEY COMMENT 'UUID', + set_id VARCHAR(36) NOT NULL COMMENT '所属知识集ID(UUID)', + name VARCHAR(255) NOT NULL COMMENT '目录名称', + relative_path VARCHAR(1000) NOT NULL COMMENT '目录相对路径', + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', + created_by VARCHAR(255) COMMENT '创建者', + updated_by VARCHAR(255) COMMENT '更新者', + FOREIGN KEY (set_id) REFERENCES t_dm_knowledge_sets(id) ON DELETE CASCADE, + UNIQUE KEY uk_dm_kd_set_path (set_id, relative_path), + INDEX idx_dm_kd_set_id (set_id), + INDEX idx_dm_kd_relative_path (relative_path) +) COMMENT='知识条目目录表(UUID 主键)'; + -- =========================================== -- 非数据管理表(如 users、t_data_sources)保持不变 -- ===========================================