一次重复支付的 bug 修复经历带来的启发
背景
客户使用我们的收款系统时,发现付了两次同样的钱,然后客诉到我们后台系统,领导非常重视,要求马上找到原因解决。
分析问题
收到消息后,我们迅速行动,开始分析产生这个 bug 的原因。后端同学查询了日志,给出的信息是:
两笔支付请求在同一秒完成,商品信息、用户信息等都相同,付款方式是支付宝扫码支付,但是两笔支付的付款码不同。
我们首先想到的是前端可能没做防抖导致的,短时间内重复请求了两次,所以支付了两次。 于是我们马上打开了代码,定位到支付请求提交的位置。
submitCash: _.debounce( function() { this.submitCashCallBack(); }, 2000, { leading: true, trailing: false } ),
发现支付的函数是用了防抖方法的,时间为 2 秒。到这里就遇到问题了:前端做了防抖,且时间为 2s,那后端是如何在 1 秒内收到两次重复的请求的?
于是我们马上开始了复现场景的测试,很遗憾,经过反复测试,都没有复现到这种异常情况,每次的结果都是正常被防抖拦截到。
这时候我们有点急了,时间一点点在流逝,我们却还没有复现出异常场景,也没有定位到异常原因。我们开始寻找别的方法来复现这种异常情况,于是我们想到了看客户支付时的录像,通过客户直接操作出异常的动作来分析产生异常的原因,很庆幸,我们的方向找对了。
在信息部的同学大力支持下,我们迅速获取了客户在门店支付的录像,经过反复观察,我们发现当时的场景是这样的:
收银员在扫描了商品编码后,唤起了付款页面,拿起了扫码枪对客户展示的支付宝付款码进行扫描,这时候出现了一个异常情况,收银员在扫描完第一次之后关闭了扫码窗口,然后再次打开了扫码窗口,并要求客户重新扫码,客户也以为没有扫上,刷新了一下付款码后,再次展示了付款码进行扫码支付,然后发现支付了两笔同样的钱。
我们通过这个录像可以发现,两次操作的时间明显超过了 2 秒,也就是说没有达到触发防抖机制的条件,但是后台却在同一秒内收到了两次请求。
于是我们分析有可能是网络原因导致第一次支付请求未发出,在第二次支付请求发出的时候,第一次支付请求跟随第二次请求一起发到了后台。
然后我们马上针对这种情况开始复现场景:
先启动监听日志,然后打开扫码弹窗,在第一笔支付前,通过控制台将网络延迟调到 10s,然后扫码触发第一次请求,在页面上确认按钮 loading 的时候,点击取消支付,然后再次打开扫码弹窗,更新付款码再次扫码,触发第二次请求,这时候立刻把网络延迟恢复正常,可以看到两个请求确实如异常情况一样发送到了后台,并且从日志上看,和异常情况表现一致,两笔支付都成功了,我们复现出了异常场景。
复现出异常场景对于我们分析原因有了很大的帮助,基本上可以推断出,当时收银台网络出现波动,导致第一笔支付请求发起的时候,页面出现了 loading,而收银员以为取消支付再重新扫码就可以解决问题,所以关闭了扫码弹窗再重新打开,然后再扫码发起了第二次支付请求,这时候网络恢复,两笔请求一起传到了后台。这中间操作耗时超过了 2 秒,所以并没有被前端防抖给拦截下来。
经过以上的分析,我们有了一个思路,问题是出在 loading 效果还没结束的时候,发起了第二次请求。于是我们找到了发起支付请求的函数:
submitCashCallBack() { this.isLoading = true; axios .post({...}) .then(res => { this.isLoading = false; ... }) .catch(err => { this.isLoading = false; ... }) ... }
乍一看好像没啥毛病,但是仔细一看就会发现,如果第一个请求还没有得到结果,那么 this.isLoading
就一直是 true,2 秒后调用第二次函数,是不会被拦截到的,仍然会发起第二个请求,这样就会导致两个请求有可能在同时进行,而且这个 this.isLoading
一直是 true,直到有一个请求有了响应才会变成 false,而我们对于请求本身并没有做到限制效果,我们只是限制了按钮不能点。而扫码枪在支付的时候,并不会因为 loading 效果没消失而触发不了这个函数,就导致了重复发起请求。
解决问题
找到了原因,解决起来就很快速了,我们只需要在这个函数中加一句判断:
submitCashCallBack() { if(this.isLoading) { return this.$message.warning('正在支付中,请勿重复提交!') } this.isLoading = true; axios .post({...}) .then(res => { this.isLoading = false; ... }) .catch(err => { this.isLoading = false; ... }) ... }
如果 loading 状态为 true 的话,就说明有请求尚未完成,需要等待这次请求完成才能发起下一次请求。这样就能避免扫码枪绕过 loading 直接调用支付函数,从而导致重复请求的 bug 了。
总结启发
这个 bug 最后修复只用了两行代码,但是却浪费了我们近一个小时的时间来分析、复现、处理。我们应该反思,在不同的业务场景下可能会有各种不同的问题,而这些问题有很多时候都是因为一个不起眼的代码造成的。所以我们在开发的时候应该更严格的要求自己,保持严谨的逻辑才能减少 bug 的数量,也需要非常熟悉业务,保障在不同的业务场景下我们能正常执行。
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论