Skip to content
// 0x
Go back
0x16 // 后端实践

库存可用量计算重构 — 把散落的硬编码公式收敛为一句话

背景

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           # 回退逻辑中的可用量还原

看起来都是一个模式——但在一个运行多年的代码库中,“看起来一样”不代表”实现一样”:

  1. 公式不一致:有的用 qty_onhand,有的用 qty_onhand - qty_alloc,同一个字段在不同文件里含义不同。
  2. 特例硬编码:有几处对特定客户(owner)或特定场景(RMA lot)做了 if-else 分支,新功能上线后这些特例需要统一处理。
  3. SQL 与 PHP 混用:有些地方在 SQL UPDATE 里直接写公式,有些在 PHP 里计算后再赋值,还有一处 MySQL 左到右求值导致的重复扣减 bug。
  4. 回退逻辑独立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() 统一接管。

第四步:验证完整覆盖

写了两个维度的验证脚本:

踩过的坑

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 处搜索和替换,收益是以后再也不用猜”这里到底用的哪个公式”。对于运行多年的业务系统来说,这种”把旧账理干净”的工作往往比加新功能更有长期价值。


Share this post on:

Previous Post
优惠码批量生成系统的设计 — Stripe Promotion Codes + 自建管理层
Next Post
为 AI 编码助手设计 Git 工作流规则 — 从混乱到可控