diff --git a/order/src/main/java/com/baiyue/order/service/MerchantBatchImportService.java b/order/src/main/java/com/baiyue/order/service/MerchantBatchImportService.java index 53b30e8fb..7c466f551 100644 --- a/order/src/main/java/com/baiyue/order/service/MerchantBatchImportService.java +++ b/order/src/main/java/com/baiyue/order/service/MerchantBatchImportService.java @@ -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; diff --git a/order/src/test/java/com/baiyue/order/service/MerchantBatchDraftServiceTest.java b/order/src/test/java/com/baiyue/order/service/MerchantBatchDraftServiceTest.java index d2ef227f1..99829a81e 100644 --- a/order/src/test/java/com/baiyue/order/service/MerchantBatchDraftServiceTest.java +++ b/order/src/test/java/com/baiyue/order/service/MerchantBatchDraftServiceTest.java @@ -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")) diff --git a/order/src/test/java/com/baiyue/order/service/MerchantBatchImportServiceTest.java b/order/src/test/java/com/baiyue/order/service/MerchantBatchImportServiceTest.java index 4080c502b..936efafcc 100644 --- a/order/src/test/java/com/baiyue/order/service/MerchantBatchImportServiceTest.java +++ b/order/src/test/java/com/baiyue/order/service/MerchantBatchImportServiceTest.java @@ -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 requiredHeaderIndexes(org.apache.poi.ss.usermodel.Sheet sheet) { + List 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 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 pickupConditionalColumns() { + return Stream.of( + Arguments.of("预约提货时间", 9, "平台上门提货时预约提货时间不能为空"), + Arguments.of("取货地址", 29, "平台上门提货时取货地址不能为空") + ); + } + private static List rowValues(org.apache.poi.ss.usermodel.Row row) { List values = new ArrayList<>(); for (var cell : row) values.add(cell.getStringCellValue());