From a8c7c9404cfc4744329c0f3d4bf310b8ed812e05 Mon Sep 17 00:00:00 2001 From: Jerry Yan <792602257@qq.com> Date: Mon, 26 Jan 2026 11:13:21 +0800 Subject: [PATCH] =?UTF-8?q?feat(knowledge):=20=E6=B7=BB=E5=8A=A0=E7=9F=A5?= =?UTF-8?q?=E8=AF=86=E6=9D=A1=E7=9B=AE=E5=AF=BC=E5=87=BA=E5=8A=9F=E8=83=BD?= =?UTF-8?q?=E5=92=8C=E6=96=87=E4=BB=B6=E4=B8=8A=E4=BC=A0=E6=94=AF=E6=8C=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 在 KnowledgeItemApplicationService 中新增 exportKnowledgeItems 方法实现知识条目导出 - 添加 export 相关常量配置包括文件名格式、内容类型等 - 在 KnowledgeItemRepository 中新增 findAllBySetId 查询方法 - 在 KnowledgeItemController 中新增 export 接口端点 - 在 KnowledgeItemEditor 组件中添加文件上传功能支持 txt/md/markdown 格式 - 在 KnowledgeSetDetail 页面中添加导出按钮并集成导出 API - 更新前端 API 文件添加 exportKnowledgeItemsUsingGet 方法 - 配置文件上传验证和自动填充标题内容逻辑 --- .../KnowledgeItemApplicationService.java | 34 ++++++++ .../repository/KnowledgeItemRepository.java | 3 + .../impl/KnowledgeItemRepositoryImpl.java | 9 +++ .../rest/KnowledgeItemController.java | 8 ++ .../Detail/KnowledgeSetDetail.tsx | 15 +++- .../components/KnowledgeItemEditor.tsx | 80 ++++++++++++++++++- .../knowledge-management.api.ts | 7 +- 7 files changed, 153 insertions(+), 3 deletions(-) diff --git a/backend/services/data-management-service/src/main/java/com/datamate/datamanagement/application/KnowledgeItemApplicationService.java b/backend/services/data-management-service/src/main/java/com/datamate/datamanagement/application/KnowledgeItemApplicationService.java index 37bf23a..9d53c89 100644 --- a/backend/services/data-management-service/src/main/java/com/datamate/datamanagement/application/KnowledgeItemApplicationService.java +++ b/backend/services/data-management-service/src/main/java/com/datamate/datamanagement/application/KnowledgeItemApplicationService.java @@ -27,10 +27,13 @@ import com.datamate.datamanagement.interfaces.dto.ImportKnowledgeItemsRequest; import com.datamate.datamanagement.interfaces.dto.KnowledgeItemPagingQuery; import com.datamate.datamanagement.interfaces.dto.KnowledgeItemResponse; import com.datamate.datamanagement.interfaces.dto.UpdateKnowledgeItemRequest; +import com.fasterxml.jackson.databind.ObjectMapper; +import jakarta.servlet.http.HttpServletResponse; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.apache.commons.collections4.CollectionUtils; import org.apache.commons.lang3.StringUtils; +import org.springframework.http.HttpHeaders; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -40,6 +43,8 @@ import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; import java.util.ArrayList; import java.util.HashSet; import java.util.List; @@ -56,6 +61,10 @@ import java.util.UUID; @RequiredArgsConstructor public class KnowledgeItemApplicationService { private static final Set SUPPORTED_TEXT_EXTENSIONS = Set.of("txt", "md", "markdown"); + private static final DateTimeFormatter EXPORT_TIME_FORMATTER = DateTimeFormatter.ofPattern("yyyyMMddHHmmss"); + private static final String EXPORT_FILE_PREFIX = "knowledge_set_"; + private static final String EXPORT_FILE_SUFFIX = ".json"; + private static final String EXPORT_CONTENT_TYPE = "application/json"; private final KnowledgeItemRepository knowledgeItemRepository; private final KnowledgeSetRepository knowledgeSetRepository; @@ -214,12 +223,37 @@ public class KnowledgeItemApplicationService { return items; } + @Transactional(readOnly = true) + public void exportKnowledgeItems(String setId, HttpServletResponse response) { + BusinessAssert.notNull(response, CommonErrorCode.PARAM_ERROR); + KnowledgeSet knowledgeSet = requireKnowledgeSet(setId); + List items = knowledgeItemRepository.findAllBySetId(setId); + List responses = KnowledgeConverter.INSTANCE.convertItemResponses(items); + + response.setContentType(EXPORT_CONTENT_TYPE); + response.setCharacterEncoding(StandardCharsets.UTF_8.name()); + response.setHeader(HttpHeaders.CONTENT_DISPOSITION, + "attachment; filename=\"" + buildExportFileName(knowledgeSet.getId()) + "\""); + + ObjectMapper objectMapper = new ObjectMapper(); + try { + objectMapper.writeValue(response.getOutputStream(), responses); + } catch (IOException e) { + log.error("export knowledge items error, setId: {}", setId, e); + throw BusinessException.of(SystemErrorCode.FILE_SYSTEM_ERROR); + } + } + private KnowledgeSet requireKnowledgeSet(String setId) { KnowledgeSet knowledgeSet = knowledgeSetRepository.getById(setId); BusinessAssert.notNull(knowledgeSet, DataManagementErrorCode.KNOWLEDGE_SET_NOT_FOUND); return knowledgeSet; } + private String buildExportFileName(String setId) { + return EXPORT_FILE_PREFIX + setId + "_" + LocalDateTime.now().format(EXPORT_TIME_FORMATTER) + EXPORT_FILE_SUFFIX; + } + private KnowledgeContentType resolveContentType(DatasetFile datasetFile) { String extension = getFileExtension(datasetFile); if (StringUtils.isBlank(extension)) { diff --git a/backend/services/data-management-service/src/main/java/com/datamate/datamanagement/infrastructure/persistence/repository/KnowledgeItemRepository.java b/backend/services/data-management-service/src/main/java/com/datamate/datamanagement/infrastructure/persistence/repository/KnowledgeItemRepository.java index 244eab8..be8295a 100644 --- a/backend/services/data-management-service/src/main/java/com/datamate/datamanagement/infrastructure/persistence/repository/KnowledgeItemRepository.java +++ b/backend/services/data-management-service/src/main/java/com/datamate/datamanagement/infrastructure/persistence/repository/KnowledgeItemRepository.java @@ -4,6 +4,7 @@ import com.baomidou.mybatisplus.core.metadata.IPage; import com.baomidou.mybatisplus.extension.repository.IRepository; import com.datamate.datamanagement.domain.model.knowledge.KnowledgeItem; import com.datamate.datamanagement.interfaces.dto.KnowledgeItemPagingQuery; +import java.util.List; /** * 知识条目仓储接口 @@ -12,4 +13,6 @@ public interface KnowledgeItemRepository extends IRepository { IPage findByCriteria(IPage page, KnowledgeItemPagingQuery query); long countBySetId(String setId); + + List findAllBySetId(String setId); } diff --git a/backend/services/data-management-service/src/main/java/com/datamate/datamanagement/infrastructure/persistence/repository/impl/KnowledgeItemRepositoryImpl.java b/backend/services/data-management-service/src/main/java/com/datamate/datamanagement/infrastructure/persistence/repository/impl/KnowledgeItemRepositoryImpl.java index 79fed67..27290c7 100644 --- a/backend/services/data-management-service/src/main/java/com/datamate/datamanagement/infrastructure/persistence/repository/impl/KnowledgeItemRepositoryImpl.java +++ b/backend/services/data-management-service/src/main/java/com/datamate/datamanagement/infrastructure/persistence/repository/impl/KnowledgeItemRepositoryImpl.java @@ -11,6 +11,8 @@ import lombok.RequiredArgsConstructor; import org.apache.commons.lang3.StringUtils; import org.springframework.stereotype.Repository; +import java.util.List; + /** * 知识条目仓储实现类 */ @@ -59,4 +61,11 @@ public class KnowledgeItemRepositoryImpl extends CrudRepository() .eq(KnowledgeItem::getSetId, setId)); } + + @Override + public List findAllBySetId(String setId) { + return knowledgeItemMapper.selectList(new LambdaQueryWrapper() + .eq(KnowledgeItem::getSetId, setId) + .orderByDesc(KnowledgeItem::getCreatedAt)); + } } diff --git a/backend/services/data-management-service/src/main/java/com/datamate/datamanagement/interfaces/rest/KnowledgeItemController.java b/backend/services/data-management-service/src/main/java/com/datamate/datamanagement/interfaces/rest/KnowledgeItemController.java index 353d153..0d02eb2 100644 --- a/backend/services/data-management-service/src/main/java/com/datamate/datamanagement/interfaces/rest/KnowledgeItemController.java +++ b/backend/services/data-management-service/src/main/java/com/datamate/datamanagement/interfaces/rest/KnowledgeItemController.java @@ -1,5 +1,6 @@ package com.datamate.datamanagement.interfaces.rest; +import com.datamate.common.infrastructure.common.IgnoreResponseWrap; import com.datamate.common.interfaces.PagedResponse; import com.datamate.datamanagement.application.KnowledgeItemApplicationService; import com.datamate.datamanagement.domain.model.knowledge.KnowledgeItem; @@ -9,6 +10,7 @@ import com.datamate.datamanagement.interfaces.dto.ImportKnowledgeItemsRequest; import com.datamate.datamanagement.interfaces.dto.KnowledgeItemPagingQuery; import com.datamate.datamanagement.interfaces.dto.KnowledgeItemResponse; import com.datamate.datamanagement.interfaces.dto.UpdateKnowledgeItemRequest; +import jakarta.servlet.http.HttpServletResponse; import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; @@ -46,6 +48,12 @@ public class KnowledgeItemController { return KnowledgeConverter.INSTANCE.convertItemResponses(items); } + @IgnoreResponseWrap + @GetMapping("/export") + public void exportKnowledgeItems(@PathVariable("setId") String setId, HttpServletResponse response) { + knowledgeItemApplicationService.exportKnowledgeItems(setId, response); + } + @GetMapping("/{itemId}") public KnowledgeItemResponse getKnowledgeItemById(@PathVariable("setId") String setId, @PathVariable("itemId") String itemId) { diff --git a/frontend/src/pages/KnowledgeManagement/Detail/KnowledgeSetDetail.tsx b/frontend/src/pages/KnowledgeManagement/Detail/KnowledgeSetDetail.tsx index 60840ac..9309444 100644 --- a/frontend/src/pages/KnowledgeManagement/Detail/KnowledgeSetDetail.tsx +++ b/frontend/src/pages/KnowledgeManagement/Detail/KnowledgeSetDetail.tsx @@ -11,7 +11,7 @@ import { Tag, Tooltip, } from "antd"; -import { DeleteOutlined, EditOutlined, EyeOutlined, PlusOutlined } from "@ant-design/icons"; +import { DeleteOutlined, DownloadOutlined, EditOutlined, EyeOutlined, PlusOutlined } from "@ant-design/icons"; import { useNavigate, useParams } from "react-router"; import DetailHeader from "@/components/DetailHeader"; import { SearchControls } from "@/components/SearchControls"; @@ -19,6 +19,7 @@ import useFetchData from "@/hooks/useFetchData"; import { deleteKnowledgeItemByIdUsingDelete, deleteKnowledgeSetByIdUsingDelete, + exportKnowledgeItemsUsingGet, queryKnowledgeItemsUsingGet, queryKnowledgeSetByIdUsingGet, } from "../knowledge-management.api"; @@ -100,6 +101,12 @@ const KnowledgeSetDetail = () => { fetchData(); }; + const handleExportItems = async () => { + if (!id) return; + await exportKnowledgeItemsUsingGet(id); + message.success("知识条目导出成功"); + }; + const isReadableItem = (record: KnowledgeItemView) => { return ( record.contentType === KnowledgeContentType.TEXT || @@ -286,6 +293,12 @@ const KnowledgeSetDetail = () => { onClick: () => setShowEdit(true), danger: false, }, + { + key: "export", + label: "导出", + icon: , + onClick: handleExportItems, + }, { key: "delete", label: "删除", diff --git a/frontend/src/pages/KnowledgeManagement/components/KnowledgeItemEditor.tsx b/frontend/src/pages/KnowledgeManagement/components/KnowledgeItemEditor.tsx index 7e392c0..0de9e44 100644 --- a/frontend/src/pages/KnowledgeManagement/components/KnowledgeItemEditor.tsx +++ b/frontend/src/pages/KnowledgeManagement/components/KnowledgeItemEditor.tsx @@ -1,5 +1,6 @@ import { useEffect, useState } from "react"; -import { DatePicker, Form, Input, message, Modal, Select } from "antd"; +import { Button, DatePicker, Form, Input, message, Modal, Select, Upload, UploadFile } from "antd"; +import { UploadOutlined } from "@ant-design/icons"; import dayjs from "dayjs"; import { createKnowledgeItemUsingPost, @@ -16,6 +17,26 @@ import { } from "../knowledge-management.model"; import { queryDatasetTagsUsingGet } from "@/pages/DataManagement/dataset.api"; +const FILE_UPLOAD_ACCEPT = ".txt,.md,.markdown"; +const SUPPORTED_FILE_EXTENSIONS = new Set(["txt", "md", "markdown"]); +const MARKDOWN_FILE_EXTENSIONS = new Set(["md", "markdown"]); + +const getFileExtension = (fileName: string) => { + const dotIndex = fileName.lastIndexOf("."); + return dotIndex > -1 ? fileName.slice(dotIndex + 1).toLowerCase() : ""; +}; + +const stripFileExtension = (fileName: string) => { + const dotIndex = fileName.lastIndexOf("."); + if (dotIndex <= 0) { + return fileName; + } + return fileName.slice(0, dotIndex); +}; + +const resolveContentType = (extension: string) => + MARKDOWN_FILE_EXTENSIONS.has(extension) ? KnowledgeContentType.MARKDOWN : KnowledgeContentType.TEXT; + export default function KnowledgeItemEditor({ open, setId, @@ -33,6 +54,7 @@ export default function KnowledgeItemEditor({ }) { const [form] = Form.useForm(); const [tagOptions, setTagOptions] = useState<{ label: string; value: string }[]>([]); + const [fileList, setFileList] = useState([]); const fetchTags = async () => { try { @@ -77,9 +99,49 @@ export default function KnowledgeItemEditor({ tags: [], }); } + setFileList([]); + } else { + setFileList([]); } }, [open, data, form]); + const handleFileBeforeUpload = async (file: File) => { + const extension = getFileExtension(file.name); + if (!SUPPORTED_FILE_EXTENSIONS.has(extension)) { + message.error("仅支持 .txt/.md/.markdown 文件"); + return Upload.LIST_IGNORE; + } + + try { + const textContent = await file.text(); + const currentTitle = form.getFieldValue("title"); + form.setFieldsValue({ + title: currentTitle || stripFileExtension(file.name), + content: textContent, + contentType: resolveContentType(extension), + }); + setFileList([ + { + uid: `${Date.now()}-${file.name}`, + name: file.name, + status: "done", + originFileObj: file, + }, + ]); + message.success("文件已读取,可继续编辑内容"); + return false; + } catch (error) { + console.error("读取文件失败", error); + message.error("读取文件失败,请重试"); + return Upload.LIST_IGNORE; + } + }; + + const handleFileRemove = () => { + setFileList([]); + return true; + }; + const handleSubmit = async () => { try { const values = await form.validateFields(); @@ -107,6 +169,7 @@ export default function KnowledgeItemEditor({ } form.resetFields(); + setFileList([]); onSuccess(); } catch { message.error("操作失败,请重试"); @@ -155,6 +218,21 @@ export default function KnowledgeItemEditor({ > + {!data?.id && ( + + + + + + )}
diff --git a/frontend/src/pages/KnowledgeManagement/knowledge-management.api.ts b/frontend/src/pages/KnowledgeManagement/knowledge-management.api.ts index e70a1b9..67765f9 100644 --- a/frontend/src/pages/KnowledgeManagement/knowledge-management.api.ts +++ b/frontend/src/pages/KnowledgeManagement/knowledge-management.api.ts @@ -1,4 +1,4 @@ -import { get, post, put, del } from "@/utils/request"; +import { get, post, put, del, download } from "@/utils/request"; // 知识集列表 export function queryKnowledgeSetsUsingGet(params?: Record) { @@ -54,3 +54,8 @@ export function updateKnowledgeItemByIdUsingPut(setId: string, itemId: string, d export function deleteKnowledgeItemByIdUsingDelete(setId: string, itemId: string) { return del(`/api/data-management/knowledge-sets/${setId}/items/${itemId}`); } + +// 导出知识条目 +export function exportKnowledgeItemsUsingGet(setId: string) { + return download(`/api/data-management/knowledge-sets/${setId}/items/export`); +}