在 Web 开发中,有些 Bug 的隐蔽性极高,它们往往藏在浏览器的底层机制和复杂的交互链路中。最近我们遇到了一个关于"邀请码重复绑定"的诡异问题,最终溯源发现,这不仅涉及前端的存储选择,还涉及浏览器对网络请求的展示逻辑。
问题背景
项目中有个典型的裂变需求:用户 A 分享链接给用户 B,B 点击链接进入站点(URL 携带邀请码),点击登录后调起微信授权,登录成功后前端调用 /invites/bind 接口完成绑定。
现象: 后端数据库出现了同一个用户被重复绑定的两条记录。 初步排查: 后端日志显示,两个 /invites/bind 请求几乎在同一秒(间隔仅几十毫秒)到达服务器。
疑点一:消失的"第二次请求"
前端最初的结论是: "后端背锅" 。理由看似非常充分:
- 控制台证据 :Network 面板从头到尾只显示了一条
bind请求。 - 逻辑自洽 :前端代码对
bind请求做了严格控制,执行前会判断SessionStorage中是否存在邀请码,请求发出的那一刻会立即清空该值。理论上,第二次请求根本过不了判断逻辑。
然而,后端日志是铁证。为什么控制台只显示一条请求,后端却收到了两条?
真相: 在极短的时间内(毫秒级)发送两个完全相同的请求,部分浏览器(或特定环境下的网络代理)在控制台中会将其合并显示,或者其中一个请求因为网络链路的并发特性被归类为同一条记录的重试/后续。这导致了开发者被 Network 面板"欺骗"了。
疑点二:SessionStorage 的"隐身双胞胎"
既然请求确实发了两次,那前端的 SessionStorage 清空逻辑为什么失效了?
我们重新梳理了业务链路:
- 用户进入页面,邀请码存入
SessionStorage。 - 用户点击登录,页面拉起微信授权窗口。
- 关键点:在微信环境或模拟环境下,授权过程可能涉及窗口的派生或重定向。
SessionStorage 的生命周期虽然局限于"标签页",但有一个容易被忽视的特性:通过 window.open 或特定重定向方式打开的新页面,会复制父页面的 SessionStorage。
在这个业务场景中,由于微信授权的交互逻辑,实际上在某一瞬间存在两个"窗口上下文"。它们各自拥有独立的 SessionStorage 副本。
- 窗口 A :触发登录逻辑,发出
bind请求,清空了自己的邀请码。 - 窗口 B :由于是并行触发或瞬间创建,它也持有一份邀请码,几乎在同一时间也发出了
bind请求。
因为两个窗口是独立的执行环境,窗口 A 的清空操作无法影响到窗口 B,导致防重逻辑在多环境并发下彻底失效。
解决方案:从 SessionStorage 切换到 LocalStorage
针对这个坑,最直接的解法是将存储介质从 SessionStorage 切换为 LocalStorage。
为什么有效?
- 全局唯一性 :
LocalStorage在同源的所有标签页和窗口之间是共享的。 - 同步状态 :当窗口 A 发出请求并执行
localStorage.removeItem('invite_code')时,窗口 B 在纳秒级的时间内再次读取时,该值已经不复存在。
总结
这两个 Bug 给我们的启示是:
- 不要盲目迷信控制台:当后端日志与前端控制台冲突时,优先信任后端原始日志,或者使用抓包工具(如 Charles/Fiddler)查看底层的 TCP/HTTP 流量,那是不会骗人的。
- 慎用 SessionStorage 做关键校验 :在涉及多窗口、弹窗授权或重定向的复杂链路中,
SessionStorage的隔离性往往会带来意想不到的并发问题。对于需要"全局一次性"的业务标记,LocalStorage或配合后端的分布式锁才是更稳妥的选择。