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]); } $isAllow = false; foreach ($area_list[$user['area_id']]['limit_agent'] 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]); } if ($zdl['money'] < $money) { $maxAllowed = intval($zdl['money']); $data = encrypt_data([ 'status' => 0, 'code' => 'AGENT_LIMIT', 'message' => 'Hạn mức nạp mỗi lần hiện tại là ' . number_format($maxAllowed, 0, '.', ',') . ' VND. Vui lòng nhập số tiền nhỏ hơn hoặc bằng mức trên.', 'max_amount' => $maxAllowed, ]); 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 || $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'); } } }