[控场AI]
· 4 分钟阅读· 2,379 字

条码生成器算错校验位,扫码枪读出了另一件商品

条码生成器算错校验位,扫码枪读出了另一件商品

EAN-13 校验位算错导致条码能扫但指向错误商品,揭示“自产自验”测试陷阱。

一位开发者用自写函数生成 EAN-13 条码,本地扫码通过,却在真实收银台扫出另一件商品——条码有效但校验位算错,指向了完全不同的 SKU。根源在于测试脚本调用同一个有 bug 的函数来“验证”输出,形成用错尺子量错东西的闭环。作者由此提炼出三条教训:不要用自己的编码器验证自己,应引入 ZXing 解码库和第二个编码器做独立 round-trip;不要只测当下一两种码制;把校验位当作一等公民。文章还对比了 Code128 默认无校验位时扫出乱码反而更麻烦,因为乱码会阻塞流程,而错误有效商品则静默污染数据。最后作者将逻辑封装为支持 14 种码制的在线生成器,仅在 round-trip 通过后返回 verified。

一个"几乎成功"的惨案

条码生成器导致扫码错误的经历分享

一位开发者的需求很朴素:为产品标签生成 EAN-13 条码。API 渲染正常,扫码枪也能读出来,本地测试全绿——按常规标准,这活儿算交付了。

直到真有人在收银台扫了一下。收银系统弹出来的不是香皂,而是一袋咖啡。条码本身完全有效,只是指向了另一个 SKU。这比"扫不出来"更可怕:它不报错,不崩溃,只是安静地把一件商品识别成另一件。

罪魁祸首:EAN-13 的最后一位

EAN-13 共 13 位数字,最后一位不是数据,是校验位——按模 10 加权算法算出来,用来验证前 12 位是否被正确读取。这位开发者的问题出在:某一种特定首位数字模式下,校验位算错了。

更隐蔽的是他的测试方法。测试脚本调用的是同一个有 bug 的生成函数来"验证"输出——等于用错的尺子量错的东西,当然每次都对。所有该前缀的条码在本地通过验证,到了真实收银台上集体失效。

修复本身只有几行代码:一趟模 10 加权计算。但教训远比修复值钱。

什么是模 10 加权算法

EAN-13 的校验位计算采用模 10 加权法:从右往左数前 12 位数据位,奇数位(从右数第 1、3、5……位)权重为 1,偶数位权重为 3,将各位数字乘以对应权重后求和,再取该和除以 10 的余数,用 10 减去余数(若余数为 0 则校验位为 0)即得校验位。例如数据位为 690123456789,加权和为 9×1+8×3+7×1+6×3+5×1+4×3+3×1+2×3+1×1+0×3+9×1+6×3,计算后若余数为 2,则校验位为 8。这种加权设计的目的是检测单一位数字错误和相邻位数字换位错误,在零售领域是强制标准。开发者出错的地方在于某一种首位前缀(如国家代码区段)下,代码对奇偶位顺序的判定发生偏移,导致权重分配错误,算出的校验位虽自身自洽,却与标准编码器生成的不同,最终被收银系统按另一套规则解读。

核心教训:测试预言不能是你的实现本身

这条经验对所有生成机器可读产物的开发者都成立:

  • 不要用自己的编码器验证自己的编码器。 开发者的做法是引入独立的解码库(扫码端用 ZXing)、第二个独立编码器做交叉验证,形成真正的 round-trip。
  • 不要只测当下需要的那一种码制。 他最初只测了手头要用的那一种。事后他把整个符号矩阵都纳入测试。
  • 把校验位当成一等公民。 该校验的地方必须校验,且用外部实现来校验。

什么是测试预言与 round-trip 测试

在软件测试中,“测试预言”(test oracle)指用来判断测试输出是否正确的外部参照标准。当开发者调用被测函数本身来生成预期结果时,预言就退化成“同义反复”,只能验证代码内部一致性,无法发现系统性偏差。正确的做法是引入独立于被测实现的预言,例如对一个条码编码器而言,可以用 ZXing 这类第三方解码库对生成图像做反向解码,得到原始数字串后再比对预期值。这条“编码—解码”路径称为 round-trip(往返)测试,它能同时捕获编码器和解码器的错误。开发者后续引入第二个独立编码器交叉比对,则进一步排除了单一解码库自身实现缺陷的可能。任何生成机器可读产物的场景——序列化协议、PDF 排版、二维码——都应把 round-trip 作为最基本回归测试。

Code128 的另一种失败:没有校验位反而更糟?

处理过程中他发现了另一个反直觉的事实:Code128 默认没有校验位。这意味着打印错误的 Code128 条码扫描出来是乱码,而不是"另一个有效商品"。

他原本以为乱码是更温和的失败模式,但实际相反。当收银台扫出乱码时,收银员和系统需要重新扫码、手动输入,流程阻塞;而扫出一个"错误的但有效的商品"时,交易照常完成,错误悄无声息地溜进库存、销售和财务数据。

于是他给支持校验位的码制加上了可选的强制校验,API 只在完成 round-trip 解码后才返回 verified: true 标记。

Code128 的校验位是可选的

Code128 规范定义了一个强制校验字符(checksum),它位于起始符之后、数据符之前,按模 103 加权计算,用于在解码时验证数据完整性。但打印或生成条码时,校验字符是否必须被编码进去,取决于具体实现和阅读器行为——很多轻量级生成器为了缩短条码长度会省略它,而部分解码器在缺失校验字符时仍能输出数据,只是不做验证。这就造成了一个反直觉的后果:没有校验位的 Code128 一旦某一位印错,扫描结果可能是乱码或截断字符串,而不是一个“变了但合法”的商品码。由于收银系统通常不会为乱码自动重试,反而需要人工介入;相比之下,带校验位的码制一旦校验失败会直接拒绝读取,更容易触发标准化错误处理流程。因此作者建议对支持校验位的码制默认开启强制校验,并在 API 层以 round-trip 成功作为返回条件。

一个工具与一个开放问题

他把整套逻辑打包成了一个条码生成器(x402.freeq.one/tools/qr_generator.html):支持 14 种码制,输出 SVG 或 PNG,以 base64 返回,无需维护本地图像库。

文章最后抛给社区的问题是:你也在生成机器可读的产物吗?你的独立验证方法是什么?

这个问题值得每个做二维码、条码、PDF、配置文件、序列化协议的开发者停下来想一想。只要下游有机器消费你的输出,"用自己验证自己"就是最危险的陷阱——错误不会报错,它会伪装成正确。

#EAN-13 校验位#条码生成器 bug#条码校验位算法#Code128 校验位#机器可读产物验证#ZXing 条码解码#QR 码生成器#独立验证测试
分享:

相关推荐