先说结论
EMV 收款二维码在 Odoo 里,绝不是“把银行账号和金额拼成一段字符串再画成二维码”那么简单。
从 /home/ubuntu/odoo-temp/addons/account_qr_code_emv/models/res_bank.py 可以看出,官方真正做的是:
- 把不同字段按 EMV Merchant-Presented 规则序列化成 TLV 结构。
- 根据金额是否为零,决定二维码是静态还是动态。
- 统一生成 CRC16,保证扫码端能校验内容完整性。
- 对商户名、城市、备注做字符清洗,避免不合规字符进入 payload。
- 把国家差异留给继承模块扩展,而不是写死在核心。
所以最短结论是:
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
评论区