Files
baiyue-lastleg/docs/firstleg-loading-to-lastleg-orders.md
T
2026-08-18 11:39:40 +08:00

276 lines
11 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 头程装柜完成生成尾程订单实施基线
## 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",
"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 消费者忽略未知字段;已有字段不改变含义或类型。
### 5.1 订单类型分支
`orderType`、`pallets`、`palletNo`、`skuPackages` 和 `cartonMarks` 必须正式加入头程公共事件契约及发送端,不能长期依赖尾程读取原始 JSON 扩展字段。三个服务升级到同一 `common-event-contract` 版本后再启用生产流量。
`ONE_CLICK_DROPSHIP` 的一笔最终实装入仓单可包含一个或多个来源托盘快照。消费时必须满足:所有托盘的 SKU 箱数之和等于订单箱数、SKU 数量之和等于订单数量、每个 SKU 的箱唛数等于其箱数、同一托盘内 SKU 不重复、同一订单内来源托盘号和箱唛不重复。通过后按来源托盘创建一件代发档案及托盘级 SKU 库存,并创建箱唛记录和不可变箱唛流水。`source_pallet_no` 是头程冻结快照;`warehouse_pallet_id/pallet_no` 是 Warehouse API 分配的海外仓实物托盘,两者不得混用。
非 `ONE_CLICK_DROPSHIP` 类型只创建 `ll_order`、冻结明细和初始状态历史,来源 `palletCount` 按 0 入库,不创建一件代发 SKU/箱唛库存。后续入仓 PDA 分配的托盘属于海外仓作业资源。
## 6. 尾程建单幂等
尾程订单表必须有以下唯一约束:
```text
UNIQUE (customer_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` 重复到达直接返回既有结果。相同来源入仓单且快照相同视为幂等;相同来源入仓单但关键快照不同必须进入冲突状态并告警,不能静默覆盖已开始履约的尾程订单。
事件内任意一笔订单建单失败时,整个事件消费事务回滚,不允许出现同一客户同一柜次只生成部分尾程订单。
同一实际集装箱的订单共享 `order_no`、`container_no` 和 `source_loading_plan_id`;`ll_order.id` 仍是每条订单独立的雪花 ID。整柜查询、整柜操作和转入库计划统一以 `containerNo` 聚合,不使用虚拟柜号 `cabinetNo`。数据库必须保留 `KEY idx_ll_order_container (customer_id, container_no)`。
## 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 后仍能完成验签、查询和后续履约。