This commit is contained in:
2026-09-29 18:47:37 +08:00
parent 2c60a29e0f
commit c2927ab9a6
291 changed files with 4008 additions and 6298 deletions
+2 -2
View File
@@ -20,7 +20,7 @@
| 海外入仓单暂扣变更 V1 | `OverseasInboundOrderDetainedChangedV1.java`;聚合键固定为来源头程入仓单 ID,暂扣与解除暂扣均由尾程 Outbox 回传,国内按入仓单绑定、客户归属及聚合版本单调消费 |
| 海外派送完成 V1 | `OverseasDeliveryCompletedV1.java`、`EventTypes.OVERSEAS_DELIVERY_COMPLETED`;每个按 FBA 仓拆分的派送单完成后,按关联来源入仓单分别发布,携带来源订单、入仓码、FBA 仓和签收时间,国内幂等推进对应 Firstleg 入仓单与 Order 完成 |
| 国内船节点记录 V1 | `FirstLegContainerShippingNodeRecordedV1.java`;按 `containerBookingId` 柜级聚合,不携带客户,信封固定使用 `tenantId=0` 平台技术范围;只接受十节点中带实际时间和完整供应商审计字段的事实,国内生产者 `../firstleg/src/main/java/com/baiyue/firstleg/portschedule/service/CarrierScheduleNodeOutboxService.java`,海外消费者 `../lastleg-service/src/main/java/com/baiyue/lastleg/shipping/service/ContainerShippingNodeConsumer.java` |
| 海外业务费用来源变更 V1 | `BusinessFeeSourceChangedV1.java`、`EventTypes.BUSINESS_FEE_SOURCE_CHANGED`;携带可独立校验入池的完整不可变客户、业务、方向、供应商、付款、来源版本与哈希快照,长整型标识使用字符串;海外生产者见 `../lastleg-service/src/main/java/com/baiyue/lastleg/financefee/`,Event Bridge 定向投递国内 `finance/`,Finance 不跨区同步回查 |
| 海外业务费用来源变更 V1 | `BusinessFeeSourceChangedV1.java`、`EventTypes.BUSINESS_FEE_SOURCE_CHANGED`;携带可独立校验入池的完整不可变客户、业务、方向、供应商、付款、来源版本与哈希快照,长整型标识使用字符串;海外生产者见 `../lastleg-service/src/main/java/com/baiyue/lastleg/financefee/`,Lastleg Outbox 直接投递 `finance/`,Finance 不跨区同步回查 |
| 海外业务资金指令与结果 V1 | `WalletCommandRequestedV1.java`、`WalletCommandCompletedV1.java`;海外业务通过 Outbox 请求国内 Finance 冻结、结算或释放,Finance 执行后再经 Outbox 回传结果,双方以事件与业务幂等键防重,不跨区域同步调用钱包 |
| IAM 安全事件 V1 | `IamSecurityEventsV1.java`;账号、授权快照等安全事件的冻结模型 |
| 契约说明 | `package-info.java` |
@@ -29,7 +29,7 @@
## 修改与验证
- 所有字段与事件类型都视为跨仓公共契约;当前正式模型不保留旧事件字段、旧版本或兼容解析,契约变化时直接修改当前模型,并同步更新 `event-bridge-service/`、国内生产者、尾程消费者和全部契约测试。若存在不可变外部事件契约,先单独定义版本升级规则。
- 所有字段与事件类型都视为跨仓公共契约;当前正式模型不保留旧事件字段、旧版本或兼容解析,契约变化时直接修改当前模型,并同步更新 Firstleg/Lastleg/Finance 直接生产者、消费者和全部契约测试。若存在不可变外部事件契约,先单独定义版本升级规则。
- 哈希算法或规范化规则变化必须同步更新全部生产者和消费者,并保留跨语言确定性测试。
- MQ 事件不携带用户 Access Token;生产者和消费者使用 MQ TLS/ACL 或内部服务身份,事件信封负责事件 ID、业务键、来源/目标区域、版本和 Payload 哈希。
- `OVERSEAS_CONTAINER_UNLOADED` 固定从 `OVERSEAS` 发往 `CN`,用于回传海外 PDA 卸柜完成及卸柜照片上传时间;预计报空时间由国内头程按照片上传时间加两天计算,实际报空时间允许后续补充。
@@ -5,7 +5,7 @@ import com.fasterxml.jackson.databind.JsonNode;
import java.time.Instant;
/**
* 跨服务和跨区域传输使用的不可变事件信封。
* 统一测试环境中跨业务服务传输使用的不可变事件信封。
*
* <p>租户 ID 和聚合 ID 使用字符串,避免跨语言传输时发生大整数精度丢失。
* {@code payloadHash} 是原始规范化负载字节的 SHA-256 摘要。</p>
@@ -14,8 +14,6 @@ import java.time.Instant;
* @param eventType 事件类型,不包含版本号
* @param eventVersion 事件契约版本,从 1 开始
* @param occurredAt 业务事实实际发生时间,使用 UTC 时间点
* @param sourceRegion 事件来源业务单元
* @param targetRegion 事件目标业务单元;国内一期允许与来源区域相同
* @param tenantId 事件所属客户租户 ID
* @param aggregateType 聚合类型
* @param aggregateId 聚合 ID
@@ -32,8 +30,6 @@ public record EventEnvelope(
String eventType,
int eventVersion,
Instant occurredAt,
RegionCode sourceRegion,
RegionCode targetRegion,
String tenantId,
String aggregateType,
String aggregateId,
@@ -37,12 +37,6 @@ public final class EventEnvelopeValidator {
if (envelope.occurredAt() == null) {
errors.add("occurredAt 不能为空");
}
if (envelope.sourceRegion() == null) {
errors.add("sourceRegion 不能为空");
}
if (envelope.targetRegion() == null) {
errors.add("targetRegion 不能为空");
}
requireText(errors, "tenantId", envelope.tenantId(), MAX_ID_LENGTH);
if (envelope.aggregateType() == null
|| !AGGREGATE_TYPE_PATTERN.matcher(envelope.aggregateType()).matches()) {
@@ -1,13 +0,0 @@
package com.baiyue.common.event;
/**
* 事件来源与目标部署区域。
*
* <p>区域表示独立业务单元,不表示用户当前所在地。</p>
*/
public enum RegionCode {
/** 国内业务单元。 */
CN,
/** 海外业务单元。 */
OVERSEAS
}
@@ -8,8 +8,7 @@ import static org.assertj.core.api.Assertions.*;
class DeliveryAckTest {
private EventEnvelope event(String type) {
return new EventEnvelope(UUID.randomUUID().toString(), type, 1, Instant.now(),
RegionCode.CN, RegionCode.OVERSEAS, "0", "TEST", "1", 1, null,
return new EventEnvelope(UUID.randomUUID().toString(), type, 1, Instant.now(), "0", "TEST", "1", 1, null,
null, null, null, new ObjectMapper().createObjectNode(), "sha256:" + "a".repeat(64));
}
@@ -13,7 +13,7 @@ import java.util.List;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
@DisplayName("跨区域事件信封契约")
@DisplayName("跨服务事件信封契约")
class EventEnvelopeTest {
private final ObjectMapper objectMapper = new ObjectMapper().registerModule(new JavaTimeModule());
@@ -26,6 +26,8 @@ class EventEnvelopeTest {
EventEnvelope restored = objectMapper.readValue(json, EventEnvelope.class);
assertThat(restored).isEqualTo(source);
assertThat(objectMapper.readTree(json).has("sourceRegion")).isFalse();
assertThat(objectMapper.readTree(json).has("targetRegion")).isFalse();
assertThat(objectMapper.readTree(json).path("eventType").asText())
.isEqualTo(EventTypes.FIRST_LEG_SHIPMENT_DEPARTED);
}
@@ -40,7 +42,7 @@ class EventEnvelopeTest {
@Test
void validatorReturnsAllBasicErrorsAtOnce() {
EventEnvelope envelope = new EventEnvelope(
"bad-id", "bad_type", 0, null, null, null, " ", "bad-type", " ",
"bad-id", "bad_type", 0, null, " ", "bad-type", " ",
-1, 0L, " ", " ", "bad-cause", null, "bad-hash");
List<String> errors = EventEnvelopeValidator.validate(envelope);
@@ -90,8 +92,6 @@ class EventEnvelopeTest {
EventTypes.FIRST_LEG_SHIPMENT_DEPARTED,
1,
Instant.parse("2026-07-29T08:30:00Z"),
RegionCode.CN,
RegionCode.OVERSEAS,
"20001",
"FIRST_LEG_SHIPMENT",
"90001",