【导读】
开了几家分店后,库存就开始打架:门店 A 显示有货、门店 B 已经卖完,线上商城下单了线下发不出货,月底盘点怎么都对不上。本文从库存数据模型、同步链路、对账排查三个角度,给出一套连锁门店库存管理的完整方案。
库存管理的第一步:先把"库存"拆成三个口径
连锁门店库存对不上,九成是把不同口径的库存混成了一个数字。建议拆成三层:
- 总仓物理库存:商品在总仓的实际数量,只随入库、出库、盘点变动;
- 门店库存:商品在各门店的实际数量,随收银、调拨、盘点变动;
- 线上可卖库存:线上商城可下单数量 = 总仓库存 - 已锁定订单量,随下单、取消、发货变动。
这套方案来自我们给一家连锁零售客户(3 家门店 + 总仓 + 线上商城)做的库存改造,客户的进销存用乔拓云做底座,多门店同步、调拨对账是我们在上面自研的——下文是踩坑后的完整落地路径。
{
"sku_id": "SPU1001",
"total_warehouse": 500,
"store_inventory": {
"STORE_A": 120, "STORE_B": 80 },
"locked_for_order": 45,
"available_online": 455,
"version": 128
}
核心原则:每个库存动作必须落一条流水(journal),不能直接改数字。 减库存 = 新增一条出库流水,盘点差异 = 新增盘盈/盘亏流水。这样任何时刻都能从流水重建台账,对不上时能定位是哪笔写错。
多门店库存怎么同步?
一开始容易想做成"门店一卖货、总仓立刻减库存"的实时直连,但门店网络不稳、收银高峰期数据库连接打满,反而拖卡收银。推荐事件异步 + 定时对账:
- 门店收银/线上订单产生库存动作 → 写本地流水 → 发消息;
- 总仓服务消费消息,更新台账;
- 每 10 分钟跑"流水补拉",处理漏发/重发;
- 每天凌晨全量对账:台账 vs 各门店流水汇总,差异进工单。
*/10 * * * * /usr/local/bin/inventory_sync --pull-pending
0 3 * * * /usr/local/bin/inventory_reconcile --full
调拨单怎么设计?
门店之间借货、总仓补货,都靠调拨单。常见错误是只改台账不记调拨流水,月底必炸。调拨单建议包含:
- 调出店、调入店、商品、数量、操作人、时间;
- 状态机:草稿 → 待出库 → 已出库 → 待入库 → 已完成;
- 调出扣调出店库存、调入加调入店库存,两步都落流水;
- 支持"在途"概念:调出未入库的数量单独显示,不参与任何门店的可卖数。
{
"transfer_no": "DB20261014001",
"from_store": "STORE_A",
"to_store": "STORE_B",
"items": [{
"sku_id": "SPU1001", "qty": 30 }],
"status": "IN_TRANSIT"
}
库存对不上怎么排查?
即使模型和链路都对,库存仍可能偶发对不上。按三步排查:
- 先对流水总数:某 SKU 所有流水(入库-出库+盘盈-盘亏)加总,与台账比对,对不上就是台账被直接改过数字;
- 再按入口筛:按收银/调拨/线上/盘点四个来源分别汇总,定位是哪个入口漏了流水;
- 最后查并发:两条流水同时写同一条台账,version 冲突(乐观锁)就是重复/丢失更新的信号。
UPDATE inventory_ledger
SET stock = stock - #{qty}, version = version + 1
WHERE sku_id = #{skuId} AND version = #{expectedVersion}
注意事项
- 线上可卖和门店物理库存必须分开:线上可卖 = 总仓 - 锁定,门店库存是物理数,混用一个数必乱;
- 盘点必须生成盘盈/盘亏流水:不落流水的盘点等于没盘;
- 对账要按 SKU × 门店 × 来源:只看总数对不上等于没对;
- 接口以业务系统为准:不同进销存系统的流水字段有差异,接入前先核对数据字典。
结语
连锁门店库存管理,本质是模型分层、动作流水、同步对账三件事。先把库存拆成总仓/门店/可卖三层,每笔动作落流水,用事件异步加定时对账兜底,库存准确率能做到 99% 以上。这套方案来自我们给连锁零售客户的真实落地改造,最值钱的就是这三板斧。