☰
yansongda/pay 翼支付插件机制全解析:自由组合插件调用任意翼支付 API
2026/10/15 2:21:57 网站建设 项目流程
  • 后端
  • 金融科技

【免费下载链接】pay

可能是我用过的最优雅的 Alipay/WeChat/Unipay/江苏银行 的支付 SDK 扩展包了

项目地址:https://gitcode.com/yansongda/pay
点击查看免费下载

导读

本文围绕yansongda/pay为翼支付(中国电信翼支付商户开放平台,MAPI)提供的高度可组合插件机制展开。得益于 yansongda/pay 的插件化架构,你可以把任意内置插件、甚至自定义插件自由拼接成一条「插件链」,通过Pay::bestpay()->pay($allPlugins, $params)调用翼支付的任何 API,而不仅限于扩展包预设的快捷方法。读完本文,你将掌握翼支付 V1 全套内置插件的职责与顺序、sdkRequest 签名信封的组装原理、公共插件的源码级实现细节,以及如何参照快捷方式编写自己的插件组合。


插件机制的核心思想

翼支付通道在yansongda/pay中并不是一堆「黑盒方法」,而是被拆解为一组职责单一的插件(Plugin)。每个插件只做一件事:

  • 注入配置、组装报文、加签、构建请求、验签、校验业务响应、解析结果……

在调用时,把这些插件按顺序排列成数组交给管道(Pipeline)依次执行,最终完成一次完整的 API 调用。这一点在 Provider/Bestpay.php 的pay()方法中直接体现:

public function pay(array $plugins, array $params): Collection|MessageInterface|Rocket|null { return Artful::artful($plugins, $params); }

也就是说,$plugins数组即整条调用链,管道的调度执行由底层yansongda/artful内核完成(扩展包内置的各类快捷方法,本质上也只是封装好的插件数组)。因此文档中的核心主张成立:你可以自由使用任何内置插件,也可以编写自定义插件,去调用翼支付的任何 API。


快速上手:手动组装插件链

这是官方文档给出的最直接用法——不依赖任何快捷方法,完全手动指定插件链:

use Yansongda\Pay\Pay; Pay::config($config); $params = [ 'outTradeNo' => 'order-1001', ]; $allPlugins = [ \Yansongda\Pay\Plugin\Bestpay\V1\StartPlugin::class, \Yansongda\Pay\Plugin\Bestpay\V1\Pay\Web\PayPlugin::class, \Yansongda\Pay\Plugin\Bestpay\V1\AddPayloadSignPlugin::class, \Yansongda\Pay\Plugin\Bestpay\V1\AddRadarPlugin::class, \Yansongda\Pay\Plugin\Bestpay\V1\VerifySignaturePlugin::class, \Yansongda\Pay\Plugin\Bestpay\V1\ResponsePlugin::class, \Yansongda\Artful\Plugin\ParserPlugin::class, ]; $result = Pay::bestpay()->pay($allPlugins, $params);

这条链在做什么?以「PC 收银台下单」为例,从源码看每一步:

顺序插件职责(对应源码)
1StartPlugin从配置注入merchantNo、institutionType、institutionCode,并合并业务参数(StartPlugin.php)
2Pay\Web\PayPlugin写入_url=/pay/tradeCreate、tradeChannel=WEBCASHIER、accessCode=CASHIER、默认ccy=156及通知/回跳地址(Pay/Web/PayPlugin.php)
3AddPayloadSignPlugin组装sdkRequest信封(path/commonParams/bizContent/sign)并加签(AddPayloadSignPlugin.php)
4AddRadarPlugin构建 HTTP 请求:网关 URL +BESTPAY_MAPI_VERSION、headers、form 请求体(AddRadarPlugin.php)
5(内核发送请求)Artful 管道按Direction发起 HTTP 调用
6VerifySignaturePlugin对响应报文用平台公钥验签(VerifySignaturePlugin.php)
7ResponsePlugin校验 HTTP 状态码与业务success标志(ResponsePlugin.php)
8ParserPlugin把最终结果解析为Collection(或Rocket)返回

需要说明的是:请求插件(第 1~4 步)在发送前执行,验签与业务校验插件(第 6~7 步)在其内部先调用$next($rocket)拿到响应后再执行校验,这是管道「洋葱模型」的典型写法,参见 VerifySignaturePlugin.php 与 ResponsePlugin.php。


内置业务插件清单

文档将翼支付 V1 内置插件分为「支付」「查询」「其他」三大类,全部位于src/Plugin/Bestpay/V1目录下。

支付

场景接口路径插件类
PC 收银台/pay/tradeCreate(tradeChannel=WEBCASHIER)\Yansongda\Pay\Plugin\Bestpay\V1\Pay\Web\PayPlugin
手机收银台/pay/tradeCreate(tradeChannel=MOBILECASHIER)\Yansongda\Pay\Plugin\Bestpay\V1\Pay\H5\PayPlugin
聚合收款码/aggregate/aggregatepay/offline/c2b/payOrder\Yansongda\Pay\Plugin\Bestpay\V1\Pay\Scan\PayPlugin

其中 PC 与手机收银台共用超级收银台tradeCreate接口,仅tradeChannel不同(Pay/Web/PayPlugin.php 与 Pay/H5/PayPlugin.php 中分别为WEBCASHIER与MOBILECASHIER);聚合收款码走线下聚合 1006 通道,返回的result.codeUrl即为二维码串(Pay/Scan/PayPlugin.php)。

查询

场景接口路径插件类
超级收银台订单查询/integrate/orderQuery\Yansongda\Pay\Plugin\Bestpay\V1\Pay\QueryPlugin
线下聚合订单查询/aggregate/aggregatepay/tradeQuery\Yansongda\Pay\Plugin\Bestpay\V1\Pay\AggregateQueryPlugin

两个查询插件只负责写入_url路由(QueryPlugin.php、AggregateQueryPlugin.php),查询参数(如outTradeNo/tradeNo)直接由业务参数透传。

其他

场景接口路径插件类
退款/integrate/refund\Yansongda\Pay\Plugin\Bestpay\V1\Pay\RefundPlugin
关单/pay/closeOrder\Yansongda\Pay\Plugin\Bestpay\V1\Pay\ClosePlugin
回调验签—(异步通知)\Yansongda\Pay\Plugin\Bestpay\V1\CallbackPlugin

退款与关单插件同样只写入_url(RefundPlugin.php、ClosePlugin.php),具体业务字段完全透传给翼支付,与官方参数无差别。


公共插件逐个拆解

公共插件是每次调用几乎都离不开的「骨架」,也是理解翼支付通道实现的关键。

StartPlugin —— 配置注入

将配置中的merchantNo、institutionType、institutionCode注入 payload,并把用户参数(_开头的控制参数除外)合并进来(StartPlugin.php)。测试 StartPluginTest.php 验证了三件事:配置注入默认值、商户号从配置自动注入、用户显式参数可覆盖配置值。

AddPayloadSignPlugin —— 签名组装(sdkRequest 信封 + 加签)

这是翼支付通道最核心的插件。它把 payload 组装成官方要求的sdkRequest信封(AddPayloadSignPlugin.php):

$envelope = [ 'path' => (string) $path, // 业务接口路径,如 /pay/tradeCreate 'commonParams' => json_encode($commonParams, ...), // institutionType + institutionCode 'bizContent' => json_encode($bizContent, ...), // 业务参数 ]; $envelope['sign'] = self::signBestpayContent($config, self::getBestpaySignContent($envelope));

几个值得注意的源码细节:

  • bizContent 过滤 null:array_filter会剔除值为null的字段以及_前缀字段和sign/institutionType/institutionCode,对齐官方 Java SDK fastjson 默认不序列化 null 的行为;但空字符串保留(对应测试 AddPayloadSignPluginTest.php)。
  • JSON 序列化选项:使用JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES,非 ASCII 与/均不转义,避免含 URL 的字段(如notifyUrl)被转义为\/导致签名不一致(对应测试 L73-L93)。
  • 缺_url会报错:如果业务插件未先执行导致缺少_url,会抛出Exception::PARAMS_BESTPAY_URL_MISSING,提示「可能插件用错顺序,应该先使用业务插件」(AddPayloadSignPlugin.php)。

签名算法与拼串规则集中在 BestpayTrait.php:

  • 拼串(getBestpaySignContent,L62-L74):按 key 升序拼成k=v&k=v,排除sign;null拼为k=null、空串拼为k=、布尔拼为true/false、嵌套数组转 key 升序 JSON——全部对齐官方 Java SDK 的AssembleUtil语义(测试见 VerifySignaturePluginTest.php)。
  • 加签(signBestpayContent,L88-L106):通过 CertManager::getPkcs12Certs 读取商户 PKCS12 证书,取第一把私钥,使用SHA256withRSA签名并 Base64 输出。

AddRadarPlugin —— 构建请求

把信封拼装成真正的 HTTP 请求(AddRadarPlugin.php):

$url = self::getBestpayUrl($config, $payload) .'?BESTPAY_MAPI_VERSION='.rawurlencode($config->getApiVersion());
  • 网关地址由 Provider/Bestpay.php 的URL常量提供(https://mapi.bestpay.com.cn/mapi),默认路径/sdkRequest;getBestpayUrl的具体拼接逻辑见 BestpayTrait.php。
  • 请求方式默认POST,Content-Type: application/x-www-form-urlencoded;charset=utf-8,请求体用http_build_query序列化信封。

VerifySignaturePlugin —— 响应验签

响应到达后,用翼支付平台公钥校验签名(VerifySignaturePlugin.php)。验签实现见 BestpayTrait::verifyBestpaySign:先尝试SHA1withRSA再尝试SHA256withRSA(官方 Demo 固定使用 SHA1withRSA 验签,请求加签为 SHA256withRSA,双试以兼容联调差异),验签失败抛出InvalidSignException。

ResponsePlugin —— 响应业务校验

双重校验(ResponsePlugin.php):

  1. HTTP 状态码必须在 2xx 区间,否则抛RESPONSE_CODE_WRONG;
  2. 业务标志success必须为true,否则抛RESPONSE_BUSINESS_CODE_WRONG,并附带errorCode/errorMsg便于排查。

对应测试覆盖了成功、业务失败、HTTP 失败三种路径(ResponsePluginTest.php)。

CallbackPlugin —— 回调验签

异步通知的入口插件。它从参数中取出_request(Collection),校验平台公钥签名后,把报文直接作为Destination返回、方向置为NoHttpRequestDirection(不发请求)(CallbackPlugin.php)。验签同样走SHA1/SHA256双试逻辑,且支持多租户隔离(用不同租户证书验签会失败,见 CallbackPluginTest.php)。


插件命名空间与版本约定

文档明确指出:

各插件命名空间均带V1(对齐 MAPI 接口版本BESTPAY_MAPI_VERSION=1.0.3),后续接口版本变更时按V{n}组织。

从源码可以得到印证:目录结构为src/Plugin/Bestpay/V1/...,配置项api_version默认值1.0.3(BestpayConfig.php),而 AddRadarPlugin 会把该版本号以BESTPAY_MAPI_VERSION参数拼到网关 URL 上。未来若翼支付升级 MAPI 接口版本,扩展包会在V2命名空间下组织新插件,两代插件互不干扰。


进阶:参照快捷方式编写自己的插件组合

如果你不想每次都手写一长串插件数组,扩展包为常见场景提供了快捷方法(如web/h5/scan/query/refund/close),其本质就是预置的插件组合。以 WebShortcut.php 为例:

return [ StartPlugin::class, Pay\Web\PayPlugin::class, AddPayloadSignPlugin::class, AddRadarPlugin::class, VerifySignaturePlugin::class, ResponsePlugin::class, ParserPlugin::class, ];

这与文档中的手写示例完全一致。更复杂的组合可以参考 QueryShortcut.php:它通过_action参数在「超级收银台订单查询」与「线下聚合订单查询」两条插件链之间分流(常量定义于 BestpayAction.php)。

因此,编写「自定义 API 调用」只需要两步:

  1. 参照上述骨架选择/编写业务插件,写入目标接口的_url;
  2. 保持公共插件骨架不变(StartPlugin→ 业务插件 →AddPayloadSignPlugin→AddRadarPlugin→VerifySignaturePlugin→ResponsePlugin→ParserPlugin),传入Pay::bestpay()->pay($plugins, $params)。

记住:业务插件必须放在AddPayloadSignPlugin之前执行,因为它依赖_url来组装信封(见上文PARAMS_BESTPAY_URL_MISSING异常)。


配套实战:支付、查询、退款、关单、回调、应答

与插件清单配套的完整实战文档位于仓库web/docs/v3/bestpay/目录,此处给出与插件对应的核心用法速览(金额单位为「分」,与其他渠道习惯不同,请特别注意换算):

支付(web / h5 / scan)

// PC 收银台 $result = Pay::bestpay()->web([ 'outTradeNo' => '1514027114', // 商户订单号 'tradeAmt' => '1', // 单位:分 'ccy' => '156', // 人民币,默认 156 'subject' => 'yansongda 测试', 'goodsInfo' => 'yansongda 测试', 'operator' => '3178033925245778', // 操作员,常与商户号相同 'mediumType' => 'WIRELESS', // 媒介类型(以商户开通产品联调为准) 'requestDate' => date('Y-m-d H:i:s'), ]); // $result 内含 result.payUrl / result.cashierUrl 等字段(以沙箱联调为准) // 手机收银台(参数与 web 一致) $result = Pay::bestpay()->h5(['outTradeNo' => '1514027114', 'tradeAmt' => '1', 'subject' => 'yansongda 测试']); // 聚合收款码 $codeUrl = Pay::bestpay()->scan([ 'outTradeNo' => '1514027114', 'tradeAmt' => '1', 'subject' => 'yansongda 测试', ])->get('result.codeUrl'); // 二维码内容

web/h5走超级收银台/pay/tradeCreate(对应Pay\Web\PayPlugin/Pay\H5\PayPlugin),scan走线下聚合/aggregate/aggregatepay/offline/c2b/payOrder(对应Pay\Scan\PayPlugin)。详细说明见 支付文档。

查询(query)

// 默认:超级收银台订单查询(/integrate/orderQuery,对应 QueryPlugin) $result = Pay::bestpay()->query(['outTradeNo' => '1514027114']); $status = $result->get('result.tradeStatus'); // 线下聚合订单查询(/aggregate/aggregatepay/tradeQuery,对应 AggregateQueryPlugin) $result = Pay::bestpay()->query(['outTradeNo' => '1514027114', '_action' => 'aggregate']);

_action分流机制对应 QueryShortcut.php 中的两条插件链。详细说明见 查询文档。

退款(refund)

$result = Pay::bestpay()->refund([ 'outTradeNo' => '1514027114', // 原交易订单号,与 tradeNo 二选一 'outRequestNo' => 'REFUND001', // 商户退款请求号 'refundAmt' => '1', // 退款金额,单位:分,支持部分退款 'requestDate' => date('Y-m-d H:i:s'), 'originalTradeDate' => '2026-09-10', 'operator' => '3178033925245778', ]);

对应RefundPlugin(/integrate/refund)。退款为异步类交易,最终结果请以回调(notifyType=REFUND)或查询为准。详细说明见 退款文档。

关单(close)

$result = Pay::bestpay()->close(['outTradeNo' => '1514027114']); // 与 tradeNo 二选一

对应ClosePlugin(/pay/closeOrder)。注意:翼支付官方无对等cancel(撤销)接口,调用Pay::bestpay()->cancel()会抛出异常(见 Provider/Bestpay.php),如需撤销请以退款方式处理。详细说明见 关单文档。

回调(callback)与应答(success)

// 接收并验签回调(自动识别翼支付请求;php-fpm 下可无参调用) $data = Pay::bestpay()->callback(); // 或传递 ServerRequestInterface(swoole 推荐)/ 自行解析的 array // 以 tradeStatus 判断交易结果 if ('SUCCESS' === $data->get('tradeStatus')) { // 业务处理(建议同时校验 outTradeNo 与金额,并处理重复通知的幂等) } // 返回成功应答,翼支付才会认为通知成功,否则会按官方契约重发通知 return Pay::bestpay()->success(); // => {"resultCode":"SUCCESS","resultMsg":"OK"}

回调返回的关键字段(以翼支付开发者文档aggregatePayOrRefundNotify契约为准,需商户登录):

字段说明取值
notifyType通知类型PAY/REFUND
tradeStatus交易状态SUCCESS/FAIL/NOTPAY/CLOSE
outTradeNo商户订单号—
tradeNo翼支付交易号—
totalAmt交易金额(分)—

回调验签走CallbackPlugin(平台公钥,SHA1/SHA256 均尝试),验签失败抛出InvalidSignException;回调支持通过第二参数切换租户,如Pay::bestpay()->callback(null, ['_config' => 'yansongda'])。应答success()的实现见 Provider/Bestpay.php。详细说明见 回调文档 与 应答文档。


前置条件:翼支付配置与证书

在使用上述任何插件之前,需要先完成配置与证书准备。完整配置示例见 翼支付快速开始,核心配置项如下:

use Yansongda\Pay\Pay; use Yansongda\Pay\Enum\PayMode; $config = [ 'bestpay' => [ 'default' => [ // 必填-商户号(自动注入 bizContent.merchantNo,下单时可覆盖) 'merchant_no' => '3178033925245778', // 必填-机构号(commonParams.institutionCode) 'institution_code' => '3178033925245778', // 选填-机构类型,默认 MERCHANT 'institution_type' => 'MERCHANT', // 必填-商户 PKCS12 证书路径 'mch_secret_cert_path' => '/path/bestpay.p12', // 必填-P12 密码 'mch_secret_cert_password' => '******', // 必填-翼支付平台公钥证书路径(响应/回调验签) 'bestpay_public_cert_path' => '/path/bestpay.cer', // 选填-接口版本,默认 1.0.3 'api_version' => '1.0.3', // 选填 'notify_url' => 'https://yansongda.cn/bestpay/notify', 'return_url' => 'https://yansongda.cn/bestpay/return', // 选填-PayMode::NORMAL / PayMode::SANDBOX 'mode' => PayMode::NORMAL, ], ], ]; Pay::config($config);

这些配置项与 BestpayConfig.php 的字段一一对应。从源码可确认的要点:

  • 必填校验:merchantNo、institutionCode、mchSecretCertPath、mchSecretCertPassword、bestpayPublicCertPath五者缺一不可,否则抛CONFIG_BESTPAY_INVALID(BestpayConfig.php)。
  • 模式限制:翼支付一期无服务商模式,mode仅支持NORMAL与SANDBOX(BestpayConfig.php),配置SERVICE会被validateMode拦截(测试见 BestpayConfigTest.php)。
  • 沙箱说明:翼支付沙箱 base URL 与生产相同(Provider/Bestpay.php),沙箱是否支持联调以商户侧开通为准。
  • 证书说明:商户 PKCS12 通过openssl_pkcs12_read读取第一把私钥(不具备官方 Java KeyStore 的 alias 选择能力),若 p12 含多把私钥请确认第一把即为加签私钥,否则需拆分证书文件(BestpayTrait.php)。
  • 多租户:可同时配置default/yansongda等多套商户配置,通过参数_config选择(getTenant逻辑见 ProviderConfigTrait.php)。

常见问题与注意事项

  • 金额单位为「分」:翼支付接口金额单位与其他渠道不同,tradeAmt/refundAmt均以分为单位,请务必换算。
  • 异步类交易:web/h5/scan下单与退款均为异步类交易,最终结果请以回调(callback)或查询(query)为准。
  • 插件顺序:业务插件必须在AddPayloadSignPlugin之前执行,否则会因缺少_url抛出PARAMS_BESTPAY_URL_MISSING。
  • 回调幂等与金额校验:翼支付回调可能重复通知,建议业务处理时同时校验outTradeNo与金额,并做幂等处理。
  • 正确应答:收到回调并处理完毕后务必调用Pay::bestpay()->success()返回成功应答,否则翼支付会按官方契约重发通知;失败应答格式为{"resultCode":"FAILED","resultMsg":"FAILED"}。
  • 版本演进:插件命名空间按V{n}组织,当前为V1(对齐BESTPAY_MAPI_VERSION=1.0.3),后续接口版本变更时按新的V{n}目录组织,两代插件互不干扰。
  • 后端
  • 金融科技

【免费下载链接】pay

可能是我用过的最优雅的 Alipay/WeChat/Unipay/江苏银行 的支付 SDK 扩展包了

项目地址:https://gitcode.com/yansongda/pay
点击查看免费下载

相关推荐

上一篇:Druid主题系统架构:可扩展样式设计
下一篇:Awesome Home Assistant 仓库的 AI 协作指南:从条目策展规则到自动化维护脚本

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询