会员通过第三方支付通道线上充值时,不再检查/扣减总代理余额。 真金白银从支付通道进来,直接加分给会员。 改动覆盖全部9个支付通道trait: - 删除下单时的代理余额检查(不再拦截"余额不足") - 回调时只检查代理是否存在,不检查余额 - 不扣减代理money字段 - 充值记录标记为"系统直充"而非"扣取总代理余分" 代理手动给会员上分(信用盘模式)不受影响,仍然扣代理自己的分。
512 lines
21 KiB
PHP
512 lines
21 KiB
PHP
<?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 = 8(AlinPay 是 7,本通道顺延)
|
||
* - 名称标识:epay
|
||
*/
|
||
trait epayPay
|
||
{
|
||
/**
|
||
* 易支付 - 发起充值下单(内嵌二维码模式)
|
||
*
|
||
* 直接跨库写 epay.pre_order + 计算 usdtpro 指纹金额,
|
||
* 前端内嵌渲染二维码,无需跳转 pay.g7g7.top。
|
||
*
|
||
* 返回的 Data 结构:
|
||
* {
|
||
* status: 1|0
|
||
* message: 文案
|
||
* url: 兼容字段(老前端仍可 window.open 跳转 /pay/leader/trade_no/)
|
||
* qrcode: { 新字段,前端内嵌渲染
|
||
* order_sn: OG 侧订单号(用于轮询)
|
||
* usdt_address: TRC20 收款地址
|
||
* usdt_amount: 含指纹尾数的精确金额(如 10.0003)
|
||
* vnd_amount: VND 原始金额
|
||
* rate: 汇率
|
||
* expires_at: 订单超时绝对时间戳
|
||
* timeout_sec: 总超时秒数
|
||
* }
|
||
* }
|
||
*/
|
||
public function epay_online_order()
|
||
{
|
||
// 兼容加密请求(Pcapi)和明文请求(Order)
|
||
$post = Request::instance()->post();
|
||
if (isset($post['encryptData'])) {
|
||
$post = decrypt_data($post);
|
||
if (!$post) {
|
||
$data = encrypt_data(['status' => 0, 'message' => 'Dữ liệu không hợp lệ']);
|
||
return json(['Success' => 1, 'Data' => $data]);
|
||
}
|
||
}
|
||
|
||
$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) {
|
||
$data = encrypt_data(['status' => 0, 'message' => 'Tham số không hợp lệ, vui lòng thử lại']);
|
||
return json(['Success' => 1, 'Data' => $data]);
|
||
}
|
||
|
||
// 地区支线权限(与 AlinPay 一致的风控)
|
||
$zdlIdArr = explode(',', $user['agent_parent_id_path']);
|
||
$area_list = config('area_list');
|
||
|
||
if (!isset($area_list[$user['area_id']])) {
|
||
$data = encrypt_data(['status' => 0, 'message' => 'Khu vực hiện chưa hỗ trợ nạp tiền']);
|
||
return json(['Success' => 1, 'Data' => $data]);
|
||
}
|
||
|
||
$limitAgents = $area_list[$user['area_id']]['limit_agent'];
|
||
$isAllow = empty($limitAgents);
|
||
foreach ($limitAgents as $allowAgentId) {
|
||
if (in_array($allowAgentId, $zdlIdArr)) {
|
||
$isAllow = true;
|
||
}
|
||
}
|
||
if (!$isAllow) {
|
||
$data = encrypt_data(['status' => 0, 'message' => 'Tài khoản này chưa được phép nạp online']);
|
||
return json(['Success' => 1, 'Data' => $data]);
|
||
}
|
||
|
||
// 总代理余额校验
|
||
$zdlId = $zdlIdArr[0];
|
||
$zdl = Db::name('user')->where([
|
||
'id' => $zdlId,
|
||
'status' => 1,
|
||
'is_delete' => 0,
|
||
'agent' => 1,
|
||
])->find();
|
||
|
||
if (!$zdl) {
|
||
$data = encrypt_data(['status' => 0, 'message' => 'Tài khoản đại lý có vấn đề, không thể nạp']);
|
||
return json(['Success' => 1, 'Data' => $data]);
|
||
}
|
||
$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'] ?? '账户充值';
|
||
$epay_api = $pay_list['epay']['api_url'] ?? 'https://pay.g7g7.top';
|
||
$merchant_id = intval($pay_list['epay']['pid'] ?? 0);
|
||
|
||
try {
|
||
$epay = new \pay\Epay();
|
||
|
||
// 写 OG 侧订单
|
||
$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,
|
||
]);
|
||
|
||
// 调用 epay 的 mapi.php,让 epay 自己走完整个流程:
|
||
// 1) INSERT pre_order
|
||
// 2) Channel::submit 算通道
|
||
// 3) Plugin::loadForSubmit → usdtpro_plugin::submit() 算 usdtpro 指纹金额并写入 pre_order
|
||
// 4) 返回 {type:'jump', url:'/pay/leader/TRADE_NO/'}
|
||
// 注意:usdtpro 指纹金额必须由 epay 侧算,OG 侧算会与 monitor 对账逻辑产生精度漂移,导致充值不到账
|
||
$apiResult = $epay->createOrderApi(
|
||
$pay_orderid,
|
||
$pay_type,
|
||
sprintf('%.2f', $money),
|
||
$goods_name,
|
||
$notify_url,
|
||
$return_url,
|
||
'uid:' . $user_id
|
||
);
|
||
|
||
// mapi.php 返回格式:
|
||
// {code:0, trade_no, pay_type:'jump', pay_info:'/pay/submit/TRADE_NO/'} 或
|
||
// {code:0, trade_no, payurl:'/pay/submit/TRADE_NO/'} (default 分支)
|
||
// {code:1, trade_no, payurl:'...'} (部分旧版)
|
||
$submitUrl = '';
|
||
$trade_no = '';
|
||
if (is_array($apiResult)) {
|
||
if (!empty($apiResult['trade_no'])) $trade_no = strval($apiResult['trade_no']);
|
||
if (!empty($apiResult['payurl'])) $submitUrl = $apiResult['payurl'];
|
||
elseif (!empty($apiResult['pay_info'])) $submitUrl = $apiResult['pay_info'];
|
||
elseif (!empty($apiResult['url'])) $submitUrl = $apiResult['url'];
|
||
}
|
||
|
||
if (!$submitUrl || !$trade_no) {
|
||
$errMsg = is_array($apiResult) ? (isset($apiResult['msg']) ? $apiResult['msg'] : json_encode($apiResult, JSON_UNESCAPED_UNICODE)) : 'mapi 无响应';
|
||
addLogToFile('epay mapi 下单失败:order=' . $pay_orderid . ' err=' . $errMsg, 'epay_error', 'epay');
|
||
$data = encrypt_data(['status' => 0, 'message' => 'Khởi tạo thanh toán thất bại: ' . $errMsg]);
|
||
return json(['Success' => 1, 'Data' => $data]);
|
||
}
|
||
|
||
// 触发 plugin::submit() 把 usdtpro 指纹金额写入 pre_order
|
||
// mapi 模式 Plugin::loadForSubmit 只调 mapi() 方法,usdtpro 插件没这个方法,
|
||
// 所以 usdtpro 不会被写入。必须再访问一次 /pay/submit/TRADE_NO/ 让插件 submit() 跑一次
|
||
$this->epay_trigger_plugin_submit($submitUrl);
|
||
|
||
// 跳转到用户看的页面(pay/leader/TRADE_NO/)
|
||
$payUrl = str_replace('/pay/submit/', '/pay/leader/', $submitUrl);
|
||
|
||
// 默认返回结构(若后续读取 usdtpro 失败仍可回退跳转)
|
||
$qrcode = null;
|
||
|
||
if ($pay_type === 'usdt' && $trade_no) {
|
||
// 从 epay.pre_order 读 epay 已经算好的 usdtpro 指纹金额 + 通道地址
|
||
$preOrder = Db::query(
|
||
"SELECT o.`trade_no`, o.`usdtpro`, o.`realmoney`, o.`addtime`, c.`config`
|
||
FROM `epay`.`pre_order` o
|
||
LEFT JOIN `epay`.`pre_channel` c ON c.`id` = o.`channel`
|
||
WHERE o.`trade_no` = :tn LIMIT 1",
|
||
['tn' => $trade_no]
|
||
);
|
||
|
||
if (!empty($preOrder) && !empty($preOrder[0]['usdtpro'])) {
|
||
$row = $preOrder[0];
|
||
$cfg = json_decode($row['config'] ?? '{}', true) ?: [];
|
||
$timeout = intval($cfg['timeout'] ?? 600);
|
||
$expiresAt = strtotime($row['addtime']) + $timeout;
|
||
|
||
$qrcode = [
|
||
'order_sn' => $pay_orderid,
|
||
'trade_no' => $row['trade_no'],
|
||
'usdt_address' => $cfg['address'] ?? '',
|
||
'usdt_amount' => rtrim(rtrim(sprintf('%.5f', floatval($row['usdtpro'])), '0'), '.'),
|
||
'vnd_amount' => $row['realmoney'],
|
||
'rate' => strval($cfg['rate'] ?? ''),
|
||
'expires_at' => $expiresAt,
|
||
'timeout_sec' => $timeout,
|
||
];
|
||
addLogToFile('epay 下单读取 usdtpro 成功: order=' . $pay_orderid . ' trade_no=' . $trade_no . ' usdt=' . $qrcode['usdt_amount'], 'epay_request', 'epay');
|
||
} else {
|
||
addLogToFile('epay 下单 pre_order 未取到 usdtpro: order=' . $pay_orderid . ' trade_no=' . $trade_no, 'epay_error', 'epay');
|
||
}
|
||
}
|
||
|
||
$data = encrypt_data([
|
||
'status' => 1,
|
||
'message' => 'Khởi tạo thanh toán thành công',
|
||
'url' => $payUrl, // 兼容老前端
|
||
'qrcode' => $qrcode, // 新前端内嵌渲染用(usdtpro 由 epay 权威给出)
|
||
]);
|
||
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' => 'Khởi tạo thanh toán thất bại: ' . $e->getMessage()]);
|
||
return json(['Success' => 1, 'Data' => $data]);
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 触发 epay plugin::submit() 写入 usdtpro 指纹金额
|
||
* 只请求不处理响应(epay 会返回 302 或 HTML,我们只要副作用)
|
||
*/
|
||
private function epay_trigger_plugin_submit($submitUrl)
|
||
{
|
||
try {
|
||
$ch = curl_init($submitUrl);
|
||
curl_setopt_array($ch, [
|
||
CURLOPT_RETURNTRANSFER => true,
|
||
CURLOPT_FOLLOWLOCATION => false,
|
||
CURLOPT_CONNECTTIMEOUT => 3,
|
||
CURLOPT_TIMEOUT => 6,
|
||
CURLOPT_SSL_VERIFYPEER => false,
|
||
CURLOPT_SSL_VERIFYHOST => false,
|
||
CURLOPT_USERAGENT => 'OG-EpayInternal/1.0',
|
||
]);
|
||
curl_exec($ch);
|
||
curl_close($ch);
|
||
} catch (\Exception $e) {
|
||
addLogToFile('epay_trigger_plugin_submit 异常:' . $e->getMessage(), 'epay_error', 'epay');
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 易支付 - 订单状态轮询
|
||
*
|
||
* 前端轮询该接口,判断是否到账或已超时。
|
||
* 返回 Data 结构:
|
||
* { status: 0|1|2, message, money, paid_time }
|
||
* 0=待支付, 1=已到账, 2=已超时
|
||
*/
|
||
public function epay_order_status()
|
||
{
|
||
$post = Request::instance()->post();
|
||
if (isset($post['encryptData'])) {
|
||
$post = decrypt_data($post);
|
||
if (!$post) {
|
||
return json(['status' => 0, 'message' => '数据解密失败']);
|
||
}
|
||
}
|
||
|
||
$order_sn = trim($post['order_sn'] ?? '');
|
||
$user_id = intval($post['user_id'] ?? 0);
|
||
|
||
if ($order_sn === '' || $user_id <= 0) {
|
||
$data = encrypt_data(['status' => 0, 'message' => '参数错误']);
|
||
return json(['Success' => 1, 'Data' => $data]);
|
||
}
|
||
|
||
$order = Db::name('order_record')
|
||
->where([
|
||
'order_sn' => $order_sn,
|
||
'user_id' => $user_id,
|
||
'pay_channel_id' => 8,
|
||
])
|
||
->find();
|
||
|
||
if (!$order) {
|
||
$data = encrypt_data(['status' => 0, 'message' => '订单不存在']);
|
||
return json(['Success' => 1, 'Data' => $data]);
|
||
}
|
||
|
||
// status=3 代表已回调结算完成(见 epay_recharge_settle)
|
||
if (intval($order['status']) == 3) {
|
||
$data = encrypt_data([
|
||
'status' => 1,
|
||
'message' => '充值成功',
|
||
'money' => $order['money'],
|
||
'paid_time' => $order['back_time'],
|
||
]);
|
||
return json(['Success' => 1, 'Data' => $data]);
|
||
}
|
||
|
||
// 超时判定:从 epay 配置拿 timeout,兜底 600
|
||
$pay_list = config('pay_list');
|
||
$timeout = intval($pay_list['epay']['timeout'] ?? 600);
|
||
if (time() - intval($order['create_time']) > $timeout) {
|
||
$data = encrypt_data([
|
||
'status' => 2,
|
||
'message' => '订单超时',
|
||
'money' => $order['money'],
|
||
]);
|
||
return json(['Success' => 1, 'Data' => $data]);
|
||
}
|
||
|
||
$data = encrypt_data([
|
||
'status' => 0,
|
||
'message' => '等待支付',
|
||
'money' => $order['money'],
|
||
]);
|
||
return json(['Success' => 1, 'Data' => $data]);
|
||
}
|
||
|
||
/**
|
||
* 易支付 - 异步回调处理
|
||
*
|
||
* 易支付回调是 GET(pay.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) {
|
||
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'],
|
||
'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'],
|
||
'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([
|
||
'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');
|
||
}
|
||
}
|
||
}
|