背景
WMS 系统的库存可用量(qty_avail)一直用的是简单公式:
qty_avail = qty_onhand
即”在手数量就是可用数量”,不扣除任何预留。这满足了大多数客户的场景。但部分客户需要在出库前对某些 SKU 做数量预留——这部分预留量不应该出现在可用量中。
于是需求来了:加上库存预留功能,且可用量计算方式要按客户配置。
这个需求的技术挑战不在”怎么预留”——加个字段就行。挑战在于:整个系统里到处都在用不同的方式算可用量,要确保改完之后不留死角。
问题:散落各处的硬编码
在开始改之前,先扫描了一遍所有修改 inventry 表的代码路径。结果发现”可用量计算”分布在 22 个地方:
update/
├── inventry_adjust.php # qty_avail = qty_onhand
├── update_inventry_lot.php # qty_avail = qty_onhand
├── update_inventry_loc_qty.php # qty_avail = qty_onhand
├── update_inventry_outbound_ship.php # qty_avail = qty_onhand (hardcoded false!)
├── update_repack.php # qty_avail = qty_onhand - qty_alloc
├── inventry_osu_may.php # qty_avail = qty_onhand - qty_alloc
├── ...
└── revert_inventry.php # 回退逻辑中的可用量还原
看起来都是一个模式——但在一个运行多年的代码库中,“看起来一样”不代表”实现一样”:
- 公式不一致:有的用
qty_onhand,有的用qty_onhand - qty_alloc,同一个字段在不同文件里含义不同。 - 特例硬编码:有几处对特定客户(owner)或特定场景(RMA lot)做了 if-else 分支,新功能上线后这些特例需要统一处理。
- SQL 与 PHP 混用:有些地方在 SQL UPDATE 里直接写公式,有些在 PHP 里计算后再赋值,还有一处 MySQL 左到右求值导致的重复扣减 bug。
- 回退逻辑独立:
revert_inventry.php里有一套反向的可用量还原逻辑,容易和新公式脱节。
方案:收敛为一句话
核心思路:让”如何算可用量”成为一个可问的问题,而不是散落在各处的假设。
第一步:客户级别的开关
在客户档案中加一个字段 qty_reserved(boolean),决定该客户的可用量计算方式:
function isQtyReservEnabled(int $ownerId): bool
{
// 查客户档案中的 qty_reserved 字段
$customer = getCustomerById($ownerId);
return (bool)($customer['qty_reserved'] ?? false);
}
第二步:公式抽象为函数
function getQtyAvailFormula(bool $reservEnabled, int $qtyOnhand, int $qtyReserv = 0): int
{
if ($reservEnabled) {
return $qtyOnhand - $qtyReserv;
}
return $qtyOnhand;
}
把两种计算方式集中到一个地方。业务规则只有这里能改。
第三步:替换所有硬编码点
22 处散落的公式全部替换为函数调用:
// 之前(硬编码)
$sql = "UPDATE inventry SET qty_avail = qty_onhand WHERE ...";
// 之后(按客户配置)
$reservEnabled = isQtyReservEnabled($ownerId);
$expr = $reservEnabled ? "qty_onhand - qty_reserved" : "qty_onhand";
$sql = "UPDATE inventry SET qty_avail = $expr WHERE ...";
同时移除了原有的客户白名单特例和 RMA lot 分支——这些逻辑被 isQtyReservEnabled() 和 getQtyAvailFormula() 统一接管。
第四步:验证完整覆盖
写了两个维度的验证脚本:
- 环境检查(11 项):逐行 grep 确认旧公式已全部替换,新函数在所有文件中被正确引用,数据库迁移已执行
- 端到端行为测试(14 项):在启用/禁用预留两种模式下走完整业务流程(下单预留→库存减少→出库→过账→Void→库存恢复),验证可用量数字在每个环节都正确
踩过的坑
MySQL 左到右求值导致的重复扣减:update_repack.php 中有一行 SQL 在同一语句里先减 qty_onhand 再赋值 qty_avail,而 qty_avail 的计算引用了更新后的 qty_onhand。旧代码中 qty_avail = qty_onhand(不扣预留)所以看不出来,改成新公式后因为同一个 UPDATE 中字段的引用顺序问题多扣了一次。修复方式是将公式计算移到 PHP 侧完成,SQL 只做值赋值。
outbound 出库时的 hardcoded false:出库流程中有一处把预留开关硬编码为 false,导致启用了预留的客户在出库后可用量计算回退到旧公式。这是个遗留调试代码,本应在功能上线时移除。
回退逻辑的可用量还原:Void 操作需要把库存恢复到操作前的状态。原逻辑直接用旧公式重新计算可用量,新增预留量字段后回退逻辑也需要感知这个字段的存在。
成果
重构后的调用链变得简单且统一:
任何修改 inventry 表的操作
→ isQtyReservEnabled($ownerId) # 查客户配置
→ getQtyAvailFormula(...) # 算可用量
→ UPDATE ... SET qty_avail = ?
所有特例消失。以后要加新的可用量计算方式(比如”预留量只扣 50%”),只需改 getQtyAvailFormula() 一个函数。
小结
这种重构的特点是不涉及架构层面的变动——没有引入新框架、新设计模式、新抽象层。它做的事只有一件:把”业务规则是什么”从各处拷贝的字符串,变成一个可查、可测、可改的函数调用。
代价是一次性的 22 处搜索和替换,收益是以后再也不用猜”这里到底用的哪个公式”。对于运行多年的业务系统来说,这种”把旧账理干净”的工作往往比加新功能更有长期价值。