feat(knowledge): 添加知识库目录管理功能

- 在知识条目表中新增relative_path字段用于存储条目相对路径
- 创建知识条目目录表用于管理知识库中的目录结构
- 实现目录的增删查接口和相应的应用服务逻辑
- 在前端知识库详情页面集成目录显示和操作功能
- 添加目录创建删除等相关的API接口和DTO定义
- 更新数据库初始化脚本包含新的目录表结构
This commit is contained in:
2026-01-31 18:36:40 +08:00
parent 310bc356b1
commit c23a9da8cb
16 changed files with 571 additions and 20 deletions

View File

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

View File

@@ -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<String> {
/**
* 所属知识集ID
*/
private String setId;
/**
* 目录名称
*/
private String name;
/**
* 目录相对路径
*/
private String relativePath;
}

View File

@@ -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<KnowledgeItemDirectory> {
}

View File

@@ -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<KnowledgeItemDirectory> {
List<KnowledgeItemDirectory> findByCriteria(KnowledgeDirectoryQuery query);
KnowledgeItemDirectory findBySetIdAndPath(String setId, String relativePath);
int removeByRelativePathPrefix(String setId, String relativePath);
}

View File

@@ -26,4 +26,8 @@ public interface KnowledgeItemRepository extends IRepository<KnowledgeItem> {
IPage<KnowledgeItemSearchResponse> searchFileItems(IPage<?> page, String keyword);
Long sumDatasetFileSize();
boolean existsBySetIdAndRelativePath(String setId, String relativePath);
int removeByRelativePathPrefix(String setId, String relativePath);
}

View File

@@ -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<KnowledgeItemDirectoryMapper, KnowledgeItemDirectory>
implements KnowledgeItemDirectoryRepository {
private static final String PATH_SEPARATOR = "/";
private final KnowledgeItemDirectoryMapper knowledgeItemDirectoryMapper;
@Override
public List<KnowledgeItemDirectory> findByCriteria(KnowledgeDirectoryQuery query) {
String relativePath = normalizeRelativePathPrefix(query.getRelativePath());
LambdaQueryWrapper<KnowledgeItemDirectory> wrapper = new LambdaQueryWrapper<KnowledgeItemDirectory>()
.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<KnowledgeItemDirectory>()
.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<KnowledgeItemDirectory> wrapper = new LambdaQueryWrapper<KnowledgeItemDirectory>()
.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;
}
}

View File

@@ -83,6 +83,31 @@ public class KnowledgeItemRepositoryImpl extends CrudRepository<KnowledgeItemMap
return knowledgeItemMapper.sumDatasetFileSize();
}
@Override
public boolean existsBySetIdAndRelativePath(String setId, String relativePath) {
if (StringUtils.isBlank(setId) || StringUtils.isBlank(relativePath)) {
return false;
}
return knowledgeItemMapper.selectCount(new LambdaQueryWrapper<KnowledgeItem>()
.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<KnowledgeItem> wrapper = new LambdaQueryWrapper<KnowledgeItem>()
.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<KnowledgeItemMap
}
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;
}
}

View File

@@ -1,9 +1,11 @@
package com.datamate.datamanagement.interfaces.converter;
import com.datamate.datamanagement.domain.model.knowledge.KnowledgeItem;
import com.datamate.datamanagement.domain.model.knowledge.KnowledgeItemDirectory;
import com.datamate.datamanagement.domain.model.knowledge.KnowledgeSet;
import com.datamate.datamanagement.interfaces.dto.CreateKnowledgeItemRequest;
import com.datamate.datamanagement.interfaces.dto.CreateKnowledgeSetRequest;
import com.datamate.datamanagement.interfaces.dto.KnowledgeDirectoryResponse;
import com.datamate.datamanagement.interfaces.dto.KnowledgeItemResponse;
import com.datamate.datamanagement.interfaces.dto.KnowledgeSetResponse;
import org.mapstruct.Mapper;
@@ -31,4 +33,8 @@ public interface KnowledgeConverter {
KnowledgeItemResponse convertToResponse(KnowledgeItem knowledgeItem);
List<KnowledgeItemResponse> convertItemResponses(List<KnowledgeItem> items);
KnowledgeDirectoryResponse convertToResponse(KnowledgeItemDirectory directory);
List<KnowledgeDirectoryResponse> convertDirectoryResponses(List<KnowledgeItemDirectory> directories);
}

View File

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

View File

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

View File

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

View File

@@ -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<KnowledgeDirectoryResponse> getKnowledgeDirectories(@PathVariable("setId") String setId,
KnowledgeDirectoryQuery query) {
List<KnowledgeItemDirectory> 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);
}
}