云枢支付聚合支付系统 · 插件开发文档
版本:V1.0.1 | 支付通道以插件形式热插拔:新建通道类型无需修改任何核心代码,上传/放置插件目录即被发现。
一、插件体系概览
| 概念 | 说明 |
| 插件目录 | extend/plugins/{插件名}/ |
| 清单文件 | plugin.json(插件元信息 + 配置字段声明) |
| 驱动类 | Plugin.php,实现 app\common\channel\ChannelInterface |
| 自动加载 | extend/ 目录 PSR-0 自动加载,无需 composer dump |
| 发现机制 | 后台「本地插件」页或任何请求经 PluginManager::scan() 扫描目录,放置即被发现(扫描缓存 5 秒过期) |
| 内置插件 | mock / alipay / wxpay / epay,不可卸载(防止支付功能损坏) |
新增一个通道类型的完整流程:
- 在
extend/plugins/{name}/ 下创建 plugin.json + Plugin.php;
- 后台 → 应用管理 → 本地插件 → 确认插件已发现且状态启用;
- 后台 → 支付接口 → 支付通道 → 新增通道:选择该插件类型,填写配置;
- 完成。下单时
type 与插件 types 匹配即自动路由到该插件。
二、plugin.json 清单
{
"name": "wechat_v3",
"display_name": "微信支付V3插件",
"version": "1.0.0",
"author": "开发者",
"description": "微信支付 APIv3 通道驱动",
"types": ["wxpay"],
"class": "plugins\\wechat_v3\\Plugin",
"builtin": false,
"config_fields": [
{ "key": "mch_id", "label": "商户号", "type": "text", "placeholder": "微信商户号" },
{ "key": "apiv3_key", "label": "APIv3密钥", "type": "password", "placeholder": "32位APIv3密钥" },
{ "key": "serial_no", "label": "证书序列号", "type": "text" }
]
}
| 字段 | 必填 | 说明 |
| name | 是 | 目录名,[a-zA-Z0-9_-]{2,40} |
| types | 是 | 支持的下单类型标识数组(如 ["alipay"]、["wxpay","wxpayn"]),与商户下单 type 参数匹配 |
| class | 是 | 驱动类全限定名(PSR-0:plugins\{name}\Plugin) |
| display_name | 否 | 中文显示名(旧清单兼容 title 字段) |
| version / author / description | 否 | 展示信息 |
| builtin | 否 | true 为内置插件(后台不可卸载) |
| config_fields | 否 | 通道配置表单声明(后台「配置密钥」弹窗自动渲染) |
config_fields 元素
| 键 | 说明 |
| key | 配置键名(后台保存为 cfg_{key}) |
| label | 表单项标签 |
| type | text / password / textarea / number / select / switch |
| placeholder | 输入提示 |
| options | select 类型的选项数组 |
通道配置在后台保存后以 AES 加密 存储,插件收到的 $config 是已解密的数组。
三、ChannelInterface 驱动接口
驱动类必须实现 app\common\channel\ChannelInterface(建议同时参考内置 extend/plugins/mock/Plugin.php 最小实现):
<?php
declare(strict_types=1);
namespace plugins\wechat_v3;
use app\common\channel\ChannelInterface;
use app\common\channel\ChannelPayResult;
class Plugin implements ChannelInterface
{
/** 下单:返回二维码/跳转链接/JSAPI 调起参数 */
public function pay(array $order, array $config): ChannelPayResult
{
$result = new ChannelPayResult();
// ... 调通道下单接口
$result->qrCode = 'weixin://wxpay/bizpayurl?pr=xxx'; // 扫码支付二维码内容
$result->payUrl = 'https://...'; // H5 跳转链接(与 qrCode 二选一)
$result->channelTradeNo = '420000123456'; // 渠道单号
$result->jsapi = ['appId' => '', 'timeStamp' => '', 'package' => '', 'signType' => '', 'paySign' => '']; // 公众号/小程序调起参数
$result->raw = []; // 渠道原始返回(日志排查用)
return $result;
}
/** 校验渠道异步回调;验签失败返回 null,成功返回规范化数据 */
public function verifyNotify(array $payload, array $config, array $headers = []): ?array
{
// ... 验签
return [
'channel_trade_no' => '420000123456',
'money' => '0.01',
'pay_time' => '2026-09-03 17:00:00',
];
}
/** 主动查单:SUCCESS / FAIL / 处理中返回 null */
public function query(array $order, array $config): ?string
{
return null;
}
/** 发起退款:done=true 同步完成;false 处理中(平台会定时调 refundQuery 轮询) */
public function refund(array $order, array $config, array $refund): array
{
return ['ok' => true, 'done' => true, 'channel_refund_no' => 'R123456', 'msg' => '退款成功'];
}
/** 退款结果查询:SUCCESS / FAIL / null(处理中或未知) */
public function refundQuery(array $order, array $config, array $refund): ?string
{
return null;
}
/** 通道自检(后台「测试通道」按钮):返回 [是否通过, 说明] */
public function test(array $config): array
{
return [true, '配置有效,连通性正常'];
}
/** 回调成功应答内容(微信要 XML/JSON,支付宝要 success,按渠道要求返回) */
public function successResponse(): string
{
return 'success';
}
/** 驱动类型标识 */
public static function getType(): string
{
return 'wechat_v3';
}
}
各方法参数说明
| 方法 | 参数 | 说明 |
| pay | $order:trade_no/out_trade_no/type/name/money/notify_url/return_url/param 等;$config:已解密通道配置 | 下单,返回 ChannelPayResult |
| verifyNotify | $payload 回调原始参数、$headers 请求头(验签用) | 回调入口 POST /pay/notify/{type}/{trade_no},平台负责验签后调用 markPaid 入账 |
| query | 同 pay | 平台轮询补单/关单用 |
| refund | $refund:refund_no/out_refund_no/refund_amount/reason | 受理退款 |
| refundQuery | 同上 | 异步退款轮询 |
| test | $config | 后台「测试通道」自检 |
ChannelPayResult 字段
| 字段 | 类型 | 说明 |
| qrCode | string | 二维码内容(收银台渲染扫码支付) |
| payUrl | string | 跳转链接(H5/网页支付) |
| channelTradeNo | string | 渠道单号(回调匹配/查单用) |
| jsapi | array | JSAPI 调起参数(公众号/小程序) |
| raw | array | 渠道原始返回 |
四、付款码收款(正扫)能力:ScanPayInterface
插件额外实现 app\common\channel\ScanPayInterface 即支持商户扫用户付款码收款(收款方式:码支付/当面付):
use app\common\channel\ScanPayInterface;
use app\common\channel\ChannelScanPayResult;
class Plugin implements ChannelInterface, ScanPayInterface
{
/** $authCode:用户付款码(微信 18 位 / 支付宝 16-24 位) */
public function scanPay(array $order, string $authCode, array $config): ChannelScanPayResult
{
$result = new ChannelScanPayResult();
// ... 调通道正扫接口
$result->paid = true; // 扣款成功(平台立即 markPaid 入账)
// $result->pending = true; // 用户输密码中:平台轮询 query 等待结果
$result->channelTradeNo = 'CH123';
$result->msg = '';
return $result;
}
}
五、插件的安装 / 上传 / 启停
| 操作 | 途径 |
| 手动安装 | 将插件目录放入 extend/plugins/,自动发现(5 秒内生效) |
| 上传安装 | 后台 → 应用管理 → 本地插件 → 上传 zip(zip 内含 插件名/plugin.json + Plugin.php) |
| 启用/停用 | 本地插件页开关;停用后该类型下单直接报「支付类型不支持」 |
| 卸载 | 本地插件页删除(内置插件不可卸载;与云端市场同名插件有冲突保护) |
安装校验规则(PluginManager::installZip):
- zip 内必须有
plugin.json 且含 name + class 字段;
- 插件名
[a-zA-Z0-9_-]{2,40};
- 同名本地插件不允许云端包覆盖(防止内置驱动被占位包替换损坏支付功能)。
六、开发规范与踩坑提示
- 命名空间与目录一致:目录
extend/plugins/foo/ → 命名空间 plugins\foo → 清单 "class": "plugins\\foo\\Plugin"(JSON 中双反斜杠转义);
- 回调应答必须正确:
successResponse() 按渠道要求返回(支付宝/易支付 success,微信 V3 是 JSON),返回错误会导致渠道重复回调;
- 幂等:
verifyNotify 中平台已按订单号去重,但插件侧验签失败必须返回 null,不要返回任何「成功」形状的数据;
- 超时与容错:通道请求建议设置超时(平台内置
HttpClient::get/post(..., $timeout, $verify),verify=false 关闭证书校验),失败返回友好错误而非抛异常;
- 金额单位:
$order['money'] 为元(两位小数字符串),调通道接口时注意转换为「分」等渠道单位;
- 配置安全:配置里任何密钥平台都做 AES 加密存储,
$config 拿到的是明文,不要在日志里输出;
- 测试闭环:开发期用
mock 插件(内置)验证下单→收银台→入账→退款全链路;sign:test 命令可测试签名互通;
- 刷新缓存:插件清单扫描缓存 5 秒自动过期,改了 plugin.json 后稍等或重启 PHP 进程即可。