永不重复入账的支付回调:签名校验、幂等与对账
作者:曾祈荣 · · 阅读约 4 分钟
我修过的大多数支付问题,都不在结账页面,而在回调:也就是通知你的系统“这笔付款成功了”的 webhook。无论是 Checkout.com、Stripe 还是本地网关,以下是我现在对每一次集成都坚持的规则。
先校验,再信任
在读取任何内容之前,先对原始请求体校验服务商的签名。校验失败的请求一律拒绝,并记录日志。webhook 地址是公开的,所以在证明可信之前,每个请求都应视为不可信。
每个事件只处理一次
服务商会重试,网络也可能在你的代码已经提交之后超时,所以同一个事件一定会到达两次。把服务商的事件 ID 或支付 ID 存下来,并加上唯一约束;处理程序遇到见过的 ID 时什么都不做。数据库约束比一句 if 判断可靠得多。
把支付建模成状态机
待支付、已授权、已扣款、已退款、失败。只允许合法的状态转换,忽略会让支付“倒退”的事件。事件可能乱序到达,迟到的“待支付”绝不能覆盖“已扣款”。
快速响应,稍后处理
尽快确认 webhook,把耗时的工作(发邮件、记账、开通服务)放到支付状态安全落库之后。处理过久会导致超时,而超时又会引发重试。
定期对账
即使做到了以上所有,回调仍可能丢失。一个定时向服务商查询所有“仍在处理中”订单的对账任务,能把“我们丢了一笔付款”变成“十分钟后就修好了”。在一个票务项目里,正是这个任务加上状态轮询,让漏掉的回调没有变成收入损失。
在本地跑通完整流程
在本机用隧道接收 webhook,跑通完整流程,并把步骤写下来给团队。有一个坑值得知道:转发工具返回 200,并不代表你的处理程序已经处理成功。要检查落库的支付记录,而不只是 HTTP 状态码。
最后,通过托管表单或令牌把卡号留在服务商那里。这样你的 PCI 合规范围很小,风险也很小。
- 支付
- Webhook
- Checkout.com
- Stripe