
erlang_pay 系列第三篇也是写给所有后端的一篇——这两条规矩跟语言无关跟钱有关。第一篇erlang_pay为 Erlang 补上支付这块拼图 · 第二篇 erlang_pay 先验章再拆包支付回调的验签到底在防什么先做一道题0.1 0.2 ?在人教版数学课本里等于 0.3。在绝大多数编程语言的浮点数里等于0.30000000000000004。这不是哪个语言的 bug是二进制表示十进制小数的先天局限。平时无所谓但这段算式如果出现在收钱的代码里0.00000000000000004 的误差乘上每天的订单量对账就能对出悬案数据库里 12.34支付机构账上 12.34000000000003谁也说不清哪笔错了。erlang_pay 的应对是把一件事定成铁律再把另一件容易想当然的事写成显式的表。这篇讲这两件事。规矩一金额一律用最小单位的整数erlang_pay 的所有接口金额只收整数单位是最小货币单位——人民币和美元是分1234 就是 12.34 元。库内部没有一个浮点数。打比方裁缝量布用毫米不用1.234 米。整数没有小数点就没有二进制小数的误差加减乘除全部是整数运算一分钱都不会凭空多出来或消失。epay_money模块负责单位换算主单位人看的12.34 元以字符串进出内部全程整数epay_money:to_minor(12.34,USD).%% {ok, 1234}epay_money:to_major(1234,USD).%% {ok, 12.34}注意输入是字符串12.34不是浮点数12.34——从你的业务代码入口处就把浮点挡在门外。规矩一的隐藏陷阱别把分想当然到这里很多人会说懂了金额×100 存分。这正是要纠正的第二个误区最小单位 分 ×100不成立。ISO 4217货币代码国际标准给每种货币规定了一个小数位数exponent它不是恒等于 2美元、欧元、人民币、港币……小数 2 位1 元 100 分×100 没错日元、韩元小数 0 位。日元没有分1 日元就是最小单位。×100 会把人家的钱放大一百倍巴林第纳尔、科威特第纳尔小数 3 位。1 第纳尔 1000 费尔×100 反而缩小了十倍erlang_pay 没有把×100写死在代码里而是维护了一张显式的表src/epay_money.erl-define(EXPONENTS,#{USD2,EUR2,GBP2,CNY2,AUD2,CAD2,HKD2,SGD2,JPY0,KRW0,BHD3,KWD3,JOD3,OMR3}).表里没有的币种换算接口直接报unsupported_currency——宁可拒绝也不替你猜。金额传了负数报negative_amount。这些错误在本地就拦下请求根本不会发到支付机构。Stripe 接口之所以多一个currency参数就是因为换算规则跟着币种走库需要知道你在收哪种钱。规矩二退款必须带流水号第二件事跟退钱有关。场景你调用退款接口网络抖了一下请求超时。此刻你不知道钱退没退——可能没发出去可能发出去了、机构也处理完了、只是响应没传回来。于是你重试一次。如果支付机构把重试当成一笔新的退款请求同一笔订单就被退了两次钱。这在支付行业有个专门的名字要防非幂等。幂等idempotent是个数学词翻译成人话同一个操作执行一次和执行一百次结果一样。Stripe 给的解法是幂等键Idempotency-Key请求带上一个你自己定的唯一编号重试时带同一个编号Stripe 就认得这笔我处理过了返回上次的结果不会重复执行。erlang_pay 把这个机制定成了硬规矩写在 README 里退款必须带稳定退款号out_refund_no缺号直接返回bad_request请求根本不发送幂等键由退款号生成Idempotency-Key rf_ 退款号——同一个号重发一百次Stripe 只执行一次明确禁止用支付单号派生幂等键。原因同一笔支付可以多次部分退款先退 1 元、再退 2 元如果用支付单号当幂等键第二次合法退款会被误判成重复而拒绝打个比方退款号是取件码。凭同一张取件码去取件柜子不会吐两份但一个快递柜里有多个包裹多次部分退款时每个包裹各有各的取件码。配套军规超时了先查后重顺着上面的场景说下去超时之后到底该干什么erlang_pay 的错误码表里有一行原话http_error传输错误超时 结果未知先查后重。超时不等于失败。正确动作是先调query查单确认这笔退款的真实状态再决定要不要重试。跳过先查直接重发就是把钱交给运气——幂等键是保险绳不是免死金牌两条一起用才保险。这些规矩不是嘴上说说erlang_pay 现在有 213 个测试用例0.3.0 里从 113 个增加而来全部通过dialyzer 类型检查零警告。更值得说的是测试的做派每个用例的签名/验签用的都是测试运行时即时生成的 RSA 密钥对真的签名、真的验签、真的加密解密mock 只打在 HTTP 边界上不会真的把请求发给支付机构。也就是说×100 陷阱防住了没有、缺退款号会不会拦下、超时错误码对不对都是有代码兜底的合同不是文档里的愿望。当然也要再说一次丑话本地测试全绿 ≠ 生产验证过跟三家真实沙箱的联调还在计划里这是 0.3.0 的诚实声明也是你试用前该知道的事。仓库信息GitHub: https://github.com/imboy-pub/erlang_payGitee: https://gitee.com/imboy-pub/erlang_payGitcode: https://gitcode.com/imboy/erlang_pay协议Apache-2.0版本0.3.02026-09-2321 个提交从 2026-06-14 到 2026-09-23自检命令rebar3 eunit213 用例、rebar3 dialyzer零警告、bash scripts/gate.sh清洁编译单测类型打包全门本文事实可复核src/epay_money.erlexponent 表与 to_minor/to_major、README「退款Stripe 幂等」「错误码」、CHANGELOG 0.3.0测试 113→213。小结成三句话够你带去任何语言的支付代码评审金额只用最小单位整数浮点别进门别假定分等于 ×100币种的小数位数要查表退款必带稳定流水号超时先查单再重试。IMBoy一个开源可私有化的 IM 平台Erlang/OTP 后端要收钱就有了这个库。erlang_pay 从 IMBoy 项目里长出来然后独立成仓回馈社区——它不绑定 IMBoy任何需要接支付渠道的 Erlang/OTP 项目都可以直接拿去用。