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);
|
||||
|
||||
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();
|
||||
}
|
||||
|
||||
@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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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 { 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<KnowledgeItemView[]>([]);
|
||||
const [directoriesLoading, setDirectoriesLoading] = useState(false);
|
||||
const [allDirectories, setAllDirectories] = useState<KnowledgeDirectory[]>([]);
|
||||
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);
|
||||
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 = () => {
|
||||
<Empty description="暂无知识条目" />
|
||||
) : (
|
||||
<Table
|
||||
loading={itemsLoading}
|
||||
loading={itemsLoading || directoriesLoading}
|
||||
columns={itemColumns}
|
||||
dataSource={pagedItemRows}
|
||||
rowKey="id"
|
||||
|
||||
@@ -35,6 +35,22 @@ export function queryKnowledgeItemsUsingGet(setId: string, params?: Record<strin
|
||||
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>) {
|
||||
return get("/api/data-management/knowledge-items/search", params);
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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)保持不变
|
||||
-- ===========================================
|
||||
|
||||
Reference in New Issue
Block a user