解决类PayPal支付平台快速点击购买导致重复支付与下单问题
解决重复支付记录问题的方案
针对用户快速多次点击购买按钮导致重复生成Income和OrderNew记录的问题,可从前端拦截、后端幂等校验、数据库约束三个核心层面解决:
一、前端拦截:从源头阻止重复点击
最直接的方式是在用户点击后立即禁用按钮或添加加载状态,避免重复触发请求:
const buyBtn = document.getElementById('purchase-btn'); buyBtn.addEventListener('click', async () => { // 点击后禁用按钮并显示加载状态 buyBtn.disabled = true; buyBtn.textContent = '处理中,请稍候...'; try { // 发起支付请求 const formData = new FormData(document.getElementById('payment-form')); const res = await fetch('/payment/process', { method: 'POST', body: formData, headers: { 'X-CSRF-TOKEN': document.querySelector('meta[name="csrf-token"]').content } }); // 根据响应跳转 if (res.ok) { window.location.href = '/payment/success'; } else { throw new Error('支付处理失败'); } } catch (err) { alert(err.message); // 失败后恢复按钮状态(可选) buyBtn.disabled = false; buyBtn.textContent = '立即购买'; } });
二、后端幂等校验:确保同一支付只处理一次
这是核心解决方案,利用MercadoPago返回的唯一order->id做幂等校验,在插入记录前先检查是否已处理过该支付:
修改后端处理逻辑
public function invoke(Request $request) { $payment_id = $request->get('payment_id'); $credenciales = config('services.mercadopago.token'); $user_id = $request->get('user_id'); // 直接从请求获取,避免依赖Session $response = Http::get("https://api.mercadopago.com/v1/payments/$payment_id?access_token=$credenciales"); $response = json_decode($response); $status = $response->status; if($status == 'approved') { // 关键:检查该支付订单是否已处理(用mp_id即MercadoPago的order_id做唯一标识) $hasProcessed = Income::where('mp_id', $response->order->id)->exists(); if ($hasProcessed) { // 已处理过,直接跳转成功页 return redirect()->route('payment.success'); } // 用事务包裹插入操作,确保数据一致性 DB::beginTransaction(); try { // 插入Income记录 Income::insert([ 'user_id' => $user_id, 'evento_id' => $request->get('variableName'), 'mp_id' => $response->order->id, 'metodo' => $response->payment_type_id, 'monto' => $response->transaction_details->total_paid_amount, 'rrpp_id' => $request->get('rrpp'), 'ingreso' => $response->transaction_details->net_received_amount, ]); // 插入OrderNew记录 OrderNew::insert([ 'user_id' => $user_id, 'dia_id' => $request->get('variableName'), 'whatsapp' => $request->get('telefono'), 'cantidad' => $request->get('cantidad'), 'anticipada' => $request->get('anticipada'), 'horario' => $request->get('horario'), 'rrpp' => $request->get('rrpp'), 'pagado' => '1', 'tipo' => 'vip', 'codigo' => rand(1580, 4005), ]); DB::commit(); } catch (\Exception $e) { DB::rollBack(); return redirect()->route('payment.error')->with('error', '支付处理失败,请重试'); } } return redirect()->route('payment.success'); }
三、数据库约束:最后一道防线
给Income表的mp_id字段添加唯一索引,即使前端和后端校验失效,数据库也会拒绝重复插入:
-- 给incomes表添加mp_id唯一索引 ALTER TABLE incomes ADD UNIQUE INDEX unique_mp_id (mp_id);
也可以根据业务需求,给OrderNew表添加复合唯一索引(比如user_id + dia_id + whatsapp),避免同一用户重复生成相同订单。
额外优化建议
- 避免依赖Session传递支付数据:直接从请求或MercadoPago响应中获取参数,减少Session共享导致的重复提交风险;
- 启用Laravel的请求频率限制:给支付路由添加
throttle中间件,限制短时间内的请求次数;
// 在路由中添加 Route::post('/payment/invoke', [PaymentController::class, 'invoke'])->middleware('throttle:3,1');
内容的提问来源于stack exchange,提问作者Jeremias Rosas
相关产品推荐
相关产品推荐

