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

PHP+jQuery AJAX表单偶发重复提交问题求助(10次中有2-3次)

解决PHP+jQuery AJAX表单偶发重复提交问题

问题背景

我开发的PHP+jQuery AJAX表单在提交时,10次请求里有8次能正常工作,但剩余请求会触发2-3次重复提交,却仅返回单个响应用于SweetAlert弹窗展示。已通过SweetAlert弹窗防止用户重复点击提交按钮,但问题仍存在。由于该问题偶发,无法通过浏览器Network标签页实时监控,且该表单用于交易场景,亟需解决。

前端提交代码

<script>
    $(document).on('click','#userForm',function(e) {
        e.preventDefault();

        var subscriptio = $('#subscription option:selected').text();
        var subscripti = $('#product option:selected').text();
        swal.fire({
            title: 'Dear  $username',
            text: 'Should we process your ' +subscriptio+ ' request?',
            icon: 'question',
            showCancelButton: true,
            confirmButtonColor: '#3085d6',
            cancelButtonColor: '#d33',
            confirmButtonText: 'Yes, Continue',
        }).then((result) => {
            if (result.value){
                var data = $('#user_form').serialize();
                console.log(data);
                $.ajax({
                    data: data,
                    type: 'POST',
                    url: 'action.php',
                    cache: false, 
                }).done(function(data){  
                    psrt();
                    $('#security').val('');
                    var dataResult = JSON.parse(data);
                    if (dataResult.status=='successful'){ 
                        swal.fire(dataResult.status, dataResult.message, dataResult.icon);
                    }else {
                        swal.fire(dataResult.status, dataResult.message, dataResult.icon); 
                    }     
                }).fail(function(){
                    swal.fire('Oops!', 'Something went wrong, kindly contact Admin', 'error'); 
                });
            } 
        }) 
    }); 
</script>

后端action.php相关代码

$mobiler = isset($_POST['mobile']) ? isints($_POST['mobile']) : '';
$mobile = in_array($mobiler, $wizz)? false :$mobiler;

//若$mobile为false,此处有拒绝并返回响应的代码

$postrayer = begins($userid, $wallet, $chan, $mypin,  $mobile, $amountsplit);  

If ($postrayer){           
    //apiFunction是用于发送HTTP请求的函数,定义如下

    function apiFunction ($url,$payload,$token){

        $ch = curl_init();

        curl_setopt($ch, CURLOPT_URL, $url);

        curl_setopt($ch, CURLOPT_POST, 1);

        curl_setopt($ch, CURLOPT_POSTFIELDS, $payload);  

        curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);

        $headers = [

             "Authorization:$token",

            'Content-Type: application/json'

        ];

        curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);

        $dataapi = curl_exec($ch);

        curl_close($ch);

        
        return $dataapi;
    }

    $fg = apiFunction ($url,$payload,$token);
    $api_result = json_decode($fg,true);
    if(!empty($api_result)){
        If($api_result['Status'] == 'successful' ){
            // 此处向SweetAlert输出结果
        }
    }  
}

问题分析

  1. 前端漏洞:仅靠SweetAlert弹窗无法完全阻止重复提交——网络卡顿、事件队列异常、弹窗渲染延迟等极端场景下,仍可能触发多次AJAX请求;且未设置请求锁定状态,也未给AJAX配置超时,导致请求无限制等待。
  2. 后端缺失幂等性校验:交易场景下,即使前端做了限制,网络重传、客户端重试等情况仍会导致重复请求,后端必须自己保证同一请求不会被重复执行。
  3. 事件绑定风险:使用$(document).on('click','#userForm',...)绑定事件,若DOM中存在重复ID元素(虽然ID应唯一,但可能存在渲染错误),会触发多次点击事件。

修复方案

前端修复:添加请求锁定与交互限制

<script>
    // 全局标记是否正在提交
    let isSubmitting = false;

    // 生成唯一请求ID(用于后端幂等校验)
    function generateUUID() {
        return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) {
            var r = Math.random() * 16 | 0, v = c == 'x' ? r : (r & 0x3 | 0x8);
            return v.toString(16);
        });
    }

    $(document).on('click','#userForm',function(e) {
        e.preventDefault();
        e.stopPropagation(); // 阻止事件冒泡

        // 正在提交则直接返回
        if (isSubmitting) return;

        var subscriptio = $('#subscription option:selected').text();
        var subscripti = $('#product option:selected').text();
        swal.fire({
            title: 'Dear <?php echo $username; ?>',
            text: 'Should we process your ' + subscriptio + ' request?',
            icon: 'question',
            showCancelButton: true,
            confirmButtonColor: '#3085d6',
            cancelButtonColor: '#d33',
            confirmButtonText: 'Yes, Continue',
            allowOutsideClick: false, // 禁止点击外部关闭弹窗
            allowEscapeKey: false // 禁止ESC关闭弹窗
        }).then((result) => {
            if (result.isConfirmed){ // SweetAlert2推荐用isConfirmed替代value
                isSubmitting = true;
                // 添加唯一请求ID到表单数据
                var data = $('#user_form').serialize() + '&request_id=' + generateUUID();
                console.log(data);
                
                $.ajax({
                    data: data,
                    type: 'POST',
                    url: 'action.php',
                    cache: false,
                    timeout: 15000, // 设置15秒超时
                    beforeSend: function() {
                        // 禁用提交按钮并显示加载状态
                        $('#userForm').prop('disabled', true).text('Processing...');
                    }
                }).done(function(data){  
                    psrt();
                    $('#security').val('');
                    var dataResult = JSON.parse(data);
                    swal.fire(dataResult.status, dataResult.message, dataResult.icon);
                }).fail(function(){
                    swal.fire('Oops!', 'Something went wrong, kindly contact Admin', 'error'); 
                }).always(function() {
                    // 请求完成后重置状态
                    isSubmitting = false;
                    $('#userForm').prop('disabled', false).text('Submit');
                });
            } 
        }); 
    }); 
</script>

后端修复:实现幂等性校验(交易场景核心)

通过唯一请求ID记录请求状态,确保同一请求仅被处理一次:

// 初始化数据库连接(根据实际环境调整)
$pdo = new PDO('mysql:host=localhost;dbname=your_database','username','password');
$pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);

// 1. 接收并校验前端传来的请求ID
$requestId = isset($_POST['request_id']) ? trim($_POST['request_id']) : '';
if(empty($requestId) || strlen($requestId) !== 36){
    echo json_encode(['status'=>'error','message'=>'Invalid request','icon'=>'error']);
    exit;
}

// 2. 开启事务,防止并发请求绕过校验
$pdo->beginTransaction();
try {
    // 检查请求ID是否已处理
    $stmt = $pdo->prepare("SELECT id FROM request_logs WHERE request_id = ? LIMIT 1");
    $stmt->execute([$requestId]);
    if($stmt->rowCount() > 0){
        // 返回已处理的结果(可根据需求查询历史结果返回)
        echo json_encode(['status'=>'successful','message'=>'Request already processed','icon'=>'info']);
        $pdo->commit();
        exit;
    }

    // 3. 执行原业务逻辑
    $mobiler = isset($_POST['mobile']) ? isints($_POST['mobile']) : '';
    $mobile = in_array($mobiler, $wizz)? false :$mobiler;

    if(!$mobile){
        $result = ['status'=>'error','message'=>'Invalid mobile','icon'=>'error'];
        // 记录请求日志
        $stmt = $pdo->prepare("INSERT INTO request_logs (request_id, status, result, created_at) VALUES (?, ?, ?, NOW())");
        $stmt->execute([$requestId, $result['status'], json_encode($result)]);
        $pdo->commit();
        echo json_encode($result);
        exit;
    }

    $postrayer = begins($userid, $wallet, $chan, $mypin,  $mobile, $amountsplit);  
    $result = [];
    if ($postrayer){           
        function apiFunction ($url,$payload,$token){
            $ch = curl_init();
            curl_setopt($ch, CURLOPT_URL, $url);
            curl_setopt($ch, CURLOPT_POST, 1);
            curl_setopt($ch, CURLOPT_POSTFIELDS, $payload);  
            curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
            // 添加curl超时配置
            curl_setopt($ch, CURLOPT_TIMEOUT, 10);
            curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 5);
            $headers = [
                 "Authorization:$token",
                'Content-Type: application/json'
            ];
            curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
            $dataapi = curl_exec($ch);
            curl_close($ch);
            return $dataapi;
        }

        $fg = apiFunction ($url,$payload,$token);
        $api_result = json_decode($fg,true);
        if(!empty($api_result)){
            if($api_result['Status'] == 'successful' ){
                $result = ['status'=>'successful','message'=>'Transaction completed','icon'=>'success'];
            } else {
                $result = ['status'=>'error','message'=>'API failed','icon'=>'error'];
            }
        } else {
            $result = ['status'=>'error','message'=>'No API response','icon'=>'error'];
        }
    } else {
        $result = ['status'=>'error','message'=>'Initial check failed','icon'=>'error'];
    }

    // 4. 记录请求日志
    $stmt = $pdo->prepare("INSERT INTO request_logs (request_id, status, result, created_at) VALUES (?, ?, ?, NOW())");
    $stmt->execute([$requestId, $result['status'], json_encode($result)]);
    $pdo->commit();
    echo json_encode($result);
} catch(Exception $e){
    $pdo->rollBack();
    echo json_encode(['status'=>'error','message'=>'Server error','icon'=>'error']);
}

注:需先创建request_logs表,字段包括id(主键)、request_id(唯一索引)、status、result、created_at

额外优化建议

  • 前端:确保DOM中#userForm元素唯一,避免重复事件绑定
  • 后端:定期清理request_logs表中的过期记录,避免数据冗余
  • 日志:在后端添加详细的请求日志,包括请求参数、响应结果、执行时间,方便排查偶发问题

内容的提问来源于stack exchange,提问作者Adetiba Oluwasola Daniel

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.24 10:14:55