优化
This commit is contained in:
+23
-1
@@ -7,7 +7,14 @@ import java.util.List;
|
||||
public record FirstLegLoadingPlanCompletedV1(
|
||||
String loadingPlanId, String planNo, String containerBookingId,
|
||||
String containerNo, String cabinetNo, String warehouseId, String warehouseName,
|
||||
String estimatedPortArrivalAt, String actualPortArrivalAt, String estimatedWarehouseArrivalAt,
|
||||
List<Order> orders) {
|
||||
public FirstLegLoadingPlanCompletedV1(String loadingPlanId, String planNo, String containerBookingId,
|
||||
String containerNo, String cabinetNo, String warehouseId,
|
||||
String warehouseName, List<Order> orders) {
|
||||
this(loadingPlanId, planNo, containerBookingId, containerNo, cabinetNo, warehouseId,
|
||||
warehouseName, null, null, null, orders);
|
||||
}
|
||||
|
||||
public record Order(
|
||||
String sourceInboundId, String rootInboundId,
|
||||
@@ -16,6 +23,7 @@ public record FirstLegLoadingPlanCompletedV1(
|
||||
String destinationWarehouseCode, String destinationWarehouseName,
|
||||
int packageCount, Integer productQuantity, Integer palletCount,
|
||||
BigDecimal grossWeightKg, BigDecimal netWeightKg, BigDecimal volumeCbm,
|
||||
BigDecimal volumeWeightKg,
|
||||
List<Item> items, String customerId, String customerName, String orderType, List<PalletSnapshot> pallets,
|
||||
String destinationType) {
|
||||
public Order {
|
||||
@@ -40,9 +48,23 @@ public record FirstLegLoadingPlanCompletedV1(
|
||||
this(sourceInboundId, rootInboundId, sourceOrderId, sourceOrderNo, rootOrderId,
|
||||
inboundCode, destinationWarehouseId, destinationWarehouseCode,
|
||||
destinationWarehouseName, packageCount, productQuantity, palletCount,
|
||||
grossWeightKg, netWeightKg, volumeCbm, items, customerId, customerName,
|
||||
grossWeightKg, netWeightKg, volumeCbm, null, items, customerId, customerName,
|
||||
orderType, pallets, null);
|
||||
}
|
||||
public Order(String sourceInboundId, String rootInboundId,
|
||||
String sourceOrderId, String sourceOrderNo, String rootOrderId,
|
||||
String inboundCode, String destinationWarehouseId,
|
||||
String destinationWarehouseCode, String destinationWarehouseName,
|
||||
int packageCount, Integer productQuantity, Integer palletCount,
|
||||
BigDecimal grossWeightKg, BigDecimal netWeightKg, BigDecimal volumeCbm,
|
||||
List<Item> items, String customerId, String customerName,
|
||||
String orderType, List<PalletSnapshot> pallets, String destinationType) {
|
||||
this(sourceInboundId, rootInboundId, sourceOrderId, sourceOrderNo, rootOrderId,
|
||||
inboundCode, destinationWarehouseId, destinationWarehouseCode,
|
||||
destinationWarehouseName, packageCount, productQuantity, palletCount,
|
||||
grossWeightKg, netWeightKg, volumeCbm, null, items, customerId, customerName,
|
||||
orderType, pallets, destinationType);
|
||||
}
|
||||
}
|
||||
|
||||
public record Item(
|
||||
|
||||
+25
-1
@@ -7,7 +7,15 @@ import java.util.List;
|
||||
public record FirstLegLoadingPlanCompletedV2(
|
||||
String loadingPlanId, String planNo, String containerBookingId,
|
||||
String containerNo, String cabinetNo, String warehouseId, String warehouseName,
|
||||
String estimatedPortArrivalAt, String actualPortArrivalAt, String estimatedWarehouseArrivalAt,
|
||||
List<Order> orders) {
|
||||
public FirstLegLoadingPlanCompletedV2(String loadingPlanId, String planNo, String containerBookingId,
|
||||
String containerNo, String cabinetNo, String warehouseId,
|
||||
String warehouseName, List<Order> orders) {
|
||||
this(loadingPlanId, planNo, containerBookingId, containerNo, cabinetNo, warehouseId,
|
||||
warehouseName, null, null, null, orders);
|
||||
}
|
||||
|
||||
public record Order(
|
||||
String sourceInboundId, String rootInboundId,
|
||||
String sourceOrderId, String sourceOrderNo, String rootOrderId,
|
||||
@@ -15,6 +23,7 @@ public record FirstLegLoadingPlanCompletedV2(
|
||||
String destinationWarehouseCode, String destinationWarehouseName,
|
||||
int packageCount, Integer productQuantity, Integer palletCount,
|
||||
BigDecimal grossWeightKg, BigDecimal netWeightKg, BigDecimal volumeCbm,
|
||||
BigDecimal volumeWeightKg,
|
||||
List<Item> items, String customerId, String customerName, String orderType, List<PalletSnapshot> pallets,
|
||||
/** 目的类型:FBA 可按箱数收货,OVERSEAS 必须逐箱扫码。 */
|
||||
String destinationType) {
|
||||
@@ -41,9 +50,24 @@ public record FirstLegLoadingPlanCompletedV2(
|
||||
this(sourceInboundId, rootInboundId, sourceOrderId, sourceOrderNo, rootOrderId,
|
||||
inboundCode, destinationWarehouseId, destinationWarehouseCode,
|
||||
destinationWarehouseName, packageCount, productQuantity, palletCount,
|
||||
grossWeightKg, netWeightKg, volumeCbm, items, customerId, customerName,
|
||||
grossWeightKg, netWeightKg, volumeCbm, null, items, customerId, customerName,
|
||||
orderType, pallets, null);
|
||||
}
|
||||
|
||||
public Order(String sourceInboundId, String rootInboundId,
|
||||
String sourceOrderId, String sourceOrderNo, String rootOrderId,
|
||||
String inboundCode, String destinationWarehouseId,
|
||||
String destinationWarehouseCode, String destinationWarehouseName,
|
||||
int packageCount, Integer productQuantity, Integer palletCount,
|
||||
BigDecimal grossWeightKg, BigDecimal netWeightKg, BigDecimal volumeCbm,
|
||||
List<Item> items, String customerId, String customerName,
|
||||
String orderType, List<PalletSnapshot> pallets, String destinationType) {
|
||||
this(sourceInboundId, rootInboundId, sourceOrderId, sourceOrderNo, rootOrderId,
|
||||
inboundCode, destinationWarehouseId, destinationWarehouseCode,
|
||||
destinationWarehouseName, packageCount, productQuantity, palletCount,
|
||||
grossWeightKg, netWeightKg, volumeCbm, null, items, customerId, customerName,
|
||||
orderType, pallets, destinationType);
|
||||
}
|
||||
}
|
||||
|
||||
public record Item(
|
||||
|
||||
@@ -7,7 +7,7 @@ components:
|
||||
schemas:
|
||||
FirstLegLoadingPlanCompletedV2:
|
||||
type: object
|
||||
required: [loadingPlanId, containerBookingId, orders]
|
||||
required: [loadingPlanId, containerBookingId, estimatedPortArrivalAt, actualPortArrivalAt, estimatedWarehouseArrivalAt, orders]
|
||||
properties:
|
||||
loadingPlanId: {type: string}
|
||||
containerBookingId: {type: string}
|
||||
@@ -15,12 +15,15 @@ components:
|
||||
cabinetNo: {type: string}
|
||||
warehouseId: {type: string}
|
||||
warehouseName: {type: string}
|
||||
estimatedPortArrivalAt: {type: [string, 'null'], format: local-date-time, description: Frozen source-local estimated port arrival; null when Firstleg has no fact}
|
||||
actualPortArrivalAt: {type: [string, 'null'], format: local-date-time, description: Frozen source-local actual port arrival; never copied from an estimate}
|
||||
estimatedWarehouseArrivalAt: {type: [string, 'null'], format: local-date-time, description: Frozen source-local estimated warehouse arrival; null when Firstleg has no fact}
|
||||
orders:
|
||||
type: array
|
||||
items: {$ref: '#/components/schemas/FirstLegLoadingPlanCompletedOrderV2'}
|
||||
FirstLegLoadingPlanCompletedOrderV2:
|
||||
type: object
|
||||
required: [sourceInboundId, customerId, customerName, packageCount, items, orderType, pallets, destinationType]
|
||||
required: [sourceInboundId, customerId, customerName, palletCount, packageCount, productQuantity, volumeCbm, volumeWeightKg, items, orderType, pallets, destinationType]
|
||||
properties:
|
||||
sourceInboundId: {type: string}
|
||||
sourceOrderId: {type: string}
|
||||
@@ -29,6 +32,10 @@ components:
|
||||
customerId: {type: string, minLength: 1, description: Trusted customer identity frozen by Firstleg}
|
||||
customerName: {type: string, minLength: 1, maxLength: 128, description: Immutable trusted container/customer name snapshot; consumers must not query domestic IAM}
|
||||
packageCount: {type: integer, minimum: 1}
|
||||
productQuantity: {type: [integer, 'null'], minimum: 0, description: Final loaded product quantity; null when unavailable}
|
||||
palletCount: {type: [integer, 'null'], minimum: 1, description: Distinct physical pallet count from Firstleg allocation pallet identity; never a planned zero placeholder}
|
||||
volumeCbm: {type: [number, 'null'], minimum: 0, description: Final loaded volume in cubic metres}
|
||||
volumeWeightKg: {type: [number, 'null'], minimum: 0, description: Sum of final loaded item dimensional weight facts; null when unavailable}
|
||||
items: {type: array, items: {type: object}}
|
||||
orderType: {type: string, enum: [FIRST_LEG, ONE_CLICK_DROPSHIP]}
|
||||
pallets: {type: array, items: {type: object}}
|
||||
|
||||
+9
-2
@@ -52,13 +52,20 @@ class FirstLegLoadingPlanCompletedV1Test {
|
||||
void legacyParserRetainsDestinationTypeWhenV2OrderCarriesIt() throws Exception {
|
||||
String json = """
|
||||
{"loadingPlanId":"1","planNo":"LP-1","containerBookingId":"2","containerNo":"CONT-1",
|
||||
"warehouseId":"3","warehouseName":"海外仓","orders":[{"sourceInboundId":"100",
|
||||
"destinationWarehouseId":"3","packageCount":1,"items":[],"customerId":"9001",
|
||||
"warehouseId":"3","warehouseName":"海外仓","estimatedPortArrivalAt":"2026-09-30T12:00",
|
||||
"actualPortArrivalAt":null,"estimatedWarehouseArrivalAt":"2026-10-03T09:00",
|
||||
"orders":[{"sourceInboundId":"100","destinationWarehouseId":"3","packageCount":1,
|
||||
"palletCount":2,"volumeWeightKg":12.500,"items":[],"customerId":"9001",
|
||||
"customerName":"客户甲","destinationType":"OVERSEAS"}]}
|
||||
""";
|
||||
|
||||
FirstLegLoadingPlanCompletedV1 restored = mapper.readValue(json, FirstLegLoadingPlanCompletedV1.class);
|
||||
|
||||
assertThat(restored.orders().get(0).destinationType()).isEqualTo("OVERSEAS");
|
||||
assertThat(restored.estimatedPortArrivalAt()).isEqualTo("2026-09-30T12:00");
|
||||
assertThat(restored.actualPortArrivalAt()).isNull();
|
||||
assertThat(restored.estimatedWarehouseArrivalAt()).isEqualTo("2026-10-03T09:00");
|
||||
assertThat(restored.orders().get(0).palletCount()).isEqualTo(2);
|
||||
assertThat(restored.orders().get(0).volumeWeightKg()).isEqualByComparingTo("12.500");
|
||||
}
|
||||
}
|
||||
|
||||
+26
-1
@@ -4,12 +4,35 @@ import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.util.List;
|
||||
import java.math.BigDecimal;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatThrownBy;
|
||||
|
||||
class FirstLegLoadingPlanCompletedV2Test {
|
||||
@Test
|
||||
void carriesOnlyFrozenContainerTimesAndActualOrderAggregates() {
|
||||
var order = new FirstLegLoadingPlanCompletedV2.Order(
|
||||
"2", "2", "3", "ORD", "3", "RK", null, "ONT8", "ONT8",
|
||||
12, 120, 3, null, null, new BigDecimal("2.500"), new BigDecimal("18.750"),
|
||||
List.of(), "11", "客户甲", "FIRST_LEG", List.of(), "OVERSEAS");
|
||||
var payload = new FirstLegLoadingPlanCompletedV2(
|
||||
"4", "P", "5", "CONT", "CAB", "6", "WH",
|
||||
"2026-09-30T12:00", null, "2026-10-03T09:00", List.of(order));
|
||||
|
||||
var json = new ObjectMapper().valueToTree(payload);
|
||||
|
||||
assertThat(json.at("/estimatedPortArrivalAt").asText()).isEqualTo("2026-09-30T12:00");
|
||||
assertThat(json.at("/actualPortArrivalAt").isNull()).isTrue();
|
||||
assertThat(json.at("/estimatedWarehouseArrivalAt").asText()).isEqualTo("2026-10-03T09:00");
|
||||
assertThat(json.at("/orders/0/palletCount").asInt()).isEqualTo(3);
|
||||
assertThat(json.at("/orders/0/packageCount").asInt()).isEqualTo(12);
|
||||
assertThat(json.at("/orders/0/productQuantity").asInt()).isEqualTo(120);
|
||||
assertThat(json.at("/orders/0/volumeCbm").decimalValue()).isEqualByComparingTo("2.500");
|
||||
assertThat(json.at("/orders/0/volumeWeightKg").decimalValue()).isEqualByComparingTo("18.750");
|
||||
}
|
||||
|
||||
@Test
|
||||
void carriesTheThreeItemRemarks() throws Exception {
|
||||
var item = new FirstLegLoadingPlanCompletedV2.Item("1", "SKU", "中文", "EN", 1,
|
||||
@@ -92,6 +115,8 @@ class FirstLegLoadingPlanCompletedV2Test {
|
||||
.contains("required: [sourceInboundId, customerId, customerName")
|
||||
.contains("customerId: {type: string, minLength: 1")
|
||||
.contains("customerName: {type: string, minLength: 1, maxLength: 128")
|
||||
.contains("consumers must not query domestic IAM");
|
||||
.contains("consumers must not query domestic IAM")
|
||||
.contains("estimatedPortArrivalAt", "actualPortArrivalAt", "estimatedWarehouseArrivalAt")
|
||||
.contains("palletCount", "volumeWeightKg", "never a planned zero placeholder");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ import org.apache.ibatis.annotations.Param;
|
||||
import org.apache.ibatis.annotations.Select;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
|
||||
/** 读取 PDA 结算后的最终实装入仓单和冻结货物快照。 */
|
||||
@@ -16,6 +17,8 @@ public interface LoadingCompletedEventMapper {
|
||||
SELECT p.id AS loading_plan_id, p.plan_no, p.container_booking_id, p.version AS aggregate_version,
|
||||
b.container_no, b.cabinet_no, b.warehouse_id, b.warehouse_name,
|
||||
b.customer_id AS booking_customer_id, b.customer_name,
|
||||
b.estimated_port_arrival_at, b.actual_port_arrival_at,
|
||||
b.estimated_warehouse_arrival_at,
|
||||
i.account_id, i.inbound_id AS source_inbound_id,
|
||||
COALESCE(o.root_inbound_id, o.id) AS root_inbound_id,
|
||||
o.order_id AS source_order_id, o.order_no AS source_order_no,
|
||||
@@ -50,7 +53,8 @@ public interface LoadingCompletedEventMapper {
|
||||
/** 读取一键代发托盘及其箱唛快照;普通订单不会使用该结果。 */
|
||||
@Select("""
|
||||
SELECT i.account_id, i.inbound_id AS source_inbound_id,
|
||||
a.pallet_code, a.allocation_no, a.picked_package_count AS allocated_package_count,
|
||||
a.pallet_id, a.reservation_id, a.pallet_code, a.allocation_no,
|
||||
a.picked_package_count AS allocated_package_count,
|
||||
i.inbound_detail_id AS source_inbound_detail_id, i.sku_code,
|
||||
i.product_name_zh, i.package_count, i.product_quantity,
|
||||
label.box_code
|
||||
@@ -82,6 +86,8 @@ public interface LoadingCompletedEventMapper {
|
||||
private Integer aggregateVersion; private String containerNo; private String cabinetNo;
|
||||
private Long warehouseId; private String warehouseName; private Long bookingCustomerId;
|
||||
private String customerName; private Long accountId;
|
||||
private LocalDateTime estimatedPortArrivalAt; private LocalDateTime actualPortArrivalAt;
|
||||
private LocalDateTime estimatedWarehouseArrivalAt;
|
||||
private Long sourceInboundId; private Long rootInboundId; private Long sourceOrderId;
|
||||
private Long loadingPlanItemId;
|
||||
private String sourceOrderNo; private Long rootOrderId; private String inboundCode;
|
||||
@@ -97,7 +103,8 @@ public interface LoadingCompletedEventMapper {
|
||||
|
||||
@Getter @Setter
|
||||
class PalletRow {
|
||||
private Long accountId; private Long sourceInboundId; private String palletCode;
|
||||
private Long accountId; private Long sourceInboundId; private Long palletId;
|
||||
private Long reservationId; private String palletCode;
|
||||
private Integer allocationNo; private Integer allocatedPackageCount;
|
||||
private Long sourceInboundDetailId; private String skuCode; private String productNameZh;
|
||||
private Integer packageCount; private Integer productQuantity; private String boxCode;
|
||||
|
||||
+14
-4
@@ -68,7 +68,8 @@ public class LoadingCompletedOutboxService {
|
||||
.filter(item -> Objects.equals(item.getSourceInboundId(), value.get(0).getSourceInboundId())).toList(), cartonMarks)).toList();
|
||||
var payload = new FirstLegLoadingPlanCompletedV2(String.valueOf(first.getLoadingPlanId()),
|
||||
first.getPlanNo(), string(first.getContainerBookingId()), first.getContainerNo(), first.getCabinetNo(),
|
||||
string(first.getWarehouseId()), first.getWarehouseName(), orders);
|
||||
string(first.getWarehouseId()), first.getWarehouseName(), iso(first.getEstimatedPortArrivalAt()),
|
||||
iso(first.getActualPortArrivalAt()), iso(first.getEstimatedWarehouseArrivalAt()), orders);
|
||||
JsonNode payloadNode = objectMapper.valueToTree(payload);
|
||||
String hash = EventPayloadHash.sha256(EventPayloadCanonicalizer.canonicalize(payloadNode));
|
||||
String eventKey = "FIRSTLEG_LOADING_COMPLETED:" + first.getLoadingPlanId() + ':' + accountId + ':' + HANDOVER_VERSION;
|
||||
@@ -103,12 +104,20 @@ public class LoadingCompletedOutboxService {
|
||||
string(first.getRootInboundId()), string(first.getSourceOrderId()), first.getSourceOrderNo(),
|
||||
string(first.getRootOrderId()), first.getInboundCode(), string(first.getDestinationWarehouseId()),
|
||||
first.getDestinationWarehouseCode(), first.getDestinationWarehouseName(), sumInt(sorted, Row::getPackageCount),
|
||||
sumNullableInt(sorted, Row::getProductQuantity), "ONE_CLICK_DROPSHIP".equals(orderType)
|
||||
? pallets.size() : 0, sum(sorted, Row::getGrossWeightKg), sum(sorted, Row::getNetWeightKg),
|
||||
sum(sorted, Row::getVolumeCbm), items, string(first.getAccountId()), first.getCustomerName().trim(), orderType, pallets,
|
||||
sumNullableInt(sorted, Row::getProductQuantity), actualPalletCount(palletRows),
|
||||
sum(sorted, Row::getGrossWeightKg), sum(sorted, Row::getNetWeightKg),
|
||||
sum(sorted, Row::getVolumeCbm), sum(sorted, Row::getDimensionalWeightKg), items,
|
||||
string(first.getAccountId()), first.getCustomerName().trim(), orderType, pallets,
|
||||
destinationType(sorted));
|
||||
}
|
||||
|
||||
/** 只按 Warehouse 已分配的物理 palletId 计数;无分配事实时保持 null。 */
|
||||
private static Integer actualPalletCount(List<PalletRow> palletRows) {
|
||||
Set<Long> palletIds = palletRows.stream().map(PalletRow::getPalletId)
|
||||
.filter(Objects::nonNull).filter(value -> value > 0).collect(java.util.stream.Collectors.toSet());
|
||||
return palletIds.isEmpty() ? null : palletIds.size();
|
||||
}
|
||||
|
||||
private void validateCustomerSnapshots(List<Row> rows) {
|
||||
for (Row row : rows) {
|
||||
if (row.getAccountId() == null || !Objects.equals(row.getAccountId(), row.getBookingCustomerId())) {
|
||||
@@ -275,6 +284,7 @@ public class LoadingCompletedOutboxService {
|
||||
private String json(Object value) { try { return objectMapper.writeValueAsString(value); }
|
||||
catch (JsonProcessingException e) { throw new BizException(ResultCode.INTERNAL_ERROR, "装柜完成事件序列化失败"); } }
|
||||
private static String string(Long value) { return value == null ? null : String.valueOf(value); }
|
||||
private static String iso(java.time.LocalDateTime value) { return value == null ? null : value.toString(); }
|
||||
private static int sumInt(List<Row> rows, java.util.function.Function<Row,Integer> getter) { return rows.stream().map(getter).filter(java.util.Objects::nonNull).mapToInt(Integer::intValue).sum(); }
|
||||
private static Integer sumNullableInt(List<Row> rows, java.util.function.Function<Row,Integer> getter) { List<Integer> values=rows.stream().map(getter).filter(java.util.Objects::nonNull).toList(); return values.isEmpty()?null:values.stream().mapToInt(Integer::intValue).sum(); }
|
||||
private static BigDecimal sum(List<Row> rows, java.util.function.Function<Row,BigDecimal> getter) { List<BigDecimal> values=rows.stream().map(getter).filter(java.util.Objects::nonNull).toList(); return values.isEmpty()?null:values.stream().reduce(BigDecimal.ZERO, BigDecimal::add); }
|
||||
|
||||
+44
-2
@@ -45,6 +45,10 @@ class LoadingCompletedOutboxServiceTest {
|
||||
|
||||
@Test void groupsByAccountAndKeepsAllOrdersAndItemsStable() throws Exception {
|
||||
when(snapshots.rows(90L)).thenReturn(List.of(row(22,300,302),row(11,200,202),row(11,100,102),row(11,100,101)));
|
||||
when(snapshots.palletRows(90L)).thenReturn(List.of(
|
||||
pallet(11,100,1,"PLT-01",1,101,"SKU101",1,10,"BOX-101"),
|
||||
pallet(11,100,1,"PLT-01",1,102,"SKU102",1,10,"BOX-102"),
|
||||
pallet(11,100,2,"PLT-02",1,102,"SKU102",1,10,"BOX-102")));
|
||||
service.publish(90L);
|
||||
ArgumentCaptor<InboundOutboxEntity> captor=ArgumentCaptor.forClass(InboundOutboxEntity.class);
|
||||
verify(outbox,org.mockito.Mockito.times(2)).insert(captor.capture());
|
||||
@@ -60,6 +64,14 @@ class LoadingCompletedOutboxServiceTest {
|
||||
assertThat(first.payload().at("/orders/0/items/0/cartonMarks/0").asText()).isEqualTo("BOX-101");
|
||||
assertThat(first.payload().at("/orders/0/destinationType").asText()).isEqualTo("OVERSEAS");
|
||||
assertThat(first.payload().at("/orders/0/customerName").asText()).isEqualTo("客户-11");
|
||||
assertThat(first.payload().at("/orders/0/palletCount").asInt()).isEqualTo(2);
|
||||
assertThat(first.payload().at("/orders/0/packageCount").asInt()).isEqualTo(2);
|
||||
assertThat(first.payload().at("/orders/0/productQuantity").asInt()).isEqualTo(20);
|
||||
assertThat(first.payload().at("/orders/0/volumeCbm").decimalValue()).isEqualByComparingTo("2");
|
||||
assertThat(first.payload().at("/orders/0/volumeWeightKg").decimalValue()).isEqualByComparingTo("4");
|
||||
assertThat(first.payload().at("/estimatedPortArrivalAt").asText()).isEqualTo("2026-09-30T12:00");
|
||||
assertThat(first.payload().at("/actualPortArrivalAt").asText()).isEqualTo("2026-10-01T08:30");
|
||||
assertThat(first.payload().at("/estimatedWarehouseArrivalAt").asText()).isEqualTo("2026-10-03T09:00");
|
||||
assertThat(rows.get(0).getEventKey()).isEqualTo("FIRSTLEG_LOADING_COMPLETED:90:11:2");
|
||||
assertThat(first.eventVersion()).isEqualTo(2);
|
||||
}
|
||||
@@ -148,6 +160,28 @@ class LoadingCompletedOutboxServiceTest {
|
||||
.isNotEqualTo(captor.getAllValues().get(1).getPayloadHash());
|
||||
}
|
||||
|
||||
@Test void keepsMissingPalletAndMeasurementFactsNullInsteadOfInventingZero() throws Exception {
|
||||
Row value = row(11, 100, 101);
|
||||
value.setProductQuantity(null); value.setVolumeCbm(null); value.setDimensionalWeightKg(null);
|
||||
value.setEstimatedPortArrivalAt(null); value.setActualPortArrivalAt(null);
|
||||
value.setEstimatedWarehouseArrivalAt(null);
|
||||
when(snapshots.rows(90L)).thenReturn(List.of(value));
|
||||
when(snapshots.palletRows(90L)).thenReturn(List.of());
|
||||
|
||||
service.publish(90L);
|
||||
|
||||
ArgumentCaptor<InboundOutboxEntity> captor = ArgumentCaptor.forClass(InboundOutboxEntity.class);
|
||||
verify(outbox).insert(captor.capture());
|
||||
EventEnvelope event = mapper.readValue(captor.getValue().getPayloadJson(), EventEnvelope.class);
|
||||
assertThat(event.payload().at("/orders/0/palletCount").isNull()).isTrue();
|
||||
assertThat(event.payload().at("/orders/0/productQuantity").isNull()).isTrue();
|
||||
assertThat(event.payload().at("/orders/0/volumeCbm").isNull()).isTrue();
|
||||
assertThat(event.payload().at("/orders/0/volumeWeightKg").isNull()).isTrue();
|
||||
assertThat(event.payload().at("/estimatedPortArrivalAt").isNull()).isTrue();
|
||||
assertThat(event.payload().at("/actualPortArrivalAt").isNull()).isTrue();
|
||||
assertThat(event.payload().at("/estimatedWarehouseArrivalAt").isNull()).isTrue();
|
||||
}
|
||||
|
||||
@Test void fillsCollapsedMeasurementDetailFromUnusedInboundLabelsWithoutDuplicates() throws Exception {
|
||||
Row first = row(11, 100, 101); first.setPackageCount(1);
|
||||
Row second = row(11, 100, 101); second.setLoadingPlanItemId(102L); second.setPackageCount(2);
|
||||
@@ -176,7 +210,9 @@ class LoadingCompletedOutboxServiceTest {
|
||||
assertThat(rowsSql).contains("p.plan_status = 'COMPLETED'")
|
||||
.contains("o.process_status = 'LOADED'")
|
||||
.contains("b.customer_id AS booking_customer_id")
|
||||
.contains("b.customer_name");
|
||||
.contains("b.customer_name")
|
||||
.contains("b.estimated_port_arrival_at", "b.actual_port_arrival_at")
|
||||
.contains("b.estimated_warehouse_arrival_at");
|
||||
for (String sql : List.of(rowsSql, palletsSql)) {
|
||||
assertThat(sql)
|
||||
.contains("actual_pick.loading_plan_item_id = i.id")
|
||||
@@ -190,6 +226,7 @@ class LoadingCompletedOutboxServiceTest {
|
||||
.doesNotContain("'PICKING'");
|
||||
}
|
||||
assertThat(palletsSql)
|
||||
.contains("a.pallet_id", "a.reservation_id")
|
||||
.contains("label.tenant_id = i.account_id")
|
||||
.contains("label.inbound_detail_id = i.inbound_detail_id")
|
||||
.contains("label.batch_status = 'ACTIVE'")
|
||||
@@ -252,6 +289,9 @@ class LoadingCompletedOutboxServiceTest {
|
||||
Row r=new Row(); r.setLoadingPlanId(90L); r.setPlanNo("P90"); r.setContainerBookingId(91L);
|
||||
r.setAggregateVersion(8); r.setContainerNo("CONT"); r.setCabinetNo("CAB"); r.setWarehouseId(92L);
|
||||
r.setWarehouseName("WH"); r.setBookingCustomerId(account); r.setCustomerName("客户-" + account);
|
||||
r.setEstimatedPortArrivalAt(java.time.LocalDateTime.of(2026, 9, 30, 12, 0));
|
||||
r.setActualPortArrivalAt(java.time.LocalDateTime.of(2026, 10, 1, 8, 30));
|
||||
r.setEstimatedWarehouseArrivalAt(java.time.LocalDateTime.of(2026, 10, 3, 9, 0));
|
||||
r.setAccountId(account); r.setSourceInboundId(inbound); r.setRootInboundId(99L);
|
||||
r.setSourceOrderId(inbound+1000); r.setSourceOrderNo("FL"+inbound); r.setRootOrderId(1099L);
|
||||
r.setInboundCode("RK"+inbound); r.setLoadingPlanItemId(detail); r.setDestinationWarehouseId(93L); r.setDestinationWarehouseCode("ONT8");
|
||||
@@ -259,13 +299,15 @@ class LoadingCompletedOutboxServiceTest {
|
||||
r.setDestinationType("OVERSEAS");
|
||||
r.setFulfillmentType("STANDARD");
|
||||
r.setPackageCount(1); r.setProductQuantity(10); r.setPalletCount(1); r.setGrossWeightKg(BigDecimal.ONE);
|
||||
r.setNetWeightKg(BigDecimal.ONE); r.setVolumeCbm(BigDecimal.ONE); return r;
|
||||
r.setNetWeightKg(BigDecimal.ONE); r.setDimensionalWeightKg(new BigDecimal("2"));
|
||||
r.setVolumeCbm(BigDecimal.ONE); return r;
|
||||
}
|
||||
|
||||
|
||||
private PalletRow pallet(long account,long inbound,int allocation,String palletNo,int allocated,
|
||||
long detail,String sku,int packages,int quantity,String boxCode) {
|
||||
PalletRow row = new PalletRow(); row.setAccountId(account); row.setSourceInboundId(inbound);
|
||||
row.setPalletId(inbound * 100 + allocation); row.setReservationId(inbound * 1000 + allocation);
|
||||
row.setAllocationNo(allocation); row.setPalletCode(palletNo); row.setAllocatedPackageCount(allocated);
|
||||
row.setSourceInboundDetailId(detail); row.setSkuCode(sku); row.setProductNameZh("产品");
|
||||
row.setPackageCount(packages); row.setProductQuantity(quantity); row.setBoxCode(boxCode); return row;
|
||||
|
||||
+28
-8
@@ -11,6 +11,7 @@ import org.springframework.context.annotation.Import;
|
||||
import org.springframework.jdbc.core.JdbcTemplate;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertNull;
|
||||
|
||||
@SpringBootTest(classes = TaillegRealProducerAcceptanceTest.AcceptanceApplication.class,
|
||||
webEnvironment = SpringBootTest.WebEnvironment.NONE, properties = {
|
||||
@@ -53,13 +54,26 @@ class TaillegRealProducerAcceptanceTest {
|
||||
assertEquals("真实验收客户", jdbc.queryForObject(
|
||||
"SELECT JSON_UNQUOTE(JSON_EXTRACT(payload_json,'$.payload.orders[0].customerName')) " +
|
||||
"FROM fl_inbound_outbox WHERE event_key=?", String.class, eventKey));
|
||||
assertEquals("2026-09-30T12:00", jdbc.queryForObject(
|
||||
"SELECT JSON_UNQUOTE(JSON_EXTRACT(payload_json,'$.payload.estimatedPortArrivalAt')) " +
|
||||
"FROM fl_inbound_outbox WHERE event_key=?", String.class, eventKey));
|
||||
assertNull(jdbc.queryForObject(
|
||||
"SELECT JSON_UNQUOTE(JSON_EXTRACT(payload_json,'$.payload.actualPortArrivalAt')) " +
|
||||
"FROM fl_inbound_outbox WHERE event_key=?", String.class, eventKey));
|
||||
assertEquals(1, jdbc.queryForObject(
|
||||
"SELECT JSON_EXTRACT(payload_json,'$.payload.orders[0].palletCount') " +
|
||||
"FROM fl_inbound_outbox WHERE event_key=?", Integer.class, eventKey));
|
||||
assertEquals("35.0", jdbc.queryForObject(
|
||||
"SELECT JSON_UNQUOTE(JSON_EXTRACT(payload_json,'$.payload.orders[0].volumeWeightKg')) " +
|
||||
"FROM fl_inbound_outbox WHERE event_key=?", String.class, eventKey));
|
||||
}
|
||||
|
||||
private void seedCompletedLoadingPlan() {
|
||||
jdbc.update("INSERT IGNORE INTO fl_container_booking(id,booking_no,container_no,cabinet_no,warehouse_id," +
|
||||
"warehouse_name,customer_id,customer_name,container_type,created_by,updated_by) VALUES(?,?,?,?,?,?,?,?,?,1,1)",
|
||||
"warehouse_name,customer_id,customer_name,estimated_port_arrival_at,container_type,created_by,updated_by) " +
|
||||
"VALUES(?,?,?,?,?,?,?,?,?,?,1,1)",
|
||||
BOOKING, MARKER + "-BOOK", MARKER + "-CNTR", MARKER + "-CAB", 92L, "真实验收海外仓",
|
||||
CUSTOMER, "真实验收客户", "40HQ");
|
||||
CUSTOMER, "真实验收客户", java.time.LocalDateTime.of(2026, 9, 30, 12, 0), "40HQ");
|
||||
jdbc.update("INSERT IGNORE INTO fl_loading_plan(id,plan_no,container_booking_id,plan_status,created_by," +
|
||||
"updated_by,version) VALUES(?,?,?,'LOADING',1,1,0)", PLAN, MARKER + "-PLAN", BOOKING);
|
||||
seedInbound(FIRST_INBOUND, 990022221L, MARKER + "-FIRST-IN", "FBA_SEA", "STANDARD", "LOADED", "ONT8");
|
||||
@@ -70,12 +84,8 @@ class TaillegRealProducerAcceptanceTest {
|
||||
MARKER + "-FIRST-IN", "FIRST-BOX-" + i, i, 3);
|
||||
for (int i = 1; i <= 2; i++) seedLabel(990022550L + i, DROPSHIP_INBOUND, 990022222L, 990022422L,
|
||||
MARKER + "-DROP-IN", "DROP-BOX-" + i, i, 2);
|
||||
jdbc.update("INSERT IGNORE INTO fl_inbound_pallet_allocation(id,tenant_id,inbound_id,allocation_no," +
|
||||
"warehouse_id,warehouse_resource_tenant_id,pallet_id,pallet_code,pallet_capacity_cbm," +
|
||||
"allocated_volume_cbm,allocated_package_count,picked_package_count,location_id,location_code," +
|
||||
"reservation_no,allocation_status) VALUES(?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,'CONFIRMED')",
|
||||
990022621L, CUSTOMER, DROPSHIP_INBOUND, 1, 92L, 0L, 990022721L, MARKER + "-PALLET-D",
|
||||
1.2, 0.4, 2, 2, 990022821L, "A-01", MARKER + "-RES");
|
||||
seedAllocation(990022620L, FIRST_INBOUND, 990022720L, 990022820L, 3, "F");
|
||||
seedAllocation(990022621L, DROPSHIP_INBOUND, 990022721L, 990022821L, 2, "D");
|
||||
jdbc.update("INSERT INTO fl_loading_pick_task(id,loading_plan_id,container_booking_id,cabinet_no," +
|
||||
"container_no,task_status,started_by,started_by_name) VALUES(?,?,?,?,?,'LOADING',1,'真实验收')",
|
||||
990022901L, PLAN, BOOKING, MARKER + "-CAB", MARKER + "-CNTR");
|
||||
@@ -84,6 +94,16 @@ class TaillegRealProducerAcceptanceTest {
|
||||
jdbc.update("UPDATE fl_loading_plan SET plan_status='COMPLETED',confirmed_at=NOW(3),version=version+1 WHERE id=?", PLAN);
|
||||
}
|
||||
|
||||
private void seedAllocation(long id, long inboundId, long palletId, long locationId,
|
||||
int packages, String suffix) {
|
||||
jdbc.update("INSERT IGNORE INTO fl_inbound_pallet_allocation(id,tenant_id,inbound_id,allocation_no," +
|
||||
"warehouse_id,warehouse_resource_tenant_id,pallet_id,pallet_code,pallet_capacity_cbm," +
|
||||
"allocated_volume_cbm,allocated_package_count,picked_package_count,location_id,location_code," +
|
||||
"reservation_id,reservation_no,allocation_status) VALUES(?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,'CONFIRMED')",
|
||||
id, CUSTOMER, inboundId, 1, 92L, 0L, palletId, MARKER + "-PALLET-" + suffix,
|
||||
1.2, 0.4, packages, packages, locationId, "A-01", id + 1000, MARKER + "-RES-" + suffix);
|
||||
}
|
||||
|
||||
private void seedPickItem(long id, long planItemId, long inboundId, long detailId, int packages) {
|
||||
jdbc.update("INSERT INTO fl_loading_pick_item(id,pick_task_id,loading_plan_item_id,inbound_id," +
|
||||
"inbound_detail_id,planned_package_count,picked_package_count,item_status) VALUES(?,?,?,?,?,?,?,'PICKED')",
|
||||
|
||||
Reference in New Issue
Block a user