更新新增三个模块

This commit is contained in:
super
2026-03-22 11:20:09 +08:00
parent 74a5fb9ce1
commit 8e66926b9e
112 changed files with 5411 additions and 606 deletions

View File

@@ -20,4 +20,7 @@ public class ConvertRunRequest {
@NotBlank(message = "请选择模板")
@Schema(description = "模板编码,例如 uk_offer")
private String templateId;
@Schema(description = "文件夹上传场景下的总压缩包名称(不含扩展名)")
private String archiveName;
}

View File

@@ -14,4 +14,7 @@ public class UploadedSourceFileDto {
@Schema(description = "原始文件名")
private String originalFilename;
@Schema(description = "相对上传目录路径")
private String relativePath;
}

View File

@@ -3,6 +3,7 @@ package com.nanri.aiimage.modules.convert.service;
import cn.hutool.core.io.FileUtil;
import cn.hutool.core.util.IdUtil;
import cn.hutool.json.JSONUtil;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.nanri.aiimage.common.exception.BusinessException;
@@ -22,24 +23,38 @@ import org.apache.poi.ss.usermodel.DataFormatter;
import org.apache.poi.ss.usermodel.Row;
import org.apache.poi.ss.usermodel.Sheet;
import org.apache.poi.ss.usermodel.Workbook;
import org.apache.poi.ss.usermodel.WorkbookFactory;
import org.springframework.stereotype.Service;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import java.util.zip.ZipEntry;
import java.util.zip.ZipOutputStream;
@Service
@RequiredArgsConstructor
public class ConvertRunService {
private static final String MODULE_TYPE = "CONVERT";
private static final String TEMPLATE_CODE_FIVE_COUNTRIES = "uk_offer";
private static final List<String> FIVE_COUNTRY_OUTPUT_FILES = List.of(
"\u82f1\u56fd.txt",
"\u6cd5\u56fd.txt",
"\u5fb7\u56fd.txt",
"\u897f\u73ed\u7259.txt",
"\u610f\u5927\u5229.txt"
);
private final FileTaskMapper fileTaskMapper;
private final FileResultMapper fileResultMapper;
private final StorageProperties storageProperties;
@@ -51,8 +66,8 @@ public class ConvertRunService {
ConvertTemplateEntity template = convertTemplateService.getByCode(request.getTemplateId());
FileTaskEntity task = new FileTaskEntity();
task.setTaskNo("CONVERT-" + IdUtil.getSnowflakeNextIdStr());
task.setModuleType("CONVERT");
task.setTaskNo(MODULE_TYPE + "-" + IdUtil.getSnowflakeNextIdStr());
task.setModuleType(MODULE_TYPE);
task.setTaskMode("IMMEDIATE");
task.setStatus("SUCCESS");
task.setSourceFileCount(request.getFiles().size());
@@ -63,63 +78,107 @@ public class ConvertRunService {
task.setFinishedAt(LocalDateTime.now());
fileTaskMapper.insert(task);
boolean folderMode = request.getArchiveName() != null && !request.getArchiveName().isBlank();
List<ConvertResultItemVo> items = new ArrayList<>();
int successCount = 0;
int failedCount = 0;
List<ConvertArchiveEntry> archiveEntries = new ArrayList<>();
for (UploadedSourceFileDto sourceFile : request.getFiles()) {
ConvertResultItemVo item = new ConvertResultItemVo();
item.setSourceFilename(sourceFile.getOriginalFilename());
try {
File inputFile = findLocalSourceFile(sourceFile.getFileKey());
if (inputFile == null || !inputFile.exists()) {
throw new BusinessException("上传文件不存在,请重新上传");
throw new BusinessException("Uploaded source file does not exist, please upload again.");
}
String inputName = sourceFile.getOriginalFilename() == null ? inputFile.getName() : sourceFile.getOriginalFilename();
String outputFilename = template.getOutputFilename() == null || template.getOutputFilename().isBlank()
? "结果.txt"
: template.getOutputFilename();
File outputDir = FileUtil.mkdir(FileUtil.file(storageProperties.getLocalTempDir(), "convert-result"));
File outputFile = buildNamedOutputFile(outputDir, outputFilename);
String inputName = sourceFile.getOriginalFilename() == null
? inputFile.getName()
: sourceFile.getOriginalFilename();
List<GeneratedConvertFile> generatedFiles = generateOutputFiles(inputFile, template);
if (folderMode) {
for (GeneratedConvertFile generatedFile : generatedFiles) {
archiveEntries.add(new ConvertArchiveEntry(sourceFile.getRelativePath(), inputName, generatedFile));
}
successCount++;
continue;
}
writeTxtByLegacyRules(inputFile, outputFile, template);
boolean useZipPackage = request.getFiles().size() == 1 && generatedFiles.size() > 1;
File packagedResultFile = useZipPackage
? packageGeneratedFilesAsZip(inputName, generatedFiles)
: generatedFiles.getFirst().file();
String packagedFilename = useZipPackage
? FileUtil.mainName(inputName) + ".zip"
: generatedFiles.getFirst().filename();
String contentType = useZipPackage ? "application/zip" : "text/plain";
String ossObjectKey = ossStorageService.uploadResultFile(outputFile, "CONVERT");
String ossObjectKey = ossStorageService.uploadResultFile(packagedResultFile, MODULE_TYPE);
String downloadUrl = ossStorageService.generateDownloadUrl(ossObjectKey);
ConvertResultItemVo item = new ConvertResultItemVo();
item.setSourceFilename(inputName);
item.setSuccess(true);
item.setOutputFilename(outputFilename);
item.setOutputFilename(packagedFilename);
item.setDownloadUrl(downloadUrl);
successCount++;
FileResultEntity resultEntity = new FileResultEntity();
resultEntity.setTaskId(task.getId());
resultEntity.setModuleType("CONVERT");
resultEntity.setModuleType(MODULE_TYPE);
resultEntity.setSourceFilename(inputName);
resultEntity.setResultFilename(outputFilename);
resultEntity.setResultFilename(packagedFilename);
resultEntity.setResultFileUrl(downloadUrl);
resultEntity.setResultFileSize(outputFile.length());
resultEntity.setResultContentType("text/plain");
resultEntity.setResultFileSize(packagedResultFile.length());
resultEntity.setResultContentType(contentType);
resultEntity.setSuccess(1);
resultEntity.setCreatedAt(LocalDateTime.now());
fileResultMapper.insert(resultEntity);
item.setResultId(resultEntity.getId());
items.add(item);
successCount++;
} catch (Exception ex) {
ConvertResultItemVo item = new ConvertResultItemVo();
item.setSourceFilename(sourceFile.getOriginalFilename());
item.setSuccess(false);
item.setError(ex.getMessage());
items.add(item);
failedCount++;
FileResultEntity resultEntity = new FileResultEntity();
resultEntity.setTaskId(task.getId());
resultEntity.setModuleType("CONVERT");
resultEntity.setModuleType(MODULE_TYPE);
resultEntity.setSourceFilename(sourceFile.getOriginalFilename());
resultEntity.setSuccess(0);
resultEntity.setErrorMessage(ex.getMessage());
resultEntity.setCreatedAt(LocalDateTime.now());
fileResultMapper.insert(resultEntity);
}
items.add(item);
}
if (folderMode && !archiveEntries.isEmpty()) {
File zipFile = packageFolderConvertResultsAsZip(request.getArchiveName(), archiveEntries);
String ossObjectKey = ossStorageService.uploadResultFile(zipFile, MODULE_TYPE);
String downloadUrl = ossStorageService.generateDownloadUrl(ossObjectKey);
ConvertResultItemVo item = new ConvertResultItemVo();
item.setSourceFilename(request.getArchiveName());
item.setOutputFilename(zipFile.getName());
item.setSuccess(true);
item.setDownloadUrl(downloadUrl);
FileResultEntity resultEntity = new FileResultEntity();
resultEntity.setTaskId(task.getId());
resultEntity.setModuleType(MODULE_TYPE);
resultEntity.setSourceFilename(request.getArchiveName());
resultEntity.setResultFilename(zipFile.getName());
resultEntity.setResultFileUrl(downloadUrl);
resultEntity.setResultFileSize(zipFile.length());
resultEntity.setResultContentType("application/zip");
resultEntity.setSuccess(1);
resultEntity.setCreatedAt(LocalDateTime.now());
fileResultMapper.insert(resultEntity);
item.setResultId(resultEntity.getId());
items.add(0, item);
}
task.setSuccessFileCount(successCount);
@@ -138,18 +197,18 @@ public class ConvertRunService {
public List<ConvertResultItemVo> listHistory() {
return fileResultMapper.selectList(new LambdaQueryWrapper<FileResultEntity>()
.eq(FileResultEntity::getModuleType, "CONVERT")
.eq(FileResultEntity::getSuccess, 1)
.eq(FileResultEntity::getModuleType, MODULE_TYPE)
.orderByDesc(FileResultEntity::getCreatedAt)
.last("limit 50"))
.last("limit 200"))
.stream()
.map(entity -> {
ConvertResultItemVo vo = new ConvertResultItemVo();
vo.setResultId(entity.getId());
vo.setSourceFilename(entity.getCreatedAt() == null ? "" : entity.getCreatedAt().toLocalDate().toString().replace("-", ""));
vo.setSourceFilename(entity.getSourceFilename());
vo.setOutputFilename(entity.getResultFilename());
vo.setDownloadUrl(entity.getResultFileUrl());
vo.setSuccess(true);
vo.setDownloadUrl(entity.getSuccess() != null && entity.getSuccess() == 1 ? entity.getResultFileUrl() : null);
vo.setSuccess(entity.getSuccess() != null && entity.getSuccess() == 1);
vo.setError(entity.getErrorMessage());
return vo;
})
.toList();
@@ -157,41 +216,71 @@ public class ConvertRunService {
public void deleteHistory(Long resultId) {
FileResultEntity entity = fileResultMapper.selectById(resultId);
if (entity == null || !"CONVERT".equals(entity.getModuleType())) {
throw new BusinessException("记录不存在");
if (entity == null || !MODULE_TYPE.equals(entity.getModuleType())) {
throw new BusinessException("Convert result record does not exist.");
}
fileResultMapper.deleteById(resultId);
}
public File getResultFile(Long resultId) {
FileResultEntity entity = fileResultMapper.selectById(resultId);
if (entity == null || entity.getResultFileUrl() == null || !"CONVERT".equals(entity.getModuleType())) {
throw new BusinessException("结果文件不存在");
if (entity == null || entity.getResultFileUrl() == null || !MODULE_TYPE.equals(entity.getModuleType())) {
throw new BusinessException("Convert result file does not exist.");
}
File file = new File(entity.getResultFileUrl());
if (!file.exists()) {
throw new BusinessException("结果文件不存在");
throw new BusinessException("Convert result file does not exist.");
}
return file;
}
private void writeTxtByLegacyRules(File inputFile, File outputFile, ConvertTemplateEntity templateEntity) throws IOException {
private List<GeneratedConvertFile> generateOutputFiles(File inputFile, ConvertTemplateEntity templateEntity) throws IOException {
List<String> outputFilenames = resolveOutputFilenames(templateEntity);
List<String> rows = buildTxtRows(inputFile, templateEntity);
File outputDir = FileUtil.mkdir(FileUtil.file(storageProperties.getLocalTempDir(), "convert-result"));
List<GeneratedConvertFile> generatedFiles = new ArrayList<>();
for (String outputFilename : outputFilenames) {
File outputFile = buildNamedOutputFile(outputDir, outputFilename);
FileUtil.writeLines(rows, outputFile, StandardCharsets.UTF_8);
generatedFiles.add(new GeneratedConvertFile(outputFilename, outputFile));
}
return generatedFiles;
}
private List<String> resolveOutputFilenames(ConvertTemplateEntity templateEntity) {
if (TEMPLATE_CODE_FIVE_COUNTRIES.equals(templateEntity.getTemplateCode())) {
return FIVE_COUNTRY_OUTPUT_FILES;
}
String outputFilename = templateEntity.getOutputFilename();
if (outputFilename == null || outputFilename.isBlank()) {
return List.of("result.txt");
}
return List.of(outputFilename);
}
private List<String> buildTxtRows(File inputFile, ConvertTemplateEntity templateEntity) throws IOException {
List<String> requiredColumns = readStringList(templateEntity.getRequiredSourceColumnsJson());
List<String> headerColumns = readStringList(templateEntity.getHeaderColumnsJson());
Map<String, String> fieldMapping = readStringMap(templateEntity.getFieldMappingJson());
Map<String, String> defaults = readStringMap(templateEntity.getDefaultsJson());
List<String> preambleLines = readStringList(templateEntity.getPreambleLinesJson());
int blankHeaderRowsAfterSchema = templateEntity.getBlankHeaderRowsAfterSchema() == null ? 0 : templateEntity.getBlankHeaderRowsAfterSchema();
int blankHeaderRowsAfterSchema = templateEntity.getBlankHeaderRowsAfterSchema() == null
? 0
: templateEntity.getBlankHeaderRowsAfterSchema();
DataFormatter formatter = new DataFormatter();
try (FileInputStream fis = new FileInputStream(inputFile); Workbook workbook = org.apache.poi.ss.usermodel.WorkbookFactory.create(fis)) {
try (FileInputStream fis = new FileInputStream(inputFile);
Workbook workbook = WorkbookFactory.create(fis)) {
Sheet sheet = workbook.getSheetAt(0);
Row headerRow = sheet.getRow(0);
if (headerRow == null) {
throw new BusinessException("Excel 表头为空");
throw new BusinessException("Excel header row is empty.");
}
Map<String, Integer> headerMap = new HashMap<>();
Map<String, Integer> headerMap = new LinkedHashMap<>();
for (int i = 0; i < headerRow.getLastCellNum(); i++) {
String value = normalizeCellText(formatter.formatCellValue(headerRow.getCell(i)));
if (!value.isBlank() && !headerMap.containsKey(value)) {
@@ -199,9 +288,11 @@ public class ConvertRunService {
}
}
List<String> missing = requiredColumns.stream().filter(column -> !headerMap.containsKey(column)).toList();
List<String> missing = requiredColumns.stream()
.filter(column -> !headerMap.containsKey(column))
.toList();
if (!missing.isEmpty()) {
throw new BusinessException("缺少列:" + String.join("", missing));
throw new BusinessException("Missing required columns: " + String.join(", ", missing));
}
List<String> rows = new ArrayList<>();
@@ -219,6 +310,7 @@ public class ConvertRunService {
if (row == null) {
continue;
}
String asin = asinIndex >= 0 ? normalizeCellText(formatter.formatCellValue(row.getCell(asinIndex))) : "";
if (asin.isBlank()) {
continue;
@@ -233,7 +325,9 @@ public class ConvertRunService {
if (fieldMapping.containsKey(column)) {
String sourceColumn = fieldMapping.get(column);
Integer sourceIndex = headerMap.get(sourceColumn);
String value = sourceIndex == null ? "" : normalizeCellText(formatter.formatCellValue(row.getCell(sourceIndex)));
String value = sourceIndex == null
? ""
: normalizeCellText(formatter.formatCellValue(row.getCell(sourceIndex)));
lineValues.add(value);
continue;
}
@@ -243,10 +337,11 @@ public class ConvertRunService {
}
lineValues.add("");
}
rows.add(String.join("\t", lineValues));
rowIndex++;
}
FileUtil.writeLines(rows, outputFile, StandardCharsets.UTF_8);
return rows;
}
}
@@ -255,20 +350,70 @@ public class ConvertRunService {
if (json == null || json.isBlank()) {
return new ArrayList<>();
}
return objectMapper.readValue(json, new TypeReference<List<String>>() {});
return objectMapper.readValue(json, new TypeReference<List<String>>() {
});
} catch (Exception ex) {
throw new BusinessException("模板配置解析失败");
throw new BusinessException("Template config parsing failed.");
}
}
private File packageGeneratedFilesAsZip(String inputName, List<GeneratedConvertFile> generatedFiles) {
File outputDir = FileUtil.mkdir(FileUtil.file(storageProperties.getLocalTempDir(), "convert-result"));
File zipFile = buildNamedOutputFile(outputDir, FileUtil.mainName(inputName) + ".zip");
try (ZipOutputStream zos = new ZipOutputStream(new FileOutputStream(zipFile))) {
for (GeneratedConvertFile generatedFile : generatedFiles) {
ZipEntry entry = new ZipEntry(generatedFile.filename());
zos.putNextEntry(entry);
Files.copy(generatedFile.file().toPath(), zos);
zos.closeEntry();
}
} catch (Exception ex) {
throw new BusinessException("Convert result packaging failed: " + ex.getMessage());
}
return zipFile;
}
private File packageFolderConvertResultsAsZip(String archiveName, List<ConvertArchiveEntry> archiveEntries) {
File outputDir = FileUtil.mkdir(FileUtil.file(storageProperties.getLocalTempDir(), "convert-result"));
File zipFile = buildNamedOutputFile(outputDir, archiveName + ".zip");
try (ZipOutputStream zos = new ZipOutputStream(new FileOutputStream(zipFile))) {
for (ConvertArchiveEntry entry : archiveEntries) {
String zipEntryName = buildFolderZipEntry(entry.relativePath(), entry.inputName(), entry.generatedFile().filename(), true);
zos.putNextEntry(new ZipEntry(zipEntryName));
Files.copy(entry.generatedFile().file().toPath(), zos);
zos.closeEntry();
}
} catch (Exception ex) {
throw new BusinessException("Convert folder result packaging failed: " + ex.getMessage());
}
return zipFile;
}
private String buildFolderZipEntry(String relativePath, String inputName, String outputFilename, boolean includeSourceStemFolder) {
String normalizedRelativePath = relativePath == null ? "" : relativePath.replace('\\', '/');
String relativeDir = normalizedRelativePath.contains("/")
? normalizedRelativePath.substring(0, normalizedRelativePath.lastIndexOf('/'))
: "";
List<String> parts = new ArrayList<>();
if (!relativeDir.isBlank()) {
parts.add(relativeDir);
}
if (includeSourceStemFolder) {
parts.add(FileUtil.mainName(inputName));
}
parts.add(outputFilename);
return String.join("/", parts);
}
private Map<String, String> readStringMap(String json) {
try {
if (json == null || json.isBlank()) {
return new HashMap<>();
}
return objectMapper.readValue(json, new TypeReference<Map<String, String>>() {});
return objectMapper.readValue(json, new TypeReference<Map<String, String>>() {
});
} catch (Exception ex) {
throw new BusinessException("模板配置解析失败");
throw new BusinessException("Template config parsing failed.");
}
}
@@ -277,8 +422,11 @@ public class ConvertRunService {
if (!baseDir.exists()) {
return null;
}
List<File> matchedFiles = FileUtil.loopFiles(baseDir, pathname -> pathname.isFile() && pathname.getName().startsWith(fileKey));
return matchedFiles.isEmpty() ? null : matchedFiles.getFirst();
List<File> matchedFiles = FileUtil.loopFiles(
baseDir,
pathname -> pathname.isFile() && pathname.getName().startsWith(fileKey)
);
return matchedFiles.isEmpty() ? null : matchedFiles.get(0);
}
private File buildNamedOutputFile(File outputDir, String filename) {
@@ -286,6 +434,7 @@ public class ConvertRunService {
if (!candidate.exists()) {
return candidate;
}
String mainName = FileUtil.mainName(filename);
String extName = FileUtil.extName(filename);
int index = 2;
@@ -314,4 +463,10 @@ public class ConvertRunService {
.trim()
.replaceAll("\\s+", " ");
}
private record GeneratedConvertFile(String filename, File file) {
}
private record ConvertArchiveEntry(String relativePath, String inputName, GeneratedConvertFile generatedFile) {
}
}

View File

@@ -45,6 +45,7 @@ public class DedupeRunController {
- 仅保留 selectedColumns 中指定的列;
- keepIntegerIds=true 时保留纯数字 ID
- keepUnderscoreIds=true 时保留类似 1_1 的 ID
- keepIntegerMainIdsWhenNoSubIds=true 且文件中不存在 1_1 这类子 ID 时,会自动保留纯数字主 ID
- 输出文件命名遵循 原文件名_cleaned.xlsx若重名自动追加序号。
""")
@ApiResponses({

View File

@@ -0,0 +1,93 @@
package com.nanri.aiimage.modules.dedupe.controller;
import com.nanri.aiimage.common.api.ApiResponse;
import com.nanri.aiimage.modules.dedupe.model.dto.DedupeTotalDataCreateRequest;
import com.nanri.aiimage.modules.dedupe.model.dto.DedupeTotalDataUpdateRequest;
import com.nanri.aiimage.modules.dedupe.model.vo.DedupeTotalDataImportVo;
import com.nanri.aiimage.modules.dedupe.model.vo.DedupeTotalDataItemVo;
import com.nanri.aiimage.modules.dedupe.model.vo.DedupeTotalDataPageVo;
import com.nanri.aiimage.modules.dedupe.service.DedupeTotalDataService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.media.Content;
import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.v3.oas.annotations.responses.ApiResponses;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;
@RestController
@RequiredArgsConstructor
@RequestMapping("/api/admin/dedupe-total-data")
@Tag(name = "数据去重总数据", description = "维护数据去重模块的总数据列表,支持增删改查。")
public class DedupeTotalDataController {
private final DedupeTotalDataService dedupeTotalDataService;
@GetMapping
@Operation(summary = "分页查询总数据", description = "分页查询数据去重总数据,支持按值模糊搜索。")
@ApiResponses({
@io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "200", description = "查询成功", content = @Content(schema = @Schema(implementation = DedupeTotalDataPageVo.class)))
})
public ApiResponse<DedupeTotalDataPageVo> page(
@Parameter(description = "页码") @RequestParam(defaultValue = "1") Long page,
@Parameter(description = "每页数量") @RequestParam(defaultValue = "15") Long pageSize,
@Parameter(description = "模糊搜索关键字") @RequestParam(required = false) String keyword) {
return ApiResponse.success(dedupeTotalDataService.page(page, pageSize, keyword));
}
@PostMapping
@Operation(summary = "新增总数据", description = "新增一条数据去重总数据。")
@ApiResponses({
@io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "200", description = "创建成功", content = @Content(schema = @Schema(implementation = DedupeTotalDataItemVo.class))),
@io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "400", description = "参数不合法或数据重复")
})
public ApiResponse<DedupeTotalDataItemVo> create(@Valid @RequestBody DedupeTotalDataCreateRequest request) {
return ApiResponse.success("创建成功", dedupeTotalDataService.create(request));
}
@PostMapping("/import")
@Operation(summary = "导入总数据", description = "上传 xlsx 文件,读取 ASIN 列并批量导入数据去重总数据。")
@ApiResponses({
@io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "200", description = "导入成功", content = @Content(schema = @Schema(implementation = DedupeTotalDataImportVo.class))),
@io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "400", description = "文件不合法或缺少 ASIN 列")
})
public ApiResponse<DedupeTotalDataImportVo> importExcel(
@Parameter(description = "xlsx 文件", required = true) @RequestParam("file") MultipartFile file) {
return ApiResponse.success("导入成功", dedupeTotalDataService.importFromExcel(file));
}
@PutMapping("/{id}")
@Operation(summary = "更新总数据", description = "按 ID 更新一条数据去重总数据。")
@ApiResponses({
@io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "200", description = "更新成功", content = @Content(schema = @Schema(implementation = DedupeTotalDataItemVo.class))),
@io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "400", description = "参数不合法或数据重复"),
@io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "404", description = "数据不存在")
})
public ApiResponse<DedupeTotalDataItemVo> update(
@Parameter(description = "主键ID", required = true) @PathVariable Long id,
@Valid @RequestBody DedupeTotalDataUpdateRequest request) {
return ApiResponse.success("更新成功", dedupeTotalDataService.update(id, request));
}
@DeleteMapping("/{id}")
@Operation(summary = "删除总数据", description = "按 ID 删除一条数据去重总数据。")
@ApiResponses({
@io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "200", description = "删除成功"),
@io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "404", description = "数据不存在")
})
public ApiResponse<Void> delete(@Parameter(description = "主键ID", required = true) @PathVariable Long id) {
dedupeTotalDataService.delete(id);
return ApiResponse.success("删除成功", null);
}
}

View File

@@ -0,0 +1,9 @@
package com.nanri.aiimage.modules.dedupe.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.nanri.aiimage.modules.dedupe.model.entity.DedupeTotalDataEntity;
import org.apache.ibatis.annotations.Mapper;
@Mapper
public interface DedupeTotalDataMapper extends BaseMapper<DedupeTotalDataEntity> {
}

View File

@@ -21,8 +21,14 @@ public class DedupeRunRequest {
private List<String> selectedColumns;
@Schema(description = "是否保留纯数字 ID")
private boolean keepIntegerIds = true;
private boolean keepIntegerIds = false;
@Schema(description = "是否保留带下划线 ID")
private boolean keepUnderscoreIds = true;
@Schema(description = "当文件中不存在下划线子 ID 时,是否自动保留纯数字主 ID")
private boolean keepIntegerMainIdsWhenNoSubIds = true;
@Schema(description = "文件夹上传场景下的总压缩包名称(不含扩展名)")
private String archiveName;
}

View File

@@ -14,4 +14,7 @@ public class DedupeSourceFileDto {
@Schema(description = "原始文件名")
private String originalFilename;
@Schema(description = "相对上传目录路径")
private String relativePath;
}

View File

@@ -0,0 +1,16 @@
package com.nanri.aiimage.modules.dedupe.model.dto;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.Size;
import lombok.Data;
@Data
@Schema(description = "数据去重总数据新增请求")
public class DedupeTotalDataCreateRequest {
@NotBlank(message = "数据不能为空")
@Size(max = 128, message = "数据长度不能超过128个字符")
@Schema(description = "总数据值", requiredMode = Schema.RequiredMode.REQUIRED)
private String dataValue;
}

View File

@@ -0,0 +1,16 @@
package com.nanri.aiimage.modules.dedupe.model.dto;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.Size;
import lombok.Data;
@Data
@Schema(description = "数据去重总数据更新请求")
public class DedupeTotalDataUpdateRequest {
@NotBlank(message = "数据不能为空")
@Size(max = 128, message = "数据长度不能超过128个字符")
@Schema(description = "总数据值", requiredMode = Schema.RequiredMode.REQUIRED)
private String dataValue;
}

View File

@@ -0,0 +1,19 @@
package com.nanri.aiimage.modules.dedupe.model.entity;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
import java.time.LocalDateTime;
@Data
@TableName("biz_dedupe_total_data")
public class DedupeTotalDataEntity {
@TableId(type = IdType.AUTO)
private Long id;
private String dataValue;
private LocalDateTime createdAt;
private LocalDateTime updatedAt;
}

View File

@@ -0,0 +1,21 @@
package com.nanri.aiimage.modules.dedupe.model.vo;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
@Data
@Schema(description = "数据去重总数据导入结果")
public class DedupeTotalDataImportVo {
@Schema(description = "读取总行数")
private Integer totalRows;
@Schema(description = "读取到的 ASIN 数量")
private Integer asinCount;
@Schema(description = "实际新增数量")
private Integer insertedCount;
@Schema(description = "跳过数量")
private Integer skippedCount;
}

View File

@@ -0,0 +1,20 @@
package com.nanri.aiimage.modules.dedupe.model.vo;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.time.LocalDateTime;
@Data
@Schema(description = "数据去重总数据项")
public class DedupeTotalDataItemVo {
@Schema(description = "主键ID")
private Long id;
@Schema(description = "总数据值")
private String dataValue;
@Schema(description = "创建时间")
private LocalDateTime createdAt;
}

View File

@@ -0,0 +1,23 @@
package com.nanri.aiimage.modules.dedupe.model.vo;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.util.List;
@Data
@Schema(description = "数据去重总数据分页结果")
public class DedupeTotalDataPageVo {
@Schema(description = "列表")
private List<DedupeTotalDataItemVo> items;
@Schema(description = "总数")
private Long total;
@Schema(description = "页码")
private Long page;
@Schema(description = "每页数量")
private Long pageSize;
}

View File

@@ -28,11 +28,14 @@ import org.springframework.stereotype.Service;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.nio.file.Files;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.zip.ZipEntry;
import java.util.zip.ZipOutputStream;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
@@ -46,7 +49,7 @@ public class DedupeRunService {
private final OssStorageService ossStorageService;
public DedupeRunVo run(DedupeRunRequest request) {
if (!request.isKeepIntegerIds() && !request.isKeepUnderscoreIds()) {
if (!request.isKeepIntegerIds() && !request.isKeepUnderscoreIds() && !request.isKeepIntegerMainIdsWhenNoSubIds()) {
throw new BusinessException("请至少选择一种 ID 保留规则");
}
@@ -63,9 +66,11 @@ public class DedupeRunService {
task.setFinishedAt(LocalDateTime.now());
fileTaskMapper.insert(task);
boolean folderMode = request.getArchiveName() != null && !request.getArchiveName().isBlank();
List<DedupeResultItemVo> items = new ArrayList<>();
int successCount = 0;
int failedCount = 0;
List<DedupeArchiveEntry> archiveEntries = new ArrayList<>();
for (DedupeSourceFileDto sourceFile : request.getFiles()) {
DedupeResultItemVo item = new DedupeResultItemVo();
@@ -81,13 +86,28 @@ public class DedupeRunService {
File outputDir = FileUtil.mkdir(FileUtil.file(storageProperties.getLocalTempDir(), "dedupe-result"));
File outputFile = buildNamedOutputFile(outputDir, outputFilename);
cleanExcelByLegacyRules(inputFile, outputFile, request.getSelectedColumns(), request.isKeepIntegerIds(), request.isKeepUnderscoreIds());
cleanExcelByLegacyRules(
inputFile,
outputFile,
request.getSelectedColumns(),
request.isKeepIntegerIds(),
request.isKeepUnderscoreIds(),
request.isKeepIntegerMainIdsWhenNoSubIds()
);
if (folderMode) {
archiveEntries.add(new DedupeArchiveEntry(sourceFile.getRelativePath(), inputName, outputFile));
successCount++;
continue;
}
String ossObjectKey = ossStorageService.uploadResultFile(outputFile, "DEDUPE");
String downloadUrl = ossStorageService.generateDownloadUrl(ossObjectKey);
String downloadFilename = request.getFiles().size() == 1 ? inputName : outputFile.getName();
item.setSuccess(true);
item.setOutputFilename(outputFile.getName());
item.setOutputFilename(downloadFilename);
item.setDownloadUrl(downloadUrl);
successCount++;
@@ -95,7 +115,7 @@ public class DedupeRunService {
resultEntity.setTaskId(task.getId());
resultEntity.setModuleType("DEDUPE");
resultEntity.setSourceFilename(inputName);
resultEntity.setResultFilename(outputFile.getName());
resultEntity.setResultFilename(downloadFilename);
resultEntity.setResultFileUrl(downloadUrl);
resultEntity.setResultFileSize(outputFile.length());
resultEntity.setResultContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet");
@@ -119,6 +139,32 @@ public class DedupeRunService {
items.add(item);
}
if (folderMode && !archiveEntries.isEmpty()) {
File zipFile = packageFolderDedupeResultsAsZip(request.getArchiveName(), archiveEntries);
String ossObjectKey = ossStorageService.uploadResultFile(zipFile, "DEDUPE");
String downloadUrl = ossStorageService.generateDownloadUrl(ossObjectKey);
DedupeResultItemVo item = new DedupeResultItemVo();
item.setSourceFilename(request.getArchiveName());
item.setOutputFilename(zipFile.getName());
item.setSuccess(true);
item.setDownloadUrl(downloadUrl);
FileResultEntity resultEntity = new FileResultEntity();
resultEntity.setTaskId(task.getId());
resultEntity.setModuleType("DEDUPE");
resultEntity.setSourceFilename(request.getArchiveName());
resultEntity.setResultFilename(zipFile.getName());
resultEntity.setResultFileUrl(downloadUrl);
resultEntity.setResultFileSize(zipFile.length());
resultEntity.setResultContentType("application/zip");
resultEntity.setSuccess(1);
resultEntity.setCreatedAt(LocalDateTime.now());
fileResultMapper.insert(resultEntity);
item.setResultId(resultEntity.getId());
items.add(0, item);
}
task.setSuccessFileCount(successCount);
task.setFailedFileCount(failedCount);
task.setResultJson(JSONUtil.toJsonStr(items));
@@ -136,17 +182,17 @@ public class DedupeRunService {
public List<DedupeResultItemVo> listHistory() {
return fileResultMapper.selectList(new LambdaQueryWrapper<FileResultEntity>()
.eq(FileResultEntity::getModuleType, "DEDUPE")
.eq(FileResultEntity::getSuccess, 1)
.orderByDesc(FileResultEntity::getCreatedAt)
.last("limit 50"))
.stream()
.map(entity -> {
DedupeResultItemVo vo = new DedupeResultItemVo();
vo.setResultId(entity.getId());
vo.setSourceFilename(entity.getCreatedAt() == null ? "" : entity.getCreatedAt().toLocalDate().toString().replace("-", ""));
vo.setSourceFilename(entity.getSourceFilename());
vo.setOutputFilename(entity.getResultFilename());
vo.setDownloadUrl(entity.getResultFileUrl());
vo.setSuccess(true);
vo.setDownloadUrl(entity.getSuccess() != null && entity.getSuccess() == 1 ? entity.getResultFileUrl() : null);
vo.setSuccess(entity.getSuccess() != null && entity.getSuccess() == 1);
vo.setError(entity.getErrorMessage());
return vo;
})
.toList();
@@ -173,7 +219,8 @@ public class DedupeRunService {
}
private void cleanExcelByLegacyRules(File inputFile, File outputFile, List<String> selectedColumns,
boolean keepIntegerIds, boolean keepUnderscoreIds) throws Exception {
boolean keepIntegerIds, boolean keepUnderscoreIds,
boolean keepIntegerMainIdsWhenNoSubIds) throws Exception {
DataFormatter formatter = new DataFormatter();
try (FileInputStream fis = new FileInputStream(inputFile);
Workbook workbook = org.apache.poi.ss.usermodel.WorkbookFactory.create(fis);
@@ -215,6 +262,26 @@ public class DedupeRunService {
}
Integer idColumnIndex = headerMap.get("id");
Map<String, Boolean> mainIdHasSubIdMap = new HashMap<>();
if (idColumnIndex != null) {
for (int rowNum = 1; rowNum <= sheet.getLastRowNum(); rowNum++) {
Row row = sheet.getRow(rowNum);
if (row == null) {
continue;
}
String idValue = normalizeCellText(formatter.formatCellValue(row.getCell(idColumnIndex)));
String mainId = extractMainId(idValue);
if (mainId.isEmpty()) {
continue;
}
if (!mainIdHasSubIdMap.containsKey(mainId)) {
mainIdHasSubIdMap.put(mainId, false);
}
if (idValue.matches("\\d+_\\d+")) {
mainIdHasSubIdMap.put(mainId, true);
}
}
}
int outputRowIndex = 1;
for (int rowNum = 1; rowNum <= sheet.getLastRowNum(); rowNum++) {
Row row = sheet.getRow(rowNum);
@@ -223,7 +290,7 @@ public class DedupeRunService {
}
if (idColumnIndex != null) {
String idValue = normalizeCellText(formatter.formatCellValue(row.getCell(idColumnIndex)));
if (!shouldKeepId(idValue, keepIntegerIds, keepUnderscoreIds)) {
if (!shouldKeepId(idValue, keepIntegerIds, keepUnderscoreIds, keepIntegerMainIdsWhenNoSubIds, mainIdHasSubIdMap)) {
continue;
}
}
@@ -242,14 +309,68 @@ public class DedupeRunService {
}
}
private boolean shouldKeepId(String text, boolean keepIntegerIds, boolean keepUnderscoreIds) {
private File packageFolderDedupeResultsAsZip(String archiveName, List<DedupeArchiveEntry> archiveEntries) {
File outputDir = FileUtil.mkdir(FileUtil.file(storageProperties.getLocalTempDir(), "dedupe-result"));
File zipFile = buildNamedOutputFile(outputDir, archiveName + ".zip");
try (ZipOutputStream zos = new ZipOutputStream(new FileOutputStream(zipFile))) {
for (DedupeArchiveEntry entry : archiveEntries) {
String zipEntryName = buildFolderZipEntry(entry.relativePath(), entry.inputName(), entry.inputName(), false);
zos.putNextEntry(new ZipEntry(zipEntryName));
Files.copy(entry.outputFile().toPath(), zos);
zos.closeEntry();
}
} catch (Exception ex) {
throw new BusinessException("去重结果打包失败:" + ex.getMessage());
}
return zipFile;
}
private String buildFolderZipEntry(String relativePath, String inputName, String outputFilename, boolean includeSourceStemFolder) {
String normalizedRelativePath = relativePath == null ? "" : relativePath.replace('\\', '/');
String relativeDir = normalizedRelativePath.contains("/")
? normalizedRelativePath.substring(0, normalizedRelativePath.lastIndexOf('/'))
: "";
List<String> parts = new ArrayList<>();
if (!relativeDir.isBlank()) {
parts.add(relativeDir);
}
if (includeSourceStemFolder) {
parts.add(FileUtil.mainName(inputName));
}
parts.add(outputFilename);
return String.join("/", parts);
}
private boolean shouldKeepId(String text, boolean keepIntegerIds, boolean keepUnderscoreIds,
boolean keepIntegerMainIdsWhenNoSubIds, Map<String, Boolean> mainIdHasSubIdMap) {
if (text == null || text.isBlank()) {
return false;
}
if (keepUnderscoreIds && text.matches("\\d+_\\d+")) {
return true;
}
if (keepIntegerIds && text.matches("\\d+")) {
return true;
}
return keepUnderscoreIds && text.matches("\\d+_\\d+");
if (keepIntegerMainIdsWhenNoSubIds && text.matches("\\d+")) {
String mainId = extractMainId(text);
return !mainId.isEmpty() && !Boolean.TRUE.equals(mainIdHasSubIdMap.get(mainId));
}
return false;
}
private String extractMainId(String text) {
if (text == null || text.isBlank()) {
return "";
}
if (text.matches("\\d+")) {
return text;
}
if (text.matches("\\d+_\\d+")) {
int idx = text.indexOf('_');
return idx > 0 ? text.substring(0, idx) : "";
}
return "";
}
private File findLocalSourceFile(String fileKey) {
@@ -300,4 +421,7 @@ public class DedupeRunService {
.trim()
.replaceAll("\\s+", " ");
}
private record DedupeArchiveEntry(String relativePath, String inputName, File outputFile) {
}
}

View File

@@ -0,0 +1,215 @@
package com.nanri.aiimage.modules.dedupe.service;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
import com.nanri.aiimage.common.exception.BusinessException;
import com.nanri.aiimage.modules.dedupe.mapper.DedupeTotalDataMapper;
import com.nanri.aiimage.modules.dedupe.model.dto.DedupeTotalDataCreateRequest;
import com.nanri.aiimage.modules.dedupe.model.dto.DedupeTotalDataUpdateRequest;
import com.nanri.aiimage.modules.dedupe.model.entity.DedupeTotalDataEntity;
import com.nanri.aiimage.modules.dedupe.model.vo.DedupeTotalDataImportVo;
import com.nanri.aiimage.modules.dedupe.model.vo.DedupeTotalDataItemVo;
import com.nanri.aiimage.modules.dedupe.model.vo.DedupeTotalDataPageVo;
import lombok.RequiredArgsConstructor;
import org.apache.poi.ss.usermodel.Cell;
import org.apache.poi.ss.usermodel.DataFormatter;
import org.apache.poi.ss.usermodel.Row;
import org.apache.poi.ss.usermodel.Sheet;
import org.apache.poi.ss.usermodel.Workbook;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.multipart.MultipartFile;
import java.io.InputStream;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
@Service
@RequiredArgsConstructor
public class DedupeTotalDataService {
private final DedupeTotalDataMapper dedupeTotalDataMapper;
public DedupeTotalDataPageVo page(long page, long pageSize, String keyword) {
long safePage = Math.max(page, 1);
long safePageSize = Math.min(Math.max(pageSize, 1), 100);
String safeKeyword = keyword == null ? "" : keyword.trim();
LambdaQueryWrapper<DedupeTotalDataEntity> query = new LambdaQueryWrapper<DedupeTotalDataEntity>()
.like(!safeKeyword.isEmpty(), DedupeTotalDataEntity::getDataValue, safeKeyword)
.orderByDesc(DedupeTotalDataEntity::getId);
Long total = dedupeTotalDataMapper.selectCount(query);
List<DedupeTotalDataItemVo> items = dedupeTotalDataMapper.selectList(query.last("LIMIT " + ((safePage - 1) * safePageSize) + ", " + safePageSize))
.stream()
.map(this::toItemVo)
.toList();
DedupeTotalDataPageVo vo = new DedupeTotalDataPageVo();
vo.setItems(items);
vo.setTotal(total);
vo.setPage(safePage);
vo.setPageSize(safePageSize);
return vo;
}
@Transactional
public DedupeTotalDataItemVo create(DedupeTotalDataCreateRequest request) {
String dataValue = normalizeDataValue(request.getDataValue());
ensureUnique(dataValue, null);
DedupeTotalDataEntity entity = new DedupeTotalDataEntity();
entity.setDataValue(dataValue);
dedupeTotalDataMapper.insert(entity);
return toItemVo(getById(entity.getId()));
}
@Transactional
public DedupeTotalDataImportVo importFromExcel(MultipartFile file) {
if (file == null || file.isEmpty()) {
throw new BusinessException("请上传 xlsx 文件");
}
String filename = file.getOriginalFilename() == null ? "" : file.getOriginalFilename().toLowerCase();
if (!(filename.endsWith(".xlsx") || filename.endsWith(".xls"))) {
throw new BusinessException("仅支持 .xlsx 或 .xls 文件");
}
DataFormatter formatter = new DataFormatter();
try (InputStream inputStream = file.getInputStream();
Workbook workbook = org.apache.poi.ss.usermodel.WorkbookFactory.create(inputStream)) {
Sheet sheet = workbook.getSheetAt(0);
Row headerRow = sheet.getRow(0);
if (headerRow == null) {
throw new BusinessException("Excel 表头为空");
}
Map<String, Integer> headerMap = new HashMap<>();
for (int i = 0; i < headerRow.getLastCellNum(); i++) {
Cell cell = headerRow.getCell(i);
String value = normalizeExcelText(cell == null ? null : formatter.formatCellValue(cell));
if (!value.isBlank() && !headerMap.containsKey(value)) {
headerMap.put(value, i);
}
}
Integer asinIndex = headerMap.get("ASIN");
if (asinIndex == null) {
throw new BusinessException("缺少 ASIN 列");
}
Set<String> seenInFile = new HashSet<>();
int totalRows = Math.max(sheet.getLastRowNum(), 0);
int asinCount = 0;
int insertedCount = 0;
int skippedCount = 0;
for (int rowNum = 1; rowNum <= sheet.getLastRowNum(); rowNum++) {
Row row = sheet.getRow(rowNum);
if (row == null) {
skippedCount++;
continue;
}
String asin = normalizeExcelText(formatter.formatCellValue(row.getCell(asinIndex)));
if (asin.isBlank()) {
skippedCount++;
continue;
}
asinCount++;
String dataValue = normalizeDataValue(asin);
if (!seenInFile.add(dataValue)) {
skippedCount++;
continue;
}
if (existsDataValue(dataValue)) {
skippedCount++;
continue;
}
DedupeTotalDataEntity entity = new DedupeTotalDataEntity();
entity.setDataValue(dataValue);
dedupeTotalDataMapper.insert(entity);
insertedCount++;
}
DedupeTotalDataImportVo vo = new DedupeTotalDataImportVo();
vo.setTotalRows(totalRows);
vo.setAsinCount(asinCount);
vo.setInsertedCount(insertedCount);
vo.setSkippedCount(skippedCount);
return vo;
} catch (BusinessException e) {
throw e;
} catch (Exception e) {
throw new BusinessException("导入 Excel 失败");
}
}
@Transactional
public DedupeTotalDataItemVo update(Long id, DedupeTotalDataUpdateRequest request) {
DedupeTotalDataEntity entity = getById(id);
String dataValue = normalizeDataValue(request.getDataValue());
ensureUnique(dataValue, id);
entity.setDataValue(dataValue);
dedupeTotalDataMapper.updateById(entity);
return toItemVo(getById(id));
}
@Transactional
public void delete(Long id) {
DedupeTotalDataEntity entity = getById(id);
dedupeTotalDataMapper.deleteById(entity.getId());
}
private DedupeTotalDataEntity getById(Long id) {
DedupeTotalDataEntity entity = dedupeTotalDataMapper.selectById(id);
if (entity == null) {
throw new BusinessException("数据不存在");
}
return entity;
}
private String normalizeDataValue(String value) {
String dataValue = value == null ? "" : value.trim();
if (dataValue.isEmpty()) {
throw new BusinessException("数据不能为空");
}
return dataValue;
}
private void ensureUnique(String dataValue, Long excludeId) {
LambdaQueryWrapper<DedupeTotalDataEntity> query = new LambdaQueryWrapper<DedupeTotalDataEntity>()
.eq(DedupeTotalDataEntity::getDataValue, dataValue)
.last("LIMIT 1");
DedupeTotalDataEntity exists = dedupeTotalDataMapper.selectOne(query);
if (exists != null && (excludeId == null || !exists.getId().equals(excludeId))) {
throw new BusinessException("数据已存在");
}
}
private boolean existsDataValue(String dataValue) {
LambdaQueryWrapper<DedupeTotalDataEntity> query = new LambdaQueryWrapper<DedupeTotalDataEntity>()
.eq(DedupeTotalDataEntity::getDataValue, dataValue)
.last("LIMIT 1");
return dedupeTotalDataMapper.selectOne(query) != null;
}
private String normalizeExcelText(String value) {
if (value == null) {
return "";
}
return value.replace("\ufeff", "")
.replace("\u3000", " ")
.replace("\r\n", " ")
.replace("\r", " ")
.replace("\n", " ")
.replace("\t", " ")
.trim()
.replaceAll("\\s+", " ");
}
private DedupeTotalDataItemVo toItemVo(DedupeTotalDataEntity entity) {
DedupeTotalDataItemVo vo = new DedupeTotalDataItemVo();
vo.setId(entity.getId());
vo.setDataValue(entity.getDataValue());
vo.setCreatedAt(entity.getCreatedAt());
return vo;
}
}

View File

@@ -46,8 +46,9 @@ public class FileUploadController {
})
public ApiResponse<UploadFileVo> upload(
@Parameter(name = "file", description = "待上传的 Excel 或业务源文件", required = true, in = ParameterIn.QUERY)
MultipartFile file) throws Exception {
return ApiResponse.success(localFileStorageService.saveTempFile(file));
MultipartFile file,
@RequestParam(required = false) String relativePath) throws Exception {
return ApiResponse.success(localFileStorageService.saveTempFile(file, relativePath));
}
@GetMapping("/excel-info")

View File

@@ -18,4 +18,7 @@ public class UploadFileVo {
@Schema(description = "文件大小")
private Long size;
@Schema(description = "相对上传目录路径")
private String relativePath;
}

View File

@@ -29,7 +29,7 @@ public class LocalFileStorageService {
private final StorageProperties storageProperties;
public UploadFileVo saveTempFile(MultipartFile file) throws IOException {
public UploadFileVo saveTempFile(MultipartFile file, String relativePath) throws IOException {
File tempDir = FileUtil.file(storageProperties.getLocalTempDir());
File parentDir = tempDir.getParentFile();
if (parentDir != null) {
@@ -46,6 +46,7 @@ public class LocalFileStorageService {
vo.setOriginalFilename(file.getOriginalFilename());
vo.setLocalPath(target.getAbsolutePath());
vo.setSize(file.getSize());
vo.setRelativePath(relativePath);
return vo;
}

View File

@@ -46,7 +46,8 @@ public class SplitRunController {
- splitMode=rows_per_file 时按固定条数切分;
- splitMode=parts 时按总条数尽量均分;
- 输出文件命名遵循 原文件名_split_序号.xlsx
- 返回结果中 rowCount 表示该结果文件包含的数据行数。
- 本次拆分任务产生的所有 xlsx 会统一打成一个 zip 上传;
- 返回结果中 rowCount 表示该压缩包包含的数据总行数entries 表示压缩包内文件摘要。
""")
@ApiResponses({
@io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "200", description = "执行成功,返回拆分结果统计", content = @Content(schema = @Schema(implementation = SplitRunVo.class))),

View File

@@ -30,4 +30,7 @@ public class SplitRunRequest {
@Schema(description = "按份数拆分时的份数")
private Integer parts;
@Schema(description = "文件夹上传场景下的总压缩包名称(不含扩展名)")
private String archiveName;
}

View File

@@ -14,4 +14,7 @@ public class SplitSourceFileDto {
@Schema(description = "原始文件名")
private String originalFilename;
@Schema(description = "相对上传目录路径")
private String relativePath;
}

View File

@@ -0,0 +1,15 @@
package com.nanri.aiimage.modules.split.model.vo;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
@Data
@Schema(description = "拆分压缩包内文件项")
public class SplitArchiveEntryVo {
@Schema(description = "文件名")
private String filename;
@Schema(description = "该文件包含的数据行数")
private Integer rowCount;
}

View File

@@ -3,6 +3,8 @@ package com.nanri.aiimage.modules.split.model.vo;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.util.List;
@Data
@Schema(description = "数据拆分结果项")
public class SplitResultItemVo {
@@ -22,9 +24,15 @@ public class SplitResultItemVo {
@Schema(description = "错误信息")
private String error;
@Schema(description = "结果行数")
@Schema(description = "结果行数")
private Integer rowCount;
@Schema(description = "下载地址")
private String downloadUrl;
@Schema(description = "压缩包内文件数量")
private Integer entryCount;
@Schema(description = "压缩包内文件列表")
private List<SplitArchiveEntryVo> entries;
}

View File

@@ -3,13 +3,15 @@ package com.nanri.aiimage.modules.split.service;
import cn.hutool.core.io.FileUtil;
import cn.hutool.core.util.IdUtil;
import cn.hutool.json.JSONUtil;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.nanri.aiimage.common.exception.BusinessException;
import com.nanri.aiimage.config.StorageProperties;
import com.nanri.aiimage.modules.file.service.oss.OssStorageService;
import com.nanri.aiimage.modules.split.model.dto.SplitRunRequest;
import com.nanri.aiimage.modules.split.model.dto.SplitSourceFileDto;
import com.nanri.aiimage.modules.split.model.vo.SplitArchiveEntryVo;
import com.nanri.aiimage.modules.split.model.vo.SplitResultItemVo;
import com.nanri.aiimage.modules.split.model.vo.SplitRunVo;
import com.nanri.aiimage.modules.file.service.oss.OssStorageService;
import com.nanri.aiimage.modules.task.mapper.FileResultMapper;
import com.nanri.aiimage.modules.task.mapper.FileTaskMapper;
import com.nanri.aiimage.modules.task.model.entity.FileResultEntity;
@@ -20,6 +22,7 @@ import org.apache.poi.ss.usermodel.DataFormatter;
import org.apache.poi.ss.usermodel.Row;
import org.apache.poi.ss.usermodel.Sheet;
import org.apache.poi.ss.usermodel.Workbook;
import org.apache.poi.ss.usermodel.WorkbookFactory;
import org.apache.poi.xssf.usermodel.XSSFWorkbook;
import org.springframework.core.io.FileSystemResource;
import org.springframework.core.io.Resource;
@@ -28,18 +31,28 @@ import org.springframework.stereotype.Service;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.nio.file.Files;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import java.util.zip.ZipEntry;
import java.util.zip.ZipOutputStream;
@Service
@RequiredArgsConstructor
public class SplitRunService {
private static final String MODULE_TYPE = "SPLIT";
private static final String ZIP_CONTENT_TYPE = "application/zip";
private static final String SPLIT_MODE_ROWS_PER_FILE = "rows_per_file";
private static final String SPLIT_MODE_PARTS = "parts";
private static final String HEADER_SUFFIX_MARKER = "idASIN\u56fd\u5bb6\u72b6\u6001\u4ef7\u683c\u53d8\u4f53\u6570\u91cf";
private static final String HEADER_STOP_COLUMN = "\u7f29\u7565\u56fe\u5730\u57408";
private static final DateTimeFormatter ZIP_NAME_DATE_FORMATTER = DateTimeFormatter.ofPattern("yyyyMMdd");
private final FileTaskMapper fileTaskMapper;
private final FileResultMapper fileResultMapper;
private final StorageProperties storageProperties;
@@ -49,8 +62,8 @@ public class SplitRunService {
validateRequest(request);
FileTaskEntity task = new FileTaskEntity();
task.setTaskNo("SPLIT-" + IdUtil.getSnowflakeNextIdStr());
task.setModuleType("SPLIT");
task.setTaskNo(MODULE_TYPE + "-" + IdUtil.getSnowflakeNextIdStr());
task.setModuleType(MODULE_TYPE);
task.setTaskMode("IMMEDIATE");
task.setStatus("SUCCESS");
task.setSourceFileCount(request.getFiles().size());
@@ -61,56 +74,42 @@ public class SplitRunService {
task.setFinishedAt(LocalDateTime.now());
fileTaskMapper.insert(task);
boolean folderMode = request.getArchiveName() != null && !request.getArchiveName().isBlank();
List<SplitResultItemVo> items = new ArrayList<>();
int successCount = 0;
int failedCount = 0;
List<TaskSplitChunkResult> allChunkResults = new ArrayList<>();
List<String> successSourceNames = new ArrayList<>();
for (SplitSourceFileDto sourceFile : request.getFiles()) {
try {
File inputFile = findLocalSourceFile(sourceFile.getFileKey());
if (inputFile == null || !inputFile.exists()) {
throw new BusinessException("上传文件不存在,请重新上传");
throw new BusinessException("Uploaded source file does not exist, please upload again.");
}
String inputName = sourceFile.getOriginalFilename() == null ? inputFile.getName() : sourceFile.getOriginalFilename();
String inputName = sourceFile.getOriginalFilename() == null
? inputFile.getName()
: sourceFile.getOriginalFilename();
List<SplitChunkResult> chunkResults = splitExcelByLegacyRules(inputFile, inputName, request);
for (SplitChunkResult chunkResult : chunkResults) {
SplitResultItemVo item = new SplitResultItemVo();
item.setSourceFilename(inputName);
item.setOutputFilename(chunkResult.outputFile().getName());
item.setSuccess(true);
item.setRowCount(chunkResult.rowCount());
String ossObjectKey = ossStorageService.uploadResultFile(chunkResult.outputFile(), "SPLIT");
String downloadUrl = ossStorageService.generateDownloadUrl(ossObjectKey);
FileResultEntity resultEntity = new FileResultEntity();
resultEntity.setTaskId(task.getId());
resultEntity.setModuleType("SPLIT");
resultEntity.setSourceFilename(inputName);
resultEntity.setResultFilename(chunkResult.outputFile().getName());
resultEntity.setResultFileUrl(downloadUrl);
resultEntity.setResultFileSize(chunkResult.outputFile().length());
resultEntity.setResultContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet");
resultEntity.setRowCount(chunkResult.rowCount());
resultEntity.setSuccess(1);
resultEntity.setCreatedAt(LocalDateTime.now());
fileResultMapper.insert(resultEntity);
item.setDownloadUrl(downloadUrl);
item.setResultId(resultEntity.getId());
items.add(item);
allChunkResults.add(new TaskSplitChunkResult(sourceFile.getRelativePath(), inputName, chunkResult.outputFile(), chunkResult.rowCount()));
}
successSourceNames.add(inputName);
successCount++;
} catch (Exception ex) {
SplitResultItemVo item = new SplitResultItemVo();
item.setSourceFilename(sourceFile.getOriginalFilename());
item.setSuccess(false);
item.setError(ex.getMessage());
item.setEntries(new ArrayList<>());
item.setEntryCount(0);
items.add(item);
failedCount++;
FileResultEntity resultEntity = new FileResultEntity();
resultEntity.setTaskId(task.getId());
resultEntity.setModuleType("SPLIT");
resultEntity.setModuleType(MODULE_TYPE);
resultEntity.setSourceFilename(sourceFile.getOriginalFilename());
resultEntity.setSuccess(0);
resultEntity.setErrorMessage(ex.getMessage());
@@ -119,6 +118,38 @@ public class SplitRunService {
}
}
if (!allChunkResults.isEmpty()) {
File zipFile = packageTaskSplitResultsAsZip(request.getArchiveName(), successSourceNames, allChunkResults);
String ossObjectKey = ossStorageService.uploadResultFile(zipFile, MODULE_TYPE);
String downloadUrl = ossStorageService.generateDownloadUrl(ossObjectKey);
SplitResultItemVo item = new SplitResultItemVo();
item.setSourceFilename(folderMode && request.getArchiveName() != null && !request.getArchiveName().isBlank()
? request.getArchiveName()
: successSourceNames.size() == 1 ? successSourceNames.get(0) : "Current split task");
item.setOutputFilename(zipFile.getName());
item.setSuccess(true);
item.setRowCount(allChunkResults.stream().mapToInt(TaskSplitChunkResult::rowCount).sum());
item.setDownloadUrl(downloadUrl);
item.setEntryCount(allChunkResults.size());
item.setEntries(allChunkResults.stream().map(this::toArchiveEntry).toList());
FileResultEntity resultEntity = new FileResultEntity();
resultEntity.setTaskId(task.getId());
resultEntity.setModuleType(MODULE_TYPE);
resultEntity.setSourceFilename(item.getSourceFilename());
resultEntity.setResultFilename(zipFile.getName());
resultEntity.setResultFileUrl(downloadUrl);
resultEntity.setResultFileSize(zipFile.length());
resultEntity.setResultContentType(ZIP_CONTENT_TYPE);
resultEntity.setRowCount(item.getRowCount());
resultEntity.setSuccess(1);
resultEntity.setCreatedAt(LocalDateTime.now());
fileResultMapper.insert(resultEntity);
item.setResultId(resultEntity.getId());
items.add(0, item);
}
task.setSuccessFileCount(successCount);
task.setFailedFileCount(failedCount);
task.setResultJson(JSONUtil.toJsonStr(items));
@@ -135,19 +166,21 @@ public class SplitRunService {
public List<SplitResultItemVo> listHistory() {
return fileResultMapper.selectList(new LambdaQueryWrapper<FileResultEntity>()
.eq(FileResultEntity::getModuleType, "SPLIT")
.eq(FileResultEntity::getSuccess, 1)
.eq(FileResultEntity::getModuleType, MODULE_TYPE)
.orderByDesc(FileResultEntity::getCreatedAt)
.last("limit 100"))
.stream()
.map(entity -> {
SplitResultItemVo vo = new SplitResultItemVo();
vo.setResultId(entity.getId());
vo.setSourceFilename(entity.getCreatedAt() == null ? "" : entity.getCreatedAt().toLocalDate().toString().replace("-", ""));
vo.setSourceFilename(entity.getSourceFilename());
vo.setOutputFilename(entity.getResultFilename());
vo.setDownloadUrl(entity.getResultFileUrl());
vo.setDownloadUrl(entity.getSuccess() != null && entity.getSuccess() == 1 ? entity.getResultFileUrl() : null);
vo.setRowCount(entity.getRowCount());
vo.setSuccess(true);
vo.setSuccess(entity.getSuccess() != null && entity.getSuccess() == 1);
vo.setError(entity.getErrorMessage());
vo.setEntryCount(0);
vo.setEntries(new ArrayList<>());
return vo;
})
.toList();
@@ -155,55 +188,45 @@ public class SplitRunService {
public void deleteHistory(Long resultId) {
FileResultEntity entity = fileResultMapper.selectById(resultId);
if (entity == null || !"SPLIT".equals(entity.getModuleType())) {
throw new BusinessException("记录不存在");
if (entity == null || !MODULE_TYPE.equals(entity.getModuleType())) {
throw new BusinessException("Split result record does not exist.");
}
fileResultMapper.deleteById(resultId);
}
public Resource getResultFile(Long resultId) {
FileResultEntity entity = fileResultMapper.selectById(resultId);
if (entity == null || entity.getResultFileUrl() == null || !"SPLIT".equals(entity.getModuleType())) {
throw new BusinessException("结果文件不存在");
if (entity == null || entity.getResultFileUrl() == null || !MODULE_TYPE.equals(entity.getModuleType())) {
throw new BusinessException("Split result file does not exist.");
}
File file = new File(entity.getResultFileUrl());
if (!file.exists()) {
throw new BusinessException("结果文件不存在");
throw new BusinessException("Split result file does not exist.");
}
return new FileSystemResource(file);
}
private List<SplitChunkResult> splitExcelByLegacyRules(File inputFile, String originalFilename, SplitRunRequest request) throws Exception {
private List<SplitChunkResult> splitExcelByLegacyRules(
File inputFile,
String originalFilename,
SplitRunRequest request
) throws Exception {
DataFormatter formatter = new DataFormatter();
try (FileInputStream fis = new FileInputStream(inputFile); Workbook workbook = org.apache.poi.ss.usermodel.WorkbookFactory.create(fis)) {
try (FileInputStream fis = new FileInputStream(inputFile);
Workbook workbook = WorkbookFactory.create(fis)) {
Sheet sheet = workbook.getSheetAt(0);
Row headerRow = sheet.getRow(0);
if (headerRow == null) {
throw new BusinessException("Excel 表头为空");
throw new BusinessException("Excel header row is empty.");
}
Map<String, Integer> headerMap = new HashMap<>();
for (int i = 0; i < headerRow.getLastCellNum(); i++) {
Cell cell = headerRow.getCell(i);
String value = normalizeCellText(cell == null ? null : formatter.formatCellValue(cell));
if (value.isBlank()) {
continue;
}
value = value.split("idASIN国家状态价格变体数量", 2)[0].trim().isEmpty()
? value
: value.split("idASIN国家状态价格变体数量", 2)[0].trim();
if (headerMap.containsKey(value)) {
continue;
}
headerMap.put(value, i);
if ("缩略图地址8".equals(value)) {
break;
}
}
List<String> missing = request.getSelectedColumns().stream().filter(column -> !headerMap.containsKey(column)).toList();
Map<String, Integer> headerMap = buildHeaderMap(headerRow, formatter);
List<String> missing = request.getSelectedColumns().stream()
.filter(column -> !headerMap.containsKey(column))
.toList();
if (!missing.isEmpty()) {
throw new BusinessException("缺少列:" + String.join("", missing));
throw new BusinessException("Missing required columns: " + String.join(", ", missing));
}
List<List<String>> rowsData = new ArrayList<>();
@@ -215,39 +238,23 @@ public class SplitRunService {
List<String> projected = new ArrayList<>();
for (String column : request.getSelectedColumns()) {
Integer sourceIndex = headerMap.get(column);
String value = sourceIndex == null ? "" : normalizeCellText(formatter.formatCellValue(row.getCell(sourceIndex)));
String value = sourceIndex == null
? ""
: normalizeCellText(formatter.formatCellValue(row.getCell(sourceIndex)));
projected.add(value);
}
rowsData.add(projected);
}
int totalRows = rowsData.size();
if (totalRows == 0) {
throw new BusinessException("没有可拆分的数据行");
}
List<List<List<String>>> chunks = new ArrayList<>();
if ("rows_per_file".equals(request.getSplitMode())) {
int chunkSize = request.getRowsPerFile();
for (int i = 0; i < totalRows; i += chunkSize) {
chunks.add(rowsData.subList(i, Math.min(i + chunkSize, totalRows)));
}
} else {
int actualParts = Math.min(request.getParts(), totalRows);
int base = totalRows / actualParts;
int remainder = totalRows % actualParts;
int start = 0;
for (int i = 0; i < actualParts; i++) {
int size = base + (i < remainder ? 1 : 0);
int end = start + size;
chunks.add(rowsData.subList(start, end));
start = end;
}
if (rowsData.isEmpty()) {
throw new BusinessException("No data rows available for splitting.");
}
List<List<List<String>>> chunks = buildChunks(rowsData, request);
File outputDir = FileUtil.mkdir(FileUtil.file(storageProperties.getLocalTempDir(), "split-result"));
String sourceStem = FileUtil.mainName(originalFilename);
List<SplitChunkResult> results = new ArrayList<>();
for (int i = 0; i < chunks.size(); i++) {
File outputFile = buildNamedOutputFile(outputDir, sourceStem + "_split_" + (i + 1) + ".xlsx");
try (XSSFWorkbook outputWorkbook = new XSSFWorkbook()) {
@@ -269,19 +276,63 @@ public class SplitRunService {
}
results.add(new SplitChunkResult(outputFile, chunks.get(i).size()));
}
return results;
}
}
private Map<String, Integer> buildHeaderMap(Row headerRow, DataFormatter formatter) {
Map<String, Integer> headerMap = new HashMap<>();
for (int i = 0; i < headerRow.getLastCellNum(); i++) {
Cell cell = headerRow.getCell(i);
String value = normalizeHeaderName(cell == null ? null : formatter.formatCellValue(cell));
if (value.isBlank() || headerMap.containsKey(value)) {
continue;
}
headerMap.put(value, i);
if (HEADER_STOP_COLUMN.equals(value)) {
break;
}
}
return headerMap;
}
private List<List<List<String>>> buildChunks(List<List<String>> rowsData, SplitRunRequest request) {
int totalRows = rowsData.size();
List<List<List<String>>> chunks = new ArrayList<>();
if (SPLIT_MODE_ROWS_PER_FILE.equals(request.getSplitMode())) {
int chunkSize = request.getRowsPerFile();
for (int i = 0; i < totalRows; i += chunkSize) {
chunks.add(rowsData.subList(i, Math.min(i + chunkSize, totalRows)));
}
return chunks;
}
int actualParts = Math.min(request.getParts(), totalRows);
int base = totalRows / actualParts;
int remainder = totalRows % actualParts;
int start = 0;
for (int i = 0; i < actualParts; i++) {
int size = base + (i < remainder ? 1 : 0);
int end = start + size;
chunks.add(rowsData.subList(start, end));
start = end;
}
return chunks;
}
private void validateRequest(SplitRunRequest request) {
if (!List.of("rows_per_file", "parts").contains(request.getSplitMode())) {
throw new BusinessException("拆分模式不合法");
if (!List.of(SPLIT_MODE_ROWS_PER_FILE, SPLIT_MODE_PARTS).contains(request.getSplitMode())) {
throw new BusinessException("Invalid split mode.");
}
if ("rows_per_file".equals(request.getSplitMode()) && (request.getRowsPerFile() == null || request.getRowsPerFile() <= 0)) {
throw new BusinessException("每份条数不合法");
if (SPLIT_MODE_ROWS_PER_FILE.equals(request.getSplitMode())
&& (request.getRowsPerFile() == null || request.getRowsPerFile() <= 0)) {
throw new BusinessException("Invalid rowsPerFile value.");
}
if ("parts".equals(request.getSplitMode()) && (request.getParts() == null || request.getParts() <= 0)) {
throw new BusinessException("拆分份数不合法");
if (SPLIT_MODE_PARTS.equals(request.getSplitMode())
&& (request.getParts() == null || request.getParts() <= 0)) {
throw new BusinessException("Invalid parts value.");
}
}
@@ -290,8 +341,11 @@ public class SplitRunService {
if (!baseDir.exists()) {
return null;
}
List<File> matchedFiles = FileUtil.loopFiles(baseDir, pathname -> pathname.isFile() && pathname.getName().startsWith(fileKey));
return matchedFiles.isEmpty() ? null : matchedFiles.getFirst();
List<File> matchedFiles = FileUtil.loopFiles(
baseDir,
pathname -> pathname.isFile() && pathname.getName().startsWith(fileKey)
);
return matchedFiles.isEmpty() ? null : matchedFiles.get(0);
}
private File buildNamedOutputFile(File outputDir, String filename) {
@@ -299,6 +353,7 @@ public class SplitRunService {
if (!candidate.exists()) {
return candidate;
}
String mainName = FileUtil.mainName(filename);
String extName = FileUtil.extName(filename);
int index = 2;
@@ -314,6 +369,61 @@ public class SplitRunService {
}
}
private File packageTaskSplitResultsAsZip(String archiveName, List<String> successSourceNames, List<TaskSplitChunkResult> chunkResults) {
File outputDir = FileUtil.mkdir(FileUtil.file(storageProperties.getLocalTempDir(), "split-result"));
String zipFilename = archiveName != null && !archiveName.isBlank()
? archiveName + ".zip"
: successSourceNames.size() == 1
? FileUtil.mainName(successSourceNames.getFirst()) + ".zip"
: LocalDateTime.now().format(ZIP_NAME_DATE_FORMATTER) + ".zip";
File zipFile = buildNamedOutputFile(outputDir, zipFilename);
try (ZipOutputStream zos = new ZipOutputStream(new FileOutputStream(zipFile))) {
for (TaskSplitChunkResult chunkResult : chunkResults) {
String zipEntryName = buildFolderZipEntry(chunkResult.relativePath(), chunkResult.sourceFilename(), chunkResult.outputFile().getName(), true);
ZipEntry entry = new ZipEntry(zipEntryName);
zos.putNextEntry(entry);
Files.copy(chunkResult.outputFile().toPath(), zos);
zos.closeEntry();
}
} catch (Exception ex) {
throw new BusinessException("拆分结果打包失败:" + ex.getMessage());
}
return zipFile;
}
private String buildFolderZipEntry(String relativePath, String inputName, String outputFilename, boolean includeSourceStemFolder) {
String normalizedRelativePath = relativePath == null ? "" : relativePath.replace('\\', '/');
String relativeDir = normalizedRelativePath.contains("/")
? normalizedRelativePath.substring(0, normalizedRelativePath.lastIndexOf('/'))
: "";
List<String> parts = new ArrayList<>();
if (!relativeDir.isBlank()) {
parts.add(relativeDir);
}
if (includeSourceStemFolder) {
parts.add(FileUtil.mainName(inputName));
}
parts.add(outputFilename);
return String.join("/", parts);
}
private SplitArchiveEntryVo toArchiveEntry(TaskSplitChunkResult chunkResult) {
SplitArchiveEntryVo vo = new SplitArchiveEntryVo();
vo.setFilename(chunkResult.outputFile().getName());
vo.setRowCount(chunkResult.rowCount());
return vo;
}
private String normalizeHeaderName(String value) {
String normalized = normalizeCellText(value);
if (normalized.isBlank()) {
return normalized;
}
String prefix = normalized.split(HEADER_SUFFIX_MARKER, 2)[0].trim();
return prefix.isEmpty() ? normalized : prefix;
}
private String normalizeCellText(String value) {
if (value == null) {
return "";
@@ -330,4 +440,7 @@ public class SplitRunService {
private record SplitChunkResult(File outputFile, int rowCount) {
}
private record TaskSplitChunkResult(String relativePath, String sourceFilename, File outputFile, int rowCount) {
}
}

View File

@@ -0,0 +1,8 @@
CREATE TABLE IF NOT EXISTS biz_dedupe_total_data (
id BIGINT PRIMARY KEY AUTO_INCREMENT COMMENT '主键',
data_value VARCHAR(128) NOT NULL COMMENT '去重总数据值',
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
UNIQUE KEY uk_data_value (data_value),
KEY idx_created_at (created_at)
) COMMENT='数据去重总数据表';

View File

@@ -0,0 +1,5 @@
UPDATE biz_convert_template
SET template_name = '五国模板',
output_filename = '英国.txt、法国.txt、德国.txt、西班牙.txt、意大利.txt',
updated_at = CURRENT_TIMESTAMP
WHERE template_code = 'uk_offer';