- 新增 extend/pay/Epay.php 易支付协议实现(MD5签名、buildPayUrl、queryOrder、verifyNotifyCallback) - 新增 traits/epayPay.php 提供 epay_online_order 和 epay_recharge_callback,独立结算事务 - config.php 加 pay_list.epay 配置段 (pid=1000, key已配, pay.g7g7.top) - Order.php / Pcapi.php 引入 epayPay trait 支付流程: 用户下单 -> 返回 pay.g7g7.top 跳转 URL -> 用户付款USDT -> 易支付异步回调 -> 验签 -> 结算加余额 通道标识: pay_channel_id=8, pay_channel_name=epay 订单号前缀: P (区别 AlinPay 的 E)
209 lines
6.5 KiB
PHP
209 lines
6.5 KiB
PHP
<?php
|
||
namespace pay;
|
||
|
||
use think\Db;
|
||
|
||
/**
|
||
* 彩虹易支付(epay)对接类
|
||
*
|
||
* 对应线上支付平台:https://pay.g7g7.top/
|
||
* 官方文档:https://pay.cccyun.cc/
|
||
*
|
||
* 支付流程:
|
||
* 1. buildPayUrl() 构造带签名的 URL,前端 302 跳转到该 URL 发起支付
|
||
* 2. 用户在易支付页面完成支付(USDT扫码/银行卡/支付宝 等,取决于通道)
|
||
* 3. 易支付异步 POST notify_url,携带 sign;本端 verifyNotifyCallback() 验签
|
||
*
|
||
* 签名规则(MD5):
|
||
* - 去除 sign、sign_type 字段
|
||
* - 过滤空值
|
||
* - 按 key 字典序排序
|
||
* - 拼接 k=v&k=v(不 urlencode,直接原值)
|
||
* - 末尾拼接 {商户key}(不是 &key=)
|
||
* - MD5(str) 得 32 位小写
|
||
*/
|
||
class Epay
|
||
{
|
||
private $config;
|
||
private $apiUrl;
|
||
|
||
public function __construct()
|
||
{
|
||
$this->getConfig();
|
||
}
|
||
|
||
/**
|
||
* 获取配置
|
||
* 优先读 pay_channel 表(和 AlinPay 一致的存储位置),
|
||
* 回退到 config('pay_list.epay')
|
||
*/
|
||
private function getConfig()
|
||
{
|
||
$dbConfig = Db::name('pay_channel')->where('key', 'EPAY')->value('value');
|
||
if ($dbConfig) {
|
||
$this->config = json_decode($dbConfig, true) ?: [];
|
||
} else {
|
||
$this->config = config('pay_list.epay') ?: [];
|
||
}
|
||
$this->apiUrl = $this->config['api_url'] ?? 'https://pay.g7g7.top';
|
||
}
|
||
|
||
public function getConfigInfo()
|
||
{
|
||
return $this->config;
|
||
}
|
||
|
||
/**
|
||
* 生成签名
|
||
*
|
||
* @param array $params
|
||
* @return string
|
||
*/
|
||
public function generateSign(array $params)
|
||
{
|
||
unset($params['sign'], $params['sign_type']);
|
||
|
||
// 过滤空值(易支付官方要求)
|
||
$params = array_filter($params, function ($val) {
|
||
return $val !== '' && $val !== null;
|
||
});
|
||
|
||
ksort($params);
|
||
|
||
$parts = [];
|
||
foreach ($params as $k => $v) {
|
||
$parts[] = $k . '=' . $v;
|
||
}
|
||
$str = implode('&', $parts) . ($this->config['key'] ?? '');
|
||
|
||
return md5($str);
|
||
}
|
||
|
||
/**
|
||
* 验证回调签名(时间安全比较)
|
||
*
|
||
* @param array $params
|
||
* @return bool
|
||
*/
|
||
public function verifySign(array $params)
|
||
{
|
||
$received = $params['sign'] ?? '';
|
||
$calculated = $this->generateSign($params);
|
||
return hash_equals($calculated, $received);
|
||
}
|
||
|
||
/**
|
||
* 构造发起支付的 URL(跳转式 submit.php)
|
||
* 前端收到该 URL 后直接 window.location 跳转即可
|
||
*
|
||
* @param string $orderNo 商户订单号
|
||
* @param string $type 支付方式:alipay|wxpay|qqpay|bank|jdpay|usdt
|
||
* @param string $amount 金额(元)
|
||
* @param string $name 商品名称
|
||
* @param string $notifyUrl 异步通知地址
|
||
* @param string $returnUrl 同步跳转地址
|
||
* @param string $param 业务扩展参数(可选,原样回传)
|
||
* @return string
|
||
*/
|
||
public function buildPayUrl($orderNo, $type, $amount, $name, $notifyUrl, $returnUrl = '', $param = '')
|
||
{
|
||
$params = [
|
||
'pid' => strval($this->config['pid'] ?? ''),
|
||
'type' => $type,
|
||
'out_trade_no' => strval($orderNo),
|
||
'notify_url' => $notifyUrl,
|
||
'return_url' => $returnUrl,
|
||
'name' => $name,
|
||
'money' => strval($amount),
|
||
'param' => $param,
|
||
'sign_type' => 'MD5',
|
||
];
|
||
|
||
$params['sign'] = $this->generateSign($params);
|
||
|
||
$params = array_filter($params, function ($v) {
|
||
return $v !== '' && $v !== null;
|
||
});
|
||
|
||
return rtrim($this->apiUrl, '/') . '/submit.php?' . http_build_query($params);
|
||
}
|
||
|
||
/**
|
||
* API 模式下单(mapi.php)
|
||
*
|
||
* @return array|null
|
||
*/
|
||
public function createOrderApi($orderNo, $type, $amount, $name, $notifyUrl, $returnUrl = '', $param = '')
|
||
{
|
||
$params = [
|
||
'pid' => strval($this->config['pid'] ?? ''),
|
||
'type' => $type,
|
||
'out_trade_no' => strval($orderNo),
|
||
'notify_url' => $notifyUrl,
|
||
'return_url' => $returnUrl,
|
||
'name' => $name,
|
||
'money' => strval($amount),
|
||
'param' => $param,
|
||
'sign_type' => 'MD5',
|
||
];
|
||
$params['sign'] = $this->generateSign($params);
|
||
|
||
$url = rtrim($this->apiUrl, '/') . '/mapi.php';
|
||
|
||
addLogToFile('epay API 下单请求:' . $url . ' params=' . json_encode($params, JSON_UNESCAPED_UNICODE), 'epay_request', 'epay');
|
||
|
||
$result = httpPost($url, $params);
|
||
|
||
addLogToFile('epay API 下单返回:' . json_encode($result, JSON_UNESCAPED_UNICODE), 'epay_response', 'epay');
|
||
|
||
if (is_array($result)) return $result;
|
||
if (is_string($result)) return json_decode($result, true);
|
||
return null;
|
||
}
|
||
|
||
/**
|
||
* 订单查询
|
||
*/
|
||
public function queryOrder($orderNo)
|
||
{
|
||
$params = [
|
||
'act' => 'order',
|
||
'pid' => strval($this->config['pid'] ?? ''),
|
||
'out_trade_no' => strval($orderNo),
|
||
];
|
||
$params['sign'] = $this->generateSign($params);
|
||
$params['sign_type'] = 'MD5';
|
||
|
||
$url = rtrim($this->apiUrl, '/') . '/api.php?' . http_build_query($params);
|
||
$resp = httpGet($url);
|
||
return is_string($resp) ? json_decode($resp, true) : $resp;
|
||
}
|
||
|
||
/**
|
||
* 处理异步回调 — 仅验签+解析状态,不操作余额
|
||
* 余额结算由 trait 内 epay_recharge_settle() 统一处理
|
||
*
|
||
* @param array $data $_POST 或 $_GET
|
||
* @param int $logId 日志 ID
|
||
* @return array ['verified' => bool, 'success' => bool, 'data' => array]
|
||
*/
|
||
public function verifyNotifyCallback($data, $logId = 0)
|
||
{
|
||
if (!$this->verifySign($data)) {
|
||
addLogToFile('epay 回调验签失败:' . json_encode($data, 320), 'epay_callback_error', 'epay');
|
||
return ['verified' => false, 'success' => false, 'data' => $data];
|
||
}
|
||
|
||
$orderNo = $data['out_trade_no'] ?? '';
|
||
if ($logId && $orderNo) {
|
||
Db::name('pay_callback_log')->where('id', $logId)->update(['unique_no' => $orderNo]);
|
||
}
|
||
|
||
$success = isset($data['trade_status']) && $data['trade_status'] === 'TRADE_SUCCESS';
|
||
|
||
addLogToFile('epay 回调验签通过:order=' . $orderNo . ' status=' . ($data['trade_status'] ?? ''), 'epay_callback', 'epay');
|
||
|
||
return ['verified' => true, 'success' => $success, 'data' => $data];
|
||
}
|
||
}
|