Skip to content

支付插件开发

支付插件放在 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

比普通插件多两段:optionscallback

php
<?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 — 配置表单

站长在后台 支付接口 里填的那些字段,由这个文件定义。格式和通用插件完全一样,详见插件开发 · 配置表单

支付插件通常按「基本配置 / 各种支付形态」分成多个页签:

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
<?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_REDIRECT2直接跳转到 setUrl() 的地址
TYPE_LOCAL_RENDER3用插件的 View/<code>.html 本地渲染支付页(比如自己画二维码)
TYPE_SUBMIT4用 POST 表单提交过去

Impl/Signature.php

IS_SIGNtrue 时必须有这个类,实现 verification()

php
<?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 钩子。

核心的回调处理流程

回调进来后,核心按顺序做这些事,全部通过才会给订单发货:

  1. Config/Info.phpcallback 定义 —— 没有就拒(plugin
  2. 如果插件有 Vendor/autoload.php,先加载
  3. IS_SIGN 为真 → 检查凭据是否配置(没配拒,credential)→ 调 Signature::verification()(失败拒,sign
  4. IS_STATUS 为真 → 比对状态字段(不符拒,status
  5. 取出订单号和金额,交给订单服务
  6. 处理完把 FIELD_RESPONSE 的内容原样回给网关

各种失败原因都会带着 $reason 触发 SERVICE_PAY_CALLBACK_FAIL0x3010)钩子,见 Hook 钩子大全。做支付告警插件订阅它就行。

金额与货币

站点用非人民币定价时,提交给网关的金额要按汇率换算。汇率的含义是**「1 个站点货币值多少人民币」**。

前台展示金额时别写死 ¥ 和「元」,用:

php
\App\Util\Currency::symbol()

写死货币符号是支付插件里最常见的显示 bug —— 扣款金额是对的,但显示出来的币种是错的。

私有依赖

插件要用第三方 SDK,放到自己的 Vendor/ 下并提供 autoload.php,核心在处理回调前会自动 require 它。不要去动全局的 composer.json

测试

后台 支付接口 里有回调测试功能(callbackTest),可以在不真实付款的情况下验证回调链路。

基于 MIT 协议开源