支付宝转账接口提示系统繁忙却扣款,导致重复付款
结论
支付宝返回“系统繁忙,请稍后再试”时,不能直接判定转账失败。支付宝可能已经受理请求,甚至完成出款,只是响应没有正常返回。
建议这样处理:
- 为每笔业务生成并永久保存唯一的商户业务单号,如
out_biz_no。 - 遇到超时、系统繁忙或网络异常时,统一将订单标记为“结果未知”,不要马上创建新单并再次付款。
- 使用原业务单号调用配套的转账查询接口,直到查到成功或明确失败。
- 只有查询结果明确为失败,并且该失败允许重新提交时,才能重试。重试时优先复用原业务单号。
- 通过数据库唯一约束、状态机和定时对账,防止重复付款。
为什么提示失败却实际扣款
一次转账至少包括以下环节:
业务系统发起请求
↓
支付宝接收并处理
↓
支付宝完成出款
↓
响应返回业务系统
“系统繁忙”只能说明业务系统没有收到确定的成功响应,不能证明支付宝没有处理这笔请求。常见情况包括:
- 支付宝已经受理请求,但处理结果未能正常返回;
- 支付宝完成转账后,网络请求才发生超时;
- 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.queryalipay.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幂等更新。任何渠道确认成功后,都必须关闭再次付款入口。
核心原则是:只要没有得到“明确失败”的结果,就不能把转账当作失败并重新付款。应先使用原业务单号查清资金状态,再决定下一步。
备注:内容仅供参考。
