$item){ $temp = [ 'name'=>__('pay_'.$item), 'type'=>$item, 'index'=>$index ]; if($item == 'balance'){ $temp['subname'] = "可用余额 ¥{$this->auth->money}"; } if($item == 'wechat'){ $platform = \app\common\library\Platform::getPlatform(); $wechatAvailable = false; // 检查支付账户池是否有可用商户号(仅验证商户号有效性,平台可用性统一由系统配置决定) if (\app\admin\library\project\App::isInstall('paymentpool') && \app\common\library\app\paymentpool\Service::isEnable()) { $activeAccount = \app\common\library\app\paymentpool\Service::getActiveAccount('wechat'); if ($activeAccount && !empty($activeAccount['config']['app_id'])) { $wechatAvailable = true; } } // 账户池未启用或无可用商户号时,回退到系统配置 if (!$wechatAvailable) { $wechatConfig = \app\common\model\config\System::getConfig('wechat'); if(!in_array($platform, $wechatConfig['platform'] ?? [])){ //微信支付未开启当前平台 continue; } $wechatAvailable = true; } if (!$wechatAvailable) { continue; } } if($item == 'alipay'){ $platform = \app\common\library\Platform::getPlatform(); $alipayAvailable = false; // 检查支付账户池是否有可用商户号(仅验证商户号有效性,平台可用性统一由系统配置决定) if (\app\admin\library\project\App::isInstall('paymentpool') && \app\common\library\app\paymentpool\Service::isEnable()) { $activeAccount = \app\common\library\app\paymentpool\Service::getActiveAccount('alipay'); if ($activeAccount && !empty($activeAccount['config']['app_id'])) { $alipayAvailable = true; } } // 账户池未启用或无可用商户号时,回退到系统配置 if (!$alipayAvailable) { $alipayConfig = \app\common\model\config\System::getConfig('alipay'); if(empty($alipayConfig) || empty($alipayConfig['app_id'])){ continue; } if(!in_array($platform, $alipayConfig['platform'] ?? ['wxOfficialAccount','H5'])){ continue; } $alipayAvailable = true; } if (!$alipayAvailable) { continue; } } if($item == 'douyinpay'){ if(\app\common\library\Platform::getPlatform() != 'dyMiniProgram'){ continue; } } if($item == 'virtual_pay'){ $virtualPayConfig = \app\common\model\config\System::getConfig('virtual_pay'); if(empty($virtualPayConfig) || ($virtualPayConfig['status'] ?? 'close') !== 'open'){ continue; } if(\app\common\library\Platform::getPlatform() != 'wxMiniProgram'){ continue; } } $data[] = $temp; } $this->success('获取成功', $data); } /** * 支付宝网页支付 */ public function alipay() { $orderNo = $this->request->get('orderNo'); $order = Order::where('orderNo', $orderNo)->find(); try { if (!$order) { throw new \Exception("订单不存在"); } if ($order->status > 0) { throw new \Exception("订单已支付"); } if ($order->status < 0) { throw new \Exception("订单已失效"); } $notify_url = $this->request->root(true) . '/pay/notifyx/pay_type/alipay/platform/H5/uniacid/'.UNIACID; $pay = new \app\common\library\pay\PayService('alipay', 'url', $notify_url); $order_data = [ 'out_trade_no' => $order->orderNo, 'total_fee' => $order->total_fee, 'subject' => '商城订单支付', ]; $result = $pay->create($order_data); $result = $result->getContent(); echo $result; } catch (\Exception $e) { echo $e->getMessage(); } } /** * 拉起支付 */ public function handle() { $orderNo = $this->request->post('order_no'); $pay_type = $this->request->post('pay_type'); $openid = $this->request->post('openid', ''); $code = $this->request->post('code', ''); $platform = request()->header('platform'); $order = Order::where([ 'order_no'=>$orderNo, 'status'=>'unpaid' ])->find(); if (!$order) { throw new Exception("订单不存在"); } if (!$pay_type || !in_array($pay_type, ['wechat', 'douyinpay', 'alipay', 'balance', 'virtual_pay'])) { throw new Exception("支付类型不能为空"); } if($order['user_id'] != $this->auth->id){ throw new Exception("无法支付此订单"); } // 购买课程前表单支付校验(防止绕过前端直接调支付) if (\app\admin\library\project\App::isInstall('form')) { $orderItems = \app\common\model\order\Item::getItemList($orderNo); foreach ($orderItems as $item) { // 通过 item_id 匹配,不按 goods_type(课程类商品的 goods_type 是 course, // 而 bind 表存储的实际类型如 column/video 等,直接匹配会查不到) $bindForms = \app\admin\model\app\form\Bind::alias('bind') ->join('app_form f', 'bind.form_id = f.id') ->where('bind.content_id', $item['item_id']) ->where('f.bind_type', 'before_buy') ->where('f.status', 1) ->field('bind.form_id, f.name') ->select(); if (!empty($bindForms)) { foreach ($bindForms as $bindForm) { $submitted = \app\admin\model\app\form\Log::where([ 'form_id' => $bindForm['form_id'], 'user_id' => $this->auth->id ])->count(); if (!$submitted) { throw new Exception("请先完成购买前表单:{$bindForm['name']}"); } } } } } if ($pay_type == 'balance') { $this->balancePay($order, $pay_type, $platform); } $isVirtualPay = !empty($order['is_virtual_pay']) && $pay_type !== 'balance'; if ($isVirtualPay) { $pay_type = 'virtual_pay'; } $order_data = [ 'out_trade_no' => $order->order_no, 'total_fee' => $order->real_price, ]; // 微信公众号,小程序支付,必须有 openid switch ($pay_type){ case 'wechat': if (in_array($platform, ['wxOfficialAccount', 'wxMiniProgram'])) { if (isset($openid) && $openid) { // 如果传的有 openid $order_data['openid'] = $openid; } else { // 没有 openid 默认拿下单人的 openid $oauth = \app\common\model\user\Oauth::where([ 'user_id' => $order->user_id, 'provider' => 'Wechat', 'platform' => $platform ])->find(); $order_data['openid'] = $oauth ? $oauth->openid : ''; } if (empty($order_data['openid'])) { // 缺少 openid return $this->success('缺少 openid', 'no_openid'); } } $order_data['body'] = '商城订单支付'; break; case 'douyinpay': if (!$openid) { // 缺少 openid return $this->success('缺少 openid', 'no_openid'); } $order_data['openid'] = $openid; break; case 'virtual_pay': if ($openid) { $oauth = \app\common\model\user\Oauth::where([ 'openid' => $openid, 'provider' => 'Wechat', 'platform' => 'wxMiniProgram' ])->find(); } else { $oauth = \app\common\model\user\Oauth::where([ 'user_id' => $order->user_id, 'provider' => 'Wechat', 'platform' => 'wxMiniProgram' ])->find(); } if (!$oauth || empty($oauth->openid)) { return $this->success('缺少openid', 'no_openid'); } $resumeResult = $this->checkExistingVirtualPayOrder($order, $oauth->openid, $isVirtualPay ? 1 : 0); if ($resumeResult !== null) { return $resumeResult; } if (empty($code)) { return $this->success('请先登录后再支付', 'no_session_key'); } $wechat = new \app\common\library\Wechat('wxMiniProgram'); $session = $wechat->code($code); if (empty($session['session_key'])) { return $this->success('获取登录信息失败:' . ($session['errmsg'] ?? '未知错误'), 'no_session_key'); } if (!empty($session['openid']) && $session['openid'] !== $oauth->openid) { return $this->success('微信登录态与当前用户不一致,请重新登录', 'no_session_key'); } $sessionKey = $session['session_key']; $oauth->session_key = $sessionKey; $oauth->expiretime = time() + 7200; $oauth->save(); $order_data['openid'] = $oauth->openid; $order_data['session_key'] = $sessionKey; $order_data['mode'] = $this->request->post('mode', 'short_series_coin'); $order_data['product_id'] = $this->request->post('product_id', ''); \app\common\model\order\VirtualPay::saveRelation($order->order_no, $oauth->openid); break; default: $order_data['subject'] = '商城订单支付'; } $notify_url = $this->getNotifyUrl($pay_type,$platform,$openid); if ($pay_type == 'alipay') { $userAgent = strtolower($this->request->server('HTTP_USER_AGENT', '')); $isMobile = preg_match('/(mobile|android|iphone|ipad|ipod|windows phone)/i', $userAgent); if (!$isMobile) { $platform = 'pc'; } } try{ $payResult = $this->createPaymentWithPool($pay_type, $platform, $notify_url, $order_data, $order); }catch (\Throwable $e){ // 使用 Throwable 捕获 \Error 和 \Exception(PHP 7+ openssl_sign 等可能抛 Error) // 内部错误(如 openssl_sign 私钥错误)不直接对外暴露,统一返回通用提示 throw new Exception('支付遇到错误,请重试'); } $result = $payResult['result']; if ($platform == 'App') { $result = $result->getContent(); } if ($pay_type == 'alipay' || ($platform == 'H5' && $pay_type == 'wechat')) { $result = $result->getContent(); } $data = [ 'is_virtual_pay' => $isVirtualPay ? 1 : 0, 'pay_data' => $result ]; return $this->success('拉取支付参数成功', $data); } /** * 创建支付参数(接入支付账户池失败自动切换重试) * * 仅 wechat / alipay 类型会触发账户池逻辑; * 其它支付方式(douyinpay / virtual_pay 等)保持原有行为。 * * 流程: * 1. 若账户池未启用,则直接调用一次 PayService::create(); * 2. 若启用,则循环选取"使用中"/"暂未启用"商户号尝试发起, * 返回错误码时标记该商户号为"不可用"并切换到下一个, * 成功后绑定订单与商户号关系并累加使用次数; * 3. 全部商户号都失败时回退到系统配置(即不传 forceAccountId)。 * * @param string $pay_type * @param string $platform * @param string $notify_url * @param array $order_data * @param \app\common\model\order\Order $order * @return array ['result' => mixed, 'account_id' => int|null] */ private function createPaymentWithPool($pay_type, $platform, $notify_url, $order_data, $order) { // 非微信/支付宝支付直接走系统配置 if (!in_array($pay_type, ['wechat', 'alipay'], true)) { $pay = new \app\common\library\pay\PayService($pay_type, $platform, $notify_url); $result = $pay->create($order_data); return ['result' => $result, 'account_id' => null]; } $poolEnable = \app\admin\library\project\App::isInstall('paymentpool') && \app\common\library\app\paymentpool\Service::isEnable(); if (!$poolEnable) { // 账户池未启用,按原流程使用系统配置 $pay = new \app\common\library\pay\PayService($pay_type, $platform, $notify_url); $result = $pay->create($order_data); return ['result' => $result, 'account_id' => null]; } // 账户池启用:循环切换商户号尝试 $excludeIds = []; $lastError = ''; $maxRetry = 5; for ($i = 0; $i < $maxRetry; $i++) { $account = \app\common\library\app\paymentpool\Service::chooseNextAccount($pay_type, $excludeIds); if (!$account) { break; } try { $pay = new \app\common\library\pay\PayService($pay_type, $platform, $notify_url, $account['id']); $result = $pay->create($order_data); // 校验返回结果中是否包含错误码(微信返回数组 / 支付宝返回 Response) $errInfo = $this->detectPayError($pay_type, $result); if ($errInfo) { // 标记不可用 + 切换 \app\common\library\app\paymentpool\Service::markInvalid( $account['id'], $errInfo['code'], $errInfo['msg'], $order['order_no'] ); $excludeIds[] = $account['id']; $lastError = $errInfo['msg']; continue; } // 成功:绑定订单和累加使用次数 \app\common\library\app\paymentpool\Service::bindOrder($order['order_no'], $account['id'], $pay_type); \app\common\library\app\paymentpool\Service::incrUseCount($account['id']); return ['result' => $result, 'account_id' => $account['id']]; } catch (\Throwable $e) { // 调用异常:标记该商户号不可用,继续切换 // 使用 Throwable 捕获 \Error 和 \Exception(PHP 7+ openssl_sign 等可能抛 Error) \app\common\library\app\paymentpool\Service::markInvalid( $account['id'], 'EXCEPTION', $e->getMessage(), $order['order_no'] ); $excludeIds[] = $account['id']; $lastError = $e->getMessage(); } } // 账户池全部失败,回退到系统配置兜底 try { $pay = new \app\common\library\pay\PayService($pay_type, $platform, $notify_url); $result = $pay->create($order_data); return ['result' => $result, 'account_id' => null]; } catch (\Throwable $e) { // 必须抛出 PHP 原生 \Exception,让外层 handle() 的 catch 能拦截到, // 避免 app\common\exception\Exception 构造函数直接 send 响应导致原始错误外泄 throw new \Exception($lastError ?: $e->getMessage()); } } /** * 检测 PayService::create() 返回结果中是否包含错误码 * * @param string $pay_type wechat|alipay * @param mixed $result * @return array|null ['code' => string, 'msg' => string],无错误返回 null */ private function detectPayError($pay_type, $result) { if ($pay_type === 'wechat') { // Yansongda 微信 mp/miniapp 返回 Collection(数组式),异常时含 return_code=FAIL $data = is_object($result) && method_exists($result, 'all') ? $result->all() : (array)$result; if (isset($data['return_code']) && $data['return_code'] !== 'SUCCESS') { return [ 'code' => $data['err_code'] ?? ($data['return_code'] ?? 'FAIL'), 'msg' => $data['err_code_des'] ?? ($data['return_msg'] ?? '微信支付下单失败'), ]; } if (isset($data['result_code']) && $data['result_code'] !== 'SUCCESS') { return [ 'code' => $data['err_code'] ?? 'FAIL', 'msg' => $data['err_code_des'] ?? '微信支付下单失败', ]; } return null; } if ($pay_type === 'alipay') { // 支付宝 wap/web 正常返回 Response 对象(HTML 表单),不会带错误码; // 异常时 SDK 会抛出 GatewayException,在 catch 中处理。 // 但部分错误(如 INVALID_PARAMETER)可能以数组形式返回,需兼容处理。 $data = is_object($result) && method_exists($result, 'toArray') ? $result->toArray() : (is_array($result) ? $result : null); if (is_array($data)) { // 兼容支付宝错误响应结构 $response = $data['alipay_trade_wap_pay_response'] ?? $data['alipay_trade_page_pay_response'] ?? $data; if (isset($response['code']) && $response['code'] !== '10000') { return [ 'code' => $response['code'], 'msg' => $response['sub_msg'] ?? ($response['msg'] ?? '支付宝支付下单失败'), ]; } } return null; } return null; } /** * 历史订单再次发起虚拟支付前,先检查微信侧是否已存在同订单号的支付单。 * 命中旧单时不再继续返回 requestVirtualPayment 参数,避免触发 ORDER_NO_DUP。 * @param Order $order * @param string $openid * @param int $isVirtualPay * @return mixed|null */ private function checkExistingVirtualPayOrder($order, $openid, $isVirtualPay) { try { $virtualPayService = new VirtualPayService(); $queryResult = $virtualPayService->queryOrder($openid, $order->order_no); } catch (\Exception $e) { return null; } if (isset($queryResult['errcode']) && $queryResult['errcode'] !== 0) { return null; } $wxOrder = $queryResult['order'] ?? []; $status = intval($wxOrder['status'] ?? -1); if (in_array($status, [2, 3, 4], true)) { return $this->error('订单已存在微信支付记录,请刷新订单状态'); } if ($status >= 0) { return $this->error('该订单已存在微信侧支付单,请先查询支付结果;若需重新支付,请重新下单'); } return null; } public function getNotifyUrl($pay_type,$platform,$openId){ global $_W; if(\app\common\library\Platform::getSystemType() == 'single'){ return MODULE_URL .'/api/pay/notifyx/pay_type/' . $pay_type . '/platform/' . $platform.'/open_id/' . $openId."/uniacid/".UNIACID; }else{ return $this->request->domain() .'/addons/tuzi_v1/payment/notify/wechat.php/pay_type/' . $pay_type.'/open_id/' . $openId . '/platform/' . $platform."/uniacid/".UNIACID; } } // 余额支付 public function balancePay ($order, $type, $method) { $order = Db::transaction(function () use ($order, $type, $method) { $balanceLibrary = new Balance(); $userInfo = $balanceLibrary->getOrderUser($order['order_no']); if(!$userInfo){ throw new Exception('获取用户信息失败'); } //余额不足 if($userInfo['money'] < $order['real_price']){ throw new Exception('余额不足'); } User::money(($order['real_price'] * -1), $userInfo['id'], "余额支付"); // 支付后流程 $notify = [ 'order_no' => $order['order_no'], 'transaction_id' => '', 'pay_time'=>time(), 'real_pay_price' => $order['real_price'], 'pay_type' => 'balance' // 支付方式 ]; $notify['payment_json'] = json_encode($notify); $order->paySuccess($order, $notify); return $order; }); $this->success('支付成功', $order); } /** * 获取回调中的参数 * @return array */ public function getNotifyParams(){ $paramArr = explode('/', $this->request->pathinfo()); $result = []; for ($i = 0; $i < count($paramArr); $i += 2) { $key = $paramArr[$i]; $value = isset($paramArr[$i + 1]) ? $paramArr[$i + 1] : null; $result[$key] = $value; } return $result; } /** * 支付成功回调 */ public function notifyx() { $params = $this->request->param(true); $pay_type = isset($params['pay_type']) ? $params['pay_type'] : ''; $platform = isset($params['platform']) ? $params['platform'] : ''; $pay = new \app\common\library\pay\PayService($pay_type, $platform); if($pay_type == 'douyinpay'){ $result = 'false'; try{ $openId = isset($params['open_id']) ? $params['open_id'] : ''; $content = file_get_contents('php://input'); if(empty($content)) return false; $paymentConfig = \app\common\model\config\System::getConfig($pay_type); $paymentConfig['notify_url'] = $this->getNotifyUrl($pay_type,$platform,$openId); $content=json_decode($content,true); $sign = \douyin\Byte::init($paymentConfig)->verifySignature($paymentConfig['rsa_public_key']); if($sign){ $data = json_decode($content['msg'],true); $data['open_id'] = $openId; $result = $this->successHandle($data, $pay, $pay_type, $platform); } } catch (\Exception $e) { } }else{ $result = $pay->notify(function ($data, $pay) use ($pay_type, $platform) { $this->successHandle($data, $pay, $pay_type, $platform); }); } return $result; } /** * 支付成功回调处理 * @param $data * @param $pay * @param $pay_type * @param $platform * @return false|string */ public function successHandle($data, $pay,$pay_type, $platform){ try { switch ($pay_type){ case 'wechat': $pay_fee = $data['cash_fee'] / 100; $orderNo = $data['out_trade_no']; $transactionId = $data['transaction_id']; break; case 'douyinpay': $pay_fee = $data['total_amount'] / 100; $orderNo = $data['out_order_no']; $transactionId = $data['order_id']; break; case 'alipay': $dataArr = is_object($data) ? $data->toArray() : $data; $pay_fee = isset($dataArr['total_amount']) ? $dataArr['total_amount'] : 0; $orderNo = isset($dataArr['out_trade_no']) ? $dataArr['out_trade_no'] : ''; $transactionId = isset($dataArr['trade_no']) ? $dataArr['trade_no'] : ''; break; } $order = Order::where('order_no', $orderNo)->find(); if (!$order || $order->status != \app\common\constant\order\Status::STATUS_UNPAID) { if (!$order && strpos($orderNo, 'TEST') === 0 && \app\admin\library\project\App::isInstall('paymentpool')) { $testRecord = \app\admin\model\app\paymentpool\Test::where('test_no', $orderNo)->find(); if ($testRecord && $testRecord->status == \app\admin\model\app\paymentpool\Test::STATUS_PENDING) { $testRecord->status = \app\admin\model\app\paymentpool\Test::STATUS_SUCCESS; $testRecord->save(); } } return $pay->success()->send(); } Db::transaction(function () use ($order,$orderNo,$transactionId, $data, $pay_type, $platform, $pay_fee) { $paymentJson = $pay_type == 'douyinpay' ? json_encode($data) : (is_object($data) ? json_encode($data->toArray()) : json_encode($data)); $notify = [ 'order_no' => $orderNo, 'pay_time'=>time(), 'transaction_id' => $transactionId, 'payment_json' => $paymentJson, 'real_pay_price' => $pay_fee, 'pay_type' => $pay_type ]; $order->paySuccess($order, $notify); }); if($pay_type == 'douyinpay'){ return '{"err_no": 0,"err_tips": "success"}'; } return $pay->success()->send(); } catch (\Exception $e) { } return false; } /** * 退款成功回调 */ public function notifyr() { $params = $this->request->param(true); $pay_type = $params['pay_type']; $platform = $params['platform']; $pay = new \app\common\library\pay\PayService($pay_type, $platform); if($pay_type == 'douyinpay'){ $result = 'false'; // try{ // $content = file_get_contents('php://input'); // if(empty($content)) return '{"err_no": 0,"err_tips": "success"}'; // $paymentConfig = \app\common\model\config\System::getConfig($pay_type); // $paymentConfig['notify_url'] = $this->getNotifyUrl($pay_type,$platform,$openId); // $content=json_decode($content,true); // $sign = \douyin\Byte::init($paymentConfig)->checkSign($content); // // if($sign == $content['msg_signature']){ // $data = json_decode($content['msg'],true); // $data['open_id'] = $openId; // $result = $this->successHandle($data, $pay, $pay_type, $platform); // } // } catch (\Exception $e) { // file_put_contents("./err1.txt",$e->getMessage()); // } return '{"err_no": 0,"err_tips": "success"}'; }else{ $result = $pay->notifyRefund(function ($data, $pay) use ($pay_type, $platform) { try { $out_refund_no = $data['out_refund_no']; $out_trade_no = $data['out_trade_no']; // 编写退款逻辑 $order = Order::where([ 'order_no'=> $out_trade_no ])->find(); $refundLog = \app\admin\model\order\RefundLog::where([ 'refund_no'=> $out_refund_no ])->order("id","desc")->find(); file_put_contents("./refund_fail.txt",json_encode([!$order , !$refundLog , $refundLog['status']])."\n\n",FILE_APPEND); if (!$order || !$refundLog || $refundLog['status'] != 0) { // 订单不存在,或者订单已支付 return $pay->success()->send(); } $item = \app\common\model\order\Item::where('id', $refundLog['order_item_id'])->find(); Db::transaction(function () use ($order, $item, $refundLog) { //退款成功 \app\admin\model\order\Refund::refundFinish($order, $item, $refundLog); }); return $pay->success()->send(); } catch (\Exception $e) { file_put_contents("./refund_fail.txt",$e->getMessage()."\n\n",FILE_APPEND); } }); return $result; } } }