支持私人地址装柜后按普通海外收货交接
This commit is contained in:
@@ -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`,明细规格拆分留在服务端 |
|
| 货柜与全局智能排柜 | `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/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,相同键不同内容返回冲突 |
|
| 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` |
|
| 海外卸柜完成事件回写 | 内部入口 `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/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` |
|
| 海外派送完成回写 | 同一内部事件入口、消费者 `service/OverseasDeliveryCompletedConsumer.java`;按来源入仓单、订单、入仓码和 FBA 仓校验后推进当前入仓单 `LOADED→DELIVERED`,同一 Order 全部有效入仓单完成后才经 `OrderFulfillmentCompletionClient` 推进 Order `COMPLETED` |
|
||||||
|
|||||||
+5
-3
@@ -335,11 +335,13 @@ public class LoadingCompletedOutboxService {
|
|||||||
Set<String> values = rows.stream().map(Row::getDestinationType)
|
Set<String> values = rows.stream().map(Row::getDestinationType)
|
||||||
.map(value -> value == null ? "" : value.trim().toUpperCase(java.util.Locale.ROOT))
|
.map(value -> value == null ? "" : value.trim().toUpperCase(java.util.Locale.ROOT))
|
||||||
.collect(java.util.stream.Collectors.toSet());
|
.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,
|
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<Row> rows) {
|
private String fulfillmentType(List<Row> rows) {
|
||||||
|
|||||||
+34
-1
@@ -189,13 +189,46 @@ class LoadingCompletedOutboxServiceTest {
|
|||||||
verify(manifests, org.mockito.Mockito.never()).labelPool(11L, 100L);
|
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<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("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() {
|
@Test void rejectsUnfrozenDestinationType() {
|
||||||
Row value = row(11, 100, 101);
|
Row value = row(11, 100, 101);
|
||||||
value.setDestinationType(null);
|
value.setDestinationType(null);
|
||||||
when(snapshots.rows(90L)).thenReturn(List.of(value));
|
when(snapshots.rows(90L)).thenReturn(List.of(value));
|
||||||
|
|
||||||
assertThatThrownBy(() -> service.publish(90L))
|
assertThatThrownBy(() -> service.publish(90L))
|
||||||
.hasMessageContaining("目的类型必须唯一且为 FBA 或 OVERSEAS");
|
.hasMessageContaining("目的类型必须唯一且为 FBA、OVERSEAS 或 PRIVATE");
|
||||||
verify(outbox, org.mockito.Mockito.never()).insert(
|
verify(outbox, org.mockito.Mockito.never()).insert(
|
||||||
org.mockito.ArgumentMatchers.any(InboundOutboxEntity.class));
|
org.mockito.ArgumentMatchers.any(InboundOutboxEntity.class));
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user