支付宝同步回调缺少 trade_status 时如何判断支付状态?
明确结论
收到同步回调,并不能说明订单已经支付成功。
支付宝同步回调是浏览器页面跳转通知,主要作用是把用户从收银台带回商户页面。即使回调参数验签通过,也只能证明参数来自支付宝且没有被篡改,不能代替支付结果确认。
订单状态应以下列结果为准:
- 支付宝异步通知中的
trade_status; - 主动调用
alipay.trade.query查询到的trade_status。
同步回调可以用来展示支付结果页面,但不能直接触发发货、充值、开通会员等业务操作。
为什么同步回调不能作为支付成功依据
同步回调依赖用户浏览器完成跳转,存在不少不确定因素:
- 用户支付后直接关闭页面,商户可能收不到同步回调;
- 浏览器跳转可能因网络中断、拦截等原因失败;
- 用户可能重复刷新或再次访问回调地址;
- 回调地址可能被人为构造或重放;
- 同步参数中有
trade_no,不代表交易一定已经支付成功; - 验签成功只能证明参数来源和完整性,不能说明交易当前处于什么状态。
所以,收到同步回调和订单支付成功是两件不同的事。
推荐处理流程
1. 对同步回调参数进行验签
先使用支付宝公钥验证 sign。如果验签失败,就不能继续信任任何回调参数。
验签通过后,还要检查:
app_id是否属于当前应用;seller_id是否属于当前商户;out_trade_no是否存在,以及是否属于当前订单;total_amount是否与本地订单金额一致。
这些检查可以防止串单、金额不一致和配置错误,但仍不足以单独确认支付成功。
2. 查询支付宝交易状态
如果同步回调中没有 trade_status,可以使用 out_trade_no 或 trade_no 调用:
alipay.trade.query
判断支付结果时,应以查询响应中的 trade_status 为准。常见状态如下:
trade_status | 含义 | 建议处理 |
|---|---|---|
WAIT_BUYER_PAY | 等待买家付款 | 保持待支付 |
TRADE_SUCCESS | 支付成功 | 可将订单更新为已支付 |
TRADE_FINISHED | 交易结束 | 通常可按已支付处理,但应结合业务规则 |
TRADE_CLOSED | 交易关闭 | 更新为已关闭或支付失败 |
只有查询接口调用成功,并且 trade_status 明确为 TRADE_SUCCESS,或为业务认可的 TRADE_FINISHED 时,才能执行支付成功后的业务逻辑。
3. 仍以异步通知作为主要入口
三种处理方式可以这样分工:
- 同步回调:验签、查询订单状态并展示结果;
- 异步通知:确认支付结果、更新订单并执行后续业务;
- 主动查询:在异步通知延迟或缺失时补偿确认。
如果收到同步回调时,异步通知还没有处理完成,可以主动查询一次。若查询仍没有得到明确结果,页面应显示“支付结果确认中”,后续由异步通知或后台补单任务继续确认。
Java 示例
下面使用支付宝 Java SDK 展示核心处理逻辑。具体类名和构造方式应与项目实际使用的 SDK 版本一致。
public String handleReturn(Map<String, String> params) throws Exception {
boolean signVerified = AlipaySignature.rsaCheckV1(
params,
ALIPAY_PUBLIC_KEY,
params.getOrDefault("charset", "UTF-8"),
params.getOrDefault("sign_type", "RSA2")
);
if (!signVerified) {
return "回调验签失败";
}
String outTradeNo = params.get("out_trade_no");
String totalAmount = params.get("total_amount");
Order order = orderService.findByOutTradeNo(outTradeNo);
if (order == null) {
return "订单不存在";
}
if (!amountEquals(order.getTotalAmount(), totalAmount)) {
return "订单金额不一致";
}
if (!EXPECTED_APP_ID.equals(params.get("app_id"))) {
return "应用标识不一致";
}
if (!EXPECTED_SELLER_ID.equals(params.get("seller_id"))) {
return "收款账号不一致";
}
AlipayTradeQueryRequest request = new AlipayTradeQueryRequest();
request.setBizContent(
"{\"out_trade_no\":\"" + escapeJson(outTradeNo) + "\"}"
);
AlipayTradeQueryResponse response = alipayClient.execute(request);
if (!response.isSuccess()) {
return "支付结果确认中";
}
String tradeStatus = response.getTradeStatus();
if ("TRADE_SUCCESS".equals(tradeStatus)
|| "TRADE_FINISHED".equals(tradeStatus)) {
orderService.markPaidIdempotently(
outTradeNo,
response.getTradeNo(),
response.getTotalAmount()
);
return "支付成功";
}
if ("WAIT_BUYER_PAY".equals(tradeStatus)) {
return "尚未完成支付";
}
if ("TRADE_CLOSED".equals(tradeStatus)) {
return "交易已关闭";
}
return "支付结果确认中";
}
比较金额时应使用 BigDecimal,不要使用 double:
private boolean amountEquals(BigDecimal expected, String actual) {
if (actual == null) {
return false;
}
try {
return expected.compareTo(new BigDecimal(actual)) == 0;
} catch (NumberFormatException e) {
return false;
}
}
异步通知处理要点
异步通知也必须先验签,再检查订单号、应用、商户和金额。支付宝可能重复发送通知,因此订单更新必须保证幂等。
@Transactional
public void markPaidIdempotently(
String outTradeNo,
String alipayTradeNo,
String totalAmount) {
Order order = orderRepository.lockByOutTradeNo(outTradeNo);
if (order == null) {
throw new IllegalStateException("订单不存在");
}
if (order.isPaid()) {
return;
}
if (order.getTotalAmount().compareTo(new BigDecimal(totalAmount)) != 0) {
throw new IllegalStateException("订单金额不一致");
}
order.markPaid(alipayTradeNo);
orderRepository.save(order);
// 发货、充值等操作也需要具备幂等性
}
异步通知全部处理成功后,应按照支付宝接口要求返回成功响应。如果业务处理失败,不要提前返回成功,否则支付宝可能不会继续重试。
注意事项
- 不能因为参数中有
trade_no就认定订单已经支付; - 不能只检查同步回调是否验签成功;
- 不能直接信任回调中的订单号和金额;
- 支付状态确认前,不得发货、充值或开通服务;
- 同步回调、异步通知和主动查询可能并发执行,因此订单更新必须幂等;
- 建议设置定时补单机制,主动查询长时间处于“待支付”或“结果确认中”的订单;
- 本地订单已经支付后,不能因重复通知再次发放权益;
- 如果查询结果与本地状态冲突,应记录原始响应和交易号,并进入人工或自动对账流程。
简而言之,同步回调负责跳转和展示,异步通知及交易查询负责确认支付结果。
备注:内容仅供参考。
