支付插件开发
支付插件放在 app/Pay/<插件名>/ 下。目录名就是 handle,全系统靠它识别这个支付方式。
目录结构
app/Pay/Demo/
Config/
Info.php 插件信息 + 回调规则(必需)
Config.php 默认配置值
Submit.js 后台「支付接口」里的表单(也可用旧格式 Submit.php)
Impl/
Pay.php 下单实现(必需)
Signature.php 回调签名校验(开了验签就必需)
View/
1.html 本地渲染支付页时用的模板
Assets/ 插件自己的静态资源
Vendor/
autoload.php 插件私有的第三方依赖(可选)Config/Info.php
比普通插件多两段:options 和 callback。
<?php
declare(strict_types=1);
return [
'version' => '1.0.0',
'name' => '演示支付',
'author' => '你的名字',
'website' => 'https://example.com',
'description' => '一句话说明',
// 这个插件支持几种支付形态,站长在后台选一种
'options' => [
1 => '扫码支付',
2 => 'PC 支付',
3 => 'WAP 支付',
],
// 回调怎么校验,核心照着这份定义自动处理
'callback' => [
\App\Consts\Pay::IS_SIGN => true, // 是否验签
\App\Consts\Pay::IS_STATUS => true, // 是否校验状态字段
\App\Consts\Pay::FIELD_STATUS_KEY => 'trade_status', // 状态字段名
\App\Consts\Pay::FIELD_STATUS_VALUE => 'TRADE_SUCCESS', // 成功时的值
\App\Consts\Pay::FIELD_ORDER_KEY => 'out_trade_no', // 订单号字段名
\App\Consts\Pay::FIELD_AMOUNT_KEY => 'total_amount', // 金额字段名
\App\Consts\Pay::FIELD_RESPONSE => 'success' // 处理完回给网关的内容
]
];
callback这段少了,回调会直接被拒,理由是「插件缺少 Config/Info.php 的 callback 定义」。
Config/Submit.js — 配置表单
站长在后台 支付接口 里填的那些字段,由这个文件定义。格式和通用插件完全一样,详见插件开发 · 配置表单。
支付插件通常按「基本配置 / 各种支付形态」分成多个页签:
[
{
name: `${util.icon("/app/Pay/Demo/Assets/Icon/Setting.png")} 基本配置`,
form: [
{ title: "商户号", name: "mch_id", type: "input", placeholder: "支付平台分配的商户号", required: true },
{ title: "API 密钥", name: "key", type: "input", placeholder: "支付平台分配的密钥", required: true }
]
},
{
name: "扫码支付",
form: [
{ title: "收款方", name: "payee", type: "input", placeholder: "收银台中显示的收款方" }
]
}
]分组的
name支持util.icon(),可以传 Font Awesome 类名,也可以传插件自己Assets/下的图片路径。
旧格式 Config/Submit.php(扁平字段数组)仍然可用,两个文件同时存在时 Submit.js 优先。
Impl/Pay.php
实现 App\Pay\Pay 接口,只有一个方法 trade():
<?php
declare(strict_types=1);
namespace App\Pay\Demo\Impl;
use App\Entity\PayEntity;
use App\Pay\Base;
use Kernel\Exception\JSONException;
class Pay extends Base implements \App\Pay\Pay
{
public function trade(): PayEntity
{
// $this->code 就是站长在后台选的那个 options 键
if ($this->code == 1) {
return $this->qrcode();
}
throw new JSONException("非法请求");
}
private function qrcode(): PayEntity
{
// 用 $this->config 里的凭据去请求支付平台,拿到支付链接
$payUrl = '...';
$entity = new PayEntity();
$entity->setType(\App\Pay\Pay::TYPE_REDIRECT);
$entity->setUrl($payUrl);
return $entity;
}
}基类给你的东西
继承 App\Pay\Base 后可以直接用:
| 属性 | 说明 |
|---|---|
$this->amount | 订单金额(float) |
$this->tradeNo | 订单号 |
$this->config | 站长在后台填的配置 |
$this->callbackUrl | 异步回调地址,交给支付平台 |
$this->returnUrl | 用户付完跳回来的地址 |
$this->clientIp | 买家 IP |
$this->code | 站长选的支付形态(对应 options 的键) |
$this->handle | 插件目录名 |
三种支付呈现方式
PayEntity::setType() 决定前台怎么把用户送到支付页:
| 常量 | 值 | 行为 |
|---|---|---|
TYPE_REDIRECT | 2 | 直接跳转到 setUrl() 的地址 |
TYPE_LOCAL_RENDER | 3 | 用插件的 View/<code>.html 本地渲染支付页(比如自己画二维码) |
TYPE_SUBMIT | 4 | 用 POST 表单提交过去 |
Impl/Signature.php
IS_SIGN 为 true 时必须有这个类,实现 verification():
<?php
declare(strict_types=1);
namespace App\Pay\Demo\Impl;
class Signature
{
public function verification(array $map, array $config): bool
{
$sign = $map['sign'] ?? '';
unset($map['sign']);
ksort($map);
$expect = md5(urldecode(http_build_query($map) . '&key=' . $config['app_key']));
return hash_equals($expect, (string)$sign);
}
}返回 false 核心就会拒掉这次回调,并触发 SERVICE_PAY_CALLBACK_FAIL 钩子。
核心的回调处理流程
回调进来后,核心按顺序做这些事,全部通过才会给订单发货:
- 读
Config/Info.php的callback定义 —— 没有就拒(plugin) - 如果插件有
Vendor/autoload.php,先加载 IS_SIGN为真 → 检查凭据是否配置(没配拒,credential)→ 调Signature::verification()(失败拒,sign)IS_STATUS为真 → 比对状态字段(不符拒,status)- 取出订单号和金额,交给订单服务
- 处理完把
FIELD_RESPONSE的内容原样回给网关
各种失败原因都会带着 $reason 触发 SERVICE_PAY_CALLBACK_FAIL(0x3010)钩子,见 Hook 钩子大全。做支付告警插件订阅它就行。
金额与货币
站点用非人民币定价时,提交给网关的金额要按汇率换算。汇率的含义是**「1 个站点货币值多少人民币」**。
前台展示金额时别写死 ¥ 和「元」,用:
\App\Util\Currency::symbol()写死货币符号是支付插件里最常见的显示 bug —— 扣款金额是对的,但显示出来的币种是错的。
私有依赖
插件要用第三方 SDK,放到自己的 Vendor/ 下并提供 autoload.php,核心在处理回调前会自动 require 它。不要去动全局的 composer.json。
测试
后台 支付接口 里有回调测试功能(callbackTest),可以在不真实付款的情况下验证回调链路。
