支付插件开发文档

思博 2026-03-21 15 阅读 44 分钟

本文档指导开发者如何为本系统开发支付插件。


目录结构

所有支付插件必须存放在 public/plugins/pay/ 目录下,每个插件一个独立文件夹:

public/plugins/pay/
├── epay/                       # 插件目录名(小写英文)
│   ├── description.php         # 插件基本信息(必需)
│   ├── set.php                 # 后台配置定义(必需)
│   ├── go.php                  # 发起支付(必需)
│   ├── notify.php              # 异步通知处理(必需)
│   └── return.php              # 同步回调处理(可选)
├── alipay/
│   └── ...
└── wxpay/
    └── ...

核心流程

用户下单 → 系统调用 go.php → 返回支付参数 → 用户完成支付
                                    ↓
支付平台 → 异步通知 notify.php → 验签成功 → 系统更新订单
    ↓
用户返回 → 同步回调 return.php → 显示支付结果

文件详解

1. description.php - 插件信息

定义插件在后台显示的基本信息。

<?php 
return [
    "DisplayName" => "易支付",           // 后台显示名称
    "APIVersion"  => "1.0.0",            // 版本号
    "HelpDoc"     => "https://..."       // 帮助文档链接(可选)
];

返回字段说明:

字段类型必填说明
DisplayNamestring插件显示名称
APIVersionstring版本号
HelpDocstring帮助文档URL

2. set.php - 后台配置

定义后台配置页面需要填写的字段。

<?php
return [
    [
        "name"    => "商户ID",           // 配置项标识(英文)
        "title"   => "商户号",            // 显示标题
        "type"    => "input",            // 类型:input/select/textarea/switch
        "prompt"  => "请输入商户号",       // 提示文字
        "value"   => ""                   // 默认值
    ],
    [
        "name"    => "密钥",
        "title"   => "API密钥",
        "type"    => "input",
        "prompt"  => "请输入API密钥",
        "value"   => ""
    ],
    [
        "name"    => "支付方式",
        "title"   => "支付方式",
        "type"    => "select",
        "prompt"  => "选择支付方式",
        "value"   => "alipay",
        "option"  => [                   // select类型专用
            "支付宝" => "alipay",
            "微信"   => "wxpay",
            "QQ"     => "qqpay"
        ]
    ],
    [
        "name"    => "调试模式",
        "title"   => "调试模式",
        "type"    => "switch",
        "prompt"  => "是否开启调试",
        "value"   => "0"                  // 0=关,1=开
    ]
];

字段类型说明:

类型说明
input单行文本输入
password密码输入(内容隐藏)
textarea多行文本输入
select下拉选择,需配合 option
switch开关,value为 "0" 或 "1"
number数字输入

配置数据使用方式:

系统在调用 go.phpnotify.phpreturn.php 时,会将用户在后台填写的配置值通过 $params['data'] 传入。


3. go.php - 发起支付

函数名: go($params)

作用: 接收订单信息,向支付平台发起请求,返回支付参数。

入参 $params

字段类型说明
order_nostring系统订单号
subjectstring订单标题
amountfloat订单金额(元)
notify_urlstring异步通知URL(系统生成)
return_urlstring同步回调URL(系统生成)
client_ipstring用户IP地址
dataarray/string插件配置数据

返回值:

// 页面跳转支付
return [
    'code' => 1,              // 1=成功,0=失败
    'type' => 'jump',         // 固定值:jump
    'data' => 'https://...'   // 支付平台跳转URL
];


// API二维码支付
return [
    'code' => 1,
    'type' => 'qrcode',       // 固定值:qrcode
    'data' => 'https://...'   // 二维码图片URL或支付链接
];


// 失败
return [
    'code' => 0,
    'msg'  => '错误信息'
];

示例代码:

<?php
function go($params)
{
    // 解析配置
    $cfg = is_array($params['data']) 
        ? $params['data'] 
        : json_decode($params['data'], true);
    
    // 检查配置
    if (empty($cfg['商户ID']) || empty($cfg['密钥'])) {
        return ['code' => 0, 'msg' => '请先配置支付参数'];
    }
    
    // 组装请求参数
    $order = [
        'mch_id'       => $cfg['商户ID'],
        'out_trade_no' => $params['order_no'],
        'total_fee'    => intval($params['amount'] * 100),  // 转为分
        'body'         => $params['subject'],
        'notify_url'   => $params['notify_url'],
        'return_url'   => $params['return_url'],
        'nonce_str'    => md5(uniqid()),
    ];
    
    // 生成签名(按支付平台规则)
    $order['sign'] = sign($order, $cfg['密钥']);
    
    // 请求支付平台
    $response = httpPost('https://pay.xxx.com/api', $order);
    $result = json_decode($response, true);
    
    if ($result['code'] == 'SUCCESS') {
        return [
            'code' => 1,
            'type' => 'qrcode',
            'data' => $result['code_url']   // 二维码链接
        ];
    }
    
    return ['code' => 0, 'msg' => $result['msg']];
}


// 签名函数示例
function sign($data, $key)
{
    ksort($data);
    $str = '';
    foreach ($data as $k => $v) {
        if ($v !== '' && $k !== 'sign') {
            $str .= $k . '=' . $v . '&';
        }
    }
    $str .= 'key=' . $key;
    return strtoupper(md5($str));
}

4. notify.php - 异步通知

函数名: notify($params)

作用: 接收支付平台的支付结果通知,验证签名,确认支付成功。

入参 $params

字段类型说明
order_nostring系统订单号
subjectstring订单标题
amountfloat订单金额
allarray支付平台POST的所有原始数据
dataarray/string插件配置数据

返回值:

// 验签成功,系统会更新订单为已支付
return ['code' => 1, 'msg' => 'success'];


// 验签失败
return ['code' => 0, 'msg' => '签名错误'];

⚠️ 重要:

  • 必须返回 ['code' => 1, 'msg' => 'success'] 才算成功
  • 不要输出任何其他内容(如echo、var_dump)
  • 系统只负责验签,订单状态更新由系统统一处理

示例代码:

<?php
function notify($params)
{
    $cfg = is_array($params['data']) 
        ? $params['data'] 
        : json_decode($params['data'], true);
    
    $post = $params['all'];  // 原始POST数据
    
    // 1. 验证签名
    $sign = $post['sign'] ?? '';
    unset($post['sign']);
    
    $mySign = sign($post, $cfg['密钥']);
    if (strtoupper($sign) !== strtoupper($mySign)) {
        return ['code' => 0, 'msg' => '签名错误'];
    }
    
    // 2. 验证订单号
    if ($post['out_trade_no'] !== $params['order_no']) {
        return ['code' => 0, 'msg' => '订单号不匹配'];
    }
    
    // 3. 验证金额(使用bccomp精确比较)
    $notifyAmount = $post['total_fee'] / 100;  // 分转元
    if (bccomp($notifyAmount, $params['amount'], 2) !== 0) {
        return ['code' => 0, 'msg' => '金额不一致'];
    }
    
    // 4. 验证支付状态
    if ($post['status'] !== 'SUCCESS') {
        return ['code' => 0, 'msg' => '未支付'];
    }
    
    // 5. 返回成功
    return ['code' => 1, 'msg' => 'success'];
}

5. return.php - 同步回调(可选)

函数名: returns($params) (注意是 returns 不是 return)

作用: 用户支付完成后,从支付平台跳转回系统的处理。

入参:notify.php

返回值:notify.php

与 notify.php 的区别:

notify.phpreturn.php
触发服务器主动通知用户浏览器跳转
可靠性高(会重试)低(可能关闭页面)
更新订单否(仅展示结果)

示例代码:

<?php
function returns($params)
{
    // 逻辑同notify,但只用于页面展示
    // 不实际修改订单状态
    
    $cfg = is_array($params['data']) 
        ? $params['data'] 
        : json_decode($params['data'], true);
    
    $get = $params['all'];  // URL参数
    
    // 验证签名、订单号、金额、状态...
    // 返回结果用于页面显示支付成功/失败
    
    return ['code' => 1, 'msg' => 'success'];
}

系统调用流程

发起支付时

// 系统内部逻辑示意
$params = [
    'order_no'   => '20240201123456',
    'subject'    => '商品购买',
    'amount'     => 99.99,
    'notify_url' => 'https://网站/notify/epay',
    'return_url' => 'https://网站/return/epay',
    'client_ip'  => '192.168.1.1',
    'data'       => ['商户ID' => 'xxx', '密钥' => 'yyy', ...]  // 后台配置
];


$result = go($params);


if ($result['code'] == 1) {
    if ($result['type'] == 'jump') {
        header('Location: ' . $result['data']);
    } else {
        // 显示二维码
        showQrcode($result['data']);
    }
} else {
    showError($result['msg']);
}

异步通知时

// 系统接收到支付平台通知后
$params = [
    'order_no' => '20240201123456',
    'subject'  => '商品购买',
    'amount'   => 99.99,
    'all'      => $_POST,  // 原始POST数据
    'data'     => ['商户ID' => 'xxx', '密钥' => 'yyy', ...]
];


$result = notify($params);


if ($result['code'] == 1) {
    // 系统更新订单状态为已支付
    updateOrderStatus($params['order_no'], 'paid');
    echo 'success';  // 响应支付平台
} else {
    echo $result['msg'];  // 响应错误
}

开发规范

1. 目录命名

  • 使用小写英文字母+数字
  • 简短有意义,如 epayalipaywxpaypaypal

2. 配置安全

  • 敏感信息(密钥等)在后台配置,不要硬编码
  • 通过 $params['data'] 获取配置

3. 金额处理

  • 使用 bccomp() 进行金额比较,避免浮点误差
  • 注意单位转换(元/分)

4. 签名验证

  • 严格按照支付平台文档实现签名算法
  • 空值字段通常不参与签名
  • 签名比较忽略大小写

5. 错误处理

  • 所有错误返回 ['code' => 0, 'msg' => '错误描述']
  • 不要直接输出错误,通过返回值传递

6. 网络请求

  • 设置合理的超时时间(建议10-30秒)
  • 处理网络异常情况

快速开始:创建新插件

以创建 "示例支付" 插件为例:

步骤1:创建目录

mkdir public/plugins/pay/demo

步骤2:创建 description.php

<?php
return [
    "DisplayName" => "示例支付",
    "APIVersion"  => "1.0.0",
    "HelpDoc"     => ""
];

步骤3:创建 set.php

<?php
return [
    [
        "name"    => "mch_id",
        "title"   => "商户号",
        "type"    => "input",
        "prompt"  => "请输入商户号",
        "value"   => ""
    ],
    [
        "name"    => "key",
        "title"   => "API密钥",
        "type"    => "input",
        "prompt"  => "请输入API密钥",
        "value"   => ""
    ]
];

步骤4:创建 go.php

<?php
function go($params)
{
    $cfg = is_array($params['data']) 
        ? $params['data'] 
        : json_decode($params['data'], true);
    
    if (empty($cfg['mch_id']) || empty($cfg['key'])) {
        return ['code' => 0, 'msg' => '配置不完整'];
    }
    
    // TODO: 根据支付平台API实现
    
    return ['code' => 0, 'msg' => '待实现'];
}

步骤5:创建 notify.php

<?php
function notify($params)
{
    $cfg = is_array($params['data']) 
        ? $params['data'] 
        : json_decode($params['data'], true);
    
    $post = $params['all'];
    
    // TODO: 验证签名、订单号、金额、状态
    
    return ['code' => 1, 'msg' => 'success'];
}

步骤6:后台启用

进入系统后台 → 支付设置 → 启用 "示例支付" 插件 → 填写配置


调试技巧

1. 记录日志

在插件中写入日志便于调试:

function logDebug($msg, $data = null)
{
    file_put_contents(
        __DIR__ . '/debug.log',
        date('Y-m-d H:i:s') . ' ' . $msg . ' ' . json_encode($data) . "\n",
        FILE_APPEND
    );
}


// 使用
logDebug('收到通知', $params);

2. 签名调试

function debugSign($data, $key)
{
    ksort($data);
    $str = http_build_query($data) . '&key=' . $key;
    
    logDebug('签名字符串', $str);
    logDebug('签名结果', md5($str));
    
    return md5($str);
}

附录:常用工具函数

<?php
/**
 * HTTP POST请求
 */
function httpPost($url, $data, $timeout = 30)
{
    $ch = curl_init();
    curl_setopt_array($ch, [
        CURLOPT_URL            => $url,
        CURLOPT_POST           => true,
        CURLOPT_POSTFIELDS     => http_build_query($data),
        CURLOPT_RETURNTRANSFER => true,
        CURLOPT_TIMEOUT        => $timeout,
        CURLOPT_SSL_VERIFYPEER => false,
        CURLOPT_HTTPHEADER     => ['Content-Type: application/x-www-form-urlencoded'],
    ]);
    $result = curl_exec($ch);
    curl_close($ch);
    return $result;
}


/**
 * 生成签名(字典序+MD5)
 */
function makeSign($data, $key, $exclude = ['sign', 'sign_type'])
{
    ksort($data);
    $pairs = [];
    foreach ($data as $k => $v) {
        if ($v !== '' && !in_array($k, $exclude)) {
            $pairs[] = $k . '=' . $v;
        }
    }
    return md5(implode('&', $pairs) . $key);
}


/**
 * 获取客户端IP
 */
function getClientIp()
{
    $keys = ['HTTP_X_FORWARDED_FOR', 'HTTP_X_REAL_IP', 'REMOTE_ADDR'];
    foreach ($keys as $k) {
        if (!empty($_SERVER[$k])) {
            $ips = explode(',', $_SERVER[$k]);
            $ip = trim($ips[0]);
            if (filter_var($ip, FILTER_VALIDATE_IP)) {
                return $ip;
            }
        }
    }
    return '127.0.0.1';
}

这篇文章对您有帮助吗?

阅读设置