This commit is contained in:
2026-09-03 20:24:52 +08:00
parent acdc0943c0
commit a24d55f526
3 changed files with 145 additions and 2 deletions
@@ -323,6 +323,7 @@ public class MerchantBatchImportService {
if (items.size() >= MAX_ROWS) throw new BizException(ResultCode.VALIDATION_FAILED, "Excel 最多 50 条订单");
String rowId = "excel-row-" + (rowNo + 1);
try {
validateRequiredColumns(row);
var draft = json.createObjectNode();
draft.put("customerName", customerName);
draft.put("tradeTerm", "FOB");
@@ -347,6 +348,8 @@ public class MerchantBatchImportService {
}
validateDestination(draft.path("destinationType").asText(null),
fbaSelected, overseasSelected, privateSelected);
validatePickup(draft.path("pickupMethod").asText(null),
draft.hasNonNull("requestedPickupAt"), draft.hasNonNull("senderAddressId"));
draft.set("cargoes", json.createArrayNode().add(cargo));
items.add(new MerchantBatchDraftDtos.Item(rowId,
json.treeToValue(draft, OrderRequests.SaveRequest.class)));
@@ -496,6 +499,15 @@ public class MerchantBatchImportService {
return false;
}
private static void validateRequiredColumns(Row row) {
for (int col = 0; col < COLUMNS.size(); col++) {
Column column = COLUMNS.get(col);
if (column.required() && !StringUtils.hasText(text(row.getCell(col)))) {
throw new BizException(ResultCode.VALIDATION_FAILED, column.title() + "不能为空");
}
}
}
private static int excelRowNumber(String clientRowId) {
String prefix = "excel-row-";
if (!StringUtils.hasText(clientRowId) || !clientRowId.startsWith(prefix)) {
@@ -576,6 +588,16 @@ public class MerchantBatchImportService {
});
}
private static void validatePickup(String pickupMethod, boolean requestedPickupAt, boolean senderAddress) {
if (!"PICKUP".equals(pickupMethod)) return;
if (!requestedPickupAt) {
throw new BizException(ResultCode.VALIDATION_FAILED, "平台上门提货时预约提货时间不能为空");
}
if (!senderAddress) {
throw new BizException(ResultCode.VALIDATION_FAILED, "平台上门提货时取货地址不能为空");
}
}
private static String label(String name, String code) {
if (!StringUtils.hasText(name)) return code;
return name.equalsIgnoreCase(code) ? code : name + "|" + code;
@@ -62,6 +62,29 @@ class MerchantBatchDraftServiceTest {
assertThat(response.items().get(1).errorMessage()).isEqualTo("SKU不能为空");
}
@Test
void partialModeReturnsA0422WithoutCreatingAParsedInvalidRow() {
var response = service.create(10L, 20L, "batch-key", List.of(
new MerchantBatchDraftDtos.Item("excel-row-9", null, "货物名称不能为空")));
assertThat(response.succeeded()).isZero();
assertThat(response.failed()).isEqualTo(1);
assertThat(response.items().get(0).errorCode()).isEqualTo("A0422");
assertThat(response.items().get(0).errorMessage()).isEqualTo("货物名称不能为空");
verify(itemService, never()).create(anyLong(), anyLong(), isNull(), any(), anyString());
}
@Test
void atomicModeRejectsWholeBatchBeforeCreatingWhenOneParsedRowIsInvalid() {
assertThatThrownBy(() -> service.createAtomic(10L, 20L, null, "batch-key", List.of(
item("excel-row-3"),
new MerchantBatchDraftDtos.Item("excel-row-9", null, "货物名称不能为空"))))
.isInstanceOf(BizException.class)
.hasMessage("货物名称不能为空");
verify(itemService, never()).createJoined(anyLong(), anyLong(), isNull(), any(), anyString());
}
@Test
void rejectsDuplicateRowWithoutCreatingItTwice() {
doAnswer(invocation -> order(101L, "FL101", "RK-101"))
@@ -11,6 +11,9 @@ import com.fasterxml.jackson.databind.ObjectMapper;
import org.apache.poi.xssf.usermodel.XSSFWorkbook;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.Arguments;
import org.junit.jupiter.params.provider.MethodSource;
import org.springframework.test.util.ReflectionTestUtils;
import java.io.ByteArrayInputStream;
@@ -20,6 +23,7 @@ import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.List;
import java.util.Set;
import java.util.stream.Stream;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
@@ -132,13 +136,54 @@ class MerchantBatchImportServiceTest {
}
}
@ParameterizedTest(name = "required column {0} rejects blank cell")
@MethodSource("requiredColumns")
void importRejectsEveryRequiredTemplateColumnWhenItIsBlank(int column, String title) throws Exception {
byte[] content = service.template(88L, "Bearer test");
try (var workbook = new XSSFWorkbook(new ByteArrayInputStream(content)); var out = new ByteArrayOutputStream()) {
var sheet = workbook.getSheet("订单导入");
var row = sheet.getRow(2);
fillValidSelfExpressFbaRow(row);
row.getCell(column).setCellValue(" ");
assertThat(requiredHeaderIndexes(sheet)).containsExactlyElementsOf(
requiredColumns().map(arguments -> (Integer) arguments.get()[0]).toList());
workbook.write(out);
var item = service.parse(88L, "Bearer test", out.toByteArray()).get(0);
assertThat(item.draft()).isNull();
assertThat(item.parseError()).isEqualTo(title + "不能为空");
}
}
@ParameterizedTest(name = "PICKUP rejects missing {0}")
@MethodSource("pickupConditionalColumns")
void importRejectsMissingPickupConditionalFields(String title, int column, String message) 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);
fillValidSelfExpressFbaRow(row);
row.getCell(2).setCellValue("平台上门提货");
row.getCell(9).setCellValue(LocalDateTime.of(2026, 9, 10, 14, 30));
row.getCell(29).setCellValue("工厂|张三|深圳|科技园1号");
row.getCell(column).setCellValue(" ");
workbook.write(out);
var item = service.parse(88L, "Bearer test", out.toByteArray()).get(0);
assertThat(item.draft()).isNull();
assertThat(item.parseError()).isEqualTo(message);
}
}
@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(5).setCellValue("亚马逊FBA");
row.createCell(7).setCellValue("洛杉矶海外仓|US-LAX");
fillValidSelfExpressFbaRow(row);
row.getCell(6).setBlank();
row.getCell(7).setCellValue("洛杉矶海外仓|US-LAX");
workbook.write(out);
var item = service.parse(88L, "Bearer test", out.toByteArray()).get(0);
assertThat(item.draft()).isNull();
@@ -205,6 +250,59 @@ class MerchantBatchImportServiceTest {
return false;
}
private static void fillValidSelfExpressFbaRow(org.apache.poi.ss.usermodel.Row row) {
row.getCell(0).setCellValue("海运");
row.getCell(2).setCellValue("自行快递");
row.getCell(3).setCellValue("义乌收货仓|CN-YW");
row.getCell(4).setCellValue("美森海运|US-SEA");
row.getCell(5).setCellValue("亚马逊FBA");
row.getCell(6).setCellValue("ATL6");
row.getCell(10).setCellValue("蓝牙音箱");
row.getCell(11).setCellValue("纸箱");
row.getCell(12).setCellValue("1");
row.getCell(13).setCellValue("1.2");
row.getCell(14).setCellValue("0.1");
row.getCell(16).setCellValue("否");
row.getCell(17).setCellValue("否");
row.getCell(22).setCellValue("买单报关");
row.getCell(23).setCellValue("不包税");
}
private static List<Integer> requiredHeaderIndexes(org.apache.poi.ss.usermodel.Sheet sheet) {
List<Integer> indexes = new ArrayList<>();
var header = sheet.getRow(1);
for (int column = 0; column < header.getLastCellNum(); column++) {
if (header.getCell(column).getStringCellValue().endsWith(" *")) indexes.add(column);
}
return indexes;
}
private static Stream<Arguments> requiredColumns() {
return Stream.of(
Arguments.of(0, "运输方式"),
Arguments.of(2, "提货方式"),
Arguments.of(3, "国内收货仓"),
Arguments.of(4, "运输渠道"),
Arguments.of(5, "目的类型"),
Arguments.of(10, "货物名称"),
Arguments.of(11, "包装类型"),
Arguments.of(12, "包装数量"),
Arguments.of(13, "毛重(千克)"),
Arguments.of(14, "体积(立方米)"),
Arguments.of(16, "是否木制品"),
Arguments.of(17, "是否危险品"),
Arguments.of(22, "报关方式"),
Arguments.of(23, "计价税模式")
);
}
private static Stream<Arguments> pickupConditionalColumns() {
return Stream.of(
Arguments.of("预约提货时间", 9, "平台上门提货时预约提货时间不能为空"),
Arguments.of("取货地址", 29, "平台上门提货时取货地址不能为空")
);
}
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());