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

View File

@@ -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);
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 = () => {
<Empty description="暂无知识条目" />
) : (
<Table
loading={itemsLoading}
loading={itemsLoading || directoriesLoading}
columns={itemColumns}
dataSource={pagedItemRows}
rowKey="id"

View File

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

View File

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

View File

@@ -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)保持不变
-- ===========================================