大多数支付回调教程只讲顺利的那条路:验签、返回200、结束。几乎没人讲端点宕机一整天之后会发生什么。
![]()
多数支付平台用指数退避重试,然后停手。Atoa 文档写明的行为是:任何非 HTTP 200 的响应都算失败,平台按退避策略重试,如果投递持续失败超过 24 小时,就停止投递,并给商户负责人发邮件,附上失败的 URL。这个设计本身合理,但它意味着存在一道悬崖——掉下去的东西,只有你提前建了第二条路径才捞得回来。
所以更稳的做法,是把回调当成门铃,而不是货物本身。它只告诉你"有东西变了,去看一眼",真正的真相在支付记录里,随时可以从 API 读回来。接受这个前提之后,三个设计决定就顺下来了。
先验签,而且对着原始报文验
Atoa 的 V2 签名在请求头里带 X-Atoa-Signature: v1=,是对整个原始请求体做的 HMAC-SHA256。密钥形如 whsec_,真正参与计算的是前缀之后那段 base64 解码后的内容。
两个容易踩的坑:必须用 express.raw 而不是 express.json,因为重新序列化过的报文对不上签名;另外文档描述的请求头里没有时间戳,也就没有内置的重放保护,不能想当然,去重得自己做。
验签通过后,正确动作是入队、立刻返回 200,而不是在请求里同步处理业务。
幂等要挂在支付单上,不是挂在投递上
报文里带着 paymentRequestId、orderId、paymentIdempotencyId,以及 COMPLETED、PENDING、FAILED 这样的状态(卡支付还有 AUTHORIZED 和 CANCELLED)。重试意味着同一个事件你会收到不止一次;运气不好时,一个 PENDING 还可能在你已经处理完 COMPLETED 之后才到。
所以状态变更应该以 paymentRequestId 为键,并且只允许向前流转。素材给出的排序是 PENDING 为 1,FAILED 和 COMPLETED 同为 2。这样重复投递和乱序到达都不会把已完成的单子打回原形。
真正的兜底是一个定时对账任务
既然 24 小时后投递会停,就得有一条不依赖回调的路径。做法是定时扫出停留超过 10 分钟仍是 PENDING 的支付单,逐条调 getPaymentStatus 拉取实时状态,再走同一个 apply 函数写回。
关键在于这个扫描和回调共用同一段代码,没有第二套"已支付"的定义,也没有额外要测的逻辑。端点宕机 30 小时,回调在第 24 小时停掉,等你恢复后下一轮扫描会安静地把一切补齐。
需要说明的是,getPaymentStatus 只是一个薄封装示意,照抄之前请先核对文档里的确切端点和字段。
几个值得知道的配置细节
- V2 端点支持一个端点订阅多种事件类型,一种事件类型最多挂 3 个端点,各自独立投递。可以把一个指向业务应用,另一个指向独立的审计或分析消费者,一边挂了不影响另一边。
- V2 的失败追踪会暴露 failureCount 和 lastDeliveredAt。与其等那封负责人邮件,不如直接对 lastDeliveredAt 变陈旧告警。
- 可选的端点认证(OAuth 2.0 或 Basic Auth)可以叠加在签名之上,文档建议两者组合使用。
- 沙箱里可以用 trigger-test-webhook 接口,覆盖 status、amount、orderId 来模拟失败。该接口仅限沙箱,生产密钥会被拒绝。
- 轮换是即时的。轮换签名密钥会让旧密钥立刻失效,启用 V2 会切换所有投递,包括 WooCommerce 和 Magento 插件。先把环境变量更新好。
落地顺序
- 对原始报文验签,入队,返回 200。
- 状态以 paymentRequestId 为键,只允许向前流转。
- 对账任务要在上生产之前就上线,而不是等第一次事故之后。
- 对 lastDeliveredAt 设告警。
回调图的是快,对账图的是对。两个都要。
特别声明:以上内容(如有图片或视频亦包括在内)为自媒体平台“网易号”用户上传并发布,本平台仅提供信息存储服务。
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.