周期扣款接口与alipay.trade.pay如何返回扣款结果?
支付小周
结论
alipay.trade.pay 会同步返回当前支付请求的处理结果。配置 notify_url 后,支付宝还可能通过异步通知发送交易状态。因此,它既不是“只实时返回”,也不是“只异步回调”。
周期扣款的返回方式要看具体接口文档:
- 如果同步响应中有明确的交易状态,可以先根据同步结果处理。
- 如果接口支持
notify_url,还要接收异步通知,并通过通知或主动查询更新最终状态。 - 如果同步结果是处理中、未知或超时,不能直接判断扣款成功或失败,需要使用支付宝交易号或商户订单号查询。
生产系统不应只依赖一种结果返回渠道。
如何判断 alipay.trade.pay 的结果
调用 alipay.trade.pay 后,支付宝会同步返回接口响应,通常可以这样处理:
- 响应明确表示支付成功时,立即更新订单状态。
- 响应明确表示失败时,根据错误码结束订单、修正参数或重试。
- 遇到网络超时、系统繁忙或状态无法确认等情况时,将订单标记为“支付处理中”,再调用交易查询接口确认。
- 配置了
notify_url后,还要接收异步通知并完成验签。
HTTP 请求成功只代表通信成功,不代表支付成功。业务判断要看支付宝响应中的业务结果和交易状态,不能只看 HTTP 状态码。
周期扣款接口如何处理
“周期扣款接口”可能指签约扣款、周期支付或代扣等不同产品,其接口名称和通知机制不完全相同。在没有明确接口名称和产品版本时,无法断定结果一定来自同步响应或异步通知。
接入时需要检查对应接口文档,确认:
- 是否支持
notify_url - 同步响应中是否包含交易状态
- 是否提供交易查询接口
- 哪些响应码分别表示成功、失败或处理中
- 异步通知是否会重复发送
- 通知中的交易状态是否为最终状态
系统可以统一按照“同步受理、异步更新、主动查询兜底”的方式处理:
发起扣款
├─ 同步明确成功:记录成功,等待异步通知校验或补充信息
├─ 同步明确失败:记录失败,根据错误码决定是否重试
└─ 同步结果未知:记录处理中,调用查询接口确认
收到异步通知
├─ 验签
├─ 校验商户号、订单号、金额和币种
├─ 幂等更新订单
└─ 按接口要求返回成功应答
Java 调用示例
以下示例用于说明处理思路。具体请求参数要根据实际签约产品和支付宝 SDK 版本调整:
AlipayTradePayRequest request = new AlipayTradePayRequest();
request.setNotifyUrl("https://example.com/alipay/notify");
request.setBizContent("""
{
"out_trade_no": "ORDER_202609090001",
"scene": "bar_code",
"auth_code": "CUSTOMER_AUTH_CODE",
"subject": "订单支付",
"total_amount": "10.00"
}
""");
AlipayTradePayResponse response = alipayClient.execute(request);
if (response.isSuccess()) {
// 同步业务处理成功。
// 仍应保存 trade_no、out_trade_no 等信息,并保证后续通知幂等。
markOrderPaid(
response.getOutTradeNo(),
response.getTradeNo()
);
} else {
String code = response.getCode();
String subCode = response.getSubCode();
if (isResultUnknown(code, subCode)) {
// 超时或状态不确定时,不要直接标记失败。
markOrderProcessing("ORDER_202609090001");
scheduleTradeQuery("ORDER_202609090001");
} else {
markOrderFailed(
"ORDER_202609090001",
code,
subCode
);
}
}
不要仅根据示例为 isResultUnknown 定义一组固定错误码。具体实现要参考当前接口文档和实际接入产品的错误码说明。
异步通知处理示例
处理异步通知前必须先验签。下面是一个简化示例:
@PostMapping("/alipay/notify")
public String handleNotify(HttpServletRequest request) throws Exception {
Map<String, String> params = new HashMap<>();
request.getParameterMap().forEach((key, values) -> {
if (values != null && values.length > 0) {
params.put(key, String.join(",", values));
}
});
boolean verified = AlipaySignature.rsaCheckV1(
params,
alipayPublicKey,
"UTF-8",
"RSA2"
);
if (!verified) {
return "failure";
}
String outTradeNo = params.get("out_trade_no");
String tradeNo = params.get("trade_no");
String tradeStatus = params.get("trade_status");
String totalAmount = params.get("total_amount");
// 必须校验订单归属、金额、商户身份等信息。
Order order = findOrder(outTradeNo);
if (order == null || !order.getTotalAmount().equals(totalAmount)) {
return "failure";
}
// 使用 out_trade_no 或 trade_no 保证重复通知不会重复入账。
updateOrderIdempotently(outTradeNo, tradeNo, tradeStatus);
return "success";
}
实际验签时,要确认公钥类型与接入模式一致,例如支付宝公钥模式或证书模式。异步通知应返回什么格式,也要以对应接口文档为准。
同步结果不确定时主动查询
发生超时、连接中断或业务状态未知时,可以通过 alipay.trade.query 查询:
AlipayTradeQueryRequest queryRequest = new AlipayTradeQueryRequest();
queryRequest.setBizContent("""
{
"out_trade_no": "ORDER_202609090001"
}
""");
AlipayTradeQueryResponse queryResponse =
alipayClient.execute(queryRequest);
if (queryResponse.isSuccess()) {
String tradeStatus = queryResponse.getTradeStatus();
switch (tradeStatus) {
case "TRADE_SUCCESS":
case "TRADE_FINISHED":
markOrderPaid(
queryResponse.getOutTradeNo(),
queryResponse.getTradeNo()
);
break;
case "WAIT_BUYER_PAY":
markOrderProcessing(queryResponse.getOutTradeNo());
break;
case "TRADE_CLOSED":
markOrderClosed(queryResponse.getOutTradeNo());
break;
default:
markOrderProcessing(queryResponse.getOutTradeNo());
}
}
如果周期扣款产品有专用查询接口,应使用该接口,不能默认调用 alipay.trade.query。
注意事项
- 同步返回、异步通知和主动查询的到达顺序不固定,更新订单时必须保证幂等。
- 网络超时不等于扣款失败,直接重新扣款可能导致重复扣费。
- 异步通知可能重复发送,也可能早于同步响应到达。
- 除了验签,还要校验
app_id、商户身份、订单号、金额和币种等字段。 - 金额应使用精确的小数类型或最小货币单位处理,不要直接用二进制浮点数比较。
- 回调接口应尽快完成必要校验并返回成功。耗时业务可以交给内部消息队列异步执行。
- 对长期处于“处理中”的订单,需要设置定时查询、重试上限和人工核对机制。
- 周期扣款的具体规则以实际使用的支付宝产品、API 名称和当前官方接口文档为准。
备注:内容仅供参考。
