PAYATHON 2026

支付宝转账接口提示系统繁忙却扣款,导致重复付款

支付小周

结论

支付宝返回“系统繁忙,请稍后再试”时,不能直接判定转账失败。支付宝可能已经受理请求,甚至完成出款,只是响应没有正常返回。

建议这样处理:

  1. 为每笔业务生成并永久保存唯一的商户业务单号,如 out_biz_no。
  2. 遇到超时、系统繁忙或网络异常时,统一将订单标记为“结果未知”,不要马上创建新单并再次付款。
  3. 使用原业务单号调用配套的转账查询接口,直到查到成功或明确失败。
  4. 只有查询结果明确为失败,并且该失败允许重新提交时,才能重试。重试时优先复用原业务单号。
  5. 通过数据库唯一约束、状态机和定时对账,防止重复付款。

为什么提示失败却实际扣款

一次转账至少包括以下环节:

业务系统发起请求
        ↓
支付宝接收并处理
        ↓
支付宝完成出款
        ↓
响应返回业务系统

“系统繁忙”只能说明业务系统没有收到确定的成功响应,不能证明支付宝没有处理这笔请求。常见情况包括:

  • 支付宝已经受理请求,但处理结果未能正常返回;
  • 支付宝完成转账后,网络请求才发生超时;
  • SDK、网关或业务系统解析响应时出现异常;
  • 转账仍在处理中,之后才变为成功。

所以,“接口调用异常”和“转账失败”不是一回事。前者描述通信结果,后者才表示资金结果。

应当如何判断转账结果

1. 为每笔业务生成稳定且唯一的业务单号

同一笔付款必须始终使用同一个 out_biz_no。不能因为接口超时或系统繁忙就换一个新单号。

付款业务 ID:PAYMENT_20260909_10001
out_biz_no:PAYMENT_20260909_10001

建议为业务付款编号和 out_biz_no 建立数据库唯一索引:

CREATE UNIQUE INDEX uk_transfer_out_biz_no
ON transfer_order(out_biz_no);

具体字段名称和幂等规则应以当前使用的支付宝转账产品文档为准。不同转账接口在产品码、业务场景或幂等范围上可能存在差异。

2. 不要把“系统繁忙”记为最终失败

业务状态至少应区分为:

INIT             待提交
SUBMITTING       正在提交
PROCESSING       支付宝处理中
SUCCESS          转账成功
FAIL             明确失败
UNKNOWN          结果未知,需要查询

遇到以下情况时,应将订单转入 UNKNOWN 或 PROCESSING:

  • 系统繁忙;
  • 请求超时;
  • 网络连接中断;
  • SDK 抛出无法确认资金结果的异常;
  • 返回结果显示仍在处理中;
  • 响应内容不完整或无法验签。

只有支付宝返回了明确且可以确认的最终失败结果,才能将订单更新为 FAIL。

3. 使用原业务单号主动查询

查询接口必须与实际调用的转账接口配套。支付宝不同代际或不同产品的转账接口可能分别对应:

  • alipay.fund.trans.order.query
  • alipay.fund.trans.common.query

应根据当前接入的转账产品和官方接口文档选择,不能混用。

查询时优先使用原请求中的 out_biz_no。如果已经取得支付宝订单号,也应将其保存下来,供后续查询使用。

可以按以下方式处理查询结果:

查询结果业务处理
明确成功更新为 SUCCESS,禁止再次付款
处理中保持 PROCESSING,稍后继续查询
明确失败更新为 FAIL,再根据失败原因判断能否重试
查询不到或查询异常保持 UNKNOWN,不要立即重新付款

查询不到不等于失败。数据可能暂时还无法查询,也可能是查询参数不匹配,或者原请求仍在处理中。

推荐的处理流程

创建本地付款单
      ↓
用唯一 out_biz_no 提交转账
      ↓
┌─────────────┬──────────────┬──────────────┐
│ 明确成功     │ 明确失败      │ 超时/繁忙/异常 │
↓             ↓              ↓
SUCCESS       FAIL           UNKNOWN
                              ↓
                       使用原单号查询
                              ↓
                 成功 / 处理中 / 明确失败

对于处于 UNKNOWN 或 PROCESSING 状态的订单,可以逐步延长查询间隔,例如:

10 秒 → 30 秒 → 1 分钟 → 5 分钟 → 30 分钟

具体的查询频率、持续时间和接口限流要求,应以实际产品规则为准。如果超过自动查询期限后仍无法确认结果,应转入人工核对或账单对账流程,不能自动创建另一笔付款。

代码示例

下面是一段不依赖具体 SDK 版本的 Java 风格示例,主要演示状态控制和幂等逻辑。实际使用时,请根据接入的支付宝接口版本调整请求类、响应字段和状态枚举。

public TransferResult transfer(String businessId, BigDecimal amount) {
    // 同一业务始终映射到相同的商户业务单号
    String outBizNo = businessId;

    TransferOrder order = transferOrderRepository.findOrCreate(
        businessId,
        outBizNo,
        amount
    );

    // 已成功的订单绝不能再次提交
    if (order.getStatus() == TransferStatus.SUCCESS) {
        return TransferResult.success(order.getAlipayOrderId());
    }

    // 使用数据库唯一约束或分布式锁,防止并发重复提交
    if (!transferLock.tryLock(outBizNo)) {
        return TransferResult.processing("该付款正在处理中");
    }

    try {
        order.markSubmitting();
        transferOrderRepository.save(order);

        AlipayTransferResponse response =
            alipayGateway.transfer(outBizNo, amount);

        if (response.isDefinitiveSuccess()) {
            order.markSuccess(response.getAlipayOrderId());
            transferOrderRepository.save(order);
            return TransferResult.success(response.getAlipayOrderId());
        }

        if (response.isDefinitiveFailure()) {
            order.markFailed(response.getCode(), response.getMessage());
            transferOrderRepository.save(order);
            return TransferResult.failed(
                response.getCode(),
                response.getMessage()
            );
        }

        // 系统繁忙、处理中或其他无法确认的结果
        order.markUnknown(response.getCode(), response.getMessage());
        transferOrderRepository.save(order);
        scheduleQuery(outBizNo);

        return TransferResult.processing("付款结果待确认");
    } catch (Exception e) {
        // 网络异常和调用异常不能直接视为转账失败
        order.markUnknown("CALL_EXCEPTION", e.getMessage());
        transferOrderRepository.save(order);
        scheduleQuery(outBizNo);

        return TransferResult.processing("付款结果待确认");
    } finally {
        transferLock.unlock(outBizNo);
    }
}

查询任务示例:

public void queryTransferResult(String outBizNo) {
    TransferOrder order =
        transferOrderRepository.findByOutBizNo(outBizNo);

    if (order == null || order.getStatus() == TransferStatus.SUCCESS) {
        return;
    }

    AlipayTransferQueryResponse response =
        alipayGateway.queryByOutBizNo(outBizNo);

    if (response.isSuccess()) {
        order.markSuccess(response.getAlipayOrderId());
    } else if (response.isProcessing()) {
        order.markProcessing();
        scheduleNextQuery(outBizNo);
    } else if (response.isDefinitiveFailure()) {
        order.markFailed(response.getCode(), response.getMessage());
    } else {
        order.markUnknown(response.getCode(), response.getMessage());
        scheduleNextQuery(outBizNo);
    }

    transferOrderRepository.save(order);
}

更新状态时还应采用条件更新,防止并发任务将已经成功的状态覆盖为失败:

UPDATE transfer_order
SET status = 'SUCCESS',
    alipay_order_id = ?
WHERE out_biz_no = ?
  AND status IN ('SUBMITTING', 'PROCESSING', 'UNKNOWN');

重试时需要注意什么

如果查询已经确认转账成功,无论原始调用返回了什么错误,都不能再次转账。

如果查询结果明确为失败,应先根据失败原因判断能否重试。余额不足、收款账户异常、参数错误等业务失败,反复提交通常没有意义。

确实需要重新调用时,应遵守对应接口的幂等规则。通常,同一笔业务应复用原 out_biz_no。如果接口规定最终失败后必须更换单号,则需要明确记录原付款单与重试付款单的关联,并在重试前再次查询原单,防止原单状态已经发生变化。

还需要补上的保护措施

  • 保存完整的请求时间、out_biz_no、支付宝订单号、响应码和响应内容,并对敏感信息进行脱敏。
  • 在付款入口使用数据库锁、乐观锁或分布式锁,防止用户重复点击,也避免定时任务并发提交。
  • 将“提交转账”和“确认结果”分开处理。前端收到“处理中”状态时,不能引导用户再次付款。
  • 为长时间处于 UNKNOWN 状态的订单设置告警,并通过支付宝账单或资金明细完成对账。
  • 判断资金结果时,不能只看 success、HTTP 状态码或 SDK 是否抛出异常,还要结合业务响应码、订单状态和查询结果。
  • 回调、主动查询和账单对账都应按照 out_biz_no 幂等更新。任何渠道确认成功后,都必须关闭再次付款入口。

核心原则是:只要没有得到“明确失败”的结果,就不能把转账当作失败并重新付款。应先使用原业务单号查清资金状态,再决定下一步。

备注:内容仅供参考。