You've already forked DataMate
feat(knowledge): 添加知识库目录管理功能
- 在知识条目表中新增relative_path字段用于存储条目相对路径 - 创建知识条目目录表用于管理知识库中的目录结构 - 实现目录的增删查接口和相应的应用服务逻辑 - 在前端知识库详情页面集成目录显示和操作功能 - 添加目录创建删除等相关的API接口和DTO定义 - 更新数据库初始化脚本包含新的目录表结构
This commit is contained in:
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
@@ -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> {
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
@@ -26,4 +26,8 @@ public interface KnowledgeItemRepository extends IRepository<KnowledgeItem> {
|
|||||||
IPage<KnowledgeItemSearchResponse> searchFileItems(IPage<?> page, String keyword);
|
IPage<KnowledgeItemSearchResponse> searchFileItems(IPage<?> page, String keyword);
|
||||||
|
|
||||||
Long sumDatasetFileSize();
|
Long sumDatasetFileSize();
|
||||||
|
|
||||||
|
boolean existsBySetIdAndRelativePath(String setId, String relativePath);
|
||||||
|
|
||||||
|
int removeByRelativePathPrefix(String setId, String relativePath);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -83,6 +83,31 @@ public class KnowledgeItemRepositoryImpl extends CrudRepository<KnowledgeItemMap
|
|||||||
return knowledgeItemMapper.sumDatasetFileSize();
|
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) {
|
private String normalizeRelativePathPrefix(String relativePath) {
|
||||||
if (StringUtils.isBlank(relativePath)) {
|
if (StringUtils.isBlank(relativePath)) {
|
||||||
return "";
|
return "";
|
||||||
@@ -99,4 +124,18 @@ public class KnowledgeItemRepositoryImpl extends CrudRepository<KnowledgeItemMap
|
|||||||
}
|
}
|
||||||
return normalized;
|
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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,9 +1,11 @@
|
|||||||
package com.datamate.datamanagement.interfaces.converter;
|
package com.datamate.datamanagement.interfaces.converter;
|
||||||
|
|
||||||
import com.datamate.datamanagement.domain.model.knowledge.KnowledgeItem;
|
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.domain.model.knowledge.KnowledgeSet;
|
||||||
import com.datamate.datamanagement.interfaces.dto.CreateKnowledgeItemRequest;
|
import com.datamate.datamanagement.interfaces.dto.CreateKnowledgeItemRequest;
|
||||||
import com.datamate.datamanagement.interfaces.dto.CreateKnowledgeSetRequest;
|
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.KnowledgeItemResponse;
|
||||||
import com.datamate.datamanagement.interfaces.dto.KnowledgeSetResponse;
|
import com.datamate.datamanagement.interfaces.dto.KnowledgeSetResponse;
|
||||||
import org.mapstruct.Mapper;
|
import org.mapstruct.Mapper;
|
||||||
@@ -31,4 +33,8 @@ public interface KnowledgeConverter {
|
|||||||
KnowledgeItemResponse convertToResponse(KnowledgeItem knowledgeItem);
|
KnowledgeItemResponse convertToResponse(KnowledgeItem knowledgeItem);
|
||||||
|
|
||||||
List<KnowledgeItemResponse> convertItemResponses(List<KnowledgeItem> items);
|
List<KnowledgeItemResponse> convertItemResponses(List<KnowledgeItem> items);
|
||||||
|
|
||||||
|
KnowledgeDirectoryResponse convertToResponse(KnowledgeItemDirectory directory);
|
||||||
|
|
||||||
|
List<KnowledgeDirectoryResponse> convertDirectoryResponses(List<KnowledgeItemDirectory> directories);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -16,11 +16,13 @@ import { useNavigate, useParams } from "react-router";
|
|||||||
import DetailHeader from "@/components/DetailHeader";
|
import DetailHeader from "@/components/DetailHeader";
|
||||||
import { SearchControls } from "@/components/SearchControls";
|
import { SearchControls } from "@/components/SearchControls";
|
||||||
import {
|
import {
|
||||||
|
createKnowledgeDirectoryUsingPost,
|
||||||
|
deleteKnowledgeDirectoryUsingDelete,
|
||||||
deleteKnowledgeItemByIdUsingDelete,
|
deleteKnowledgeItemByIdUsingDelete,
|
||||||
deleteKnowledgeItemsByIdsUsingPost,
|
|
||||||
deleteKnowledgeSetByIdUsingDelete,
|
deleteKnowledgeSetByIdUsingDelete,
|
||||||
downloadKnowledgeItemFileUsingGet,
|
downloadKnowledgeItemFileUsingGet,
|
||||||
exportKnowledgeItemsUsingGet,
|
exportKnowledgeItemsUsingGet,
|
||||||
|
queryKnowledgeDirectoriesUsingGet,
|
||||||
queryKnowledgeItemsUsingGet,
|
queryKnowledgeItemsUsingGet,
|
||||||
queryKnowledgeSetByIdUsingGet,
|
queryKnowledgeSetByIdUsingGet,
|
||||||
} from "../knowledge-management.api";
|
} from "../knowledge-management.api";
|
||||||
@@ -33,6 +35,7 @@ import {
|
|||||||
} from "../knowledge-management.const";
|
} from "../knowledge-management.const";
|
||||||
import {
|
import {
|
||||||
KnowledgeItem,
|
KnowledgeItem,
|
||||||
|
KnowledgeDirectory,
|
||||||
KnowledgeSet,
|
KnowledgeSet,
|
||||||
KnowledgeContentType,
|
KnowledgeContentType,
|
||||||
KnowledgeSourceType,
|
KnowledgeSourceType,
|
||||||
@@ -108,6 +111,8 @@ const KnowledgeSetDetail = () => {
|
|||||||
const [fileKeyword, setFileKeyword] = useState("");
|
const [fileKeyword, setFileKeyword] = useState("");
|
||||||
const [itemsLoading, setItemsLoading] = useState(false);
|
const [itemsLoading, setItemsLoading] = useState(false);
|
||||||
const [allItems, setAllItems] = useState<KnowledgeItemView[]>([]);
|
const [allItems, setAllItems] = useState<KnowledgeItemView[]>([]);
|
||||||
|
const [directoriesLoading, setDirectoriesLoading] = useState(false);
|
||||||
|
const [allDirectories, setAllDirectories] = useState<KnowledgeDirectory[]>([]);
|
||||||
const [filePagination, setFilePagination] = useState({
|
const [filePagination, setFilePagination] = useState({
|
||||||
current: 1,
|
current: 1,
|
||||||
pageSize: 10,
|
pageSize: 10,
|
||||||
@@ -157,6 +162,29 @@ const KnowledgeSetDetail = () => {
|
|||||||
}
|
}
|
||||||
}, [fileKeyword, filePrefix, id, message]);
|
}, [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(() => {
|
useEffect(() => {
|
||||||
fetchKnowledgeSet();
|
fetchKnowledgeSet();
|
||||||
}, [fetchKnowledgeSet]);
|
}, [fetchKnowledgeSet]);
|
||||||
@@ -164,8 +192,9 @@ const KnowledgeSetDetail = () => {
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (id) {
|
if (id) {
|
||||||
fetchItems();
|
fetchItems();
|
||||||
|
fetchDirectories();
|
||||||
}
|
}
|
||||||
}, [id, fetchItems]);
|
}, [id, fetchItems, fetchDirectories]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setFilePagination((prev) => ({ ...prev, current: 1 }));
|
setFilePagination((prev) => ({ ...prev, current: 1 }));
|
||||||
@@ -213,6 +242,11 @@ const KnowledgeSetDetail = () => {
|
|||||||
return normalizePath(rawPath).replace(/^\/+/, "");
|
return normalizePath(rawPath).replace(/^\/+/, "");
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
const resolveDirectoryRelativePath = useCallback((directory: KnowledgeDirectory) => {
|
||||||
|
const rawPath = directory.relativePath || "";
|
||||||
|
return normalizePath(rawPath).replace(/^\/+/, "");
|
||||||
|
}, []);
|
||||||
|
|
||||||
const resolveDisplayName = useCallback(
|
const resolveDisplayName = useCallback(
|
||||||
(record: KnowledgeItemView) => {
|
(record: KnowledgeItemView) => {
|
||||||
const relativePath = resolveItemRelativePath(record);
|
const relativePath = resolveItemRelativePath(record);
|
||||||
@@ -395,21 +429,12 @@ const KnowledgeSetDetail = () => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const currentPrefix = normalizePrefix(filePrefix);
|
const currentPrefix = normalizePrefix(filePrefix);
|
||||||
const directoryPrefix = normalizePrefix(`${currentPrefix}${directoryName}`);
|
const directoryPath = normalizePrefix(`${currentPrefix}${directoryName}`).replace(/\/$/, "");
|
||||||
const targetIds = allItems
|
|
||||||
.filter((item) => {
|
|
||||||
const fullPath = resolveItemRelativePath(item);
|
|
||||||
return fullPath.startsWith(directoryPrefix);
|
|
||||||
})
|
|
||||||
.map((item) => item.id);
|
|
||||||
if (targetIds.length === 0) {
|
|
||||||
message.info("该文件夹为空");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
try {
|
try {
|
||||||
await deleteKnowledgeItemsByIdsUsingPost(id, { ids: targetIds });
|
await deleteKnowledgeDirectoryUsingDelete(id, directoryPath);
|
||||||
message.success(`已删除 ${targetIds.length} 个条目`);
|
message.success("文件夹已删除");
|
||||||
fetchItems();
|
fetchItems();
|
||||||
|
fetchDirectories();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("删除文件夹失败", error);
|
console.error("删除文件夹失败", error);
|
||||||
message.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) => ({
|
const folderItems: KnowledgeItemRow[] = Array.from(folderMap.values()).map((entry) => ({
|
||||||
id: `directory-${normalizedPrefix}${entry.name}`,
|
id: `directory-${normalizedPrefix}${entry.name}`,
|
||||||
setId: id || "",
|
setId: id || "",
|
||||||
@@ -486,7 +526,15 @@ const KnowledgeSetDetail = () => {
|
|||||||
|
|
||||||
const combined = [...folderItems, ...fileItems];
|
const combined = [...folderItems, ...fileItems];
|
||||||
return { rows: combined, total: combined.length };
|
return { rows: combined, total: combined.length };
|
||||||
}, [allItems, id, normalizedPrefix, resolveDisplayName, resolveItemRelativePath]);
|
}, [
|
||||||
|
allDirectories,
|
||||||
|
allItems,
|
||||||
|
id,
|
||||||
|
normalizedPrefix,
|
||||||
|
resolveDisplayName,
|
||||||
|
resolveDirectoryRelativePath,
|
||||||
|
resolveItemRelativePath,
|
||||||
|
]);
|
||||||
|
|
||||||
const pageCurrent = filePagination.current;
|
const pageCurrent = filePagination.current;
|
||||||
const pageSize = filePagination.pageSize;
|
const pageSize = filePagination.pageSize;
|
||||||
@@ -786,9 +834,23 @@ const KnowledgeSetDetail = () => {
|
|||||||
message.warning("请输入合法的文件夹名称");
|
message.warning("请输入合法的文件夹名称");
|
||||||
return Promise.reject();
|
return Promise.reject();
|
||||||
}
|
}
|
||||||
|
if (!id) {
|
||||||
|
return Promise.reject();
|
||||||
|
}
|
||||||
const currentPrefix = normalizePrefix(filePrefix);
|
const currentPrefix = normalizePrefix(filePrefix);
|
||||||
const nextPrefix = normalizePrefix(`${currentPrefix}${dirName}`);
|
try {
|
||||||
setFilePrefix(nextPrefix);
|
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 = () => {
|
|||||||
<Empty description="暂无知识条目" />
|
<Empty description="暂无知识条目" />
|
||||||
) : (
|
) : (
|
||||||
<Table
|
<Table
|
||||||
loading={itemsLoading}
|
loading={itemsLoading || directoriesLoading}
|
||||||
columns={itemColumns}
|
columns={itemColumns}
|
||||||
dataSource={pagedItemRows}
|
dataSource={pagedItemRows}
|
||||||
rowKey="id"
|
rowKey="id"
|
||||||
|
|||||||
@@ -35,6 +35,22 @@ export function queryKnowledgeItemsUsingGet(setId: string, params?: Record<strin
|
|||||||
return get(`/api/data-management/knowledge-sets/${setId}/items`, params);
|
return get(`/api/data-management/knowledge-sets/${setId}/items`, params);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 知识条目目录列表
|
||||||
|
export function queryKnowledgeDirectoriesUsingGet(setId: string, params?: Record<string, unknown>) {
|
||||||
|
return get(`/api/data-management/knowledge-sets/${setId}/directories`, params);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 创建知识条目目录
|
||||||
|
export function createKnowledgeDirectoryUsingPost(setId: string, data: Record<string, unknown>) {
|
||||||
|
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<string, unknown>) {
|
export function searchKnowledgeItemsUsingGet(params?: Record<string, unknown>) {
|
||||||
return get("/api/data-management/knowledge-items/search", params);
|
return get("/api/data-management/knowledge-items/search", params);
|
||||||
|
|||||||
@@ -69,6 +69,15 @@ export interface KnowledgeItem {
|
|||||||
updatedBy?: string;
|
updatedBy?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface KnowledgeDirectory {
|
||||||
|
id: string;
|
||||||
|
setId: string;
|
||||||
|
name: string;
|
||||||
|
relativePath: string;
|
||||||
|
createdAt?: string;
|
||||||
|
updatedAt?: string;
|
||||||
|
}
|
||||||
|
|
||||||
export interface KnowledgeManagementStatistics {
|
export interface KnowledgeManagementStatistics {
|
||||||
totalKnowledgeSets: number;
|
totalKnowledgeSets: number;
|
||||||
totalFiles: number;
|
totalFiles: number;
|
||||||
|
|||||||
@@ -158,6 +158,7 @@ CREATE TABLE IF NOT EXISTS t_dm_knowledge_items (
|
|||||||
sensitivity VARCHAR(50) COMMENT '敏感级别',
|
sensitivity VARCHAR(50) COMMENT '敏感级别',
|
||||||
source_dataset_id VARCHAR(36) COMMENT '来源数据集ID',
|
source_dataset_id VARCHAR(36) COMMENT '来源数据集ID',
|
||||||
source_file_id VARCHAR(36) COMMENT '来源文件ID',
|
source_file_id VARCHAR(36) COMMENT '来源文件ID',
|
||||||
|
relative_path VARCHAR(1000) COMMENT '条目相对路径',
|
||||||
tags JSON COMMENT '标签列表',
|
tags JSON COMMENT '标签列表',
|
||||||
metadata JSON COMMENT '扩展元数据',
|
metadata JSON COMMENT '扩展元数据',
|
||||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP 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_from (valid_from),
|
||||||
INDEX idx_dm_ki_valid_to (valid_to),
|
INDEX idx_dm_ki_valid_to (valid_to),
|
||||||
INDEX idx_dm_ki_source_dataset (source_dataset_id),
|
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 主键)';
|
) 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)保持不变
|
-- 非数据管理表(如 users、t_data_sources)保持不变
|
||||||
-- ===========================================
|
-- ===========================================
|
||||||
|
|||||||
Reference in New Issue
Block a user