Files
baiyue-lastleg/docs/firstleg-loading-to-lastleg-orders.md
2026-08-13 17:10:02 +08:00

9.8 KiB
Raw Permalink Blame History

头程装柜完成生成尾程订单实施基线

1. 已确认范围

  • 头程一个柜次可以包含多个客户、多个入仓单和每个入仓单下的多条商品明细。
  • 尾程订单以“本柜最终实装入仓单”为建单维度:一笔实装入仓单生成一笔尾程订单。
  • 一个柜次因此可以生成多笔尾程订单。
  • 同一客户在同一柜次中的多笔入仓单不得按海外仓、FBA 仓、渠道或地址再次合并。
  • 头程与尾程保持独立数据库和运行环境,尾程不得回查头程数据库或调用头程业务接口补齐建单数据。

2. 业务触发点

事件只能在 PDA 人工确认装柜完成的本地事务中产生。头程当前链路为:

LoadingPickService.complete
  -> PdaPickSettlementService.settle
  -> LoadingListService.finishFromPda
  -> 装柜计划 LOADING -> COMPLETED
  -> 同一事务写头程业务 Outbox

不能以下列动作作为尾程建单触发点:

  • 页面进入“装柜完成”页签;
  • 单条拣货完成;
  • 入仓单状态投影更新;
  • Event Bridge 收到同步 HTTP 请求后再反查头程数据。

3. 租户隔离与事件数量

装柜计划明细允许包含多个 account_id,而事件信封只有一个 tenantId。因此一个柜次不能发布一个包含所有客户数据的事件。

发布规则:

一个已完成柜次
  -> 按 account_id 分组
  -> 每个 account_id 发布一个 FirstLegLoadingPlanCompleted 事件
  -> 每个事件的 orders 包含该客户在本柜中的全部实装入仓单
  -> 尾程对 orders 中每一项分别建单

事件级幂等键:

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:

{
  "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. 尾程建单幂等

尾程订单表必须有以下唯一约束:

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. 尾程订单初始状态

事件自动生成的订单建议使用:

PENDING_ASSIGNMENT(待分配)

首期状态机建议:

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 后仍能完成验签、查询和后续履约。