feat(data-management): 修改知识项导出功能为ZIP格式

- 将导出文件格式从JSON改为ZIP压缩包
- 使用ZipArchiveOutputStream实现ZIP文件创建
- 为每个知识项创建独立的文件条目
- 添加文件名规范化和长度限制逻辑
- 实现重复文件名的索引编号处理
- 移除Jackson ObjectMapper依赖引入
- 更新响应头内容类型为application/zip
This commit is contained in:
2026-01-26 11:15:58 +08:00
parent a8c7c9404c
commit 6835511f5a

View File

@@ -27,12 +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.apache.commons.compress.archivers.zip.ZipArchiveEntry;
import org.apache.commons.compress.archivers.zip.ZipArchiveOutputStream;
import org.springframework.http.HttpHeaders;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@@ -46,8 +47,10 @@ import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.UUID;
@@ -63,8 +66,9 @@ public class KnowledgeItemApplicationService {
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 static final String EXPORT_FILE_SUFFIX = ".zip";
private static final String EXPORT_CONTENT_TYPE = "application/zip";
private static final int MAX_FILE_BASE_LENGTH = 120;
private final KnowledgeItemRepository knowledgeItemRepository;
private final KnowledgeSetRepository knowledgeSetRepository;
@@ -228,16 +232,25 @@ public class KnowledgeItemApplicationService {
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);
try (ZipArchiveOutputStream zos = new ZipArchiveOutputStream(response.getOutputStream())) {
zos.setEncoding(StandardCharsets.UTF_8.name());
Map<String, Integer> nameCounter = new HashMap<>();
for (KnowledgeItem item : items) {
String entryName = buildItemEntryName(item, nameCounter);
ZipArchiveEntry entry = new ZipArchiveEntry(entryName);
zos.putArchiveEntry(entry);
byte[] contentBytes = (item.getContent() == null ? "" : item.getContent())
.getBytes(StandardCharsets.UTF_8);
zos.write(contentBytes);
zos.closeArchiveEntry();
}
zos.finish();
} catch (IOException e) {
log.error("export knowledge items error, setId: {}", setId, e);
throw BusinessException.of(SystemErrorCode.FILE_SYSTEM_ERROR);
@@ -254,6 +267,50 @@ public class KnowledgeItemApplicationService {
return EXPORT_FILE_PREFIX + setId + "_" + LocalDateTime.now().format(EXPORT_TIME_FORMATTER) + EXPORT_FILE_SUFFIX;
}
private String buildItemEntryName(KnowledgeItem item, Map<String, Integer> nameCounter) {
String rawTitle = StringUtils.isNotBlank(item.getTitle()) ? item.getTitle() : "item-" + item.getId();
String baseName = sanitizeFileName(rawTitle);
if (StringUtils.isBlank(baseName)) {
baseName = "item-" + item.getId();
}
baseName = trimToLength(baseName, MAX_FILE_BASE_LENGTH);
String extension = item.getContentType() == KnowledgeContentType.MARKDOWN ? "md" : "txt";
String baseKey = baseName + "." + extension;
int count = nameCounter.getOrDefault(baseKey, 0);
nameCounter.put(baseKey, count + 1);
if (count == 0) {
return baseKey;
}
return buildIndexedFileName(baseName, extension, count);
}
private String buildIndexedFileName(String baseName, String extension, int index) {
String suffix = "_" + index;
int maxBaseLength = Math.max(1, MAX_FILE_BASE_LENGTH - suffix.length());
String safeBase = trimToLength(baseName, maxBaseLength);
return safeBase + suffix + "." + extension;
}
private String sanitizeFileName(String name) {
if (name == null) {
return "";
}
String normalized = name.replaceAll("[\\\\/:*?\"<>|]", "_");
normalized = normalized.replaceAll("\\s+", " ").trim();
return normalized;
}
private String trimToLength(String value, int maxLength) {
if (value == null) {
return "";
}
if (value.length() <= maxLength) {
return value;
}
return value.substring(0, maxLength);
}
private KnowledgeContentType resolveContentType(DatasetFile datasetFile) {
String extension = getFileExtension(datasetFile);
if (StringUtils.isBlank(extension)) {