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)
This commit is contained in:
li
2026-04-15 23:06:20 +08:00
parent 75e9bafb54
commit e0de54a9e9
5 changed files with 551 additions and 0 deletions
+14
View File
@@ -359,5 +359,19 @@ return [
'11' => '越南代收'
]
],
'epay' => [//彩虹易支付(USDT/支付宝/微信 聚合)
'online_order_fac' => 'epay_online_order',
'notify_url' => 'https://api.g7g7.top/order/epay_recharge_callback',// 异步回调
'callback_url' => 'https://api.g7g7.top/order/epay_return',// 同步跳转回业务
'api_url' => 'https://pay.g7g7.top',
'pid' => '1000',
'key' => 'bf7b83653f5c056a6103e26263a75fa1',
'goods_name' => '账户充值',
'type' => [
'usdt' => 'USDT支付',
// 'alipay' => '支付宝',
// 'wxpay' => '微信支付',
]
],
]
];
@@ -14,6 +14,7 @@ class Order Extends Controller{
use traits\happyPay;
use traits\hengfuPay;
use traits\alinPay;
use traits\epayPay;
public function __construct()
{
@@ -17,6 +17,7 @@ use Waybill\WaybillRoulette;
class Pcapi extends Controller{
use traits\alinPay;
use traits\epayPay;
public function __construct(){
parent::__construct();
// CORS headers
@@ -0,0 +1,327 @@
<?php
namespace app\onlinechip\controller\traits;
use think\Db;
use think\Log;
use think\Request;
/**
* 彩虹易支付 Trait
*
* 业务定位:
* - 作为 AlinPay(越南银行卡)之外的第二个充值通道,走易支付接 USDT/支付宝/微信 等
* - 当前主要用于 USDT TRC20 收款
*
* 对接文件:
* - extend/pay/Epay.php
* - 支付站点:https://pay.g7g7.top
*
* 公开方法:
* - epay_online_order() 发起充值,前端拿到 url 后跳转易支付
* - epay_recharge_callback() 接收易支付异步回调,验签后结算
*
* 通道编号:
* - pay_channel_id = 8AlinPay 是 7,本通道顺延)
* - 名称标识:epay
*/
trait epayPay
{
/**
* 易支付 - 发起充值下单
*/
public function epay_online_order()
{
// 兼容加密请求(Pcapi)和明文请求(Order)
$post = Request::instance()->post();
if (isset($post['encryptData'])) {
$post = decrypt_data($post);
if (!$post) {
return json(['status' => 0, 'message' => '数据解密失败']);
}
}
$user_id = intval($post['user_id'] ?? 0);
$money = intval($post['price'] ?? 0);
$client = intval($post['client'] ?? 0);
$username = trim($post['username'] ?? '');
// 支付子类型:usdt / alipay / wxpay 等,默认 usdt
$pay_type = trim($post['pay_type'] ?? 'usdt');
$user = Db::name('user')->where([
'id' => $user_id,
'status' => 1,
'is_delete' => 0,
'agent' => 0,
])->find();
if (!$user || $user_id <= 0 || $money <= 0 || $client <= 0) {
return json(['status' => 0, 'message' => '参数错误,请稍后再试']);
}
// 地区支线权限(与 AlinPay 一致的风控)
$zdlIdArr = explode(',', $user['agent_parent_id_path']);
$area_list = config('area_list');
if (!isset($area_list[$user['area_id']])) {
return json(['status' => 0, 'message' => '所在地区暂不支持充值']);
}
$isAllow = false;
foreach ($area_list[$user['area_id']]['limit_agent'] as $allowAgentId) {
if (in_array($allowAgentId, $zdlIdArr)) {
$isAllow = true;
}
}
if (!$isAllow) {
return json(['status' => 0, 'message' => '该账号暂不支持在线充值']);
}
// 总代理余额校验
$zdlId = $zdlIdArr[0];
$zdl = Db::name('user')->where([
'id' => $zdlId,
'status' => 1,
'is_delete' => 0,
'agent' => 1,
])->find();
if (!$zdl) {
return json(['status' => 0, 'message' => '所属代理账号异常,暂不能充值']);
}
if ($zdl['money'] < $money) {
return json(['status' => 0, 'message' => '所属代理余额不足,暂不能充值']);
}
$pay_list = config('pay_list');
$pay_orderid = 'P' . date('YmdHis') . rand(100000, 999999); // P 前缀区分 AlinPay 的 E
$notify_url = $pay_list['epay']['notify_url'] ?? '';
$return_url = $pay_list['epay']['callback_url'] ?? '';
$goods_name = $pay_list['epay']['goods_name'] ?? '账户充值';
try {
$epay = new \pay\Epay();
$payUrl = $epay->buildPayUrl(
$pay_orderid,
$pay_type,
sprintf('%.2f', $money),
$goods_name,
$notify_url,
$return_url,
'uid:' . $user_id
);
if (!$payUrl) {
addLogToFile('epay 下单构造 URL 失败:order=' . $pay_orderid, 'epay_error', 'epay');
$data = encrypt_data(['status' => 0, 'message' => '支付地址生成失败,请稍后再试']);
return json(['Success' => 1, 'Data' => $data]);
}
// 写订单
$order_id = Db::name('order_record')->insertGetId([
'pay_channel_id' => 8,
'pay_channel_name' => 'epay',
'user_id' => $user_id,
'order_sn' => $pay_orderid,
'appid' => strval($epay->getConfigInfo()['pid'] ?? ''),
'api_key' => '',
'money' => $money,
'sign' => '',
'create_time' => time(),
'back_order_sn' => '',
'client' => $client,
]);
Db::name('order_log')->insert([
'pay_channel_id' => 8,
'pay_channel_name' => 'epay',
'money' => $money,
'create_time' => time(),
'remake' => 'user_id:' . $user_id . ',用户名:' . $username . ',发起充值(' . $pay_type . ')' . $money . ',时间:' . date('Y-m-d H:i:s'),
'order_id' => $order_id,
'client' => $client,
]);
$data = encrypt_data(['status' => 1, 'message' => '发起支付成功,即将跳转...', 'url' => $payUrl]);
return json(['Success' => 1, 'Data' => $data]);
} catch (\Exception $e) {
addLogToFile('epay 下单异常:' . $e->getMessage() . ' | file:' . $e->getFile() . ':' . $e->getLine(), 'epay_error', 'epay');
$data = encrypt_data(['status' => 0, 'message' => '支付发起失败[' . $e->getMessage() . ']']);
return json(['Success' => 1, 'Data' => $data]);
}
}
/**
* 易支付 - 异步回调处理
*
* 易支付回调是 GETpay.php 跳转后用 GET 带参数回 notify_url),
* 但也可能 POST —— 都兼容
*/
public function epay_recharge_callback()
{
$request = Request::instance();
$data = $request->post();
if (empty($data)) {
$data = $request->get();
}
$json = json_encode($data, JSON_UNESCAPED_UNICODE);
addLogToFile('epay 回调原始数据:' . $json, 'epay_callback', 'epay');
if (!is_array($data) || empty($data['out_trade_no']) || empty($data['sign'])) {
Log::record($data, 'epay_error_callback');
exit('fail');
}
// 记录回调日志
$logId = Db::name('pay_callback_log')->insertGetId([
'pay_channel' => 'EPAY_RECHARGE',
'type' => 'recharge',
'json' => $json,
'create_time' => date('Y-m-d H:i:s'),
]);
try {
$epay = new \pay\Epay();
$result = $epay->verifyNotifyCallback($data, $logId);
if (!$result['verified']) {
exit('fail');
}
if ($result['success']) {
$this->epay_recharge_settle($data);
exit('success');
} else {
// 非 TRADE_SUCCESS —— 返 success 让对方不再重试
addLogToFile('epay 回调非成功状态,ack 防重试:order=' . ($data['out_trade_no'] ?? ''), 'epay_callback', 'epay');
exit('success');
}
} catch (\Exception $e) {
addLogToFile('epay 回调处理异常:' . $e->getMessage(), 'epay_callback_error', 'epay');
exit('fail');
}
}
/**
* 易支付结算(事务保护:加锁 → 扣代理 → 加用户 → 写日志)
* 和 alin_recharge_settle 同构,只是 pay_channel 改成 epay
*/
private function epay_recharge_settle($data)
{
$orderNo = $data['out_trade_no'];
Db::startTrans();
try {
$order = Db::name('order_record')->where('order_sn', $orderNo)->lock(true)->find();
if (!$order || $order['status'] == 3) {
Db::rollback();
return;
}
$user = Db::name('user')->where([
'id' => $order['user_id'],
'status' => 1,
'is_delete' => 0,
'agent' => 0,
])->lock(true)->find();
if (!$user) {
Db::rollback();
return;
}
$zdlIdArr = explode(',', $user['agent_parent_id_path']);
$zdlId = $zdlIdArr[0];
$zdl = Db::name('user')->where([
'id' => $zdlId,
'status' => 1,
'is_delete' => 0,
'agent' => 1,
])->lock(true)->find();
if (!$zdl || $zdl['money'] < $order['money']) {
Db::rollback();
addLogToFile('epay 结算失败:代理余额不足 order=' . $orderNo, 'epay_callback_error', 'epay');
return;
}
// 实际到账金额:易支付回调中的 money 字段(CNY)
$actualMoney = isset($data['money']) ? floatval($data['money']) : $order['money'];
// 1. 更新订单
Db::name('order_record')->where('id', $order['id'])->update([
'back_money' => $actualMoney,
'back_time' => time(),
'status' => 3,
'agent_parent_id' => $user['agent_parent_id'],
'agent_parent_username' => $user['agent_parent_username'],
'zdl_id' => $zdl['id'],
'zdl_username' => $zdl['username'],
'zdl_money_before' => $zdl['money'],
'zdl_money_after' => $zdl['money'] - $order['money'],
'money_before' => $user['money'],
'money_after' => $user['money'] + $order['money'],
]);
// 2. 订单日志
Db::name('order_log')->insert([
'pay_channel_id' => 8,
'pay_channel_name' => 'epay',
'money' => $order['money'],
'create_time' => time(),
'remake' => '易支付回调成功 trade_no=' . ($data['trade_no'] ?? '') . ' type=' . ($data['type'] ?? ''),
'order_id' => $order['id'],
]);
// 3. recharge 流水
Db::name('recharge')->insert([
'type' => 4,
'amount' => $order['money'],
'mode' => 1,
'agent_or_admin' => 3,
'controller_id' => $zdl['id'],
'controller_username' => $zdl['username'],
'controller_nickname' => $zdl['nickname'],
'controller_type' => '易支付第三方充值,扣取总代理余分',
'controller_old_money' => $zdl['money'],
'controller_new_money' => $zdl['money'] - $order['money'],
'user_id' => $order['user_id'],
'user_type' => $user['agent'],
'user_agent_level' => 0,
'username_for' => $user['username'],
'nickname_for' => $user['nickname'],
'user_parent_id' => $user['agent_parent_id'],
'create_time' => time(),
'old_money' => $user['money'],
'new_money' => $user['money'] + $order['money'],
'controller_system' => 3,
'remake' => '易支付第三方充值成功(' . ($data['type'] ?? '') . ')',
]);
// 4. 扣代理
Db::name('user')->where('id', $zdl['id'])->update([
'money' => $zdl['money'] - $order['money'],
'last_recharge_out' => $order['money'],
'last_recharge_out_time' => time(),
'recharge_out_count' => $zdl['recharge_out_count'] + 1,
'recharge_out_total_amount' => $zdl['recharge_out_total_amount'] + $order['money'],
]);
// 5. 加用户
Db::name('user')->where('id', $order['user_id'])->update([
'money' => $user['money'] + $order['money'],
'last_recharge' => $order['money'],
'last_recharge_time' => time(),
'recharge_count' => $user['recharge_count'] + 1,
'recharge_total_amount' => $user['recharge_total_amount'] + $order['money'],
]);
Db::commit();
addLogToFile('epay 结算成功:order=' . $orderNo . ' money=' . $order['money'], 'epay_callback', 'epay');
} catch (\Exception $e) {
Db::rollback();
addLogToFile('epay 结算异常:' . $e->getMessage(), 'epay_callback_error', 'epay');
}
}
}
+208
View File
@@ -0,0 +1,208 @@
<?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];
}
}