app = new Application(config('wechat.pay.default')); } /** * [微信]获取支付配置 * * @param int $orderId 订单编号 * @return array * @throws Exception */ public function getPaymentConfig($orderId) { // 获取当前用户 $userId = Auth::id(); $user = MemberUser::where('state', UserStatus::OPEN->value)->findOrFail($userId); // 查询订单 $order = Order::where('id', $orderId) ->where('user_id', $userId) ->where('state', OrderStatus::CREATED->value) ->firstOrFail(); // 生成微信支付配置 $outTradeNo = $this->generateOutTradeNo(); $config = $this->generateWxPayConfig($order, $outTradeNo); // 更新订单号(使用 order_no 而不是 out_trade_no) $order->order_no = $outTradeNo; $order->save(); // 发送抢单通知 $this->sendGrabOrderNotification($order); return $config; } /** * 生成外部交易号 */ private function generateOutTradeNo() { return date('YmdHis') . mt_rand(1000, 9999); } /** * 生成微信支付配置 */ private function generateWxPayConfig($order, $outTradeNo) { $appId = config('wechat.official_account.default.app_id'); // 创建支付订单 $result = $this->app->getClient()->postJson('v3/pay/transactions/jsapi', [ 'appid' => $appId, 'mchid' => config('wechat.pay.default.mch_id'), 'out_trade_no' => $outTradeNo, 'description' => "订单支付#{$order->id}", 'notify_url' => config('wechat.pay.default.notify_url'), 'amount' => [ 'total' => intval($order->pay_amount * 100), // 单位:分 'currency' => 'CNY' ], 'payer' => [ 'openid' => $order->member->socialAccounts() ->where('platform', 'WECHAT') ->value('social_id') ], ]); // 获取预支付交易会话标识 $prepayId = $result['prepay_id']; // 使用 EasyWeChat 的工具方法生成支付配置 $config = $this->app->getUtils()->buildBridgeConfig($prepayId, $appId, 'RSA'); // 确保 appId 存在 $config['appId'] = $appId; return $config; } /** * 发送抢单通知 */ private function sendGrabOrderNotification($order) { // TODO: 对接极光推送,发送抢单通知 } /** * [微信]处理支付回调通知 */ public function handleNotify(Request $request) { try { // 记录请求头信息 $headers = [ 'Wechatpay-Serial' => $request->header('Wechatpay-Serial'), 'Wechatpay-Signature' => $request->header('Wechatpay-Signature'), 'Wechatpay-Timestamp' => $request->header('Wechatpay-Timestamp'), 'Wechatpay-Nonce' => $request->header('Wechatpay-Nonce'), ]; Log::info('微信支付回调请求头', $headers); try { // 获取并记录原始请求内容 $content = $request->getContent(); Log::info('原始请求内容', ['content' => $content]); // 解析 JSON $message = json_decode($content, true); if (json_last_error() !== JSON_ERROR_NONE) { throw new \Exception('JSON 解析失败: ' . json_last_error_msg()); } Log::info('解析后的请求数据', ['message' => $message]); // 验证必要字段 if ( empty($message['resource']) || empty($message['resource']['ciphertext']) || empty($message['resource']['nonce']) || empty($message['resource']['associated_data']) ) { throw new \Exception('回调数据缺少必要字段'); } // 获取解密器 $aesKey = $this->app->getConfig()['secret_key'] ?? ''; if (empty($aesKey)) { throw new \Exception('未配置 API v3 密钥'); } Log::info('获取到密钥'); // 解密数据 try { $ciphertext = $message['resource']['ciphertext']; $nonce = $message['resource']['nonce']; $associatedData = $message['resource']['associated_data']; // 使用 AEAD_AES_256_GCM 算法解密 $decryptedData = \EasyWeChat\Kernel\Support\AesGcm::decrypt( $ciphertext, $aesKey, $nonce, $associatedData ); Log::info('解密成功', ['raw' => $decryptedData]); // 解析解密后的 JSON $decryptedData = json_decode($decryptedData, true); if (json_last_error() !== JSON_ERROR_NONE) { throw new \Exception('解密后的 JSON 解析失败: ' . json_last_error_msg()); } Log::info('解密后的数据', ['decrypted' => $decryptedData]); } catch (\Exception $e) { Log::error('解密失败', [ 'error' => $e->getMessage(), 'trace' => $e->getTraceAsString() ]); throw $e; } // 处理支付结果 if ($decryptedData['trade_state'] === 'SUCCESS') { $result = $this->handlePaymentResult([ 'out_trade_no' => $decryptedData['out_trade_no'], 'transaction_id' => $decryptedData['transaction_id'], 'trade_state' => $decryptedData['trade_state'], 'amount' => [ 'total' => $decryptedData['amount']['total'] ?? 0, 'payer_total' => $decryptedData['amount']['payer_total'] ?? 0, 'currency' => $decryptedData['amount']['currency'] ?? 'CNY', ], 'success_time' => $decryptedData['success_time'], 'payer' => [ 'openid' => $decryptedData['payer']['openid'] ?? '' ] ]); Log::info('支付结果处理完成', [ 'out_trade_no' => $decryptedData['out_trade_no'], 'result' => $result ]); } else { Log::warning('支付状态不是成功', [ 'trade_state' => $decryptedData['trade_state'] ]); } // 返回成功响应 return response()->json([ 'code' => 'SUCCESS', 'message' => 'OK' ]); } catch (\Exception $e) { Log::error('回调处理过程出错', [ 'error' => $e->getMessage(), 'trace' => $e->getTraceAsString() ]); throw $e; } } catch (\Exception $e) { Log::error('微信支付回调处理失败', [ 'error' => $e->getMessage(), 'trace' => $e->getTraceAsString(), 'request' => [ 'headers' => $request->headers->all(), 'content' => $request->getContent() ] ]); return response()->json([ 'code' => 'FAIL', 'message' => '处理失败: ' . $e->getMessage() ]); } } /** * 处理支付结果 * * 逻辑描述: * 1. 校验订单信息 * 2. 校验订单金额(注意单位转换) * 3. 检查订单状态防止重复处理 * 4. 事务中更新订单状态 * 5. 创建支付记录 * 6. 创建平台收入财务记录 * 7. 发送支付成功通知 */ private function handlePaymentResult(array $data): bool { Log::info('开始处理支付结果', $data); // 1. 校验订单信息 $order = Order::where('order_no', $data['out_trade_no'])->first(); if (!$order) { Log::error('订单不存在', ['out_trade_no' => $data['out_trade_no']]); throw new \Exception('订单不存在'); } // 2. 校验订单金额(注意微信支付金额单位是分) $orderAmount = intval($order->pay_amount * 100); if ($orderAmount !== $data['amount']['total']) { Log::error('订单金额不匹配', [ 'order_amount' => $orderAmount, 'paid_amount' => $data['amount']['total'] ]); throw new \Exception('订单金额不匹配'); } // 3. 检查订单状态,防止重复处理 if ($order->state !== OrderStatus::CREATED->value) { Log::warning('订单状态异常', [ 'order_no' => $order->order_no, 'current_state' => $order->state ]); return true; // 已处理过的订单直接返回成功 } // 4. 更新订单状态 if ($data['trade_state'] === 'SUCCESS') { return DB::transaction(function () use ($order, $data) { // 更新订单状态为已支付 $order->update([ 'state' => OrderStatus::PAID->value, 'payment_time' => $data['success_time'], 'transaction_id' => $data['transaction_id'] ]); // 创建支付记录 $order->records()->create([ 'object_id' => $order->user_id, 'object_type' => MemberUser::class, 'state' => OrderRecordStatus::PAID->value, 'remark' => '微信支付成功' ]); // 创建平台收入财务记录 FinanceRecord::create([ 'record_no' => 'IN' . date('YmdHis') . mt_rand(1000, 9999), // 生成收入记录单号(IN-Income) 'owner_type' => 'platform', // 所属主体:平台 'order_id' => $order->id, // 关联订单ID 'type' => 'income', // 类型:收入 'business_type' => 'order_income', // 业务类型:订单收入 'amount' => $order->pay_amount, // 支付金额 'payment_type' => 'wechat', // 支付方式:微信支付 'region_code' => $order->area_code, // 区域编码 'user_id' => $order->user_id, // 关联用户ID 'coach_id' => $order->coach_id, // 关联教练ID 'remark' => '订单微信支付收入' // 备注 ]); // 删除订单位置的Redis记录 Redis::zrem('order_locations', 'order_' . $order->id); // 检测订单类型,如果是抢单并且有指定技师,则改订单状态为已接单 if ($order->type === OrderType::GRAB->value && $order->coach_id) { // 更新抢单记录状态为成功 OrderGrabRecord::where('order_id', $order->id) ->where('coach_id', $order->coach_id) ->update([ 'state' => OrderGrabRecordStatus::SUCCEEDED->value ]); // 更新订单状态为已接单 $order->update([ 'state' => OrderStatus::ACCEPTED->value ]); } // TODO: 发送支付成功通知 // event(new OrderPaidEvent($order)); Log::info('订单支付成功', [ 'order_no' => $order->order_no, 'transaction_id' => $data['transaction_id'] ]); return true; }); } return false; } /** * 申请退款 * * @param string $transactionId 微信支付交易号 * @param float $refundAmount 退款金额 * @param float $totalAmount 原订单金额 * @param string $reason 退款原因 * @return array * @throws \Exception */ public function refund(string $transactionId, float $refundAmount, float $totalAmount, string $reason = '订单退款'): array { try { // 生成退款单号 $refundNo = 'RF' . date('YmdHis') . mt_rand(1000, 9999); // 调用微信退款接口 $response = $this->app->getClient()->postJson('v3/refund/domestic/refunds', [ 'transaction_id' => $transactionId, // 微信支付交易号 'out_refund_no' => $refundNo, // 商户退款单号 'amount' => [ 'refund' => intval($refundAmount * 100), // 退款金额,单位:分 'total' => intval($totalAmount * 100), // 原订单金额,单位:分 'currency' => 'CNY' ], 'reason' => $reason, 'notify_url' => config('wechat.pay.default.refund_notify_url') // 添加退款回调地址 ]); // 获取响应内容 $result = json_decode($response->getContent(), true); // 记录退款日志 Log::info('微信退款请求结果', [ 'transaction_id' => $transactionId, 'refund_no' => $refundNo, 'refund_amount' => $refundAmount, 'total_amount' => $totalAmount, 'result' => $result ]); return [ 'success' => true, 'refund_no' => $refundNo, 'result' => $result ]; } catch (\Exception $e) { Log::error('微信退款请求异常', [ 'transaction_id' => $transactionId, 'refund_amount' => $refundAmount, 'error' => $e->getMessage() ]); // 如果是400错误,说明请求已经发送到微信服务器 // 此时退款可能已经成功,我们返回成功状态并记录退款号 if (str_contains($e->getMessage(), '400 Bad Request')) { return [ 'success' => true, 'refund_no' => $refundNo, 'message' => '退款请求已发送,请等待处理结果' ]; } return [ 'success' => false, 'message' => $e->getMessage() ]; } } /** * 处理微信退款回调通知 * @param Request $request * @return \Illuminate\Http\JsonResponse */ public function handleRefundNotify(Request $request) { try { // 记录请求头信息 $headers = [ 'Wechatpay-Serial' => $request->header('Wechatpay-Serial'), 'Wechatpay-Signature' => $request->header('Wechatpay-Signature'), 'Wechatpay-Timestamp' => $request->header('Wechatpay-Timestamp'), 'Wechatpay-Nonce' => $request->header('Wechatpay-Nonce'), ]; Log::info('微信退款回调请求头', $headers); // 获取并记录原始请求内容 $content = $request->getContent(); Log::info('退款回调原始请求内容', ['content' => $content]); // 解析 JSON $message = json_decode($content, true); if (json_last_error() !== JSON_ERROR_NONE) { throw new \Exception('JSON 解析失败: ' . json_last_error_msg()); } Log::info('解析后的退款回调数据', ['message' => $message]); // 验证必要字段 if ( empty($message['resource']) || empty($message['resource']['ciphertext']) || empty($message['resource']['nonce']) || empty($message['resource']['associated_data']) ) { throw new \Exception('回调数据缺少必要字段'); } // 获取解密器 $aesKey = $this->app->getConfig()['secret_key'] ?? ''; if (empty($aesKey)) { throw new \Exception('未配置 API v3 密钥'); } // 解密数据 try { $ciphertext = $message['resource']['ciphertext']; $nonce = $message['resource']['nonce']; $associatedData = $message['resource']['associated_data']; // 使用 AEAD_AES_256_GCM 算法解密 $decryptedData = \EasyWeChat\Kernel\Support\AesGcm::decrypt( $ciphertext, $aesKey, $nonce, $associatedData ); Log::info('退款回调解密成功', ['raw' => $decryptedData]); // 解析解密后的 JSON $decryptedData = json_decode($decryptedData, true); if (json_last_error() !== JSON_ERROR_NONE) { throw new \Exception('解密后的 JSON 解析失败: ' . json_last_error_msg()); } Log::info('解密后的退款回调数据', ['decrypted' => $decryptedData]); // 处理退款结果 if ($decryptedData['refund_status'] === 'SUCCESS') { // 根据商户退款单号查找退款记录 $refundRecord = OrderRefundRecord::where('refund_no', $decryptedData['out_refund_no'])->first(); if (!$refundRecord) { Log::error('未找到对应的退款记录', ['refund_no' => $decryptedData['out_refund_no']]); return response()->json(['code' => 'SUCCESS', 'message' => 'OK']); } // 更新退款记录状态 DB::transaction(function () use ($refundRecord, $decryptedData) { // 获取订单 $order = $refundRecord->order; // 检查订单状态是否为退款中,避免重复处理 if ($order->state !== OrderStatus::REFUNDING->value) { Log::info('订单状态不是退款中,可能已处理过', [ 'order_id' => $order->id, 'current_state' => $order->state ]); return; } // 更新退款记录 $refundRecord->update([ 'state' => $decryptedData['refund_status'], 'transaction_id' => $decryptedData['transaction_id'], 'refund_time' => Carbon::parse($decryptedData['success_time']) ]); // 更新订单状态为已退款 $order->update([ 'state' => OrderStatus::REFUNDED->value, 'refund_time' => Carbon::parse($decryptedData['success_time']) ]); // 创建订单记录 $order->records()->create([ 'object_id' => $order->user_id, 'object_type' => MemberUser::class, 'state' => OrderRecordStatus::REFUNDED->value, 'remark' => '订单退款成功' ]); Log::info('退款成功,订单状态已更新', [ 'order_id' => $order->id, 'refund_no' => $decryptedData['out_refund_no'] ]); }); } return response()->json(['code' => 'SUCCESS', 'message' => 'OK']); } catch (\Exception $e) { Log::error('退款回调解密失败', [ 'error' => $e->getMessage(), 'trace' => $e->getTraceAsString() ]); throw $e; } } catch (\Exception $e) { Log::error('处理退款回调异常', [ 'error' => $e->getMessage(), 'trace' => $e->getTraceAsString(), 'request' => [ 'headers' => $request->headers->all(), 'content' => $request->getContent() ] ]); return response()->json(['code' => 'SUCCESS', 'message' => 'OK']); // 即使处理失败也返回成功,避免微信重复通知 } } }