1
This commit is contained in:
@@ -78,3 +78,4 @@ com.baiyue.shared:common-event-contract
|
||||
|
||||
详细仓库边界见 [仓库边界说明](docs/repository-boundary.md)。
|
||||
|
||||
头程装柜完成后按实装入仓单批量生成尾程订单的事件契约、幂等规则和开发窗口拆分,见 [头程装柜完成生成尾程订单实施基线](docs/firstleg-loading-to-lastleg-orders.md)。
|
||||
|
||||
@@ -0,0 +1,266 @@
|
||||
# 头程装柜完成生成尾程订单实施基线
|
||||
|
||||
## 1. 已确认范围
|
||||
|
||||
- 头程一个柜次可以包含多个客户、多个入仓单和每个入仓单下的多条商品明细。
|
||||
- 尾程订单以“本柜最终实装入仓单”为建单维度:一笔实装入仓单生成一笔尾程订单。
|
||||
- 一个柜次因此可以生成多笔尾程订单。
|
||||
- 同一客户在同一柜次中的多笔入仓单不得按海外仓、FBA 仓、渠道或地址再次合并。
|
||||
- 头程与尾程保持独立数据库和运行环境,尾程不得回查头程数据库或调用头程业务接口补齐建单数据。
|
||||
|
||||
## 2. 业务触发点
|
||||
|
||||
事件只能在 PDA 人工确认装柜完成的本地事务中产生。头程当前链路为:
|
||||
|
||||
```text
|
||||
LoadingPickService.complete
|
||||
-> PdaPickSettlementService.settle
|
||||
-> LoadingListService.finishFromPda
|
||||
-> 装柜计划 LOADING -> COMPLETED
|
||||
-> 同一事务写头程业务 Outbox
|
||||
```
|
||||
|
||||
不能以下列动作作为尾程建单触发点:
|
||||
|
||||
- 页面进入“装柜完成”页签;
|
||||
- 单条拣货完成;
|
||||
- 入仓单状态投影更新;
|
||||
- Event Bridge 收到同步 HTTP 请求后再反查头程数据。
|
||||
|
||||
## 3. 租户隔离与事件数量
|
||||
|
||||
装柜计划明细允许包含多个 `account_id`,而事件信封只有一个 `tenantId`。因此一个柜次不能发布一个包含所有客户数据的事件。
|
||||
|
||||
发布规则:
|
||||
|
||||
```text
|
||||
一个已完成柜次
|
||||
-> 按 account_id 分组
|
||||
-> 每个 account_id 发布一个 FirstLegLoadingPlanCompleted 事件
|
||||
-> 每个事件的 orders 包含该客户在本柜中的全部实装入仓单
|
||||
-> 尾程对 orders 中每一项分别建单
|
||||
```
|
||||
|
||||
事件级幂等键:
|
||||
|
||||
```text
|
||||
FIRSTLEG_LOADING_COMPLETED:{loadingPlanId}:{tenantId}:{handoverVersion}
|
||||
```
|
||||
|
||||
## 4. 入仓单标识规则
|
||||
|
||||
PDA 部分拣货会生成本柜实装 `LOADED` 分片以及可继续分柜的 `REMAINDER` 分片。事件中的 `sourceInboundId` 必须取本柜最终实装入仓单 ID,不能固定使用拆分前的父入仓单 ID。
|
||||
|
||||
每个订单单元同时保存:
|
||||
|
||||
| 字段 | 语义 |
|
||||
|---|---|
|
||||
| `sourceInboundId` | 本柜最终实装入仓单 ID,尾程建单唯一来源 |
|
||||
| `sourceInboundCode` | 本柜最终实装入仓码快照 |
|
||||
| `rootInboundId` | 头程根入仓单 ID,用于追溯 |
|
||||
| `sourceOrderId` | 本柜最终实装订单 ID |
|
||||
| `sourceOrderNo` | 本柜最终实装订单号快照 |
|
||||
| `rootOrderId` | 根订单 ID,用于追溯 |
|
||||
|
||||
未发生部分拆分时,`sourceInboundId` 可以等于原入仓单 ID。发生拆分时,必须使用 `LOADED` 分片 ID。
|
||||
|
||||
## 5. 事件契约
|
||||
|
||||
事件信封:
|
||||
|
||||
| 属性 | 值 |
|
||||
|---|---|
|
||||
| `eventType` | `FirstLegLoadingPlanCompleted` |
|
||||
| `eventVersion` | `1` |
|
||||
| `sourceRegion` | `CN` |
|
||||
| `targetRegion` | `OVERSEAS` |
|
||||
| `tenantId` | 当前分组的客户账号 ID |
|
||||
| `aggregateType` | `LOADING_PLAN` |
|
||||
| `aggregateId` | 装柜计划 ID |
|
||||
| `aggregateVersion` | 装柜计划完成后的版本 |
|
||||
| `handoverVersion` | 当前租户在该柜次的交接版本,首版为 1 |
|
||||
|
||||
建议的 V1 payload:
|
||||
|
||||
```json
|
||||
{
|
||||
"loadingPlanId": "208600001",
|
||||
"loadingPlanNo": "LP202608130001",
|
||||
"containerBookingId": "208500001",
|
||||
"containerNo": "MSCU1234567",
|
||||
"cabinetNo": "CAB20260813001",
|
||||
"containerType": "40HQ",
|
||||
"sealNo": "SEAL001",
|
||||
"voyageNo": "V001",
|
||||
"originWarehouseId": "1001",
|
||||
"completedAt": "2026-08-13T08:30:00Z",
|
||||
"orders": [
|
||||
{
|
||||
"sourceInboundId": "208700001",
|
||||
"sourceInboundCode": "RK-202608130001",
|
||||
"rootInboundId": "208600101",
|
||||
"sourceOrderId": "208800001",
|
||||
"sourceOrderNo": "FL202608130001",
|
||||
"rootOrderId": "208800001",
|
||||
"customerName": "示例客户",
|
||||
"channelCode": "SEA",
|
||||
"channelName": "海运",
|
||||
"destinationWarehouseId": "3001",
|
||||
"destinationWarehouseCode": "ONT8",
|
||||
"destinationWarehouseName": "FBA ONT8",
|
||||
"packageCount": 10,
|
||||
"palletCount": 2,
|
||||
"grossWeightKg": 1200.000,
|
||||
"volumeCbm": 8.5000,
|
||||
"items": [
|
||||
{
|
||||
"sourceInboundDetailId": "208710001",
|
||||
"skuCode": "SKU-001",
|
||||
"productNameZh": "示例商品",
|
||||
"productNameEn": "Sample product",
|
||||
"packageCount": 10,
|
||||
"productQuantity": 200,
|
||||
"unitsPerCarton": 20,
|
||||
"grossWeightKg": 1200.000,
|
||||
"volumeCbm": 8.5000,
|
||||
"fbaTrackingNo": "FBA123",
|
||||
"shipmentNo": "SHIP123",
|
||||
"shippingMark": "MARK-001"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
约束:
|
||||
|
||||
- `orders` 非空,并按 `sourceInboundId` 升序输出,保证 payload 摘要稳定。
|
||||
- 同一个事件内 `sourceInboundId` 唯一。
|
||||
- `items` 非空,并按 `sourceInboundDetailId` 升序输出。
|
||||
- 订单汇总箱数、重量和体积必须由该订单的实装明细计算,不使用拆分前总量。
|
||||
- 文件只传不可变文件引用、区域和摘要,不传二进制内容。
|
||||
- V1 消费者忽略未知字段;已有字段不改变含义或类型。
|
||||
|
||||
## 6. 尾程建单幂等
|
||||
|
||||
尾程订单表必须有以下唯一约束:
|
||||
|
||||
```text
|
||||
UNIQUE (tenant_id, source_region, source_inbound_id)
|
||||
```
|
||||
|
||||
建议同时保存:
|
||||
|
||||
- `source_event_id`
|
||||
- `source_loading_plan_id`
|
||||
- `source_loading_plan_version`
|
||||
- `source_handover_version`
|
||||
- `source_payload_hash`
|
||||
|
||||
消费事务必须原子完成:
|
||||
|
||||
1. 锁定或领取尾程业务 Inbox;
|
||||
2. 校验事件类型、版本、租户和 payload;
|
||||
3. 对 `orders` 中每一笔执行幂等建单;
|
||||
4. 保存商品明细和来源快照;
|
||||
5. 将业务 Inbox 标记为 `PROCESSED`。
|
||||
|
||||
相同 `eventId + payloadHash` 重复到达直接返回既有结果。相同来源入仓单且快照相同视为幂等;相同来源入仓单但关键快照不同必须进入冲突状态并告警,不能静默覆盖已开始履约的尾程订单。
|
||||
|
||||
事件内任意一笔订单建单失败时,整个事件消费事务回滚,不允许出现同一客户同一柜次只生成部分尾程订单。
|
||||
|
||||
## 7. 尾程订单初始状态
|
||||
|
||||
事件自动生成的订单建议使用:
|
||||
|
||||
```text
|
||||
PENDING_ASSIGNMENT(待分配)
|
||||
```
|
||||
|
||||
首期状态机建议:
|
||||
|
||||
```text
|
||||
PENDING_ASSIGNMENT -> ASSIGNED -> ARRIVED -> CUSTOMS_CLEARED
|
||||
-> DEVANNED -> WAREHOUSED -> DISPATCHED -> DELIVERED
|
||||
```
|
||||
|
||||
异常使用独立异常标记和历史记录,不用一个 `ABNORMAL` 主状态覆盖实际履约节点。原型里的“是否异常”和“订单状态”应分别存储。
|
||||
|
||||
## 8. IAM 接入边界
|
||||
|
||||
- 账号、登录、租户、角色、菜单和按钮权限仍由头程 Auth/IAM 唯一写入。
|
||||
- 头程需要从 HS256 迁移到 RS256 或 ES256,并发布可轮换 JWKS。
|
||||
- 尾程网关和业务服务只验证公钥签名及本地权限投影,不读取国内 Redis,不保存签名私钥。
|
||||
- 用户停用、租户冻结和权限版本变化通过跨区事件更新尾程本地投影。
|
||||
- 尾程权限建议使用独立命名空间,例如 `platform:lastleg:order:list`、`platform:lastleg:order:assign`、`platform:lastleg:order:transfer` 和 `platform:lastleg:order:merge-pallet`,权限主数据仍维护在头程 IAM。
|
||||
|
||||
## 9. 独立窗口拆分
|
||||
|
||||
### W0:契约基线
|
||||
|
||||
仓库:头程公共事件契约和本文档。
|
||||
|
||||
交付:事件常量、V1 payload DTO/JSON Schema、兼容性测试和样例。W1-W4 开始编码前必须冻结字段语义。
|
||||
|
||||
### W1:头程事件生产
|
||||
|
||||
仓库:`baiyue-firstleg` 的 `firstleg` 模块。
|
||||
|
||||
交付:按租户/最终实装入仓单构建快照;在装柜完成事务中写业务 Outbox;发布器、重试和测试。
|
||||
|
||||
不得修改尾程代码。必须覆盖多租户同柜、多入仓单、部分拆分、重复完成和事务回滚测试。
|
||||
|
||||
### W2:跨区 Event Bridge
|
||||
|
||||
仓库:两仓的 `event-bridge-service`,两端分别提交。
|
||||
|
||||
交付:Outbox 扫描、租约、HTTPS/mTLS 发送、Inbox 调度、指数退避、死信、人工补投、指标和告警。
|
||||
|
||||
不得在 Event Bridge 中执行尾程领域建单。
|
||||
|
||||
### W3:尾程订单领域
|
||||
|
||||
仓库:`baiyue-lastleg/lastleg`。
|
||||
|
||||
交付:尾程订单、明细、状态历史、业务 Inbox、事件消费者、列表/详情 API 和 OpenAPI;一笔 `sourceInboundId` 对应一笔尾程订单。
|
||||
|
||||
### W4:统一 IAM
|
||||
|
||||
仓库:`baiyue-firstleg` 的 `auth`、`iam`、`common-security`、`gateway`,以及尾程 `gateway` 的契约测试。
|
||||
|
||||
交付:非对称 JWT、JWKS、密钥轮换、尾程权限资源、权限/冻结事件和尾程本地拒绝策略。
|
||||
|
||||
### W5:尾程作业能力
|
||||
|
||||
仓库:`baiyue-lastleg/lastleg`,在 W3 数据模型冻结后开始。
|
||||
|
||||
交付:分配、批量转仓、批量合托、到港、清关、拆柜、入仓、派送和签收命令及审计。
|
||||
|
||||
### W6:前端原型
|
||||
|
||||
仓库:待确认的前端仓库。
|
||||
|
||||
交付:尾程订单列表、待分配页签、筛选、详情、批量操作和权限按钮。当前尾程仓没有前端工程,不应将页面代码放入 Maven 后端仓。
|
||||
|
||||
### W7:双仓联调验收
|
||||
|
||||
在 W1-W5 完成后执行。覆盖重复、乱序、跨区断网、Event Bridge 重启、数据库事务回滚、毒消息、人工补投、权限冻结和 JWKS 轮换。
|
||||
|
||||
## 10. 并行规则
|
||||
|
||||
- W0 先完成;W1、W2、W3、W4 可在契约冻结后并行。
|
||||
- W5 等 W3 的表结构和状态机冻结后开始。
|
||||
- W6 等 OpenAPI 冻结且前端仓确认后开始。
|
||||
- 每个窗口独立分支,禁止两个窗口同时修改同一模块。
|
||||
- 两仓分别提交和评审,不能用一个提交跨越两个 Git 仓库。
|
||||
|
||||
## 11. 完成定义
|
||||
|
||||
- 一个柜次、一个租户、三笔实装入仓单,稳定生成三笔尾程订单。
|
||||
- 一个柜次包含两个租户时,产生两个隔离事件,任何一方 payload 不包含另一租户数据。
|
||||
- 部分拣货使用 `LOADED` 分片建单,余量分到后续柜次时能生成另一笔尾程订单且不冲突。
|
||||
- 同一事件投递 10 次只产生一次业务效果。
|
||||
- 消费到第 N 笔失败时不保留前 N-1 笔半成品订单。
|
||||
- 跨区中断后恢复能够自动追平,无需重新点击头程“装柜完成”。
|
||||
- 尾程完全断开国内 MySQL、Redis、Nacos 和业务 Feign 后仍能完成验签、查询和后续履约。
|
||||
Reference in New Issue
Block a user