You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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...
}

问题根源分析

  1. 响应提前终止导致页面无法渲染
    你在回调中执行了echo ""; ob_flush(); flush();,这会立即向浏览器发送一个空的HTTP响应(包含响应头和空内容)。浏览器收到响应后会认为请求已完成,后续processTrasactionPayment中通过require输出的HTML内容会被当作无效的后续数据,无法被正确解析和显示。

  2. 重复请求拦截逻辑存在漏洞
    用publicToken作为缓存值来判断是否重复处理,逻辑不准确:同一交易的重复回调可能携带相同的publicToken,但如果第三方返回的token有变化,这个判断会失效。更合理的方式应该标记交易ID的处理状态,而非token。

  3. 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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.19 02:14:58