PAYATHON 2026

收银台订单不存在或已过期,扫码支付提示订单超时

支付阿杰

结论

这类提示并不一定意味着支付失败。收银台查询订单时,如果订单已过期、状态不可用,或者平台没有查到对应订单,都可能提示订单超时。

如果已经收到支付成功回调,但扫码页面仍显示订单超时,应重点检查支付结果页与异步回调之间的状态同步。最终支付结果应以服务端查询到的平台状态,以及验签通过的异步通知为准,不能只看扫码页面的提示。

常见原因

1. 订单有效期设置过短

创建支付订单时设置了过期时间,用户打开二维码、扫码或确认支付时可能已经超出期限。还要检查时间单位是否正确。例如,接口要求传秒级时间戳,程序却传入了毫秒时间戳。

long nowSeconds = System.currentTimeMillis() / 1000;
long expireSeconds = nowSeconds + 15 * 60;

下面这种毫秒值不能直接传给只接受秒级时间戳的字段:

long expireMilliseconds = System.currentTimeMillis() + 15 * 60 * 1000;

具体的字段名称、时间格式和允许范围,应以所用支付接口的文档为准。

2. 服务器时间不准确

支付请求中的时间戳、签名时间和订单失效时间通常依赖服务器时钟。如果应用服务器与支付平台的时间相差较大,刚创建的订单也可能被判断为尚未生效或已经过期。

可以检查系统时间和 NTP 同步状态:

date
timedatectl status
chronyc tracking

在容器环境中,还要检查宿主机时间。集群部署时,需要确认所有节点的时间和时区配置一致。

3. 商户订单号重复

部分支付接口要求商户订单号全局唯一。重复使用已经支付、关闭或过期的订单号时,平台可能返回旧订单的状态,也可能直接拒绝创建新订单。

订单号应由服务端生成,并在数据库中建立唯一约束:

ALTER TABLE payment_order
ADD UNIQUE KEY uk_merchant_order_no (merchant_order_no);

例如:

String merchantOrderNo =
        "PAY" + System.currentTimeMillis() + randomSixDigits();

随机数只能降低冲突概率,数据库唯一约束仍然不能省。创建失败后能否复用原订单号,要按支付平台的规则处理。如果无法确认,建议生成新的订单号。

4. 二维码或支付链接被缓存

如果前端、CDN、反向代理或浏览器缓存了旧二维码,页面展示的可能还是上一笔订单的支付地址。用户扫码后,平台查到的自然也是已经过期或关闭的订单。

支付二维码接口应避免公共缓存:

Cache-Control: no-store, no-cache, must-revalidate
Pragma: no-cache
Expires: 0

前端刷新二维码时,需要确认:

  • 旧的二维码内容已被替换,而不只是重新渲染了图片;
  • 页面使用的是本次创建接口返回的支付地址;
  • 没有从 localStorage、Redis 或页面状态中读到上一笔订单;
  • CDN 和网关没有缓存创建订单接口的响应。

5. 请求发到了不同环境或商户账号

测试环境与生产环境混用,或者商户号发生切换、各节点使用的密钥不一致,都可能造成“创建成功后查不到订单”。

需要逐项核对:

  • 创建订单、查询订单和回调验签使用的商户号是否一致;
  • 各接口网关是否属于同一环境;
  • 应用 ID、渠道号、证书序列号和密钥是否匹配;
  • 多台应用服务器是否加载了相同配置;
  • 最近是否更换过证书、密钥、域名或网关路由。

日志中不要输出完整的私钥、密钥或用户敏感信息。

6. 本地订单写入与支付请求存在竞态

如果程序先调用支付平台,再异步写入本地订单,支付回调可能早于数据库事务提交。此时,回调处理程序查不到本地订单,可能直接忽略通知,前端轮询时仍会看到“待支付”或“已超时”。

更稳妥的处理顺序是:

  1. 在本地创建订单并提交事务;
  2. 调用支付平台创建支付单;
  3. 保存平台订单号和二维码;
  4. 接收异步通知并更新支付状态;
  5. 前端通过服务端接口查询结果。
@Transactional
public PaymentOrder prepareOrder(CreateOrderCommand command) {
    PaymentOrder order = new PaymentOrder();
    order.setMerchantOrderNo(generateOrderNo());
    order.setStatus("CREATED");
    order.setExpireAt(Instant.now().plusSeconds(15 * 60));
    paymentOrderRepository.insert(order);
    return order;
}

不要在一个长事务中调用外部支付接口,然后长时间不提交数据库事务。

7. 回调成功,但页面状态没有更新

收到支付成功回调,只能说明回调地址接到了请求。还要逐项确认后续处理是否成功:

  • 回调签名已经验证通过;
  • 回调中的商户号、订单号和金额与本地记录一致;
  • 数据库状态已从待支付更新为支付成功;
  • 回调接口按平台要求返回了成功响应;
  • 前端查询的是同一个订单号;
  • 查询接口没有读到延迟缓存或数据库从库中的旧数据。

状态更新需要具备幂等性:

UPDATE payment_order
SET status = 'PAID',
    paid_at = CURRENT_TIMESTAMP,
    platform_order_no = ?
WHERE merchant_order_no = ?
  AND status IN ('CREATED', 'PAYING');

如果更新行数为 0,不能直接认定支付失败。订单可能已经被另一次回调更新为 PAID,需要继续检查当前状态。

8. 前端超时逻辑与真实订单状态不一致

有些扫码页面使用本地倒计时。倒计时结束后,页面会直接显示“订单超时”,但用户可能已经在支付渠道完成付款,异步通知也可能已经到达。

倒计时结束时,不应立即把订单判定为失败,而应再次向服务端查询:

async function handleCountdownEnd(orderNo) {
  const response = await fetch(
    `/api/payment/orders/${encodeURIComponent(orderNo)}/status`,
    { cache: "no-store" }
  );

  const result = await response.json();

  if (result.status === "PAID") {
    showPaymentSuccess();
  } else if (result.status === "CLOSED" || result.status === "EXPIRED") {
    showOrderExpired();
  } else {
    showPaymentPending();
  }
}

如果异步通知长时间没有到达,服务端还应主动调用支付平台的订单查询接口进行补偿。不同平台的查询接口名称和参数并不相同,需要使用对应支付渠道提供的 API 或 SDK。

建议的排查顺序

第一步:用同一个订单号串联完整日志

日志中至少记录以下信息:

request_id
merchant_order_no
platform_order_no
merchant_id
request_timestamp
expire_time
create_request_time
create_response_time
qr_code_hash
callback_receive_time
callback_verify_result
local_order_status
platform_query_status

二维码内容可能包含敏感参数。生产日志应记录哈希值或脱敏后的内容,不要直接输出完整支付链接。

第二步:核对三个关键时间

针对一笔异常订单,对比以下时间:

  • 应用服务器创建订单的时间;
  • 请求中传给支付平台的时间戳和失效时间;
  • 支付平台响应、用户扫码以及回调到达的时间。

如果订单刚创建就被判定为过期,应优先检查时间单位、时区、服务器时钟和字段格式。

第三步:检查订单号和二维码是否匹配

确认页面展示的二维码确实属于当前订单,同时检查是否存在以下情况:

  • 商户订单号重复;
  • 创建请求被重复提交;
  • 浏览器返回了旧页面;
  • CDN 或接口缓存了旧响应;
  • 多个浏览器标签页相互覆盖订单状态。

第四步:主动查询支付平台订单状态

对于页面显示超时、但已经收到成功回调的订单,应使用商户订单号或平台订单号调用官方查询接口。

可以按以下原则处理:

  • 平台状态为成功:把本地订单补偿为支付成功;
  • 平台状态为待支付:继续轮询,或等订单到期后关闭;
  • 平台状态为关闭、撤销或不存在:创建新的订单;
  • 平台查询成功,但本地状态不一致:重点检查回调验签、事务、缓存和从库延迟。

第五步:检查最近两天的变更

如果此前联调正常,最近才突然出现问题,应重点查看近期是否有以下变化:

  • 支付平台证书或密钥发生更新;
  • 服务器重启、迁移或出现时间漂移;
  • 订单有效期被调整;
  • 新启用了 CDN、缓存或反向代理;
  • 系统扩容为多实例,但配置或会话没有同步;
  • 数据库、Redis 或消息队列出现延迟;
  • 支付平台发布了维护公告或接口出现异常。

处理原则

确认支付结果时,应按以下优先级判断:

  1. 支付平台订单查询接口返回的最终状态;
  2. 验签通过,并且金额、商户号和订单号均匹配的异步通知;
  3. 本地订单状态;
  4. 扫码页面或前端倒计时提示。

不要因为页面显示超时就自动退款、重新扣款,或把订单改成支付失败。订单状态不明确时,应先主动查询支付平台,再执行补单、关单或退款操作,避免重复支付和资金账务不一致。

备注:内容仅供参考。