Files
Pro/extend/pay/Epay.php
T
OG Backup 171f8fce5c backup: 线上最终版本全量备份
包含所有线上运行的改动:
- 代理编辑功能修复(码率/占成/返水率保存)
- CORS跨域修复(Pcapi构造函数统一Origin)
- 支付通道(ccpay/epay新增)
- 代理报表洗码量显示
- AlinPay金额调整
- 代理/会员编辑页面优化
- 多语言更新
2026-06-14 14:19:49 +08:00

214 lines
6.8 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
*
* @param string $clientIp 下单用户的 IPmapi.php 必填,否则报 "用户IP地址(clientip)不能为空"
* @return array|null
*/
public function createOrderApi($orderNo, $type, $amount, $name, $notifyUrl, $returnUrl = '', $param = '', $clientIp = '')
{
if ($clientIp === '' || $clientIp === null) {
$clientIp = \think\Request::instance()->ip();
}
$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,
'clientip' => strval($clientIp),
'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];
}
}