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