线上缺陷
跟夏天赶蚊子一样,这是一类非常让人头疼和烦恼的问题。业务人员在操作过程中点击提交之后,页面依旧停留在了待提交界面,于是又提交了一次。系统接受了来自前端的提交,并向前端返回了2条“提交成功”的响应,并在数据库中插入了两条待处理的业务记录(内容相同)。由于这个业务同一时间内只能做一次,在后续处理过程中就报错了。
此类问题虽然不会有严重的业务影响,但是此类问题跟打地鼠一样频繁、重复、跨团队地出现,也暴露出了开发、测试团队的不少问题。

问题产生原因
那么为什么会产生预期外的重复提交的问题呢? 可能有用户、系统实现、业务逻辑和测试等几个方面的原因。
一、用户操作层面的无意行为
1. 误触或操作失误
o 用户因手滑、点击过快(如双击代替单击)、对操作反馈不明确而重复点击(例如按钮点击后无 Loading 提示,用户误以为未触发)。
o 移动端或触摸屏设备上,因触控灵敏度问题可能导致一次物理操作被识别为多次点击。
2. 等待焦虑与重复尝试
o 当请求响应缓慢(如网络延迟、后端处理耗时),用户未看到明确的 “提交中” 反馈时,可能会多次点击按钮试图 “确认操作生效”。
o 对操作结果存疑(如表单提交后未跳转 / 无提示),用户可能重复提交以验证是否成功。
二、技术与系统层面的潜在问题
1. 前端状态管理漏洞
o 按钮禁用逻辑未覆盖所有场景(如异步请求发起后未立即禁用按钮,导致短时间内多次触发)。
o 前端状态未正确重置(如请求失败后,“提交中” 状态未解除,或禁用的按钮未恢复,可能迫使用户刷新页面后再次提交,导致重复)。
2. 网络与请求特性
o 网络波动导致请求 “延迟到达”:用户首次提交后因网络延迟未收到响应,再次提交,最终两个请求都被后端接收。
o 重复请求未被拦截:如前端未使用防抖、节流或 Token 机制,导致同一请求被多次发送(尤其在高频操作场景下)。
3. 浏览器或设备特性
o 浏览器后退 / 刷新操作:用户提交后通过后退按钮返回表单页,再次提交相同内容(可能因缓存导致表单数据未清空)。
o 多标签页 / 窗口:在多标签页中打开同一表单页面,分别提交可能导致重复请求(尤其当页面未做跨标签状态同步时)。
三、业务逻辑设计的疏忽
1. 缺乏明确的操作反馈
o 提交后无 Loading 动画、文字提示(如 “提交中,请稍候”)或进度指示,用户无法判断操作状态,进而重复点击。
o 错误提示不清晰(如仅提示 “提交失败” 而不说明原因),用户可能误以为是点击未生效而重试。
2. 未限制高频操作场景
o 部分业务场景(如点赞、评论、快速保存)允许短时间内多次操作,但未做频率限制,可能因用户连续点击导致重复提交。
四、测试人员的疏忽
在事件发生后,测试人员对该问题进行了复盘。当时的测试人员查看了测试设计和用例,发现
1)测试设计涵盖了“前台重复提交”的测试场景
2)测试执行时也执行了该用例
3)执行时报告了两次“提交成功”,测试人员就认为功能正常。然后就没有报告缺陷。
可以看到
1)测试人员对于重复提交等防呆设计类的场景认知是不足的,即使是触发了问题场景,也眼睁睁看着问题从眼前溜了过去。
2)测试过程也是有瑕疵的,测试人员只查看了前台返回的数据,没有看到后台后续人物执行的报错日志。错过了发现问题的另一个机会。
如何规避
在软件防呆设计中,规避前端意外重复点击提交按钮(如表单提交、订单确认等场景)是常见需求,核心目标是防止重复触发请求(如重复提交数据、重复下单),同时保证用户体验。以下是让豆包梳理的几种常用的技术方案(示例代码仅是示例哦,保鲜不保活):
1. 即时禁用按钮(最常用)
点击按钮后立即禁用,阻止后续点击,直到请求完成(成功 / 失败)后再恢复状态。
• 实现方式:
点击事件触发时,通过disabled属性(HTML 按钮)或样式禁用按钮,并可添加加载状态(如 “提交中...”)。
示例代码(JavaScript):
javascript
const submitBtn = document.getElementById('submitBtn');
submitBtn.addEventListener('click', async () => {
// 点击后立即禁用按钮,防止重复点击
submitBtn.disabled = true;
submitBtn.textContent = '提交中...';
try {
// 发送请求
await api.submitData(formData);
// 成功后可跳转或提示,按钮可保持禁用(如表单一次性提交)
alert('提交成功');
} catch (error) {
// 失败后恢复按钮可用,允许用户重试
alert('提交失败,请重试');
submitBtn.disabled = false;
submitBtn.textContent = '重新提交';
}
});• 优点:简单直接,用户能直观感知 “操作中”,减少重复点击冲动。
• 注意:需在请求失败时恢复按钮状态,避免用户无法重试。
2. 添加点击间隔限制(防抖)
通过防抖(debounce)机制,限制短时间内重复点击(如 1 秒内仅允许一次有效点击)。
• 实现方式:
记录上次点击时间,若两次点击间隔小于阈值(如 500ms),则忽略后续点击。
•
示例代码:
javascript
let lastClickTime = 0;
const clickThreshold = 1000; // 1秒内不允许重复点击
submitBtn.addEventListener('click', async () => {
const now = Date.now();
if (now - lastClickTime < clickThreshold) {
return; // 忽略重复点击
}
lastClickTime = now;
// 执行提交逻辑...
});• 适用场景:不希望完全禁用按钮(如高频操作但需限制频率),但可能存在漏判(如请求未完成时用户再次点击)。
3. 前端生成唯一标识(Token)
结合后端校验,确保同一操作仅被处理一次(从根源防止重复提交)。
• 流程:
1. 页面加载时,前端从后端获取一个唯一的token(如 UUID),存储在表单或本地(如localStorage、隐藏字段)。
2. 点击提交时,将token随请求一起发送给后端。
3. 后端校验token:若未使用过,则处理请求并标记token为 “已使用”;若已使用,则拒绝重复请求。
4. 前端提交后,清除本地token(或请求成功后由后端返回新token,供后续操作使用)。• 优点:即使前端限制被绕过(如恶意攻击),后端仍能拦截重复请求,安全性高。
• 注意:需配合后端逻辑实现,适合对数据一致性要求高的场景(如支付、订单提交)。

一句话总结:
前端防呆是为了提升体验,后端校验才是防止重复提交的根本保障,而测试只有懂了原理才不会在缺陷站在眼前时还是熟视无睹。