erlang_pay

纯 Erlang 第三方支付网关库:支付宝(App 支付)、微信支付 v3(JSAPI/Native)、Stripe(PaymentIntent)。 A pure-Erlang payment gateway library for Alipay (App pay), WeChat Pay v3 (JSAPI/Native) and Stripe (PaymentIntent).

特性 / Features

安装 / Install

%% rebar.config
{deps, [{erlang_pay, {git, "https://github.com/imboy-pub/erlang_pay.git", {tag, "0.3.0"}}}]}.
%% Gitee 镜像 / Gitee mirror:
%% {deps, [{erlang_pay, {git, "https://gitee.com/imboy-pub/erlang_pay.git", {tag, "0.3.0"}}}]}.

快速上手 / Quick Start

金额单位为「分 / cents」整数。/ Amounts are integers in the smallest currency unit.

支付宝 App 支付 / Alipay App pay

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 唤起 / hand OrderStr to the client SDK.

微信 Native(扫码)/ WeChat Native

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

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">>}).

退款(Stripe 幂等)/ Refund (Stripe idempotency)

%% 必带稳定退款号 out_refund_no(或显式 idempotency_key)→ Idempotency-Key = "rf_" + 退款号;
%% 缺号返回 bad_request,绝不发送请求。禁止用 payment_intent 派生(同一 PI 可多次部分退款)。
%% A stable out_refund_no (or explicit idempotency_key) is required; without one
%% the call is rejected and nothing is sent. Never derive from payment_intent.
{ok, _} = erlang_pay:refund(stripe, Cfg, #{payment_intent => Pi, out_refund_no => <<"RF123">>}).

回调验签 / Webhook verification

%% 微信/Stripe:Ctx = #{headers => Headers, body => RawBody}(原始字节验签)
%% 支付宝:Ctx = #{form => FormMap}(已 url-decode 的通知表单)
{ok, Event} = erlang_pay:verify_notify(wechat, Cfg, #{headers => H, body => RawBody}).

安全要点 / Security Notes

错误码 / Error Codes

所有失败统一 {error, {Code, Msg}}。 All failures return {error, {Code, Msg}}.

Code 含义 / Meaning
bad_request 调用方输入不合法 / invalid caller input
no_credential 缺商户凭据 / missing merchant credential
unknown_gateway / unsupported 未知网关 / 网关不支持该能力 / unknown gateway or capability
bad_signature / missing_signature / serial_mismatch 验签失败 / 缺签名 / 序列号不匹配 / signature failures
timestamp_expired / invalid_timestamp 时间戳超窗或非法(防重放)/ timestamp out of window
app_id_mismatch / missing_app_id 支付宝通知 app_id 不符 / Alipay notify app_id mismatch
refund_failed / invalid_refund_response 退款终态失败 / 响应缺 status(结果不明按失败处理)/ refund failed or ambiguous
gateway_error / invalid_response 网关业务错误 / 响应解析失败 / gateway error or unparseable response
http_error 传输错误;超时=结果未知,先查后重 / transport error; on timeout the result is unknown — query before retry
insecure_url 出站 URL 非 https 等 / outbound URL rejected

测试 / Testing

rebar3 eunit # 213 tests / 213 个用例
rebar3 dialyzer # zero warnings / 零警告
bash scripts/gate.sh # 清洁编译+单测+类型+打包全门 / full clean gate

测试用即时生成的 RSA 密钥对做真签名/真验签,mock 只打在 HTTP 边界。 Tests generate real RSA key pairs and real signatures; mocking happens only at the HTTP boundary.

状态 / Status

0.3.0(2026-09-23):本地测试全绿;尚未对真实沙箱环境联调。 0.3.0 (2026-09-23): all local tests green; verification against live sandbox providers is still pending.

协议参考 / Protocol References

License

Apache-2.0