I3.05.3UI debounce is not idempotency设计

界面防重不能替代服务端保护

别名: 按钮禁用不够 · 前端防重 · client-only dedupe · 服务端幂等

概念解释

提交后立刻把按钮关掉,能挡住同一屏幕上的连点。它挡不住:超时后的第二次进入、另一台设备、系统恢复的第二个 webview、脚本重放、支付渠道的回调重试。界面防重不是幂等。幂等是服务端对同一意图的第二次到达返回第一次的结果;界面防重是减少到达次数。次数减到很低仍然不是零,零只能由服务端保证。

和「提交后禁用入口」那一层的差别在这里:那里谈的是把第二次点击挡在手上;这里谈的是挡在手上之后,仍然会飞到网上的那些副本。

机制

界面状态活在一个进程、一块内存、一次页面寿命里。不确定窗口跨越的是进程寿命:杀应用、系统回收、用户从通知点进来开了另一个实例。新实例带着一份干净的可点按钮,界面防重的记忆是空的。网络路径上还有客户端管不到的复制:负载均衡重试、网关超时再发、合作方 webhook 至少一次投递。这些到达根本不经过那颗按钮。

所以防重是分层的。最外层减少手误(禁用、节流);中间层给意图命名(客户端键);最内层以名为约束执行。抽掉最内层,外两层是过滤器,过滤器有漏。漏的那一次在支付和建单上就是一笔多扣。产品把「我们禁用了按钮」写进验收,等于用过滤器的存在证明管道密封——类别错误。

边界

没有副作用的请求(刷新、搜索)可以只做界面防重,用来减流量,不需要服务端键。本地-only 的写入没有服务端。多人故意连点「再来一条」是新意图,服务端应执行,界面也不该禁用到无法连发——这时防重和幂等都要以键区分,而不是把按钮冻死。对内工具、低金额、可人工纠错的场景,有人用界面防重加事后对账顶着;对账不是幂等,是补偿,补偿有时间窗和人力成本。公开 API、开放重放的渠道(支付、短信、邮件)必须假定到达次数大于界面看见的点击次数。

怎么落地

  • 把「按钮禁用」列为减少连点的手段,验收里单独列「同一幂等键到达两次,业务只发生一次」。
  • 对有资金、有对外发送、有唯一资源创建的路径,服务端无键或无唯一约束则不允许上线,即使前端已经禁用。
  • 不要用「两次点击间隔小于 N 毫秒则忽略」当服务端逻辑:合法重试往往在秒到分钟之后。
  • 验证:禁用按钮的前提下,用代理把同一请求(同一键)连发两次,业务计数必须为 1。再开一个无前端的客户端(curl、第二台设备、支付回调)发同一键,仍为 1。最后不带键发两次,应为 2——用来证明少了服务端保护,界面挡不住。

延伸

  • 同组I3.05.1 网络不确定时用户必然重试 · I3.05.2 幂等标识需由客户端生成
  • 相邻H1.07 提交防重复 · I3.02 乐观更新
  • 站内检索idempotency · client-side debounce · server-side dedupe

同组卡片

快捷操作

分享

分享当前页面

ios_share

https://hci.top/zh/handbook/I3.05.3