You've already forked DataMate
feat(knowledge): 添加知识条目导出功能和文件上传支持
- 在 KnowledgeItemApplicationService 中新增 exportKnowledgeItems 方法实现知识条目导出 - 添加 export 相关常量配置包括文件名格式、内容类型等 - 在 KnowledgeItemRepository 中新增 findAllBySetId 查询方法 - 在 KnowledgeItemController 中新增 export 接口端点 - 在 KnowledgeItemEditor 组件中添加文件上传功能支持 txt/md/markdown 格式 - 在 KnowledgeSetDetail 页面中添加导出按钮并集成导出 API - 更新前端 API 文件添加 exportKnowledgeItemsUsingGet 方法 - 配置文件上传验证和自动填充标题内容逻辑
This commit is contained in:
@@ -27,10 +27,13 @@ import com.datamate.datamanagement.interfaces.dto.ImportKnowledgeItemsRequest;
|
|||||||
import com.datamate.datamanagement.interfaces.dto.KnowledgeItemPagingQuery;
|
import com.datamate.datamanagement.interfaces.dto.KnowledgeItemPagingQuery;
|
||||||
import com.datamate.datamanagement.interfaces.dto.KnowledgeItemResponse;
|
import com.datamate.datamanagement.interfaces.dto.KnowledgeItemResponse;
|
||||||
import com.datamate.datamanagement.interfaces.dto.UpdateKnowledgeItemRequest;
|
import com.datamate.datamanagement.interfaces.dto.UpdateKnowledgeItemRequest;
|
||||||
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
|
import jakarta.servlet.http.HttpServletResponse;
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import org.apache.commons.collections4.CollectionUtils;
|
import org.apache.commons.collections4.CollectionUtils;
|
||||||
import org.apache.commons.lang3.StringUtils;
|
import org.apache.commons.lang3.StringUtils;
|
||||||
|
import org.springframework.http.HttpHeaders;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
import org.springframework.transaction.annotation.Transactional;
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
|
||||||
@@ -40,6 +43,8 @@ import java.nio.file.Files;
|
|||||||
import java.nio.file.Path;
|
import java.nio.file.Path;
|
||||||
import java.nio.file.Paths;
|
import java.nio.file.Paths;
|
||||||
import java.time.LocalDate;
|
import java.time.LocalDate;
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
import java.time.format.DateTimeFormatter;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.HashSet;
|
import java.util.HashSet;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
@@ -56,6 +61,10 @@ import java.util.UUID;
|
|||||||
@RequiredArgsConstructor
|
@RequiredArgsConstructor
|
||||||
public class KnowledgeItemApplicationService {
|
public class KnowledgeItemApplicationService {
|
||||||
private static final Set<String> SUPPORTED_TEXT_EXTENSIONS = Set.of("txt", "md", "markdown");
|
private static final Set<String> 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 KnowledgeItemRepository knowledgeItemRepository;
|
||||||
private final KnowledgeSetRepository knowledgeSetRepository;
|
private final KnowledgeSetRepository knowledgeSetRepository;
|
||||||
@@ -214,12 +223,37 @@ public class KnowledgeItemApplicationService {
|
|||||||
return items;
|
return items;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Transactional(readOnly = true)
|
||||||
|
public void exportKnowledgeItems(String setId, HttpServletResponse response) {
|
||||||
|
BusinessAssert.notNull(response, CommonErrorCode.PARAM_ERROR);
|
||||||
|
KnowledgeSet knowledgeSet = requireKnowledgeSet(setId);
|
||||||
|
List<KnowledgeItem> items = knowledgeItemRepository.findAllBySetId(setId);
|
||||||
|
List<KnowledgeItemResponse> 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) {
|
private KnowledgeSet requireKnowledgeSet(String setId) {
|
||||||
KnowledgeSet knowledgeSet = knowledgeSetRepository.getById(setId);
|
KnowledgeSet knowledgeSet = knowledgeSetRepository.getById(setId);
|
||||||
BusinessAssert.notNull(knowledgeSet, DataManagementErrorCode.KNOWLEDGE_SET_NOT_FOUND);
|
BusinessAssert.notNull(knowledgeSet, DataManagementErrorCode.KNOWLEDGE_SET_NOT_FOUND);
|
||||||
return knowledgeSet;
|
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) {
|
private KnowledgeContentType resolveContentType(DatasetFile datasetFile) {
|
||||||
String extension = getFileExtension(datasetFile);
|
String extension = getFileExtension(datasetFile);
|
||||||
if (StringUtils.isBlank(extension)) {
|
if (StringUtils.isBlank(extension)) {
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import com.baomidou.mybatisplus.core.metadata.IPage;
|
|||||||
import com.baomidou.mybatisplus.extension.repository.IRepository;
|
import com.baomidou.mybatisplus.extension.repository.IRepository;
|
||||||
import com.datamate.datamanagement.domain.model.knowledge.KnowledgeItem;
|
import com.datamate.datamanagement.domain.model.knowledge.KnowledgeItem;
|
||||||
import com.datamate.datamanagement.interfaces.dto.KnowledgeItemPagingQuery;
|
import com.datamate.datamanagement.interfaces.dto.KnowledgeItemPagingQuery;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 知识条目仓储接口
|
* 知识条目仓储接口
|
||||||
@@ -12,4 +13,6 @@ public interface KnowledgeItemRepository extends IRepository<KnowledgeItem> {
|
|||||||
IPage<KnowledgeItem> findByCriteria(IPage<KnowledgeItem> page, KnowledgeItemPagingQuery query);
|
IPage<KnowledgeItem> findByCriteria(IPage<KnowledgeItem> page, KnowledgeItemPagingQuery query);
|
||||||
|
|
||||||
long countBySetId(String setId);
|
long countBySetId(String setId);
|
||||||
|
|
||||||
|
List<KnowledgeItem> findAllBySetId(String setId);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,6 +11,8 @@ import lombok.RequiredArgsConstructor;
|
|||||||
import org.apache.commons.lang3.StringUtils;
|
import org.apache.commons.lang3.StringUtils;
|
||||||
import org.springframework.stereotype.Repository;
|
import org.springframework.stereotype.Repository;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 知识条目仓储实现类
|
* 知识条目仓储实现类
|
||||||
*/
|
*/
|
||||||
@@ -59,4 +61,11 @@ public class KnowledgeItemRepositoryImpl extends CrudRepository<KnowledgeItemMap
|
|||||||
return knowledgeItemMapper.selectCount(new LambdaQueryWrapper<KnowledgeItem>()
|
return knowledgeItemMapper.selectCount(new LambdaQueryWrapper<KnowledgeItem>()
|
||||||
.eq(KnowledgeItem::getSetId, setId));
|
.eq(KnowledgeItem::getSetId, setId));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<KnowledgeItem> findAllBySetId(String setId) {
|
||||||
|
return knowledgeItemMapper.selectList(new LambdaQueryWrapper<KnowledgeItem>()
|
||||||
|
.eq(KnowledgeItem::getSetId, setId)
|
||||||
|
.orderByDesc(KnowledgeItem::getCreatedAt));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
package com.datamate.datamanagement.interfaces.rest;
|
package com.datamate.datamanagement.interfaces.rest;
|
||||||
|
|
||||||
|
import com.datamate.common.infrastructure.common.IgnoreResponseWrap;
|
||||||
import com.datamate.common.interfaces.PagedResponse;
|
import com.datamate.common.interfaces.PagedResponse;
|
||||||
import com.datamate.datamanagement.application.KnowledgeItemApplicationService;
|
import com.datamate.datamanagement.application.KnowledgeItemApplicationService;
|
||||||
import com.datamate.datamanagement.domain.model.knowledge.KnowledgeItem;
|
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.KnowledgeItemPagingQuery;
|
||||||
import com.datamate.datamanagement.interfaces.dto.KnowledgeItemResponse;
|
import com.datamate.datamanagement.interfaces.dto.KnowledgeItemResponse;
|
||||||
import com.datamate.datamanagement.interfaces.dto.UpdateKnowledgeItemRequest;
|
import com.datamate.datamanagement.interfaces.dto.UpdateKnowledgeItemRequest;
|
||||||
|
import jakarta.servlet.http.HttpServletResponse;
|
||||||
import jakarta.validation.Valid;
|
import jakarta.validation.Valid;
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
@@ -46,6 +48,12 @@ public class KnowledgeItemController {
|
|||||||
return KnowledgeConverter.INSTANCE.convertItemResponses(items);
|
return KnowledgeConverter.INSTANCE.convertItemResponses(items);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@IgnoreResponseWrap
|
||||||
|
@GetMapping("/export")
|
||||||
|
public void exportKnowledgeItems(@PathVariable("setId") String setId, HttpServletResponse response) {
|
||||||
|
knowledgeItemApplicationService.exportKnowledgeItems(setId, response);
|
||||||
|
}
|
||||||
|
|
||||||
@GetMapping("/{itemId}")
|
@GetMapping("/{itemId}")
|
||||||
public KnowledgeItemResponse getKnowledgeItemById(@PathVariable("setId") String setId,
|
public KnowledgeItemResponse getKnowledgeItemById(@PathVariable("setId") String setId,
|
||||||
@PathVariable("itemId") String itemId) {
|
@PathVariable("itemId") String itemId) {
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ import {
|
|||||||
Tag,
|
Tag,
|
||||||
Tooltip,
|
Tooltip,
|
||||||
} from "antd";
|
} 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 { 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";
|
||||||
@@ -19,6 +19,7 @@ import useFetchData from "@/hooks/useFetchData";
|
|||||||
import {
|
import {
|
||||||
deleteKnowledgeItemByIdUsingDelete,
|
deleteKnowledgeItemByIdUsingDelete,
|
||||||
deleteKnowledgeSetByIdUsingDelete,
|
deleteKnowledgeSetByIdUsingDelete,
|
||||||
|
exportKnowledgeItemsUsingGet,
|
||||||
queryKnowledgeItemsUsingGet,
|
queryKnowledgeItemsUsingGet,
|
||||||
queryKnowledgeSetByIdUsingGet,
|
queryKnowledgeSetByIdUsingGet,
|
||||||
} from "../knowledge-management.api";
|
} from "../knowledge-management.api";
|
||||||
@@ -100,6 +101,12 @@ const KnowledgeSetDetail = () => {
|
|||||||
fetchData();
|
fetchData();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleExportItems = async () => {
|
||||||
|
if (!id) return;
|
||||||
|
await exportKnowledgeItemsUsingGet(id);
|
||||||
|
message.success("知识条目导出成功");
|
||||||
|
};
|
||||||
|
|
||||||
const isReadableItem = (record: KnowledgeItemView) => {
|
const isReadableItem = (record: KnowledgeItemView) => {
|
||||||
return (
|
return (
|
||||||
record.contentType === KnowledgeContentType.TEXT ||
|
record.contentType === KnowledgeContentType.TEXT ||
|
||||||
@@ -286,6 +293,12 @@ const KnowledgeSetDetail = () => {
|
|||||||
onClick: () => setShowEdit(true),
|
onClick: () => setShowEdit(true),
|
||||||
danger: false,
|
danger: false,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
key: "export",
|
||||||
|
label: "导出",
|
||||||
|
icon: <DownloadOutlined />,
|
||||||
|
onClick: handleExportItems,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
key: "delete",
|
key: "delete",
|
||||||
label: "删除",
|
label: "删除",
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { useEffect, useState } from "react";
|
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 dayjs from "dayjs";
|
||||||
import {
|
import {
|
||||||
createKnowledgeItemUsingPost,
|
createKnowledgeItemUsingPost,
|
||||||
@@ -16,6 +17,26 @@ import {
|
|||||||
} from "../knowledge-management.model";
|
} from "../knowledge-management.model";
|
||||||
import { queryDatasetTagsUsingGet } from "@/pages/DataManagement/dataset.api";
|
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({
|
export default function KnowledgeItemEditor({
|
||||||
open,
|
open,
|
||||||
setId,
|
setId,
|
||||||
@@ -33,6 +54,7 @@ export default function KnowledgeItemEditor({
|
|||||||
}) {
|
}) {
|
||||||
const [form] = Form.useForm();
|
const [form] = Form.useForm();
|
||||||
const [tagOptions, setTagOptions] = useState<{ label: string; value: string }[]>([]);
|
const [tagOptions, setTagOptions] = useState<{ label: string; value: string }[]>([]);
|
||||||
|
const [fileList, setFileList] = useState<UploadFile[]>([]);
|
||||||
|
|
||||||
const fetchTags = async () => {
|
const fetchTags = async () => {
|
||||||
try {
|
try {
|
||||||
@@ -77,9 +99,49 @@ export default function KnowledgeItemEditor({
|
|||||||
tags: [],
|
tags: [],
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
setFileList([]);
|
||||||
|
} else {
|
||||||
|
setFileList([]);
|
||||||
}
|
}
|
||||||
}, [open, data, form]);
|
}, [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 () => {
|
const handleSubmit = async () => {
|
||||||
try {
|
try {
|
||||||
const values = await form.validateFields();
|
const values = await form.validateFields();
|
||||||
@@ -107,6 +169,7 @@ export default function KnowledgeItemEditor({
|
|||||||
}
|
}
|
||||||
|
|
||||||
form.resetFields();
|
form.resetFields();
|
||||||
|
setFileList([]);
|
||||||
onSuccess();
|
onSuccess();
|
||||||
} catch {
|
} catch {
|
||||||
message.error("操作失败,请重试");
|
message.error("操作失败,请重试");
|
||||||
@@ -155,6 +218,21 @@ export default function KnowledgeItemEditor({
|
|||||||
>
|
>
|
||||||
<Input.TextArea rows={8} placeholder="请输入内容" />
|
<Input.TextArea rows={8} placeholder="请输入内容" />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
|
{!data?.id && (
|
||||||
|
<Form.Item label="上传文件" extra="支持 .txt/.md/.markdown,将自动填充标题与内容">
|
||||||
|
<Upload
|
||||||
|
accept={FILE_UPLOAD_ACCEPT}
|
||||||
|
beforeUpload={handleFileBeforeUpload}
|
||||||
|
fileList={fileList}
|
||||||
|
maxCount={1}
|
||||||
|
onRemove={handleFileRemove}
|
||||||
|
showUploadList={{ showPreviewIcon: false }}
|
||||||
|
disabled={readOnly}
|
||||||
|
>
|
||||||
|
<Button icon={<UploadOutlined />}>选择文件</Button>
|
||||||
|
</Upload>
|
||||||
|
</Form.Item>
|
||||||
|
)}
|
||||||
<div className="grid grid-cols-2 gap-4">
|
<div className="grid grid-cols-2 gap-4">
|
||||||
<Form.Item label="领域" name="domain">
|
<Form.Item label="领域" name="domain">
|
||||||
<Input placeholder="请输入领域" />
|
<Input placeholder="请输入领域" />
|
||||||
|
|||||||
@@ -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<string, unknown>) {
|
export function queryKnowledgeSetsUsingGet(params?: Record<string, unknown>) {
|
||||||
@@ -54,3 +54,8 @@ export function updateKnowledgeItemByIdUsingPut(setId: string, itemId: string, d
|
|||||||
export function deleteKnowledgeItemByIdUsingDelete(setId: string, itemId: string) {
|
export function deleteKnowledgeItemByIdUsingDelete(setId: string, itemId: string) {
|
||||||
return del(`/api/data-management/knowledge-sets/${setId}/items/${itemId}`);
|
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`);
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user