优化
This commit is contained in:
@@ -14,7 +14,7 @@
|
||||
| 事件 ID 与业务键 | `EventIdentifiers.java` |
|
||||
| Payload 规范化与哈希 | `EventPayloadCanonicalizer.java`、`EventPayloadHash.java` |
|
||||
| 事件类型与区域 | `EventTypes.java`、`RegionCode.java` |
|
||||
| 头程装柜完成 V2(当前生产) | `FirstLegLoadingPlanCompletedV2.java`;订单级冻结 `destinationType=FBA|OVERSEAS`,FBA 可按箱数接收,OVERSEAS 逐明细冻结与实装箱数一致的 SKU/箱号;同时冻结来源/根订单与入仓标识、客户、业务类型、目的仓/备注/托盘及一键代发托盘 SKU 快照;国内生产者见 `../firstleg/src/main/java/com/baiyue/firstleg/service/LoadingCompletedOutboxService.java`,海外消费者见 `../lastleg-service/src/main/java/com/baiyue/lastleg/service/LoadingCompletedConsumer.java` |
|
||||
| 头程装柜完成 V3(当前生产) | `FirstLegLoadingPlanCompletedV3.java` 在 V2 订单快照上增加物理柜空仓、半仓、满仓、锁仓、带锁码五类不可变照片引用;订单级仍冻结 `destinationType=FBA|OVERSEAS`,FBA 可按箱数接收,OVERSEAS 逐明细冻结与实装箱数一致的 SKU/箱号;国内生产者见 `../firstleg/src/main/java/com/baiyue/firstleg/service/LoadingCompletedOutboxService.java`,海外消费者见 `../lastleg-service/src/main/java/com/baiyue/lastleg/service/LoadingCompletedConsumer.java`;V1/V2 仅用于历史事件读取 |
|
||||
| 头程装柜完成 V1 解析模型 | `FirstLegLoadingPlanCompletedV1.java`;用于消费端冻结字段映射,新增生产不得回退为 V1 |
|
||||
| 海外卸柜完成 V1 | `OverseasContainerUnloadedV1.java`;按来源柜预订 ID 做柜级聚合/幂等,同一柜只发布一次,信封固定使用 `tenantId=0` 平台技术范围;`unloadingCompletedAt` 必须是全柜 complete 时刻,不得用签收拍照时刻代替;海外生产者见 `../lastleg-service/src/main/java/com/baiyue/lastleg/inboundplan/pda/service/InboundUnloadCompletedOutboxService.java`,国内消费者见 `../firstleg/src/main/java/com/baiyue/firstleg/service/OverseasContainerEventConsumer.java` |
|
||||
| 海外入仓单暂扣变更 V1 | `OverseasInboundOrderDetainedChangedV1.java`;聚合键固定为来源头程入仓单 ID,暂扣与解除暂扣均由尾程 Outbox 回传,国内按入仓单绑定、客户归属及聚合版本单调消费 |
|
||||
@@ -24,7 +24,7 @@
|
||||
| 海外业务资金指令与结果 V1 | `WalletCommandRequestedV1.java`、`WalletCommandCompletedV1.java`;海外业务通过 Outbox 请求国内 Finance 冻结、结算或释放,Finance 执行后再经 Outbox 回传结果,双方以事件与业务幂等键防重,不跨区域同步调用钱包 |
|
||||
| IAM 安全事件 V1 | `IamSecurityEventsV1.java`;账号、授权快照等安全事件的冻结模型 |
|
||||
| 契约说明 | `package-info.java` |
|
||||
| 契约测试 | `src/test/java/com/baiyue/common/event/EventEnvelopeTest.java`、`FirstLegLoadingPlanCompletedV1Test.java`、`FirstLegLoadingPlanCompletedV2Test.java`、`OverseasContainerUnloadedV1Test.java`、`FirstLegContainerShippingNodeRecordedV1Test.java` |
|
||||
| 契约测试 | `src/test/java/com/baiyue/common/event/EventEnvelopeTest.java`、`FirstLegLoadingPlanCompletedV1Test.java`、`FirstLegLoadingPlanCompletedV2Test.java`、`FirstLegLoadingPlanCompletedV3Test.java`、`OverseasContainerUnloadedV1Test.java`、`FirstLegContainerShippingNodeRecordedV1Test.java` |
|
||||
| 真实生产/消费验收 | `../firstleg/src/test/java/com/baiyue/firstleg/service/TaillegRealProducerAcceptanceTest.java`、`../lastleg-service/src/test/java/com/baiyue/lastleg/service/TaillegRealConsumerAcceptanceTest.java` |
|
||||
|
||||
## 修改与验证
|
||||
|
||||
+45
@@ -0,0 +1,45 @@
|
||||
package com.baiyue.common.event;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import java.util.List;
|
||||
|
||||
/** 装柜完成 V3:在 V2 货物快照上增加整柜五类国内装柜照片引用。 */
|
||||
public record FirstLegLoadingPlanCompletedV3(
|
||||
String loadingPlanId, String planNo,
|
||||
@JsonProperty("sourceContainerBookingId") String containerBookingId,
|
||||
String containerNo, String cabinetNo, String warehouseId, String warehouseName,
|
||||
String estimatedPortArrivalAt, String actualPortArrivalAt, String estimatedWarehouseArrivalAt,
|
||||
List<FirstLegLoadingPlanCompletedV2.Order> orders, List<LoadingEvidence> loadingEvidence) {
|
||||
public FirstLegLoadingPlanCompletedV3 {
|
||||
if (loadingPlanId == null || loadingPlanId.isBlank()
|
||||
|| containerBookingId == null || containerBookingId.isBlank()) {
|
||||
throw new IllegalArgumentException("loadingPlanId and sourceContainerBookingId are required");
|
||||
}
|
||||
containerNo = ContainerNumbers.requireCanonical(containerNo);
|
||||
orders = orders == null ? List.of() : List.copyOf(orders);
|
||||
loadingEvidence = loadingEvidence == null ? List.of() : List.copyOf(loadingEvidence);
|
||||
if (orders.isEmpty()) throw new IllegalArgumentException("orders are required");
|
||||
var owner = orders.get(0);
|
||||
if (orders.stream().anyMatch(order -> !owner.customerId().equals(order.customerId())
|
||||
|| !owner.customerName().equals(order.customerName()))) {
|
||||
throw new IllegalArgumentException("orders must share one immutable customerId/customerName snapshot");
|
||||
}
|
||||
if (loadingEvidence.size() != 5
|
||||
|| loadingEvidence.stream().map(LoadingEvidence::evidenceType).distinct().count() != 5) {
|
||||
throw new IllegalArgumentException("five unique loading evidence types are required");
|
||||
}
|
||||
}
|
||||
|
||||
public record LoadingEvidence(String evidenceType, long fileId, String fileName) {
|
||||
public LoadingEvidence {
|
||||
if (evidenceType == null || !evidenceType.matches("PHOTO_STEP_[1-4]|SEAL")) {
|
||||
throw new IllegalArgumentException("unsupported loading evidence type");
|
||||
}
|
||||
if (fileId <= 0) throw new IllegalArgumentException("loading evidence fileId must be positive");
|
||||
if (fileName == null || fileName.isBlank() || fileName.length() > 255) {
|
||||
throw new IllegalArgumentException("loading evidence fileName is required");
|
||||
}
|
||||
fileName = fileName.trim();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -5,6 +5,25 @@ info:
|
||||
paths: {}
|
||||
components:
|
||||
schemas:
|
||||
FirstLegLoadingPlanCompletedV3:
|
||||
allOf:
|
||||
- $ref: '#/components/schemas/FirstLegLoadingPlanCompletedV2'
|
||||
- type: object
|
||||
required: [loadingEvidence]
|
||||
properties:
|
||||
loadingEvidence:
|
||||
type: array
|
||||
minItems: 5
|
||||
maxItems: 5
|
||||
description: Exactly one active domestic loading photo reference for each of PHOTO_STEP_1 through PHOTO_STEP_4 and SEAL.
|
||||
items: {$ref: '#/components/schemas/FirstLegLoadingEvidenceV3'}
|
||||
FirstLegLoadingEvidenceV3:
|
||||
type: object
|
||||
required: [evidenceType, fileId, fileName]
|
||||
properties:
|
||||
evidenceType: {type: string, enum: [PHOTO_STEP_1, PHOTO_STEP_2, PHOTO_STEP_3, PHOTO_STEP_4, SEAL]}
|
||||
fileId: {type: integer, format: int64, minimum: 1, description: Domestic File service identity; never exposed to PDA clients}
|
||||
fileName: {type: string, minLength: 1, maxLength: 255}
|
||||
FirstLegLoadingPlanCompletedV2:
|
||||
type: object
|
||||
required: [loadingPlanId, sourceContainerBookingId, containerNo, estimatedPortArrivalAt, actualPortArrivalAt, estimatedWarehouseArrivalAt, orders]
|
||||
|
||||
+46
@@ -0,0 +1,46 @@
|
||||
package com.baiyue.common.event;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatThrownBy;
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import java.util.List;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
class FirstLegLoadingPlanCompletedV3Test {
|
||||
@Test
|
||||
void carriesExactlyFiveDomesticLoadingPhotoReferences() {
|
||||
var json = new ObjectMapper().valueToTree(payload(evidence()));
|
||||
|
||||
assertThat(json.path("loadingEvidence")).hasSize(5);
|
||||
assertThat(json.at("/loadingEvidence/4/evidenceType").asText()).isEqualTo("SEAL");
|
||||
}
|
||||
|
||||
@Test
|
||||
void rejectsMissingOrDuplicatePhotoTypes() {
|
||||
assertThatThrownBy(() -> payload(evidence().subList(0, 4)))
|
||||
.hasMessageContaining("five unique");
|
||||
assertThatThrownBy(() -> payload(List.of(
|
||||
photo("PHOTO_STEP_1", 1), photo("PHOTO_STEP_1", 2), photo("PHOTO_STEP_2", 3),
|
||||
photo("PHOTO_STEP_3", 4), photo("SEAL", 5))))
|
||||
.hasMessageContaining("five unique");
|
||||
}
|
||||
|
||||
private FirstLegLoadingPlanCompletedV3 payload(List<FirstLegLoadingPlanCompletedV3.LoadingEvidence> photos) {
|
||||
var order = new FirstLegLoadingPlanCompletedV2.Order(
|
||||
"2", "2", "3", "ORD", "3", "RK", null, "ONT8", "FBA ONT8",
|
||||
2, 20, 0, null, null, null, List.of(), "11", "客户甲", "FIRST_LEG", List.of(), "FBA");
|
||||
return new FirstLegLoadingPlanCompletedV3(
|
||||
"4", "P", "5", "ZCSU7881970", "CAB", "6", "WH",
|
||||
null, null, null, List.of(order), photos);
|
||||
}
|
||||
|
||||
private List<FirstLegLoadingPlanCompletedV3.LoadingEvidence> evidence() {
|
||||
return List.of(photo("PHOTO_STEP_1", 1), photo("PHOTO_STEP_2", 2),
|
||||
photo("PHOTO_STEP_3", 3), photo("PHOTO_STEP_4", 4), photo("SEAL", 5));
|
||||
}
|
||||
|
||||
private FirstLegLoadingPlanCompletedV3.LoadingEvidence photo(String type, long id) {
|
||||
return new FirstLegLoadingPlanCompletedV3.LoadingEvidence(type, id, type + ".jpg");
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user