From 0d8e64c42c40452b29b1f59872b8ae74fb665fa9 Mon Sep 17 00:00:00 2001 From: ouyangxq <775153912@qq.com> Date: Wed, 4 Nov 2026 12:45:05 +0800 Subject: [PATCH] =?UTF-8?q?=E6=94=AF=E6=8C=81=E7=A7=81=E4=BA=BA=E5=9C=B0?= =?UTF-8?q?=E5=9D=80=E8=A3=85=E6=9F=9C=E5=90=8E=E6=8C=89=E6=99=AE=E9=80=9A?= =?UTF-8?q?=E6=B5=B7=E5=A4=96=E6=94=B6=E8=B4=A7=E4=BA=A4=E6=8E=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- firstleg/docs/business-index.md | 2 +- .../LoadingCompletedOutboxService.java | 8 +++-- .../LoadingCompletedOutboxServiceTest.java | 35 ++++++++++++++++++- 3 files changed, 40 insertions(+), 5 deletions(-) diff --git a/firstleg/docs/business-index.md b/firstleg/docs/business-index.md index e4aef16b0..2509dab84 100644 --- a/firstleg/docs/business-index.md +++ b/firstleg/docs/business-index.md @@ -47,7 +47,7 @@ | 货柜与全局智能排柜 | `controller/PlatformContainerController.java`、`PlatformSmartLoadingController.java`、`service/SmartLoadingService.java`、`repository/SmartLoadingRepository.java`、`client/PendingReviewOrderClient.java`、`loading/`;全局候选合并本服务履约投影与 Order `/internal/order/v1/pending-review-orders` 权威待审核页,工作台柜次从 `/inbound-candidates`、`/containers` 进入;普通箱数、待审核占位、同柜组和全部未拣移除分别走 `/inbound-assignments`、`/pending-review-assignments`、`/group-assignments`、`/unpicked-items`,明细规格拆分留在服务端 | | 货柜资料、PDA 证据与业务总览 | `controller/PlatformContainerController.java` 的 `/{id}/overview`、`/{id}/documents/{documentType}`,聚合 `mapper/ContainerOverviewMapper.java`;契约 `src/main/resources/openapi/phase3-container-overview-v1.yaml`,四类资料固定为目的港清关、国内出口报关、DO、PU 的当前唯一版本 | | PDA 拣货与装柜、证据幂等上传 | `controller/PlatformLoadingPdaController.java`、`loading/controller/PlatformLoadingDeliveryController.java`、`loading/service/CabinetLoadingEvidenceService.java`;证据写入和 multipart 上传均先以任务内幂等键匹配请求哈希,上传哈希覆盖仓库、版本、证据类型、锁码、备注、文件名、媒体类型、大小与文件内容;完全重放返回当前视图且不得再次上传 File,相同键不同内容返回冲突 | -| 装柜完成直连事件 | `service/LoadingCompletedOutboxService.java`、`LoadingCompletedOutboxRelay.java`、`repository/LoadingCompletedOutboxRepository.java`、`mapper/LoadingCartonManifestMapper.java`、`loading/mapper/CabinetLoadingEvidenceMapper.java`、`client/LastlegEventClient.java`;当前发布 V3,从 `fl_inbound_order.destination_type` 冻结 `orders[].destinationType`(`FBA` 或 `OVERSEAS`),OVERSEAS 按最终实装与稳定箱码冻结,FBA 可仅冻结箱数,并冻结物理柜五类有效装柜照片引用;真实生产验收 `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`、`loading/mapper/CabinetLoadingEvidenceMapper.java`、`client/LastlegEventClient.java`;当前发布 V3,从 `fl_inbound_order.destination_type` 冻结 `orders[].destinationType`(`FBA` 或 `OVERSEAS`),国内 PRIVATE 按 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` | | 海外暂扣状态回写 | 同一内部事件入口、消费者 `service/OverseasInboundOrderDetainedConsumer.java`;按来源入仓单 ID 更新 `fl_inbound_order.hold_flag/current_hold_reason`,复用跨区 Inbox 并写 `fl_inbound_hold_record` | | 海外派送完成回写 | 同一内部事件入口、消费者 `service/OverseasDeliveryCompletedConsumer.java`;按来源入仓单、订单、入仓码和 FBA 仓校验后推进当前入仓单 `LOADED→DELIVERED`,同一 Order 全部有效入仓单完成后才经 `OrderFulfillmentCompletionClient` 推进 Order `COMPLETED` | diff --git a/firstleg/src/main/java/com/baiyue/firstleg/service/LoadingCompletedOutboxService.java b/firstleg/src/main/java/com/baiyue/firstleg/service/LoadingCompletedOutboxService.java index 63f00e5cb..c2f20cd65 100644 --- a/firstleg/src/main/java/com/baiyue/firstleg/service/LoadingCompletedOutboxService.java +++ b/firstleg/src/main/java/com/baiyue/firstleg/service/LoadingCompletedOutboxService.java @@ -335,11 +335,13 @@ public class LoadingCompletedOutboxService { Set 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"))) { + if (values.size() != 1 + || (!values.contains("FBA") && !values.contains("OVERSEAS") && !values.contains("PRIVATE"))) { throw new BizException(ResultCode.CONFLICT, - "装柜入仓单目的类型必须唯一且为 FBA 或 OVERSEAS,不能生成海外交接事件"); + "装柜入仓单目的类型必须唯一且为 FBA、OVERSEAS 或 PRIVATE,不能生成海外交接事件"); } - return values.iterator().next(); + String value = values.iterator().next(); + return "PRIVATE".equals(value) ? "OVERSEAS" : value; } private String fulfillmentType(List rows) { diff --git a/firstleg/src/test/java/com/baiyue/firstleg/service/LoadingCompletedOutboxServiceTest.java b/firstleg/src/test/java/com/baiyue/firstleg/service/LoadingCompletedOutboxServiceTest.java index 4024a4694..ad49ea843 100644 --- a/firstleg/src/test/java/com/baiyue/firstleg/service/LoadingCompletedOutboxServiceTest.java +++ b/firstleg/src/test/java/com/baiyue/firstleg/service/LoadingCompletedOutboxServiceTest.java @@ -189,13 +189,46 @@ class LoadingCompletedOutboxServiceTest { verify(manifests, org.mockito.Mockito.never()).labelPool(11L, 100L); } + @Test void privateAddressFreezesAsOverseasWithOriginalCartonIdentity() throws Exception { + Row value = row(11, 100, 101); + value.setDestinationType("PRIVATE"); + value.setDestinationWarehouseId(null); + value.setDestinationWarehouseCode(null); + value.setDestinationWarehouseName("私人地址"); + when(snapshots.rows(90L)).thenReturn(List.of(value)); + + service.publish(90L); + + ArgumentCaptor 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("OVERSEAS"); + assertThat(event.payload().at("/orders/0/destinationWarehouseId").isNull()).isTrue(); + assertThat(event.payload().at("/orders/0/destinationWarehouseCode").isNull()).isTrue(); + assertThat(event.payload().at("/orders/0/destinationWarehouseName").asText()).isEqualTo("私人地址"); + assertThat(event.payload().at("/orders/0/items/0/sourceInboundDetailId").asText()).isEqualTo("101"); + assertThat(event.payload().at("/orders/0/items/0/skuCode").asText()).isEqualTo("SKU101"); + assertThat(event.payload().at("/orders/0/items/0/cartonMarks/0").asText()).isEqualTo("BOX-101"); + verify(manifests).labelPool(11L, 100L); + } + + @Test void rejectsMixedPrivateAndOverseasDestinationsInOneInbound() { + Row privateAddress = row(11, 100, 101); + privateAddress.setDestinationType("PRIVATE"); + when(snapshots.rows(90L)).thenReturn(List.of(privateAddress, row(11, 100, 102))); + + assertThatThrownBy(() -> service.publish(90L)).hasMessageContaining("目的类型必须唯一"); + verify(outbox, org.mockito.Mockito.never()).insert( + org.mockito.ArgumentMatchers.any(InboundOutboxEntity.class)); + } + @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"); + .hasMessageContaining("目的类型必须唯一且为 FBA、OVERSEAS 或 PRIVATE"); verify(outbox, org.mockito.Mockito.never()).insert( org.mockito.ArgumentMatchers.any(InboundOutboxEntity.class)); }