24 KiB
海外仓库位与 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 规则
- 普通库位有可用位置时优先使用普通库位。
- 普通位候选竞争失败且现场确认后,托盘进入同仓 A0。
- A0 可同时承载多个托盘,但每个托盘必须有独立
resourceLockId和reservation_key。 - A0 是系统保留编码,不可由后台编辑、停用或删除。
- A0 中的单个托盘可以换绑到普通库位,不影响 A0 中其他托盘。
- 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. 验收重点
- 新建托盘转换阶段使用公共作用域虚拟位;复用已有真实托盘时保留其当前位,且不提前占用新的普通位。
- 普通位候选仅读,最终命令原子竞争;普通位全部竞争失败后由操作员确认进入同仓 A0。
- 多托盘并发进入 A0,A0 始终
ACTIVE,互不覆盖、互不误释放。 - 覆盖
VIRTUAL→A0、A0→NORMAL、NORMAL↔NORMAL;同一 A0 的A0→A0返回幂等 no-op,NORMAL/A0→VIRTUAL失败关闭。 - A0 单托换绑普通位不影响其他 A0 托盘;普通位并发竞争只能一个托盘成功。
- 合托实物托盘移动后,所有入仓单库存位置一致,数量桶守恒并写零差额流水。
wh_resource_lock.location_id、wh_pallet.location_id、计划当前冗余和库存 position 一致;批次reserved_location_id仍明确为快照。PENDING虚拟位库存不进入排车、拣货或出库候选;部分收货按已形成的实物托盘逐托落位,失败可幂等重试。- 位置为空或多份位置不一致时返回
RECONCILIATION_REQUIRED,阻断履约和再次移库并进入修复队列;远程物理成功、本地更新失败时可重放/补偿,后续作业已修改远端时补偿必须拒绝并进入对账。 - 平台与 PDA 并发同托、停用/改码/改类型/删除被引用库位、跨仓库位、错误版本和幂等键复用均失败关闭。
- A0 缺失、重复、编码碰撞、仓库失效、非公共作用域和阈值超限均失败关闭;A0 永远不可编辑、停用或删除。
- Admin、库存查询、托盘管理和 PDA 展示同一业务库位码及
sharedOverflow;NORMAL/SHARED_OVERFLOW枚举、OpenAPI、Gateway、Fixture 和测试已同步。 - 验收夹具至少准备 1 个可用
NORMAL、系统 A0 和 2 个托盘;另覆盖无普通位/普通位全停用时进入 A0,以及普通位竞争时只有一个托盘成功。
10. 待评审决策
- 是否采用“一托一普通库位,普通位不足时由现场确认进入 A0”?建议采用。
- 是否允许收货完成但未上架的托盘暂留虚拟位?建议允许,但
PENDING必须阻断排车、拣货、出库和发车。 - A0 是否视为合法正式落位并允许履约?建议允许;A0 与普通位一样进入履约候选,受 A0 阈值和现场安全门禁约束。
- 第一期是否同时开放 Admin 平台移库,并复用现有 PDA 移库补 A0 语义?建议同时开放,但共用同一 Lastleg 平台移库命令和物理状态机。
- 是否每个海外仓固定一个 A0,还是按库区配置多个 A0?建议第一期每仓一个,ID/编码永久稳定。
- 第一期是否暂不做重量/体积容量校验?建议暂不做累计容量计算,但保留托盘数/时间阈值告警和超限阻断。
11. 建议结论
建议第一期采用:每个海外仓一个使用公共作用域、按仓库 ID 幂等懒创建的 A0;新建托盘计划阶段使用虚拟位,正式上架时普通位候选优先、现场确认后 A0 兜底;A0 作为合法正式落位并允许履约;Admin 和 PDA 共用 Lastleg 平台移库命令;位置、资源锁、库存和计划当前字段按同一事实同步;跨服务使用可持久化的 Saga 状态机、补偿和对账;暂不实现重量/体积累计容量,但保留 A0 数量阈值和告警。
12. 开发前置清单
开发开始前应先把以下合同一次性冻结,避免各端按自己的理解实现:
- 冻结库位枚举和迁移规则:
VIRTUAL/NORMAL/SHARED_OVERFLOW,将现有STORAGE统一收敛为NORMAL,同步基线 SQL、增量 SQL、OpenAPI、Fixture、Gateway 和测试。 - 冻结 A0 常量和作用域:
customer_id=0、location_code=A0、zone_code=OVERFLOW、status=ACTIVE、max_load_kg=999999999.000;冻结懒创建入口、冲突码和阈值阻断码。 - 冻结资源服务状态机和占用事实:A0/虚拟位不改库位状态,普通位 CAS 独占;
wh_resource_lock.location_id是当前值,按托盘和reservation_key精确释放;移库同步锁、托盘、占用事实和操作记录。 - 冻结 Lastleg 事实字段:
wh_pallet与ll_inventory_position为当前物理位置事实,计划托盘为同步冗余,wh_pallet_batch_item.reserved_location_id为预留快照,收货凭证/库存流水为不可变历史;putawayStatus由位置派生或只落在一个唯一权威字段。 - 冻结平台/PDA 共用移库 API、权限、请求哈希、版本快照、幂等键命名空间和 Saga 状态;先持久化
PREPARED,禁止在 Lastleg 事务中直接调用资源服务。 - 冻结履约门禁:虚拟位
PENDING不得进入排车、拣货、出库和发车候选;A0 与普通位均可履约;整托移动保持所有库存桶、客户集合和活动业务锁一致。 - 冻结验收数据夹具:至少一个普通位、一个系统 A0、两个托盘,覆盖混托双客户、部分收货、普通位竞争、A0 并发、A0→A0 no-op、远端成功本地失败、补偿被后续作业拒绝、A0 缺失/冲突和普通位改码阻断。
满足以上清单后,开发可以按 warehouse-overseas-service → lastleg-service → gateway/OpenAPI → Admin/PDA → 端到端验收 的顺序拆分实现;任何偏离都应先回写本方案,而不是在代码中引入第二套语义。