Files

267 lines
9.8 KiB
Markdown
Raw Permalink Normal View History

2026-08-13 17:10:02 +08:00
# 头程装柜完成生成尾程订单实施基线
## 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 后仍能完成验签、查询和后续履约。