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输出结果 } } }
问题分析
- 前端漏洞:仅靠SweetAlert弹窗无法完全阻止重复提交——网络卡顿、事件队列异常、弹窗渲染延迟等极端场景下,仍可能触发多次AJAX请求;且未设置请求锁定状态,也未给AJAX配置超时,导致请求无限制等待。
- 后端缺失幂等性校验:交易场景下,即使前端做了限制,网络重传、客户端重试等情况仍会导致重复请求,后端必须自己保证同一请求不会被重复执行。
- 事件绑定风险:使用
$(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
相关产品推荐
相关产品推荐

