PHP第三方支付回调URL重复请求问题排查求助
问题背景
集成第三方支付时,用户完成支付后点击「DONE」按钮会跳转至我方回调URL,但用户多次点击会导致回调收到重复请求。已尝试用connection_aborted()和缓存机制处理,但出现异常:首次请求会进入processTrasactionPayment方法,后续请求能被日志拦截,但首次请求完成后,该方法中加载的页面无法正常显示。
现有代码
回调控制器代码
<?php try { switch ($action) { case 'action_1': // some code... break; case 'paymentResponseHandler': $transactionId = request()->get('txn_id'); $publicToken = request()->get('public_token'); $cacheKey = $transactionId . '_cache'; if (isValidAndActiveTxn($transactionId)) { if (!empty($publicToken) && isValidToken($publicToken)) { echo ""; ob_flush(); flush(); if (!connection_aborted()) { if (\Cache::me()->get($cacheKey) != $publicToken) { \Cache::me()->set($cacheKey, $publicToken, 60); $this->paymentService->processTrasactionPayment($transactionId, $publicToken); } else { log_info("Transaction already processed for Public Token: {$publicToken}"); } } else { log_info("Multiple Connection Aborted for Public Token: {$publicToken}"); } } else { log_info("Invalid Payment Token {$publicToken}"); return 'Invalid Payment Token'; } } else { log_info("Invalid Transaction {$transactionId}"); return 'Invalid Transaction'; } break; default: // some code... break; } } catch (\Exception $e) { // Exception handling code... }
支付服务代码
<?php namespace App\Services\Payment; class PaymentService { // some code... public function processTrasactionPayment($transactionId, $publicToken): void { try { log_info("processTrasactionPayment() initiated", ['transactionId' => $transactionId, 'publicToken' => $publicToken]); $apiResponse = $this->callSomeApi($transactionId, $publicToken); log_info("API call done.", ['apiResponse' => $apiResponse]); if (!empty($apiResponse) && $apiResponse['response']) { require PAGE_PATH . "payments/header.php"; require PAGE_PATH . "payments/pre-loader.php"; require PAGE_PATH . "payments/post-payment-process.php"; require PAGE_PATH . "payments/footer.php"; log_info("Success page loaded."); } else { log_error('Transaction failed'); require PAGE_PATH . 'payments/transaction-failed.php'; } } catch (Exception $e) { log_error('Exception in processTrasactionPayment()', $e); require PAGE_PATH . 'payments/transaction-failed.php'; } } // some code... }
问题根源分析
响应提前终止导致页面无法渲染
你在回调中执行了echo ""; ob_flush(); flush();,这会立即向浏览器发送一个空的HTTP响应(包含响应头和空内容)。浏览器收到响应后会认为请求已完成,后续processTrasactionPayment中通过require输出的HTML内容会被当作无效的后续数据,无法被正确解析和显示。重复请求拦截逻辑存在漏洞
用publicToken作为缓存值来判断是否重复处理,逻辑不准确:同一交易的重复回调可能携带相同的publicToken,但如果第三方返回的token有变化,这个判断会失效。更合理的方式应该标记交易ID的处理状态,而非token。connection_aborted()的局限性
该函数仅在调用时检查连接状态,无法覆盖后续处理过程中用户断开连接的场景,且对并发重复请求的拦截效果有限。
解决方案建议
1. 修复响应输出逻辑,移除提前flush操作
删除echo ""; ob_flush(); flush();这三行代码,确保完整的HTML页面能作为一个整体的HTTP响应发送给浏览器。
2. 优化重复请求拦截机制(基于交易ID的幂等处理)
改用交易ID标记处理状态,结合缓存锁确保同一交易只有一个请求进入处理流程:
// 修改回调控制器中的paymentResponseHandler分支 case 'paymentResponseHandler': $transactionId = request()->get('txn_id'); $publicToken = request()->get('public_token'); $processedKey = "txn_processed_{$transactionId}"; $lockKey = "txn_lock_{$transactionId}"; if (isValidAndActiveTxn($transactionId)) { if (!empty($publicToken) && isValidToken($publicToken)) { // 先检查是否已处理过 if (\Cache::me()->get($processedKey)) { log_info("Transaction {$transactionId} already processed"); // 直接输出成功页面,避免重复处理 require PAGE_PATH . "payments/header.php"; require PAGE_PATH . "payments/pre-loader.php"; require PAGE_PATH . "payments/post-payment-process.php"; require PAGE_PATH . "payments/footer.php"; return; } // 获取分布式锁,防止并发请求同时进入处理 if (\Cache::me()->add($lockKey, 1, 30)) { // 锁有效期30秒,根据处理耗时调整 try { $this->paymentService->processTrasactionPayment($transactionId, $publicToken); // 标记交易已处理 \Cache::me()->set($processedKey, true, 86400); // 缓存1天 } finally { // 释放锁 \Cache::me()->delete($lockKey); } } else { // 有其他请求正在处理,直接输出等待或结果页 log_info("Transaction {$transactionId} is being processed by another request"); require PAGE_PATH . "payments/processing.php"; } } else { log_info("Invalid Payment Token {$publicToken}"); require PAGE_PATH . 'payments/transaction-failed.php'; } } else { log_info("Invalid Transaction {$transactionId}"); require PAGE_PATH . 'payments/transaction-failed.php'; } break;
3. 确保支付处理逻辑的幂等性
在processTrasactionPayment中,先检查交易当前状态,避免重复执行核心业务逻辑:
public function processTrasactionPayment($transactionId, $publicToken): void { try { // 先检查交易是否已经处理完成 $currentStatus = $this->getTransactionStatus($transactionId); if ($currentStatus === 'SUCCESS') { log_info("Transaction {$transactionId} already succeeded"); require PAGE_PATH . "payments/header.php"; require PAGE_PATH . "payments/pre-loader.php"; require PAGE_PATH . "payments/post-payment-process.php"; require PAGE_PATH . "payments/footer.php"; return; } log_info("processTrasactionPayment() initiated", ['transactionId' => $transactionId, 'publicToken' => $publicToken]); $apiResponse = $this->callSomeApi($transactionId, $publicToken); log_info("API call done.", ['apiResponse' => $apiResponse]); if (!empty($apiResponse) && $apiResponse['response']) { // 更新交易状态为成功 $this->updateTransactionStatus($transactionId, 'SUCCESS'); require PAGE_PATH . "payments/header.php"; require PAGE_PATH . "payments/pre-loader.php"; require PAGE_PATH . "payments/post-payment-process.php"; require PAGE_PATH . "payments/footer.php"; log_info("Success page loaded."); } else { log_error('Transaction failed'); $this->updateTransactionStatus($transactionId, 'FAILED'); require PAGE_PATH . 'payments/transaction-failed.php'; } } catch (Exception $e) { log_error('Exception in processTrasactionPayment()', $e); $this->updateTransactionStatus($transactionId, 'FAILED'); require PAGE_PATH . 'payments/transaction-failed.php'; } }
4. 可选:使用异步处理优化用户体验
如果交易处理耗时较长,可以将核心业务逻辑放入异步队列,回调接口立即返回成功页面,后台异步处理交易结果。这样用户无需等待,也能避免重复请求的影响。
内容的提问来源于stack exchange,提问作者Aakash Martand

