如何让 alipay.trade.wap.pay 返回的 URL 立即失效?
结论
alipay.trade.wap.pay 生成的支付 URL,有些 SDK 返回的是自动提交的 HTML 表单,不能通过另一个接口直接撤销。如果支付宝还没有创建交易,alipay.trade.close 就没有可关闭的交易对象,也无法让已经泄露给用户的支付宝支付 URL 立即失效。
如果需要控制支付入口,应避免提前向用户暴露支付宝的原始支付地址,改为提供商户自己的短期支付入口。取消支付时,先禁用商户入口;如果支付宝已经创建交易,再调用 alipay.trade.close。
还要处理取消和付款同时发生的情况。如果交易已经支付,应进入退款流程,不能继续调用关闭接口。
为什么不能直接让 URL 失效
alipay.trade.wap.pay 返回的内容本质上是一份带签名的支付请求。支付宝通常会在用户访问并提交请求后创建交易,因此可能出现以下状态:
- 商户已经生成支付请求。
- 用户还没有打开支付页面。
- 支付宝侧还没有对应交易。
- 此时调用
alipay.trade.close,无法根据out_trade_no找到可关闭的交易。
支付宝的交易关闭接口只处理已经存在且状态允许关闭的交易,并不是用来撤销支付 URL 的。
return_url 和 quit_url 只控制页面跳转,不能限制支付。删除本地订单、修改前端页面或停止接收异步通知,同样无法阻止用户通过已经拿到的原始 URL 继续付款。
推荐方案:使用商户侧支付入口
不要长期保存预先生成的支付宝 URL,也不要直接把它发送给用户。可以先生成一个商户自己的支付地址:
https://pay.example.com/pay/7d0f1c8a...
该地址绑定一个短期、随机且可撤销的支付令牌。用户访问时,服务端先检查业务订单状态。只有订单仍然允许付款,才调用 alipay.trade.wap.pay,并向用户输出支付表单。
示例流程:
用户访问商户支付入口
↓
校验支付令牌、订单状态和有效期
↓
订单已取消 ──→ 返回“订单已取消”
↓
订单可支付
↓
调用 alipay.trade.wap.pay
↓
将支付宝返回的表单响应给用户
下面是简化后的 Java 示例。具体类名和异常处理应以项目使用的支付宝 SDK 为准:
@GetMapping("/pay/{token}")
public void pay(@PathVariable String token,
HttpServletResponse response) throws Exception {
PayToken payToken = payTokenService.find(token);
if (payToken == null
|| payToken.isExpired()
|| payToken.isRevoked()) {
response.sendError(HttpServletResponse.SC_GONE, "支付链接已失效");
return;
}
Order order = orderService.findById(payToken.getOrderId());
if (order == null || !order.isPayable()) {
response.sendError(HttpServletResponse.SC_CONFLICT, "订单当前不可支付");
return;
}
AlipayTradeWapPayRequest request = new AlipayTradeWapPayRequest();
request.setNotifyUrl("https://pay.example.com/alipay/notify");
request.setReturnUrl("https://pay.example.com/alipay/return");
String bizContent = """
{
"out_trade_no": "%s",
"total_amount": "%s",
"subject": "%s",
"product_code": "QUICK_WAP_WAY",
"timeout_express": "10m"
}
""".formatted(
order.getOutTradeNo(),
order.getAmount().toPlainString(),
order.getSubject()
);
request.setBizContent(bizContent);
String form = alipayClient.pageExecute(request).getBody();
response.setContentType("text/html;charset=UTF-8");
response.getWriter().write(form);
}
取消订单时,先让商户入口失效:
@Transactional
public void cancelOrder(String orderId) {
Order order = orderService.lockById(orderId);
if (order.isPaid()) {
throw new IllegalStateException("订单已支付,不能直接取消");
}
order.markCancelled();
payTokenService.revokeByOrderId(orderId);
}
这样可以阻止尚未取得支付宝原始请求的用户继续发起支付。
已经返回原始 URL 后怎么处理
如果支付宝原始 URL 或表单已经发给用户,商户就无法保证它立即失效,只能通过状态补偿降低风险:
- 将业务订单标记为“取消中”或“已取消”,并停止生成新的支付请求。
- 使用同一个
out_trade_no调用alipay.trade.query查询交易。 - 如果交易已经存在且状态允许关闭,调用
alipay.trade.close。 - 如果查询结果仍为交易不存在,可以在有限时间内重试查询。
- 如果异步通知显示交易已经支付,不要交付业务权益,并根据业务规则调用退款接口。
- 关闭或退款完成后,再把业务订单更新为最终状态。
示意代码:
public void cancelPayment(String outTradeNo) {
orderService.markCancelling(outTradeNo);
payTokenService.revokeByOutTradeNo(outTradeNo);
TradeStatus status = alipayService.query(outTradeNo);
switch (status) {
case WAIT_BUYER_PAY:
alipayService.close(outTradeNo);
orderService.markCancelled(outTradeNo);
break;
case TRADE_SUCCESS:
case TRADE_FINISHED:
orderService.markRefundRequired(outTradeNo);
break;
case NOT_EXIST:
retryService.scheduleTradeQuery(outTradeNo);
break;
default:
orderService.markManualReviewRequired(outTradeNo);
}
}
其中的 TradeStatus、retryService 和业务状态都是示意封装,并非支付宝 SDK 提供的固定类型。
设置支付超时时间
创建支付请求时,可以根据当前接口文档和签约产品的支持情况设置 timeout_express,例如:
{
"out_trade_no": "ORDER_202609090001",
"total_amount": "99.00",
"subject": "测试订单",
"product_code": "QUICK_WAP_WAY",
"timeout_express": "10m"
}
该参数用于限制交易创建后的待支付时间,可以降低旧订单被继续支付的风险。但它不能让 URL 立即失效,也不能代替商户侧支付入口和交易关闭逻辑。具体支持的格式、最短期限和实际生效范围,应以当前支付宝开放平台文档及商户签约产品为准。
注意事项
- 不要试图通过更换应用密钥让单个支付 URL 失效。这会影响整个应用,而且已经生成的签名请求是否会立即受到影响,也不能作为可靠的订单控制手段。
- 不要重复使用同一个支付令牌。令牌应短期有效、随机且不可预测,并在成功进入支付流程后,根据业务需要将其标记为已使用。
- 检查订单状态与生成支付请求之间存在并发窗口。应使用数据库锁、条件更新或状态机,避免取消和支付同时发生。
- 支付是否成功,应以支付宝服务端异步通知和主动查询结果为准,不能只依赖同步跳转。
- 即使业务订单已经取消,也要正常验签并处理支付宝异步通知,以免出现支付宝已经收款但商户没有入账的资金差错。
- 交易关闭只适用于尚未支付且状态允许关闭的交易。已经支付的交易必须按退款流程处理。
备注:内容仅供参考。
