PAYATHON 2026

支付宝取消订单后仍可付款成功,如何解决?

支付阿杰

结论

不能只关闭本地订单。只要支付宝付款页面、支付链接或支付凭证已经生成,用户就仍有可能完成付款。如果取消订单时,支付宝尚未创建可查询的交易,调用 alipay.trade.close 也未必能阻止后续交易生成。

建议按以下方式处理:

  1. 将本地订单设为“取消处理中”,不要立即认定支付链路已经终止。
  2. 使用同一个 out_trade_no 持续查询支付宝交易。
  3. 交易创建后,立即调用 alipay.trade.close。
  4. 设置合理的支付有效期,避免旧付款页面长期可用。
  5. 收到异步通知后,再次校验订单状态。如果本地订单已经关闭,但支付宝付款成功,应自动退款或转入人工处理队列。

为什么取消后仍然能够付款

本地订单和支付宝交易各有一套独立的状态。

用户打开支付宝收银台后,即使支付宝当时还查不到对应交易,已经生成的付款页面或支付凭证仍可能继续执行。系统只把本地订单改为 CLOSED,并不会让支付宝侧的支付入口同时失效。

取消和支付之间还可能出现这样的时序竞争:

用户进入收银台
    ↓
用户点击取消订单
    ↓
系统查询支付宝:交易不存在
    ↓
系统关闭本地订单
    ↓
支付宝侧创建交易
    ↓
用户重新登录并完成付款

所以,“查询不到支付宝交易”只说明当前没有查到,不能据此认定之后不会创建交易。

推荐的订单状态设计

订单状态至少应区分为:

PENDING_PAYMENT   待支付
CANCEL_PENDING    取消处理中
CLOSED            已关闭
PAID              已支付
REFUND_PENDING    退款处理中
REFUNDED          已退款
PAYMENT_EXCEPTION 支付状态异常

取消订单时,先通过原子更新把 PENDING_PAYMENT 改为 CANCEL_PENDING,然后查询并关闭支付宝交易。确认支付链路处理完成后,再把本地订单改为 CLOSED。

如果取消操作和支付成功同时发生,应以支付宝查询结果及通过验签的异步通知为准,再按照业务规则决定继续履约还是退款。

取消订单的处理流程

1. 禁止继续生成新的支付请求

订单进入 CANCEL_PENDING 或 CLOSED 后,支付接口必须拒绝再次生成付款页面:

if (order.getStatus() != OrderStatus.PENDING_PAYMENT) {
    throw new IllegalStateException("当前订单状态不允许支付");
}

同一个业务订单应始终使用固定且唯一的 out_trade_no。不能因为用户重新发起支付,就随意生成新的商户订单号,否则支付宝侧可能产生多笔独立交易。

2. 查询并关闭支付宝交易

先调用 alipay.trade.query 查询交易。如果交易已经存在但尚未付款,再调用 alipay.trade.close。

以下是省略 SDK 细节的 Java 风格示例:

public void cancelOrder(String orderNo) {
    Order order = orderRepository.findByOrderNoForUpdate(orderNo);

    boolean changed = orderRepository.compareAndSetStatus(
        orderNo,
        OrderStatus.PENDING_PAYMENT,
        OrderStatus.CANCEL_PENDING
    );

    if (!changed && order.getStatus() != OrderStatus.CANCEL_PENDING) {
        return;
    }

    AlipayTrade trade = alipayClient.query(orderNo);

    if (trade == null) {
        // 支付宝暂时查不到交易,不代表之后不会创建。
        closeRetryService.schedule(orderNo);
        return;
    }

    switch (trade.getTradeStatus()) {
        case "WAIT_BUYER_PAY":
            alipayClient.close(orderNo);
            orderRepository.updateStatus(orderNo, OrderStatus.CLOSED);
            break;

        case "TRADE_SUCCESS":
        case "TRADE_FINISHED":
            handlePaidAfterCancel(orderNo, trade);
            break;

        case "TRADE_CLOSED":
            orderRepository.updateStatus(orderNo, OrderStatus.CLOSED);
            break;

        default:
            orderRepository.updateStatus(
                orderNo,
                OrderStatus.PAYMENT_EXCEPTION
            );
    }
}

如果第一次查询返回“交易不存在”,可以通过延迟任务进行有限次数的重试。例如,间隔数秒再次调用 alipay.trade.query,发现交易状态变为 WAIT_BUYER_PAY 后,立即执行 alipay.trade.close。具体的重试间隔和持续时间,需要结合业务时效、流量以及支付宝接口的限流策略确定。

3. 设置支付有效期

创建支付请求时,应设置当前产品接口支持的支付有效期参数。例如,部分支付宝支付接口支持 timeout_express。参数是否可用以及具体格式,应以实际接入的支付宝产品和当前接口文档为准。

bizContent.put("out_trade_no", orderNo);
bizContent.put("total_amount", amount);
bizContent.put("subject", subject);
bizContent.put("timeout_express", "15m");

支付有效期只能缩短付款窗口,不能替代订单查询、关单、异步通知和退款补偿。

4. 正确处理异步通知

不能因为本地订单已经关闭,就直接向支付宝返回成功并忽略异步通知。处理通知时,必须完成以下校验:

  • 验证支付宝签名。
  • 校验 app_id、seller_id 等商户身份信息。
  • 校验 out_trade_no。
  • 校验付款金额和币种。
  • 确认通知对应当前商户订单。
  • 对通知进行幂等处理。

示例:

@Transactional
public void handleAlipayNotify(Map<String, String> params) {
    if (!alipaySignatureVerifier.verify(params)) {
        throw new SecurityException("支付宝通知验签失败");
    }

    String orderNo = params.get("out_trade_no");
    String tradeNo = params.get("trade_no");
    String tradeStatus = params.get("trade_status");

    Order order = orderRepository.findByOrderNoForUpdate(orderNo);
    verifyMerchantAndAmount(order, params);

    if (!"TRADE_SUCCESS".equals(tradeStatus)
            && !"TRADE_FINISHED".equals(tradeStatus)) {
        return;
    }

    if (paymentRepository.existsByTradeNo(tradeNo)) {
        return;
    }

    paymentRepository.save(orderNo, tradeNo, params);

    if (order.getStatus() == OrderStatus.PENDING_PAYMENT) {
        orderRepository.updateStatus(orderNo, OrderStatus.PAID);
        return;
    }

    if (order.getStatus() == OrderStatus.CANCEL_PENDING
            || order.getStatus() == OrderStatus.CLOSED) {
        orderRepository.updateStatus(
            orderNo,
            OrderStatus.REFUND_PENDING
        );
        refundService.submit(orderNo, tradeNo);
    }
}

只有可靠记录通知,并把订单纳入后续处理流程后,才能向支付宝返回成功响应,以免通知重试引发重复处理。

已关闭订单付款成功后的处理

遇到这种情况,不能只修改数据库状态,也不能丢弃支付通知,因为资金已经实际进入商户账户。

通常有两种处理策略:

自动退款

如果业务不允许取消后的订单继续履约,可以调用 alipay.trade.refund 原路退款。退款请求号必须唯一,同时要做好幂等控制:

public void refundPaidClosedOrder(Order order, Payment payment) {
    String refundRequestNo = "CANCEL_REFUND_" + order.getOrderNo();

    RefundResult result = alipayClient.refund(
        payment.getTradeNo(),
        order.getPaidAmount(),
        refundRequestNo
    );

    if (result.isSuccess()) {
        orderRepository.updateStatus(
            order.getOrderNo(),
            OrderStatus.REFUNDED
        );
    } else {
        orderRepository.updateStatus(
            order.getOrderNo(),
            OrderStatus.PAYMENT_EXCEPTION
        );
    }
}

实际接入时,还应保存退款请求、支付宝响应和后续查询结果。不能只根据一次接口调用,就认定退款已经最终完成。

恢复为已支付

如果业务允许在取消和支付并发时继续履约,也可以把订单从 CANCEL_PENDING 改为 PAID。不过,在恢复订单前,必须确认库存、价格和服务资格仍然有效。对于库存已经释放或优惠已经失效的订单,不能无条件恢复。

具体采用哪种策略,应由明确的业务规则决定,不能交给回调代码临时判断。

还需要注意什么

  • 本地关单、支付宝关单和支付回调无法组成一个数据库事务,需要通过状态机、幂等处理和补偿任务保证最终一致性。
  • 调用 alipay.trade.close 前后都有可能发生付款。系统应检查关单响应,必要时再次调用 alipay.trade.query 确认最终状态。
  • 定时对账时,应重点检查“本地已关闭、支付宝已支付”和“本地已支付、支付宝未成功”等异常组合。
  • 不能只根据浏览器同步跳转判断支付成功。服务端应通过异步通知和主动查询确认支付结果。
  • 数据库状态更新应使用条件更新或行锁,防止取消请求和支付通知相互覆盖。
  • 应保存支付宝的 trade_no、out_trade_no、交易状态、通知内容、关单结果和退款结果,方便审计和排查问题。
  • 不要复用已关闭订单的 out_trade_no 创建另一笔业务订单。用户重新购买时,应创建新的本地订单和新的商户订单号。

本地订单关闭并不等于支付宝交易关闭。系统需要分别处理“未创建交易”“待付款”“付款成功”和“已关闭”等状态,并针对取消与付款并发的情况,准备查询、关单、退款和对账补偿机制。

备注:内容仅供参考。