简介:面向需要将UPC商品编码批量转换为SGTIN96 EPC编码的C#开发人员,资源提供一套完整可行的实现方案,解决传统零售商品标识向现代供应链单品级追踪体系对接的难题,可应用于库存管理、物流追溯或物联网设备中。压缩包共177个文件,以15个.cs核心源码、83个DLL运行库及18个XML配置为主,辅以Visual Studio工程文件、解决方案、缓存文件、说明文档和示例数据,整体大小约82.36MB,便于直接打开调试或抽取算法模块复用。目前已有1543人学习下载,适合供应链管理、仓储系统或EPC应用开发人员参考。内容涵盖UPC到SGTIN96的编码结构解析、GS1公司前缀提取与扩展、单品标识映射、序列号生成、校验位计算等关键步骤,并包含完整的C#工程源码、依赖库及配置文件,可帮助读者快速理解编码规则、掌握批量转换实现逻辑,并在此基础上进行二次开发。
1. UPC 到 SGTIN96:零售 RFID 落地第一道坎
做零售 RFID 项目的人,迟早会撞上同一个需求:仓库里的商品条码是标准 UPC-A,而打印机/RFID 读写器要写入电子标签的,却是 SGTIN96。UPC 是 12 位纯数字,表达的是“这家公司生产的这一款商品”;SGTIN96 是 96 位二进制编码,要在商品唯一身份之外再塞进一个序列号,告诉系统“这箱里第 37 件”。两者之间不是简单补几位数字,而是要完成一次结构重排:滤值、分区值、公司前缀、项目参考、序列号,每一段位宽都受 96 位总量约束。这个转换是 EPG Gen2 标签单品级应用的第一步,也是服装、快消、3C 零售里跑通“一物一码”的前提。反直觉的地方在于:UPC 的校验位在转换中会被丢弃,真正的校验发生在 GTIN-14 这一层,序列号才是新增的核心资产。这篇文章面向要做标签编码转换或正在对接打印系统的开发与实施工程师,把 UPC 转 SGTIN96 的位级原理、参数设置和可复现代码一次讲透。
2. SGTIN96 编码结构与 UPC 字段映射
2.1 从 UPC-A 到 GTIN-14:先补齐层级
UPC-A 在 EAN/GTIN 体系中属于“12 位”的北美规格。它由厂商代码(UPC Company Prefix)和产品代码(Item Reference)构成,前 6 位是厂商代码,后 5 位是产品代码,第 12 位是校验位。但 SGTIN96 中的 GTIN 部分按 EPC Tag Data Standard 定义,是 14 位的 GTIN-14。要从 UPC 走到 SGTIN,得先把 UPC-A 升维成 GTIN-14。
常见做法是补一位前缀 0 到最前面,把 12 位 UPC-A 变成 13 位 GTIN-13,再在 GTIN-13 前面补一个 0,得到 14 位 GTIN-14。UPC-A 的厂商代码 6 位加产品代码 5 位,在 GTIN-14 里仍然保持同样的语义位置:第 2 到 7 位是厂商代码,第 8 到 12 位是产品代码,第 1 位固定为 0,最后一位是重新计算出来的校验位。这第 14 位是整条链路里最容易出错的字段——很多转换器直接照搬 UPC 的最后一位,结果厂商代码边界一旦偏移(如 7 位或 8 位前缀),校验就全错了。
校验位算法采用模 10:从右往左,奇数位乘 1,偶数位乘 3,全部求和,取使总和为 10 的倍数的最小差值。下面这段 Python 代码同时完成校验、升维两个动作,输入一个 UPC-A 字符串,输出 GTIN-14:
def upc_a_to_gtin14(upc: str) -> str: # 去掉可能存在的横线或空格,只保留数字 digits = ''.join(ch for ch in upc if ch.isdigit()) if len(digits) != 12: raise ValueError(f'UPC-A 必须有 12 位,当前 {len(digits)} 位') # 常规校验:前 11 位按 EAN/UCC 算法算出校验位 def calc_check(data: str) -> int: total = 0 # 从右向左:奇数位权重 1,偶数位权重 3 for i, ch in enumerate(reversed(data)): weight = 1 if (i % 2 == 0) else 3 total += int(ch) * weight return (10 - total % 10) % 10 if calc_check(digits[:11]) != int(digits[11]): raise ValueError('UPC-A 校验位不通过') # 前缀 '0' 升维到 GTIN-13,再补前导 '0' 到 GTIN-14 gtin13 = '0' + digits[:11] + str(calc_check('0' + digits[:11])) gtin14 = '0' + gtin13 return gtin14这里的 calc_check 用 reversed 实现从右向左权重计算,符合 GS1 规范,而不是从左向右的“奇数位乘 1、偶数位乘 3”那种容易记反的口诀。注意 gtin13 的校验位输入是 13 位数据0 + 前 11 位,因为 GTIN-13 本身 13 位,有校验位时也纳入计算;GTIN-14 的校验位则是在 13 位基础上再次计算。你可以看到,UPC 的校验位在这里并没有进入任何 GTIN 字段,它只在源数据核验阶段起作用。
2.2 SGTIN96 的二进制布局:96 位如何切成六段
SGTIN96 的位分配是固定死板的,总共 96 位,分为六段:Header(8 位)、Filter(3 位)、Partition(3 位)、Company Prefix(可变长度 G)、Item Reference(可变长度 N)、Serial(38 - G - N? 实际为 24 位)。严谨地说,SGTIN96 的分段是这样的:
| 字段 | 位宽 | 说明 |
|---|---|---|
| Header | 8 | 固定为 0x30(00110000) |
| Filter | 3 | 指示包装类型,零售单品一般取 1 |
| Partition | 3 | 决定 Company Prefix 和 Item Reference 的位宽切分 |
| Company Prefix | 随 Partition 变化 | 厂商代码二进制值 |
| Item Reference | 随 Partition 变化 | 产品代码二进制值 |
| Serial | 24 | 序列号,0 被保留不可用 |
这里要特别重视 Partition 的作用。SGTIN96 用 3 位二进制表示 0 到 7 共八个分区,每个分区定义 Company Prefix 的位宽和 Item Reference 的位宽。厂商代码位数越多,能表达的公司数量越多,但产品代码位数相应变少,产品种类容量变小。UPC-A 升级到 GTIN-14 后,其厂商代码是 6 位十进制数字,产品代码是 5 位十进制数字。在 SGTIN96 中,这对应 Partition = 4?不对,要查表:分区 0 时 Company Prefix 40 位、Item Reference 4 位;分区 1 时 37 位/7 位;分区 2 时 34 位/10 位;分区 3 时 30 位/14 位;分区 4 时 27 位/17 位;分区 5 时 24 位/20 位;分区 6 时 20 位/24 位;分区 7 保留。
UPC-A 的 6 位厂商代码需要 20 位二进制(2^20 = 1048576,覆盖 000000-999999),所以 Partition 选 6(Company Prefix 20 位,Item Reference 24 位)。但要注意,Product Code 5 位十进制只需要 17 位,选 Partition 6 时 Item Reference 是 24 位,填入 5 位产品代码会把高位置 0,EPC 解码器依然识别。反过来,如果选 Partition 4,Company Prefix 27 位可以容纳更大的厂商代码,但 Item Reference 只有 17 位,也刚好塞下 5 位产品代码。区别在于语义长度和后续扩展性——GS1 建议按实际 Company Prefix 的十进制位数选择最小能覆盖的分区。
# 分区值快速查表(十进制位数 -> Partition 值) # 厂商前缀 6 位:Partition = 6(Company Prefix 20 bit) # 厂商前缀 7 位:Partition = 5(Company Prefix 24 bit) # 厂商前缀 8 位:Partition = 4(Company Prefix 27 bit) # 厂商前缀 9 位:Partition = 3(Company Prefix 30 bit) # 厂商前缀 10 位:Partition = 2(Company Prefix 34 bit)SGTIN96 的总位宽是 8 + 3 + 3 + 20 + 24 + 24 = 82 位,剩下的 14 位是 0 填充位。注意:填充位在二进制转十六进制时会被自动包含,导致 EPC 十六进制串是固定的 24 位(96 位)。这也是为什么很多人把填充位和 Serial 位混淆——actually SGTIN96 里 Serial 是 24 位,填充位不是独立的,因为 96 - (8+3+3+20+24+24) = 14,但 Serial 后面确实有 14 位补零。等等,再来算一次:Header 8 + Filter 3 + Partition 3 + Company Prefix 20 + Item Reference 24 + Serial 24 = 82,96 - 82 = 14 位 Padding。有些文档将 Padding 放在 Serial 之前、Item Reference 之后,有些则放在最后。按 TDS 1.9 的定义,SGTIN-96 布局顺序是:Header、Filter、Partition、Company Prefix、Item Reference、Serial,总位宽 96,未使用的位补 0,没有独立 Padding 段名称——最后那 14 位零就含在 Item Reference 的有效值高位或 Serial 的有效值高位之外。为了避免概念混乱,编码时直接按固定位宽拼出 96 位,不要手工去“留 Padding 位”。
2.3 Filter 值与序列号保留
Filter 是 3 位值,在 SGTIN96 中表示装载物的逻辑级别,不是物理包装级别。零售单品挂签一般写 1,含义是“单品(Consumer Trade Item)”;内盒写 2,外箱写 3,物流单元写 4 之类。如果上游系统只给了 UPC 而没给包装级别信息,默认取 1,并且要把这个默认值写进配置,避免现场操作员随意改动。序列号范围是 1 到 2^24-2(约 1600 万),0 在 EPC 语义中视为无效标签,全 1 保留用于特殊用途,所以实际可分配序列号不要填 0 或 16777215。
序列号的分配策略直接决定整个项目的实施成本。每件单品一个唯一序列号,是 SGTIN96 价值所在;如果序列号在多个门店或多次入库中重复,标签管理会出现乱账。常见方案有:用数据库自增 ID、用生产批次加流水号、或直接按条码打印计数器生成。但 UPC 本身不含序列号,所以“UPC 转 EPC”在业务上必须回答一个问题:序列号从哪来?我一般会在 MT 层预先生成一张临时表,维护 UPC、批次、起始序列号和已用数量,编码服务从这张表取号,保证唯一性。
3. UPC 转 SGTIN96 的位操作实现
3.1 核心编码步骤拆解
完整转换可以提炼成五个步骤,每一步都有明确的输入输出,适合独立单元测试:
- 校验并清洗 UPC-A,得到 12 位数字;
- 按 2.1 的算法升维成 GTIN-14,并拆出 6 位厂商代码、5 位产品代码;
- 查分区表确定 Partition 值,并计算出 Company Prefix 和 Item Reference 的二进制宽度;
- 组装二进制:Header(8) + Filter(3) + Partition(3) + CompanyPrefix(N) + ItemReference(M) + Serial(24),总共拼出 96 位;
- 把 96 位二进制转成 24 位十六进制 EPC,以及纯标识 URI。
function upcToSgtin96(upc, serial, filter = 1) { // 1) 清洗并校验 if (!/^\d{12}$/.test(upc)) throw new Error('UPC-A 格式错误'); const sum = [...upc.slice(0, 11)] .map((d, i) => parseInt(d) * (i % 2 === 0 ? 1 : 3)) .reduce((a, b) => a + b, 0); const calc = (10 - (sum % 10)) % 10; if (parseInt(upc[11]) !== calc) throw new Error('UPC-A 校验位错误'); // 2) GTIN-14 const gtin14 = '0' + '0' + upc.slice(0, 11) + calc; // 0 + GTIN-13 // 实际更严谨:gtin13 = '0'+upc[0..10]; check = calcCheck(gtin13) // gtin14 = '0' + gtin13 // 3) 厂商代码与产品代码(UPC-A 固定 6+5) const companyPrefix = upc.slice(0, 6); const itemRef = upc.slice(6, 11); const partition = 6; // 6 位厂商代码对应 20 bit / 24 bit // 4) 拼 96 位 const header = 0x30; const filterBits = filter & 0x7; const partitionBits = partition & 0x7; if (serial < 1 || serial > 0xFFFFFE) throw new Error('序列号超出可用范围'); const companyBits = parseInt(companyPrefix, 10); const itemBits = parseInt(itemRef, 10); // 位运算组装:8+3+3+20+24+24=82,左移至 96 位高位对齐 let epcBits = BigInt(header) << 88n; epcBits |= BigInt(filterBits) << 85n; epcBits |= BigInt(partitionBits) << 82n; epcBits |= BigInt(companyBits) << 62n; // 82-20=62 epcBits |= BigInt(itemBits) << 38n; // 62-24=38 epcBits |= BigInt(serial) << 14n; // 38-24=14 // 低 14 位自动为 0 const epcHex = epcBits.toString(16).padStart(24, '0').toUpperCase(); return `urn:epc:tag:sgtin-96:${filter}.${companyPrefix}.${itemRef}.${serial}`; }这段代码把 UCP-A 的 6+5 结构直接映射到 Partition=6,其中companyBits << 62n是因为 Company Prefix 段起始位在第 62 位(从高位开始数),itemBits << 38n在 38 位。这种位偏移计算最容易出错,建议写成常量并用单元测试锁定:UPC036000291452转换后,十六进制 EPC 应该是一个固定可用去反查的串。关键参数说明:BigInt 是必须的,因为 96 位超过 JavaScript Number 的安全整数范围;padStart(24, '0')保证输出 24 位十六进制;URI 里 filter 和序列号用点号分隔,这是 EPC 纯标识格式的标准写法。
3.2 二进制拼接与位偏移设计
位偏移表可以通过一个通用函数生成,不依赖硬编码。SGTIN96 的位宽随 Partition 变化,因此把位偏移做成查表结构更稳妥:
| Partition | Company Prefix 位宽 | Item Reference 位宽 | Company 起始偏移 | Item 起始偏移 |
|---|---|---|---|---|
| 6 | 20 | 24 | 62 | 38 |
| 5 | 24 | 20 | 58 | 34 |
| 4 | 27 | 17 | 55 | 28 |
| 3 | 30 | 14 | 52 | 22 |
起始偏移都以“高位第 0 位”计算:Header 占 0-7,Filter 占 8-10,Partition 占 11-13,之后紧跟 Company Prefix。所以 Company 起始偏移恒为 14,Company 结束位 = 13 + CompanyBits;Item 起始 = 14 + CompanyBits;Serial 起始 = 14 + CompanyBits + ItemBits。以 Partition=6 来计算:Company 从 14 到 33(20 位),Item 从 34 到 57(24 位),Serial 从 58 到 81(24 位)。用 BigInt 表达时,为了移位方便把 96 位整体左移(64 - 段结束位)是不对的——更简单的办法是直接在 96 位累积器里左移总位数。
推荐用累计位宽的写法替代手工偏移:
def sgtin96_assemble(partition, company_int, item_int, serial, filter_val): cfg = {6: (20, 24), 5: (24, 20), 4: (27, 17), 3: (30, 14)} cp_bits, ir_bits = cfg[partition] total = 8 + 3 + 3 + cp_bits + ir_bits + 24 if total != 96: raise ValueError(f'分区 {partition} 位宽组合不是 96 位') value = 0 value = (value << 8) | 0x30 value = (value << 3) | (filter_val & 0x7) value = (value << 3) | (partition & 0x7) value = (value << cp_bits) | company_int value = (value << ir_bits) | item_int value = (value << 24) | serial shift = 96 - value.bit_length() return value << shift注意shift处不是“填充补齐”,而是因为 company_int 和 item_int 没有占满所有位宽时,bit_length()小于 96,整体左移制造尾部的 0。这个写法把总位宽校验放在组装前,比逐步左移后才发现溢出更友好。序列号超过 24 位时会自动截断——这里应该主动校验而不是靠截断兜底。
4. 参数选型与边界场景处理
4.1 分区值、厂商代码边界与 UPC 的 0 开头问题
选 Partition 时有一个隐藏陷阱:UPC-A 厂商代码前导 0 的处理。当一家厂商的 GS1 公司前缀以 0 开头,比如实际存的是036000,数字化转二进制时parseInt('036000')变成 36000,在 SGTIN96 二进制里是 36000,这是正确的。真正的问题在解码回显:解码器把 20 位 Company Prefix 数字转回十进制后可能不足 6 位,需要左侧补零到厂商代码实际位数。这提示我们在做 UPC 转 EPC 时,要记录源 UPC 厂商代码的实际十进制位数才能正确还原。如果厂商代码是 6 位但实际值前面有 0,转换时只存数值,展示层再补 0。
另一个边界是 UPC-A 的编码结构。UPC-A 是 12 位,其中第二个数字到第七个数字是厂商代码,第八到第十二是产品代码。但少数 UPC 使用 7 位厂商代码和 5 位产品代码的结构——UPC 体系有个历史包袱:厂商代码位数可以是 6 到 10 位,由 GTIN-12 的总体结构决定。对 UPC-A 来说规则是:第 2 位到第 7 位(共 6 位)固定是厂商代码吗?实际上 UPC-A 不像 GS1-8 那样固定,厂商代码可以由第 2 位数字的长度决定,UPC-A 的 12 位中:厂商代码长度由第 7 位是什么数字的校验规则推断?标准答案是:UPC-A 的前 6 位是厂商代码,后 5 位是产品代码,第 1 位是数字系统字符。但很多书籍讲 UCC-12 的厂商代码是 6-10 位可变长,这是对 GTIN-12 整体而言的,UPC-A 具体是 6+5,除非使用零压缩前缀(Number System 0 对应 6+5)。这里为了避免误导从业者,约定本方案的输入限定为标准 UPC-A(6 位前缀 + 5 位产品 + 1 位校验)。如果遇到 7 位厂商前缀的 UPC-A,应走“UPC-A with 7-digit prefix”?实际上 UPC-A 不可能有 7 位厂商代码,真正可变长的是 UPC-E 和 GTIN-12 的扩展。UPC-E 转为 UPC-A 后仍然是 6+5,所以此场景无需担心。
如果上游给的是 UPC-E(8 位压缩码),必须先做 UPC-E 转 UPC-A 再进入这个流程,不能直接把 8 位数据当 UPC-A 用。UPC-E 转 UPC-A 有固定规则表,按数字系统字符和第 7 位展开,是另一个独立模块。
4.2 校验位被丢弃后的重算风险
3.1 代码里,最后组装到 SGTIN96 的 Company Prefix 与 Item Reference 都不包含 UPC 校验位,校验位只在输入阶段用。这意味着相同 UPC 号的商品,序列号从 1 到 10000,会得到 10000 个不同 EPC;而不同 UPC 只要前 11 位相同,它们的 GTIN-14 相同(校验位由 11 位算出),所以 SGTIN96 天然支持“同一商品,多序列号”。但 GTIN-14 的最后一位如果算错,解码时标签能被读出来但无法通过 EPCIS 事件的验证。GS1 的 EPCIS 事件会执行 GTIN 校验,错误校验位会导致上层应用拒绝入库。
为规避这类问题,批量转换脚本里必须加入自校验函数——把生成的 SGTIN96 解析回 GTIN-14,再比对源 UPC 升维结果:
def verify_upc_to_sgtin(upc: str, epc_hex: str) -> bool: gtin14_from_upc = upc_a_to_gtin14(upc) # 解析 EPC 十六进制串(简化:去头与分区切换) bits = bin(int(epc_hex, 16))[2:].zfill(96) partition = int(bits[11:14], 2) cp_len, ir_len = {6:(20,24),5:(24,20),4:(27,17),3:(30,14)}[partition] cp_int = int(bits[14:14+cp_len], 2) ir_int = int(bits[14+cp_len:14+cp_len+ir_len], 2) gtin14_from_epc = f'0{cp_int:0{6}d}{ir_int:0{5}d}' # GTIN-14 补足 14 位 return gtin14_from_upc == gtin14_from_epc.zfill(14)重点看两处:cp_int:0{6}d把厂家代码补成 6 位,但如果源厂商代码是 7 位,这行会出错——所以建议把厂商代码长度也作为参数传入,不要默认 6。SGTIN96 里,实际厂商前缀长度隐含在分区和值里,但解码端往往默认厂商前缀为 6 位,这时 7 位前缀的 UPC 转换出的标签必然无法正确回显。因此当 UPC 厂前缀超过 6 位时,应选择公司前缀 24 位(前缀 7 位)的 Partition,不能照着 UPC 固有长度硬选分区。
5. 序列号策略与批量转换落地
5.1 序列号池与并发分配
SGTIN96 能容纳的序列号上限约 1600 万,实际项目实施中最大的坑不是容量,而是重复分配。当多台打印机并发生成吊牌时,如果每台设备都从 1 开始计数,会出现大量重复 EPC,RFID 盘点时同一编码对应多件商品,系统无法区分。我一般会做一张分配表,记录每个 UPC 已用到的最大序列号,生成时用数据库事务或 Redis INCR 保证原子性:
CREATE TABLE epc_serial_pool ( upc CHAR(12) NOT NULL, gtin14 CHAR(14) NOT NULL, last_serial INT UNSIGNED NOT NULL DEFAULT 0, batch_no VARCHAR(32) NOT NULL, PRIMARY KEY (upc, batch_no) ) ENGINE=InnoDB; UPDATE epc_serial_pool SET last_serial = last_serial + 1 WHERE upc = '036000291452' AND batch_no = 'B20240501';取回last_serial新值后作为本次标签的序列号,如果ROW_COUNT()为 0,说明该 batch 首次使用,插入新行且序列号从 1 开始。这个方案优点是断点续传,缺点是单行更新有锁竞争——对标签打印场景完全够用,因为每秒并发一般不超过几十次。如果吞吐要求上千每秒,改为 Redis 自增键,键名规则sgtin:serial:036000291452:B20240501,但 Redis 的持久化策略要保证 AOF 至少每秒同步,防止断电回拨。
序列号与业务字段的对应关系也要定义清楚。常见做法是:序列号 = 生产日期(5 位)+ 流水号(N 位),但这样会吃掉大量序列号位宽。SGTIN96 的 Serial 是 24 位,约 1600 万容量,如果每天产量超过 1600 万,需要按天滚动批次。如果产品是鞋服类,建议直接用纯流水号,把日期信息放到 EPCIS 事件里而非序列号本身,保持 Serial 的简单性。
5.2 Filter 值与读写器读取的匹配
读写器(如 Impinj R700 / Zebra FX960)在读取模式下可以按 EPC 前缀过滤。SGTIN96 的 TAG 十六进制串以30开头(Header 0x30),所以读写器过滤规则写E30*还是30*取决于设备设置。注意 Filter 值虽然在二进制 EPC 里存在,但很多读写器显示时会把 Filter 单独解析为“包装级别”字段,不影响 EPC 十六进制串。真正影响过滤的是 Header 字节:SGTIN96 的 Tag 编码 Header 是 0x30,SGTIN-198 是 0x36,SSCC-96 是 0x31,SGTIN-96 和 SGTIN-198 的 Filter 语义一致,但位宽不一样,写标签前要再三确认打印机固件选的是 SGTIN96 还是 SGTIN198 模板。
另一个现场高频问题:UPC 转出的 SGTIN96 EPC 在打印软件里被转成 EPC 纯标识 URI 后,中间多了点号,被误认为“非标准”。其实urn:epc:tag:sgtin-96:1.036000.29145.2是合法的纯标识 EPC,对应二进制是十六进制30...。有些老系统只认十六进制不认 URI,转交前统一用十六进制格式最安全。打印软件配置项中,EPC 编码方式选“SGTIN-96”,然后在 Company Prefix 列填 6 位厂商代码,Item Reference 列填 5 位产品代码,Serial 列填序号,滤值列从下拉里选“单品”,即可完成配置,而不必手写二进制。
6. 逆向验证与常见编码错误的排查技巧
验证一个 SGTIN96 是否成功,最直接的手段是反向解析:把 24 位十六进制 EPC 拆回 Filter、Partition、Company Prefix、Item Reference、Serial,再组装成 UPC-A,与源 UPC 对比。下面的 Python 片段可在命令行里直接跑:
def sgtin96_to_upc(epc_hex: str, company_digits: int = 6): bits = bin(int(epc_hex, 16))[2:].zfill(96) if bits[:8] != '00110000': raise ValueError('Header 不是 SGTIN96') partition = int(bits[11:14], 2) cp_bits, ir_bits = {6: (20, 24), 5: (24, 20), 4: (27, 17), 3: (30, 14)}[partition] cp_val = int(bits[14:14+cp_bits], 2) ir_val = int(bits[14+cp_bits:14+cp_bits+ir_bits], 2) serial = int(bits[-24:], 2) # 固定低 24 位 cp_str = str(cp_val).zfill(company_digits) ir_str = str(ir_val).zfill(5) upc_no_check = f'0{cp_str}{ir_str}' upc = upc_no_check + str(calc_check(upc_no_check)) return upc, serial排查时按以下顺序定位错误:先看十六进制是否 24 位且以 0x30 开头;再看 partition 对应位是否为 6;随后解析出的厂商代码是否和 UPC 匹配;序列号是否落在 1-16777214 区间。最常见的错误是十六进制长度不对——因为二进制拼接后若高位缺失,zfill(24)会补上 0,这不会导致错误,但如果用了 8 位十六进制存储中间值再拼,就会丢失前导位。
拦截校验的最佳时机是在标签写入前做一次软件校验,而不是写完用读写器试读。Zebra ZT411 打印头写 EPC 后可以用内置 RFID 测试模式读回 TID 和 EPC,但打印耗材浪费较大。更省事的方式是把生成 EPC 的接口做成纯函数,对同一 UPC、同一序列号,输出必须恒定。一旦发现同一输入两次输出不同,优先怀疑序列号池分配重复或并行任务里用了共享的 static 变量。
最后提一个隐藏坑:当 UPC 的第 1 位(Number System)不是 0 时,比如 UPC-A 以 3 开头(药品),升维 GTIN-14 的规则需要确认目标系统是否接受。GS1 规定 GTIN-14 前导位不能用于扩充 EAN-13,但实际零售系统对药品 UPC 通常用 GTIN-14 整体对待。如果你处理的是 7 位厂商前缀的 GTIN-12 扩展结构,请先把它展开成 UPC-A 的 12 位形态再走本流程,否则第 14 位校验会算错。牢记一点:SGTIN96 只认 GTIN-14 的数值,不认“UPC 变体”,源头数据结构不合法时,一切位操作都白搭。
本文还有配套的精品资源,点击获取