优化
This commit is contained in:
@@ -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());
|
||||
|
||||
Reference in New Issue
Block a user