virtualPayService = new VirtualPayService(); } /** * 获取虚拟支付配置信息(是否开启等) * @return mixed */ public function getConfig() { $virtualPayConfig = \app\common\model\config\System::getConfig('virtual_pay'); $data = [ 'status' => $virtualPayConfig['status'] ?? 'close', 'ios_refund_policy' => $virtualPayConfig['ios_refund_policy'] ?? 'none', 'virtual_goods_types' => $virtualPayConfig['virtual_goods_types'] ?? [], 'exchange_rate' => $virtualPayConfig['exchange_rate'] ?? '1', ]; $this->success('获取成功', $data); } /** * 查询用户代币余额 * @return mixed */ public function queryBalance() { $oauth = \app\common\model\user\Oauth::where([ 'user_id' => $this->auth->id, 'provider' => 'Wechat', 'platform' => 'wxMiniProgram' ])->find(); if (!$oauth || empty($oauth->openid)) { $this->error('用户微信小程序授权信息不存在'); } $result = $this->virtualPayService->queryUserBalance($oauth->openid); if (isset($result['errcode']) && $result['errcode'] !== 0) { $this->error('查询余额失败:' . ($result['errmsg'] ?? '未知错误')); } $this->success('查询成功', $result); } /** * 检查余额并支付(代币模式) * 支付前先查询余额,如果足够直接扣减代币完成支付 * 如果不足返回需要充值的信息,充值后再次调用此接口 * @return mixed */ public function checkAndPay() { $orderNo = $this->request->post('order_no'); $mode = $this->request->post('mode', 'short_series_coin'); if (empty($orderNo)) { $this->error('订单号不能为空'); } $order = Order::where([ 'order_no' => $orderNo, 'status' => \app\common\constant\order\Status::STATUS_UNPAID ])->find(); if (!$order) { $this->error('订单不存在或已支付'); } if ($order['user_id'] != $this->auth->id) { $this->error('无权操作此订单'); } $oauth = $this->getOrderWxOauth($order); try { $balanceResult = $this->virtualPayService->queryUserBalance($oauth->openid); if (isset($balanceResult['errcode']) && $balanceResult['errcode'] !== 0) { $this->error('查询余额失败:' . ($balanceResult['errmsg'] ?? '未知错误')); } $currentBalance = intval($balanceResult['balance'] ?? 0); $requiredAmount = $this->getRequiredCoinAmount($order, $mode); if ($currentBalance >= $requiredAmount) { return $this->executeCurrencyPay($order, $oauth, $requiredAmount); } else { $deficit = $requiredAmount - $currentBalance; return $this->returnRechargeInfo($order, $currentBalance, $requiredAmount, $deficit, $balanceResult); } } catch (\Exception $e) { $this->error('检查余额异常:' . $e->getMessage()); } } /** * 获取订单需要的代币数量 * @param Order $order 订单对象 * @param string $mode 支付模式 * @return int */ private function getRequiredCoinAmount($order, $mode) { $exchangeRate = VirtualPayService::getExchangeRate(); $priceInYuan = floatval($order->real_price); if ($mode === 'short_series_coin') { return intval($priceInYuan * $exchangeRate); } return intval($priceInYuan * $exchangeRate); } /** * 执行代币扣减支付 * @param Order $order 订单对象 * @param mixed $oauth 用户授权信息 * @param int $amount 扣减的代币数量 * @return void */ private function executeCurrencyPay($order, $oauth, $amount) { $payItem = $this->buildPayItem($order); try { $result = $this->virtualPayService->currencyPay( $oauth->openid, $amount, $order->order_no, $payItem ); if (isset($result['errcode']) && $result['errcode'] !== 0) { $errMsg = $result['errmsg'] ?? '未知错误'; if ($result['errcode'] == 268490006) { $this->error('代币余额不足,请先充值'); } elseif ($result['errcode'] == 268490004) { $this->success('支付成功(重复操作)', [ 'status' => 'success', 'order_no' => $order->order_no, 'balance' => $result['balance'] ?? 0, 'message' => '订单已处理' ]); } else { $this->error('扣减代币失败:' . $errMsg); } } $newBalance = $result['balance'] ?? 0; $this->success('支付成功', [ 'status' => 'success', 'order_no' => $order->order_no, 'amount' => $amount, 'balance' => $newBalance, 'used_present' => $result['used_present_amount'] ?? 0, 'message' => '代币扣减成功,等待微信推送确认' ]); } catch (\Exception $e) { $this->error('执行代币支付异常:' . $e->getMessage()); } } /** * 返回充值信息(余额不足时) * @param Order $order 订单对象 * @param int $currentBalance 当前余额 * @param int $requiredAmount 需要的金额 * @param int $deficit 差额 * @param array $balanceResult 余额查询结果 * @return void */ private function returnRechargeInfo($order, $currentBalance, $requiredAmount, $deficit, $balanceResult) { $this->success('余额不足,请先充值', [ 'status' => 'insufficient_balance', 'order_no' => $order->order_no, 'current_balance' => $currentBalance, 'required_amount' => $requiredAmount, 'deficit' => $deficit, 'present_balance' => $balanceResult['present_balance'] ?? 0, 'sum_save' => $balanceResult['sum_save'] ?? 0, 'sum_present' => $balanceResult['sum_present'] ?? 0, 'first_save_flag' => $balanceResult['first_save_flag'] ?? false, 'message' => "当前代币余额:{$currentBalance},需要:{$requiredAmount},还差:{$deficit}" ]); } /** * 构建支付项信息 * @param Order $order 订单对象 * @return array */ private function buildPayItem($order) { $orderItems = \app\admin\model\order\Item::where(['order_no' => $order->order_no])->select()->toArray(); $payItems = []; foreach ($orderItems as $item) { $payItems[] = [ 'productid' => $item['item_id'] ?? '', 'unit_price' => floatval($item['unit_price'] ?? $item['real_price'] ?? 0), 'quantity' => intval($item['goods_count'] ?? 1), 'title' => $item['item_name'] ?? '', 'goods_type' => $item['goods_type'] ?? '' ]; } return $payItems; } /** * 拉起虚拟支付(客户端调用wx.requestVirtualPayment前获取参数) * @return mixed */ public function handle() { $orderNo = $this->request->post('order_no'); $code = $this->request->post('code', ''); $order = Order::where([ 'order_no' => $orderNo, 'status' => 'unpaid' ])->find(); if (!$order) { $this->error('订单不存在或已支付'); } if ($order['user_id'] != $this->auth->id) { $this->error('无权支付此订单'); } $oauth = \app\common\model\user\Oauth::where([ 'user_id' => $this->auth->id, 'provider' => 'Wechat', 'platform' => 'wxMiniProgram' ])->find(); if (!$oauth || empty($oauth->openid)) { $this->error('请先登录微信小程序'); } $this->tryResumeExistingWxOrder($order, $oauth); if (empty($code)) { $this->error('请先调用wx.login并传入code'); } $wechat = new \app\common\library\Wechat('wxMiniProgram'); $session = $wechat->code($code); if (empty($session['session_key'])) { $this->error('获取session_key失败:' . ($session['errmsg'] ?? '未知错误')); } if (!empty($session['openid']) && $session['openid'] !== $oauth->openid) { $this->error('微信登录态与当前用户不一致,请重新登录'); } $sessionKey = $session['session_key']; $oauth->session_key = $sessionKey; $oauth->expiretime = time() + 7200; $oauth->save(); $orderData = [ 'openid' => $oauth->openid, 'session_key' => $sessionKey, 'out_trade_no' => $order->order_no, 'total_fee' => $order->real_price, 'mode' => $this->request->post('mode', 'short_series_coin'), 'product_id' => $this->request->post('product_id', ''), ]; try { $result = $this->virtualPayService->getClientPayConfig( $orderData['openid'], $orderData['session_key'], $orderData['out_trade_no'], $orderData['total_fee'], $orderData['mode'], $orderData['product_id'] ); } catch (\Exception $e) { $this->error('获取支付参数失败:' . $e->getMessage()); } $this->success('获取成功', $result); } /** * 历史订单再次发起支付前,先检查微信侧是否已经存在同订单号的支付单。 * 如果微信侧已成功,则直接补齐本地订单;如果已存在未完成支付单,则阻止重复拉起。 * @param Order $order * @param mixed $oauth * @return void */ private function tryResumeExistingWxOrder($order, $oauth) { try { $queryResult = $this->virtualPayService->queryOrder($oauth->openid, $order->order_no); } catch (\Exception $e) { return; } if (isset($queryResult['errcode']) && $queryResult['errcode'] !== 0) { return; } $wxOrder = $queryResult['order'] ?? []; $status = intval($wxOrder['status'] ?? -1); if (in_array($status, [2, 3, 4], true)) { if ($order['status'] == \app\common\constant\order\Status::STATUS_UNPAID) { if ($order['is_virtual_pay'] == 1) { $this->processCoinPayAfterRecharge($order, $oauth, $queryResult); } else { $this->processCashOrderDeliver($order, $oauth, $queryResult); } } $this->success('订单已存在微信支付记录,已自动处理', [ 'status' => 'completed', 'order_no' => $order->order_no, ]); } if ($status >= 0) { $this->error('该订单已存在微信侧支付单,请先查询支付结果;若需重新支付,请重新下单'); } } /** * 虚拟支付成功回调(小程序端支付成功后调用,用于确认发货) * 余额不足充值场景:queryOrder确认充值成功 -> query_user_balance -> currency_pay -> paySuccess * @return mixed */ public function notify() { $orderNo = $this->request->post('order_no'); $order = Order::where('order_no', $orderNo)->find(); if (!$order) { $this->error('订单不存在'); } if ($order->user_id != $this->auth->id) { $this->error('无权处理此订单'); } $oauth = $this->getOrderWxOauth($order); if ($order->status != \app\common\constant\order\Status::STATUS_UNPAID) { $this->success('订单已处理'); } try { $orderResult = $this->virtualPayService->queryOrder($oauth->openid, $orderNo); } catch (\Exception $e) { $this->error('查询虚拟支付订单失败:' . $e->getMessage()); } if (isset($orderResult['errcode']) && $orderResult['errcode'] !== 0) { $this->error('查询虚拟支付订单失败:' . ($orderResult['errmsg'] ?? '未知错误')); } $wxOrder = $orderResult['order'] ?? []; if (!in_array(intval($wxOrder['status'] ?? -1), [2, 3, 4])) { $this->error('微信侧订单尚未支付成功'); } if ($order['is_virtual_pay'] == 1) { $this->processCoinPayAfterRecharge($order, $oauth, $orderResult); } else { $this->processCashOrderDeliver($order, $oauth, $orderResult); } $this->success('发货通知成功'); } /** * 处理代币充值成功后的扣币流程 * queryOrder确认充值成功 -> query_user_balance -> currency_pay -> paySuccess * @param Order $order 订单对象 * @param mixed $oauth 用户授权信息 * @param array $orderResult 微信订单查询结果 * @return void */ private function processCoinPayAfterRecharge($order, $oauth, $orderResult) { $orderNo = $order->order_no; try { $balanceResult = $this->virtualPayService->queryUserBalance($oauth->openid); if (isset($balanceResult['errcode']) && $balanceResult['errcode'] !== 0) { throw new \Exception('查询余额失败:' . ($balanceResult['errmsg'] ?? '未知错误')); } $currentBalance = intval($balanceResult['balance'] ?? 0); $requiredAmount = $this->getRequiredCoinAmount($order, 'short_series_coin'); if ($currentBalance < $requiredAmount) { throw new \Exception("充值后余额仍不足,当前:{$currentBalance},需要:{$requiredAmount}"); } $payItem = $this->buildPayItem($order); $result = $this->virtualPayService->currencyPay( $oauth->openid, $requiredAmount, $orderNo, $payItem ); if (isset($result['errcode']) && $result['errcode'] !== 0) { if ($result['errcode'] == 268490004) { $this->handleDuplicateCurrencyPay($order, $oauth, $orderResult); return; } throw new \Exception('扣减代币失败:' . ($result['errmsg'] ?? '未知错误')); } $notify = [ 'order_no' => $orderNo, 'transaction_id' => $orderResult['order']['wxpay_order_id'] ?? ($orderResult['order']['wx_order_id'] ?? $orderNo), 'pay_time' => time(), 'real_pay_price' => $order->real_price, 'pay_type' => 'virtual_pay', 'payment_json' => json_encode([ 'query_order' => $orderResult, 'balance_result' => $balanceResult, 'currency_pay' => $result, ], JSON_UNESCAPED_UNICODE), ]; Db::transaction(function () use ($order, $notify) { $order->paySuccess($order, $notify); }); OrderLog::set($orderNo, "充值后扣减代币成功,余额:{$result['balance'] ?? 0}"); } catch (\Exception $e) { OrderLog::set($orderNo, "充值后扣币失败:{$e->getMessage()}"); $this->error($e->getMessage()); } } /** * 处理重复扣减代币的情况(currency_pay返回268490004) * 查询微信订单确认是否已扣币,然后完成本地订单 * @param Order $order 订单对象 * @param mixed $oauth 用户授权信息 * @param array $orderResult 微信订单查询结果 * @return void */ private function handleDuplicateCurrencyPay($order, $oauth, $orderResult) { $orderNo = $order->order_no; try { $queryResult = $this->virtualPayService->queryOrder($oauth->openid, $orderNo); if (isset($queryResult['errcode']) && $queryResult['errcode'] !== 0) { throw new \Exception('查询订单失败:' . ($queryResult['errmsg'] ?? '未知错误')); } $wxOrder = $queryResult['order'] ?? []; $orderStatus = intval($wxOrder['status'] ?? -1); if ($orderStatus === 3 || $orderStatus === 4) { $notify = [ 'order_no' => $orderNo, 'transaction_id' => $wxOrder['wxpay_order_id'] ?? ($wxOrder['wx_order_id'] ?? $orderNo), 'pay_time' => time(), 'real_pay_price' => $order->real_price, 'pay_type' => 'virtual_pay', 'payment_json' => json_encode([ 'query_order' => $orderResult, 'duplicate_check' => $queryResult, ], JSON_UNESCAPED_UNICODE), ]; Db::transaction(function () use ($order, $notify) { $order->paySuccess($order, $notify); }); OrderLog::set($orderNo, '重复扣币,订单已完成支付'); } else { throw new \Exception('订单状态异常,status=' . $orderStatus); } } catch (\Exception $e) { OrderLog::set($orderNo, "重复扣币处理异常:{$e->getMessage()}"); $this->error($e->getMessage()); } } /** * 处理现金订单发货(道具直购模式) * @param Order $order 订单对象 * @param mixed $oauth 用户授权信息 * @param array $orderResult 微信订单查询结果 * @return void */ private function processCashOrderDeliver($order, $oauth, $orderResult) { $orderNo = $order->order_no; try { $result = $this->virtualPayService->notifyProvideGoods($orderNo); } catch (\Exception $e) { $this->error('通知发货失败:' . $e->getMessage()); } if (isset($result['errcode']) && $result['errcode'] !== 0) { $this->error('通知发货失败:' . ($result['errmsg'] ?? '未知错误')); } $wxOrder = $orderResult['order'] ?? []; $notify = [ 'order_no' => $orderNo, 'transaction_id' => $wxOrder['wxpay_order_id'] ?? ($wxOrder['wx_order_id'] ?? $orderNo), 'pay_time' => time(), 'real_pay_price' => $order->real_price, 'pay_type' => 'virtual_pay', 'payment_json' => json_encode(['query_order' => $orderResult, 'notify_provide_goods' => $result], JSON_UNESCAPED_UNICODE), ]; Db::transaction(function () use ($order, $notify) { $order->paySuccess($order, $notify); }); } /** * 代币支付退款 * @return mixed */ public function refund() { $orderNo = $this->request->post('order_no'); $amount = $this->request->post('amount'); $order = Order::where('order_no', $orderNo)->find(); if (!$order) { $this->error('订单不存在'); } if ($order->user_id != $this->auth->id) { $this->error('无权处理此订单'); } if (!is_numeric($amount) || intval($amount) <= 0) { $this->error('退款金额不合法'); } if (!in_array($order->status, [ \app\common\constant\order\Status::STATUS_PAID, \app\common\constant\order\Status::STATUS_UNRECEIVE, \app\common\constant\order\Status::STATUS_UNSEND, \app\common\constant\order\Status::STATUS_SUREUNRECEIVE, \app\common\constant\order\Status::STATUS_SUCCESS, ])) { $this->error('订单状态不支持退款'); } $oauth = $this->getOrderWxOauth($order); $refundId = 'RF' . date('YmdHis') . str_pad(mt_rand(1, 99999), 5, '0', STR_PAD_LEFT); try { $result = $this->virtualPayService->cancelCurrencyPay( $oauth->openid, $amount, $orderNo, $refundId ); } catch (\Exception $e) { $this->error('退款失败:' . $e->getMessage()); } if (isset($result['errcode']) && $result['errcode'] !== 0) { $this->error('退款失败:' . ($result['errmsg'] ?? '未知错误')); } $this->success('退款成功', $result); } /** * 查询虚拟支付订单 * 余额不足充值场景:queryOrder确认充值成功 -> query_user_balance -> currency_pay -> paySuccess * @return mixed */ public function queryOrder() { $orderNo = $this->request->post('order_no', ''); $wxOrderId = $this->request->post('wx_order_id', ''); $autoComplete = $this->request->post('auto_complete', 0); if (empty($orderNo) && empty($wxOrderId)) { $this->error('订单号不能为空'); } if (!empty($orderNo)) { $order = Order::where('order_no', $orderNo)->find(); if (!$order) { $this->error('订单不存在'); } if ($order->user_id != $this->auth->id) { $this->error('无权查询此订单'); } $oauth = $this->getOrderWxOauth($order); } else { $oauth = $this->getCurrentWxOauth(); } $result = $this->virtualPayService->queryOrder($oauth->openid, $orderNo, $wxOrderId); if (isset($result['errcode']) && $result['errcode'] !== 0) { $this->error('查询失败:' . ($result['errmsg'] ?? '未知错误')); } if ($autoComplete && !empty($orderNo) && $order['is_virtual_pay'] == 1 && $order['status'] == \app\common\constant\order\Status::STATUS_UNPAID) { $wxOrder = $result['order'] ?? []; if (in_array(intval($wxOrder['status'] ?? -1), [2, 3, 4])) { $this->processCoinPayAfterRecharge($order, $oauth, $result); } } $this->success('查询成功', $result); } /** * 查询现金订单并启动退款 * @return mixed */ public function refundOrder() { $orderNo = $this->request->post('order_no'); $refundFee = $this->request->post('refund_fee', null); $reason = $this->request->post('refund_reason', '0'); $reqFrom = $this->request->post('req_from', '3'); if (empty($orderNo)) { $this->error('订单号不能为空'); } $order = Order::where('order_no', $orderNo)->find(); if (!$order) { $this->error('订单不存在'); } if ($order->user_id != $this->auth->id) { $this->error('无权处理此订单'); } $oauth = $this->getOrderWxOauth($order); $orderResult = $this->virtualPayService->queryOrder($oauth->openid, $orderNo); if (isset($orderResult['errcode']) && $orderResult['errcode'] !== 0) { $this->error('查询订单失败:' . ($orderResult['errmsg'] ?? '未知错误')); } $leftFee = intval($orderResult['order']['left_fee'] ?? 0); if ($leftFee <= 0) { $this->error('订单无可退金额'); } if ($refundFee === null || $refundFee === '') { $refundFee = $leftFee; } else { $refundFee = intval($refundFee); } if ($refundFee <= 0 || $refundFee > $leftFee) { $this->error('退款金额不合法'); } $refundId = 'RF' . date('YmdHis') . str_pad(mt_rand(1, 99999), 5, '0', STR_PAD_LEFT); $result = $this->virtualPayService->refundOrder($oauth->openid, $orderNo, $refundId, $leftFee, $refundFee, $reason, $reqFrom); if (isset($result['errcode']) && $result['errcode'] !== 0) { $this->error('退款失败:' . ($result['errmsg'] ?? '未知错误')); } $this->success('退款任务已启动', $result); } /** * 获取订单所属用户的小程序授权信息。 * @param Order $order * @return \think\Model */ private function getOrderWxOauth($order) { $oauth = \app\common\model\user\Oauth::where([ 'user_id' => $order->user_id, 'provider' => 'Wechat', 'platform' => 'wxMiniProgram' ])->find(); if (!$oauth || empty($oauth->openid)) { $this->error('用户微信授权信息不存在'); } return $oauth; } /** * 获取当前用户的小程序授权信息。 * @return \think\Model */ private function getCurrentWxOauth() { $oauth = \app\common\model\user\Oauth::where([ 'user_id' => $this->auth->id, 'provider' => 'Wechat', 'platform' => 'wxMiniProgram' ])->find(); if (!$oauth || empty($oauth->openid)) { $this->error('用户微信授权信息不存在'); } return $oauth; } }