完成新需求

This commit is contained in:
super
2026-03-20 14:26:34 +08:00
parent 43d508f527
commit 92d3fc965b
71 changed files with 3265 additions and 540 deletions

View File

@@ -0,0 +1,84 @@
package com.nanri.aiimage.modules.convert.controller;
import com.nanri.aiimage.common.api.ApiResponse;
import com.nanri.aiimage.modules.convert.model.dto.ConvertRunRequest;
import com.nanri.aiimage.modules.convert.model.vo.ConvertHistoryVo;
import com.nanri.aiimage.modules.convert.model.vo.ConvertRunVo;
import com.nanri.aiimage.modules.convert.service.ConvertRunService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import org.springframework.core.io.FileSystemResource;
import org.springframework.core.io.Resource;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
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.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.io.File;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
@RestController
@RequiredArgsConstructor
@RequestMapping("/api/convert")
@Tag(name = "格式转换执行", description = "负责根据已上传文件和模板执行格式转换,并返回结果下载地址。当前先打通接口链路,下一步再把旧 Python 的模板生成逻辑完整迁移到 Java。")
public class ConvertRunController {
private final ConvertRunService convertRunService;
@PostMapping("/run")
@Operation(
summary = "执行格式转换",
description = """
使用上传接口返回的 fileKey 和选中的模板执行格式转换。
当前已按旧 Python 桌面端规则迁移以下逻辑:
- required_source_columns 缺失校验;
- preamble_lines 输出;
- header_columns 输出;
- blank_header_rows_after_schema 空白行补齐;
- field_mapping 字段映射;
- defaults 默认值填充;
- ASIN 为空的行跳过;
- sku 按 时间戳-序号 生成。
输出文件名遵循模板配置的 outputFilename若临时目录中重名则自动追加序号。
""")
public ApiResponse<ConvertRunVo> run(@Valid @RequestBody ConvertRunRequest request) {
return ApiResponse.success(convertRunService.run(request));
}
@GetMapping("/history")
@Operation(summary = "查询格式转换历史", description = "查询最近的格式转换成功记录,用于页面右侧历史下载列表展示。")
public ApiResponse<ConvertHistoryVo> history() {
ConvertHistoryVo vo = new ConvertHistoryVo();
vo.setItems(convertRunService.listHistory());
return ApiResponse.success(vo);
}
@DeleteMapping("/history/{resultId}")
@Operation(summary = "删除格式转换历史", description = "删除一条格式转换历史记录。")
public ApiResponse<Void> deleteHistory(@PathVariable Long resultId) {
convertRunService.deleteHistory(resultId);
return ApiResponse.success(null);
}
@GetMapping("/results/{resultId}/download")
@Operation(summary = "下载格式转换结果", description = "根据结果记录 ID 下载已生成的转换结果文件。桌面端前端可继续通过 save_file_from_url 选择保存位置。")
public ResponseEntity<Resource> download(@PathVariable Long resultId) {
File resultFile = convertRunService.getResultFile(resultId);
String encodedFilename = URLEncoder.encode(resultFile.getName(), StandardCharsets.UTF_8).replaceAll("\\+", "%20");
return ResponseEntity.ok()
.contentType(MediaType.APPLICATION_OCTET_STREAM)
.header(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename*=UTF-8''" + encodedFilename)
.body(new FileSystemResource(resultFile));
}
}

View File

@@ -0,0 +1,53 @@
package com.nanri.aiimage.modules.convert.controller;
import com.nanri.aiimage.common.api.ApiResponse;
import com.nanri.aiimage.modules.convert.model.dto.ConvertTemplateImportRequest;
import com.nanri.aiimage.modules.convert.model.vo.ConvertTemplateVo;
import com.nanri.aiimage.modules.convert.service.ConvertTemplateService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
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.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.List;
@RestController
@RequiredArgsConstructor
@RequestMapping("/api/convert/templates")
@Tag(name = "格式转换模板", description = "用于维护和查询格式转换模板,支持查询、导入、设默认、删除。")
public class ConvertTemplateController {
private final ConvertTemplateService convertTemplateService;
@GetMapping
@Operation(summary = "查询启用模板", description = "返回当前启用的格式转换模板列表,供前端‘格式转换’模块展示。")
public ApiResponse<List<ConvertTemplateVo>> listTemplates() {
return ApiResponse.success(convertTemplateService.listEnabledTemplates());
}
@PostMapping("/import")
@Operation(summary = "导入模板", description = "导入自定义 txt 模板,成功后返回新模板并可立即刷新模板列表。")
public ApiResponse<ConvertTemplateVo> importTemplate(@RequestBody ConvertTemplateImportRequest request) {
return ApiResponse.success(convertTemplateService.importTemplate(request));
}
@PostMapping("/{templateCode}/default")
@Operation(summary = "设为默认模板", description = "将指定模板设为默认模板。")
public ApiResponse<Void> setDefault(@PathVariable String templateCode) {
convertTemplateService.setDefaultTemplate(templateCode);
return ApiResponse.success(null);
}
@DeleteMapping("/{templateCode}")
@Operation(summary = "删除模板", description = "删除指定模板。内置模板不允许删除。")
public ApiResponse<Void> delete(@PathVariable String templateCode) {
convertTemplateService.deleteTemplate(templateCode);
return ApiResponse.success(null);
}
}

View File

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

View File

@@ -0,0 +1,23 @@
package com.nanri.aiimage.modules.convert.model.dto;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.Valid;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotEmpty;
import lombok.Data;
import java.util.List;
@Data
@Schema(description = "格式转换执行请求")
public class ConvertRunRequest {
@Valid
@NotEmpty(message = "请先上传待转换文件")
@Schema(description = "已上传的源文件列表")
private List<UploadedSourceFileDto> files;
@NotBlank(message = "请选择模板")
@Schema(description = "模板编码,例如 uk_offer")
private String templateId;
}

View File

@@ -0,0 +1,15 @@
package com.nanri.aiimage.modules.convert.model.dto;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
@Data
@Schema(description = "格式转换模板导入请求")
public class ConvertTemplateImportRequest {
@Schema(description = "模板显示名称")
private String templateName;
@Schema(description = "模板文本内容")
private String templateContent;
}

View File

@@ -0,0 +1,17 @@
package com.nanri.aiimage.modules.convert.model.dto;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotBlank;
import lombok.Data;
@Data
@Schema(description = "已上传源文件")
public class UploadedSourceFileDto {
@NotBlank(message = "fileKey 不能为空")
@Schema(description = "上传接口返回的临时文件键")
private String fileKey;
@Schema(description = "原始文件名")
private String originalFilename;
}

View File

@@ -0,0 +1,30 @@
package com.nanri.aiimage.modules.convert.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_convert_template")
public class ConvertTemplateEntity {
@TableId(type = IdType.AUTO)
private Long id;
private String templateCode;
private String templateName;
private String outputFilename;
private String requiredSourceColumnsJson;
private String headerColumnsJson;
private String fieldMappingJson;
private String defaultsJson;
private String preambleLinesJson;
private Integer blankHeaderRowsAfterSchema;
private Integer isDefault;
private Integer builtIn;
private Integer enabled;
private LocalDateTime createdAt;
private LocalDateTime updatedAt;
}

View File

@@ -0,0 +1,14 @@
package com.nanri.aiimage.modules.convert.model.vo;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.util.List;
@Data
@Schema(description = "格式转换历史列表")
public class ConvertHistoryVo {
@Schema(description = "历史记录")
private List<ConvertResultItemVo> items;
}

View File

@@ -0,0 +1,27 @@
package com.nanri.aiimage.modules.convert.model.vo;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
@Data
@Schema(description = "格式转换结果项")
public class ConvertResultItemVo {
@Schema(description = "结果记录ID")
private Long resultId;
@Schema(description = "源文件名")
private String sourceFilename;
@Schema(description = "结果文件名")
private String outputFilename;
@Schema(description = "是否成功")
private boolean success;
@Schema(description = "错误信息")
private String error;
@Schema(description = "下载地址")
private String downloadUrl;
}

View File

@@ -0,0 +1,23 @@
package com.nanri.aiimage.modules.convert.model.vo;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.util.List;
@Data
@Schema(description = "格式转换执行结果")
public class ConvertRunVo {
@Schema(description = "处理文件总数")
private Integer total;
@Schema(description = "成功文件数")
private Integer successCount;
@Schema(description = "失败文件数")
private Integer failedCount;
@Schema(description = "结果列表")
private List<ConvertResultItemVo> items;
}

View File

@@ -0,0 +1,27 @@
package com.nanri.aiimage.modules.convert.model.vo;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
@Data
@Schema(description = "格式转换模板")
public class ConvertTemplateVo {
@Schema(description = "模板编码")
private String id;
@Schema(description = "模板编码")
private String templateCode;
@Schema(description = "模板名称")
private String templateName;
@Schema(description = "输出文件名")
private String outputFilename;
@Schema(description = "是否默认模板")
private Boolean isDefault;
@Schema(description = "是否内置模板")
private Boolean builtIn;
}

View File

@@ -0,0 +1,317 @@
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.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.nanri.aiimage.common.exception.BusinessException;
import com.nanri.aiimage.config.StorageProperties;
import com.nanri.aiimage.modules.convert.model.dto.ConvertRunRequest;
import com.nanri.aiimage.modules.convert.model.dto.UploadedSourceFileDto;
import com.nanri.aiimage.modules.convert.model.entity.ConvertTemplateEntity;
import com.nanri.aiimage.modules.convert.model.vo.ConvertResultItemVo;
import com.nanri.aiimage.modules.convert.model.vo.ConvertRunVo;
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;
import com.nanri.aiimage.modules.task.model.entity.FileTaskEntity;
import lombok.RequiredArgsConstructor;
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 java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
@Service
@RequiredArgsConstructor
public class ConvertRunService {
private final FileTaskMapper fileTaskMapper;
private final FileResultMapper fileResultMapper;
private final StorageProperties storageProperties;
private final ConvertTemplateService convertTemplateService;
private final ObjectMapper objectMapper;
private final OssStorageService ossStorageService;
public ConvertRunVo run(ConvertRunRequest request) {
ConvertTemplateEntity template = convertTemplateService.getByCode(request.getTemplateId());
FileTaskEntity task = new FileTaskEntity();
task.setTaskNo("CONVERT-" + IdUtil.getSnowflakeNextIdStr());
task.setModuleType("CONVERT");
task.setTaskMode("IMMEDIATE");
task.setStatus("SUCCESS");
task.setSourceFileCount(request.getFiles().size());
task.setCreatedBy("system");
task.setRequestJson(JSONUtil.toJsonStr(request));
task.setCreatedAt(LocalDateTime.now());
task.setUpdatedAt(LocalDateTime.now());
task.setFinishedAt(LocalDateTime.now());
fileTaskMapper.insert(task);
List<ConvertResultItemVo> items = new ArrayList<>();
int successCount = 0;
int failedCount = 0;
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("上传文件不存在,请重新上传");
}
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);
writeTxtByLegacyRules(inputFile, outputFile, template);
String ossObjectKey = ossStorageService.uploadResultFile(outputFile, "CONVERT");
String downloadUrl = ossStorageService.generateDownloadUrl(ossObjectKey);
item.setSuccess(true);
item.setOutputFilename(outputFilename);
item.setDownloadUrl(downloadUrl);
successCount++;
FileResultEntity resultEntity = new FileResultEntity();
resultEntity.setTaskId(task.getId());
resultEntity.setModuleType("CONVERT");
resultEntity.setSourceFilename(inputName);
resultEntity.setResultFilename(outputFilename);
resultEntity.setResultFileUrl(downloadUrl);
resultEntity.setResultFileSize(outputFile.length());
resultEntity.setResultContentType("text/plain");
resultEntity.setSuccess(1);
resultEntity.setCreatedAt(LocalDateTime.now());
fileResultMapper.insert(resultEntity);
item.setResultId(resultEntity.getId());
} catch (Exception ex) {
item.setSuccess(false);
item.setError(ex.getMessage());
failedCount++;
FileResultEntity resultEntity = new FileResultEntity();
resultEntity.setTaskId(task.getId());
resultEntity.setModuleType("CONVERT");
resultEntity.setSourceFilename(sourceFile.getOriginalFilename());
resultEntity.setSuccess(0);
resultEntity.setErrorMessage(ex.getMessage());
resultEntity.setCreatedAt(LocalDateTime.now());
fileResultMapper.insert(resultEntity);
}
items.add(item);
}
task.setSuccessFileCount(successCount);
task.setFailedFileCount(failedCount);
task.setResultJson(JSONUtil.toJsonStr(items));
task.setUpdatedAt(LocalDateTime.now());
fileTaskMapper.updateById(task);
ConvertRunVo vo = new ConvertRunVo();
vo.setTotal(request.getFiles().size());
vo.setSuccessCount(successCount);
vo.setFailedCount(failedCount);
vo.setItems(items);
return vo;
}
public List<ConvertResultItemVo> listHistory() {
return fileResultMapper.selectList(new LambdaQueryWrapper<FileResultEntity>()
.eq(FileResultEntity::getModuleType, "CONVERT")
.eq(FileResultEntity::getSuccess, 1)
.orderByDesc(FileResultEntity::getCreatedAt)
.last("limit 50"))
.stream()
.map(entity -> {
ConvertResultItemVo vo = new ConvertResultItemVo();
vo.setResultId(entity.getId());
vo.setSourceFilename(entity.getCreatedAt() == null ? "" : entity.getCreatedAt().toLocalDate().toString().replace("-", ""));
vo.setOutputFilename(entity.getResultFilename());
vo.setDownloadUrl(entity.getResultFileUrl());
vo.setSuccess(true);
return vo;
})
.toList();
}
public void deleteHistory(Long resultId) {
FileResultEntity entity = fileResultMapper.selectById(resultId);
if (entity == null || !"CONVERT".equals(entity.getModuleType())) {
throw new BusinessException("记录不存在");
}
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("结果文件不存在");
}
File file = new File(entity.getResultFileUrl());
if (!file.exists()) {
throw new BusinessException("结果文件不存在");
}
return file;
}
private void writeTxtByLegacyRules(File inputFile, File outputFile, 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();
DataFormatter formatter = new DataFormatter();
try (FileInputStream fis = new FileInputStream(inputFile); Workbook workbook = org.apache.poi.ss.usermodel.WorkbookFactory.create(fis)) {
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++) {
String value = normalizeCellText(formatter.formatCellValue(headerRow.getCell(i)));
if (!value.isBlank() && !headerMap.containsKey(value)) {
headerMap.put(value, i);
}
}
List<String> missing = requiredColumns.stream().filter(column -> !headerMap.containsKey(column)).toList();
if (!missing.isEmpty()) {
throw new BusinessException("缺少列:" + String.join("", missing));
}
List<String> rows = new ArrayList<>();
rows.addAll(preambleLines);
rows.add(String.join("\t", headerColumns));
for (int i = 0; i < blankHeaderRowsAfterSchema; i++) {
rows.add("\t".repeat(Math.max(0, headerColumns.size() - 1)));
}
long batchId = System.currentTimeMillis();
int rowIndex = 1;
int asinIndex = headerMap.getOrDefault("ASIN", -1);
for (int rowNum = 1; rowNum <= sheet.getLastRowNum(); rowNum++) {
Row row = sheet.getRow(rowNum);
if (row == null) {
continue;
}
String asin = asinIndex >= 0 ? normalizeCellText(formatter.formatCellValue(row.getCell(asinIndex))) : "";
if (asin.isBlank()) {
continue;
}
List<String> lineValues = new ArrayList<>();
for (String column : headerColumns) {
if ("sku".equals(column)) {
lineValues.add(batchId + "-" + rowIndex);
continue;
}
if (fieldMapping.containsKey(column)) {
String sourceColumn = fieldMapping.get(column);
Integer sourceIndex = headerMap.get(sourceColumn);
String value = sourceIndex == null ? "" : normalizeCellText(formatter.formatCellValue(row.getCell(sourceIndex)));
lineValues.add(value);
continue;
}
if (defaults.containsKey(column)) {
lineValues.add(defaults.get(column));
continue;
}
lineValues.add("");
}
rows.add(String.join("\t", lineValues));
rowIndex++;
}
FileUtil.writeLines(rows, outputFile, StandardCharsets.UTF_8);
}
}
private List<String> readStringList(String json) {
try {
if (json == null || json.isBlank()) {
return new ArrayList<>();
}
return objectMapper.readValue(json, new TypeReference<List<String>>() {});
} catch (Exception ex) {
throw new BusinessException("模板配置解析失败");
}
}
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>>() {});
} catch (Exception ex) {
throw new BusinessException("模板配置解析失败");
}
}
private File findLocalSourceFile(String fileKey) {
File baseDir = FileUtil.file(storageProperties.getLocalTempDir());
if (!baseDir.exists()) {
return null;
}
List<File> matchedFiles = FileUtil.loopFiles(baseDir, pathname -> pathname.isFile() && pathname.getName().startsWith(fileKey));
return matchedFiles.isEmpty() ? null : matchedFiles.getFirst();
}
private File buildNamedOutputFile(File outputDir, String filename) {
File candidate = FileUtil.file(outputDir, filename);
if (!candidate.exists()) {
return candidate;
}
String mainName = FileUtil.mainName(filename);
String extName = FileUtil.extName(filename);
int index = 2;
while (true) {
String nextFilename = extName == null || extName.isBlank()
? mainName + "_" + index
: mainName + "_" + index + "." + extName;
File nextCandidate = FileUtil.file(outputDir, nextFilename);
if (!nextCandidate.exists()) {
return nextCandidate;
}
index++;
}
}
private String normalizeCellText(String value) {
if (value == null) {
return "";
}
return value.replace("\ufeff", "")
.replace("\u3000", " ")
.replace("\r\n", " ")
.replace("\r", " ")
.replace("\n", " ")
.replace("\t", " ")
.trim()
.replaceAll("\\s+", " ");
}
}

View File

@@ -0,0 +1,99 @@
package com.nanri.aiimage.modules.convert.service;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.nanri.aiimage.common.exception.BusinessException;
import com.nanri.aiimage.modules.convert.mapper.ConvertTemplateMapper;
import com.nanri.aiimage.modules.convert.model.dto.ConvertTemplateImportRequest;
import com.nanri.aiimage.modules.convert.model.entity.ConvertTemplateEntity;
import com.nanri.aiimage.modules.convert.model.vo.ConvertTemplateVo;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.time.LocalDateTime;
import java.util.List;
@Service
@RequiredArgsConstructor
public class ConvertTemplateService {
private final ConvertTemplateMapper convertTemplateMapper;
public List<ConvertTemplateVo> listEnabledTemplates() {
return convertTemplateMapper.selectList(new LambdaQueryWrapper<ConvertTemplateEntity>()
.eq(ConvertTemplateEntity::getEnabled, 1)
.orderByDesc(ConvertTemplateEntity::getIsDefault)
.orderByAsc(ConvertTemplateEntity::getId))
.stream()
.map(this::toVo)
.toList();
}
public ConvertTemplateEntity getByCode(String templateCode) {
ConvertTemplateEntity entity = convertTemplateMapper.selectOne(new LambdaQueryWrapper<ConvertTemplateEntity>()
.eq(ConvertTemplateEntity::getTemplateCode, templateCode)
.eq(ConvertTemplateEntity::getEnabled, 1)
.last("limit 1"));
if (entity == null) {
throw new BusinessException("模板不存在或已禁用");
}
return entity;
}
@Transactional
public ConvertTemplateVo importTemplate(ConvertTemplateImportRequest request) {
if (request.getTemplateContent() == null || request.getTemplateContent().isBlank()) {
throw new BusinessException("模板内容不能为空");
}
ConvertTemplateEntity entity = new ConvertTemplateEntity();
entity.setTemplateCode("user_" + System.currentTimeMillis());
entity.setTemplateName((request.getTemplateName() == null || request.getTemplateName().isBlank()) ? "自定义 Offer 模板" : request.getTemplateName().trim());
entity.setOutputFilename(entity.getTemplateName().endsWith(".txt") ? entity.getTemplateName() : entity.getTemplateName() + ".txt");
entity.setRequiredSourceColumnsJson("[\"ASIN\",\"价格\"]");
entity.setHeaderColumnsJson("[\"sku\",\"price\",\"quantity\",\"product-id\",\"product-id-type\",\"condition-type\",\"condition-note\",\"ASIN-hint\",\"title\",\"product-tax-code\",\"operation-type\",\"sale-price\",\"sale-start-date\",\"sale-end-date\",\"leadtime-to-ship\",\"launch-date\",\"is-giftwrap-available\",\"is-gift-message-available\"]");
entity.setFieldMappingJson("{\"price\":\"价格\",\"product-id\":\"ASIN\"}");
entity.setDefaultsJson("{\"quantity\":\"100\",\"product-id-type\":\"ASIN\",\"condition-type\":\"new\",\"leadtime-to-ship\":\"4\"}");
entity.setPreambleLinesJson("[\"TemplateType=Offer\\tVersion=1.4\"]");
entity.setBlankHeaderRowsAfterSchema(1);
entity.setIsDefault(0);
entity.setBuiltIn(0);
entity.setEnabled(1);
entity.setCreatedAt(LocalDateTime.now());
entity.setUpdatedAt(LocalDateTime.now());
convertTemplateMapper.insert(entity);
return toVo(entity);
}
@Transactional
public void setDefaultTemplate(String templateCode) {
ConvertTemplateEntity entity = getByCode(templateCode);
convertTemplateMapper.selectList(new LambdaQueryWrapper<ConvertTemplateEntity>().eq(ConvertTemplateEntity::getEnabled, 1))
.forEach(item -> {
item.setIsDefault(item.getId().equals(entity.getId()) ? 1 : 0);
item.setUpdatedAt(LocalDateTime.now());
convertTemplateMapper.updateById(item);
});
}
@Transactional
public void deleteTemplate(String templateCode) {
ConvertTemplateEntity entity = getByCode(templateCode);
if (Integer.valueOf(1).equals(entity.getBuiltIn())) {
throw new BusinessException("内置模板不允许删除");
}
entity.setEnabled(0);
entity.setUpdatedAt(LocalDateTime.now());
convertTemplateMapper.updateById(entity);
}
private ConvertTemplateVo toVo(ConvertTemplateEntity entity) {
ConvertTemplateVo vo = new ConvertTemplateVo();
vo.setId(entity.getTemplateCode());
vo.setTemplateCode(entity.getTemplateCode());
vo.setTemplateName(entity.getTemplateName());
vo.setOutputFilename(entity.getOutputFilename());
vo.setIsDefault(Integer.valueOf(1).equals(entity.getIsDefault()));
vo.setBuiltIn(Integer.valueOf(1).equals(entity.getBuiltIn()));
return vo;
}
}