javascript技巧

关注公众号 jb51net

关闭
首页 > 网络编程 > JavaScript > javascript技巧 > 前端防止重复支付

前端防止重复支付解决方案完整记录

作者:sophie旭

前端方法能防止用户误操作,但不能完全依赖,因为可以通过工具绕过前端验证,这篇文章主要介绍了前端防止重复支付解决方案的相关资料,文中通过代码介绍的非常详细,需要的朋友可以参考下

背景

这期并不是什么高大上的主题,但是对于支付业务却是尤为重要,那就是如何在前端角度防止重复支付,在这边把我的解决方案记录下来,也想剖析一下里面的细节,同时也分享给大家。

解决方案

// 假设使用 lodash 的 throttle 函数
import { throttle } from 'lodash';

// 定义 loading 状态(Vue 中可放在 data/setup 中)
let isPayLoading = false;

// 支付核心函数
const goToPay = async () => {
  // 1. Loading 状态锁:拦截重复请求
  if (isPayLoading) return;
  
  try {
    // 2. 开启 loading(按钮置灰、显示加载动画)
    isPayLoading = true;
    
    // 3. 执行支付请求(示例接口)
    const res = await fetch('/api/pay', {
      method: 'POST',
      body: JSON.stringify({ orderId: '123456' })
    });
    
    const data = await res.json();
    if (data.code === 200) {
      alert('支付成功!');
    } else {
      alert('支付失败:' + data.msg);
    }
  } catch (err) {
    alert('支付请求异常:' + err.message);
  } finally {
    // 4. 无论成功/失败,关闭 loading
    isPayLoading = false;
  }
};

// 节流包装支付函数:拦截快速点击
const throttlePay = throttle(
  () => {
    goToPay();
  },
  2000,
  { leading: true, trailing: false }
);

// 支付按钮点击事件绑定这个节流后的函数
// <button onclick="throttlePay()" :disabled="isPayLoading">支付</button>

一、先理清整体逻辑

你这段代码的核心是:

二、防止重复支付的核心原因

重复支付的本质是:用户短时间内多次点击“支付”按钮,导致多次触发支付请求,后端可能接收到多个支付指令,最终造成重复扣款。而节流 + loading 的组合从不同维度阻断了这个问题:

1. 节流(throttle)的作用:阻断“快速连续触发”

2. Loading 控制的作用:阻断“请求未完成时的触发”

goToPay 中加 loading 通常是这样的逻辑:

// 示例:goToPay 核心逻辑
let isLoading = false; // 全局/局部的 loading 状态
function goToPay() {
  // 1. 如果正在加载中,直接返回,不执行后续逻辑
  if (isLoading) return;
  
  // 2. 开启 loading(按钮置灰、显示加载动画)
  isLoading = true;
  
  // 3. 执行支付请求
  payRequest()
    .then(res => {
      // 支付成功逻辑
    })
    .catch(err => {
      // 支付失败逻辑
    })
    .finally(() => {
      // 4. 请求完成(成功/失败),关闭 loading
      isLoading = false;
    });
}

三、设计上的巧妙之处

节流 + loading 是“互补式”设计,完美解决了单一方案的不足,核心巧妙点有 3 个:

1. 分层防护:前端“触发层” + “执行层”双重拦截

2. 体验与安全兼顾

3. 容错性强:适配不同网络/场景

4. 2秒时长的核心价值

我把节流时长设为2秒,而非1秒或更短,本质是给“异步loading”留足“兜底容错时间”

1. 为什么短时长(比如500ms)会有重复点击风险?

POS机和普通浏览器不同,它的特点是:

2. 2秒时长的“兜底作用”

2秒是一个「足够覆盖POS机异步操作最大延时」的安全值:

总结

核心关键点回顾:

  1. 双重防护逻辑:节流控制「触发频率」(防快速点击),loading 控制「请求状态」(防请求中点击),覆盖所有重复支付场景;
  2. 巧妙的设计互补:节流保证“点击即时响应”的体验,loading 作为“兜底保障”适配不确定的请求耗时;
  3. 体验与安全兼顾:loading 既是防重复的逻辑锁,也是给用户的视觉反馈,减少重复点击的动机。

节流防抖 傻傻分不清楚

一、先明确核心需求:支付场景的本质要求

支付按钮的核心诉求是:

  1. 用户点击后必须立即执行支付逻辑(不能等、不能吞掉用户的点击);
  2. 短时间内(比如2秒)多次点击,只能执行一次(防止重复支付);
  3. 2秒后再次点击,仍能正常执行(用户第一次支付失败,2秒后可以重新点击)。

这三个诉求是判断用节流还是防抖的关键,我们先对比两者的核心差异:

特性节流 (throttle)防抖 (debounce)
核心逻辑「固定时间窗口内只能执行一次」,像水流一样匀速通过「等待最后一次触发后,延迟执行」,像弹簧一样松手才回弹
触发时机窗口内第一次触发(leading: true)立即执行只有停止触发后,等待指定时间才执行
多次触发的结果窗口内只执行一次,窗口过期后可再次执行只要一直在触发,就永远不执行

二、为什么这里用节流,而不是防抖?

1. 防抖完全不符合支付场景的核心诉求

假设把代码中的 throttle 换成 debounce,参数同样设为2秒:

// 错误示例:用防抖包装支付函数
const debouncePay = debounce(() => {
  goToPay()
}, 2000);

会出现两个致命问题:

简单说:防抖的核心是「等用户停手后再执行」,而支付需要「用户动手就立即执行,且短时间内只执行一次」,两者的核心逻辑完全相悖。

2. 节流完美匹配支付场景的诉求

你代码中的节流配置 { leading: true, trailing: false } 刚好命中支付需求:

三、补充:什么时候才会用防抖?

防抖的适用场景是「需要等待用户操作结束后再执行」的场景,比如:

  1. 搜索框输入联想(等用户输完关键词,再发请求查联想词,避免边输边发请求);
  2. 窗口大小调整(等用户拖完窗口,再执行布局重绘,避免频繁重绘);
  3. 手机号/验证码输入校验(等用户输完,再校验格式,避免边输边提示错误)。

这些场景的核心是“不着急执行,等用户停手再执行”,和支付“必须立即执行”的诉求完全相反。

总结

核心关键点回顾:

  1. 核心逻辑差异:节流是「固定时间内只执行一次」,保证触发即响应;防抖是「等最后一次触发后延迟执行」,会吞掉中间的触发;
  2. 场景匹配度:支付需要“点击立即执行+短时间防重复”,节流刚好满足,防抖会导致“点击不立即响应”甚至“永远不执行”;
  3. 记忆技巧:节流=“控制频率”(多久执行一次),防抖=“等待结束”(停手才执行),支付场景要“控频率”而非“等结束”。

防抖/节流 设计的巧妙之处

一、核心设计巧思:用「状态管理」驯服高频触发

防抖和节流的本质,是通过管理“唯一状态” 把「无规律的高频触发」转化为「可控的低频执行」,这是最核心的巧妙之处:

1. 防抖:用「定时器状态」实现“等待最后一次”

2. 节流:用「开关/时间戳状态」实现“频率控制”

二、场景适配巧思:既解决技术问题,又贴合「用户行为」

防抖和节流的设计不只是“技术层面的优化”,更精准适配了「人类操作的特点」,这是容易被忽略的巧妙之处:

1. 防抖:贴合“用户需要完成操作后再反馈”的行为

2. 节流:贴合“用户需要即时反馈,但不能太频繁”的行为

3. 可配置化扩展:兼顾“通用性”和“个性化”

优秀的防抖/节流实现(比如lodash版)还会设计leading(是否立即执行)、trailing(是否延迟执行)等参数,比如:

三、实现细节巧思:最小侵入性+无副作用

防抖/节流的设计还藏着很多“细节上的巧思”,保证了函数的健壮性和易用性:

1. 保留this和参数:无副作用封装

// 核心代码片段
return function(...args) {
  fn.apply(this, args); // 关键:绑定原函数的this和参数
};

2. 支持取消:应对极端场景

// 防抖扩展:添加取消功能
function debounce(fn, delay) {
  let timer = null;
  const debounced = function(...args) {
    clearTimeout(timer);
    timer = setTimeout(() => fn.apply(this, args), delay);
  };
  // 新增取消方法
  debounced.cancel = function() {
    clearTimeout(timer);
    timer = null;
  };
  return debounced;
}

3. 无全局污染:状态私有化

总结

防抖/节流设计的核心巧妙点回顾:

  1. 极简状态管理:只用一个核心状态(定时器/开关/时间戳),就驯服了高频触发,逻辑简单且性能开销极低;
  2. 贴合用户行为:不是单纯的技术优化,而是精准适配人类操作特点(防抖等结束、节流控频率),兼顾性能和体验;
  3. 无侵入性封装:保留原函数的this和参数,状态私有化,支持扩展(取消、配置参数),做到“通用、无副作用、可扩展”。

防抖/节流 实现如何快速记住

记「极简固定模板」(只记核心结构,不用记细节)

我帮你提炼了“万能模板”,核心代码只有几行,记模板比记零散代码容易10倍:

1. 防抖(debounce)模板(核心:重置定时器)

模板逻辑

手写代码(带注释,只记标★的核心行)

// 防抖函数:fn=目标函数,delay=延迟时间
function debounce(fn, delay) {
  let timer = null; // ★ 唯一状态:定时器(电梯的“等待倒计时”)
  
  // 返回包装后的函数(用户每次点击/触发都会执行这个函数)
  return function(...args) {
    // ★ 核心1:触发时先清旧定时器(按关门键,重置2秒等待)
    clearTimeout(timer);
    
    // ★ 核心2:设新定时器,延迟后执行目标函数(等2秒,没人按就关门)
    timer = setTimeout(() => {
      fn.apply(this, args); // 保留this和参数(适配实际场景)
    }, delay);
  };
}

简化记忆:防抖=「清旧定时器→设新定时器」,就这两步核心,其他都是适配性代码(apply是为了绑定this,可后期补)。

2. 节流(throttle)模板(核心:判断冷却期)

模板逻辑

手写代码(两种常见写法,记一种就行,推荐第一种)

// 节流函数:fn=目标函数,delay=冷却时间
function throttle(fn, delay) {
  let canRun = true; // ★ 唯一状态:冷却开关(闸机的“是否可用”)
  
  return function(...args) {
    // ★ 核心1:冷却期内,直接返回(闸机不可用,刷了也白刷)
    if (!canRun) return;
    
    // ★ 核心2:关闭开关,进入冷却(闸机用一次,锁2秒)
    canRun = false;
    // 执行目标函数(闸机开门)
    fn.apply(this, args);
    
    // ★ 核心3:延迟后打开开关(2秒后闸机恢复可用)
    setTimeout(() => {
      canRun = true;
    }, delay);
  };
}

简化记忆:节流=「判断开关→关开关→执行→延迟开开关」,核心是“开关控制冷却期”。

补充:节流的另一种写法(按时间戳,逻辑一致)

如果面试官让用时间戳写,只是“冷却期判断方式”变了,核心还是“控冷却”:

function throttle(fn, delay) {
  let lastTime = 0; // 上次执行时间(替代canRun)
  
  return function(...args) {
    const now = Date.now();
    // 核心:判断当前时间 - 上次执行时间 ≥ 延迟时间(冷却期过了)
    if (now - lastTime >= delay) {
      fn.apply(this, args);
      lastTime = now; // 更新上次执行时间(关开关)
    }
  };
}

记忆:时间戳写法只是把“开关”换成了“时间差判断”,核心还是“冷却期内不执行”。

四、第三步:记「唯一差异点」(避免混淆)

防抖和节流的代码就一个核心区别,记死这一点,永远不会混:

对比项防抖 (debounce)节流 (throttle)
核心操作每次触发都「清定时器」(重置)触发时先「判断冷却期」(拦截)
定时器作用延迟执行目标函数延迟结束冷却期
执行时机停止触发后延迟执行触发时立即执行(冷却期过的话)

一句话总结差异

五、记忆技巧:3分钟快速默写训练(每天练1次,3天就记住)

不用死背,按这个步骤练,每次只花3分钟:

  1. 第一步(1分钟):先默念场景→防抖=电梯、节流=闸机;
  2. 第二步(1分钟):写核心模板(只写标★的行):
    • 防抖:let timer → clearTimeout(timer) → timer = setTimeout(...);
    • 节流:let canRun=true → if(!canRun)return → canRun=false → setTimeout(开canRun);
  3. 第三步(1分钟):补全适配代码(apply(this, args))。

练3次后,你会发现:不用记完整代码,只要写出核心逻辑,剩下的都是“填空”。

六、完整可运行代码(对照练)

最后给你完整的防抖+节流代码,练的时候对照:

// 1. 防抖函数(带立即执行可选参数,进阶版,先记基础版,再补这个)
function debounce(fn, delay, immediate = false) {
  let timer = null;
  return function(...args) {
    // 每次触发清旧定时器
    clearTimeout(timer);
    
    // 立即执行版(可选,基础版不用记这个)
    if (immediate && !timer) {
      fn.apply(this, args);
    }
    
    // 设新定时器
    timer = setTimeout(() => {
      fn.apply(this, args);
      timer = null; // 重置timer,方便immediate判断
    }, delay);
  };
}

// 2. 节流函数(开关版,最易记)
function throttle(fn, delay) {
  let canRun = true;
  return function(...args) {
    if (!canRun) return;
    canRun = false;
    fn.apply(this, args);
    setTimeout(() => {
      canRun = true;
    }, delay);
  };
}

// 测试用例(练完可以跑一下,加深印象)
// 防抖测试:连续点击,只在最后一次点击后1秒执行
const debounceClick = debounce(() => console.log('防抖执行'), 1000);
// 节流测试:连续点击,每1秒执行一次
const throttleClick = throttle(() => console.log('节流执行'), 1000);

总结

核心关键点回顾:

  1. 记锚点:防抖=搜索框输入(重置等待),节流=闸机(冷却期),先想场景再想代码;
  2. 记模板:防抖核心是「清旧定时器→设新定时器」,节流核心是「判断开关→关开关→延迟开开关」;
  3. 记差异:防抖是“重置时间”,节流是“控制频率”,核心操作一个清定时器、一个判断冷却期。

到此这篇关于前端防止重复支付解决方案的文章就介绍到这了,更多相关前端防止重复支付内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

您可能感兴趣的文章:
阅读全文