Files
docs/海外仓业务/海外仓库位与A0溢出位补齐方案评审稿.md
T
2026-09-22 14:01:47 +08:00

24 KiB
Raw Blame History

海外仓库位与 A0 溢出位补齐方案(评审稿)

1. 目的与范围

当前海外仓可以完成接柜、收货、组托和库存入账,但尚未形成与国内仓一致的“库位—托盘—库存”闭环。本方案只讨论海外仓库位、托盘位置、A0 共享溢出位及入库后的上架/移库,不扩展海外仓主档、客户自助仓储服务或新的库存业务模型。

2. 当前实现与缺口

已有能力

  • warehouse-overseas-service 已拥有海外库位、托盘、资源锁和托盘物理流转能力。
  • 海外入库计划托盘已保存 warehousePalletId、warehouseLocationId 和资源锁。
  • PDA 入库库存落账会读取托盘库位,并写入库存 position 的库位 ID/编码。
  • Lastleg 已有 PDA 商品移库能力,且海外 PDA 已有移库页面和路由。
  • 海外资源服务已经有平台库位 CRUD:/api/platform/v1/warehouse-admin/{warehouseId}/locations;缺的是 Admin 正式海外库位页面和完整业务接入。

主要缺口

  • 真实库位分配通常没有进入入库业务,托盘会落到虚拟收货位。
  • 海外库位没有国内仓已有的 A0 共享溢出语义。
  • wh_location 没有 shared_overflow 标识,普通位和 A0 的并发占用规则未建立。
  • Admin 现有 /platform/v1/locations 是国内仓页面接口,不能作为海外库位接口复用。
  • 海外入库详情不展示当前库位,也没有平台移库入口。

2.1 评审发现的实现偏差

  • 当前资源服务、资源锁和托盘流转代码只把 VIRTUAL 当作非独占库位;仅增加 shared_overflow 字段不会让 A0 生效,reserve/release/scrap/transition/resolve/reservationBlockReason 以及 PDA 库位查询都必须改用统一的“非独占库位”判定。
  • 当前柜级转换会调用 ensurePalletBatch 并把返回的 locationId 写入入库计划托盘。新建托盘通常得到虚拟位,但复用已有托盘时可能已经带有普通位。因此“转换阶段不锁普通位”只对新建资源成立,复用托盘的行为必须在接口契约中明确,不能把所有转换都描述成虚拟位。
  • 当前 PDA 移库把 PROCESSING 记录、远程物理变更和本地库存更新放在同一个 Lastleg 事务中。远程成功而本地失败时,PROCESSING 记录也会回滚,现有实现不具备方案中所称的可重放 Saga;必须先补持久化命令/Outbox 或独立事务状态机。
  • 当前 locationType 仍允许任意字符串,现有测试和数据使用过 STORAGE。本方案采用 NORMAL 后,需在最新基线中直接收敛并同步 OpenAPI、Fixture 和测试,不保留 STORAGE 的双重语义。

3. 目标规则

3.1 库位类型

  • VIRTUAL:虚拟收货位,仅用于收货阶段临时承载。
  • NORMAL:普通库位,例如货架位、地面位,默认一托一位。
  • SHARED_OVERFLOW:系统共享溢出位 A0,每个海外仓默认一个。
  • UNKNOWN:仅用于发现历史缺失或不一致时的诊断返回,不是可创建或可移动到的库位类型。

3.2 A0 规则

  1. 普通库位有可用位置时优先使用普通库位。
  2. 普通位候选竞争失败且现场确认后,托盘进入同仓 A0。
  3. A0 可同时承载多个托盘,但每个托盘必须有独立 resourceLockId 和 reservation_key。
  4. A0 是系统保留编码,不可由后台编辑、停用或删除。
  5. A0 中的单个托盘可以换绑到普通库位,不影响 A0 中其他托盘。
  6. A0 是合法的实际落位,不等于未上架;只有虚拟收货位是 putawayStatus=PENDING。

3.3 库位状态矩阵(必须固定)

库位 locationType shared_overflow status 占用事实
虚拟收货位 VIRTUAL 0 永远 ACTIVE 不以库位状态计数,按托盘和资源锁区分
普通库位 NORMAL 0 空闲 IDLE、占用 OCCUPIED 一个活动托盘;由服务端 CAS/占用事实保证唯一
A0 SHARED_OVERFLOW 1 永远 ACTIVE 多个活动托盘;按托盘、资源锁和 reservation key 区分

locationType 与 shared_overflow 不得出现不一致组合;不一致时查询、预留和流转均失败关闭。A0 不得通过把库位状态改成 OCCUPIED 表示“已占用”,否则第二个托盘会被错误拒绝或释放掉其他托盘。普通位才允许使用 IDLE/OCCUPIED 的库位状态机。

3.4 分配与物理落位必须分离

服务端可以返回普通位优先、A0 兜底的候选目标,但候选不是实物已落位。只有“扫描托盘 + 扫描目标库位”的物理变更成功,并且托盘、资源锁、库存位置和操作记录同步成功后,才把该托盘视为已上架。普通位没有候选时可以返回 A0 候选;第一期不得在没有现场确认的情况下把库存自动改写为 A0,避免把虚拟收货位误报成实际库位。

普通位候选必须由服务端选择,至少按 warehouse_id、启用状态、库位类型和确定排序(建议 zone_code、location_code、id)返回建议;候选读取本身不占用库位,最终上架/移库命令锁定目标并执行 CAS。竞争失败后重选,重选仍无普通位才返回 A0。前端提交的 locationId 只能作为扫描结果,不能绕过候选和同仓校验。若未来需要提前占位,必须另建带命令 ID 和 TTL 的预留表,不能提前把普通位改成 OCCUPIED。

最终命令仍需在服务端复验“普通位优先”:普通位仍有可用位时,直接提交 A0 必须返回稳定阻断原因;若业务确需越过普通位,只能使用独立的 overflow override 权限并要求填写原因,不能由前端隐藏候选来代替门禁。

3.5 分配时机

入库计划转换阶段对新建托盘继续使用虚拟收货位,不提前锁死未来数日可能需要的普通位;复用已有真实托盘时必须保留其真实当前库位并在返回中标明“已有落位”。收货完成后由正式上架命令返回候选,现场确认后再按“普通位优先、A0 兜底”提交真实位置。

平台海外实物资源的虚拟位也统一使用 customer_id=0。历史上按客户作用域创建的虚拟位不得继续生成新资源;迁移后若仍被引用,先完成资源锁和托盘归属收口,再允许进入新流程,不能让公共 resolve 因作用域不同找不到位置。

4. 目标流程

柜到仓 / PDA 签收
  ↓
拆柜收货、扫描箱码、组托
  ↓
托盘暂在虚拟收货位
  ↓
  库存入账
  ↓
  服务端返回普通位候选;无可用普通位时返回 A0 候选
  ↓
  扫描托盘 + 扫描目标库位完成上架/移库
  ↓
  更新托盘、资源锁、库存 position 和移库流水
  ↓
  必要时将 A0 托盘换绑到普通库位

复用已有 NORMAL 或 A0 实际托盘时,不再经过“虚拟位→候选”分支:库存入账直接沿用当前位置并标记该托盘 putawayStatus=COMPLETED;只有当前位为 VIRTUAL 且状态为 PENDING 的托盘才进入上架候选。

5. 改造范围

5.1 warehouse-overseas-service

数据库

在 wh_location 增加:

shared_overflow TINYINT(1) NOT NULL DEFAULT 0 COMMENT '是否为系统共享溢出位A0'

字段本身不构成完整约束。A0 必须使用公共资源作用域 customer_id=0、固定库位码 A0、固定分区 OVERFLOW、location_type=SHARED_OVERFLOW、shared_overflow=1、status=ACTIVE;客户端不能提交或覆盖这些值。每个海外仓只能存在一个 A0,A0 的 ID 和编码永久稳定。max_load_kg 固定使用 999999999.000 哨兵值并在页面显示“不参与容量判定”,候选逻辑不得读取该值。懒创建必须在国内仓目录确认 OVERSEAS + ACTIVE + RECEIVING 后执行,并用固定编码唯一键加并发锁保证幂等;编码冲突、重复 A0、仓库失效或目录不可用均失败关闭。

A0 的懒创建只由候选命令(使用写语义)或正式落位命令触发;普通列表 GET 不产生写副作用。初始化失败必须返回明确的 A0_NOT_READY/A0_CONFLICT 阻断原因,不得静默把托盘留在一个不存在的 A0。

同时补齐:

  • 按 warehouseId 懒创建且幂等的 A0;
  • A0 编码冲突、仓库失效、并发创建的失败规则;
  • 普通库位活动占用唯一约束;
  • A0 允许多托盘共享但按托盘锁区分;
  • 按 resourceLockId/reservationKey 单独释放;
  • 基线 SQL、增量 SQL、中文字段注释和 schema 合同测试。

不能只增加 shared_overflow 字段:现有资源服务大量以 locationType=VIRTUAL 判断非独占位置,需要统一改为“虚拟位或 A0 均为非独占位置”,覆盖 reserve、release、scrap、transition、resolve、reservationBlockReason 和状态校验。A0 的 status 永远保持 ACTIVE,任何托盘进入或离开 A0 都不能修改 A0 的状态;普通位才执行 IDLE/OCCUPIED CAS。

当前 wh_resource_lock 的真实字段名是 reservation_key,不是 reservationId。它可以作为托盘级锁基础,但必须明确 location_id 的语义:本方案将其定义为当前物理库位,并要求移库时与 wh_pallet.location_id 在资源服务内原子更新;wh_pallet_batch_item.location_id 改为 reserved_location_id 快照,不再被当作当前库位。普通位唯一占用必须通过占用事实表,或带 shared_overflow 快照的生成列/唯一索引明确保证,不能只依赖查询后再写入。A0 的多托盘释放、报废必须按托盘和 reservation key 精确匹配,不能释放整个位。

建议复用国内仓二期的模式:保存共享标识快照,普通位用条件唯一约束保证一个活动托盘,A0 不写独占占用状态;海外实现仍以本服务的 wh_ 表和内部接口为准,不跨库复制仓库主档。

库位解析、批量托盘响应、Lastleg Gateway、PDA/Admin DTO 和 OpenAPI 必须贯穿返回 zoneCode、locationType、sharedOverflow、status、version;批量转换不能只返回 locationId,还要让 Lastleg 区分新建虚拟位与复用的真实落位。

接口边界

已有资源 CRUD 继续使用:

/api/platform/v1/warehouse-admin/{warehouseId}/locations

该服务负责库位、托盘资源和内部物理位置变更;不负责更新 Lastleg 库存或入库业务。资源服务的物理变更必须同时更新 wh_pallet、活动资源锁、普通位占用事实和 wh_pallet_operation;Lastleg 只在远程成功后更新自己的库存和业务快照,不能直接写 wh_ 表。

5.2 lastleg-service

  • 增加或扩展平台移库命令,Admin 不直接调用 PDA 权限接口。
  • 入库计划/托盘 DTO 增加库位编码、库位类型、sharedOverflow、库位状态和上架状态。
  • 增加服务端库位候选查询和同仓校验,前端不能任意提交 locationId。
  • 资源 Gateway 的 locationId 使用可空类型兼容历史无库位托盘;库位返回必须显式携带 sharedOverflow、locationType 和 status,调用方不得猜测 A0 语义。
  • 复用 ll_pda_inventory_move 作为平台/PDA 共用移库命令记录,扩展 PREPARED/REMOTE_APPLIED/LOCAL_COMMITTED/COMPENSATION_REQUIRED、租约和远端 operation 快照;平台与 PDA 使用不同的操作人/幂等键命名空间。
  • 移库时同步更新 ll_inventory_position;计划托盘的 warehouse_location_id 只作为当前值冗余字段并在成功移库后同步,wh_pallet_batch_item.location_id 改名为 reserved_location_id 历史快照,收货凭证和库存流水中的位置只读不可变。
  • wh_resource_lock.location_id 定义为当前物理库位;普通位、A0、虚拟位换绑都必须与 wh_pallet.location_id 在资源服务同一事务内更新。任何锁、托盘、库存 position 或计划当前字段不一致时,新的移库、排车和出库均失败关闭并进入对账队列。
  • 合托按实物托盘级移动,同一托盘内多个入仓单的库存明细一起移动;不得只移动某个客户或某条库存 position。若要部分移动,先使用既有拆托/换绑能力形成新实物托盘,再执行整托移库。
  • 移库前锁定同仓该托盘全部库存 position,要求所有 position 的库位一致,保持 available/hold/allocated/picked/receiving 数量桶、客户集合和托盘号不变,并写零差额库存流水。正在收货、拣货、出库、换标冻结或存在未完成命令时返回 canMove=false 和稳定 blockReason;零库存托盘和孤儿 position 先对账处理。
  • 混托场景要同时锁定该物理托盘的全部活动业务锁和业务引用;移动时所有客户的库存 position 一起更新。一方客户库存清零不能释放公共托盘,只有全部库存、活动锁和未完成业务引用均结束后才能释放或报废。

跨服务不使用分布式事务,但不得在持有 Lastleg 数据库事务时调用资源服务。移库命令先独立提交 PREPARED(请求哈希、租约、托盘版本、源位置快照和目标快照),再调用资源服务 transition;远程按 operation key 重放后进入 REMOTE_APPLIED,随后本地锁定全部 position/计划当前字段并提交 LOCAL_COMMITTED。本地失败进入 COMPENSATION_REQUIRED,补偿必须核对远端 afterVersion/afterLocation 未被后续作业修改;无法补偿进入人工对账和告警。批量移库按托盘逐项返回结果,不声称跨托盘全局原子。验收必须覆盖“远程成功、本地失败、重放、后续作业后拒绝补偿”。

同一 move command 重试必须保持原 targetSnapshot 和 requestHash;目标普通位被其他托盘占用时原命令返回冲突,由新候选命令重选,不能用同一幂等键静默改投 A0。只有确认远端 operation 已成功时,才允许按 operation key 恢复本地提交。

优先复用 pda/move 的核心逻辑,不新增绕过资源服务的直接写库接口。

5.3 Admin Web

新增海外仓库位正式页面、route、navigation/menu、权限和接口类型,接入已有海外 locations API,不能复用国内 /platform/v1/locations。

页面支持:海外仓、分区、库位编码、类型、状态、A0 标识、占用摘要;A0 禁止编辑/停用/删除。普通库位改码、改类型、改分区、停用或删除前检查托盘、库存、活动锁和未完成移库,有任何当前引用时阻断。

海外入库详情的托盘明细增加:

  • 当前库位编码和库位类型;
  • A0 标识;
  • 上架状态;
  • 单托/批量移库;
  • 移库历史。

Admin 只调用 Lastleg 平台移库命令,携带当前托盘版本、源位置版本、幂等键、操作原因和源位置快照,并消费服务端返回的 canMove/blockReason 及每托结果。该命令经 gateway-overseas 的平台路由暴露,使用 lastleg:inventory:manage 等后台权限,与 PDA 的 lastleg:pda:operate 分离;请求不接收 customerId,客户集合由服务端从整托库存 position 解析。

5.4 海外 PDA

现有 PDA 已有扫描移库入口。第一期不新增完整“上架任务”体系,但需补充:

  • A0 目标库位展示;
  • sharedOverflow 文案和状态;
  • 虚拟位、普通位、A0 的目标校验;
  • A0 到普通位的换绑。

一件代发当前存在客户作用域的专用虚拟托盘。该特例不能直接套用公共 A0 规则;如后续需要把一件代发托盘转入 A0,必须先完成公共资源归属和锁模型迁移,并单独验收。

6. 状态与门禁

位置类型和流程状态分开:

locationKind: VIRTUAL / NORMAL / SHARED_OVERFLOW / UNKNOWN
putawayStatus: PENDING / COMPLETED / RECONCILIATION_REQUIRED

putawayStatus 当前没有现成字段,第一期建议由当前物理位置派生,并在 DTO 中统一返回:VIRTUAL → PENDING,NORMAL/A0 → COMPLETED,位置为空或多份位置不一致 → RECONCILIATION_REQUIRED。后者必须阻断排车、拣货、出库和再次移库,进入修复队列,不能当作普通的未上架。若为了查询性能落库,只能在 ll_inbound_plan_pallet 增加一个唯一权威字段,由上架命令写入;wh_pallet.location_id 和 ll_inventory_position.warehouse_location_id/code 仍是当前位置事实,计划字段只是同步冗余,ll_inbound_stock_receipt 与库存流水只保留不可变快照。不能让三处各自推进状态。

允许 PDA 收货完成后仍有托盘暂留虚拟位,但虚拟位的 PENDING 必须阻断智能排车、PDA 拣货、出库和发车候选;只有物理落到普通位或 A0 后才进入这些履约候选。INBOUND_COMPLETED 仍表示库存入账完成,不等于已经完成上架。

允许的物理流转为 VIRTUAL→NORMAL/A0、NORMAL↔NORMAL、A0↔NORMAL;NORMAL/A0→VIRTUAL 一律拒绝(仅人工对账恢复命令例外),同一 A0 的 A0→A0 是幂等 no-op,不增加托盘版本、位置占用或库存流水。平台和 PDA 必须共用这套门禁。

7. 容量边界

第一期按“一托一普通库位”判断普通位是否可用,A0 只解决普通位数量不足,不实现重量/体积累计容量。A0 不是无限容量,也不改变托盘自身 capacity_cbm;至少维护当前托盘数、最早进入时间和可配置的软阈值告警,超过硬阈值时停止返回 A0 候选并返回稳定阻断原因。

若后续需要按承重或体积判断,还需增加当前库位占用重量/体积、托盘实际重量/体积快照、并发容量扣减和超限转 A0 的事务门禁。

8. 权限、审计与跨客户

库位查询、库位维护、平台移库、A0 换绑和 PDA 现场移库分别定义权限。平台资源实体仍使用公共资源作用域 customer_id=0,资源锁使用真实业务客户 ID;A0 共享不等于跨客户越权。普通库位和 A0 的查询、候选、库存过滤均必须以 warehouseId 为边界,不能让客户端传入客户或跨仓位置。

移库写入当前版本、幂等键、请求哈希、操作原因、操作人、源/目标库位快照和跨服务关联 key。优先复用现有 wh_pallet_operation、Lastleg 移库记录和库存流水,不新建无关联的第二套历史。增加定时或人工对账,核对托盘当前位置、活动资源锁、库存 position、计划当前冗余字段和普通位占用事实;发现不一致时记录 COMPENSATION_REQUIRED/人工处理,不静默覆盖后续作业。

9. 验收重点

  1. 新建托盘转换阶段使用公共作用域虚拟位;复用已有真实托盘时保留其当前位,且不提前占用新的普通位。
  2. 普通位候选仅读,最终命令原子竞争;普通位全部竞争失败后由操作员确认进入同仓 A0。
  3. 多托盘并发进入 A0,A0 始终 ACTIVE,互不覆盖、互不误释放。
  4. 覆盖 VIRTUAL→A0、A0→NORMAL、NORMAL↔NORMAL;同一 A0 的 A0→A0 返回幂等 no-op,NORMAL/A0→VIRTUAL 失败关闭。
  5. A0 单托换绑普通位不影响其他 A0 托盘;普通位并发竞争只能一个托盘成功。
  6. 合托实物托盘移动后,所有入仓单库存位置一致,数量桶守恒并写零差额流水。
  7. wh_resource_lock.location_id、wh_pallet.location_id、计划当前冗余和库存 position 一致;批次 reserved_location_id 仍明确为快照。
  8. PENDING 虚拟位库存不进入排车、拣货或出库候选;部分收货按已形成的实物托盘逐托落位,失败可幂等重试。
  9. 位置为空或多份位置不一致时返回 RECONCILIATION_REQUIRED,阻断履约和再次移库并进入修复队列;远程物理成功、本地更新失败时可重放/补偿,后续作业已修改远端时补偿必须拒绝并进入对账。
  10. 平台与 PDA 并发同托、停用/改码/改类型/删除被引用库位、跨仓库位、错误版本和幂等键复用均失败关闭。
  11. A0 缺失、重复、编码碰撞、仓库失效、非公共作用域和阈值超限均失败关闭;A0 永远不可编辑、停用或删除。
  12. Admin、库存查询、托盘管理和 PDA 展示同一业务库位码及 sharedOverflow;NORMAL/SHARED_OVERFLOW 枚举、OpenAPI、Gateway、Fixture 和测试已同步。
  13. 验收夹具至少准备 1 个可用 NORMAL、系统 A0 和 2 个托盘;另覆盖无普通位/普通位全停用时进入 A0,以及普通位竞争时只有一个托盘成功。

10. 待评审决策

  1. 是否采用“一托一普通库位,普通位不足时由现场确认进入 A0”?建议采用。
  2. 是否允许收货完成但未上架的托盘暂留虚拟位?建议允许,但 PENDING 必须阻断排车、拣货、出库和发车。
  3. A0 是否视为合法正式落位并允许履约?建议允许;A0 与普通位一样进入履约候选,受 A0 阈值和现场安全门禁约束。
  4. 第一期是否同时开放 Admin 平台移库,并复用现有 PDA 移库补 A0 语义?建议同时开放,但共用同一 Lastleg 平台移库命令和物理状态机。
  5. 是否每个海外仓固定一个 A0,还是按库区配置多个 A0?建议第一期每仓一个,ID/编码永久稳定。
  6. 第一期是否暂不做重量/体积容量校验?建议暂不做累计容量计算,但保留托盘数/时间阈值告警和超限阻断。

11. 建议结论

建议第一期采用:每个海外仓一个使用公共作用域、按仓库 ID 幂等懒创建的 A0;新建托盘计划阶段使用虚拟位,正式上架时普通位候选优先、现场确认后 A0 兜底;A0 作为合法正式落位并允许履约;Admin 和 PDA 共用 Lastleg 平台移库命令;位置、资源锁、库存和计划当前字段按同一事实同步;跨服务使用可持久化的 Saga 状态机、补偿和对账;暂不实现重量/体积累计容量,但保留 A0 数量阈值和告警。

12. 开发前置清单

开发开始前应先把以下合同一次性冻结,避免各端按自己的理解实现:

  1. 冻结库位枚举和迁移规则:VIRTUAL/NORMAL/SHARED_OVERFLOW,将现有 STORAGE 统一收敛为 NORMAL,同步基线 SQL、增量 SQL、OpenAPI、Fixture、Gateway 和测试。
  2. 冻结 A0 常量和作用域:customer_id=0、location_code=A0、zone_code=OVERFLOW、status=ACTIVE、max_load_kg=999999999.000;冻结懒创建入口、冲突码和阈值阻断码。
  3. 冻结资源服务状态机和占用事实:A0/虚拟位不改库位状态,普通位 CAS 独占;wh_resource_lock.location_id 是当前值,按托盘和 reservation_key 精确释放;移库同步锁、托盘、占用事实和操作记录。
  4. 冻结 Lastleg 事实字段:wh_pallet 与 ll_inventory_position 为当前物理位置事实,计划托盘为同步冗余,wh_pallet_batch_item.reserved_location_id 为预留快照,收货凭证/库存流水为不可变历史;putawayStatus 由位置派生或只落在一个唯一权威字段。
  5. 冻结平台/PDA 共用移库 API、权限、请求哈希、版本快照、幂等键命名空间和 Saga 状态;先持久化 PREPARED,禁止在 Lastleg 事务中直接调用资源服务。
  6. 冻结履约门禁:虚拟位 PENDING 不得进入排车、拣货、出库和发车候选;A0 与普通位均可履约;整托移动保持所有库存桶、客户集合和活动业务锁一致。
  7. 冻结验收数据夹具:至少一个普通位、一个系统 A0、两个托盘,覆盖混托双客户、部分收货、普通位竞争、A0 并发、A0→A0 no-op、远端成功本地失败、补偿被后续作业拒绝、A0 缺失/冲突和普通位改码阻断。

满足以上清单后,开发可以按 warehouse-overseas-service → lastleg-service → gateway/OpenAPI → Admin/PDA → 端到端验收 的顺序拆分实现;任何偏离都应先回写本方案,而不是在代码中引入第二套语义。