收银台订单不存在或已过期,扫码支付提示订单超时
结论
这类提示并不一定意味着支付失败。收银台查询订单时,如果订单已过期、状态不可用,或者平台没有查到对应订单,都可能提示订单超时。
如果已经收到支付成功回调,但扫码页面仍显示订单超时,应重点检查支付结果页与异步回调之间的状态同步。最终支付结果应以服务端查询到的平台状态,以及验签通过的异步通知为准,不能只看扫码页面的提示。
常见原因
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. 本地订单写入与支付请求存在竞态
如果程序先调用支付平台,再异步写入本地订单,支付回调可能早于数据库事务提交。此时,回调处理程序查不到本地订单,可能直接忽略通知,前端轮询时仍会看到“待支付”或“已超时”。
更稳妥的处理顺序是:
- 在本地创建订单并提交事务;
- 调用支付平台创建支付单;
- 保存平台订单号和二维码;
- 接收异步通知并更新支付状态;
- 前端通过服务端接口查询结果。
@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 或消息队列出现延迟;
- 支付平台发布了维护公告或接口出现异常。
处理原则
确认支付结果时,应按以下优先级判断:
- 支付平台订单查询接口返回的最终状态;
- 验签通过,并且金额、商户号和订单号均匹配的异步通知;
- 本地订单状态;
- 扫码页面或前端倒计时提示。
不要因为页面显示超时就自动退款、重新扣款,或把订单改成支付失败。订单状态不明确时,应先主动查询支付平台,再执行补单、关单或退款操作,避免重复支付和资金账务不一致。
备注:内容仅供参考。
