From b1772bce423d138bd42df30c4a0241e4e55b123c Mon Sep 17 00:00:00 2001 From: liuyihe <2513762445@qq.com> Date: Thu, 13 Aug 2026 17:10:02 +0800 Subject: [PATCH] 1 --- README.md | 1 + docs/firstleg-loading-to-lastleg-orders.md | 266 +++++++++++++++++++++ 2 files changed, 267 insertions(+) create mode 100644 docs/firstleg-loading-to-lastleg-orders.md diff --git a/README.md b/README.md index 7544a2c..e34041c 100644 --- a/README.md +++ b/README.md @@ -78,3 +78,4 @@ com.baiyue.shared:common-event-contract 详细仓库边界见 [仓库边界说明](docs/repository-boundary.md)。 +头程装柜完成后按实装入仓单批量生成尾程订单的事件契约、幂等规则和开发窗口拆分,见 [头程装柜完成生成尾程订单实施基线](docs/firstleg-loading-to-lastleg-orders.md)。 diff --git a/docs/firstleg-loading-to-lastleg-orders.md b/docs/firstleg-loading-to-lastleg-orders.md new file mode 100644 index 0000000..35cf437 --- /dev/null +++ b/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 后仍能完成验签、查询和后续履约。