Files
Pro/extend/pay/Epay.php
T
li e0de54a9e9 feat: 接入彩虹易支付(USDT TRC20)充值通道
- 新增 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)
2026-04-15 23:06:20 +08:00

209 lines
6.5 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?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];
}
}