yansongda/pay v3 使用笔记:配置结构、回调边界与证书金额的几个坑
项目主仓:yansongda/pay,文档在 pay.yansongda.cn。框架扩展包是分开的:Laravel 用 yansongda/laravel-pay,Hyperf 用 yansongda/hyperf-pay,Yii 侧由社区维护 guanguans/yii-pay。
安装:
composer require yansongda/pay:~3.7.0 -vvv
覆盖范围
支付宝侧支持电脑支付、手机网站、APP、刷卡、扫码、账户转账、小程序;微信侧是公众号、小程序、H5、扫码、APP、刷卡;此外还有抖音小程序支付/退款/回调、银联、江苏银行 e融支付、通联支付、翼支付,以及 Apple 相关能力(Apple Pay 令牌验签解密、App Store Server Notifications V2 验签)。
README 里的说法是对支付宝/微信/银联功能 100% 兼容,其余走插件机制引入。代码遵循 PSR-2/3/4/7/11/14/18,支持多租户、Swoole、插件机制和事件系统,内置微信平台证书的自动获取方法。
配置:按网关分组的一个数组
所有配置集中在一个数组里,用 Pay::config() 一次性注入,之后按网关取门面:
use Yansongda\Pay\Pay;
Pay::config($this->config);
顶层按网关分组,每个网关下用 default 区分多套配置(多商户/多租户就是在这里扩展):
[
'alipay' => ['default' => [/* ... */]],
'wechat' => ['default' => [/* ... */]],
'logger' => [
'enable' => false,
'file' => './logs/alipay.log',
'level' => 'info',
'type' => 'single',
'max_file' => 30,
],
'http' => [
'timeout' => 5.0,
'connect_timeout' => 5.0,
],
]
logger 和 http 是公共项,超时时间建议按自己网关链路的 P99 调,别用默认值裸奔。
支付宝:三张证书各管什么
支付宝配置键:app_id、app_secret_cert(应用私钥)、app_public_cert_path(应用公钥证书)、alipay_public_cert_path(支付宝公钥证书)、alipay_root_cert_path(支付宝根证书),加上 return_url、notify_url、app_auth_token、service_provider_id,以及 mode(PayMode::NORMAL / PayMode::SANDBOX / PayMode::SERVICE)。
三张证书不是备份关系,用途分得很清楚:
app_secret_cert:应用私钥,给自己发出的请求签名用。app_public_cert_path:应用公钥证书,随请求一起上传给支付宝,供对方验你发的签名。alipay_public_cert_path:支付宝公钥证书,用来验证支付宝返回值和异步通知的签名。alipay_root_cert_path:支付宝根证书,用于校验证书链,确认支付宝公钥证书本身可信。
用证书模式就得把这三个路径都配齐,缺一个就会出现验签失败而非报配置错误,排查时先看这里。
下单:
Pay::alipay()->web([
'out_trade_no' => ...,
'total_amount' => '0.01',
'subject' => '...',
]);
回调就一行,README 的注释写的是「是的,验签就这么简单」:
$data = Pay::alipay()->callback();
应答:
return Pay::alipay()->success();
这里最容易出错的理解是:callback() 只做验签,不校验金额、不校验订单归属。 它确认的是「这条通知确实来自支付宝」,不代表这笔钱该收。README 明确列出了必须自己判断的几点:
- 只有
trade_status为TRADE_SUCCESS或TRADE_FINISHED才算付款成功; - 校验
out_trade_no是否为系统创建的订单号,防止被塞入伪造订单号; - 判断
total_amount是否为订单实际金额(注意金额单位问题,见下文); - 校验
seller_id/seller_email是否对应本商户,避免多商户场景串单; - 验证
app_id是否为本商户。
另外,处理逻辑无论成功失败,都要返回 Pay::alipay()->success() 这样的应答,否则支付宝会持续重推通知。业务异常建议在本地落库并做幂等,而不是靠「不返回」来表达失败。
微信:平台公钥证书为什么建议落盘
微信配置键:mch_id、mch_secret_key_v2、mch_secret_key(v3 密钥)、mch_secret_cert(商户私钥,字符串或路径均可)、mch_public_cert_path(商户公钥证书路径)、notify_url、mp_app_id、mini_app_id、app_id、sub_mp_app_id、sub_app_id、sub_mini_app_id、sub_mch_id、wechat_public_cert_path、mode。
wechat_public_cert_path 在 README 里标的是「选填」,但紧跟着一句「强烈建议 php-fpm 模式下配置此参数」。原因在运行模型上:微信 v3 的应答验签需要使用微信平台公钥证书,而这个证书会轮换。库内置了自动获取方法,在 Swoole 这类常驻内存的进程里,拉一次放在内存里就行;但 php-fpm 每个请求结束后进程状态就丢了,等于每次支付请求都要额外发一次 HTTP 去取平台证书,多一次网络往返、多一个失败点,赶上微信侧抖动,直接表现为支付不可用。
落盘配置的写法是「证书 ID => 本地 pem 路径」的映射:
'wechat_public_cert_path' => [
'45F59D4DABF31918AFCEC556D5D2C6E376675D57' => __DIR__ . '/Cert/wechatpay_45F***D57.pem',
],
key 是证书/公钥 ID,实际值以微信商户平台下载到的为准,不要照抄示例。
下单(小程序):
Pay::wechat()->mp([
'out_trade_no' => time() . '',
'description' => 'subject-测试',
'amount' => ['total' => 1],
'payer' => ['openid' => '...'],
]);
返回对象里是 appId、timeStamp、nonceStr、package、signType,直接给前端调起支付即可。
金额单位要特别注意:微信 amount.total 的单位是分,上面的 1 表示 0.01 元(整数);支付宝 total_amount 的单位是元,且是字符串 '0.01'。 两边混用是回调金额校验失配的常见来源——同一个订单在库里存的是分,拿微信回调的 total 直接比是对的,拿支付宝回调的 total_amount 直接比就差了 100 倍。
回调和应答:
$data = Pay::wechat()->callback();
return Pay::wechat()->success();
和支付宝一样,callback() 只做验签,out_trade_no、金额、mch_id/appid 的比对仍然要自己写。抖音的 callback 还多一步:需要传入 PSR-7 的 ServerRequestInterface。
抖音与江苏银行
抖音配置键:app_id(即 client_key)、app_secret、app_private_key、douyin_public_key、notify_url。下单调 Pay::douyin()->mini([...]),服务端不发 HTTP 请求,只把官方 camelCase 字段透传过去并生成签名,返回 data 和 byteAuthorization 给前端去调 tt.requestOrder。也就是说这一步的「下单」实际发生在前端,服务端负责的是签名。
江苏银行 e融支付配置键:svr_code、partner_id、public_key_code、mch_secret_cert_path、mch_public_cert_path、jsb_public_cert_path、notify_url、mode。
v3 与 v2 的差别
README 强调 v3 与 v2 底层差异很大:基础架构重写,引入插件机制,把不需要关注的细节隐藏起来,并支持自动获取微信平台证书。如果是从 v2 迁移过来,配置结构和调用方式基本都要改,不要指望改个版本号就能跑。
什么时候用它,什么时候自己封装
用它的合适场景:项目要同时接支付宝 + 微信 + 抖音/银联中的两个以上;团队不想维护两套签名、验签、证书轮换代码;已经在 Laravel/Hyperf 里,直接用对应扩展包;或者只需要标准的下单/回调链路,不涉及特殊报文定制。
自己封装更省事的情况:只接一家、只用一个支付产品(比如只有 Native 扫码),接口就两三个,引入一个带插件体系和事件系统的库反而增加排查成本;或者对请求参数有强定制需求,需要替换底层 HTTP 客户端、自己控制证书加载时机,此时框架的封装会变成阻碍。
折中做法是只用它的签名/验签和证书管理,下单与回调业务逻辑自己写——Pay::alipay()->callback() 那一步的验签,仍是最值得直接复用的部分。