|
@@ -3,26 +3,18 @@ package com.dtm.upload.service;
|
|
|
import com.dtm.common.utils.StringUtils;
|
|
import com.dtm.common.utils.StringUtils;
|
|
|
import com.dtm.storage.util.ExcelSheet;
|
|
import com.dtm.storage.util.ExcelSheet;
|
|
|
import com.dtm.storage.util.ExcelUtils;
|
|
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.CSVFormat;
|
|
|
import org.apache.commons.csv.CSVParser;
|
|
import org.apache.commons.csv.CSVParser;
|
|
|
import org.apache.commons.csv.CSVRecord;
|
|
import org.apache.commons.csv.CSVRecord;
|
|
|
|
|
+import org.springframework.jdbc.core.JdbcTemplate;
|
|
|
import org.springframework.stereotype.Service;
|
|
import org.springframework.stereotype.Service;
|
|
|
|
|
+import org.springframework.transaction.annotation.Transactional;
|
|
|
import org.springframework.web.multipart.MultipartFile;
|
|
import org.springframework.web.multipart.MultipartFile;
|
|
|
|
|
|
|
|
import java.io.BufferedReader;
|
|
import java.io.BufferedReader;
|
|
|
import java.io.InputStreamReader;
|
|
import java.io.InputStreamReader;
|
|
|
import java.math.BigDecimal;
|
|
import java.math.BigDecimal;
|
|
|
import java.nio.charset.Charset;
|
|
import java.nio.charset.Charset;
|
|
|
-import java.nio.charset.StandardCharsets;
|
|
|
|
|
-import java.security.MessageDigest;
|
|
|
|
|
import java.sql.Date;
|
|
import java.sql.Date;
|
|
|
import java.sql.Timestamp;
|
|
import java.sql.Timestamp;
|
|
|
import java.time.LocalDate;
|
|
import java.time.LocalDate;
|
|
@@ -31,42 +23,24 @@ import java.time.format.DateTimeFormatter;
|
|
|
import java.util.ArrayList;
|
|
import java.util.ArrayList;
|
|
|
import java.util.Arrays;
|
|
import java.util.Arrays;
|
|
|
import java.util.HashMap;
|
|
import java.util.HashMap;
|
|
|
|
|
+import java.util.HashSet;
|
|
|
import java.util.LinkedHashMap;
|
|
import java.util.LinkedHashMap;
|
|
|
|
|
+import java.util.LinkedHashSet;
|
|
|
import java.util.List;
|
|
import java.util.List;
|
|
|
import java.util.Locale;
|
|
import java.util.Locale;
|
|
|
import java.util.Map;
|
|
import java.util.Map;
|
|
|
|
|
+import java.util.Set;
|
|
|
|
|
|
|
|
@Service
|
|
@Service
|
|
|
public class PreparedDataUploadService {
|
|
public class PreparedDataUploadService {
|
|
|
private static final Charset CSV_CHARSET = Charset.forName("GB18030");
|
|
private static final Charset CSV_CHARSET = Charset.forName("GB18030");
|
|
|
private static final List<String> SUPPORTED_EXTENSIONS = Arrays.asList(".csv", ".xls", ".xlsx");
|
|
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 JdbcTemplate jdbcTemplate;
|
|
|
private final Map<String, UploadConfig> configs;
|
|
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;
|
|
|
|
|
|
|
+ public PreparedDataUploadService(JdbcTemplate jdbcTemplate) {
|
|
|
|
|
+ this.jdbcTemplate = jdbcTemplate;
|
|
|
this.configs = buildConfigs();
|
|
this.configs = buildConfigs();
|
|
|
}
|
|
}
|
|
|
|
|
|
|
@@ -76,32 +50,40 @@ public class PreparedDataUploadService {
|
|
|
Map<String, Object> item = new LinkedHashMap<>();
|
|
Map<String, Object> item = new LinkedHashMap<>();
|
|
|
item.put("key", config.key);
|
|
item.put("key", config.key);
|
|
|
item.put("label", config.label);
|
|
item.put("label", config.label);
|
|
|
|
|
+ item.put("tableName", config.tableName);
|
|
|
|
|
+ item.put("order", config.order);
|
|
|
|
|
+ item.put("dependencies", config.dependencies);
|
|
|
item.put("replaceBeforeInsert", config.replaceBeforeInsert);
|
|
item.put("replaceBeforeInsert", config.replaceBeforeInsert);
|
|
|
|
|
+ item.put("databaseFields", buildDatabaseFields(config));
|
|
|
items.add(item);
|
|
items.add(item);
|
|
|
}
|
|
}
|
|
|
return items;
|
|
return items;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- public Map<String, Object> upload(String key, MultipartFile file) {
|
|
|
|
|
|
|
+ @Transactional
|
|
|
|
|
+ public Map<String, Object> upload(String key, MultipartFile file, Map<String, String> requestedMappings) {
|
|
|
UploadConfig config = configs.get(key);
|
|
UploadConfig config = configs.get(key);
|
|
|
validateFile(config, file);
|
|
validateFile(config, file);
|
|
|
|
|
|
|
|
try {
|
|
try {
|
|
|
FileRows fileRows = readRows(file, resolveExtension(file.getOriginalFilename()));
|
|
FileRows fileRows = readRows(file, resolveExtension(file.getOriginalFilename()));
|
|
|
- List<FieldRequirement> missingFields = getMissingFields(config, fileRows.headers);
|
|
|
|
|
- if (!missingFields.isEmpty()) {
|
|
|
|
|
- throw new IllegalArgumentException("文件缺少必要内容:" + joinFieldLabels(missingFields));
|
|
|
|
|
|
|
+ MappingValidation validation = validateMappings(config, fileRows.headers, requestedMappings);
|
|
|
|
|
+ if (!validation.valid) {
|
|
|
|
|
+ throw new IllegalArgumentException("字段映射未完成:" + joinFieldLabels(validation.missingFields));
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- int affected = writeRows(config, fileRows.rows);
|
|
|
|
|
|
|
+ List<Map<String, Object>> normalizedRows = normalizeRows(config, fileRows.rows, validation.mappings);
|
|
|
|
|
+ int affected = writeRows(config, normalizedRows, validation.mappings.keySet());
|
|
|
|
|
|
|
|
Map<String, Object> result = new LinkedHashMap<>();
|
|
Map<String, Object> result = new LinkedHashMap<>();
|
|
|
result.put("key", key);
|
|
result.put("key", key);
|
|
|
result.put("label", config.label);
|
|
result.put("label", config.label);
|
|
|
|
|
+ result.put("tableName", config.tableName);
|
|
|
result.put("fileName", file.getOriginalFilename());
|
|
result.put("fileName", file.getOriginalFilename());
|
|
|
- result.put("totalRows", fileRows.rows.size());
|
|
|
|
|
|
|
+ result.put("totalRows", normalizedRows.size());
|
|
|
result.put("affectedRows", affected);
|
|
result.put("affectedRows", affected);
|
|
|
- result.put("message", config.label + "处理完成,共读取 " + fileRows.rows.size() + " 行");
|
|
|
|
|
|
|
+ result.put("fieldMappings", validation.mappings);
|
|
|
|
|
+ result.put("message", config.label + "处理完成,共读取 " + normalizedRows.size() + " 行");
|
|
|
return result;
|
|
return result;
|
|
|
} catch (IllegalArgumentException e) {
|
|
} catch (IllegalArgumentException e) {
|
|
|
throw e;
|
|
throw e;
|
|
@@ -110,23 +92,33 @@ public class PreparedDataUploadService {
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- public Map<String, Object> preview(String key, MultipartFile file) {
|
|
|
|
|
|
|
+ public Map<String, Object> upload(String key, MultipartFile file) {
|
|
|
|
|
+ return upload(key, file, null);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ public Map<String, Object> preview(String key, MultipartFile file, Map<String, String> requestedMappings) {
|
|
|
UploadConfig config = configs.get(key);
|
|
UploadConfig config = configs.get(key);
|
|
|
validateFile(config, file);
|
|
validateFile(config, file);
|
|
|
|
|
|
|
|
try {
|
|
try {
|
|
|
FileRows fileRows = readRows(file, resolveExtension(file.getOriginalFilename()));
|
|
FileRows fileRows = readRows(file, resolveExtension(file.getOriginalFilename()));
|
|
|
- List<FieldRequirement> missingFields = getMissingFields(config, fileRows.headers);
|
|
|
|
|
|
|
+ MappingValidation validation = validateMappings(config, fileRows.headers, requestedMappings);
|
|
|
|
|
+
|
|
|
Map<String, Object> result = new LinkedHashMap<>();
|
|
Map<String, Object> result = new LinkedHashMap<>();
|
|
|
result.put("key", key);
|
|
result.put("key", key);
|
|
|
result.put("label", config.label);
|
|
result.put("label", config.label);
|
|
|
|
|
+ result.put("tableName", config.tableName);
|
|
|
result.put("fileName", file.getOriginalFilename());
|
|
result.put("fileName", file.getOriginalFilename());
|
|
|
result.put("totalRows", fileRows.rows.size());
|
|
result.put("totalRows", fileRows.rows.size());
|
|
|
result.put("headers", fileRows.headers);
|
|
result.put("headers", fileRows.headers);
|
|
|
result.put("columns", buildPreviewColumns(fileRows.headers));
|
|
result.put("columns", buildPreviewColumns(fileRows.headers));
|
|
|
result.put("sampleRows", fileRows.rows.subList(0, Math.min(fileRows.rows.size(), 20)));
|
|
result.put("sampleRows", fileRows.rows.subList(0, Math.min(fileRows.rows.size(), 20)));
|
|
|
- result.put("missingFields", missingFields);
|
|
|
|
|
- result.put("valid", missingFields.isEmpty());
|
|
|
|
|
|
|
+ result.put("databaseFields", buildDatabaseFields(config));
|
|
|
|
|
+ result.put("fieldMappings", validation.mappings);
|
|
|
|
|
+ result.put("fieldStatuses", validation.fieldStatuses);
|
|
|
|
|
+ result.put("missingFields", validation.missingFields);
|
|
|
|
|
+ result.put("duplicateHeaders", validation.duplicateHeaders);
|
|
|
|
|
+ result.put("valid", validation.valid);
|
|
|
return result;
|
|
return result;
|
|
|
} catch (IllegalArgumentException e) {
|
|
} catch (IllegalArgumentException e) {
|
|
|
throw e;
|
|
throw e;
|
|
@@ -135,6 +127,10 @@ public class PreparedDataUploadService {
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+ public Map<String, Object> preview(String key, MultipartFile file) {
|
|
|
|
|
+ return preview(key, file, null);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
private void validateFile(UploadConfig config, MultipartFile file) {
|
|
private void validateFile(UploadConfig config, MultipartFile file) {
|
|
|
if (config == null) {
|
|
if (config == null) {
|
|
|
throw new IllegalArgumentException("这项资料暂未整理完成,暂不支持上传");
|
|
throw new IllegalArgumentException("这项资料暂未整理完成,暂不支持上传");
|
|
@@ -148,11 +144,121 @@ public class PreparedDataUploadService {
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- private int writeRows(UploadConfig config, List<Map<String, String>> rows) {
|
|
|
|
|
|
|
+ private int writeRows(UploadConfig config, List<Map<String, Object>> rows, Set<String> mappedFieldNames) {
|
|
|
|
|
+ if (jdbcTemplate == null) {
|
|
|
|
|
+ throw new IllegalStateException("数据源未初始化");
|
|
|
|
|
+ }
|
|
|
if (rows.isEmpty()) {
|
|
if (rows.isEmpty()) {
|
|
|
throw new IllegalArgumentException("文件中没有可导入的数据");
|
|
throw new IllegalArgumentException("文件中没有可导入的数据");
|
|
|
}
|
|
}
|
|
|
- return config.writer.write(rows);
|
|
|
|
|
|
|
+
|
|
|
|
|
+ List<FieldDefinition> insertFields = new ArrayList<>();
|
|
|
|
|
+ for (FieldDefinition field : config.fields) {
|
|
|
|
|
+ if (!field.autoIncrement && mappedFieldNames.contains(field.name)) {
|
|
|
|
|
+ insertFields.add(field);
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ if (insertFields.isEmpty()) {
|
|
|
|
|
+ throw new IllegalArgumentException("没有可写入的字段");
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ if (config.replaceBeforeInsert) {
|
|
|
|
|
+ jdbcTemplate.update("delete from `" + config.tableName + "`");
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ String sql = buildInsertSql(config, insertFields);
|
|
|
|
|
+ List<Object[]> args = new ArrayList<>();
|
|
|
|
|
+ for (Map<String, Object> row : rows) {
|
|
|
|
|
+ Object[] values = new Object[insertFields.size()];
|
|
|
|
|
+ for (int i = 0; i < insertFields.size(); i++) {
|
|
|
|
|
+ values[i] = row.get(insertFields.get(i).name);
|
|
|
|
|
+ }
|
|
|
|
|
+ args.add(values);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ int affected = 0;
|
|
|
|
|
+ int[] results = jdbcTemplate.batchUpdate(sql, args);
|
|
|
|
|
+ for (int result : results) {
|
|
|
|
|
+ affected += Math.max(result, 0);
|
|
|
|
|
+ }
|
|
|
|
|
+ return affected;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private String buildInsertSql(UploadConfig config, List<FieldDefinition> fields) {
|
|
|
|
|
+ StringBuilder columns = new StringBuilder();
|
|
|
|
|
+ StringBuilder placeholders = new StringBuilder();
|
|
|
|
|
+ StringBuilder updates = new StringBuilder();
|
|
|
|
|
+
|
|
|
|
|
+ for (int i = 0; i < fields.size(); i++) {
|
|
|
|
|
+ FieldDefinition field = fields.get(i);
|
|
|
|
|
+ if (i > 0) {
|
|
|
|
|
+ columns.append(", ");
|
|
|
|
|
+ placeholders.append(", ");
|
|
|
|
|
+ }
|
|
|
|
|
+ columns.append("`").append(field.name).append("`");
|
|
|
|
|
+ placeholders.append("?");
|
|
|
|
|
+
|
|
|
|
|
+ if (!field.primaryKey) {
|
|
|
|
|
+ if (updates.length() > 0) {
|
|
|
|
|
+ updates.append(", ");
|
|
|
|
|
+ }
|
|
|
|
|
+ updates.append("`").append(field.name).append("`=values(`").append(field.name).append("`)");
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ String sql = "insert into `" + config.tableName + "` (" + columns + ") values (" + placeholders + ")";
|
|
|
|
|
+ if (updates.length() > 0) {
|
|
|
|
|
+ sql += " on duplicate key update " + updates;
|
|
|
|
|
+ }
|
|
|
|
|
+ return sql;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private List<Map<String, Object>> normalizeRows(UploadConfig config,
|
|
|
|
|
+ List<Map<String, String>> fileRows,
|
|
|
|
|
+ Map<String, String> mappings) {
|
|
|
|
|
+ List<Map<String, Object>> rows = new ArrayList<>();
|
|
|
|
|
+ int rowNumber = 1;
|
|
|
|
|
+ for (Map<String, String> fileRow : fileRows) {
|
|
|
|
|
+ Map<String, Object> row = new LinkedHashMap<>();
|
|
|
|
|
+ for (FieldDefinition field : config.fields) {
|
|
|
|
|
+ String header = mappings.get(field.name);
|
|
|
|
|
+ if (StringUtils.isEmpty(header)) {
|
|
|
|
|
+ continue;
|
|
|
|
|
+ }
|
|
|
|
|
+ String rawValue = clean(fileRow.get(normalizeHeader(header)));
|
|
|
|
|
+ if (StringUtils.isEmpty(rawValue) && !field.required) {
|
|
|
|
|
+ row.put(field.name, null);
|
|
|
|
|
+ continue;
|
|
|
|
|
+ }
|
|
|
|
|
+ if (StringUtils.isEmpty(rawValue) && field.required) {
|
|
|
|
|
+ throw new IllegalArgumentException("第 " + rowNumber + " 行缺少必填内容: " + field.label);
|
|
|
|
|
+ }
|
|
|
|
|
+ row.put(field.name, convertValue(field, rawValue, rowNumber));
|
|
|
|
|
+ }
|
|
|
|
|
+ rows.add(row);
|
|
|
|
|
+ rowNumber++;
|
|
|
|
|
+ }
|
|
|
|
|
+ return rows;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private Object convertValue(FieldDefinition field, String rawValue, int rowNumber) {
|
|
|
|
|
+ try {
|
|
|
|
|
+ if ("int".equals(field.valueType) || "bigint".equals(field.valueType)) {
|
|
|
|
|
+ return new BigDecimal(rawValue.replace(",", "")).longValue();
|
|
|
|
|
+ }
|
|
|
|
|
+ if ("decimal".equals(field.valueType)) {
|
|
|
|
|
+ return new BigDecimal(rawValue.replace(",", ""));
|
|
|
|
|
+ }
|
|
|
|
|
+ if ("date".equals(field.valueType)) {
|
|
|
|
|
+ return Date.valueOf(parseDate(rawValue));
|
|
|
|
|
+ }
|
|
|
|
|
+ if ("datetime".equals(field.valueType)) {
|
|
|
|
|
+ return parseTimestamp(rawValue);
|
|
|
|
|
+ }
|
|
|
|
|
+ return limit(rawValue, field.maxLength);
|
|
|
|
|
+ } catch (Exception e) {
|
|
|
|
|
+ throw new IllegalArgumentException("第 " + rowNumber + " 行字段 " + field.label + " 格式不正确: " + rawValue);
|
|
|
|
|
+ }
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
private FileRows readRows(MultipartFile file, String extension) throws Exception {
|
|
private FileRows readRows(MultipartFile file, String extension) throws Exception {
|
|
@@ -218,54 +324,95 @@ public class PreparedDataUploadService {
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- 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 MappingValidation validateMappings(UploadConfig config, List<String> headers, Map<String, String> requestedMappings) {
|
|
|
|
|
+ Map<String, String> mappings = requestedMappings == null || requestedMappings.isEmpty()
|
|
|
|
|
+ ? recommendMappings(config, headers)
|
|
|
|
|
+ : normalizeRequestedMappings(config, headers, requestedMappings);
|
|
|
|
|
+ List<FieldRequirement> missingFields = new ArrayList<>();
|
|
|
|
|
+ List<Map<String, Object>> fieldStatuses = new ArrayList<>();
|
|
|
|
|
+ Set<String> seenHeaders = new HashSet<>();
|
|
|
|
|
+ Set<String> duplicateHeaders = new LinkedHashSet<>();
|
|
|
|
|
+
|
|
|
|
|
+ for (FieldDefinition field : config.fields) {
|
|
|
|
|
+ String header = mappings.get(field.name);
|
|
|
|
|
+ if (StringUtils.isNotEmpty(header) && !seenHeaders.add(header)) {
|
|
|
|
|
+ duplicateHeaders.add(header);
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ for (FieldDefinition field : config.fields) {
|
|
|
|
|
+ String header = mappings.get(field.name);
|
|
|
|
|
+ String type = "success";
|
|
|
|
|
+ String message = "匹配成功";
|
|
|
|
|
+ if (field.required && StringUtils.isEmpty(header)) {
|
|
|
|
|
+ missingFields.add(field.requirement());
|
|
|
|
|
+ type = "danger";
|
|
|
|
|
+ message = "必填未匹配";
|
|
|
|
|
+ } else if (StringUtils.isEmpty(header)) {
|
|
|
|
|
+ type = "info";
|
|
|
|
|
+ message = "选填未匹配";
|
|
|
|
|
+ } else if (duplicateHeaders.contains(header)) {
|
|
|
|
|
+ type = "danger";
|
|
|
|
|
+ message = "重复映射";
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ Map<String, Object> status = new LinkedHashMap<>();
|
|
|
|
|
+ status.put("name", field.name);
|
|
|
|
|
+ status.put("header", header);
|
|
|
|
|
+ status.put("type", type);
|
|
|
|
|
+ status.put("message", message);
|
|
|
|
|
+ fieldStatuses.add(status);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ return new MappingValidation(mappings, missingFields, new ArrayList<>(duplicateHeaders), fieldStatuses);
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- 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);
|
|
|
|
|
|
|
+ private Map<String, String> recommendMappings(UploadConfig config, List<String> headers) {
|
|
|
|
|
+ Map<String, String> mappings = new LinkedHashMap<>();
|
|
|
|
|
+ Set<String> usedHeaders = new HashSet<>();
|
|
|
|
|
+ for (FieldDefinition field : config.fields) {
|
|
|
|
|
+ String matchedHeader = "";
|
|
|
|
|
+ for (String header : headers) {
|
|
|
|
|
+ if (usedHeaders.contains(header)) {
|
|
|
|
|
+ continue;
|
|
|
|
|
+ }
|
|
|
|
|
+ if (field.matches(header)) {
|
|
|
|
|
+ matchedHeader = header;
|
|
|
|
|
+ usedHeaders.add(header);
|
|
|
|
|
+ break;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ if (StringUtils.isNotEmpty(matchedHeader)) {
|
|
|
|
|
+ mappings.put(field.name, matchedHeader);
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
- return missingFields;
|
|
|
|
|
|
|
+ return mappings;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- private String joinFieldLabels(List<FieldRequirement> fields) {
|
|
|
|
|
- List<String> labels = new ArrayList<>();
|
|
|
|
|
- for (FieldRequirement field : fields) {
|
|
|
|
|
- labels.add(field.label + "(" + field.name + ")");
|
|
|
|
|
|
|
+ private Map<String, String> normalizeRequestedMappings(UploadConfig config,
|
|
|
|
|
+ List<String> headers,
|
|
|
|
|
+ Map<String, String> requestedMappings) {
|
|
|
|
|
+ Set<String> allowedFields = new HashSet<>();
|
|
|
|
|
+ for (FieldDefinition field : config.fields) {
|
|
|
|
|
+ allowedFields.add(field.name);
|
|
|
}
|
|
}
|
|
|
- return String.join("、", labels);
|
|
|
|
|
|
|
+ Set<String> allowedHeaders = new HashSet<>(headers);
|
|
|
|
|
+ Map<String, String> mappings = new LinkedHashMap<>();
|
|
|
|
|
+ for (Map.Entry<String, String> entry : requestedMappings.entrySet()) {
|
|
|
|
|
+ String fieldName = normalizeHeader(entry.getKey());
|
|
|
|
|
+ String header = normalizeHeader(entry.getValue());
|
|
|
|
|
+ if (StringUtils.isEmpty(header)) {
|
|
|
|
|
+ continue;
|
|
|
|
|
+ }
|
|
|
|
|
+ if (!allowedFields.contains(fieldName)) {
|
|
|
|
|
+ throw new IllegalArgumentException("字段不属于当前数据表: " + entry.getKey());
|
|
|
|
|
+ }
|
|
|
|
|
+ if (!allowedHeaders.contains(header)) {
|
|
|
|
|
+ throw new IllegalArgumentException("文件中不存在表头: " + entry.getValue());
|
|
|
|
|
+ }
|
|
|
|
|
+ mappings.put(fieldName, header);
|
|
|
|
|
+ }
|
|
|
|
|
+ return mappings;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
private List<Map<String, Object>> buildPreviewColumns(List<String> headers) {
|
|
private List<Map<String, Object>> buildPreviewColumns(List<String> headers) {
|
|
@@ -280,234 +427,234 @@ public class PreparedDataUploadService {
|
|
|
return columns;
|
|
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);
|
|
|
|
|
- }
|
|
|
|
|
|
|
+ private List<Map<String, Object>> buildDatabaseFields(UploadConfig config) {
|
|
|
|
|
+ List<Map<String, Object>> fields = new ArrayList<>();
|
|
|
|
|
+ for (FieldDefinition field : config.fields) {
|
|
|
|
|
+ Map<String, Object> item = new LinkedHashMap<>();
|
|
|
|
|
+ item.put("name", field.name);
|
|
|
|
|
+ item.put("label", field.label);
|
|
|
|
|
+ item.put("type", field.sqlType);
|
|
|
|
|
+ item.put("required", field.required);
|
|
|
|
|
+ item.put("primaryKey", field.primaryKey);
|
|
|
|
|
+ item.put("autoIncrement", field.autoIncrement);
|
|
|
|
|
+ fields.add(item);
|
|
|
}
|
|
}
|
|
|
- return list;
|
|
|
|
|
|
|
+ return fields;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- 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 Map<String, UploadConfig> buildConfigs() {
|
|
|
|
|
+ Map<String, UploadConfig> map = new LinkedHashMap<>();
|
|
|
|
|
+ map.put("warehouse", config("warehouse", 1, "dtm_warehouse", "仓库资料", false,
|
|
|
|
|
+ fields(
|
|
|
|
|
+ text("warehouse_code", "仓库编码", "varchar(32)", 32, true, true, false, "仓库编号"),
|
|
|
|
|
+ text("warehouse_name", "仓库名称", "varchar(100)", 100, true, false, false),
|
|
|
|
|
+ text("warehouse_type", "仓库类型", "varchar(32)", 32, false, false, false),
|
|
|
|
|
+ text("warehouse_address", "仓库地址", "varchar(200)", 200, false, false, false)
|
|
|
|
|
+ )));
|
|
|
|
|
+ map.put("supplier", config("supplier", 2, "dtm_supplier", "供应商资料", false,
|
|
|
|
|
+ fields(
|
|
|
|
|
+ text("supplier_id", "供应商编号", "varchar(32)", 32, true, true, false, "供应商ID", "供应商编码"),
|
|
|
|
|
+ text("supplier_name", "供应商名称", "varchar(100)", 100, true, false, false)
|
|
|
|
|
+ )));
|
|
|
|
|
+ map.put("semi_finished_product", config("semi_finished_product", 3, "dtm_semi_finished_product", "半成品资料", false,
|
|
|
|
|
+ fields(
|
|
|
|
|
+ text("sku", "半成品SKU", "varchar(64)", 64, true, true, false, "半成品编码"),
|
|
|
|
|
+ text("semi_name", "半成品名称", "varchar(100)", 100, true, false, false),
|
|
|
|
|
+ decimal("price", "半成品单价", "decimal(10,2)", true, false, false, "单价"),
|
|
|
|
|
+ text("bom_level_id", "产品结构层级编号", "varchar(32)", 32, false, false, false)
|
|
|
|
|
+ )));
|
|
|
|
|
+ map.put("product", config("product", 4, "dtm_product", "产品资料", false,
|
|
|
|
|
+ fields(
|
|
|
|
|
+ text("sku", "产品SKU", "varchar(64)", 64, true, true, false, "产品编码", "商品SKU"),
|
|
|
|
|
+ text("spu", "SPU", "varchar(64)", 64, false, false, false),
|
|
|
|
|
+ text("product_name", "产品名称", "varchar(128)", 128, true, false, false, "商品名称"),
|
|
|
|
|
+ text("attributes", "产品属性", "varchar(32)", 32, false, false, false),
|
|
|
|
|
+ text("bom_level_id", "产品结构层级编号", "varchar(32)", 32, false, false, false),
|
|
|
|
|
+ decimal("product_price", "产品价格", "decimal(18,2)", false, false, false, "价格", "单价")
|
|
|
|
|
+ )));
|
|
|
|
|
+ map.put("product_structure", config("product_structure", 5, "dtm_product_structure", "产品分类资料", false,
|
|
|
|
|
+ fields(
|
|
|
|
|
+ text("level_no", "层级编号", "varchar(32)", 32, true, true, false),
|
|
|
|
|
+ text("parent_id", "父类ID", "varchar(32)", 32, false, false, false),
|
|
|
|
|
+ text("parent_name", "父类分类名称", "varchar(100)", 100, false, false, false),
|
|
|
|
|
+ text("category_id", "分类ID", "varchar(32)", 32, true, false, false),
|
|
|
|
|
+ text("level_name", "层级名称", "varchar(100)", 100, true, false, false, "分类名称")
|
|
|
|
|
+ )));
|
|
|
|
|
+ map.put("store", config("store", 6, "dtm_store", "店铺资料", true,
|
|
|
|
|
+ fields(
|
|
|
|
|
+ text("store_code", "店铺编码", "varchar(32)", 32, true, true, false, "店铺代码"),
|
|
|
|
|
+ text("store_name", "店铺名称", "varchar(32)", 32, true, false, false),
|
|
|
|
|
+ text("dept_name", "事业部名称", "varchar(32)", 32, true, false, false, "事业部", "部门"),
|
|
|
|
|
+ text("channel_name", "渠道名称", "varchar(32)", 32, true, false, false, "渠道"),
|
|
|
|
|
+ dateField("record_start_date", "记录开始日期", "date", false, false, false),
|
|
|
|
|
+ dateField("record_end_date", "记录结束日期", "date", false, false, false),
|
|
|
|
|
+ integer("sales_qty", "销量", "int", false, false, false, "销售数量", "数量"),
|
|
|
|
|
+ decimal("sales_amount", "销售额", "decimal(10,2)", false, false, false, "销售金额")
|
|
|
|
|
+ )));
|
|
|
|
|
+ map.put("structure_rule_setting", config("structure_rule_setting", 7, "dtm_rule_setting", "编码规则资料", false,
|
|
|
|
|
+ fields(
|
|
|
|
|
+ integer("rule_id", "规则ID", "bigint", false, true, true),
|
|
|
|
|
+ text("rule_type", "规则对象", "varchar(32)", 32, true, false, false),
|
|
|
|
|
+ text("parent_field", "父编码字段", "varchar(32)", 32, false, false, false),
|
|
|
|
|
+ integer("parent_length", "父编码占用位数", "int", true, false, false),
|
|
|
|
|
+ text("current_level", "当前层级名称", "varchar(32)", 32, true, false, false),
|
|
|
|
|
+ integer("current_length", "当前层级追加位数", "int", true, false, false),
|
|
|
|
|
+ integer("is_enabled", "是否启用", "tinyint", false, false, false),
|
|
|
|
|
+ text("remark", "备注", "varchar(255)", 255, false, false, false)
|
|
|
|
|
+ )));
|
|
|
|
|
+ map.put("location", config("location", 8, "dtm_location", "库位资料", false,
|
|
|
|
|
+ fields(
|
|
|
|
|
+ text("location_code", "库位编码", "varchar(32)", 32, true, true, false),
|
|
|
|
|
+ text("warehouse_code", "仓库编码", "varchar(32)", 32, true, false, false),
|
|
|
|
|
+ text("location_type", "库位类型", "varchar(32)", 32, false, false, false),
|
|
|
|
|
+ integer("capacity_limit", "库位容量限制", "int", false, false, false),
|
|
|
|
|
+ text("location_status", "库位状态", "varchar(20)", 20, false, false, false)
|
|
|
|
|
+ ), "warehouse"));
|
|
|
|
|
+ map.put("bom_list", config("bom_list", 9, "dtm_bom_list", "组合清单资料", true,
|
|
|
|
|
+ fields(
|
|
|
|
|
+ integer("id", "主键ID", "bigint", false, true, true),
|
|
|
|
|
+ text("finished_sku", "成品SKU", "varchar(64)", 64, true, false, false, "成品编码"),
|
|
|
|
|
+ text("semi_sku", "半成品SKU", "varchar(64)", 64, true, false, false, "半成品编码"),
|
|
|
|
|
+ integer("quantity", "数量", "int", true, false, false)
|
|
|
|
|
+ ), "product", "semi_finished_product"));
|
|
|
|
|
+ map.put("purchase_receipt", config("purchase_receipt", 10, "dtm_purchase_receipt", "采购入库资料", false,
|
|
|
|
|
+ fields(
|
|
|
|
|
+ text("receipt_id", "入库单号", "varchar(32)", 32, true, true, false, "单据编号"),
|
|
|
|
|
+ text("sku", "产品SKU", "varchar(64)", 64, true, false, false, "产品编码"),
|
|
|
|
|
+ text("supplier_id", "供应商编号", "varchar(32)", 32, true, false, false),
|
|
|
|
|
+ integer("quantity", "采购入库数量", "int", false, false, false, "数量"),
|
|
|
|
|
+ dateField("receipt_date", "入库日期", "date", false, false, false),
|
|
|
|
|
+ decimal("actual_amount", "实际金额", "decimal(18,2)", false, false, false),
|
|
|
|
|
+ text("status", "状态", "varchar(16)", 16, false, false, false),
|
|
|
|
|
+ text("warehouse_code", "仓库编码", "varchar(32)", 32, false, false, false),
|
|
|
|
|
+ decimal("unitprice", "单价", "decimal(18,2)", false, false, false)
|
|
|
|
|
+ ), "product", "supplier"));
|
|
|
|
|
+ map.put("order_main", config("order_main", 11, "dtm_order_main", "订单资料", false,
|
|
|
|
|
+ fields(
|
|
|
|
|
+ text("order_id", "订单编号", "varchar(64)", 64, true, true, false),
|
|
|
|
|
+ text("title", "商品标题", "varchar(255)", 255, false, false, false),
|
|
|
|
|
+ decimal("price", "商品原价", "decimal(10,2)", false, false, false, "单价"),
|
|
|
|
|
+ integer("quantity", "购买数量", "int", false, false, false, "数量"),
|
|
|
|
|
+ text("sku", "产品代码", "varchar(32)", 32, false, false, false, "产品编码"),
|
|
|
|
|
+ text("attrs", "商品属性", "varchar(500)", 500, false, false, false),
|
|
|
|
|
+ text("order_status", "订单状态", "varchar(32)", 32, false, false, false),
|
|
|
|
|
+ text("pay_number", "支付流水号", "varchar(32)", 32, false, false, false),
|
|
|
|
|
+ decimal("pay_amount", "应付金额", "decimal(10,2)", false, false, false),
|
|
|
|
|
+ decimal("paid_amount", "实付金额", "decimal(10,2)", false, false, false),
|
|
|
|
|
+ text("refund_status", "退款状态", "varchar(32)", 32, false, false, false),
|
|
|
|
|
+ text("refund_amount", "退款金额", "varchar(32)", 32, false, false, false),
|
|
|
|
|
+ datetime("create_time", "订单创建时间", "datetime", false, false, false),
|
|
|
|
|
+ datetime("pay_time", "订单付款时间", "datetime", false, false, false)
|
|
|
|
|
+ )));
|
|
|
|
|
+ map.put("assembly_record", config("assembly_record", 12, "dtm_assembly_record", "组装记录资料", true,
|
|
|
|
|
+ fields(
|
|
|
|
|
+ integer("id", "主键ID", "bigint", false, true, true),
|
|
|
|
|
+ text("product_code", "产品SKU", "varchar(64)", 64, true, false, false, "产品编码"),
|
|
|
|
|
+ dateField("assembly_date", "组装日期", "date", true, false, false),
|
|
|
|
|
+ integer("quantity", "组装数量", "int", true, false, false, "数量")
|
|
|
|
|
+ ), "product"));
|
|
|
|
|
+ map.put("outbound_order", config("outbound_order", 13, "dtm_outbound_order", "出库资料", false,
|
|
|
|
|
+ fields(
|
|
|
|
|
+ text("order_no", "出库单据编号", "varchar(32)", 32, true, true, false, "出库单号"),
|
|
|
|
|
+ text("location_code", "库位编码", "varchar(32)", 32, true, false, false),
|
|
|
|
|
+ text("sku", "产品SKU", "varchar(64)", 64, true, false, false, "产品编码"),
|
|
|
|
|
+ dateField("outbound_date", "出库日期", "date", true, false, false),
|
|
|
|
|
+ integer("quantity", "出库数量", "int", true, false, false, "数量")
|
|
|
|
|
+ ), "location", "product"));
|
|
|
|
|
+ map.put("inventory_detail", config("inventory_detail", 14, "dtm_stock_detail", "库存资料", false,
|
|
|
|
|
+ fields(
|
|
|
|
|
+ text("location_code", "库位编码", "varchar(32)", 32, true, true, false),
|
|
|
|
|
+ text("sku", "产品SKU", "varchar(64)", 64, true, true, false, "产品编码"),
|
|
|
|
|
+ integer("stock_quantity", "可用库存数量", "int", false, false, false, "可用库存", "库存数量"),
|
|
|
|
|
+ integer("lock_quantity", "锁定库存数量", "int", false, false, false, "锁定库存")
|
|
|
|
|
+ ), "location", "product"));
|
|
|
|
|
+ map.put("metrics", config("metrics", 15, "dtm_metrics", "经营指标资料", false,
|
|
|
|
|
+ fields(
|
|
|
|
|
+ integer("metrics_id", "指标ID", "bigint", false, true, true),
|
|
|
|
|
+ text("metrics_type", "指标类型", "varchar(32)", 32, true, false, false),
|
|
|
|
|
+ text("biz_code", "业务编码", "varchar(64)", 64, true, false, false),
|
|
|
|
|
+ dateField("stat_start_date", "统计开始日期", "date", true, false, false, "开始日期"),
|
|
|
|
|
+ dateField("stat_end_date", "统计结束日期", "date", true, false, false, "结束日期"),
|
|
|
|
|
+ integer("total_stock", "库存总量", "int", false, false, false),
|
|
|
|
|
+ decimal("stock_holding_cost", "库存持有成本", "decimal(10,2)", false, false, false),
|
|
|
|
|
+ decimal("stock_turnover_rate", "库存周转率", "decimal(10,4)", false, false, false),
|
|
|
|
|
+ decimal("warehouse_operation_cost", "仓储作业成本", "decimal(10,2)", false, false, false),
|
|
|
|
|
+ decimal("stock_accuracy", "库存准确率", "decimal(5,2)", false, false, false),
|
|
|
|
|
+ decimal("stock_sales_ratio", "库销比", "decimal(10,4)", false, false, false),
|
|
|
|
|
+ decimal("logistics_cost", "物流成本", "decimal(10,2)", false, false, false),
|
|
|
|
|
+ integer("ap_days", "应付账期", "int", false, false, false),
|
|
|
|
|
+ decimal("delivery_score", "交付能力评分", "decimal(5,2)", false, false, false),
|
|
|
|
|
+ decimal("quality_loss", "品质损失费", "decimal(10,2)", false, false, false),
|
|
|
|
|
+ decimal("inspection_fee", "品质检查费", "decimal(10,2)", false, false, false),
|
|
|
|
|
+ decimal("cost_price", "成本价格", "decimal(10,2)", false, false, false),
|
|
|
|
|
+ decimal("reverse_logistics", "逆向物流成本", "decimal(10,2)", false, false, false),
|
|
|
|
|
+ decimal("return_cost", "退货处理成本", "decimal(10,2)", false, false, false),
|
|
|
|
|
+ decimal("platform_value", "平台价值评分", "decimal(10,2)", false, false, false),
|
|
|
|
|
+ decimal("sum_amount", "店铺总销售额", "decimal(10,2)", false, false, false),
|
|
|
|
|
+ decimal("loss_rate", "漏损率", "decimal(5,4)", false, false, false),
|
|
|
|
|
+ decimal("large_order_pct", "大额订单贡献率", "decimal(5,4)", false, false, false),
|
|
|
|
|
+ decimal("funnel_conv", "支付决策漏斗转化率", "decimal(5,4)", false, false, false),
|
|
|
|
|
+ integer("avg_pay_time", "平均支付时长", "int", false, false, false),
|
|
|
|
|
+ text("remark", "备注", "varchar(255)", 255, false, false, false)
|
|
|
|
|
+ )));
|
|
|
|
|
+ return map;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- 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 UploadConfig config(String key, int order, String tableName, String label, boolean replaceBeforeInsert,
|
|
|
|
|
+ List<FieldDefinition> fields, String... dependencies) {
|
|
|
|
|
+ return new UploadConfig(key, order, tableName, label, replaceBeforeInsert, fields, Arrays.asList(dependencies));
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- 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 List<FieldDefinition> fields(FieldDefinition... fields) {
|
|
|
|
|
+ return Arrays.asList(fields);
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- 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 FieldDefinition text(String name, String label, String sqlType, int maxLength,
|
|
|
|
|
+ boolean required, boolean primaryKey, boolean autoIncrement, String... aliases) {
|
|
|
|
|
+ return new FieldDefinition(name, label, sqlType, "text", maxLength, required, primaryKey, autoIncrement, aliases);
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- 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 FieldDefinition integer(String name, String label, String sqlType,
|
|
|
|
|
+ boolean required, boolean primaryKey, boolean autoIncrement, String... aliases) {
|
|
|
|
|
+ return new FieldDefinition(name, label, sqlType, "int", 0, required, primaryKey, autoIncrement, aliases);
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- 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 FieldDefinition decimal(String name, String label, String sqlType,
|
|
|
|
|
+ boolean required, boolean primaryKey, boolean autoIncrement, String... aliases) {
|
|
|
|
|
+ return new FieldDefinition(name, label, sqlType, "decimal", 0, required, primaryKey, autoIncrement, aliases);
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- 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 FieldDefinition dateField(String name, String label, String sqlType,
|
|
|
|
|
+ boolean required, boolean primaryKey, boolean autoIncrement, String... aliases) {
|
|
|
|
|
+ return new FieldDefinition(name, label, sqlType, "date", 0, required, primaryKey, autoIncrement, aliases);
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- 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 FieldDefinition datetime(String name, String label, String sqlType,
|
|
|
|
|
+ boolean required, boolean primaryKey, boolean autoIncrement, String... aliases) {
|
|
|
|
|
+ return new FieldDefinition(name, label, sqlType, "datetime", 0, required, primaryKey, autoIncrement, aliases);
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- private static Date date(Map<String, String> row, String key) {
|
|
|
|
|
- String value = value(row, key);
|
|
|
|
|
- if (StringUtils.isEmpty(value)) {
|
|
|
|
|
- return null;
|
|
|
|
|
|
|
+ private String joinFieldLabels(List<FieldRequirement> fields) {
|
|
|
|
|
+ List<String> labels = new ArrayList<>();
|
|
|
|
|
+ for (FieldRequirement field : fields) {
|
|
|
|
|
+ labels.add(field.label + "(" + field.name + ")");
|
|
|
}
|
|
}
|
|
|
- return Date.valueOf(parseDate(value));
|
|
|
|
|
|
|
+ return String.join("、", labels);
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- 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());
|
|
|
|
|
|
|
+ private List<String> normalizeHeaders(List<String> headers) {
|
|
|
|
|
+ List<String> result = new ArrayList<>();
|
|
|
|
|
+ if (headers == null) {
|
|
|
|
|
+ return result;
|
|
|
}
|
|
}
|
|
|
- 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) {
|
|
|
|
|
- }
|
|
|
|
|
|
|
+ for (String header : headers) {
|
|
|
|
|
+ result.add(normalizeHeader(header));
|
|
|
}
|
|
}
|
|
|
- throw new IllegalArgumentException("无法解析时间: " + value);
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- private static LocalDate parseDate(String value) {
|
|
|
|
|
- String normalized = value.trim().replace('/', '-');
|
|
|
|
|
- return LocalDate.parse(normalized, DateTimeFormatter.ofPattern("yyyy-M-d"));
|
|
|
|
|
|
|
+ return result;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- private static String value(Map<String, String> row, String key) {
|
|
|
|
|
- return clean(row.get(key.toLowerCase(Locale.ROOT)));
|
|
|
|
|
|
|
+ private static String normalizeHeader(String header) {
|
|
|
|
|
+ return clean(header).toLowerCase(Locale.ROOT);
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
private static String clean(Object value) {
|
|
private static String clean(Object value) {
|
|
@@ -522,7 +669,7 @@ public class PreparedDataUploadService {
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
private static String limit(String value, int maxLength) {
|
|
private static String limit(String value, int maxLength) {
|
|
|
- if (value == null || value.length() <= maxLength) {
|
|
|
|
|
|
|
+ if (value == null || maxLength <= 0 || value.length() <= maxLength) {
|
|
|
return value;
|
|
return value;
|
|
|
}
|
|
}
|
|
|
return value.substring(0, maxLength);
|
|
return value.substring(0, maxLength);
|
|
@@ -536,12 +683,27 @@ public class PreparedDataUploadService {
|
|
|
return idx < 0 ? "" : fileName.substring(idx).toLowerCase(Locale.ROOT);
|
|
return idx < 0 ? "" : fileName.substring(idx).toLowerCase(Locale.ROOT);
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- private interface UploadWriter {
|
|
|
|
|
- int write(List<Map<String, String>> rows);
|
|
|
|
|
|
|
+ private static LocalDate parseDate(String value) {
|
|
|
|
|
+ String normalized = value.trim().replace('/', '-');
|
|
|
|
|
+ return LocalDate.parse(normalized, DateTimeFormatter.ofPattern("yyyy-M-d"));
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- private interface RowMapper<T> {
|
|
|
|
|
- T map(Map<String, String> row);
|
|
|
|
|
|
|
+ private static Timestamp parseTimestamp(String value) {
|
|
|
|
|
+ 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 class FileRows {
|
|
private static class FileRows {
|
|
@@ -572,20 +734,89 @@ public class PreparedDataUploadService {
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+ private static class FieldDefinition {
|
|
|
|
|
+ private final String name;
|
|
|
|
|
+ private final String label;
|
|
|
|
|
+ private final String sqlType;
|
|
|
|
|
+ private final String valueType;
|
|
|
|
|
+ private final int maxLength;
|
|
|
|
|
+ private final boolean required;
|
|
|
|
|
+ private final boolean primaryKey;
|
|
|
|
|
+ private final boolean autoIncrement;
|
|
|
|
|
+ private final List<String> aliases;
|
|
|
|
|
+
|
|
|
|
|
+ private FieldDefinition(String name, String label, String sqlType, String valueType, int maxLength,
|
|
|
|
|
+ boolean required, boolean primaryKey, boolean autoIncrement, String... aliases) {
|
|
|
|
|
+ this.name = name;
|
|
|
|
|
+ this.label = label;
|
|
|
|
|
+ this.sqlType = sqlType;
|
|
|
|
|
+ this.valueType = valueType;
|
|
|
|
|
+ this.maxLength = maxLength;
|
|
|
|
|
+ this.required = required;
|
|
|
|
|
+ this.primaryKey = primaryKey;
|
|
|
|
|
+ this.autoIncrement = autoIncrement;
|
|
|
|
|
+ this.aliases = new ArrayList<>();
|
|
|
|
|
+ this.aliases.add(name);
|
|
|
|
|
+ this.aliases.add(label);
|
|
|
|
|
+ this.aliases.addAll(Arrays.asList(aliases));
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private boolean matches(String header) {
|
|
|
|
|
+ String normalizedHeader = normalizeMatchText(header);
|
|
|
|
|
+ for (String alias : aliases) {
|
|
|
|
|
+ if (normalizeMatchText(alias).equals(normalizedHeader)) {
|
|
|
|
|
+ return true;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ return false;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private FieldRequirement requirement() {
|
|
|
|
|
+ return new FieldRequirement(name, label);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private static String normalizeMatchText(String value) {
|
|
|
|
|
+ return clean(value).toLowerCase(Locale.ROOT).replace("_", "").replace("-", "").replace(" ", "");
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
private static class UploadConfig {
|
|
private static class UploadConfig {
|
|
|
private final String key;
|
|
private final String key;
|
|
|
|
|
+ private final int order;
|
|
|
|
|
+ private final String tableName;
|
|
|
private final String label;
|
|
private final String label;
|
|
|
private final boolean replaceBeforeInsert;
|
|
private final boolean replaceBeforeInsert;
|
|
|
- private final List<FieldRequirement> requiredFields;
|
|
|
|
|
- private final UploadWriter writer;
|
|
|
|
|
|
|
+ private final List<FieldDefinition> fields;
|
|
|
|
|
+ private final List<String> dependencies;
|
|
|
|
|
|
|
|
- private UploadConfig(String key, String label, boolean replaceBeforeInsert,
|
|
|
|
|
- List<FieldRequirement> requiredFields, UploadWriter writer) {
|
|
|
|
|
|
|
+ private UploadConfig(String key, int order, String tableName, String label, boolean replaceBeforeInsert,
|
|
|
|
|
+ List<FieldDefinition> fields, List<String> dependencies) {
|
|
|
this.key = key;
|
|
this.key = key;
|
|
|
|
|
+ this.order = order;
|
|
|
|
|
+ this.tableName = tableName;
|
|
|
this.label = label;
|
|
this.label = label;
|
|
|
this.replaceBeforeInsert = replaceBeforeInsert;
|
|
this.replaceBeforeInsert = replaceBeforeInsert;
|
|
|
- this.requiredFields = requiredFields;
|
|
|
|
|
- this.writer = writer;
|
|
|
|
|
|
|
+ this.fields = fields;
|
|
|
|
|
+ this.dependencies = dependencies;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private static class MappingValidation {
|
|
|
|
|
+ private final Map<String, String> mappings;
|
|
|
|
|
+ private final List<FieldRequirement> missingFields;
|
|
|
|
|
+ private final List<String> duplicateHeaders;
|
|
|
|
|
+ private final List<Map<String, Object>> fieldStatuses;
|
|
|
|
|
+ private final boolean valid;
|
|
|
|
|
+
|
|
|
|
|
+ private MappingValidation(Map<String, String> mappings,
|
|
|
|
|
+ List<FieldRequirement> missingFields,
|
|
|
|
|
+ List<String> duplicateHeaders,
|
|
|
|
|
+ List<Map<String, Object>> fieldStatuses) {
|
|
|
|
|
+ this.mappings = mappings;
|
|
|
|
|
+ this.missingFields = missingFields;
|
|
|
|
|
+ this.duplicateHeaders = duplicateHeaders;
|
|
|
|
|
+ this.fieldStatuses = fieldStatuses;
|
|
|
|
|
+ this.valid = missingFields.isEmpty() && duplicateHeaders.isEmpty();
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|