小程序唤起支付时报错 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. 更新本地订单
确认支付成功后,服务端需要:
- 检查查询结果中的
out_trade_no是否属于当前业务订单。 - 核对支付金额、商户身份等信息。
- 以幂等方式将订单更新为”已支付”。
- 记录支付宝交易号
trade_no、支付时间和原始响应。 - 触发发货、开通权益等后续流程。
订单更新必须支持幂等处理。这样,即使主动查询和异步通知同时到达,也不会重复发货、充值或发放权益。
前端处理示例
以下代码只用于说明处理思路,实际字段名称以小程序支付 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当作普通支付失败并立即生成新订单,否则可能出现用户已经扣款,但业务订单没有到账的情况。 - 不要信任前端传入的”支付成功”状态。服务端必须查询交易,或验证支付通知。
- 除了交易状态,还要校验商户订单号、金额和商户身份等信息。
- 不要依赖前端回调完成发货。用户可能关闭页面,也可能遇到网络中断。
- 如果支付宝侧显示支付成功,但本地订单长期没有更新,应检查异步通知验签、通知地址、响应内容、数据库事务和幂等逻辑。
- 如果确认属于误支付,或者业务订单不应履约,应先核对交易,再调用对应的退款接口处理。不能通过再次创建支付来”覆盖”原交易。
备注:内容仅供参考。
