From 79123e079feb5ee701f8fd1682c3bd72b124148b Mon Sep 17 00:00:00 2001 From: li Date: Thu, 12 Mar 2026 22:24:49 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E5=AF=B9=E6=8E=A5=E8=B6=8A=E5=8D=97Ali?= =?UTF-8?q?nPay=E6=94=AF=E4=BB=98=E7=BD=91=E5=85=B3=EF=BC=88=E4=BB=A3?= =?UTF-8?q?=E6=94=B6+=E4=BB=A3=E4=BB=98=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增 AlinPay 网关核心类:签名/验签/代收/代付/查询/账户接口 - 新增 alinPay trait:充值发起 + 代收回调结算(事务+行锁) - 修改 admin/agent Report:提现审批自动路由 ALIN_VN 发起代付 - 新增 Pcapi:银行卡提现申请 + 越南银行列表接口 - 修改 Crypto:代付回调/订单查询/账户查询入口 - 新增越南区 area_list(id=3) + alinPay pay_list 配置 - DDL: user_withdraw 表增加 pay_channel/ext 字段 --- alter_withdraw_alin.sql | 15 + application/admin/controller/Report.php | 61 ++- application/agent/controller/Report.php | 35 +- application/config.php | 16 + application/onlinechip/controller/Crypto.php | 85 ++++ application/onlinechip/controller/Order.php | 1 + application/onlinechip/controller/Pcapi.php | 115 ++++++ .../onlinechip/controller/traits/alinPay.php | 294 +++++++++++++ extend/pay/AlinPay.php | 388 ++++++++++++++++++ 9 files changed, 997 insertions(+), 13 deletions(-) create mode 100644 alter_withdraw_alin.sql create mode 100644 application/onlinechip/controller/traits/alinPay.php create mode 100644 extend/pay/AlinPay.php diff --git a/alter_withdraw_alin.sql b/alter_withdraw_alin.sql new file mode 100644 index 0000000..e0c0363 --- /dev/null +++ b/alter_withdraw_alin.sql @@ -0,0 +1,15 @@ +-- AlinPay 越南代付对接 - 数据库变更 +-- 执行时间: 2026-03-12 + +-- 1. user_withdraw 表增加支付渠道和扩展字段 +ALTER TABLE `cg_user_withdraw` + ADD COLUMN `pay_channel` varchar(50) NOT NULL DEFAULT '' COMMENT '支付渠道: USDT, YBF, ALIN_VN' AFTER `type`, + ADD COLUMN `ext` text NULL COMMENT '扩展字段JSON: 银行卡信息等' AFTER `pay_channel`; + +-- 2. pay_channel 表插入 AlinPay 配置(secret 需从三方后台获取后替换) +INSERT INTO `cg_pay_channel` (`name`, `key`, `value`, `status`) VALUES ( + '越南AlinPay', + 'ALIN_VN', + '{"app_id":"276a857ff36814028b772724","secret":"请从三方后台获取并替换","collection_product_id":11,"payment_product_id":14}', + 1 +); diff --git a/application/admin/controller/Report.php b/application/admin/controller/Report.php index 254e55a..137a9b0 100644 --- a/application/admin/controller/Report.php +++ b/application/admin/controller/Report.php @@ -3,6 +3,7 @@ namespace app\admin\controller; use pay\Usdt; +use pay\AlinPay; use think\Db; use think\Request; use think\Session; @@ -405,19 +406,61 @@ class Report extends Common{ die(json_encode(['code'=>0,'msg'=>'非待审核状态无法操作!'])); } - // 审核通过 - $Usdt = new Usdt(); - $res = $Usdt->applyUserWithdraw($withdraw['order_no'],$withdraw['to_address'],$withdraw['amount']); - if($res['code'] == 0){ - Db::name('user_withdraw')->where('id',$id)->update([ + // 根据支付渠道路由 + $payChannel = $withdraw['pay_channel'] ?: 'USDT'; + $withdrawResult = []; + + if($payChannel == 'ALIN_VN'){ + // 越南AlinPay代付 + $ext = json_decode($withdraw['ext'], true) ?: []; + $pay_list = config('pay_list'); + $notifyUrl = $pay_list['alinPay']['payment_notify_url']; + + $alinPay = new AlinPay(); + $res = $alinPay->createPayment( + $withdraw['order_no'], + sprintf('%.2f', $withdraw['amount']), + $ext['accountName'] ?? '', + $ext['accountNumber'] ?? '', + $ext['bankCode'] ?? '', + $ext['bankName'] ?? '', + $notifyUrl + ); + + $withdrawResult = [ + 'status' => (is_array($res) && isset($res['code']) && $res['code'] == 200) ? true : false, + 'type' => 'ALIN_VN', + 'out_trade_no' => $res['data']['trade_no'] ?? '', + 'err_msg' => $res['message'] ?? '请求失败', + ]; + }else{ + // 默认USDT + $Usdt = new Usdt(); + $res = $Usdt->applyUserWithdraw($withdraw['order_no'],$withdraw['to_address'],$withdraw['amount']); + $withdrawResult = [ + 'status' => $res['code'] == 0 ? true : false, + 'type' => $res['processor']['asset'] ?? 'USDT', + 'from_address' => $res['processor']['from_addr'] ?? '', + 'err_msg' => $res['debug'] ?? '', + ]; + } + + if($withdrawResult['status']){ + $updateData = [ 'status' => 'AGREE', - 'type' => $res['processor']['asset'], - 'from_address' => $res['processor']['from_addr'], + 'type' => $withdrawResult['type'] ?? '', 'audit_time' => time(), 'operator_source' => 2, 'operator_id' => $user_info['id'], 'operator_username' => $user_info['admin'], - ]); + ]; + if(!empty($withdrawResult['from_address'])){ + $updateData['from_address'] = $withdrawResult['from_address']; + } + if(!empty($withdrawResult['out_trade_no'])){ + $updateData['out_trade_no'] = $withdrawResult['out_trade_no']; + } + Db::name('user_withdraw')->where('id',$id)->update($updateData); die(json_encode(['code'=>1,'msg'=>'审核通过!'])); }else{ Db::startTrans(); @@ -430,7 +473,7 @@ class Report extends Common{ // 更新记录 Db::name('user_withdraw')->where('id',$id)->update([ 'status' => 'FAIL', - 'err_msg' => $res['debug'], + 'err_msg' => $withdrawResult['err_msg'], 'audit_time' => time(), 'operator_source' => 2, 'operator_id' => $user_info['id'], diff --git a/application/agent/controller/Report.php b/application/agent/controller/Report.php index 412eb5c..af89f64 100755 --- a/application/agent/controller/Report.php +++ b/application/agent/controller/Report.php @@ -2,6 +2,7 @@ namespace app\agent\controller; use pay\Usdt; use pay\Ybf; +use pay\AlinPay; use \think\Controller; use think\Lang; use \think\Session; @@ -2644,8 +2645,8 @@ class Report Extends Common{ die(json_encode(['code'=>0,'msg'=>'非待审核状态无法操作!'])); } - // 审核通过 - $withdrawChannel = "YBF"; + // 审核通过 - 根据提现记录的支付渠道路由 + $withdrawChannel = $withdraw['pay_channel'] ?: 'YBF'; $withdrawResult = []; if($withdrawChannel == "USDT"){ $Usdt = new Usdt(); @@ -2665,13 +2666,35 @@ class Report Extends Common{ 'type' => 'YBF', 'err_msg' => $res['message'], ]; + }else if($withdrawChannel == "ALIN_VN"){ + // 越南AlinPay代付 + $ext = json_decode($withdraw['ext'], true) ?: []; + $pay_list = config('pay_list'); + $notifyUrl = $pay_list['alinPay']['payment_notify_url']; + + $alinPay = new AlinPay(); + $res = $alinPay->createPayment( + $withdraw['order_no'], + sprintf('%.2f', $withdraw['amount']), + $ext['accountName'] ?? '', + $ext['accountNumber'] ?? '', + $ext['bankCode'] ?? '', + $ext['bankName'] ?? '', + $notifyUrl + ); + $withdrawResult = [ + 'status' => (is_array($res) && isset($res['code']) && $res['code'] == 200) ? true : false, + 'type' => 'ALIN_VN', + 'out_trade_no' => $res['data']['trade_no'] ?? '', + 'err_msg' => $res['message'] ?? '请求失败', + ]; } if(empty($withdrawResult)){ die(json_encode(['code'=>0,'msg'=>'未配置支付渠道!'])); } if($withdrawResult['status']){ - Db::name('user_withdraw')->where('id',$id)->update([ + $updateData = [ 'status' => 'AGREE', 'type' => $withdrawResult['type'] ?? '', 'from_address' => $withdrawResult['from_address'] ?? '', @@ -2680,7 +2703,11 @@ class Report Extends Common{ 'operator_id' => $user_info['id'], 'operator_username' => $user_info['username'], 'operator_nickname' => $user_info['nickname'] - ]); + ]; + if(!empty($withdrawResult['out_trade_no'])){ + $updateData['out_trade_no'] = $withdrawResult['out_trade_no']; + } + Db::name('user_withdraw')->where('id',$id)->update($updateData); die(json_encode(['code'=>1,'msg'=>'审核通过!'])); }else{ Db::startTrans(); diff --git a/application/config.php b/application/config.php index eb75dbb..5598563 100644 --- a/application/config.php +++ b/application/config.php @@ -281,6 +281,13 @@ return [ 'default_wt_agent' => 566, 'limit_agent' => [87, 1371], ], + '3' => [ + 'name' => '越南', + 'pay' => 'alinPay', + 'default_agent' => 0, // TODO: 魔尊需填入越南区总代理ID + 'default_wt_agent' => 0, // TODO: 魔尊需填入越南区委托代理ID + 'limit_agent' => [], // TODO: 魔尊需填入允许充值的代理ID列表 + ], ], 'limit_list' => [ @@ -343,5 +350,14 @@ return [ '1' => '在線繳費' ] ], + 'alinPay' => [//越南AlinPay + 'online_order_fac' => 'alin_online_order',//越南 + 'notify_url' => 'https://api.g7g7.top/order/alin_recharge_callback',//代收回调地址 + 'payment_notify_url' => 'https://api.g7g7.top/crypto/alin_payment_callback',//代付回调地址 + 'callback_url' => '', + 'type' => [ + '11' => '越南代收' + ] + ], ] ]; diff --git a/application/onlinechip/controller/Crypto.php b/application/onlinechip/controller/Crypto.php index b0c3f57..253d764 100644 --- a/application/onlinechip/controller/Crypto.php +++ b/application/onlinechip/controller/Crypto.php @@ -2,6 +2,7 @@ namespace app\onlinechip\controller; use pay\Ybf; +use pay\AlinPay; use think\Cache; use \think\Controller; use think\Db; @@ -230,6 +231,90 @@ class Crypto Extends Controller{ return $sign == $data['sign']; } + /** + * AlinPay 越南代付回调 + */ + public function alin_payment_callback() + { + $post = Request::instance()->post(); + $json = json_encode($post, JSON_UNESCAPED_UNICODE); + + addLogToFile('越南代付回调原始数据:' . $json, 'alin_callback', 'alin'); + + if (!is_array($post) || empty($post['out_trade_no']) || empty($post['sign'])) { + exit('fail'); + } + + // 记录回调日志 + $logId = Db::name('pay_callback_log')->insertGetId([ + 'pay_channel' => 'ALIN_PAYMENT', + 'type' => 'payment', + 'json' => $json, + 'create_time' => date('Y-m-d H:i:s'), + ]); + + try { + $alinPay = new AlinPay(); + $res = $alinPay->paymentCallback($post, $logId); + exit($res ?: 'fail'); + } catch (\Exception $e) { + addLogToFile('越南代付回调异常:' . $e->getMessage(), 'alin_callback_error', 'alin'); + exit('fail'); + } + } + + /** + * AlinPay 越南代收查询 + */ + public function alin_query_order() + { + $orderNo = Request::instance()->post('order_no'); + if (!$orderNo) { + return json(['code' => 0, 'msg' => '缺少订单号']); + } + + try { + $alinPay = new AlinPay(); + $result = $alinPay->queryOrder($orderNo); + return json($result); + } catch (\Exception $e) { + return json(['code' => 0, 'msg' => $e->getMessage()]); + } + } + + /** + * AlinPay 越南代付查询 + */ + public function alin_query_payment() + { + $orderNo = Request::instance()->post('order_no'); + if (!$orderNo) { + return json(['code' => 0, 'msg' => '缺少订单号']); + } + + try { + $alinPay = new AlinPay(); + $result = $alinPay->queryPayment($orderNo); + return json($result); + } catch (\Exception $e) { + return json(['code' => 0, 'msg' => $e->getMessage()]); + } + } + + /** + * AlinPay 账户余额查询 + */ + public function alin_query_account() + { + try { + $alinPay = new AlinPay(); + $result = $alinPay->queryAccount(); + return json($result); + } catch (\Exception $e) { + return json(['code' => 0, 'msg' => $e->getMessage()]); + } + } + /** * 记录日志 * @param $type diff --git a/application/onlinechip/controller/Order.php b/application/onlinechip/controller/Order.php index c34b781..86b3bcf 100644 --- a/application/onlinechip/controller/Order.php +++ b/application/onlinechip/controller/Order.php @@ -13,6 +13,7 @@ class Order Extends Controller{ use traits\tingPay; use traits\happyPay; use traits\hengfuPay; + use traits\alinPay; public function __construct() { diff --git a/application/onlinechip/controller/Pcapi.php b/application/onlinechip/controller/Pcapi.php index 00e630c..47745c7 100755 --- a/application/onlinechip/controller/Pcapi.php +++ b/application/onlinechip/controller/Pcapi.php @@ -3966,6 +3966,121 @@ class Pcapi extends Controller{ } + // 申请银行卡提现(越南AlinPay) + public function apply_withdraw_bank(){ + header('Access-Control-Allow-Origin: *'); + header("Access-Control-Allow-Headers: token, Origin, X-Requested-With, Content-Type, Accept, Authorization"); + header('Access-Control-Allow-Methods: POST,GET,PUT,DELETE'); + if(Request::instance()->isPost()){ + $json_arr = decrypt_data(Request::instance()->post()); + if($json_arr){ + $userId = intval($json_arr['user_id']); + $amount = floatval($json_arr['amount']); + $accountName = trim($json_arr['account_name'] ?? ''); + $accountNumber = trim($json_arr['account_number'] ?? ''); + $bankCode = trim($json_arr['bank_code'] ?? ''); + $bankName = trim($json_arr['bank_name'] ?? ''); + + if(!$amount || $amount <= 0){ + return json(array('Success' => 0, 'Msg' => 'Số tiền rút không hợp lệ')); + } + if(empty($accountName)){ + return json(array('Success' => 0, 'Msg' => 'Tên tài khoản không được để trống')); + } + if(empty($accountNumber)){ + return json(array('Success' => 0, 'Msg' => 'Số tài khoản không được để trống')); + } + if(empty($bankName)){ + return json(array('Success' => 0, 'Msg' => 'Tên ngân hàng không được để trống')); + } + + $user = Db::name('user')->where(array('id' => $userId))->find(); + if($user){ + Db::startTrans(); + + $user = Db::name('user')->where(array('id' => $userId))->lock(true)->find(); + $system = Db::name('system')->find(); + $money = $user['money']; + $withdrawMoney = empty($system['withdraw_money']) ? 100 : $system['withdraw_money']; + $withdrawFee = empty($system['withdraw_fee']) ? 1 : $system['withdraw_fee']; + + // 计算积分兑换 + $amountMoney = $withdrawMoney/100*($amount); + $feeMoney = $withdrawMoney/100*($withdrawFee); + $needMoney = $amountMoney + $feeMoney; + if($needMoney > $money){ + Db::rollback(); + return json(array('Success' => 0, 'Msg' => 'Số dư không đủ')); + } + $afterMoney = $money - $needMoney; + + // 银行卡信息存入ext + $ext = json_encode([ + 'accountName' => $accountName, + 'accountNumber' => $accountNumber, + 'bankCode' => $bankCode, + 'bankName' => $bankName, + ], JSON_UNESCAPED_UNICODE); + + // 写入提现申请 + $res = Db::name('user_withdraw')->insert([ + 'order_no' => createOrderNo('TX'), + 'user_id' => $userId, + 'amount' => $amount, + 'money' => $amountMoney, + 'old_money' => $money, + 'new_money' => $afterMoney, + 'service_fee' => $withdrawFee, + 'service_fee_money' => $feeMoney, + 'withdraw_rate' => $withdrawFee, + 'to_address' => $accountNumber, + 'pay_channel' => 'ALIN_VN', + 'ext' => $ext, + 'create_time' => time() + ]); + if(!$res){ + Db::rollback(); + return json(array('Success' => 0, 'Msg' => 'Thao tác thất bại')); + } + + // 扣除用户余额 + $res = Db::name('user')->where('id',$userId)->update([ + 'money' => $afterMoney, + 'update_time' => time() + ]); + if(!$res){ + Db::rollback(); + return json(array('Success' => 0, 'Msg' => 'Thao tác thất bại')); + } + + Db::commit(); + return json(array('Success' => 1, 'Msg' => 'Thành công, vui lòng chờ xử lý')); + }else{ + return json(array('Success' => 0, 'Msg' => 'Lấy thông tin thất bại')); + } + } + } + } + + // 获取越南银行列表 + public function get_bank_list_vn(){ + header('Access-Control-Allow-Origin: *'); + header("Access-Control-Allow-Headers: token, Origin, X-Requested-With, Content-Type, Accept, Authorization"); + header('Access-Control-Allow-Methods: POST,GET,PUT,DELETE'); + + $banks = \pay\AlinPay::getBankCodes(); + $list = []; + foreach($banks as $index => $name){ + if(!empty($name)){ + $list[] = [ + 'code' => $name, + 'name' => $name, + ]; + } + } + return json(array('Success' => 1, 'Data' => $list)); + } + // 取消提现 public function cancel_withdraw(){ header('Access-Control-Allow-Origin: *'); diff --git a/application/onlinechip/controller/traits/alinPay.php b/application/onlinechip/controller/traits/alinPay.php new file mode 100644 index 0000000..0a2b49d --- /dev/null +++ b/application/onlinechip/controller/traits/alinPay.php @@ -0,0 +1,294 @@ +post('user_id')); + $money = intval(Request::instance()->post('price')); + $client = intval(Request::instance()->post('client')); + $username = trim(Request::instance()->post('username')); + + $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' => '参数错误,请稍后再试']); + } + + // 判断地区支线权限 + $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 = 'E' . date("YmdHis") . rand(100000, 999999); + $notify_url = $pay_list['alinPay']['notify_url']; + + try { + $alinPay = new \pay\AlinPay(); + $result = $alinPay->createOrder($pay_orderid, sprintf('%.2f', $money), $notify_url, 'uid:' . $user_id); + + if (is_array($result) && isset($result['code']) && $result['code'] == 200) { + $returnUrl = $result['data']['url'] ?? ''; + + if (!$returnUrl) { + return json(['status' => 0, 'message' => '支付地址获取失败']); + } + + // 记录订单 + $order_record = [ + 'pay_channel_id' => 7, + 'pay_channel_name' => 'alinPay', + 'user_id' => $user_id, + 'order_sn' => $pay_orderid, + 'appid' => $alinPay->getConfigInfo()['app_id'], + 'api_key' => '', + 'money' => $money, + 'sign' => '', + 'create_time' => time(), + 'back_order_sn' => '', + 'client' => $client, + ]; + $order_id = Db::name('order_record')->insertGetId($order_record); + + // 记录日志 + Db::name('order_log')->insert([ + 'pay_channel_id' => 7, + 'pay_channel_name' => 'alinPay', + 'money' => $money, + 'create_time' => time(), + 'remake' => 'user_id:' . $user_id . ',用户名:' . $username . ',发起充值:' . $money . ',充值时间:' . date('Y-m-d H:i:s'), + 'order_id' => $order_id, + 'client' => $client, + ]); + + return json(['status' => 1, 'message' => '发起支付成功,即将跳转...', 'url' => $returnUrl]); + } else { + $errMsg = $result['message'] ?? '未知错误'; + return json(['status' => 0, 'message' => '支付发起失败:' . $errMsg]); + } + } catch (\Exception $e) { + addLogToFile('代收下单异常:' . $e->getMessage(), 'alin_error', 'alin'); + return json(['status' => 0, 'message' => '支付发起失败,请稍后再试']); + } + } + + /** + * 越南代收 - 回调处理 + */ + public function alin_recharge_callback() + { + $post = Request::instance()->post(); + $json = json_encode($post, JSON_UNESCAPED_UNICODE); + + addLogToFile('代收回调原始数据:' . $json, 'alin_callback', 'alin'); + + if (!is_array($post) || empty($post['out_trade_no']) || empty($post['sign'])) { + Log::record($post, 'alin_error_callback'); + exit('fail'); + } + + // 记录回调日志 + $logId = Db::name('pay_callback_log')->insertGetId([ + 'pay_channel' => 'ALIN_RECHARGE', + 'type' => 'recharge', + 'json' => $json, + 'create_time' => date('Y-m-d H:i:s'), + ]); + + try { + $alinPay = new \pay\AlinPay(); + $result = $alinPay->verifyRechargeCallback($post, $logId); + + if (!$result['verified']) { + exit('fail'); + } + + $tradeStatus = $result['trade_status']; + + // trade_status: 1=成功, -1=处理中, 2=失败 + if ($tradeStatus === 1) { + $this->alin_recharge_settle($post); + exit('success'); + } elseif ($tradeStatus === 2) { + // 失败,终态,确认收到 + addLogToFile('代收回调失败:order=' . $post['out_trade_no'], 'alin_callback', 'alin'); + exit('success'); + } else { + // 处理中(-1),不返回success让网关重试 + exit('fail'); + } + } catch (\Exception $e) { + addLogToFile('代收回调处理异常:' . $e->getMessage(), 'alin_callback_error', 'alin'); + exit('fail'); + } + } + + /** + * 代收成功后结算(事务保护:扣代理、写recharge表、加用户余额) + */ + private function alin_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('代收结算失败:代理余额不足 order=' . $orderNo, 'alin_callback_error', 'alin'); + return; + } + + // 1. 更新订单状态为已完成 + Db::name('order_record')->where('id', $order['id'])->update([ + 'back_money' => $data['real_amount'] ?: $data['amount'], + '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' => 7, + 'pay_channel_name' => 'alinPay', + 'money' => $order['money'], + 'create_time' => time(), + 'remake' => '回调支付成功', + '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' => '越南AlinPay第三方充值成功', + ]); + + // 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('代收结算成功:order=' . $orderNo . ' money=' . $order['money'], 'alin_callback', 'alin'); + } catch (\Exception $e) { + Db::rollback(); + addLogToFile('代收结算异常:' . $e->getMessage(), 'alin_callback_error', 'alin'); + } + } +} diff --git a/extend/pay/AlinPay.php b/extend/pay/AlinPay.php new file mode 100644 index 0000000..b8a657a --- /dev/null +++ b/extend/pay/AlinPay.php @@ -0,0 +1,388 @@ +getConfig(); + } + + /** + * 获取配置信息 + */ + private function getConfig() + { + $config = Db::name('pay_channel')->where('key', 'ALIN_VN')->value('value'); + $config = json_decode($config, true); + $this->config = $config; + + // 域名从配置读取,回退默认值 + $this->domain = $config['domain'] ?? 'https://alinintermon-api.fantianchuangshi.com'; + } + + /** + * 生成签名 + * 规则:参数按key排序 → key=value&连接(空值不参与) → 拼接key={secret} → MD5 32位小写 + * ext字段不参与签名 + * + * @param array $params + * @return string + */ + public function generateSign(array $params) + { + // 移除不参与签名的字段 + unset($params['sign']); + unset($params['ext']); + + // 过滤空值 + $params = array_filter($params, function ($val) { + return $val !== '' && $val !== null; + }); + + // 按key排序 + ksort($params); + + // 拼接 + $parts = []; + foreach ($params as $key => $val) { + $parts[] = $key . '=' . $val; + } + $str = implode('&', $parts); + $str .= '&key=' . $this->config['secret']; + + return md5($str); + } + + /** + * 验证回调签名(时间安全比较,防timing attack) + * + * @param array $params + * @return bool + */ + public function verifySign(array $params) + { + $receivedSign = $params['sign'] ?? ''; + $calculatedSign = $this->generateSign($params); + return hash_equals($calculatedSign, $receivedSign); + } + + /** + * 发送HTTP请求 + * + * @param string $url + * @param array $data + * @return array|null + */ + private function request($url, $data) + { + $jsonData = json_encode($data, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES); + + addLogToFile('请求地址:' . $url, 'alin_request', 'alin'); + addLogToFile('请求报文:' . $jsonData, 'alin_request', 'alin'); + + $header = ['Content-Type: application/json']; + $result = httpPost($url, $jsonData, $header); + + addLogToFile('返回结果:' . json_encode($result, JSON_UNESCAPED_UNICODE), 'alin_response', 'alin'); + + return $result; + } + + /** + * 代收下单(用户充值) + * + * @param string $orderNo 商户订单号 + * @param string $amount 金额(元) + * @param string $notifyUrl 回调地址 + * @param string $desc 订单描述 + * @return array|null + */ + public function createOrder($orderNo, $amount, $notifyUrl, $desc = '') + { + $params = [ + 'app_id' => $this->config['app_id'], + 'product_id' => $this->config['collection_product_id'], // 代收 product_id=11 + 'out_trade_no' => $orderNo, + 'notify_url' => $notifyUrl, + 'amount' => $amount, + 'time' => time(), + 'desc' => $desc, + ]; + + $params['sign'] = $this->generateSign($params); + + $url = $this->domain . '/api/order'; + return $this->request($url, $params); + } + + /** + * 代收查询 + * + * @param string $orderNo 商户订单号 + * @return array|null + */ + public function queryOrder($orderNo) + { + $params = [ + 'app_id' => $this->config['app_id'], + 'out_trade_no' => $orderNo, + 'time' => time(), + ]; + + $params['sign'] = $this->generateSign($params); + + $url = $this->domain . '/api/order/status'; + return $this->request($url, $params); + } + + /** + * 代付下单(用户提现) + * + * @param string $orderNo 商户订单号 + * @param string $amount 金额(元) + * @param string $accountName 账户名称 + * @param string $accountNumber 卡号 + * @param string $bankCode 银行编码 + * @param string $bankName 银行名称 + * @param string $notifyUrl 回调地址 + * @param string $desc 订单描述 + * @return array|null + */ + public function createPayment($orderNo, $amount, $accountName, $accountNumber, $bankCode, $bankName, $notifyUrl, $desc = '') + { + $params = [ + 'app_id' => $this->config['app_id'], + 'product_id' => $this->config['payment_product_id'], // 代付 product_id=14 + 'out_trade_no' => $orderNo, + 'notify_url' => $notifyUrl, + 'amount' => $amount, + 'time' => time(), + 'desc' => $desc, + ]; + + $params['sign'] = $this->generateSign($params); + + // ext不参与签名,单独附加 + $params['ext'] = [ + 'accountName' => $accountName, + 'accountNumber' => $accountNumber, + 'bankCode' => $bankCode, + 'bankName' => $bankName, + ]; + + $url = $this->domain . '/api/payment'; + return $this->request($url, $params); + } + + /** + * 代付查询 + * + * @param string $orderNo 商户订单号 + * @return array|null + */ + public function queryPayment($orderNo) + { + $params = [ + 'app_id' => $this->config['app_id'], + 'out_trade_no' => $orderNo, + 'time' => time(), + ]; + + $params['sign'] = $this->generateSign($params); + + $url = $this->domain . '/api/payment/status'; + return $this->request($url, $params); + } + + /** + * 账户查询 + * + * @return array|null + */ + public function queryAccount() + { + $params = [ + 'app_id' => $this->config['app_id'], + 'time' => time(), + ]; + + $params['sign'] = $this->generateSign($params); + + $url = $this->domain . '/api/account'; + return $this->request($url, $params); + } + + /** + * 处理代收回调 — 仅验签+解析状态,不操作余额 + * 余额结算由 trait alin_recharge_settle() 统一处理 + * + * @param array $data 回调数据 + * @param int $logId 日志ID + * @return array ['verified' => bool, 'trade_status' => int, 'data' => array] + */ + public function verifyRechargeCallback($data, $logId = 0) + { + // 验签 + if (!$this->verifySign($data)) { + addLogToFile('代收回调签名验证失败:' . json_encode($data, 320), 'alin_callback_error', 'alin'); + return ['verified' => false, 'trade_status' => 0, 'data' => $data]; + } + + $orderNo = $data['out_trade_no'] ?? ''; + if ($logId && $orderNo) { + Db::name('pay_callback_log')->where('id', $logId)->update(['unique_no' => $orderNo]); + } + + $tradeStatus = intval($data['trade_status']); + addLogToFile('代收回调验签通过:order=' . $orderNo . ' status=' . $tradeStatus, 'alin_callback', 'alin'); + + return ['verified' => true, 'trade_status' => $tradeStatus, 'data' => $data]; + } + + /** + * 处理代付回调(提现回调) + * + * @param array $data 回调数据 + * @param int $logId 日志ID + * @return string|void + */ + public function paymentCallback($data, $logId = 0) + { + // 验签 + if (!$this->verifySign($data)) { + addLogToFile('代付回调签名验证失败:' . json_encode($data, 320), 'alin_callback_error', 'alin'); + return; + } + + $orderNo = $data['out_trade_no'] ?? ''; + if (!$orderNo) return; + + if ($logId) { + Db::name('pay_callback_log')->where('id', $logId)->update(['unique_no' => $orderNo]); + } + + $tradeStatus = intval($data['trade_status']); + + $order = Db::name('user_withdraw')->where('order_no', $orderNo)->find(); + if (!$order) { + addLogToFile('代付回调订单不存在:' . $orderNo, 'alin_callback', 'alin'); + return 'success'; + } + + // 只处理 AGREE 状态的订单 + if ($order['status'] !== 'AGREE') { + addLogToFile('代付回调订单状态非AGREE:' . $orderNo . ' status=' . $order['status'], 'alin_callback', 'alin'); + return 'success'; + } + + $payTime = strtotime($data['complete_time'] ?? '') ?: time(); + + if ($tradeStatus === 1) { + // 成功 + $updateData = [ + 'status' => 'SUCCESS', + 'pay_time' => $payTime, + 'out_trade_no' => $data['trade_no'] ?? '', + ]; + Db::name('user_withdraw')->where('id', $order['id'])->update($updateData); + addLogToFile('代付回调成功:' . $orderNo, 'alin_callback', 'alin'); + return 'success'; + } elseif ($tradeStatus === 2) { + // 失败 — 退回用户余额 + Db::startTrans(); + try { + $user = Db::name('user')->where('id', $order['user_id'])->lock(true)->find(); + $newMoney = $user['money'] + $order['money'] + $order['service_fee_money']; + + Db::name('user')->where('id', $order['user_id'])->update([ + 'money' => $newMoney, + ]); + + Db::name('user_withdraw')->where('id', $order['id'])->update([ + 'status' => 'FAIL', + 'pay_time' => $payTime, + ]); + + Db::commit(); + addLogToFile('代付回调失败已退款:' . $orderNo . ' refund=' . ($order['money'] + $order['service_fee_money']), 'alin_callback', 'alin'); + return 'success'; + } catch (\Exception $e) { + Db::rollback(); + addLogToFile('代付回调退款异常:' . $e->getMessage(), 'alin_callback_error', 'alin'); + return; + } + } + + // 处理中(-1),不返回success让网关重试 + addLogToFile('代付回调处理中:' . $orderNo . ' status=' . $tradeStatus, 'alin_callback', 'alin'); + return; + } + + /** + * 获取银行编码列表 + * + * @return array + */ + public static function getBankCodes() + { + return self::$bankCodes; + } + + /** + * 获取配置信息(外部使用) + * + * @return array + */ + public function getConfigInfo() + { + return $this->config; + } +}