feat(knowledge): 添加知识条目文件预览和替换功能

- 后端实现知识条目文件预览接口,支持多种文件类型在线预览
- 后端实现知识条目文件替换功能,保留原有文件管理逻辑
- 前端新增文件预览模态框组件,支持文本、图片、音视频预览
- 前端知识条目编辑器添加文件替换上传功能
- 前端优化文件内容截断预览逻辑,统一使用工具函数处理
- 前端修复 PUT 请求中 FormData 处理问题,确保文件上传正常工作
- 新增文件预览相关工具函数和常量配置
This commit is contained in:
2026-01-29 11:37:36 +08:00
parent d0b5473068
commit ce98be5778
10 changed files with 467 additions and 46 deletions

View File

@@ -27,6 +27,7 @@ import com.datamate.datamanagement.interfaces.dto.CreateKnowledgeItemRequest;
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.ReplaceKnowledgeItemFileRequest;
import com.datamate.datamanagement.interfaces.dto.UpdateKnowledgeItemRequest;
import com.datamate.datamanagement.interfaces.dto.UploadKnowledgeItemsRequest;
import jakarta.servlet.http.HttpServletResponse;
@@ -366,6 +367,118 @@ public class KnowledgeItemApplicationService {
}
}
@Transactional(readOnly = true)
public void previewKnowledgeItemFile(String setId, String itemId, HttpServletResponse response) {
BusinessAssert.notNull(response, CommonErrorCode.PARAM_ERROR);
KnowledgeItem knowledgeItem = knowledgeItemRepository.getById(itemId);
BusinessAssert.notNull(knowledgeItem, DataManagementErrorCode.KNOWLEDGE_ITEM_NOT_FOUND);
BusinessAssert.isTrue(Objects.equals(knowledgeItem.getSetId(), setId), CommonErrorCode.PARAM_ERROR);
BusinessAssert.isTrue(knowledgeItem.getContentType() == KnowledgeContentType.FILE
|| knowledgeItem.getSourceType() == KnowledgeSourceType.FILE_UPLOAD,
CommonErrorCode.PARAM_ERROR);
String relativePath = knowledgeItem.getContent();
BusinessAssert.isTrue(StringUtils.isNotBlank(relativePath), CommonErrorCode.PARAM_ERROR);
Path filePath = resolveKnowledgeItemStoragePath(relativePath);
BusinessAssert.isTrue(Files.exists(filePath) && Files.isRegularFile(filePath), CommonErrorCode.PARAM_ERROR);
String previewName = StringUtils.isNotBlank(knowledgeItem.getSourceFileId())
? knowledgeItem.getSourceFileId()
: filePath.getFileName().toString();
String contentType = null;
try {
contentType = Files.probeContentType(filePath);
} catch (IOException e) {
log.warn("probe knowledge item file content type failed, itemId: {}", itemId, e);
}
if (StringUtils.isBlank(contentType)) {
contentType = "application/octet-stream";
}
response.setContentType(contentType);
response.setCharacterEncoding(StandardCharsets.UTF_8.name());
response.setHeader(HttpHeaders.CONTENT_DISPOSITION,
"inline; filename=\"" + URLEncoder.encode(previewName, StandardCharsets.UTF_8) + "\"");
try (InputStream inputStream = Files.newInputStream(filePath)) {
inputStream.transferTo(response.getOutputStream());
response.flushBuffer();
} catch (IOException e) {
log.error("preview knowledge item file error, itemId: {}", itemId, e);
throw BusinessException.of(SystemErrorCode.FILE_SYSTEM_ERROR);
}
}
public KnowledgeItem replaceKnowledgeItemFile(String setId, String itemId, ReplaceKnowledgeItemFileRequest request) {
KnowledgeSet knowledgeSet = requireKnowledgeSet(setId);
KnowledgeItem knowledgeItem = knowledgeItemRepository.getById(itemId);
BusinessAssert.notNull(knowledgeItem, DataManagementErrorCode.KNOWLEDGE_ITEM_NOT_FOUND);
BusinessAssert.isTrue(Objects.equals(knowledgeItem.getSetId(), setId), CommonErrorCode.PARAM_ERROR);
BusinessAssert.isTrue(!isReadOnlyStatus(knowledgeItem.getStatus()),
DataManagementErrorCode.KNOWLEDGE_ITEM_STATUS_ERROR);
BusinessAssert.isTrue(!isReadOnlyStatus(knowledgeSet.getStatus()),
DataManagementErrorCode.KNOWLEDGE_SET_STATUS_ERROR);
MultipartFile file = request == null ? null : request.getFile();
BusinessAssert.notNull(file, CommonErrorCode.PARAM_ERROR);
BusinessAssert.isTrue(!file.isEmpty(), CommonErrorCode.PARAM_ERROR);
BusinessAssert.isTrue(knowledgeItem.getContentType() == KnowledgeContentType.FILE
|| knowledgeItem.getSourceType() == KnowledgeSourceType.FILE_UPLOAD,
CommonErrorCode.PARAM_ERROR);
String oldRelativePath = knowledgeItem.getContent();
BusinessAssert.isTrue(StringUtils.isNotBlank(oldRelativePath), CommonErrorCode.PARAM_ERROR);
Path oldFilePath = resolveKnowledgeItemStoragePath(oldRelativePath);
BusinessAssert.isTrue(Files.exists(oldFilePath) && Files.isRegularFile(oldFilePath), CommonErrorCode.PARAM_ERROR);
Path uploadRoot = resolveUploadRootPath();
Path setDir = uploadRoot.resolve(KNOWLEDGE_ITEM_UPLOAD_DIR).resolve(setId).normalize();
BusinessAssert.isTrue(setDir.startsWith(uploadRoot), CommonErrorCode.PARAM_ERROR);
createDirectories(setDir);
String originalName = resolveOriginalFileName(file);
String safeOriginalName = sanitizeFileName(originalName);
if (StringUtils.isBlank(safeOriginalName)) {
safeOriginalName = "file";
}
String extension = getFileExtension(safeOriginalName);
String storedName = UUID.randomUUID().toString() +
(StringUtils.isBlank(extension) ? "" : "." + extension);
Path targetPath = setDir.resolve(storedName).normalize();
BusinessAssert.isTrue(targetPath.startsWith(setDir), CommonErrorCode.PARAM_ERROR);
saveMultipartFile(file, targetPath);
String title = stripExtension(safeOriginalName);
if (StringUtils.isBlank(title)) {
title = "未命名文件";
}
title = trimToLength(title, MAX_TITLE_LENGTH);
String sourceFileId = trimToLength(safeOriginalName, MAX_TITLE_LENGTH);
String newRelativePath = buildRelativeFilePath(setId, storedName);
try {
knowledgeItem.setTitle(title);
knowledgeItem.setContent(newRelativePath);
knowledgeItem.setContentType(KnowledgeContentType.FILE);
knowledgeItem.setSourceType(KnowledgeSourceType.FILE_UPLOAD);
knowledgeItem.setSourceFileId(sourceFileId);
knowledgeItemRepository.updateById(knowledgeItem);
deleteFile(oldFilePath);
} catch (Exception e) {
deleteFileQuietly(targetPath);
if (e instanceof BusinessException) {
throw (BusinessException) e;
}
throw BusinessException.of(SystemErrorCode.FILE_SYSTEM_ERROR);
}
return knowledgeItem;
}
private byte[] resolveExportContent(KnowledgeItem item) {
if (item.getContentType() == KnowledgeContentType.FILE) {
String relativePath = item.getContent();
@@ -441,6 +554,23 @@ public class KnowledgeItemApplicationService {
}
}
private void deleteFile(Path filePath) {
try {
Files.deleteIfExists(filePath);
} catch (IOException e) {
log.error("delete knowledge item file error, path: {}", filePath, e);
throw BusinessException.of(SystemErrorCode.FILE_SYSTEM_ERROR);
}
}
private void deleteFileQuietly(Path filePath) {
try {
Files.deleteIfExists(filePath);
} catch (IOException e) {
log.warn("delete knowledge item file quietly error, path: {}", filePath, e);
}
}
private String resolveOriginalFileName(MultipartFile file) {
String originalName = file.getOriginalFilename();
if (StringUtils.isBlank(originalName)) {

View File

@@ -0,0 +1,19 @@
package com.datamate.datamanagement.interfaces.dto;
import jakarta.validation.constraints.NotNull;
import lombok.Getter;
import lombok.Setter;
import org.springframework.web.multipart.MultipartFile;
/**
* 替换知识条目文件请求DTO
*/
@Getter
@Setter
public class ReplaceKnowledgeItemFileRequest {
/**
* 新文件
*/
@NotNull(message = "文件不能为空")
private MultipartFile file;
}

View File

@@ -9,6 +9,7 @@ import com.datamate.datamanagement.interfaces.dto.CreateKnowledgeItemRequest;
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.ReplaceKnowledgeItemFileRequest;
import com.datamate.datamanagement.interfaces.dto.UpdateKnowledgeItemRequest;
import com.datamate.datamanagement.interfaces.dto.UploadKnowledgeItemsRequest;
import jakarta.servlet.http.HttpServletResponse;
@@ -71,6 +72,14 @@ public class KnowledgeItemController {
knowledgeItemApplicationService.downloadKnowledgeItemFile(setId, itemId, response);
}
@IgnoreResponseWrap
@GetMapping("/{itemId}/preview")
public void previewKnowledgeItemFile(@PathVariable("setId") String setId,
@PathVariable("itemId") String itemId,
HttpServletResponse response) {
knowledgeItemApplicationService.previewKnowledgeItemFile(setId, itemId, response);
}
@GetMapping("/{itemId}")
public KnowledgeItemResponse getKnowledgeItemById(@PathVariable("setId") String setId,
@PathVariable("itemId") String itemId) {
@@ -86,6 +95,14 @@ public class KnowledgeItemController {
return KnowledgeConverter.INSTANCE.convertToResponse(knowledgeItem);
}
@PutMapping(value = "/{itemId}/file", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
public KnowledgeItemResponse replaceKnowledgeItemFile(@PathVariable("setId") String setId,
@PathVariable("itemId") String itemId,
@Valid ReplaceKnowledgeItemFileRequest request) {
KnowledgeItem knowledgeItem = knowledgeItemApplicationService.replaceKnowledgeItemFile(setId, itemId, request);
return KnowledgeConverter.INSTANCE.convertToResponse(knowledgeItem);
}
@DeleteMapping("/{itemId}")
public void deleteKnowledgeItem(@PathVariable("setId") String setId,
@PathVariable("itemId") String itemId) {