Nuxt 3 发布按钮被连点写出两篇文章:我用提交令牌、请求锁和服务端唯一约束堵住重复创建
一次网络慢让用户连点发布,博客里出现两篇一模一样的文章。后来我把防重拆成三层:前端按钮锁、服务端一次性令牌、数据库唯一约束。

作者
ZHOUYI
前阵子遇到一个很典型的事故:用户在后台写了一篇长文,点发布时网络卡了一下,按钮没有立刻跳转。他以为没生效,又连点了几下,结果列表里出现两篇标题、正文完全相同的文章。表面看是按钮没禁用,实际上只要请求离开浏览器,前端就已经不可信了。
第一层:前端按钮锁,只解决体验
在 Nuxt 3 里,我会在提交函数里维护 submitting 状态,请求发出前禁用按钮,请求结束后恢复。如果失败,还要给用户明确提示。这一步能挡住大多数误触,但它只能防正常用户,不能防网络重试、脚本请求、多标签页同时提交,所以不能把正确性押在这里。
第二层:一次性提交令牌,挡住同一表单重复提交
进入发布页时,服务端先生成一个 submitToken 并写入 Redis,设置过期时间。提交文章时必须带上这个 token,服务端用原子方式检查并占用。成功后这个 token 就不能再用;如果用户刷新页面,就重新申请新的 token。这样同一个表单即使被点两次,第二次也会因为 token 已失效而被拒绝。
const key = `submit:${token}`;
const ok = await redis.set(key, userId, 'NX', 'EX', 300);
if (!ok) return { code: 1, msg: '请勿重复提交' };
await redis.del(key);这个思路其实和支付下单一样:用户可能重复点击、网络可能超时重试,服务端必须把同一笔操作识别出来。对博客发布来说,重复创建只是脏数据;对支付来说,可能就是重复扣款。所以幂等不是可选项,而是写接口的基本功。
第三层:数据库唯一约束,做最后兜底
- 用 userId、标题、内容指纹和分钟级时间窗生成幂等键。
- 给幂等键建唯一索引,重复插入直接失败。
- 命中重复时不报错,而是返回已有文章或提示稍后再试。
“前端锁是为了体验,令牌是为了请求幂等,唯一约束是为了数据正确。少一层都可能漏。”
踩坑点也很明确:只靠前端 loading,遇到慢网络一定会翻车;只靠 Redis 令牌,如果并发判断不严谨,也可能漏;只靠数据库唯一索引,用户会看到生硬报错。现在我把三层串起来:按钮锁负责交互,令牌负责请求幂等,唯一约束负责数据兜底。发布这种写操作,宁可多一层防御,也别等脏数据出现再补。