Skip to main content
确认支付结果确立了本页所依赖的规则:**跳转永远不是支付成功的证明,权威结果是 Webhook Webhook 通知,Order Inquiry 查询作为兜底。**本页讲的是这条规则的后半部分——一个结果何时才允许被视为最终结果,以及为什么超时不属于这些时刻。
**只需记住这一句:**订单超时意味着 Waffo 停止等待了。它不意味着支付方式停止了,也不意味着用户不能继续付款。

我的订单超时了,支付是不是肯定不会发生了?

不是。超时是 Waffo 对你的订单做出的决定,不是支付方式对这笔支付做出的决定。 对于不少支付方式,你的订单关闭之后,支付在支付方式一侧仍处于非最终状态——他们那边根本没有自动关闭。在这种情况下:
  • 用户仍然可以打开支付页面或 App 并完成付款。
  • 支付可以在订单显示为超时之后成功。
  • 结果通知可以在关单之后到达。
因此,超时的订单是一种暂定的失败。不要销毁购买记录,不要不可逆地释放预留的座位或许可,也不要告诉用户「支付失败了,不会扣钱」——告诉他们这次尝试没有完成,再次付款前应先核实。

订单到达关单时间时,Waffo 实际做了什么?

分两种情况,取决于支付进行到了哪一步: 在支付方式支持的情况下,Waffo 此时也会请求支付方式关闭这笔支付。若不支持,支付就在他们那边保持打开,直到按他们自己的规则过期。 这就是为什么 TIMEOUT_CLOSE 和 TIMEOUT_CHANNEL_CLOSE 含义不同——两者的区分见 确认支付结果。这也是为什么上表第二行比商户预期的更重要:一个在授权开始之后停滞的订单,不会被定时器强制置为失败。

一笔支付是否算迟到,由谁的时钟决定——我的还是支付方式的?

支付方式的。你通过 orderExpiredAt 设置的过期时间,管的是用户可以在收银台页面提交订单的窗口。一旦支付已交给支付方式,由该支付方式自己的服务器时间决定这笔支付是否及时到达。 实际影响:
  • 支付方式一侧的网络延迟和重试,可能让一笔支付超出你的窗口,但按他们的时钟仍算及时。
  • 恰好在你的截止时间之前或之后到达支付方式的支付,仍然可以被正常处理。
  • 你的关单时间和支付方式的关单时间是两个不同的时钟,它们并不同步。
把你自己的过期时间当作 UI 和清理用的截止线,而不是资金上的截止线。

支付失败或用户一直没付时,我会收到通知吗?

订单关闭时会——但单次尝试失败时不一定会,而这两者之间的差异是「我们什么都没收到」类工单最常见的原因。 所以你等待的那条通知并没有丢——它在等订单结束。在此之前,确定状态的唯一方式是主动查询: /api/v1/order/inquiry。请在上线前就把轮询做好,而不是等第一次对账出现缺口之后;关单通知携带的内容见 每个状态下我能对订单做什么?。 一个可行的模式:订单打开期间每 30 秒轮询一次,最多轮询有限次数,然后在你自己的关单时间之后再轮询一次以捕获迟到的成功。一旦拿到最终状态就立即停止轮询。

我已经关闭订单之后才收到通知,这是 bug 吗?

不是,在若干支付方式上这是预期行为。支付方式的通知机制不会因为你的订单过期而停止。如果支付成功了,通知就会发送——而且如果没有被确认,它会被重试,有时会重试多次。 因此你的处理程序必须能接受一条针对你认为已「死掉」的订单的成功通知。具体来说:
  1. 永远不要因为订单在你这边已关闭就拒绝通知。接受它、确认它,然后事后对账。
  2. 让处理程序幂等——重试意味着同一事件会到达多次。见Webhook 重试。
  3. 用 Content-Type: application/json 和响应体 {"message":"success"} 进行确认,否则重试计划会持续触发。
  4. 一次性决定你的业务如何处理迟到的成功:履约,或退款。两者都合理;没有规则则不合理。

用户在超时后付款了,他们的钱会自动退回吗?

有时会。在某些卡组织钱包类支付方式上,在支付方式自身的支付超时之后到达的支付会被自动退回——支付方式自己发起退回,金额退回持卡人,你和 Waffo 都不需要做任何事。退回不是即时的——它按支付方式自己的时间表到达持卡人账户。这是支付方式自身的行为,不是 Waffo 的服务级别承诺。 在其他大多数支付方式上没有自动退回。迟到的成功是真实的成功:钱已经动了,订单已付款,如果你不想履约就必须退款——见 各支付方式的退款限制。 因为两种结果都存在,所以不要告诉用户「你会被自动退款」。先查订单。

我的前端在显示错误之前应该等多久?

设置一个明确的上限。对于跳转再返回的流程,合理的默认值是最多 5 分钟,等待期间每隔几秒轮询一次订单状态。之后:
  • 停止等待并显示中性的提示:支付无法确认,用户应重试。
  • **为重试创建一个新订单。**不要复用原订单。
  • 保持你服务端对旧订单的轮询继续运行——用户的浏览器放弃了,并不说明支付的任何情况。
不该做的事:在等待页上无限转圈,或在等待时间一到就显示「支付失败」。前者会流失用户,后者会造成重复支付。

3DS 认证超时了,可以在同一订单上重试吗?

不可以。3DS 认证有自己的过期时间,独立于你的订单过期时间——认证链接仅在生成后一段较短的固定时间内有效,比典型的订单过期时间短得多。如果用户没有在该窗口内完成认证,订单会因此进入失败状态,并且不能被复用。 正确的恢复方式是创建一个新订单。把用户送回旧的认证链接,或重新提交原订单,都不会有效: 注意,无感(frictionless)3DS 认证同样会产生一次用户很快穿过的跳转。看到那次跳转本身并不是结果——和其他所有地方一样的规则同样适用。

一次查询说支付失败,后来一次查询又说成功了,哪个是对的?

后一个。在若干支付方式上,一次失败的尝试不会关闭订单——用户可以在同一订单内自由再试,后续的尝试可以成功。在两次尝试之间执行的查询,如实地报告了那次失败的尝试。 一个带时间的序列示例: 由此得出的规则:**只有订单本身是最终的,失败才是最终的。**订单仍然打开时,把失败结果当作「这次尝试没成功」,而不是「这个订单永远不会被支付」。具体来说:
  • 不要在第一次失败时释放库存或取消购买。
  • 订单打开期间,不要因为一次失败就停止轮询。
  • 要给用户展示重试路径——在这些支付方式上,在现有订单内重试正是应该发生的事。
确实是最终的失败,是那些伴随已关闭订单状态而来的,或者在创建订单时就被拒绝的。

到底什么才算最终结果?

只有前两行可以驱动履约、记账或退款决定。其他一切都只是提示。

一套对超时安全的集成是什么样的?

一份可以用来对照你实现的检查清单:
  1. orderExpiredAt 是有意设置的,采用 UTC+0,并且你知道它只管收银台页面的窗口。
  2. 每个订单都有服务端轮询,用户关闭浏览器后依然继续。
  3. 你的 Webhook 处理程序会接受、确认并对账那些你已经关闭的订单的通知。
  4. 处理程序对同一事件的重复投递是幂等的。
  5. 重试会创建新订单;没有任何地方复用过期的订单。
  6. 你对迟到的成功有一条书面规则——履约或退款。
  7. 你的代码中没有任何地方把时间流逝、跳转或 E 类错误码当作最终结果。

接下来该看哪里?