想象一下:用户在支付页面点了“确认”,应用卡了一秒,他下意识又点了一下。如果后端没处理好,用户就被扣了两次款——客服工单、退款流程、信任崩塌,全来了。
大部分开发者知道标准解法:给请求生成一个唯一键,附带在调用里,处理前先查这个键是否已用过。这能很好解决单次调用被重复发送的问题。可还有一个更隐蔽的版本,不少后端会被它偷袭:同一种逻辑交易,几天甚至几周后再次出现,来源不同,附带元数据也略有差异。这时光靠事务ID检查就不够用了,因为它们的ID可能压根对不上。
先说大家熟悉的“重试安全幂等”。通常做法是客户端生成一个幂等键,随请求发送,请求一旦处理完就存下来。下次收到相同键,就知道是同一个请求的重试,而不是新请求。在 NestJS 里,一个拦截器很适合在请求层统一施加这个检查,让控制器不必各自实现。
代码里会看到,服务拿到幂等键后先去查记录表,如果已经存在,就直接返回对应支付记录,不再重复创建。拦截器则检查请求头里的幂等键字段,没有就报错,有就查询,如果记录已存在,直接返回一个“已处理”的信号并附上原有支付ID。这能兜住慢网络、客户端 bug 或用户紧张连点造成的重复调用。
但问题来了:这只覆盖了“明显重试”的场景。如果一个支付指令通过不同的渠道再次到来——比如先从 App 发起,失败后改从网页端手动补单,或者批量文件里隔天又出现同一笔业务编号——它们的幂等键根本不重复,后端会当成两笔独立交易处理。于是,用户依然可能被扣两次,而你很可能要等到对账时才悚然发现。
所以,光有请求级别的重试安全是不够的。业务层还需要一层基于业务唯一标识(比如订单号、外部交易参考)的去重逻辑。这种逻辑不是简单地把拦截器里的数据库改为用业务键查重就行,还得考虑冲突策略:是报错还是幂等返回?如果第二笔请求的金额或币种不同,要不要拦截?这些取舍就进入产品设计领域了。
这件事的有趣之处在于,它暴露了一个常被忽略的事实:幂等不仅仅是技术上的“同一个 HTTP 请求不要处理两次”,更是“同一笔业务意图不要生效两次”。前者用请求级幂等键解决,后者得结合业务键和冲突处理规则。不少团队只做了前半部分,就以为万事大吉,直到业务量上来,各种迂回补单路径把重复交易带进来。
NestJS 的拦截器很适合承载请求级的防重,但千万别幻想它能包办一切。真正健壮的系统,会在请求入口用拦截器快速检查,在服务层用业务键再做一层保障,并在数据库加好唯一约束兜底。下次写支付流程时,可以问自己一句:如果同一个订单号明天又来了,但带着一个新的幂等键,我的代码会怎么做?
特别声明:以上内容(如有图片或视频亦包括在内)为自媒体平台“网易号”用户上传并发布,本平台仅提供信息存储服务。
Notice: The content above (including the pictures and videos if any) is uploaded and posted by a user of NetEase Hao, which is a social media platform and only provides information storage services.