1. 食品批次追溯编码规则设计与校验
批次编码的字段构成与解析逻辑
乳制品批次追溯要求编码在离线、打印、人工抄录等场景下仍能唯一识别一批产品。本例采用定长分段编码:总长 18 位,由生产日期、产线、班次、产品代码、当日流水号和校验位组成。定长分段的好处是无需分隔符即可按位置切片解析,适合贴在箱体标签上由扫码枪或人工录入。
各字段定义如下(示例企业约定,实际落地时需与企业编码管理制度对齐):
| 字段 | 位数 | 位置 | 取值规则 | 示例 |
|---|---|---|---|---|
| 生产日期 | 8 | 1–8 | YYYYMMDD | 20240517 |
| 产线 | 2 | 9–10 | 01–99,灌装线编号 | 03 |
| 班次 | 1 | 11 | A 早班 / B 中班 / C 夜班 | B |
| 产品代码 | 4 | 12–15 | 企业产品主数据中的 4 位码 | 1027 |
| 流水号 | 2 | 16–17 | 同日期+产线+班次+产品下从 01 递增 | 05 |
| 校验位 | 1 | 18 | 前 17 位加权模 36 校验 | K |
校验位采用加权模 36:字符集为 0-9A-Z(共 36 个字符),对前 17 位按位置权重 1、3、7、9 循环加权求和,取模 36 后映射为字符。这样能拦截抄录错位、单字符错写等常见错误,同时保持纯字母数字、便于打印。
编码生成函数实现
生成函数需要解决两个问题:一是流水号在同键下递增,二是校验位计算。下面用 Python 实现,字符集与权重与上表一致,可直接运行验证。
CHARSET = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ"
WEIGHTS = [1, 3, 7, 9]
def char_value(ch: str) -> int:
return CHARSET.index(ch)
def check_char(payload: str) -> str:
"""payload 为前 17 位,返回 1 位校验字符"""
total = 0
for i, ch in enumerate(payload):
total += char_value(ch) * WEIGHTS[i % 4]
return CHARSET[total % 36]
def build_batch_code(date: str, line: str, shift: str,
product: str, seq: int) -> str:
# 基础格式校验
assert len(date) == 8 and date.isdigit(), "日期须为 YYYYMMDD"
assert len(line) == 2 and line.isdigit(), "产线须为 2 位数字"
assert shift in ("A", "B", "C"), "班次须为 A/B/C"
assert len(product) == 4 and product.isalnum(), "产品代码须为 4 位字母数字"
assert 1 <= seq <= 99, "流水号范围 01-99"
payload = f"{date}{line}{shift}{product}{seq:02d}"
assert len(payload) == 17
return payload + check_char(payload)
调用示例:build_batch_code("20240517", "03", "B", "1027", 5) 返回 2024051703B102705K。流水号上限 99 是刻意的约束——单产线单班次单产品超过 99 批时,应改用更细的时段或扩展流水位,而不是让编码溢出。
校验与解析函数
校验函数要区分三类失败:长度/字符集不合法、校验位不匹配、字段语义越界。分开报错便于现场快速定位是抄录错误还是规则误用。
def parse_batch_code(code: str) -> dict:
if len(code) != 18:
raise ValueError(f"长度错误:期望 18 位,实际 {len(code)} 位")
if any(c not in CHARSET for c in code):
raise ValueError("含非法字符,仅允许 0-9 与 A-Z")
payload, given = code[:17], code[17]
expect = check_char(payload)
if given != expect:
raise ValueError(f"校验位不匹配:期望 {expect},实际 {given}")
date, line, shift = payload[0:8], payload[8:10], payload[10]
product, seq = payload[11:15], payload[15:17]
if shift not in ("A", "B", "C"):
raise ValueError(f"班次非法:{shift}")
if not (1 <= int(seq) <= 99):
raise ValueError(f"流水号越界:{seq}")
return {
"date": f"{date[0:4]}-{date[4:6]}-{date[6:8]}",
"line": line, "shift": shift,
"product": product, "seq": int(seq), "check": given,
}
验证用例(示例预期结果,由上述规则直接推出):
| 输入 | 预期 | 说明 |
|---|---|---|
| 2024051703B102705K | 解析成功 | 合法编码 |
| 2024051703B102705X | 校验位不匹配 | 末位被抄错 |
| 2024051703B102705 | 长度错误 | 漏抄校验位 |
| 2024051703D102705? | 含非法字符 | 班次与符号均非法,先报字符集 |
唯一性保障与落地要点
- 流水号分配:由产线终端在提交时向批次服务申请,服务端按
date+line+shift+product加锁自增,避免多终端并发取到同一号。 - 校验位不参与唯一性:唯一性键只取前 17 位,校验位是冗余校验,不能作为区分批次的依据。
- 规则版本化:编码规则一旦上线,历史批次编码不可重算。建议在批次主数据中记录
rule_version,未来扩展位数时按版本解析。 - 人工录入兜底:标签打印模糊时,校验位能拦截大部分错录;校验失败一律拒绝入库,不做自动纠错,防止错误批次混入追溯链。
设计阶段先用 20–30 个真实产品代码和产线编号跑一遍生成与解析,确认字符集不含易混淆字符(如 O 与 0、I 与 1)。若企业标签规范要求排除,可在 CHARSET 中剔除并同步调整模数。