From e0de54a9e9c39e5b993b05c176128f8a14e55503 Mon Sep 17 00:00:00 2001 From: li Date: Wed, 15 Apr 2026 23:06:20 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=8E=A5=E5=85=A5=E5=BD=A9=E8=99=B9?= =?UTF-8?q?=E6=98=93=E6=94=AF=E4=BB=98(USDT=20TRC20)=E5=85=85=E5=80=BC?= =?UTF-8?q?=E9=80=9A=E9=81=93?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增 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) --- application/config.php | 14 + application/onlinechip/controller/Order.php | 1 + application/onlinechip/controller/Pcapi.php | 1 + .../onlinechip/controller/traits/epayPay.php | 327 ++++++++++++++++++ extend/pay/Epay.php | 208 +++++++++++ 5 files changed, 551 insertions(+) create mode 100644 application/onlinechip/controller/traits/epayPay.php create mode 100644 extend/pay/Epay.php diff --git a/application/config.php b/application/config.php index 6eb0e7b..2438492 100644 --- a/application/config.php +++ b/application/config.php @@ -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' => '微信支付', + ] + ], ] ]; diff --git a/application/onlinechip/controller/Order.php b/application/onlinechip/controller/Order.php index 86b3bcf..92c8f6f 100644 --- a/application/onlinechip/controller/Order.php +++ b/application/onlinechip/controller/Order.php @@ -14,6 +14,7 @@ class Order Extends Controller{ use traits\happyPay; use traits\hengfuPay; use traits\alinPay; + use traits\epayPay; public function __construct() { diff --git a/application/onlinechip/controller/Pcapi.php b/application/onlinechip/controller/Pcapi.php index 8a9391f..37b9beb 100755 --- a/application/onlinechip/controller/Pcapi.php +++ b/application/onlinechip/controller/Pcapi.php @@ -17,6 +17,7 @@ use Waybill\WaybillRoulette; class Pcapi extends Controller{ use traits\alinPay; + use traits\epayPay; public function __construct(){ parent::__construct(); // CORS headers diff --git a/application/onlinechip/controller/traits/epayPay.php b/application/onlinechip/controller/traits/epayPay.php new file mode 100644 index 0000000..2b26970 --- /dev/null +++ b/application/onlinechip/controller/traits/epayPay.php @@ -0,0 +1,327 @@ +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]); + } + } + + /** + * 易支付 - 异步回调处理 + * + * 易支付回调是 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'); + } + } +} diff --git a/extend/pay/Epay.php b/extend/pay/Epay.php new file mode 100644 index 0000000..9de924b --- /dev/null +++ b/extend/pay/Epay.php @@ -0,0 +1,208 @@ +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]; + } +}