返回学院
BUILD从 0 到 1

第一次接支付,最难的不是付款按钮,而是重复请求、回调和状态

支付会遇到超时、重试、异步回调和重复事件。真正可靠的系统必须做到同一个业务动作不会被执行两次,并且不能把“用户跳回成功页”当成到账事实。

支付 Demo 很容易做:点一个按钮,跳到收银台,付款完成,再回到成功页面。

真正上线以后,麻烦从网络不稳定那一刻开始。用户点了付款,浏览器超时,于是又点一次;服务器请求支付平台成功了,但响应在路上断掉,于是代码重试;支付已经完成,webhook 过几秒才到;平台发现 webhook 没及时返回,又重新投递。

如果系统把每一次请求都当成新动作,结果可能是重复订单、重复扣款、重复发积分。

第一个概念:幂等

Stripe 官方 API 明确支持 idempotency key,用来安全重试创建或更新请求。相同 key 的后续请求会返回第一次请求的结果,从而避免因为网络重试再次执行同一操作。

但支付系统不能把幂等完全外包给支付平台。自己的数据库也要有业务唯一性:一个 checkout session、一个支付事件、一个订单发货动作,应该有稳定 ID 和唯一约束或事务保护。

否则 webhook 收到两次,平台没重复扣款,你仍然可能给用户发两次权益。

第二个概念:异步事实

用户从支付平台跳回成功页,只说明浏览器走到了这个 URL,不代表系统应该立刻把订单改成 paid。

成功页参数可以被手工构造,浏览器也可能在支付最终状态还没同步时先回来。更可靠的做法是:支付平台的签名事件或服务端查询成为付款事实来源,本地订单根据经过验证的事实更新。页面只读取当前订单状态并展示。

Stripe 的 webhook 文档也把支付确认、争议、周期性付款等列为典型异步事件,并要求服务端验证签名。

第三个概念:回调会重复

Webhook handler 要假设同一事件可能重新送达。

先记录 provider event id 是否处理过,再在事务内做状态变更和权益发放。处理成功后快速返回 2xx;复杂工作可以进入持久化任务队列,而不是让 webhook 请求一直等模型生成、发邮件或做大计算。

如果任务失败,要有可重试状态,而不是“接口报 500,下次看运气”。

第四个概念:付款和交付是两个状态

卖一个 PDF,支付成功后可以立即生成下载权限;卖 AI 解读,付款成功以后还要调用模型并产出结果;卖次数包,则付款成功是增加余额,真正使用时再消费。

这几个动作不能混成一个 paid 状态。至少要区分订单支付、权益和具体交付任务。这样生成失败时,才能明确是退款、重试还是释放预留次数,而不是靠人工猜用户现在到底欠没欠一次服务。

上线前至少做五个破坏性测试

连续点两次购买;创建订单响应超时后重试;同一个成功 webhook 重放两次;付款成功但交付任务失败;用户不经过收银台直接打开成功页。

再根据业务加入退款、取消订阅、支付失败等分支。

支付系统可靠的标准不是“正常付款一次成功”,而是异常发生时钱和权益仍然对得上。按钮只是入口,真正的支付产品是一台状态机。

KEEP READING

继续看这个方向