扫码状态机:串号计数和数量计数混在一起时提示会失真
扫码入口一开始很容易被设计成一个简单接口:传入一个码,返回成功或失败。
真做起来就不是这样。唯一串号商品可以按每次扫码计数;数量盘点商品扫到以后不能直接加一,只能提醒去汇总页填数量;底表外的码要进入复核;清洗剔除过的商品要告诉用户它不属于本次盘点。还有重复扫码、格式错误、作废事件、会话关闭这些边界。
如果接口只返回 success: true 或 success: false,前端只能猜。猜出来的文案通常很难看,也不可靠。
我踩到的坑是,把”扫码动作成功”和”盘点数量增加”混在了一起。对唯一串号来说,这两个动作经常同时发生;对数量盘点 SKU 来说,扫码动作可以成功,但数量不应该增加。它只是在记录一次提示事件:这个商品要走手工数量。
后来状态被拆成几类:
invalid_format
accepted
quantity_count_prompt
product_excluded
out_of_baseline_pending
voided
这些状态不是给界面好看的标签,而是给后续规则用的事实。比如 quantity_count_prompt 不是异常,它说明系统识别到了这个商品,但当前计数方式不是按串号累加。再比如 product_excluded 也不是普通的找不到商品,它说明这个商品曾出现在源数据里,只是被清洗规则排除在本次盘点外。
一个简化的决策顺序可以写成这样:
function decideScan(input) {
if (session.closed) return conflict("session_closed");
const parsed = parseCode(input.raw_code);
if (!parsed) return transient("invalid_format");
if (baselineItem && countMode === "unique_sn") {
return persisted("accepted");
}
if (baselineItem && countMode === "manual_quantity") {
return persisted("quantity_count_prompt");
}
if (excludedItem) {
return persisted("product_excluded");
}
return persisted("out_of_baseline_pending");
}
这里还有一个容易忽略的点:不是所有扫码结果都应该持久化。
格式错误这种结果可以只作为瞬时反馈。它没有进入盘点事实,也不应该污染后面的审计导出。相反,扫到数量盘点商品虽然不增加数量,但它确实说明现场扫过这个码,应该留下事件,方便复核时解释为什么用户被引导去填数量。
作废也不能粗暴地删事件。盘点工具里,删除会让证据链断掉。更稳妥的做法是保留原事件,再记录作废时间、作废人和原因。汇总时不再计数,审计时还能看见它曾经发生过。
这套状态拆开以后,前端文案反而简单了。它不需要自己判断”这算成功还是失败”,只要按状态展示下一步动作:
accepted -> 继续扫码
quantity_count_prompt -> 去汇总页填数量
product_excluded -> 不纳入本次盘点
out_of_baseline_pending -> 等待补录或复核
voided -> 保留记录但不计数
这次的教训是:扫码结果不要按按钮反馈来建模,要按后续事实来建模。用户看到的是提示,系统真正需要保存的是状态边界。