支付编码

Odoo EMV 收款二维码为什么不只是“生成个码”:TLV 组装、CRC 校验、代理字段与国家扩展边界讲透

很多人以为收款二维码只是把账号和金额拼进一张图,但 account_qr_code_emv 源码显示,Odoo 真正在做的是按 TLV 规则序列化字段、控制金额是否出现、生成 CRC16 校验、清洗商户名称城市与备注,并把各国差异下沉到继承模块。本文把这条支付码链路讲透。

框架
进阶 开发者 2 分钟阅读
0 评论 0 点赞 0 收藏 5 阅读

先说结论

EMV 收款二维码在 Odoo 里,绝不是“把银行账号和金额拼成一段字符串再画成二维码”那么简单。

/home/ubuntu/odoo-temp/addons/account_qr_code_emv/models/res_bank.py 可以看出,官方真正做的是:

  1. 把不同字段按 EMV Merchant-Presented 规则序列化成 TLV 结构。
  2. 根据金额是否为零,决定二维码是静态还是动态。
  3. 统一生成 CRC16,保证扫码端能校验内容完整性。
  4. 对商户名、城市、备注做字符清洗,避免不合规字符进入 payload。
  5. 把国家差异留给继承模块扩展,而不是写死在核心。

所以最短结论是:

Odoo 生成的不是一张码图,而是一段必须被支付生态正确理解的结构化支付载荷。


_serialize() 为什么看起来简单,却是整个协议骨架

_serialize(header, value) 只有一行核心逻辑:

  • 把 header 格式化成两位;
  • 再加两位长度;
  • 再加 value。

也就是经典 TLV:

  • Tag
  • Length
  • Value

这说明 EMV 二维码的本质不是“自由拼字符串”,而是有字段编号和长度约束的协议编码

一旦这层理解错了,后面的所有事情都会错:

  • 扫码端读不到正确字段;
  • CRC 对不上;
  • 不同支付 app 兼容性变差;
  • 某些国家扩展位放错位置会直接失效。

所以真正的起点不是二维码图片,而是协议序列化。


为什么 Odoo 会把金额为 0 和金额不为 0 分开处理

_get_qr_code_vals_list() 里,金额处理非常有意思:

  • 如果 currency 判断金额不是零,就放交易金额;
  • 如果金额是零,就把 amount 置为 None

这不是细枝末节,而是付款二维码体验上的核心分界。

因为现实里二维码通常有两类:

  • 静态码:门店长期贴着,顾客自己输金额;
  • 动态码:本次账单专属,金额直接写死在码里。

Odoo 在 tag 01 里还写了 12,明确标成动态二维码语义,但金额本身仍然是按业务情况可空的。

这反映出官方理解得很清楚:

  • 协议标准是一层;
  • 具体金额是否出现在 payload 里,是另一层业务选择。

为什么商户名、城市、备注都要先去重音、清字符

源码里有两个关键处理:

  • _remove_accents()
  • 对 comment 做正则过滤,只保留受控字符集。

这说明 Odoo 不相信“数据库里能存的字符,就一定适合进支付码”。

原因很现实:

  • 某些扫码终端对 Unicode 支持有限;
  • 某些支付网络要求字段只能使用特定 ASCII 范围;
  • 同一个备注在后端能显示,不代表在扫码侧不会被截断或误解析。

所以官方选择先把:

  • 商户名裁成 25 个字符;
  • 城市裁成 15 个字符;
  • 注释清洗到安全字符集;

这是一种非常典型的“支付边界思维”:

支付协议中的字符串不是给人看的文章,而是给不同终端、不同清算网络稳定解析的机器字段。


CRC16 为什么是这类模块里最不能省的一步

_get_qr_vals() 会先把 TLV 拼好,再追加:

  • 6304

然后对这整段字符串做 CRC16,最后把校验值拼回去。

这意味着 CRC 并不是装饰,而是 payload 的组成部分。

为什么这件事这么重要?

因为二维码图片只是载体。真正要被扫码端信任的是:

  • 这段内容有没有被截断;
  • 有没有误码;
  • 有没有字段长度错位。

CRC 在这里承担的,就是一种低成本的内容完整性校验。

如果没有它,你可能仍然扫得出来,但支付 app 在解释字段时就可能:

  • 拒绝;
  • 错读金额;
  • 错读商户信息;
  • 把本来可识别的码当坏码。

所以从支付实现角度看,二维码能显示出来不代表二维码有效


include_reference 和 Additional Data Field 代表什么

include_reference 会影响 _get_additional_data_field(comment) 是否参与组装。

这说明备注 / reference 在 Odoo 里并不是默认无脑放进去,而是:

  • 由具体国家 / 本地扩展决定怎么编码;
  • 由业务配置决定要不要带入支付码。

这是对的,因为 reference 一旦进二维码,后果很实际:

  • 对账可能更顺;
  • 但长度、字符、隐私、兼容性也更敏感。

有些国家的码制对 reference 有很强规范,有些则没有。因此核心桥接模块只负责提供一个“可挂接的位置”,不负责替所有国家拍板。


为什么 _get_merchant_account_info() 默认返回 None

这是整份源码里我最喜欢的一个信号。

核心 EMV 模块并没有自作主张把所有国家的银行标识都硬编码进去,而是让:

  • _get_merchant_account_info()
  • _get_additional_data_field()
  • _get_merchant_category_code()

都预留为可继承点。

这说明 account_qr_code_emv 的定位不是“世界通用二维码模板”,而是:

一个负责通用 EMV 骨架的桥接层,真正的国家差异下沉到本地化模块。

这很关键,因为不同国家可能在 Merchant Account Information 里要求:

  • 不同 tag;
  • 不同账号映射;
  • 不同 proxy 类型;
  • 不同 reference 编码方式。

如果核心模块试图一把抓,最后只会变成一锅条件分支粥。


错误检查为什么盯着 city、proxy_type、proxy_value

_check_for_qr_code_errors()emv_qr 分支下会检查:

  • 是否有 merchant account info;
  • 是否有 merchant city;
  • 是否有 proxy type;
  • 是否有 proxy value。

很多人第一次看会觉得奇怪:

  • 为什么城市也是必填?

原因就在于协议层字段要求和国家实现差异。

对 Odoo 来说,真正重要的不是“后台觉得这些字段差不多”,而是:

  • 这些字段缺了之后,最终码是否仍属于标准或本地实现允许的可扫码对象。

而 proxy_type / proxy_value 的存在也说明,某些国家的 EMV QR 并不直接把纯银行账号当唯一入口,可能还涉及:

  • 手机号;
  • 虚拟支付别名;
  • 商户 proxy 标识。

这也是为什么核心模块只给了一个最小通用接口。


一句话理解 Odoo EMV 模块的真正定位

如果用一句话概括这个模块,我会说:

它不是“生成二维码图片”的模块,而是“把收款信息编码成支付网络认可的结构化载荷,再交给二维码条码层展示”的模块。

所以做支付码集成时,最该盯住的不是图片长什么样,而是:

  • TLV 字段对不对;
  • 金额是否该进 payload;
  • CRC 是否正确;
  • 字符是否被安全清洗;
  • 国家差异是不是落在正确扩展点上。

这也是为什么看似只有几十行代码,平台意味却非常浓。

DISCUSSION

评论区

想参与讨论?先 登录 再发表评论。
还没有评论,你可以成为第一个留言的人。