> ## Documentation Index
> Fetch the complete documentation index at: https://waffo.com/docs/llms.txt
> Use this file to discover all available pages before exploring further.

# 超时与结果何时算最终

> 为什么订单超时不能证明支付失败：Waffo 在关单时间做了什么、超时由谁的时钟决定、哪些失败会以通知形式到达你、哪些只有查询才能看到、迟到的通知与重试、迟到成功后的自动退回、独立的 3DS 认证超时，以及你的前端应该等多久。

[确认支付结果](/docs/zh/support/payment-results-faq)确立了本页所依赖的规则：\*\*跳转永远不是支付成功的证明，权威结果是 Webhook Webhook 通知，Order Inquiry 查询作为兜底。\*\*本页讲的是这条规则的后半部分——一个结果*何时*才允许被视为最终结果，以及为什么超时不属于这些时刻。

> \*\*只需记住这一句：\*\*订单超时意味着 Waffo 停止等待了。它不意味着支付方式停止了，也不意味着用户不能继续付款。

## 我的订单超时了，支付是不是肯定不会发生了？

不是。超时是 Waffo 对你的订单做出的决定，不是支付方式对这笔支付做出的决定。

对于不少支付方式，你的订单关闭之后，支付在支付方式一侧仍处于非最终状态——他们那边根本没有自动关闭。在这种情况下：

* 用户仍然可以打开支付页面或 App 并完成付款。
* 支付可以在订单显示为超时**之后**成功。
* 结果通知可以在关单之后到达。

因此，超时的订单是一种*暂定*的失败。不要销毁购买记录，不要不可逆地释放预留的座位或许可，也不要告诉用户「支付失败了，不会扣钱」——告诉他们这次尝试没有完成，再次付款前应先核实。

## 订单到达关单时间时，Waffo 实际做了什么？

分两种情况，取决于支付进行到了哪一步：

| 订单进行到哪一步 | 关单时间发生什么 |
| - | - |
| 用户从未提交支付——订单从未进入支付尝试 | Waffo 关闭订单并向你报告超时 |
| 该订单已经存在一次支付尝试 | 定时关单会**跳过**它——订单留待按真实结果收敛，而不是被强制置为失败 |

在支付方式支持的情况下，Waffo 此时也会请求支付方式关闭这笔支付。若不支持，支付就在他们那边保持打开，直到按他们自己的规则过期。

这就是为什么 `TIMEOUT_CLOSE` 和 `TIMEOUT_CHANNEL_CLOSE` 含义不同——两者的区分见
[确认支付结果](/docs/zh/support/payment-results-faq)。这也是为什么上表第二行比商户预期的更重要：一个在授权开始*之后*停滞的订单，不会被定时器强制置为失败。

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

支付方式的。你通过 `orderExpiredAt` 设置的过期时间，管的是用户可以在收银台页面提交订单的窗口。一旦支付已交给支付方式，**由该支付方式自己的服务器时间决定**这笔支付是否及时到达。

实际影响：

* 支付方式一侧的网络延迟和重试，可能让一笔支付超出你的窗口，但按他们的时钟仍算及时。
* 恰好在你的截止时间之前或之后到达支付方式的支付，仍然可以被正常处理。
* 你的关单时间和支付方式的关单时间是两个不同的时钟，它们并不同步。

把你自己的过期时间当作 UI 和清理用的截止线，而不是资金上的截止线。

## 支付失败或用户一直没付时，我会收到通知吗？

**订单**关闭时会——但单次**尝试**失败时不一定会，而这两者之间的差异是「我们什么都没收到」类工单最常见的原因。

| 结果 | 通知 |
| - | - |
| 支付成功 | `PAYMENT_NOTIFICATION`，`orderStatus` = `PAY_SUCCESS` |
| 订单关闭——因超时、因取消，或因支付方式报告了最终失败 | `PAYMENT_NOTIFICATION`，`orderStatus` = `ORDER_CLOSE`，原因在 `orderFailedReason` 中。这是 Waffo 自己发送的，不依赖支付方式 |
| 一次尝试失败但订单仍然打开 | \*\*暂时没有。\*\*在若干支付方式上，一次失败的尝试不会关闭订单——用户可以在订单内重试——所以在订单本身到达最终状态之前不会产生通知 |
| 用户一直没付 | 直到订单过期关闭前都没有；之后是上面的 `ORDER_CLOSE` 通知 |

所以你等待的那条通知并没有丢——它在等订单结束。在此之前，确定状态的唯一方式是主动查询：
`/api/v1/order/inquiry`。请在上线前就把轮询做好，而不是等第一次对账出现缺口之后；关单通知携带的内容见
[每个状态下我能对订单做什么？](/docs/zh/support/order-status-actions-and-capture-faq)。

一个可行的模式：订单打开期间每 30 秒轮询一次，最多轮询有限次数，然后在你自己的关单时间之后再轮询一次以捕获迟到的成功。一旦拿到最终状态就立即停止轮询。

## 我已经关闭订单之后才收到通知，这是 bug 吗？

不是，在若干支付方式上这是预期行为。支付方式的通知机制不会因为你的订单过期而停止。如果支付成功了，通知就会发送——而且如果没有被确认，它会被**重试**，有时会重试多次。

因此你的处理程序必须能接受一条针对你认为已「死掉」的订单的成功通知。具体来说：

1. 永远不要因为订单在你这边已关闭就拒绝通知。接受它、确认它，然后事后对账。
2. 让处理程序幂等——重试意味着同一事件会到达多次。见[Webhook 重试](/docs/zh/developer-docs/webhook/retry)。
3. 用 `Content-Type: application/json` 和响应体
   `{"message":"success"}` 进行确认，否则重试计划会持续触发。
4. 一次性决定你的业务如何处理迟到的成功：履约，或退款。两者都合理；没有规则则不合理。

## 用户在超时后付款了，他们的钱会自动退回吗？

有时会。在某些卡组织钱包类支付方式上，在支付方式自身的支付超时之后到达的支付会被**自动退回**——支付方式自己发起退回，金额退回持卡人，你和 Waffo 都不需要做任何事。退回不是即时的——它按支付方式自己的时间表到达持卡人账户。这是支付方式自身的行为，不是 Waffo 的服务级别承诺。

在其他大多数支付方式上**没有**自动退回。迟到的成功是真实的成功：钱已经动了，订单已付款，如果你不想履约就必须退款——见
[各支付方式的退款限制](/docs/zh/support/payment-method-refund-limits-faq)。

因为两种结果都存在，所以不要告诉用户「你会被自动退款」。先查订单。

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

设置一个明确的上限。对于跳转再返回的流程，合理的默认值是**最多 5 分钟**，等待期间每隔几秒轮询一次订单状态。之后：

* 停止等待并显示中性的提示：支付无法确认，用户应重试。
* \*\*为重试创建一个新订单。\*\*不要复用原订单。
* 保持你服务端对旧订单的轮询继续运行——用户的浏览器放弃了，并不说明支付的任何情况。

不该做的事：在等待页上无限转圈，或在等待时间一到就显示「支付失败」。前者会流失用户，后者会造成重复支付。

## 3DS 认证超时了，可以在同一订单上重试吗？

不可以。3DS 认证有**自己的过期时间，独立于你的订单过期时间**——认证链接仅在生成后一段较短的固定时间内有效，比典型的订单过期时间短得多。如果用户没有在该窗口内完成认证，订单会因此进入失败状态，并且**不能被复用**。

正确的恢复方式是**创建一个新订单**。把用户送回旧的认证链接，或重新提交原订单，都不会有效：

| 情形 | 操作 |
| - | - |
| 用户放弃了 3DS 页面，之后又回来 | 创建新订单 |
| 认证链接已过期 | 创建新订单 |
| 认证已完成但还没有结果 | 等待 / 轮询——这不是超时 |

注意，无感（frictionless）3DS 认证同样会产生一次用户很快穿过的跳转。看到那次跳转本身并不是结果——和其他所有地方一样的规则同样适用。

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

后一个。在若干支付方式上，一次失败的尝试**不会**关闭订单——用户可以在同一订单内自由再试，后续的尝试可以成功。在两次尝试之间执行的查询，如实地报告了那次失败的尝试。

一个带时间的序列示例：

| 时间 | 发生了什么 | 查询返回什么 |
| - | - | - |
| 1:00 | 用户第一次尝试失败 | — |
| 1:03 | 你查询 | 失败 |
| 1:05 | 用户再试并成功 | — |
| 1:06 | 你查询 | 成功 |

由此得出的规则：\*\*只有订单本身是最终的，失败才是最终的。\*\*订单仍然打开时，把失败结果当作「这次尝试没成功」，而不是「这个订单永远不会被支付」。具体来说：

* 不要在第一次失败时释放库存或取消购买。
* 订单打开期间，不要因为一次失败就停止轮询。
* 要给用户展示重试路径——在这些支付方式上，在现有订单内重试正是应该发生的事。

*确实*是最终的失败，是那些伴随已关闭订单状态而来的，或者在创建订单时就被拒绝的。

## 到底什么才算最终结果？

| 信号 | 最终？ |
| - | - |
| Webhook 支付结果报告了最终订单状态 | **是** |
| `/api/v1/order/inquiry` 返回了最终订单状态 | **是** |
| 跳转回你的站点 | 否 |
| 你自己的超时时间到了 | 否 |
| `E`xxxxx 类「状态未知」响应 | 否——继续查询 |
| 用户的截图或口头说法 | 否 |

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

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

一份可以用来对照你实现的检查清单：

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

## 接下来该看哪里？

| 问题 | 页面 |
| - | - |
| 每个 `orderStatus` 的含义、`webUrl` 与 `successRedirectUrl` 的区别、Webhook 缺失 | [确认支付结果](/docs/zh/support/payment-results-faq) |
| 会保持打开数天的银行转账和虚拟账户订单 | [银行转账与虚拟账户](/docs/zh/support/bank-transfer-and-virtual-account-faq) |
| 对迟到的成功进行退款 | [各支付方式的退款限制](/docs/zh/support/payment-method-refund-limits-faq) |
| 创建订单时看到的错误码 | [错误码与限额补充](/docs/zh/support/error-codes-and-limits-supplement) |


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.