PAYATHON 2026

用户中途退出支付后,如何发起二次支付?

支付小周

结论

不要重新创建业务订单,也不要给业务订单改名。用户再次点击「去支付」时,先查询本地订单,再向支付宝确认支付状态:

  • 已支付:更新本地状态,进入支付成功流程。
  • 待支付,且原支付请求仍然有效:继续使用原支付交易。
  • 已关闭、已超时或无法继续支付:保留原业务订单,创建新的支付尝试,并使用新的 out_trade_no 发起支付。

这样可以避免产生重复订单、重复付款以及库存统计混乱。至于同一个 out_trade_no 能否再次唤起支付,需要根据所用支付宝产品及接口规则判断,不能只看本地订单状态。

业务订单和支付流水要分开

业务订单记录用户购买的内容,支付流水记录每一次付款尝试。两者不应共用同一套生命周期。

例如:

业务订单号:ORDER_202609130001

第一次支付流水:PAY_202609130001_01
第二次支付流水:PAY_202609130001_02

用户退出支付页面,不等于业务订单失效,也不能直接判定支付失败。此时通常只能说明系统还没有拿到明确的支付结果。

建议至少保存以下数据:

order
- order_no
- amount
- status
- expire_time

payment
- payment_no
- order_no
- channel
- out_trade_no
- gateway_trade_no
- status
- expire_time

传给支付宝的 out_trade_no 最好使用支付流水号,不要直接使用业务订单号。这样既能保留原订单,也可以在需要时发起新的支付尝试。

二次支付的处理流程

1. 查询本地业务订单

先检查订单是否存在,并确认:

  • 当前用户是否有权支付该订单;
  • 金额是否仍然有效;
  • 商品、库存和优惠是否仍然有效;
  • 订单是否已经取消或超过业务有效期。

不要采用前端提交的金额。支付金额必须从服务端订单数据中读取。

2. 检查是否已经支付

如果本地订单已经处于支付成功状态,直接返回成功结果,不要再次发起支付。

如果本地仍显示未支付,但此前已经创建过支付流水,应调用 alipay.trade.query 查询支付宝交易状态。常见状态包括:

  • TRADE_SUCCESS:支付成功;
  • TRADE_FINISHED:交易结束且不可退款,具体含义需要结合所用产品确认;
  • WAIT_BUYER_PAY:等待用户付款;
  • TRADE_CLOSED:交易已关闭;
  • 查询不到交易:交易可能尚未成功创建,也可能是请求参数或商户订单号不一致。

如果支付宝已经显示支付成功,而本地状态没有更新,应先补偿本地订单状态,再向用户展示支付成功页面。

3. 决定继续原支付还是创建新支付流水

支付宝返回 WAIT_BUYER_PAY 时,可以优先继续原交易,但需要确认当前支付产品支持再次唤起,而且原支付链接或支付参数仍然有效。

如果原交易已经关闭、超时,或者原支付参数无法再次使用,则按以下步骤处理:

  1. 关闭仍在等待支付的旧交易,避免多笔交易同时处于可支付状态;
  2. 创建新的支付流水;
  3. 为新流水生成新的 out_trade_no;
  4. 按原业务订单的应付金额重新发起支付。

业务订单号保持不变,只新增一条支付尝试记录。

Java 示例

以下代码只用于说明处理思路,省略了签名配置、数据库事务、异常分类和具体返回对象。接口字段需要以项目实际使用的支付宝 SDK 版本为准。

public String retryPay(String orderNo, Long userId) throws AlipayApiException {
    Order order = orderRepository.findByOrderNoForUpdate(orderNo);

    if (order == null || !order.getUserId().equals(userId)) {
        throw new IllegalArgumentException("订单不存在");
    }

    if (order.getStatus() == OrderStatus.PAID) {
        return "/pay/success?orderNo=" + orderNo;
    }

    if (order.getStatus() == OrderStatus.CANCELLED
            || order.getExpireTime().isBefore(LocalDateTime.now())) {
        throw new IllegalStateException("订单已失效");
    }

    Payment oldPayment =
            paymentRepository.findLatestByOrderNo(orderNo, "ALIPAY");

    if (oldPayment != null) {
        AlipayTradeQueryRequest queryRequest = new AlipayTradeQueryRequest();
        queryRequest.setBizContent("""
            {
              "out_trade_no": "%s"
            }
            """.formatted(oldPayment.getOutTradeNo()));

        AlipayTradeQueryResponse queryResponse =
                alipayClient.execute(queryRequest);

        if (queryResponse.isSuccess()) {
            String tradeStatus = queryResponse.getTradeStatus();

            if ("TRADE_SUCCESS".equals(tradeStatus)
                    || "TRADE_FINISHED".equals(tradeStatus)) {
                confirmPaid(
                    order,
                    oldPayment,
                    queryResponse.getTradeNo()
                );

                return "/pay/success?orderNo=" + orderNo;
            }

            if ("WAIT_BUYER_PAY".equals(tradeStatus)
                    && oldPayment.getExpireTime().isAfter(LocalDateTime.now())) {
                /*
                 * 此处可以重新生成用于唤起原交易的支付响应。
                 * 是否允许重复提交同一 out_trade_no,以及响应是否可以直接复用,
                 * 需要按照实际使用的支付宝产品和接口规则处理。
                 */
                return buildAlipayPagePay(order, oldPayment);
            }
        }

        closeOldPaymentIfNecessary(oldPayment);
    }

    Payment newPayment = createPaymentAttempt(order);
    return buildAlipayPagePay(order, newPayment);
}

创建新支付流水时,需要生成唯一的 out_trade_no:

private Payment createPaymentAttempt(Order order) {
    int attemptNo =
            paymentRepository.countByOrderNo(order.getOrderNo()) + 1;

    String outTradeNo = order.getOrderNo()
            + "_"
            + String.format("%02d", attemptNo);

    Payment payment = new Payment();
    payment.setOrderNo(order.getOrderNo());
    payment.setOutTradeNo(outTradeNo);
    payment.setAmount(order.getPayAmount());
    payment.setStatus(PaymentStatus.CREATED);
    payment.setExpireTime(order.getExpireTime());

    paymentRepository.save(payment);
    return payment;
}

发起网页支付的示意代码如下:

private String buildAlipayPagePay(Order order, Payment payment)
        throws AlipayApiException {

    AlipayTradePagePayRequest request =
            new AlipayTradePagePayRequest();

    request.setNotifyUrl(alipayNotifyUrl);
    request.setReturnUrl(alipayReturnUrl);

    request.setBizContent("""
        {
          "out_trade_no": "%s",
          "total_amount": "%s",
          "subject": "%s",
          "product_code": "FAST_INSTANT_TRADE_PAY"
        }
        """.formatted(
            payment.getOutTradeNo(),
            order.getPayAmount().toPlainString(),
            escapeJson(order.getSubject())
        ));

    AlipayTradePagePayResponse response =
            alipayClient.pageExecute(request);

    if (!response.isSuccess()) {
        throw new IllegalStateException(
            "发起支付宝支付失败:" + response.getSubMsg()
        );
    }

    return response.getBody();
}

支付回调必须保证幂等

不能把同步跳转页作为支付成功的最终依据。用户可能关闭页面,网络也可能中断,支付宝异步通知还可能晚于页面跳转到达。

服务端收到异步通知后,需要完成以下校验和处理:

  • 验证支付宝签名;
  • 校验 app_id、商户身份等字段;
  • 根据 out_trade_no 查找支付流水;
  • 校验通知金额是否与数据库金额一致;
  • 校验交易状态;
  • 在事务中更新支付流水和业务订单;
  • 对已经处理过的通知直接返回成功,避免重复发货或重复增加余额。

示意逻辑:

@Transactional
public void handleAlipayNotify(AlipayNotify notify) {
    Payment payment =
            paymentRepository.findByOutTradeNoForUpdate(
                notify.getOutTradeNo()
            );

    if (payment == null) {
        throw new IllegalStateException("支付流水不存在");
    }

    if (payment.getStatus() == PaymentStatus.SUCCESS) {
        return;
    }

    if (notify.getTotalAmount().compareTo(payment.getAmount()) != 0) {
        throw new IllegalStateException("支付金额不一致");
    }

    if ("TRADE_SUCCESS".equals(notify.getTradeStatus())
            || "TRADE_FINISHED".equals(notify.getTradeStatus())) {
        payment.markSuccess(
            notify.getTradeNo(),
            notify.getGmtPayment()
        );

        orderRepository.markPaidIfUnpaid(
            payment.getOrderNo(),
            notify.getGmtPayment()
        );
    }
}

需要特别注意的问题

防止用户连续点击

「去支付」接口需要加锁或使用数据库唯一约束,防止用户连续点击时同时创建多条有效支付流水。可以限制同一业务订单在同一时刻只能有一条状态为 WAIT_BUYER_PAY 的支付宝流水。

不要直接覆盖原支付流水

旧流水可能会在稍后收到异步通知。如果直接修改旧流水的 out_trade_no 或删除记录,回调到达后就无法正确关联订单。因此,每次支付尝试都要保留独立记录。

新支付前处理旧交易

如果旧支付宝交易仍可付款,新旧交易同时存在就可能导致重复支付。创建新支付交易前,应根据接口能力关闭旧交易,或者确认旧交易已经自然过期。

服务端还要处理极端情况下的重复支付。如果同一业务订单收到第二笔成功付款,应转入异常处理或退款流程,不能再次发货。

以服务端状态为准

用户退出前端页面、同步跳转失败或接口超时,都不等于支付失败。只要支付结果不明确,就应通过异步通知和 alipay.trade.query 确认状态。

较稳妥的处理方式是保留原业务订单,二次支付前先查单。原交易还能继续就继续使用,无法继续时再创建新的支付流水。

备注:内容仅供参考。