CSV 导入清洗:为什么不能把底表行直接当库存事实
导入底表时,我最容易犯的错,是把”能解析”当成”能盘点”。
CSV 行能被读出来,必填列也齐,数量还是合法数字,看起来就像一条库存事实。但在盘点工具里,这一步还不够。底表里混着服务、物料、订金、安装类条目,甚至还有一些名字很像服务、实际却需要保留的实物条目。把这些行直接丢进盘点会话,后面所有动作都会变形:扫码会提示奇怪的状态,汇总里会出现不该盘的短缺,导出时还要解释这些差异为什么存在。
这个问题不是 CSV 解析问题。解析只回答一件事:这份文件能不能读。清洗要回答的是另一件事:这行是不是本次要数的实物。
我后来把导入拆成两层契约。
第一层是格式契约。文件必须能解码,表头必须齐,数量字段必须是非负整数。这里失败就直接拒绝导入,不进入后续流程。
第二层是盘点契约。每一行都会落到两个集合之一:included_items 或 excluded_items。前者参与盘点,后者保留原因,但不进入扫码和汇总的计数主体。
一个简化后的规则长这样:
function classify(row) {
const item = normalize(row);
if (!isInteger(item.expected_qty)) {
throw new Error("invalid quantity");
}
if (isServiceLike(item) || isMaterialLike(item)) {
return { bucket: "excluded", reason: "not_physical_count_target" };
}
return { bucket: "included" };
}
这里有个细节值得单独记:清洗规则不能只靠一个字段。某些条目从 ID 前缀看像物料,某些条目从名称看像服务,还有一些特例会把简单规则打穿。规则写得越粗,后面越容易在人工复核里补锅。
我一开始倾向于只在导入结果里显示”剔除数量”,后来发现这不够。被剔除的行也要保留下来,至少要保留一个可解释的 reason。否则到了汇总或复盘阶段,只能说”系统没算它”,说不清为什么没算。
最后比较稳的模型是:
source_rows
-> normalize
-> validate required fields
-> classify for countability
-> included_items
-> excluded_items with reason
这样做之后,扫码状态也清楚了。扫到已纳入底表的商品,可以进入正常计数;扫到清洗剔除清单里的商品,可以明确提示”这不是本次盘点目标”;扫到完全不在底表里的商品,再走底表外盘盈流程。
这三个分支看起来接近,但含义完全不同。把它们混成一个”找不到商品”,用户会以为是系统漏了数据;把它们混成一个”异常”,导出时又会多出一堆没法解释的差异。
这次复盘留下的经验很简单:导入不是把外部文件搬进系统,而是在建立第一份事实边界。字段完整只是入场券,不是库存事实。