PAYATHON 2026

小程序唤起支付时报错 ACQ.TRADE_HAS_SUCCESS 如何处理?

支付小周

结论

ACQ.TRADE_HAS_SUCCESS 表示当前交易已经支付成功,系统不会再次收款。遇到这个错误时,不要反复唤起支付,也不要只根据前端提示将订单标记为失败。

服务端应查询支付宝交易状态,并根据查询结果更新本地订单。状态为 TRADE_SUCCESS 或 TRADE_FINISHED 时,按支付成功处理。如果业务需要用户重新付款,必须创建新的商户订单号,不能复用已经支付成功的 out_trade_no。

出现该报错的原因

通常有以下几种情况:

  • 用户支付成功后,页面没有及时收到或处理结果,用户又点击了支付按钮。
  • 支付成功的异步通知尚未处理完成,本地订单仍显示”待支付”。
  • 前端回调、网络请求或页面跳转出现异常,客户端误以为支付失败。
  • 服务端使用同一个 out_trade_no 重复创建或发起支付。
  • 多次点击、接口重试或并发请求引发了重复收款操作。
  • 本地订单状态没有与支付宝侧的交易状态同步。

支付接口返回”失败”,只表示这次重复收款请求没有被受理,不代表原交易未成功。

处理步骤

1. 停止重复支付

收到 ACQ.TRADE_HAS_SUCCESS 后,不要再用相同的交易信息唤起支付。前端可以提示用户”订单已支付,正在确认支付结果”,同时请求服务端核实订单状态。

2. 由服务端查询交易状态

服务端应调用 alipay.trade.query 查询交易。一般使用商户订单号 out_trade_no;如果已经保存了支付宝交易号,也可以按照接口要求使用 trade_no。

查询后需要判断以下状态:

  • TRADE_SUCCESS:支付成功,可以更新业务订单。
  • TRADE_FINISHED:交易已完成,也应按已支付处理,但通常不能再发起退款以外的后续支付操作。具体能力以实际产品规则为准。
  • WAIT_BUYER_PAY:交易仍在等待付款。
  • TRADE_CLOSED:交易已经关闭,无法继续支付,需要根据业务规则创建新订单。

不同接入产品的支付状态枚举和可执行操作可能有所不同,请以当前使用的支付宝接口文档和实际响应为准。

3. 更新本地订单

确认支付成功后,服务端需要:

  1. 检查查询结果中的 out_trade_no 是否属于当前业务订单。
  2. 核对支付金额、商户身份等信息。
  3. 以幂等方式将订单更新为”已支付”。
  4. 记录支付宝交易号 trade_no、支付时间和原始响应。
  5. 触发发货、开通权益等后续流程。

订单更新必须支持幂等处理。这样,即使主动查询和异步通知同时到达,也不会重复发货、充值或发放权益。

前端处理示例

以下代码只用于说明处理思路,实际字段名称以小程序支付 API 的返回结构为准。

async function startPayment(tradeNo) {
  try {
    await my.tradePay({
      tradeNO: tradeNo
    });

    await confirmOrderStatus();
  } catch (error) {
    const errorCode = error.resultCode || error.code;

    if (errorCode === 'ACQ.TRADE_HAS_SUCCESS') {
      // 本次重复支付被拒绝,但原交易可能已经成功
      await confirmOrderStatus();
      return;
    }

    throw error;
  }
}

async function confirmOrderStatus() {
  const response = await fetch('/api/orders/current/payment-status');
  const result = await response.json();

  if (result.status === 'PAID') {
    my.showToast({
      type: 'success',
      content: '支付成功'
    });
    return;
  }

  my.showToast({
    type: 'none',
    content: '支付结果确认中,请稍后查看订单'
  });
}

前端捕获到 ACQ.TRADE_HAS_SUCCESS 后,不能自行将订单改为已支付。订单状态必须经过服务端查询和校验才能确认。

服务端处理示例

下面用 Java 风格的伪代码说明处理流程。具体的 SDK 类名和响应字段,需要根据项目使用的支付宝 SDK 版本调整。

public PaymentResult confirmPayment(String outTradeNo) {
    Order order = orderRepository.findByOutTradeNo(outTradeNo);

    if (order == null) {
        throw new IllegalArgumentException("订单不存在");
    }

    // 已经处理成功时直接返回,保证接口幂等
    if (order.isPaid()) {
        return PaymentResult.paid(order.getAlipayTradeNo());
    }

    AlipayTradeQueryResponse response = queryAlipayTrade(outTradeNo);

    if (!response.isSuccess()) {
        return PaymentResult.pending(response.getSubCode());
    }

    String tradeStatus = response.getTradeStatus();

    if ("TRADE_SUCCESS".equals(tradeStatus)
            || "TRADE_FINISHED".equals(tradeStatus)) {

        verifyTrade(response, order);

        // 数据库更新应使用事务和条件更新,避免并发重复处理
        boolean updated = orderRepository.markPaidIfUnpaid(
                order.getId(),
                response.getTradeNo()
        );

        if (updated) {
            paymentService.processPaidOrder(order.getId());
        }

        return PaymentResult.paid(response.getTradeNo());
    }

    if ("TRADE_CLOSED".equals(tradeStatus)) {
        return PaymentResult.closed();
    }

    return PaymentResult.pending(tradeStatus);
}

交易校验至少要覆盖订单号和金额:

private void verifyTrade(
        AlipayTradeQueryResponse response,
        Order order
) {
    if (!order.getOutTradeNo().equals(response.getOutTradeNo())) {
        throw new SecurityException("商户订单号不一致");
    }

    BigDecimal actualAmount = new BigDecimal(response.getTotalAmount());

    if (order.getPayAmount().compareTo(actualAmount) != 0) {
        throw new SecurityException("支付金额不一致");
    }
}

比较金额时,应使用 BigDecimal,或者使用以最小货币单位保存的整数。不要直接使用 float 或 double 判断金额是否相等。

防止问题再次发生

商户订单号必须保持唯一

一个 out_trade_no 只能对应一笔支付交易。已经支付成功的订单号不能再用于另一笔收款。

如果用户要再次购买同一商品,应创建新的业务订单,并生成新的 out_trade_no。不要通过修改金额或商品名称来复用旧订单号。

支付按钮需要防重复点击

用户点击支付后,前端应立即进入处理中状态,并在结果明确前禁用支付按钮:

let paying = false;

async function onPay() {
  if (paying) {
    return;
  }

  paying = true;

  try {
    await createAndStartPayment();
  } finally {
    paying = false;
  }
}

防止重复点击只能改善用户体验,不能代替服务端的幂等控制。并发请求、网络重试和恶意请求仍可能绕过前端限制。

创建支付交易也要幂等

服务端为同一个业务订单创建支付交易前,应先检查本地订单状态:

  • 已支付:直接返回已支付状态。
  • 已创建支付交易且交易仍然有效:返回现有交易信息。
  • 尚未创建:执行创建操作。
  • 已关闭且允许重新支付:创建新的业务支付单号,同时保留它与原订单的关联关系。

同时处理异步通知和主动查询

支付结果可能通过多个入口到达:

  • 支付异步通知。
  • 前端完成支付后的主动确认。
  • 定时补偿查询。
  • 人工触发的订单核对。

这些入口应共用同一套幂等支付确认逻辑。数据库可以通过”只有未支付状态才能更新”的条件更新来控制并发,例如:

UPDATE orders
SET payment_status = 'PAID',
    alipay_trade_no = ?
WHERE id = ?
  AND payment_status = 'UNPAID';

只有受影响行数为 1 时,才首次执行发货或权益发放。受影响行数为 0,通常说明订单已经由其他流程处理。

注意事项

  • 不要把 ACQ.TRADE_HAS_SUCCESS 当作普通支付失败并立即生成新订单,否则可能出现用户已经扣款,但业务订单没有到账的情况。
  • 不要信任前端传入的”支付成功”状态。服务端必须查询交易,或验证支付通知。
  • 除了交易状态,还要校验商户订单号、金额和商户身份等信息。
  • 不要依赖前端回调完成发货。用户可能关闭页面,也可能遇到网络中断。
  • 如果支付宝侧显示支付成功,但本地订单长期没有更新,应检查异步通知验签、通知地址、响应内容、数据库事务和幂等逻辑。
  • 如果确认属于误支付,或者业务订单不应履约,应先核对交易,再调用对应的退款接口处理。不能通过再次创建支付来”覆盖”原交易。

备注:内容仅供参考。