支付插件是 DCSHOP 的核心扩展之一,用于对接各种支付渠道。
支付插件需要实现以下核心函数:
| 函数 | 说明 | 必须 |
|---|---|---|
init_插件名() | 初始化支付方式,注册到支付列表 | 是 |
pay_插件名($order, $list) | 发起支付请求 | 是 |
插件名CheckSign($type) | 验证支付回调签名 | 是 |
<?php
/*
Plugin Name: 示例支付
Version: 1.0.0
Plugin URL:
Description: 示例支付接口插件
Author: DCSHOP
Author URL:
Ui: Layui
*/
defined('DC_ROOT') || exit('access denied!');
/**
* 初始化支付方式
* 钩子:mode_payment
*/
function init_my_pay() {
$storage = Storage::getInstance('my_pay');
$enabled = $storage->getValue('enabled');
if ($enabled !== 'y') {
return;
}
// 注册支付方式到全局数组
$GLOBALS['mode_payment'][] = [
'plugin_name' => 'my_pay', // 插件名,与目录名一致
'icon' => './content/plugins/my_pay/icon-btn.png', // 支付按钮图标
'title' => '示例支付', // 显示名称
'unique' => 'my_pay', // 唯一标识,全局不可重复
'name' => '示例支付' // 支付方式名称
];
}
// 注册到支付方式钩子
addAction('mode_payment', 'init_my_pay');
/**
* 发起支付
* 函数命名规则:pay_插件名
*
* @param array $order_info 主订单信息
* @param array $order_list 子订单列表
*/
function pay_my_pay($order_info, $order_list) {
// 获取插件配置
$storage = Storage::getInstance('my_pay');
$merchant_id = $storage->getValue('merchant_id');
$secret_key = $storage->getValue('secret_key');
// 检查订单是否过期
if ($order_info['expire_time'] <= time()) {
emMsg('订单已过期,请重新发起支付', 'javascript:window.close();');
}
// 构建支付参数
$params = [
'merchant_id' => $merchant_id,
'out_trade_no' => $order_info['out_trade_no'],
'amount' => round($order_info['amount'] / 100, 2), // 金额单位:元
'notify_url' => DC_URL . 'action/notify/my_pay', // 异步回调地址
'return_url' => DC_URL . 'action/return/my_pay', // 同步跳转地址
'timestamp' => time(),
];
// 生成签名
ksort($params);
$sign_str = http_build_query($params) . '&key=' . $secret_key;
$params['sign'] = md5($sign_str);
// 方式1:跳转到支付网关
$gateway_url = 'https://pay.example.com/gateway';
header('Location: ' . $gateway_url . '?' . http_build_query($params));
exit;
// 方式2:生成二维码(扫码支付)
// $qr_url = getQrCodeUrl($params);
// header('Location: ?plugin=my_pay&out_trade_no=' . $order_info['out_trade_no'] . '&qr=' . urlencode($qr_url));
// exit;
}
/**
* 验证支付回调签名
* 函数命名规则:插件名CheckSign
*
* @param string $notify_type 回调类型:'return'(同步) 或 'notify'(异步)
* @return array|false 成功返回订单信息数组,失败返回 false
*/
function my_payCheckSign($notify_type) {
$storage = Storage::getInstance('my_pay');
$secret_key = $storage->getValue('secret_key');
// 获取回调参数
if ($notify_type == 'return') {
$params = $_GET;
} else {
$params = $_POST;
// 或者从 php://input 获取
// $params = json_decode(file_get_contents('php://input'), true);
}
// 验证签名
$sign = $params['sign'] ?? '';
unset($params['sign']);
ksort($params);
$sign_str = http_build_query($params) . '&key=' . $secret_key;
$expected_sign = md5($sign_str);
if ($sign !== $expected_sign) {
return false;
}
// 验证支付状态
if (($params['status'] ?? '') !== 'success') {
return false;
}
// 返回订单信息
return [
'timestamp' => time(),
'out_trade_no' => $params['out_trade_no'], // 商户订单号
'up_no' => $params['trade_no'] ?? '', // 上游订单号
];
}
| 类型 | 地址格式 | 说明 |
|---|---|---|
| 异步回调 | DC_URL/action/notify/插件名 |
支付成功后,支付平台服务器调用此地址通知支付结果 |
| 同步跳转 | DC_URL/action/return/插件名 |
用户支付完成后,浏览器跳转到此地址 |
success 或支付平台要求的成功响应,否则支付平台会重复发送回调。
| 字段 | 类型 | 说明 |
|---|---|---|
| id | int | 订单ID |
| out_trade_no | string | 商户订单号 |
| amount | int | 订单金额(单位:分) |
| status | int | 订单状态:0待支付 1已支付待发货 2已完成 |
| expire_time | int | 订单过期时间戳 |
| create_time | int | 订单创建时间戳 |
| user_id | int | 用户ID(游客为0) |
如果是扫码支付,需要创建一个二维码展示页面:
<?php
// 在插件目录下创建 qrcode.php 或在主文件中处理
$out_trade_no = Input::getStrVar('out_trade_no');
$qr_code = Input::getStrVar('qr');
// 引入二维码生成库或使用在线API
$qr_img = 'https://api.qrserver.com/v1/create-qr-code/?size=200x200&data=' . urlencode($qr_code);
?>
<!DOCTYPE html>
<html>
<head>
<title>扫码支付</title>
</head>
<body>
<div style="text-align: center; padding: 50px;">
<h2>请使用手机扫码支付</h2>
<img src="<?= $qr_img ?>" alt="支付二维码">
<p>订单号:<?= htmlspecialchars($out_trade_no) ?></p>
</div>
<script>
// 轮询检查支付状态
setInterval(function() {
fetch('/api.php?action=check_order&out_trade_no=<?= $out_trade_no ?>')
.then(r => r.json())
.then(data => {
if (data.status === 'paid') {
location.href = '/order_result.php?out_trade_no=<?= $out_trade_no ?>';
}
});
}, 3000);
</script>
</body>
</html>