feat(order): refine batch import workbook
This commit is contained in:
@@ -12,8 +12,18 @@ import com.baiyue.order.dto.OrderRequests;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import jakarta.annotation.Resource;
|
||||
import org.apache.poi.ss.usermodel.Cell;
|
||||
import org.apache.poi.ss.usermodel.CellStyle;
|
||||
import org.apache.poi.ss.usermodel.BorderStyle;
|
||||
import org.apache.poi.ss.usermodel.DataFormatter;
|
||||
import org.apache.poi.ss.usermodel.DataValidationConstraint;
|
||||
import org.apache.poi.ss.usermodel.DateUtil;
|
||||
import org.apache.poi.ss.usermodel.FillPatternType;
|
||||
import org.apache.poi.ss.usermodel.Font;
|
||||
import org.apache.poi.ss.usermodel.HorizontalAlignment;
|
||||
import org.apache.poi.ss.usermodel.IndexedColors;
|
||||
import org.apache.poi.ss.usermodel.Row;
|
||||
import org.apache.poi.ss.usermodel.VerticalAlignment;
|
||||
import org.apache.poi.ss.util.CellRangeAddress;
|
||||
import org.apache.poi.ss.util.CellRangeAddressList;
|
||||
import org.apache.poi.xssf.usermodel.XSSFWorkbook;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
@@ -25,9 +35,15 @@ import java.io.ByteArrayInputStream;
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.security.MessageDigest;
|
||||
import java.time.LocalDate;
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.time.format.DateTimeParseException;
|
||||
import java.time.format.ResolverStyle;
|
||||
import java.util.ArrayList;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
import java.util.function.Function;
|
||||
|
||||
@@ -44,24 +60,48 @@ public class MerchantBatchImportService {
|
||||
@Resource private ObjectMapper json;
|
||||
@Value("${baiyue.security.internal-service-token:}") private String token;
|
||||
private static final List<Column> COLUMNS = List.of(
|
||||
new Column("客户行号", "clientRowId", null), new Column("客户名称", "customerName", null),
|
||||
new Column("运输方式", "transportMode", "transport"), new Column("贸易术语", "tradeTerm", null),
|
||||
new Column("目的国家代码", "destinationCountryCode", null), new Column("目的仓库", "$destinationWarehouse", "destinationWarehouses"),
|
||||
new Column("预计出运日期", "expectedShipDate", null), new Column("客户备注", "customerRemark", null),
|
||||
new Column("提货方式", "pickupMethod", "pickup"), new Column("预约提货时间", "requestedPickupAt", null),
|
||||
new Column("取货地址", "$senderAddress", "pickupAddresses"), new Column("国内收货仓", "$receivingWarehouse", "receivingWarehouses"),
|
||||
new Column("运输渠道", "$channel", "channels"), new Column("报关方式", "taxMode", "tax"),
|
||||
new Column("计价税模式", "pricingTaxMode", "pricingTax"), new Column("装箱方式", "containerMode", "container"),
|
||||
new Column("是否需要商检", "customsInspection", "boolean"), new Column("目的类型", "destinationType", "destination"),
|
||||
new Column("私人目的仓地址", "$privateAddress", "privateAddresses"), new Column("亚马逊仓库代码", "amazonWarehouseCode", null),
|
||||
new Column("货件编号", "shipmentId", null), new Column("参考编号", "referenceId", null),
|
||||
new Column("履约类型", "fulfillmentType", "fulfillment"), new Column("货物名称", "cargo.cargoName", null),
|
||||
new Column("包装类型", "cargo.packageType", "package"), new Column("包装数量", "cargo.packageCount", null),
|
||||
new Column("毛重(千克)", "cargo.grossWeightKg", null), new Column("体积(立方米)", "cargo.volumeCbm", null),
|
||||
new Column("材质", "cargo.material", null), new Column("是否木制品", "cargo.woodenProduct", "boolean"),
|
||||
new Column("是否危险品", "cargo.dangerousGoods", "boolean"), new Column("SKU", "cargo.skuNo", null),
|
||||
new Column("UPC", "cargo.upcNo", null), new Column("货物货件编号", "cargo.shipmentId", null),
|
||||
new Column("货物参考编号", "cargo.referenceId", null));
|
||||
col("订单基础", "客户行号", "clientRowId", null, true, "每行唯一,用于错误定位", 16),
|
||||
col("订单基础", "客户名称(企业名称)", "customerName", null, true, "填写当前下单企业的正式名称,系统不会猜测或自动替换", 24),
|
||||
col("订单基础", "运输方式", "transportMode", "transport", true, "始终填写,从下拉选择", 14),
|
||||
col("订单基础", "贸易术语", "tradeTerm", null, false, "有约定时填写,如FOB、EXW", 14),
|
||||
col("订单基础", "预计出运日期", "expectedShipDate", null, false, "有预计日期时填写,格式yyyy-mm-dd", 18),
|
||||
col("订单基础", "客户备注", "customerRemark", null, false, "有补充说明时填写", 24),
|
||||
col("目的地与仓库", "目的类型", "destinationType", "destination", true, "始终填写,并与对应目的仓字段保持一致", 16),
|
||||
col("目的地与仓库", "FBA目的仓", "$fbaWarehouse", "fbaWarehouses", false, "目的类型为亚马逊FBA时必填", 22),
|
||||
col("目的地与仓库", "海外仓目的仓", "$overseasWarehouse", "overseasWarehouses", false, "目的类型为海外仓时必填", 24),
|
||||
col("目的地与仓库", "私人目的仓地址", "$privateAddress", "privateAddresses", false, "目的类型为私人地址时必填", 32),
|
||||
col("国内提货与运输", "提货方式", "pickupMethod", "pickup", true, "始终填写,从下拉选择", 18),
|
||||
col("国内提货与运输", "预约提货时间", "requestedPickupAt", null, false, "提货方式为平台上门提货时填写,格式yyyy-mm-dd hh:mm:ss", 23),
|
||||
col("国内提货与运输", "取货地址", "$senderAddress", "pickupAddresses", false, "提货方式为平台上门提货时必填", 32),
|
||||
col("国内提货与运输", "国内收货仓", "$receivingWarehouse", "receivingWarehouses", true, "始终填写,从启用的国内仓选择", 24),
|
||||
col("国内提货与运输", "运输渠道", "$channel", "channels", true, "始终填写;导入时校验与国内收货仓的组合", 24),
|
||||
col("报关与计价", "报关方式", "taxMode", "tax", true, "始终填写", 16),
|
||||
col("报关与计价", "计价税模式", "pricingTaxMode", "pricingTax", true, "始终填写", 16),
|
||||
col("报关与计价", "装箱方式", "containerMode", "container", false, "已确定整柜或散货拼箱时填写", 16),
|
||||
col("报关与计价", "是否需要商检", "customsInspection", "boolean", false, "木制品或业务要求商检时填写", 18),
|
||||
col("FBA业务信息", "货件编号", "shipmentId", null, false, "FBA业务有货件编号时填写", 18),
|
||||
col("FBA业务信息", "参考编号", "referenceId", null, false, "FBA业务有参考编号时填写", 18),
|
||||
col("FBA业务信息", "履约类型", "fulfillmentType", "fulfillment", false, "未填写时按普通头程处理", 16),
|
||||
col("货物信息", "货物名称", "cargo.cargoName", null, true, "填写本行订单的货物名称", 22),
|
||||
col("货物信息", "包装类型", "cargo.packageType", "package", true, "始终填写,从下拉选择", 16),
|
||||
col("货物信息", "包装数量", "cargo.packageCount", null, true, "正整数", 14),
|
||||
col("货物信息", "毛重(千克)", "cargo.grossWeightKg", null, true, "填写大于0的数字", 16),
|
||||
col("货物信息", "体积(立方米)", "cargo.volumeCbm", null, true, "填写大于0的数字", 18),
|
||||
col("货物信息", "材质", "cargo.material", null, false, "按实际材质填写", 18),
|
||||
col("货物信息", "是否木制品", "cargo.woodenProduct", "boolean", true, "始终填写", 16),
|
||||
col("货物信息", "是否危险品", "cargo.dangerousGoods", "boolean", true, "始终填写", 16),
|
||||
col("货物信息", "SKU", "cargo.skuNo", null, false, "有SKU时填写", 16),
|
||||
col("货物信息", "UPC", "cargo.upcNo", null, false, "有UPC时填写", 16),
|
||||
col("货物信息", "货物货件编号", "cargo.shipmentId", null, false, "货物级货件编号,有则填写", 20),
|
||||
col("货物信息", "货物参考编号", "cargo.referenceId", null, false, "货物级参考编号,有则填写", 20));
|
||||
private static final int DATA_START_ROW = 2;
|
||||
private static final int EXPECTED_SHIP_DATE_COLUMN = 4;
|
||||
private static final int REQUESTED_PICKUP_AT_COLUMN = 11;
|
||||
private static final ThreadLocal<DataFormatter> DATA_FORMATTER =
|
||||
ThreadLocal.withInitial(() -> new DataFormatter(Locale.SIMPLIFIED_CHINESE));
|
||||
private static final DateTimeFormatter DATE = DateTimeFormatter.ISO_LOCAL_DATE;
|
||||
private static final DateTimeFormatter DATE_TIME =
|
||||
DateTimeFormatter.ofPattern("uuuu-MM-dd HH:mm:ss").withResolverStyle(ResolverStyle.STRICT);
|
||||
private static final Map<String, List<String>> STATIC_OPTIONS = Map.of(
|
||||
"transport", List.of("海运", "空运", "铁路", "卡车"),
|
||||
"pickup", List.of("自行快递", "平台上门提货", "自行送仓"),
|
||||
@@ -82,17 +122,30 @@ public class MerchantBatchImportService {
|
||||
public byte[] template(long tenantId, String authorization) {
|
||||
Catalog catalog = catalog(tenantId, authorization);
|
||||
try (var workbook = new XSSFWorkbook(); var out = new ByteArrayOutputStream()) {
|
||||
workbook.getFontAt(0).setFontName("Microsoft YaHei");
|
||||
var sheet = workbook.createSheet("订单导入");
|
||||
Row header = sheet.createRow(0);
|
||||
for (int i = 0; i < COLUMNS.size(); i++) header.createCell(i).setCellValue(COLUMNS.get(i).title());
|
||||
sheet.createFreezePane(0, 1);
|
||||
for (int i = 0; i < COLUMNS.size(); i++) sheet.setColumnWidth(i, 20 * 256);
|
||||
createHeaders(workbook, sheet);
|
||||
CellStyle baseStyle = workbook.createCellStyle();
|
||||
baseStyle.setVerticalAlignment(VerticalAlignment.CENTER);
|
||||
CellStyle dateStyle = workbook.createCellStyle(); dateStyle.cloneStyleFrom(baseStyle);
|
||||
dateStyle.setDataFormat(workbook.createDataFormat().getFormat("yyyy-mm-dd"));
|
||||
CellStyle dateTimeStyle = workbook.createCellStyle(); dateTimeStyle.cloneStyleFrom(baseStyle);
|
||||
dateTimeStyle.setDataFormat(workbook.createDataFormat().getFormat("yyyy-mm-dd hh:mm:ss"));
|
||||
for (int rowNo = DATA_START_ROW; rowNo < DATA_START_ROW + MAX_ROWS; rowNo++) {
|
||||
Row row = sheet.createRow(rowNo); row.setHeightInPoints(22);
|
||||
for (int col = 0; col < COLUMNS.size(); col++) {
|
||||
Cell cell = row.createCell(col);
|
||||
cell.setCellStyle(col == EXPECTED_SHIP_DATE_COLUMN ? dateStyle
|
||||
: col == REQUESTED_PICKUP_AT_COLUMN ? dateTimeStyle : baseStyle);
|
||||
}
|
||||
}
|
||||
var options = workbook.createSheet("选项数据");
|
||||
Map<String, List<String>> optionValues = new LinkedHashMap<>(STATIC_OPTIONS);
|
||||
optionValues.put("pickupAddresses", catalog.pickupAddresses().keySet().stream().toList());
|
||||
optionValues.put("privateAddresses", catalog.privateAddresses().keySet().stream().toList());
|
||||
optionValues.put("receivingWarehouses", catalog.receivingWarehouses().keySet().stream().toList());
|
||||
optionValues.put("destinationWarehouses", catalog.destinationWarehouses().keySet().stream().toList());
|
||||
optionValues.put("fbaWarehouses", catalog.fbaWarehouses().keySet().stream().toList());
|
||||
optionValues.put("overseasWarehouses", catalog.overseasWarehouses().keySet().stream().toList());
|
||||
optionValues.put("channels", catalog.channels().keySet().stream().toList());
|
||||
int optionCol = 0;
|
||||
Map<String, String> ranges = new LinkedHashMap<>();
|
||||
@@ -112,17 +165,15 @@ public class MerchantBatchImportService {
|
||||
String range = ranges.get(COLUMNS.get(col).optionKey());
|
||||
if (range == null) continue;
|
||||
DataValidationConstraint constraint = helper.createFormulaListConstraint(range);
|
||||
var validation = helper.createValidation(constraint, new CellRangeAddressList(1, MAX_ROWS, col, col));
|
||||
var validation = helper.createValidation(constraint,
|
||||
new CellRangeAddressList(DATA_START_ROW, DATA_START_ROW + MAX_ROWS - 1, col, col));
|
||||
validation.setShowErrorBox(true); validation.createErrorBox("选项不正确", "请从下拉列表中选择");
|
||||
sheet.addValidationData(validation);
|
||||
}
|
||||
addDateValidation(helper, sheet, EXPECTED_SHIP_DATE_COLUMN, "请输入有效日期(yyyy-mm-dd)");
|
||||
addDateValidation(helper, sheet, REQUESTED_PICKUP_AT_COLUMN, "请输入有效日期时间(yyyy-mm-dd hh:mm:ss)");
|
||||
workbook.setSheetHidden(workbook.getSheetIndex(options), true);
|
||||
var guide = workbook.createSheet("填写说明");
|
||||
String[] guides = {"每一行代表一张订单,最多填写50行。", "客户行号由您自定义且在文件内唯一,用于定位错误行。",
|
||||
"带下拉的字段必须从列表选择;系统导入时会重新核验仓库、渠道和地址的状态及客户归属。",
|
||||
"日期格式:yyyy-MM-dd;预约提货时间格式:yyyy-MM-dd HH:mm:ss。", "本说明页不会被导入,请勿在订单导入页放置示例行。"};
|
||||
for (int i = 0; i < guides.length; i++) guide.createRow(i).createCell(0).setCellValue(guides[i]);
|
||||
guide.setColumnWidth(0, 100 * 256);
|
||||
createGuide(workbook);
|
||||
workbook.write(out);
|
||||
return out.toByteArray();
|
||||
} catch (Exception exception) {
|
||||
@@ -130,6 +181,97 @@ public class MerchantBatchImportService {
|
||||
}
|
||||
}
|
||||
|
||||
private static void createHeaders(XSSFWorkbook workbook, org.apache.poi.ss.usermodel.Sheet sheet) {
|
||||
Font groupFont = workbook.createFont(); groupFont.setFontName("Microsoft YaHei");
|
||||
groupFont.setBold(true); groupFont.setColor(IndexedColors.WHITE.getIndex());
|
||||
CellStyle groupStyle = workbook.createCellStyle(); groupStyle.setFont(groupFont);
|
||||
groupStyle.setFillForegroundColor(IndexedColors.DARK_BLUE.getIndex()); groupStyle.setFillPattern(FillPatternType.SOLID_FOREGROUND);
|
||||
groupStyle.setAlignment(HorizontalAlignment.CENTER); groupStyle.setVerticalAlignment(VerticalAlignment.CENTER);
|
||||
groupStyle.setBorderBottom(BorderStyle.THIN); groupStyle.setBorderRight(BorderStyle.THIN);
|
||||
Font headerFont = workbook.createFont(); headerFont.setFontName("Microsoft YaHei");
|
||||
headerFont.setBold(true); headerFont.setColor(IndexedColors.DARK_BLUE.getIndex());
|
||||
CellStyle headerStyle = workbook.createCellStyle(); headerStyle.setFont(headerFont);
|
||||
headerStyle.setFillForegroundColor(IndexedColors.PALE_BLUE.getIndex()); headerStyle.setFillPattern(FillPatternType.SOLID_FOREGROUND);
|
||||
headerStyle.setAlignment(HorizontalAlignment.CENTER); headerStyle.setVerticalAlignment(VerticalAlignment.CENTER);
|
||||
headerStyle.setWrapText(true); headerStyle.setBorderBottom(BorderStyle.THIN); headerStyle.setBorderRight(BorderStyle.THIN);
|
||||
|
||||
Row groupRow = sheet.createRow(0); groupRow.setHeightInPoints(26);
|
||||
Row headerRow = sheet.createRow(1); headerRow.setHeightInPoints(38);
|
||||
var drawing = sheet.createDrawingPatriarch();
|
||||
var creation = workbook.getCreationHelper();
|
||||
int start = 0;
|
||||
while (start < COLUMNS.size()) {
|
||||
String group = COLUMNS.get(start).group();
|
||||
int end = start;
|
||||
while (end + 1 < COLUMNS.size() && group.equals(COLUMNS.get(end + 1).group())) end++;
|
||||
if (end > start) sheet.addMergedRegion(new CellRangeAddress(0, 0, start, end));
|
||||
for (int col = start; col <= end; col++) {
|
||||
Cell cell = groupRow.createCell(col); cell.setCellStyle(groupStyle);
|
||||
if (col == start) cell.setCellValue(group);
|
||||
}
|
||||
start = end + 1;
|
||||
}
|
||||
for (int col = 0; col < COLUMNS.size(); col++) {
|
||||
Column column = COLUMNS.get(col);
|
||||
Cell cell = headerRow.createCell(col); cell.setCellValue(column.displayTitle()); cell.setCellStyle(headerStyle);
|
||||
var anchor = creation.createClientAnchor();
|
||||
anchor.setCol1(col); anchor.setCol2(Math.min(COLUMNS.size(), col + 3)); anchor.setRow1(1); anchor.setRow2(5);
|
||||
var comment = drawing.createCellComment(anchor);
|
||||
comment.setString(creation.createRichTextString(column.instruction())); comment.setAuthor("订单服务");
|
||||
cell.setCellComment(comment);
|
||||
sheet.setColumnWidth(col, Math.min(60, column.width()) * 256);
|
||||
}
|
||||
sheet.createFreezePane(1, 2);
|
||||
sheet.setAutoFilter(new CellRangeAddress(1, 1, 0, COLUMNS.size() - 1));
|
||||
}
|
||||
|
||||
private static void addDateValidation(org.apache.poi.ss.usermodel.DataValidationHelper helper,
|
||||
org.apache.poi.ss.usermodel.Sheet sheet, int column, String message) {
|
||||
var constraint = helper.createDateConstraint(DataValidationConstraint.OperatorType.BETWEEN,
|
||||
"DATE(2000,1,1)", "DATE(2100,12,31)", "yyyy-mm-dd");
|
||||
var validation = helper.createValidation(constraint,
|
||||
new CellRangeAddressList(DATA_START_ROW, DATA_START_ROW + MAX_ROWS - 1, column, column));
|
||||
validation.setEmptyCellAllowed(true); validation.setShowErrorBox(true);
|
||||
validation.createErrorBox("日期格式不正确", message); sheet.addValidationData(validation);
|
||||
}
|
||||
|
||||
private static void createGuide(XSSFWorkbook workbook) {
|
||||
var guide = workbook.createSheet("填写说明");
|
||||
Font titleFont = workbook.createFont(); titleFont.setFontName("Microsoft YaHei");
|
||||
titleFont.setBold(true); titleFont.setFontHeightInPoints((short) 14);
|
||||
CellStyle titleStyle = workbook.createCellStyle(); titleStyle.setFont(titleFont);
|
||||
guide.createRow(0).createCell(0).setCellValue("头程订单批量导入填写说明");
|
||||
guide.getRow(0).getCell(0).setCellStyle(titleStyle);
|
||||
guide.createRow(1).createCell(0).setCellValue("订单导入页每行代表一张订单,最多50行;示例不会放入导入区域。动态选项在导入时按当前客户重新校验。");
|
||||
Row header = guide.createRow(3);
|
||||
String[] titles = {"字段", "什么时候填写 / 说明", "格式", "是否必填"};
|
||||
for (int col = 0; col < titles.length; col++) header.createCell(col).setCellValue(titles[col]);
|
||||
int rowNo = 4;
|
||||
for (Column column : COLUMNS) {
|
||||
Row row = guide.createRow(rowNo++);
|
||||
row.createCell(0).setCellValue(column.title()); row.createCell(1).setCellValue(column.instruction());
|
||||
row.createCell(2).setCellValue(formatDescription(column));
|
||||
row.createCell(3).setCellValue(column.required() ? "必填" : "按条件/选填");
|
||||
}
|
||||
guide.setColumnWidth(0, 26 * 256); guide.setColumnWidth(1, 62 * 256);
|
||||
guide.setColumnWidth(2, 28 * 256); guide.setColumnWidth(3, 16 * 256);
|
||||
guide.createFreezePane(0, 4); guide.setAutoFilter(new CellRangeAddress(3, 3, 0, 3));
|
||||
}
|
||||
|
||||
private static String formatDescription(Column column) {
|
||||
if ("expectedShipDate".equals(column.field())) return "日期:yyyy-mm-dd";
|
||||
if ("requestedPickupAt".equals(column.field())) return "日期时间:yyyy-mm-dd hh:mm:ss";
|
||||
if (column.optionKey() != null) return "从下拉列表选择";
|
||||
if (column.field().contains("Count")) return "正整数";
|
||||
if (column.field().contains("Weight") || column.field().contains("volume")) return "数字";
|
||||
return "文本";
|
||||
}
|
||||
|
||||
private static Column col(String group, String title, String field, String optionKey, boolean required,
|
||||
String instruction, int width) {
|
||||
return new Column(group, title, field, optionKey, required, instruction, width);
|
||||
}
|
||||
|
||||
public MerchantBatchImportDtos.Response importFile(long tenantId, long userId, String username, String authorization,
|
||||
String idempotencyKey, MerchantBatchImportDtos.Mode mode,
|
||||
MultipartFile file) {
|
||||
@@ -154,16 +296,16 @@ public class MerchantBatchImportService {
|
||||
Catalog catalog = catalog(tenantId, authorization);
|
||||
try (var workbook = new XSSFWorkbook(new ByteArrayInputStream(content))) {
|
||||
var sheet = workbook.getSheetAt(0);
|
||||
if (sheet.getLastRowNum() < 1) throw new BizException(ResultCode.VALIDATION_FAILED, "Excel 没有订单数据行");
|
||||
if (sheet.getLastRowNum() < DATA_START_ROW) throw new BizException(ResultCode.VALIDATION_FAILED, "Excel 没有订单数据行");
|
||||
List<MerchantBatchDraftDtos.Item> items = new ArrayList<>();
|
||||
Row header = sheet.getRow(0);
|
||||
if (header == null || !COLUMNS.get(0).title().equals(text(header.getCell(0)))) {
|
||||
Row header = sheet.getRow(1);
|
||||
if (header == null || !COLUMNS.get(0).displayTitle().equals(text(header.getCell(0)))) {
|
||||
throw new BizException(ResultCode.VALIDATION_FAILED, "Excel 模板表头不正确");
|
||||
}
|
||||
if (header.getLastCellNum() != COLUMNS.size()) throw new BizException(ResultCode.VALIDATION_FAILED, "Excel 模板表头不正确");
|
||||
for (int col = 0; col < COLUMNS.size(); col++) if (!COLUMNS.get(col).title().equals(text(header.getCell(col))))
|
||||
for (int col = 0; col < COLUMNS.size(); col++) if (!COLUMNS.get(col).displayTitle().equals(text(header.getCell(col))))
|
||||
throw new BizException(ResultCode.VALIDATION_FAILED, "Excel 模板表头不正确");
|
||||
for (int rowNo = 1; rowNo <= sheet.getLastRowNum(); rowNo++) {
|
||||
for (int rowNo = DATA_START_ROW; rowNo <= sheet.getLastRowNum(); rowNo++) {
|
||||
Row row = sheet.getRow(rowNo);
|
||||
if (row == null || !StringUtils.hasText(text(row.getCell(0)))) continue;
|
||||
if (items.size() >= MAX_ROWS) throw new BizException(ResultCode.VALIDATION_FAILED, "Excel 最多 50 条订单");
|
||||
@@ -171,14 +313,26 @@ public class MerchantBatchImportService {
|
||||
try {
|
||||
var draft = json.createObjectNode();
|
||||
var cargo = json.createObjectNode();
|
||||
boolean fbaSelected = false;
|
||||
boolean overseasSelected = false;
|
||||
boolean privateSelected = false;
|
||||
for (int col = 1; col < COLUMNS.size(); col++) {
|
||||
String field = COLUMNS.get(col).field(); String value = text(row.getCell(col));
|
||||
String field = COLUMNS.get(col).field();
|
||||
String value = value(row.getCell(col), field);
|
||||
if (!StringUtils.hasText(field) || !StringUtils.hasText(value)) continue;
|
||||
value = VALUE_CODES.getOrDefault(value, value);
|
||||
if (field.startsWith("$")) { applySelection(draft, field, value, catalog); continue; }
|
||||
if (field.startsWith("$")) {
|
||||
applySelection(draft, field, value, catalog);
|
||||
fbaSelected |= "$fbaWarehouse".equals(field);
|
||||
overseasSelected |= "$overseasWarehouse".equals(field);
|
||||
privateSelected |= "$privateAddress".equals(field);
|
||||
continue;
|
||||
}
|
||||
if (field.startsWith("cargo.")) cargo.put(field.substring("cargo.".length()), value);
|
||||
else draft.put(field, value);
|
||||
}
|
||||
validateDestination(draft.path("destinationType").asText(null),
|
||||
fbaSelected, overseasSelected, privateSelected);
|
||||
draft.set("cargoes", json.createArrayNode().add(cargo));
|
||||
items.add(new MerchantBatchDraftDtos.Item(rowId,
|
||||
json.treeToValue(draft, OrderRequests.SaveRequest.class)));
|
||||
@@ -203,7 +357,7 @@ public class MerchantBatchImportService {
|
||||
int rowNo = 1;
|
||||
for (var item : result.items()) if (!item.success()) {
|
||||
Row row = sheet.createRow(rowNo++);
|
||||
row.createCell(0).setCellValue(item.index() + 2);
|
||||
row.createCell(0).setCellValue(item.index() + DATA_START_ROW + 1);
|
||||
row.createCell(1).setCellValue(item.clientRowId());
|
||||
row.createCell(2).setCellValue(item.errorCode());
|
||||
row.createCell(3).setCellValue(item.errorMessage());
|
||||
@@ -221,7 +375,9 @@ public class MerchantBatchImportService {
|
||||
var receiving = optionMap(allWarehouses.stream().filter(value -> "DOMESTIC".equals(value.warehouseType())).toList(),
|
||||
value -> label(value.warehouseName(), value.warehouseCode()));
|
||||
if (receiving.isEmpty()) throw new BizException(ResultCode.SERVICE_UNAVAILABLE, "暂无可用国内收货仓");
|
||||
var destination = optionMap(allWarehouses.stream().filter(value -> !"DOMESTIC".equals(value.warehouseType())).toList(),
|
||||
var fba = optionMap(allWarehouses.stream().filter(value -> "FBA".equals(value.warehouseType())).toList(),
|
||||
value -> label(value.warehouseName(), value.warehouseCode()));
|
||||
var overseas = optionMap(allWarehouses.stream().filter(value -> "OVERSEAS".equals(value.warehouseType())).toList(),
|
||||
value -> label(value.warehouseName(), value.warehouseCode()));
|
||||
Map<String, MerchantPricingCatalogClient.Channel> channels = new LinkedHashMap<>();
|
||||
for (var warehouse : receiving.values()) {
|
||||
@@ -241,7 +397,7 @@ public class MerchantBatchImportService {
|
||||
return new Catalog(
|
||||
addressOptions(addresses.list(tenantId, "PICKUP")),
|
||||
addressOptions(addresses.list(tenantId, "PRIVATE_DESTINATION")),
|
||||
receiving, destination, channels);
|
||||
receiving, fba, overseas, channels);
|
||||
} catch (BizException exception) {
|
||||
throw exception;
|
||||
} catch (RuntimeException exception) {
|
||||
@@ -319,15 +475,19 @@ public class MerchantBatchImportService {
|
||||
String value, Catalog catalog) {
|
||||
switch (field) {
|
||||
case "$senderAddress" -> draft.put("senderAddressId", required(catalog.pickupAddresses(), value, "取货地址").id());
|
||||
case "$privateAddress" -> draft.put("privateDestinationAddressId", required(catalog.privateAddresses(), value, "私人目的仓地址").id());
|
||||
case "$privateAddress" -> {
|
||||
var address = required(catalog.privateAddresses(), value, "私人目的仓地址");
|
||||
draft.put("privateDestinationAddressId", address.id());
|
||||
draft.put("destinationCountryCode", address.countryCode());
|
||||
}
|
||||
case "$receivingWarehouse" -> {
|
||||
var warehouse = required(catalog.receivingWarehouses(), value, "国内收货仓");
|
||||
draft.put("receivingWarehouseId", warehouse.id()); draft.put("receivingWarehouseName", warehouse.warehouseName());
|
||||
}
|
||||
case "$destinationWarehouse" -> {
|
||||
var warehouse = required(catalog.destinationWarehouses(), value, "目的仓库");
|
||||
draft.put("destinationWarehouseId", warehouse.id()); draft.put("destinationWarehouseName", warehouse.warehouseName());
|
||||
}
|
||||
case "$fbaWarehouse" -> applyDestinationWarehouse(draft,
|
||||
required(catalog.fbaWarehouses(), value, "FBA目的仓"), true);
|
||||
case "$overseasWarehouse" -> applyDestinationWarehouse(draft,
|
||||
required(catalog.overseasWarehouses(), value, "海外仓目的仓"), false);
|
||||
case "$channel" -> {
|
||||
var channel = required(catalog.channels(), value, "运输渠道");
|
||||
draft.put("channelCode", channel.channelCode()); draft.put("channelName", channel.channelName());
|
||||
@@ -342,15 +502,44 @@ public class MerchantBatchImportService {
|
||||
return selected;
|
||||
}
|
||||
|
||||
private static String label(String name, String code) { return name + "|" + code; }
|
||||
private static void applyDestinationWarehouse(com.fasterxml.jackson.databind.node.ObjectNode draft,
|
||||
MerchantWarehouseCatalogClient.Warehouse warehouse,
|
||||
boolean fba) {
|
||||
draft.put("destinationWarehouseId", warehouse.id());
|
||||
draft.put("destinationWarehouseName", warehouse.warehouseName());
|
||||
draft.put("destinationCountryCode", warehouse.countryCode());
|
||||
if (fba) draft.put("amazonWarehouseCode", warehouse.warehouseCode());
|
||||
}
|
||||
|
||||
private static void validateDestination(String destinationType, boolean fba, boolean overseas, boolean privateAddress) {
|
||||
if ("FBA".equals(destinationType) && fba && !overseas && !privateAddress) return;
|
||||
if ("OVERSEAS".equals(destinationType) && overseas && !fba && !privateAddress) return;
|
||||
if ("PRIVATE".equals(destinationType) && privateAddress && !fba && !overseas) return;
|
||||
if (!StringUtils.hasText(destinationType)) throw new BizException(ResultCode.VALIDATION_FAILED, "请选择目的类型");
|
||||
throw new BizException(ResultCode.VALIDATION_FAILED, switch (destinationType) {
|
||||
case "FBA" -> "亚马逊FBA订单必须且只能选择FBA目的仓";
|
||||
case "OVERSEAS" -> "海外仓订单必须且只能选择海外仓目的仓";
|
||||
case "PRIVATE" -> "私人地址订单必须且只能选择私人目的仓地址";
|
||||
default -> "目的类型不正确";
|
||||
});
|
||||
}
|
||||
|
||||
private static String label(String name, String code) {
|
||||
if (!StringUtils.hasText(name)) return code;
|
||||
return name.equalsIgnoreCase(code) ? code : name + "|" + code;
|
||||
}
|
||||
private static String columnLetter(int column) { return String.valueOf((char) ('A' + column)); }
|
||||
|
||||
private record Column(String title, String field, String optionKey) { }
|
||||
private record Column(String group, String title, String field, String optionKey, boolean required,
|
||||
String instruction, int width) {
|
||||
String displayTitle() { return required ? title + " *" : title; }
|
||||
}
|
||||
private record Catalog(
|
||||
Map<String, CustomerAddressDtos.Response> pickupAddresses,
|
||||
Map<String, CustomerAddressDtos.Response> privateAddresses,
|
||||
Map<String, MerchantWarehouseCatalogClient.Warehouse> receivingWarehouses,
|
||||
Map<String, MerchantWarehouseCatalogClient.Warehouse> destinationWarehouses,
|
||||
Map<String, MerchantWarehouseCatalogClient.Warehouse> fbaWarehouses,
|
||||
Map<String, MerchantWarehouseCatalogClient.Warehouse> overseasWarehouses,
|
||||
Map<String, MerchantPricingCatalogClient.Channel> channels) { }
|
||||
|
||||
private Long upload(long tenantId, long userId, String batchNo, String type, MultipartFile file) {
|
||||
@@ -373,11 +562,31 @@ public class MerchantBatchImportService {
|
||||
|
||||
private static String text(Cell cell) {
|
||||
if (cell == null) return null;
|
||||
cell.setCellType(org.apache.poi.ss.usermodel.CellType.STRING);
|
||||
String value = cell.getStringCellValue();
|
||||
String value = DATA_FORMATTER.get().formatCellValue(cell);
|
||||
return value == null ? null : value.trim();
|
||||
}
|
||||
|
||||
private static String value(Cell cell, String field) {
|
||||
String formatted = text(cell);
|
||||
if (!StringUtils.hasText(formatted)) return null;
|
||||
boolean date = "expectedShipDate".equals(field);
|
||||
boolean dateTime = "requestedPickupAt".equals(field);
|
||||
if (!date && !dateTime) return formatted;
|
||||
try {
|
||||
if (cell.getCellType() == org.apache.poi.ss.usermodel.CellType.NUMERIC) {
|
||||
if (!DateUtil.isValidExcelDate(cell.getNumericCellValue())) throw new DateTimeParseException("invalid", formatted, 0);
|
||||
LocalDateTime excelValue = DateUtil.getLocalDateTime(cell.getNumericCellValue());
|
||||
return date ? DATE.format(excelValue.toLocalDate())
|
||||
: DateTimeFormatter.ISO_LOCAL_DATE_TIME.format(excelValue.withNano(0));
|
||||
}
|
||||
if (date) return DATE.format(LocalDate.parse(formatted, DATE));
|
||||
return DateTimeFormatter.ISO_LOCAL_DATE_TIME.format(LocalDateTime.parse(formatted, DATE_TIME));
|
||||
} catch (RuntimeException exception) {
|
||||
throw new BizException(ResultCode.VALIDATION_FAILED,
|
||||
date ? "预计出运日期格式必须为yyyy-mm-dd" : "预约提货时间格式必须为yyyy-mm-dd hh:mm:ss");
|
||||
}
|
||||
}
|
||||
|
||||
private static String safeName(String name) { return StringUtils.hasText(name) ? name : "order-batch.xlsx"; }
|
||||
private static String batchNo(long tenantId, long userId, String key) {
|
||||
if (!StringUtils.hasText(key)) throw new BizException(ResultCode.BAD_REQUEST, "必须提供 Idempotency-Key");
|
||||
|
||||
@@ -71,8 +71,10 @@ paths:
|
||||
get:
|
||||
summary: 下载 Excel 批量下单模板
|
||||
description: >-
|
||||
返回仅含中文业务列名的 xlsx。静态业务枚举使用中文下拉;仓库、渠道和客户地址按当前登录客户及正式主数据实时生成,
|
||||
工作表不暴露内部 ID。示例与填写规则位于独立说明页,不占用可导入数据行。导入时重新校验动态选项的状态、归属和组合有效性。
|
||||
返回两级中文业务表头的 xlsx,按订单基础、目的地与仓库、国内提货与运输、报关与计价、FBA业务信息、货物信息分组。
|
||||
静态业务枚举使用中文下拉;仓库按全部 ACTIVE 分页结果构建,FBA目的仓仅包含 FBA,海外仓目的仓仅包含 OVERSEAS,
|
||||
客户地址按当前客户生成。预计出运日期和预约提货时间提供日期格式及校验。工作表不暴露内部 ID,权威仓库选择自动推导目的国、
|
||||
目的仓 ID/名称及 FBA 仓库代码。示例与逐列填写规则位于独立说明页,不占用50行编辑区。
|
||||
responses:
|
||||
'200': {description: xlsx 模板}
|
||||
'403': {description: 需要 merchant:order:create 权限}
|
||||
@@ -83,6 +85,7 @@ paths:
|
||||
ATOMIC 在任一行失败时整批回滚;PARTIAL 每行独立事务,错误行不影响成功行。
|
||||
同一操作者重试必须复用 Idempotency-Key,每行再以 clientRowId 派生稳定幂等键。
|
||||
仅接受当前中文模板,中文下拉值由服务端映射为权威代码,不兼容历史英文技术表头。
|
||||
导入支持 Excel 原生日期值,并重新校验动态选项;FBA、OVERSEAS、PRIVATE 必须分别且只能填写对应目的仓字段。
|
||||
原文件和错误报告分别通过 File 的 ORDER_BATCH_IMPORT_SOURCE、ORDER_BATCH_IMPORT_ERROR_REPORT 契约保存。
|
||||
parameters:
|
||||
- $ref: '#/components/parameters/IdempotencyKey'
|
||||
|
||||
@@ -13,6 +13,8 @@ import org.springframework.test.util.ReflectionTestUtils;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.time.LocalDate;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
@@ -34,13 +36,14 @@ class MerchantBatchImportServiceTest {
|
||||
ReflectionTestUtils.setField(service, "addresses", addresses);
|
||||
ReflectionTestUtils.setField(service, "warehouses", warehouses);
|
||||
ReflectionTestUtils.setField(service, "pricing", pricing);
|
||||
ReflectionTestUtils.setField(service, "json", new ObjectMapper());
|
||||
ReflectionTestUtils.setField(service, "json", new ObjectMapper().findAndRegisterModules());
|
||||
when(addresses.list(88L, "PICKUP")).thenReturn(List.of(address(11L, "工厂", "PICKUP")));
|
||||
when(addresses.list(88L, "PRIVATE_DESTINATION")).thenReturn(List.of(address(12L, "美国门店", "PRIVATE_DESTINATION")));
|
||||
var domestic = new MerchantWarehouseCatalogClient.Warehouse(21L, "CN-YW", "义乌收货仓", "DOMESTIC", "CN", "ACTIVE");
|
||||
var overseas = new MerchantWarehouseCatalogClient.Warehouse(22L, "US-LAX", "洛杉矶海外仓", "OVERSEAS", "US", "ACTIVE");
|
||||
var fba = new MerchantWarehouseCatalogClient.Warehouse(22L, "ATL6", "ATL6", "FBA", "US", "ACTIVE");
|
||||
var overseas = new MerchantWarehouseCatalogClient.Warehouse(23L, "US-LAX", "洛杉矶海外仓", "OVERSEAS", "US", "ACTIVE");
|
||||
when(warehouses.page("Bearer test", 1, 200, "ACTIVE"))
|
||||
.thenReturn(ApiResponse.success(new PageResult<>(List.of(domestic, overseas), 1, 200, 2, 1)));
|
||||
.thenReturn(ApiResponse.success(new PageResult<>(List.of(domestic, fba, overseas), 1, 200, 3, 1)));
|
||||
when(pricing.channels("Bearer test", 21L)).thenReturn(ApiResponse.success(List.of(
|
||||
new MerchantPricingCatalogClient.Channel("US-SEA", "美森海运"))));
|
||||
}
|
||||
@@ -50,14 +53,27 @@ class MerchantBatchImportServiceTest {
|
||||
byte[] content = service.template(88L, "Bearer test");
|
||||
try (var workbook = new XSSFWorkbook(new ByteArrayInputStream(content))) {
|
||||
var sheet = workbook.getSheet("订单导入");
|
||||
assertThat(sheet.getRow(0).getCell(0).getStringCellValue()).isEqualTo("客户行号");
|
||||
assertThat(sheet.getRow(0).getCell(10).getStringCellValue()).isEqualTo("取货地址");
|
||||
assertThat(sheet.getRow(0).getCell(11).getStringCellValue()).isEqualTo("国内收货仓");
|
||||
assertThat(sheet.getRow(0).getCell(29).getStringCellValue()).isEqualTo("是否木制品");
|
||||
assertThat((Object) sheet.getRow(1)).isNull();
|
||||
assertThat(sheet.getRow(0).getCell(0).getStringCellValue()).isEqualTo("订单基础");
|
||||
assertThat(sheet.getRow(0).getCell(6).getStringCellValue()).isEqualTo("目的地与仓库");
|
||||
assertThat(sheet.getRow(1).getCell(0).getStringCellValue()).isEqualTo("客户行号 *");
|
||||
assertThat(sheet.getRow(1).getCell(6).getStringCellValue()).isEqualTo("目的类型 *");
|
||||
assertThat(sheet.getRow(1).getCell(7).getStringCellValue()).isEqualTo("FBA目的仓");
|
||||
assertThat(sheet.getRow(1).getCell(8).getStringCellValue()).isEqualTo("海外仓目的仓");
|
||||
assertThat(sheet.getRow(1).getLastCellNum()).isEqualTo((short) 34);
|
||||
assertThat(rowValues(sheet.getRow(1))).doesNotContain("目的国家代码", "目的仓库名称", "亚马逊仓库代码");
|
||||
assertThat(sheet.getRow(2).getCell(0).getStringCellValue()).isEmpty();
|
||||
assertThat(sheet.getRow(51).getCell(0).getStringCellValue()).isEmpty();
|
||||
assertThat(sheet.getRow(2).getCell(4).getCellStyle().getDataFormatString()).isEqualTo("yyyy-mm-dd");
|
||||
assertThat(sheet.getRow(2).getCell(11).getCellStyle().getDataFormatString()).isEqualTo("yyyy-mm-dd hh:mm:ss");
|
||||
assertThat(sheet.getPaneInformation().isFreezePane()).isTrue();
|
||||
assertThat(sheet.getMergedRegions()).hasSize(6);
|
||||
assertThat(sheet.getCTWorksheet().isSetAutoFilter()).isTrue();
|
||||
assertThat(sheet.getRow(1).getCell(0).getCellComment()).isNotNull();
|
||||
assertThat(sheet.getDataValidations()).hasSizeGreaterThanOrEqualTo(10);
|
||||
assertThat(workbook.getSheet("填写说明")).isNotNull();
|
||||
assertThat(workbook.isSheetHidden(workbook.getSheetIndex("选项数据"))).isTrue();
|
||||
assertThat(hasCellValue(workbook.getSheet("选项数据"), "ATL6")).isTrue();
|
||||
assertThat(hasCellValue(workbook.getSheet("选项数据"), "洛杉矶海外仓|US-LAX")).isTrue();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -65,16 +81,18 @@ class MerchantBatchImportServiceTest {
|
||||
void importMapsChineseAndBusinessLabelsToCodesAndAuthoritativeIds() throws Exception {
|
||||
byte[] content = service.template(88L, "Bearer test");
|
||||
try (var workbook = new XSSFWorkbook(new ByteArrayInputStream(content)); var out = new ByteArrayOutputStream()) {
|
||||
var row = workbook.getSheet("订单导入").createRow(1);
|
||||
var row = workbook.getSheet("订单导入").getRow(2);
|
||||
row.createCell(0).setCellValue("第1行"); row.createCell(1).setCellValue("测试客户");
|
||||
row.createCell(2).setCellValue("海运"); row.createCell(8).setCellValue("平台上门提货");
|
||||
row.createCell(10).setCellValue("工厂|张三|深圳|科技园1号");
|
||||
row.createCell(11).setCellValue("义乌收货仓|CN-YW"); row.createCell(12).setCellValue("美森海运|US-SEA");
|
||||
row.createCell(13).setCellValue("买单报关"); row.createCell(14).setCellValue("不包税");
|
||||
row.createCell(17).setCellValue("私人地址"); row.createCell(18).setCellValue("美国门店|张三|深圳|科技园1号");
|
||||
row.createCell(23).setCellValue("蓝牙音箱"); row.createCell(24).setCellValue("纸箱");
|
||||
row.createCell(25).setCellValue("1"); row.createCell(26).setCellValue("1.2"); row.createCell(27).setCellValue("0.1");
|
||||
row.createCell(29).setCellValue("否"); row.createCell(30).setCellValue("是");
|
||||
row.createCell(2).setCellValue("海运"); row.getCell(4).setCellValue(LocalDate.of(2026, 9, 20));
|
||||
row.createCell(6).setCellValue("亚马逊FBA"); row.createCell(7).setCellValue("ATL6");
|
||||
row.createCell(10).setCellValue("平台上门提货");
|
||||
row.getCell(11).setCellValue(LocalDateTime.of(2026, 9, 10, 14, 30, 0));
|
||||
row.createCell(12).setCellValue("工厂|张三|深圳|科技园1号");
|
||||
row.createCell(13).setCellValue("义乌收货仓|CN-YW"); row.createCell(14).setCellValue("美森海运|US-SEA");
|
||||
row.createCell(15).setCellValue("买单报关"); row.createCell(16).setCellValue("不包税");
|
||||
row.createCell(22).setCellValue("蓝牙音箱"); row.createCell(23).setCellValue("纸箱");
|
||||
row.createCell(24).setCellValue("1"); row.createCell(25).setCellValue("1.2"); row.createCell(26).setCellValue("0.1");
|
||||
row.createCell(28).setCellValue("否"); row.createCell(29).setCellValue("是");
|
||||
workbook.write(out);
|
||||
var item = service.parse(88L, "Bearer test", out.toByteArray()).get(0);
|
||||
assertThat(item.parseError()).isNull();
|
||||
@@ -84,11 +102,31 @@ class MerchantBatchImportServiceTest {
|
||||
assertThat(item.draft().receivingWarehouseId()).isEqualTo(21L);
|
||||
assertThat(item.draft().channelCode()).isEqualTo("US-SEA");
|
||||
assertThat(item.draft().taxMode()).isEqualTo("BUY_DECLARATION");
|
||||
assertThat(item.draft().destinationWarehouseId()).isEqualTo(22L);
|
||||
assertThat(item.draft().destinationWarehouseName()).isEqualTo("ATL6");
|
||||
assertThat(item.draft().destinationCountryCode()).isEqualTo("US");
|
||||
assertThat(item.draft().amazonWarehouseCode()).isEqualTo("ATL6");
|
||||
assertThat(item.draft().expectedShipDate()).isEqualTo(LocalDate.of(2026, 9, 20));
|
||||
assertThat(item.draft().requestedPickupAt()).isEqualTo(LocalDateTime.of(2026, 9, 10, 14, 30, 0));
|
||||
assertThat(item.draft().cargoes().get(0).packageType()).isEqualTo("CARTON");
|
||||
assertThat(item.draft().cargoes().get(0).dangerousGoods()).isTrue();
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void importRejectsOverseasWarehouseWhenDestinationTypeIsFba() throws Exception {
|
||||
byte[] content = service.template(88L, "Bearer test");
|
||||
try (var workbook = new XSSFWorkbook(new ByteArrayInputStream(content)); var out = new ByteArrayOutputStream()) {
|
||||
var row = workbook.getSheet("订单导入").getRow(2);
|
||||
row.createCell(0).setCellValue("第1行"); row.createCell(6).setCellValue("亚马逊FBA");
|
||||
row.createCell(8).setCellValue("洛杉矶海外仓|US-LAX");
|
||||
workbook.write(out);
|
||||
var item = service.parse(88L, "Bearer test", out.toByteArray()).get(0);
|
||||
assertThat(item.draft()).isNull();
|
||||
assertThat(item.parseError()).isEqualTo("亚马逊FBA订单必须且只能选择FBA目的仓");
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void templateReadsEveryActiveWarehousePageWithoutApplyingBusinessLimit() throws Exception {
|
||||
List<MerchantWarehouseCatalogClient.Warehouse> firstPage = new ArrayList<>();
|
||||
@@ -135,6 +173,12 @@ class MerchantBatchImportServiceTest {
|
||||
return false;
|
||||
}
|
||||
|
||||
private static List<String> rowValues(org.apache.poi.ss.usermodel.Row row) {
|
||||
List<String> values = new ArrayList<>();
|
||||
for (var cell : row) values.add(cell.getStringCellValue());
|
||||
return values;
|
||||
}
|
||||
|
||||
private static CustomerAddressDtos.Response address(long id, String name, String type) {
|
||||
return new CustomerAddressDtos.Response(id, type, name, "测试公司", "张三", "13800000000", null,
|
||||
"CN", "广东", "深圳", "南山", "科技园1号", "518000", true);
|
||||
|
||||
Reference in New Issue
Block a user