|
@@ -0,0 +1,591 @@
|
|
|
|
|
+package com.dtm.upload.service;
|
|
|
|
|
+
|
|
|
|
|
+import com.dtm.common.utils.StringUtils;
|
|
|
|
|
+import com.dtm.storage.util.ExcelSheet;
|
|
|
|
|
+import com.dtm.storage.util.ExcelUtils;
|
|
|
|
|
+import com.dtm.upload.domain.DtmAssemblyRecord;
|
|
|
|
|
+import com.dtm.upload.domain.DtmBomList;
|
|
|
|
|
+import com.dtm.upload.domain.DtmOrderMain;
|
|
|
|
|
+import com.dtm.upload.domain.DtmProduct;
|
|
|
|
|
+import com.dtm.upload.domain.DtmPurchaseReceipt;
|
|
|
|
|
+import com.dtm.upload.domain.DtmSemiFinishedProduct;
|
|
|
|
|
+import com.dtm.upload.domain.DtmStore;
|
|
|
|
|
+import com.dtm.upload.domain.DtmSupplier;
|
|
|
|
|
+import org.apache.commons.csv.CSVFormat;
|
|
|
|
|
+import org.apache.commons.csv.CSVParser;
|
|
|
|
|
+import org.apache.commons.csv.CSVRecord;
|
|
|
|
|
+import org.springframework.stereotype.Service;
|
|
|
|
|
+import org.springframework.web.multipart.MultipartFile;
|
|
|
|
|
+
|
|
|
|
|
+import java.io.BufferedReader;
|
|
|
|
|
+import java.io.InputStreamReader;
|
|
|
|
|
+import java.math.BigDecimal;
|
|
|
|
|
+import java.nio.charset.Charset;
|
|
|
|
|
+import java.nio.charset.StandardCharsets;
|
|
|
|
|
+import java.security.MessageDigest;
|
|
|
|
|
+import java.sql.Date;
|
|
|
|
|
+import java.sql.Timestamp;
|
|
|
|
|
+import java.time.LocalDate;
|
|
|
|
|
+import java.time.LocalDateTime;
|
|
|
|
|
+import java.time.format.DateTimeFormatter;
|
|
|
|
|
+import java.util.ArrayList;
|
|
|
|
|
+import java.util.Arrays;
|
|
|
|
|
+import java.util.HashMap;
|
|
|
|
|
+import java.util.LinkedHashMap;
|
|
|
|
|
+import java.util.List;
|
|
|
|
|
+import java.util.Locale;
|
|
|
|
|
+import java.util.Map;
|
|
|
|
|
+
|
|
|
|
|
+@Service
|
|
|
|
|
+public class PreparedDataUploadService {
|
|
|
|
|
+ private static final Charset CSV_CHARSET = Charset.forName("GB18030");
|
|
|
|
|
+ private static final List<String> SUPPORTED_EXTENSIONS = Arrays.asList(".csv", ".xls", ".xlsx");
|
|
|
|
|
+
|
|
|
|
|
+ private final IDtmSupplierService supplierService;
|
|
|
|
|
+ private final IDtmSemiFinishedProductService semiFinishedProductService;
|
|
|
|
|
+ private final IDtmProductService productService;
|
|
|
|
|
+ private final IDtmBomListService bomListService;
|
|
|
|
|
+ private final IDtmPurchaseReceiptService purchaseReceiptService;
|
|
|
|
|
+ private final IDtmOrderMainService orderMainService;
|
|
|
|
|
+ private final IDtmAssemblyRecordService assemblyRecordService;
|
|
|
|
|
+ private final IDtmStoreService storeService;
|
|
|
|
|
+ private final Map<String, UploadConfig> configs;
|
|
|
|
|
+
|
|
|
|
|
+ public PreparedDataUploadService(IDtmSupplierService supplierService,
|
|
|
|
|
+ IDtmSemiFinishedProductService semiFinishedProductService,
|
|
|
|
|
+ IDtmProductService productService,
|
|
|
|
|
+ IDtmBomListService bomListService,
|
|
|
|
|
+ IDtmPurchaseReceiptService purchaseReceiptService,
|
|
|
|
|
+ IDtmOrderMainService orderMainService,
|
|
|
|
|
+ IDtmAssemblyRecordService assemblyRecordService,
|
|
|
|
|
+ IDtmStoreService storeService) {
|
|
|
|
|
+ this.supplierService = supplierService;
|
|
|
|
|
+ this.semiFinishedProductService = semiFinishedProductService;
|
|
|
|
|
+ this.productService = productService;
|
|
|
|
|
+ this.bomListService = bomListService;
|
|
|
|
|
+ this.purchaseReceiptService = purchaseReceiptService;
|
|
|
|
|
+ this.orderMainService = orderMainService;
|
|
|
|
|
+ this.assemblyRecordService = assemblyRecordService;
|
|
|
|
|
+ this.storeService = storeService;
|
|
|
|
|
+ this.configs = buildConfigs();
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ public List<Map<String, Object>> listSupportedItems() {
|
|
|
|
|
+ List<Map<String, Object>> items = new ArrayList<>();
|
|
|
|
|
+ for (UploadConfig config : configs.values()) {
|
|
|
|
|
+ Map<String, Object> item = new LinkedHashMap<>();
|
|
|
|
|
+ item.put("key", config.key);
|
|
|
|
|
+ item.put("label", config.label);
|
|
|
|
|
+ item.put("replaceBeforeInsert", config.replaceBeforeInsert);
|
|
|
|
|
+ items.add(item);
|
|
|
|
|
+ }
|
|
|
|
|
+ return items;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ public Map<String, Object> upload(String key, MultipartFile file) {
|
|
|
|
|
+ UploadConfig config = configs.get(key);
|
|
|
|
|
+ validateFile(config, file);
|
|
|
|
|
+
|
|
|
|
|
+ try {
|
|
|
|
|
+ FileRows fileRows = readRows(file, resolveExtension(file.getOriginalFilename()));
|
|
|
|
|
+ List<FieldRequirement> missingFields = getMissingFields(config, fileRows.headers);
|
|
|
|
|
+ if (!missingFields.isEmpty()) {
|
|
|
|
|
+ throw new IllegalArgumentException("文件缺少必要内容:" + joinFieldLabels(missingFields));
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ int affected = writeRows(config, fileRows.rows);
|
|
|
|
|
+
|
|
|
|
|
+ Map<String, Object> result = new LinkedHashMap<>();
|
|
|
|
|
+ result.put("key", key);
|
|
|
|
|
+ result.put("label", config.label);
|
|
|
|
|
+ result.put("fileName", file.getOriginalFilename());
|
|
|
|
|
+ result.put("totalRows", fileRows.rows.size());
|
|
|
|
|
+ result.put("affectedRows", affected);
|
|
|
|
|
+ result.put("message", config.label + "处理完成,共读取 " + fileRows.rows.size() + " 行");
|
|
|
|
|
+ return result;
|
|
|
|
|
+ } catch (IllegalArgumentException e) {
|
|
|
|
|
+ throw e;
|
|
|
|
|
+ } catch (Exception e) {
|
|
|
|
|
+ throw new IllegalStateException("上传处理失败: " + e.getMessage(), e);
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ public Map<String, Object> preview(String key, MultipartFile file) {
|
|
|
|
|
+ UploadConfig config = configs.get(key);
|
|
|
|
|
+ validateFile(config, file);
|
|
|
|
|
+
|
|
|
|
|
+ try {
|
|
|
|
|
+ FileRows fileRows = readRows(file, resolveExtension(file.getOriginalFilename()));
|
|
|
|
|
+ List<FieldRequirement> missingFields = getMissingFields(config, fileRows.headers);
|
|
|
|
|
+ Map<String, Object> result = new LinkedHashMap<>();
|
|
|
|
|
+ result.put("key", key);
|
|
|
|
|
+ result.put("label", config.label);
|
|
|
|
|
+ result.put("fileName", file.getOriginalFilename());
|
|
|
|
|
+ result.put("totalRows", fileRows.rows.size());
|
|
|
|
|
+ result.put("headers", fileRows.headers);
|
|
|
|
|
+ result.put("columns", buildPreviewColumns(fileRows.headers));
|
|
|
|
|
+ result.put("sampleRows", fileRows.rows.subList(0, Math.min(fileRows.rows.size(), 20)));
|
|
|
|
|
+ result.put("missingFields", missingFields);
|
|
|
|
|
+ result.put("valid", missingFields.isEmpty());
|
|
|
|
|
+ return result;
|
|
|
|
|
+ } catch (IllegalArgumentException e) {
|
|
|
|
|
+ throw e;
|
|
|
|
|
+ } catch (Exception e) {
|
|
|
|
|
+ throw new IllegalStateException("文件预览失败: " + e.getMessage(), e);
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private void validateFile(UploadConfig config, MultipartFile file) {
|
|
|
|
|
+ if (config == null) {
|
|
|
|
|
+ throw new IllegalArgumentException("这项资料暂未整理完成,暂不支持上传");
|
|
|
|
|
+ }
|
|
|
|
|
+ if (file == null || file.isEmpty()) {
|
|
|
|
|
+ throw new IllegalArgumentException("请选择要上传的文件");
|
|
|
|
|
+ }
|
|
|
|
|
+ String extension = resolveExtension(file.getOriginalFilename());
|
|
|
|
|
+ if (!SUPPORTED_EXTENSIONS.contains(extension)) {
|
|
|
|
|
+ throw new IllegalArgumentException("文件格式不支持,请上传 CSV、XLS 或 XLSX 文件");
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private int writeRows(UploadConfig config, List<Map<String, String>> rows) {
|
|
|
|
|
+ if (rows.isEmpty()) {
|
|
|
|
|
+ throw new IllegalArgumentException("文件中没有可导入的数据");
|
|
|
|
|
+ }
|
|
|
|
|
+ return config.writer.write(rows);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private FileRows readRows(MultipartFile file, String extension) throws Exception {
|
|
|
|
|
+ if (".csv".equals(extension)) {
|
|
|
|
|
+ return readCsv(file);
|
|
|
|
|
+ }
|
|
|
|
|
+ return readExcel(file);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private FileRows readCsv(MultipartFile file) throws Exception {
|
|
|
|
|
+ try (BufferedReader reader = new BufferedReader(new InputStreamReader(file.getInputStream(), CSV_CHARSET));
|
|
|
|
|
+ CSVParser parser = CSVFormat.DEFAULT.builder()
|
|
|
|
|
+ .setHeader()
|
|
|
|
|
+ .setSkipHeaderRecord(true)
|
|
|
|
|
+ .setIgnoreEmptyLines(true)
|
|
|
|
|
+ .setTrim(true)
|
|
|
|
|
+ .build()
|
|
|
|
|
+ .parse(reader)) {
|
|
|
|
|
+ List<String> headers = normalizeHeaders(new ArrayList<>(parser.getHeaderMap().keySet()));
|
|
|
|
|
+ List<Map<String, String>> rows = new ArrayList<>();
|
|
|
|
|
+ for (CSVRecord record : parser) {
|
|
|
|
|
+ Map<String, String> row = new HashMap<>();
|
|
|
|
|
+ boolean hasValue = false;
|
|
|
|
|
+ for (int i = 0; i < headers.size() && i < record.size(); i++) {
|
|
|
|
|
+ String value = clean(record.get(i));
|
|
|
|
|
+ if (StringUtils.isNotEmpty(value)) {
|
|
|
|
|
+ hasValue = true;
|
|
|
|
|
+ }
|
|
|
|
|
+ row.put(headers.get(i), value);
|
|
|
|
|
+ }
|
|
|
|
|
+ if (hasValue) {
|
|
|
|
|
+ rows.add(row);
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ return new FileRows(headers, rows);
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private FileRows readExcel(MultipartFile file) throws Exception {
|
|
|
|
|
+ java.nio.file.Path temp = java.nio.file.Files.createTempFile("prepared-data-upload-", resolveExtension(file.getOriginalFilename()));
|
|
|
|
|
+ try {
|
|
|
|
|
+ file.transferTo(temp.toFile());
|
|
|
|
|
+ ExcelSheet sheet = ExcelUtils.readSheet(temp, 0);
|
|
|
|
|
+ List<String> headers = normalizeHeaders(sheet.getHeaders());
|
|
|
|
|
+ List<Map<String, String>> rows = new ArrayList<>();
|
|
|
|
|
+ for (List<Object> values : sheet.getRows()) {
|
|
|
|
|
+ Map<String, String> row = new HashMap<>();
|
|
|
|
|
+ boolean hasValue = false;
|
|
|
|
|
+ for (int i = 0; i < headers.size() && i < values.size(); i++) {
|
|
|
|
|
+ String value = clean(values.get(i));
|
|
|
|
|
+ if (StringUtils.isNotEmpty(value)) {
|
|
|
|
|
+ hasValue = true;
|
|
|
|
|
+ }
|
|
|
|
|
+ row.put(headers.get(i), value);
|
|
|
|
|
+ }
|
|
|
|
|
+ if (hasValue) {
|
|
|
|
|
+ rows.add(row);
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ return new FileRows(headers, rows);
|
|
|
|
|
+ } finally {
|
|
|
|
|
+ java.nio.file.Files.deleteIfExists(temp);
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private Map<String, UploadConfig> buildConfigs() {
|
|
|
|
|
+ Map<String, UploadConfig> map = new LinkedHashMap<>();
|
|
|
|
|
+ map.put("supplier", new UploadConfig("supplier", "供应商资料", false, fields(
|
|
|
|
|
+ field("supplier_id", "供应商编号"), field("supplier_name", "供应商名称")),
|
|
|
|
|
+ rows -> supplierService.batchSaveDtmSupplier(mapRows(rows, this::mapSupplierRow))));
|
|
|
|
|
+ map.put("semi_finished_product", new UploadConfig("semi_finished_product", "半成品资料", false, fields(
|
|
|
|
|
+ field("sku", "半成品编码"), field("semi_name", "半成品名称"), field("price", "单价")),
|
|
|
|
|
+ rows -> semiFinishedProductService.batchSaveDtmSemiFinishedProduct(mapRows(rows, this::mapSemiFinishedProductRow))));
|
|
|
|
|
+ map.put("product", new UploadConfig("product", "产品资料", false, fields(
|
|
|
|
|
+ field("sku", "产品编码"), field("product_name", "产品名称"), field("product_price", "产品价格")),
|
|
|
|
|
+ rows -> productService.batchSaveDtmProduct(mapRows(rows, this::mapProductRow))));
|
|
|
|
|
+ map.put("bom_list", new UploadConfig("bom_list", "组合清单资料", true, fields(
|
|
|
|
|
+ field("finished_sku", "成品编码"), field("semi_sku", "半成品编码"), field("quantity", "数量")),
|
|
|
|
|
+ rows -> bomListService.replaceDtmBomList(mapRows(rows, this::mapBomListRow))));
|
|
|
|
|
+ map.put("purchase_receipt", new UploadConfig("purchase_receipt", "采购入库资料", false, fields(
|
|
|
|
|
+ field("receipt_id", "入库单号"), field("product_code", "产品编码"), field("supplier_id", "供应商编号"),
|
|
|
|
|
+ field("quantity", "数量"), field("receipt_date", "入库日期")),
|
|
|
|
|
+ rows -> purchaseReceiptService.batchSaveDtmPurchaseReceipt(mapRows(rows, this::mapPurchaseReceiptRow))));
|
|
|
|
|
+ map.put("order_main", new UploadConfig("order_main", "订单资料", false, fields(
|
|
|
|
|
+ field("order_id", "订单编号"), field("price", "单价"), field("quantity", "数量")),
|
|
|
|
|
+ rows -> orderMainService.batchSaveDtmOrderMain(mapRows(rows, this::mapOrderMainRow))));
|
|
|
|
|
+ map.put("assembly_record", new UploadConfig("assembly_record", "组装记录资料", true, fields(
|
|
|
|
|
+ field("assembly_date", "组装日期"), field("product_code", "产品编码"), field("quantity", "数量")),
|
|
|
|
|
+ rows -> assemblyRecordService.replaceDtmAssemblyRecord(mapRows(rows, this::mapAssemblyRecordRow))));
|
|
|
|
|
+ map.put("store", new UploadConfig("store", "店铺资料", true, fields(
|
|
|
|
|
+ field("business_unit_name", "事业部"), field("channel_name", "渠道"), field("platform_name", "店铺名称"),
|
|
|
|
|
+ field("stat_year", "年份"), field("stat_month", "月份"), field("stat_day", "日期"),
|
|
|
|
|
+ field("quantity", "数量"), field("sales_amount", "销售金额")),
|
|
|
|
|
+ rows -> storeService.replaceDtmStore(mapRows(rows, this::mapStoreRow))));
|
|
|
|
|
+ return map;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private List<FieldRequirement> getMissingFields(UploadConfig config, List<String> headers) {
|
|
|
|
|
+ List<FieldRequirement> missingFields = new ArrayList<>();
|
|
|
|
|
+ for (FieldRequirement field : config.requiredFields) {
|
|
|
|
|
+ if (!headers.contains(field.name)) {
|
|
|
|
|
+ missingFields.add(field);
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ return missingFields;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private String joinFieldLabels(List<FieldRequirement> fields) {
|
|
|
|
|
+ List<String> labels = new ArrayList<>();
|
|
|
|
|
+ for (FieldRequirement field : fields) {
|
|
|
|
|
+ labels.add(field.label + "(" + field.name + ")");
|
|
|
|
|
+ }
|
|
|
|
|
+ return String.join("、", labels);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private List<Map<String, Object>> buildPreviewColumns(List<String> headers) {
|
|
|
|
|
+ List<Map<String, Object>> columns = new ArrayList<>();
|
|
|
|
|
+ for (String header : headers) {
|
|
|
|
|
+ Map<String, Object> column = new LinkedHashMap<>();
|
|
|
|
|
+ column.put("prop", header);
|
|
|
|
|
+ column.put("label", header);
|
|
|
|
|
+ column.put("minWidth", 140);
|
|
|
|
|
+ columns.add(column);
|
|
|
|
|
+ }
|
|
|
|
|
+ return columns;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private static List<FieldRequirement> fields(FieldRequirement... fields) {
|
|
|
|
|
+ return Arrays.asList(fields);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private static FieldRequirement field(String name, String label) {
|
|
|
|
|
+ return new FieldRequirement(name, label);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private DtmSupplier mapSupplierRow(Map<String, String> row) {
|
|
|
|
|
+ DtmSupplier supplier = new DtmSupplier();
|
|
|
|
|
+ supplier.setSupplierId(text(row, "supplier_id", 32));
|
|
|
|
|
+ supplier.setSupplierName(text(row, "supplier_name", 100));
|
|
|
|
|
+ return supplier;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private DtmSemiFinishedProduct mapSemiFinishedProductRow(Map<String, String> row) {
|
|
|
|
|
+ DtmSemiFinishedProduct product = new DtmSemiFinishedProduct();
|
|
|
|
|
+ product.setSku(text(row, "sku", 64));
|
|
|
|
|
+ product.setSemiName(text(row, "semi_name", 100));
|
|
|
|
|
+ product.setPrice(decimal(row, "price"));
|
|
|
|
|
+ product.setBomLevelId(nullableText(row, "bom_level_id", 32));
|
|
|
|
|
+ return product;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private DtmProduct mapProductRow(Map<String, String> row) {
|
|
|
|
|
+ DtmProduct product = new DtmProduct();
|
|
|
|
|
+ product.setSku(text(row, "sku", 64));
|
|
|
|
|
+ product.setProductName(text(row, "product_name", 128));
|
|
|
|
|
+ product.setSpu(nullableText(row, "spu", 64));
|
|
|
|
|
+ product.setProductPrice(decimal(row, "product_price"));
|
|
|
|
|
+ product.setAttributes(nullableText(row, "attributes", 32));
|
|
|
|
|
+ product.setBomLevelId(nullableText(row, "bom_level_id", 32));
|
|
|
|
|
+ return product;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private DtmBomList mapBomListRow(Map<String, String> row) {
|
|
|
|
|
+ DtmBomList bom = new DtmBomList();
|
|
|
|
|
+ bom.setFinishedSku(text(row, "finished_sku", 64));
|
|
|
|
|
+ bom.setSemiSku(text(row, "semi_sku", 64));
|
|
|
|
|
+ bom.setQuantity(integer(row, "quantity"));
|
|
|
|
|
+ return bom;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private DtmPurchaseReceipt mapPurchaseReceiptRow(Map<String, String> row) {
|
|
|
|
|
+ DtmPurchaseReceipt receipt = new DtmPurchaseReceipt();
|
|
|
|
|
+ receipt.setReceiptId(text(row, "receipt_id", 32));
|
|
|
|
|
+ receipt.setSku(text(row, "product_code", 64));
|
|
|
|
|
+ receipt.setSupplierId(text(row, "supplier_id", 32));
|
|
|
|
|
+ receipt.setWarehouseCode(nullableText(row, "warehouse_code", 32));
|
|
|
|
|
+ receipt.setUnitprice(decimal(row, "unitprice"));
|
|
|
|
|
+ receipt.setActualAmount(decimal(row, "actual_amount"));
|
|
|
|
|
+ receipt.setQuantity(integer(row, "quantity"));
|
|
|
|
|
+ receipt.setReceiptDate(date(row, "receipt_date"));
|
|
|
|
|
+ receipt.setStatus(nullableText(row, "status", 16));
|
|
|
|
|
+ return receipt;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private DtmOrderMain mapOrderMainRow(Map<String, String> row) {
|
|
|
|
|
+ DtmOrderMain order = new DtmOrderMain();
|
|
|
|
|
+ order.setOrderId(text(row, "order_id", 64));
|
|
|
|
|
+ order.setTitle(nullableText(row, "title", 255));
|
|
|
|
|
+ order.setPrice(decimal(row, "price"));
|
|
|
|
|
+ order.setQuantity(integer(row, "quantity"));
|
|
|
|
|
+ order.setSku(nullableText(row, "sku", 32));
|
|
|
|
|
+ order.setAttrs(nullableText(row, "attrs", 500));
|
|
|
|
|
+ order.setOrderStatus(nullableText(row, "order_status", 32));
|
|
|
|
|
+ order.setPayNumber(nullableText(row, "pay_number", 32));
|
|
|
|
|
+ order.setPayAmount(decimal(row, "pay_amount"));
|
|
|
|
|
+ order.setPaidAmount(decimal(row, "paid_amount"));
|
|
|
|
|
+ order.setRefundStatus(nullableText(row, "refund_status", 32));
|
|
|
|
|
+ order.setRefundAmount(nullableText(row, "refund_amount", 32));
|
|
|
|
|
+ order.setCreateTime(timestamp(row, "create_time"));
|
|
|
|
|
+ order.setPayTime(timestamp(row, "pay_time"));
|
|
|
|
|
+ return order;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private DtmAssemblyRecord mapAssemblyRecordRow(Map<String, String> row) {
|
|
|
|
|
+ DtmAssemblyRecord record = new DtmAssemblyRecord();
|
|
|
|
|
+ record.setAssemblyDate(date(row, "assembly_date"));
|
|
|
|
|
+ record.setProductCode(text(row, "product_code", 64));
|
|
|
|
|
+ record.setQuantity(integer(row, "quantity"));
|
|
|
|
|
+ return record;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private DtmStore mapStoreRow(Map<String, String> row) {
|
|
|
|
|
+ String deptName = text(row, "business_unit_name", 32);
|
|
|
|
|
+ String channelName = text(row, "channel_name", 32);
|
|
|
|
|
+ String storeName = text(row, "platform_name", 32);
|
|
|
|
|
+ LocalDate date = dateFromParts(row);
|
|
|
|
|
+
|
|
|
|
|
+ DtmStore store = new DtmStore();
|
|
|
|
|
+ store.setStoreCode(buildStoreCode(row, deptName, channelName, storeName, date));
|
|
|
|
|
+ store.setStoreName(storeName);
|
|
|
|
|
+ store.setDeptName(deptName);
|
|
|
|
|
+ store.setChannelName(channelName);
|
|
|
|
|
+ store.setRecordStartDate(Date.valueOf(date));
|
|
|
|
|
+ store.setRecordEndDate(Date.valueOf(date));
|
|
|
|
|
+ store.setSalesQty(integer(row, "quantity"));
|
|
|
|
|
+ store.setSalesAmount(decimal(row, "sales_amount"));
|
|
|
|
|
+ return store;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private <T> List<T> mapRows(List<Map<String, String>> rows, RowMapper<T> mapper) {
|
|
|
|
|
+ List<T> list = new ArrayList<>();
|
|
|
|
|
+ for (Map<String, String> row : rows) {
|
|
|
|
|
+ T item = mapper.map(row);
|
|
|
|
|
+ if (item != null) {
|
|
|
|
|
+ list.add(item);
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ return list;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private LocalDate dateFromParts(Map<String, String> row) {
|
|
|
|
|
+ int year = safeInt(value(row, "stat_year"), 1970);
|
|
|
|
|
+ int month = safeInt(value(row, "stat_month"), 1);
|
|
|
|
|
+ int day = safeInt(value(row, "stat_day"), 1);
|
|
|
|
|
+ return LocalDate.of(year, month, day);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private String buildStoreCode(Map<String, String> row, String deptName, String channelName, String storeName, LocalDate date) {
|
|
|
|
|
+ String raw = deptName + "|" + channelName + "|" + storeName + "|" + date + "|"
|
|
|
|
|
+ + value(row, "product_code") + "|" + value(row, "quantity") + "|" + value(row, "sales_amount");
|
|
|
|
|
+ return "ST" + md5(raw).substring(0, 14).toUpperCase(Locale.ROOT);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private String md5(String raw) {
|
|
|
|
|
+ try {
|
|
|
|
|
+ MessageDigest digest = MessageDigest.getInstance("MD5");
|
|
|
|
|
+ byte[] bytes = digest.digest(raw.getBytes(StandardCharsets.UTF_8));
|
|
|
|
|
+ StringBuilder builder = new StringBuilder();
|
|
|
|
|
+ for (byte b : bytes) {
|
|
|
|
|
+ builder.append(String.format("%02x", b));
|
|
|
|
|
+ }
|
|
|
|
|
+ return builder.toString();
|
|
|
|
|
+ } catch (Exception e) {
|
|
|
|
|
+ return Integer.toHexString(raw.hashCode());
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private List<String> normalizeHeaders(List<String> headers) {
|
|
|
|
|
+ List<String> result = new ArrayList<>();
|
|
|
|
|
+ if (headers == null) {
|
|
|
|
|
+ return result;
|
|
|
|
|
+ }
|
|
|
|
|
+ for (String header : headers) {
|
|
|
|
|
+ result.add(clean(header).toLowerCase(Locale.ROOT));
|
|
|
|
|
+ }
|
|
|
|
|
+ return result;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private static String text(Map<String, String> row, String key, int maxLength) {
|
|
|
|
|
+ String value = value(row, key);
|
|
|
|
|
+ if (StringUtils.isEmpty(value)) {
|
|
|
|
|
+ throw new IllegalArgumentException("缺少必填内容: " + key);
|
|
|
|
|
+ }
|
|
|
|
|
+ return limit(value, maxLength);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private static String nullableText(Map<String, String> row, String key, int maxLength) {
|
|
|
|
|
+ String value = value(row, key);
|
|
|
|
|
+ return StringUtils.isEmpty(value) ? null : limit(value, maxLength);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private static Integer integer(Map<String, String> row, String key) {
|
|
|
|
|
+ String value = value(row, key);
|
|
|
|
|
+ if (StringUtils.isEmpty(value)) {
|
|
|
|
|
+ return null;
|
|
|
|
|
+ }
|
|
|
|
|
+ return new BigDecimal(value.replace(",", "")).intValue();
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private static int safeInt(String value, int fallback) {
|
|
|
|
|
+ if (StringUtils.isEmpty(value)) {
|
|
|
|
|
+ return fallback;
|
|
|
|
|
+ }
|
|
|
|
|
+ try {
|
|
|
|
|
+ return new BigDecimal(value.replace(",", "")).intValue();
|
|
|
|
|
+ } catch (Exception e) {
|
|
|
|
|
+ return fallback;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private static BigDecimal decimal(Map<String, String> row, String key) {
|
|
|
|
|
+ String value = value(row, key);
|
|
|
|
|
+ if (StringUtils.isEmpty(value) || "无退款金额".equals(value)) {
|
|
|
|
|
+ return null;
|
|
|
|
|
+ }
|
|
|
|
|
+ return new BigDecimal(value.replace(",", ""));
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private static Date date(Map<String, String> row, String key) {
|
|
|
|
|
+ String value = value(row, key);
|
|
|
|
|
+ if (StringUtils.isEmpty(value)) {
|
|
|
|
|
+ return null;
|
|
|
|
|
+ }
|
|
|
|
|
+ return Date.valueOf(parseDate(value));
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private static Timestamp timestamp(Map<String, String> row, String key) {
|
|
|
|
|
+ String value = value(row, key);
|
|
|
|
|
+ if (StringUtils.isEmpty(value)) {
|
|
|
|
|
+ return null;
|
|
|
|
|
+ }
|
|
|
|
|
+ String normalized = value.trim().replace('/', '-');
|
|
|
|
|
+ if (normalized.length() <= 10) {
|
|
|
|
|
+ return Timestamp.valueOf(LocalDate.parse(normalized, DateTimeFormatter.ofPattern("yyyy-M-d")).atStartOfDay());
|
|
|
|
|
+ }
|
|
|
|
|
+ List<DateTimeFormatter> formatters = Arrays.asList(
|
|
|
|
|
+ DateTimeFormatter.ofPattern("yyyy-M-d H:m:s"),
|
|
|
|
|
+ DateTimeFormatter.ofPattern("yyyy-M-d H:m")
|
|
|
|
|
+ );
|
|
|
|
|
+ for (DateTimeFormatter formatter : formatters) {
|
|
|
|
|
+ try {
|
|
|
|
|
+ return Timestamp.valueOf(LocalDateTime.parse(normalized, formatter));
|
|
|
|
|
+ } catch (Exception ignored) {
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ throw new IllegalArgumentException("无法解析时间: " + value);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private static LocalDate parseDate(String value) {
|
|
|
|
|
+ String normalized = value.trim().replace('/', '-');
|
|
|
|
|
+ return LocalDate.parse(normalized, DateTimeFormatter.ofPattern("yyyy-M-d"));
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private static String value(Map<String, String> row, String key) {
|
|
|
|
|
+ return clean(row.get(key.toLowerCase(Locale.ROOT)));
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private static String clean(Object value) {
|
|
|
|
|
+ if (value == null) {
|
|
|
|
|
+ return "";
|
|
|
|
|
+ }
|
|
|
|
|
+ String text = String.valueOf(value).trim();
|
|
|
|
|
+ if (text.startsWith("\uFEFF")) {
|
|
|
|
|
+ text = text.substring(1);
|
|
|
|
|
+ }
|
|
|
|
|
+ return text;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private static String limit(String value, int maxLength) {
|
|
|
|
|
+ if (value == null || value.length() <= maxLength) {
|
|
|
|
|
+ return value;
|
|
|
|
|
+ }
|
|
|
|
|
+ return value.substring(0, maxLength);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private String resolveExtension(String fileName) {
|
|
|
|
|
+ if (fileName == null) {
|
|
|
|
|
+ return "";
|
|
|
|
|
+ }
|
|
|
|
|
+ int idx = fileName.lastIndexOf('.');
|
|
|
|
|
+ return idx < 0 ? "" : fileName.substring(idx).toLowerCase(Locale.ROOT);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private interface UploadWriter {
|
|
|
|
|
+ int write(List<Map<String, String>> rows);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private interface RowMapper<T> {
|
|
|
|
|
+ T map(Map<String, String> row);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private static class FileRows {
|
|
|
|
|
+ private final List<String> headers;
|
|
|
|
|
+ private final List<Map<String, String>> rows;
|
|
|
|
|
+
|
|
|
|
|
+ private FileRows(List<String> headers, List<Map<String, String>> rows) {
|
|
|
|
|
+ this.headers = headers;
|
|
|
|
|
+ this.rows = rows;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private static class FieldRequirement {
|
|
|
|
|
+ private final String name;
|
|
|
|
|
+ private final String label;
|
|
|
|
|
+
|
|
|
|
|
+ private FieldRequirement(String name, String label) {
|
|
|
|
|
+ this.name = name;
|
|
|
|
|
+ this.label = label;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ public String getName() {
|
|
|
|
|
+ return name;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ public String getLabel() {
|
|
|
|
|
+ return label;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private static class UploadConfig {
|
|
|
|
|
+ private final String key;
|
|
|
|
|
+ private final String label;
|
|
|
|
|
+ private final boolean replaceBeforeInsert;
|
|
|
|
|
+ private final List<FieldRequirement> requiredFields;
|
|
|
|
|
+ private final UploadWriter writer;
|
|
|
|
|
+
|
|
|
|
|
+ private UploadConfig(String key, String label, boolean replaceBeforeInsert,
|
|
|
|
|
+ List<FieldRequirement> requiredFields, UploadWriter writer) {
|
|
|
|
|
+ this.key = key;
|
|
|
|
|
+ this.label = label;
|
|
|
|
|
+ this.replaceBeforeInsert = replaceBeforeInsert;
|
|
|
|
|
+ this.requiredFields = requiredFields;
|
|
|
|
|
+ this.writer = writer;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+}
|