App 下单页按钮连点两下就多出一条订单,为什么测试时没复现、上线后用户却碰上了?
结论先说:App 里一次点击产生两条订单,通常不是某一个人的锅,而是前端防抖、网络重试与服务端去重三者叠加的结果。前端置灰只能挡住误触,真正能兜底的是服务端幂等键加数据库唯一约束。2026 年常见的做法是两端同时做:前端加提交锁与一次性令牌,服务端按业务语义去重,并把重复请求留成可查、可对账的记录。这也是本文要回答的:为什么测试时没复现,上线后用户却碰上了?
重复提交为什么比接口报错更难处理
重复提交指的是用户只操作了一次,系统却写入了两条或多条业务记录。它和接口报错不同:报错会有人看到,重复提交往往界面正常、日志也没有异常,等到发货、对账或发券时才暴露出来。
按 2026 年移动端交付经验,重复请求主要来自三类:用户手快连点或误触;客户端超时重试、切后台恢复后重发;网关或网络库的重试策略把一次调用放大成多次。三类里只有一类算用户的问题,另外两类属于链路设计问题。
- 影响面大:库存、优惠券、积分、余额、退款都可能直接产生资产差错,客诉常常来自财务对账而不是测试。
- 排查成本高:没有唯一约束时,两条记录看起来都合法,定位要靠抓包与请求号比对。
- 上线后补更贵:补幂等通常要连带清理历史脏数据并补偿用户,工作量常见是开发期预留的半倍到一倍。
先判断问题出在哪一层:四步排查
遇到重复数据不要先改代码,先定位。判断责任归属较快的方法,是对比两次请求的请求号与幂等键:同一个键写了两条,基本出在服务端侧;两个键不同,多半出在前端或重试链路。
- 抓包复现:确认客户端实际发出了几次请求。同一秒内发出两次以上,说明前端提交锁基本没生效。
- 比对请求标识:看请求头里的请求号、幂等键是否一致。如果客户端每次都生成新的随机数,幂等就形同虚设。
- 检查重试配置:网关、网络库、SDK 的自动重试常见是 1~3 次,超时时间设得太短会让重试更频繁。
- 核对数据库约束:业务唯一键上有没有唯一索引,是兜底能不能成立的关键。
第三步容易被忽略:不少看起来像「服务端重复」的情况,其实是客户端超时后自动重试,服务端两次都正常处理了,所以日志里看不到任何错误。
在项目里常见的情况是:这一期只给了两周做下单流程,前端做了按钮置灰,接口里加了一次「是否已存在」的判断就交付上线,结果灰度当天出现一笔订单扣了两次券,只能暂停迭代回头补服务端幂等。按经验区间,这类回头补做的工作量常见是原下单模块开发工期的三到五成,工期多出三到五天,还要单独给用户做补偿。
一套可落地的三层防重框架
防重不适合只押在一层上。按 2026 年项目交付习惯,可以用「三层拦截」来划分:交互层管住操作手法,传输层管住重复发送,数据层管住最终写入。三层之间是备份关系,不是三选一。
- 第一层,交互层:提交按钮进入 loading 并锁定,配合防抖节流。成本低,但页面重建、切后台恢复后状态会重置,挡不住重发。
- 第二层,传输层:服务端下发一次性令牌,提交后立即失效;请求统一携带请求号,自动重试只对查询类接口开放。注意令牌要短时效,不能长期存在本地复用。
- 第三层,数据层:幂等键加唯一索引。同一幂等键的第二次请求,应返回与首次一致的结果,而不是报错,也不是再写一条。
合格标准可以量化:连点按钮多次只产生一条记录;弱网超时后重试,最终结果与单次提交一致;同一请求重放两次,第二次返回成功且数据不新增;重复请求在日志里能关联到同一个幂等键。
三种常见做法怎么选
三种做法不是替代关系,而是按业务风险叠加使用。按经验区间,三者投入大致如下,判断的起点很简单:这次操作会不会写数据、会不会动钱。
- 前端防抖加按钮置灰:投入常见在半天以内,能覆盖误触和手快,挡不住网络重试、请求重放和多端同时操作。
- 一次性令牌:投入常见 1~2 天,能覆盖客户端重复发送,对脚本重放和跨端并发的覆盖有限。
- 幂等键加唯一索引:投入常见 1~3 天(视涉及的业务表数量),能覆盖重试、重放与并发写入,代价是要设计键的生成规则和保留周期。
如果涉及支付回调,还要在回调处理里加状态判断:先读订单当前状态,再决定是否改动余额,不能只凭金额和单号做条件更新,否则重复回调会把同一笔钱处理两次。涉及资金的动作,幂等属于硬要求,不是可选项。
容易踩的坑与验收口径
坑大多不在技术难度,而在「以为已经做了」。下面几条在交付评审里被提到的频率比较高。
- 只在前端做防抖,接口层不做任何去重,换端或脚本刷单时直接失守。
- 幂等键里带了时间戳或页面随机数,每次请求都不同,等于没做。
- 超时后提示「提交失败,请重试」,把用户引向第二次下单;正确提示应是「处理中,请稍后到列表查看」。
- 令牌长期存在本地或跨页面复用,重放时仍然有效。
- 只处理「新增」路径,忘了退款、撤回、撤销等反向操作同样需要幂等。
验收口径建议按场景核对,而不是只点一次按钮:连点、弱网、切后台恢复、请求重放四种情况都要覆盖。按企业项目交付习惯,这类核对通常放在联调与验收两个节点,不合格就退回补做,不要等到灰度期用真实用户的数据来验证。
适用场景与边界
判断标准可以说得更直白:一次操作会新增记录、扣一次余额或发一次券,就值得做防重;只读接口做防抖主要是省流量,不是必须。
- 建议做:下单、支付、退款、提现、领券、签到、报名、评论发布等有副作用的动作。
- 可以不做:纯查询、静态展示页、内部一次性工具,以及本地草稿保存——草稿反而应当允许重复保存。
- 需要单独评估:点赞、浏览量这类可容忍少量重复的计数场景,可以先做服务端合并统计,不必上完整幂等链路。
常见问题
按钮已经置灰了,为什么还会重复下单?
置灰只在当前页面生命周期内有效,切后台恢复、页面重建或网络重试时按钮状态会重置,第二次请求照样发得出去,最终仍要服务端去重兜底。
幂等键用客户端生成的随机 UUID 行不行?
常见做法是服务端下发令牌,或按用户 ID 加业务标识拼键。纯客户端随机数在重装、清数据、请求重放时会变,挡不住重复写入。
接口超时了,前端该提示什么?
不要提示失败引导重试,应提示「处理中,请稍后到订单列表查看」。服务端按幂等键判断,已处理过就返回原结果。
用户量不大的小项目也要做这一套吗?
只要操作会写数据或动钱,建议做。按 2026 年交付经验,核心接口补幂等的投入常见是半天到三天,比上线后清理脏数据便宜。
行动上可以先做一件事:把会写数据的接口列出来,按是否涉及资金排序,先从支付、退款、领券三个入口补上幂等键与唯一索引。项目若仍在开发期,把它写进需求文档的验收标准,比上线后再补省事得多。用户量极小、纯展示的内部工具可以先不做;一旦涉及资产账目,就不建议省这一步。
-
用户只点了一次提交,后台日志里却出现两条一样的下单请求,先查哪一端?
日期:2026年10月11日 阅读:71
-
用户填收货地址时键盘一弹,提交按钮就点不到,先改页面结构还是先换输入框组件?
日期:2026年10月14日 阅读:80
-
App 用跨平台框架做完,安卓同事说列表滑起来发涩、iPhone 同事说侧滑返回不灵,该先动框架还是先补适配?
日期:2026年10月13日 阅读:51
-
用户上传的视频,iPhone 能看、个别安卓点开只有声音没画面,先让人重拍有用吗?
日期:2026年10月12日 阅读:118
-
公司蓝牙设备同事手机能搜到,我的新手机却搜不到,一般先看哪几项?
日期:2026年10月11日 阅读:56




