手机网站支付关闭订单不回调及消息通知幂等处理
结论
timeout_express表示支付宝交易允许付款的时间范围。它不是业务系统的定时器,也不能保证到期瞬间一定会收到TRADE_CLOSED通知。业务系统需要自行维护订单超时时间,到期后主动查询交易状态,并在必要时调用关单接口。- 支付入账主要依据
TRADE_SUCCESS。TRADE_FINISHED表示交易已经结束,通常说明此前已支付成功,但业务系统不应等到这一状态才发货或开通服务。 - 异步通知需要围绕业务订单和状态流转做幂等处理。
notify_id可用于辅助记录和排查,但不适合作为唯一的去重依据。 - 支付结果不能只依赖异步通知。更稳妥的方案是以异步通知推动状态,用主动查询进行补偿,并通过数据库条件更新保证幂等。
为什么订单过期后没有收到关闭通知
timeout_express 控制支付宝侧的可支付期限。超过期限后,订单不能继续付款,但业务系统仍可能无法按预期收到关闭通知,常见原因包括:
- 交易的实际关闭时间可能晚于业务系统计算的到期时间,无法保证精确到秒。
- 异步通知通过网络传递,可能出现延迟、重复或发送失败。
notify_url无法从公网访问,HTTPS 配置异常,发生重定向,或响应超时。- 回调地址没有返回支付宝要求的成功响应。支付宝会继续重试,但业务系统最终仍可能没有正确处理通知。
- 程序在签名验证失败后直接丢弃通知,又没有留下足够的日志。
- 创建支付请求时没有正确传入异步通知地址,或相关参数未按当前接口要求生效。
- 用户没有真正进入支付宝收银台。部分业务链路虽然已经创建商户订单,但支付宝交易可能尚未成功建立。
- 支付宝已经关闭交易,但通知处理程序受应用发布、网关拦截或数据库异常等因素影响,没有完成处理。
因此,本地订单不能依靠 TRADE_CLOSED 通知准时释放库存或关闭订单。
推荐的超时关单流程
创建业务订单时,应同时保存本地过期时间,例如 expire_at。定时任务扫描已经到期、状态仍为“待支付”的订单,并按以下流程处理:
- 调用
alipay.trade.query查询支付宝交易状态。 - 返回
TRADE_SUCCESS或TRADE_FINISHED时,按支付成功处理。 - 返回
WAIT_BUYER_PAY时,调用alipay.trade.close主动关闭交易。 - 交易不存在时,根据业务规则关闭本地订单,同时保留查询结果和请求日志。
- 使用数据库条件更新,只允许“待支付”订单变更为“已支付”或“已关闭”,避免并发覆盖。
- 查询或关单失败时安排重试,不要直接认定交易已经关闭。
伪代码如下:
void handleExpiredOrder(Order order) {
if (order.getStatus() != OrderStatus.PENDING) {
return;
}
AlipayTradeQueryResponse response = queryAlipayTrade(order.getOrderNo());
if (response.isSuccess()) {
switch (response.getTradeStatus()) {
case "TRADE_SUCCESS":
case "TRADE_FINISHED":
confirmPayment(order, response.getTradeNo(), response.getTotalAmount());
return;
case "WAIT_BUYER_PAY":
closeAlipayTrade(order.getOrderNo());
closeLocalOrderIfPending(order.getOrderNo());
return;
case "TRADE_CLOSED":
closeLocalOrderIfPending(order.getOrderNo());
return;
default:
scheduleRetry(order.getOrderNo());
return;
}
}
if (isTradeNotExist(response)) {
closeLocalOrderIfPending(order.getOrderNo());
} else {
scheduleRetry(order.getOrderNo());
}
}
主动关单和用户支付可能同时发生,因此不能简单地先修改本地订单,再调用支付宝关单。无论处理查询结果、关单结果还是异步通知,都需要重新检查订单的当前状态。
如何处理 TRADE_SUCCESS 和 TRADE_FINISHED
常见交易状态的业务含义如下:
WAIT_BUYER_PAY:交易已创建,正在等待买家付款。TRADE_CLOSED:未付款交易已关闭,或者交易已按接口规则关闭。TRADE_SUCCESS:付款成功,交易后续仍可能处于可退款等阶段。TRADE_FINISHED:交易已经结束,通常不能再通过普通交易流程继续操作。
收到 TRADE_SUCCESS 后即可确认支付到账,并执行发货、充值或开通会员等业务动作。
如果系统第一次收到的状态是 TRADE_FINISHED,也不能直接忽略。该状态通常表示交易已经成功完成,处理程序应先检查本地订单:
- 本地已经支付:只更新必要的终态信息,不再重复发货。
- 本地仍为待支付:校验交易信息后补记支付成功,再把交易阶段更新为已结束。
- 本地已经关闭:进入人工或自动对账流程,不能直接覆盖原有状态。
状态流转可以设计为:
PENDING -> PAID -> FINISHED
PENDING -> CLOSED
FINISHED 是交易生命周期的终态,但商户确认首次收款不需要等到这个状态。
回调验签和业务校验
收到通知后,不能只检查 trade_status,至少还要完成以下校验:
- 使用支付宝公钥验证签名。
- 核对
app_id是否属于当前应用。 - 核对
seller_id或收款账号是否属于当前商户。 - 根据
out_trade_no查找本地订单。 - 核对
total_amount、币种和订单关联商品。 - 核对
trade_no,避免同一个支付宝交易号绑定到不同的业务订单。 - 检查当前本地订单状态是否允许此次状态迁移。
以 Java SDK 为例,验签代码可以写成:
boolean verified = AlipaySignature.rsaCheckV1(
requestParams,
alipayPublicKey,
"UTF-8",
"RSA2"
);
if (!verified) {
throw new SecurityException("Invalid Alipay notification signature");
}
具体的方法签名应以项目实际使用的支付宝 SDK 版本为准,不要自行拼接待验签字符串。
幂等处理不能只依赖 notify_id
notify_id 是通知层面的标识,适合记录在通知日志中,用来定位某次回调,但不适合作为支付业务唯一的幂等键,原因有以下几点:
- 消息去重与业务幂等不是一回事。
- 同一笔交易可能因状态变化产生多次通知。
- 系统也可能通过主动查询、人工补单或对账文件确认支付,而这些流程没有相同的
notify_id。 - 即使通知标识不同,同一个业务订单也不能重复发货或重复入账。
可以分成消息层和业务层处理:
- 消息层记录
notify_id、trade_no、通知状态和原始报文摘要,用于审计和快速过滤重复请求。 - 业务层以
out_trade_no为核心,通过数据库唯一约束和条件更新,保证订单只能成功入账一次。
例如:
UPDATE payment_order
SET status = 'PAID',
alipay_trade_no = :tradeNo,
paid_at = :paidAt,
updated_at = CURRENT_TIMESTAMP
WHERE order_no = :orderNo
AND status = 'PENDING';
随后检查受影响的行数:
int updated = paymentOrderRepository.markPaidIfPending(
orderNo,
tradeNo,
paidAt
);
if (updated == 1) {
createFulfillmentEvent(orderNo);
} else {
verifyExistingPaymentResult(orderNo, tradeNo);
}
数据库还应设置必要的唯一约束:
ALTER TABLE payment_order
ADD CONSTRAINT uk_payment_order_no UNIQUE (order_no);
ALTER TABLE payment_order
ADD CONSTRAINT uk_alipay_trade_no UNIQUE (alipay_trade_no);
如果支付成功后还要发货、充值或发送消息,最好使用本地事务消息或 Outbox 模式,让“订单改为已支付”和“生成后续业务事件”在同一个数据库事务中完成:
@Transactional
public void confirmPayment(PaymentNotification notification) {
int updated = orderRepository.markPaidIfPending(
notification.getOutTradeNo(),
notification.getTradeNo(),
notification.getPaidAt()
);
if (updated == 1) {
outboxRepository.insert(
"PAYMENT_SUCCEEDED",
notification.getOutTradeNo()
);
}
}
消费 PAYMENT_SUCCEEDED 事件时仍需保证幂等。例如,可以为“订单号 + 业务动作类型”建立唯一索引,防止重复发货。
回调接口的正确响应方式
回调处理成功后,应按支付宝接口要求返回纯文本成功标识,通常为:
success
不要返回 JSON、HTML 页面或额外的提示文字。处理失败时也不要伪造成功响应,否则支付宝可能停止重试。
回调接口还需注意以下事项:
- 不要在回调线程中执行耗时的发货、短信或第三方调用。
- 完成验签和核心状态落库后,尽快返回响应。
- 保存必要的结构化日志,但不要记录私钥等敏感信息。
- 遇到签名失败、金额不一致或商户身份不一致等情况时及时告警。
- 回调地址不能依赖登录状态,也不要执行浏览器跳转。
return_url是用户浏览器的同步跳转地址,不能作为支付成功的依据。- 长期保留定时查询和对账任务,用于修复通知丢失、程序异常和极端并发造成的问题。
支付系统需要能够应对通知延迟、重复、乱序甚至丢失。异步通知用于及时推动状态,主动查询和对账用于补偿,数据库状态机与唯一约束则保证每项业务只执行一次。
备注:内容仅供参考。
