开发小程序「食愿小灶」时,食材数据里有个问题很容易被一句"统一单位"带过去:菜谱要 400g 番茄,冰箱里记了 3 个番茄,该算还缺多少?
如果只按名称合并,再做减法,得出的 397 没有意义;如果替每个番茄估一个重量,又把未经确认的经验写进了用户的数据。我在仓库的领域层保留了一条限制:名称相同、单位可比较,数量才能参与同一笔计算。这篇讲的是这条用量规则;产品里的采购清单由用户按菜选择需要准备的材料。
第一步是收紧换算范围。源码只明确换算两组:重量统一到 g,体积统一到 ml。kg、g,以及按中国大陆市制处理的斤、两,可以放进同一组;L 和 ml 属于另一组。瓶、把、勺等单位通过输入校验后仍保留自身单位,换算系数为 1。一瓶牛奶多大、一把葱多重,当前记录没有提供答案。
关键落在归一化后的键上。下面是 normalizedIngredient 的数字用量分支,name 已经过名称清洗:
js
const [unit, factor] = conversions[ingredient.unit] || [ingredient.unit, 1];
return {
name, unit, factor,
quantity: round(Number(ingredient.quantity) * factor),
key: `${name}|${unit}`
};
例如,西红柿 0.4kg 会得到 番茄|g,数量为 400。番茄 3 个得到 番茄|个。名称别名可以归一,单位的边界仍在。后面的聚合函数用这个键组织 Map,半瓶牛奶和 100ml 牛奶就会留下两行,不会因为名字一样而混成一个总量。重量与体积之间也没有默认换算。
库存比较沿用同一个键。在 getRequirements 中,只有键相同的库存进入可用量;同名却单位不同的库存,会触发 unitMismatch。开头那个例子因此得到 missing: 400 和 unitMismatch: true。
这里还藏着一个容易误读的地方:400 表示已录入的、可按克比较的库存没有抵扣这项需求。它不能单独证明还应再买 400g。那三个番茄仍然存在,只是模型不知道它们多重。上层展示需要解释这个标记,使用者也需要核对实际材料。保留标记只是让问题可见,包装规格和实际重量仍需另外获取。
"适量"和"少许"也受这条规则约束。它们保存为文字用量,quantity 是 null,unit 是空字符串。聚合时保留文字及关联菜品,缺口计算返回待确认状态。两道菜都需要盐适量,不会被乘成"盐两份适量",也不会因没有数字而当成零克。校验还会拒绝文字用量同时携带数字和单位,避免一条记录出现两套解释。
我用现有回归用例把几个边界固定下来:
- 400g 番茄遇到 3 个番茄库存,克重缺口保留,并标记单位不一致。
- 半瓶牛奶和 100ml 牛奶汇总后仍是两行。
- 0.5 斤、2 两和 0.1kg 大米合计 450g,原菜谱中的数量与单位保持原样。
第三项也提醒我把计算结果和用户输入分开:计算可以统一到克,菜谱依然应该保留原来的表达。以后再次编辑时,用户能认出自己填过的半斤米;其他逻辑也不会把已转换的数值重复转换。
这套约束有代价:同名食材可能保留多行,遇到不同单位还需要人判断。若以后要支持"一瓶等于多少毫升",就要把具体包装规格及其适用范围存下来。仅往通用换算表里添一个"瓶"的系数,会让所有瓶装材料共享一个未经确认的容量。
实现可从仓库的 kitchen.js:归一化与校验、需求计算 读起;例子对应 基础计算测试 和 单位测试。链接固定到同一份源码提交,方便对照。
我是小程序「食愿小灶」的开发者,这是我自己的项目,主要做家庭共同选菜、餐食安排和备料清单。
体验入口:在小程序中搜索「食愿小灶」即可找到。
源码在 GitHub:pydaxing/shiyuan。如果你也做过类似的数量模型,欢迎交流那些应该交给程序计算、又需要留给人确认的边界。
创作说明:本文使用 AI 辅助整理文字,技术描述、代码片段和测试用例已对照公开源码核查。