本文是 erlang_pay 系列三篇中的第一篇(总览)。第二篇讲回调验签,第三篇讲金额与退款的两个坑。
你的产品要收钱了。加个会员、卖个课程、开个打赏,总之要接支付。
然后你打开支付宝开放平台,发现官方 SDK 有 Java 版、Go 版、Python 版、PHP 版、.NET 版------就是没有 Erlang 版。打开微信支付文档,同样。Stripe 好一点,官方库列表里依然没有 Erlang。
这不是 erlang_pay 要解决的唯一问题,但它是起点。
先说结论
erlang_pay 是一个纯 Erlang 的支付网关库,2026 年 6 月 14 日提交第一行代码,2026 年 9 月 23 日发布 0.3.0,Apache-2.0 协议。
它现在支持三家:
- 支付宝 App 支付
- 微信支付 v3(JSAPI / Native 扫码)
- Stripe(PaymentIntent)
全部源码 10 个文件、2741 行 。运行时依赖只有 OTP 自带的几个库(crypto、public_key、inets、ssl),加一个 JSON 库 jsone------你的 rebar.config 里只需要写一行 {deps, [{jsone, "1.8.1"}]}。
它管的事:下单、退款、回调验签、查单、对账单下载、关单、撤单,从收钱到退钱的完整生命周期。
它不管的事:订单是你的、账是你的、发货逻辑也是你的。它只负责跟支付机构打交道那一段。
为什么"统一门面"值得单独说
三家支付机构的接口风格,打个比方,就像三家快递公司各有一个下单 App:注册三套账号、学三套下单流程、看三种格式的物流通知。
支付宝用 RSA2 签名,微信 v3 用平台公钥加 AES-GCM 解密,Stripe 用 webhook secret 算 HMAC。下单参数一个叫 out_trade_no,一个叫 payment_intent,返回值更是各说各话。
erlang_pay 做的事,是给你的业务代码面前放一个统一的前台 。整个库对外的入口就 12 个函数,都在 erlang_pay 这一个模块里:
erlang
%% 下单
erlang_pay:create_payment(Gateway, Cfg, Order).
%% 退款
erlang_pay:refund(Gateway, Cfg, Refund).
%% 回调验签
erlang_pay:verify_notify(Gateway, Cfg, Ctx).
%% 查单 / 下载对账单 / 关单 / 撤单
erlang_pay:query(Gateway, Cfg, Q).
erlang_pay:download_bill(Gateway, Cfg, Bill).
erlang_pay:close(Gateway, Cfg, O).
erlang_pay:cancel(Gateway, Cfg, O).
%% 还有 version/0、build_pay_sign/3、gateway_module/1、capabilities/1、supports/2
第一个参数换一下,alipay、wechat、stripe,流程不变。三家之间的差异,库用「打了 tag 的返回 map」隔开------你拿到的结果 map 里,支付宝是 order_str(交给客户端 SDK 唤起),微信 Native 是 code_url(生成二维码),Stripe 是 client_secret(给前端 Stripe.js)。字段名不同,但都在 {ok, Map} 这个统一的壳里,你的代码 switch 一下就行。
三段代码走一遍
支付宝 App 支付
erlang
Cfg = #{app_id => AppId, private_key => MchPriPem, public_key => AlipayPubPem,
notify_url => <<"https://example.com/pay/callback/alipay">>},
{ok, #{order_str := OrderStr}} =
erlang_pay:create_payment(alipay, Cfg, #{out_trade_no => <<"R123">>, amount_fen => 1000}).
%% 把 OrderStr 交给客户端的 AlipaySDK,用户手机的支付宝就被唤起了
这一笔是 10 元(amount_fen => 1000,单位是"分"------为什么用分,系列第三篇细讲)。
微信 Native(用户扫码)
erlang
Cfg = #{app_id => AppId, mch_id => MchId, api_v3_key => V3Key,
mch_serial_no => Serial, private_key => MchPriPem,
platform_public_key => PlatformPubPem,
notify_url => <<"https://example.com/pay/callback/wechat">>},
{ok, #{code_url := CodeUrl}} =
erlang_pay:create_payment(wechat, Cfg, #{out_trade_no => <<"R123">>,
amount_fen => 1000, pay_type => native}).
Stripe
erlang
Cfg = #{secret_key => <<"sk_...">>, webhook_secret => <<"whsec_...">>},
{ok, #{payment_no := Pi, client_secret := Cs}} =
erlang_pay:create_payment(stripe, Cfg, #{out_trade_no => <<"R123">>,
amount_fen => 1000, currency => <<"usd">>}).
注意到没有:三段代码长得几乎一样。差异全在 Cfg(每家要的凭据不同)和返回 map 的字段上。
几条值得说的设计规矩
这些规矩本身比代码更有普适性,做任何支付接入都用得上:
1. 凭据用参数传,库不偷看配置。 商户私钥、API 密钥全部从 Cfg 这个 map 传进去,库绝不读 application env (Erlang 里的全局配置)。好处:密钥的存放方式是你的架构决策------放数据库、放 KMS、放启动参数都行,库不掺和;密钥缺了也不会崩溃,直接返回 {error, {no_credential, _}},而且密钥、签名、报文绝不写进日志。
2. 金额只收整数。 amount_fen => 1000 就是 1000 分。库内部没有任何浮点数。为什么,第三篇用一整篇讲,这里只说一句:0.1 + 0.2 在绝大多数编程语言里不等于 0.3。
3. 出错不崩溃,统一格式。 所有失败返回 {error, {Code, Msg}},比如 bad_signature(验签失败)、timestamp_expired(回调时间戳超窗,疑似重放)、insecure_url(你要我访问的地址不是 https,拒发)。业务代码 case 一层就能兜住所有错误,不用担心哪天支付网关抽风把你的进程带崩。
4. 出站只走 HTTPS。 URL 校验不通过,请求根本不会发出去。
丑话说到前头
两条,都是实话:
-
还没跟真实沙箱联调过。 0.3.0 的状态是:本地 213 个测试用例全绿,dialyzer 零警告,CI 跑 OTP 28/29 双版本矩阵------但这些都发生在测试环境里。跟支付宝、微信、Stripe 的真实沙箱环境对一遍,是发布前还没做完的功课。想在生产用,请先跑通沙箱再上小流量。
-
测试是真的,但 mock 的位置要说清楚。 213 个用例里,签名和验签全部用测试时即时生成的 RSA 密钥对做真签名、真验签,mock 只打在 HTTP 边界上(不会真的向支付机构发请求)。也就是说,协议逻辑是动真格验证过的,"支付机构会怎么回"这一半还没有。
另外 0.3.0 还删掉了一个模块 epay_cert_mgr(微信平台证书的自动轮换)------因为证书生命周期没有做成闭环,与其留一个"看起来能用"的模块,不如删掉,改成调用方直接注入平台公钥。这个决策的来龙去脉,第二篇展开。
仓库信息
- GitHub: github.com/imboy-pub/e...
- Gitee: gitee.com/imboy-pub/e...
- Gitcode: gitcode.com/imboy/erlan...
- 协议:Apache-2.0
- 版本:0.3.0(2026-09-23),21 个提交,从 2026-06-14 到 2026-09-23
- 自检命令:
rebar3 eunit(213 用例)、rebar3 dialyzer(零警告)、bash scripts/gate.sh(清洁编译+单测+类型+打包全门)
IMBoy(一个开源可私有化的 IM 平台,Erlang/OTP 后端)要收钱,就有了这个库。erlang_pay 从 IMBoy 项目里长出来,然后独立成仓回馈社区------它不绑定 IMBoy,任何需要接支付渠道的 Erlang/OTP 项目都可以直接拿去用。
下一篇:先验章,再拆包:支付回调的验签到底在防什么