This commit is contained in:
2026-08-24 17:15:48 +08:00
parent 132d96a99b
commit 3d8a1fac20
45 changed files with 1250 additions and 35 deletions
+1 -1
View File
@@ -28,7 +28,7 @@
| 分柜与预装 | `split/controller/PlatformSplitPreloadController.java`、`controller/PlatformLoadingListController.java` |
| 货柜与智能排柜 | `controller/PlatformContainerController.java`、`PlatformSmartLoadingController.java`、`service/SmartLoadingService.java`、`repository/SmartLoadingRepository.java`、`loading/`;按入仓码候选与箱数分配分别从 `/inbound-candidates`、`/plans/{planId}/inbound-assignments` 进入,明细规格拆分留在服务端 |
| PDA 拣货与装柜 | `controller/PlatformLoadingPdaController.java`、`loading/controller/PlatformLoadingDeliveryController.java` |
| 装柜完成跨区事件 | `service/LoadingCompletedOutboxService.java`、`LoadingCompletedOutboxRelay.java`、`repository/LoadingCompletedOutboxRepository.java`、`mapper/LoadingCartonManifestMapper.java`、`client/EventBridgeClient.java`;按最终实装与稳定箱码冻结 V2,真实生产验收 `src/test/java/com/baiyue/firstleg/service/TaillegRealProducerAcceptanceTest.java`;事件模型见 `../common-event-contract/AGENTS.md` |
| 装柜完成跨区事件 | `service/LoadingCompletedOutboxService.java`、`LoadingCompletedOutboxRelay.java`、`repository/LoadingCompletedOutboxRepository.java`、`mapper/LoadingCartonManifestMapper.java`、`client/EventBridgeClient.java`;从 `fl_inbound_order.destination_type` 冻结 V2 `orders[].destinationType=FBA|OVERSEAS`,OVERSEAS 按最终实装与稳定箱码冻结,FBA 可仅冻结箱数;真实生产验收 `src/test/java/com/baiyue/firstleg/service/TaillegRealProducerAcceptanceTest.java`;事件模型见 `../common-event-contract/AGENTS.md` |
| 海外卸柜完成事件回写 | 内部入口 `controller/InternalOverseasContainerEventController.java`、消费者 `service/OverseasContainerEventConsumer.java`、Inbox `entity/ContainerEventInboxEntity.java` 与 `mapper/ContainerEventInboxMapper.java`;事件模型见 `../common-event-contract/AGENTS.md` |
| 三期海运订舱与出仓前船期资料 | `oceanbooking/controller/PlatformOceanBookingController.java`、`oceanbooking/service/OceanBookingService.java`、`portschedule/controller/PlatformPortScheduleController.java`;订舱契约见 `src/main/resources/openapi/ocean-bookings-v1.yaml`,表结构见 `src/main/resources/db/phase3-ocean-booking-schema.sql` |
| 四期国内出仓后船节点定时查询 | `outbound/dispatch/service/OutboundDispatchService.java` 在确认出仓事务内幂等创建现有 `CarrierScheduleQuery`;编排与租约 `portschedule/service/CarrierScheduleQueryScheduler.java`,任务/十节点事实/异常事实/不可变 attempt `CarrierScheduleQueryService.java`,平台查询与人工补录入口 `PlatformCarrierScheduleQueryController.java` 的 `/{queryId}/manual-nodes`;停止只由实际 `EMPTY_RETURNED` 节点触发,不提供无节点停止入口;外部船司/轨迹调用经 `integration-service/`;标准见 `../../docs/四期/船节点标准化契约.md` |
@@ -19,7 +19,7 @@ public interface LoadingCompletedEventMapper {
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,
COALESCE(root_order.order_id, o.order_id) AS root_order_id,
o.inbound_code, o.business_type, o.destination_warehouse_id, o.destination_code AS destination_warehouse_code,
o.inbound_code, o.business_type, o.destination_type, o.destination_warehouse_id, o.destination_code AS destination_warehouse_code,
o.destination_name AS destination_warehouse_name,
i.id AS loading_plan_item_id, i.inbound_detail_id AS source_inbound_detail_id, i.sku_code,
i.product_name_zh, i.product_name_en, i.package_count, i.product_quantity,
@@ -73,7 +73,7 @@ public interface LoadingCompletedEventMapper {
private Integer productQuantity; private Integer palletCount; private BigDecimal grossWeightKg;
private BigDecimal netWeightKg; private BigDecimal dimensionalWeightKg; private BigDecimal volumeCbm;
private String fbaTrackingNo; private String shipmentNo; private String shippingMark;
private String businessType;
private String businessType; private String destinationType;
private String customerRemark; private String internalRemark; private String warehouseRemark;
}
@@ -103,13 +103,15 @@ public class LoadingCompletedOutboxService {
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()), orderType, pallets);
sum(sorted, Row::getVolumeCbm), items, string(first.getAccountId()), orderType, pallets,
destinationType(sorted));
}
/** 国内按实装箱数从可用标签池稳定随机抽取箱唛,重复发布同一计划时结果不变。 */
private Map<Long, List<String>> cartonLabelPools(List<Row> rows) {
Map<Long, List<String>> result = new java.util.LinkedHashMap<>();
Map<LabelPoolKey, List<Row>> byInbound = rows.stream().filter(row -> !dropship(row.getBusinessType()))
.filter(row -> "OVERSEAS".equals(destinationType(List.of(row))))
.collect(java.util.stream.Collectors.groupingBy(
row -> new LabelPoolKey(row.getAccountId(), row.getSourceInboundId()),
LinkedHashMap::new, java.util.stream.Collectors.toList()));
@@ -155,6 +157,17 @@ public class LoadingCompletedOutboxService {
return result;
}
private String destinationType(List<Row> rows) {
Set<String> values = rows.stream().map(Row::getDestinationType)
.map(value -> value == null ? "" : value.trim().toUpperCase(java.util.Locale.ROOT))
.collect(java.util.stream.Collectors.toSet());
if (values.size() != 1 || (!values.contains("FBA") && !values.contains("OVERSEAS"))) {
throw new BizException(ResultCode.CONFLICT,
"装柜入仓单目的类型必须唯一且为 FBA 或 OVERSEAS,不能生成海外交接事件");
}
return values.iterator().next();
}
private record LabelPoolKey(long accountId, long inboundId) { }
private List<FirstLegLoadingPlanCompletedV2.PalletSnapshot> palletSnapshots(List<Row> values, List<PalletRow> source) {
@@ -58,6 +58,7 @@ class LoadingCompletedOutboxServiceTest {
assertThat(first.payload().at("/orders/0/items/0/sourceInboundDetailId").asText()).isEqualTo("101");
assertThat(first.payload().at("/orders/0/items/0/skuCode").asText()).isEqualTo("SKU101");
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(rows.get(0).getEventKey()).isEqualTo("FIRSTLEG_LOADING_COMPLETED:90:11:2");
assertThat(first.eventVersion()).isEqualTo(2);
}
@@ -71,6 +72,31 @@ class LoadingCompletedOutboxServiceTest {
.hasMessageContaining("缺少 SKU");
}
@Test void fbaFreezesPackageCountWithoutRequiringCartonLabels() throws Exception {
Row value = row(11, 100, 101);
value.setDestinationType("FBA");
when(snapshots.rows(90L)).thenReturn(List.of(value));
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/destinationType").asText()).isEqualTo("FBA");
assertThat(event.payload().at("/orders/0/items/0/cartonMarks")).isEmpty();
verify(manifests, org.mockito.Mockito.never()).labelPool(11L, 100L);
}
@Test void rejectsUnfrozenDestinationType() {
Row value = row(11, 100, 101);
value.setDestinationType(null);
when(snapshots.rows(90L)).thenReturn(List.of(value));
assertThatThrownBy(() -> service.publish(90L))
.hasMessageContaining("目的类型必须唯一且为 FBA 或 OVERSEAS");
verify(outbox, org.mockito.Mockito.never()).insert(org.mockito.ArgumentMatchers.any());
}
@Test void freezesExactlyLoadedPackagesFromLabelPoolDeterministically() throws Exception {
Row value = row(11, 100, 101);
value.setPackageCount(2);
@@ -148,6 +174,7 @@ class LoadingCompletedOutboxServiceTest {
r.setSourceOrderId(inbound+1000); r.setSourceOrderNo("FL"+inbound); r.setRootOrderId(1099L);
r.setInboundCode("RK"+inbound); r.setLoadingPlanItemId(detail); r.setDestinationWarehouseId(93L); r.setDestinationWarehouseCode("ONT8");
r.setDestinationWarehouseName("ONT8"); r.setSourceInboundDetailId(detail); r.setSkuCode("SKU"+detail);
r.setDestinationType("OVERSEAS");
r.setPackageCount(1); r.setProductQuantity(10); r.setPalletCount(1); r.setGrossWeightKg(BigDecimal.ONE);
r.setNetWeightKg(BigDecimal.ONE); r.setVolumeCbm(BigDecimal.ONE); return r;
}
@@ -88,9 +88,9 @@ class TaillegRealProducerAcceptanceTest {
private void seedInbound(long id, long orderId, String code, String type, String status, String destination) {
jdbc.update("INSERT IGNORE INTO fl_inbound_order(id,tenant_id,order_id,order_no,inbound_code,process_status," +
"order_status_snapshot,customer_name,warehouse_id,warehouse_name,business_type,destination_warehouse_id," +
"order_status_snapshot,customer_name,warehouse_id,warehouse_name,business_type,destination_type,destination_warehouse_id," +
"destination_code,destination_name,package_count,actual_volume_cbm,created_by,updated_by) " +
"VALUES(?,?,?,?,?,?, 'IN_TRANSIT',?,92,'真实验收海外仓',?,9001,?,?,?,0.4000,1,1)",
"VALUES(?,?,?,?,?,?, 'IN_TRANSIT',?,92,'真实验收海外仓',?,'FBA',9001,?,?,?,0.4000,1,1)",
id, CUSTOMER, orderId, MARKER + "-ORDER-" + id, code, status, "真实验收客户", type,
destination, "FBA " + destination, type.equals("ONE_CLICK_DROPSHIP") ? 2 : 3);
}