H5 唤起支付宝支付时重复支付上一笔订单如何解决?
结论
每次选择新订单,都要由服务端重新调用 alipay.trade.wap.pay,使用新的商户订单号生成支付表单,再让浏览器加载并提交这份表单。上一笔支付的表单 HTML、跳转地址、签名参数和中间页面都不能复用。
出现这种情况,通常不是支付宝 APP 自动选中了旧订单,而是 H5 页面、业务服务端或浏览器历史缓存仍在使用上一笔订单的支付请求。
常见原因
建议依次检查以下环节:
-
复用了
out_trade_noout_trade_no是商户订单号。同一笔业务订单再次发起支付时,可以继续使用原订单号;切换到另一笔订单后,则必须传入该订单对应的唯一编号。 -
复用了支付表单或跳转地址
alipay.trade.wap.pay返回的表单、签名和跳转参数都与当前订单绑定。不要提前生成后长期保存在变量、DOM、缓存或会话中,也不要反复提交同一份表单。 -
支付接口被缓存
如果生成支付表单的接口使用
GET,或者请求经过浏览器、CDN、网关缓存,相同 URL 可能直接返回上一笔订单的支付页面。 -
页面恢复了旧状态
用户从支付宝 APP 返回浏览器时,部分浏览器会通过页面历史记录或 Back-Forward Cache 恢复原页面。如果代码继续读取旧的全局变量、隐藏字段或 DOM 节点,就可能再次提交上一笔订单。
-
服务端按用户或 Session 缓存了支付参数
如果服务端将
payForm、out_trade_no等支付数据保存在 Session 中,切换订单后又没有及时覆盖,就可能生成或返回旧请求。
推荐处理流程
建议按下面的流程处理:
- 用户选择订单。
- 前端只向服务端提交订单 ID,不自行拼接金额和支付宝签名参数。
- 服务端重新查询订单,并校验订单所属用户、金额和支付状态。
- 服务端取得当前订单唯一的
out_trade_no。 - 服务端实时调用
alipay.trade.wap.pay生成支付表单。 - 浏览器提交新表单并唤起支付宝。
- 用户返回后,前端重新查询订单状态,不直接根据前端页面状态判断支付结果。
前端示例
可以通过普通 POST 表单进入服务端支付入口。时间戳可以避免业务支付入口被错误缓存,但不能代替唯一的商户订单号。
<button type="button" onclick="pay('ORDER_20260908_002')">
支付当前订单
</button>
<script>
function pay(orderId) {
const form = document.createElement('form');
form.method = 'POST';
form.action = `/api/pay/alipay/wap?_=${Date.now()}`;
form.target = '_self';
const orderInput = document.createElement('input');
orderInput.type = 'hidden';
orderInput.name = 'orderId';
orderInput.value = orderId;
form.appendChild(orderInput);
document.body.appendChild(form);
form.submit();
form.remove();
}
</script>
不要再次提交支付宝上一次返回的表单,例如:
// 错误示例:payForm 可能仍属于上一笔订单
container.innerHTML = cachedPayForm;
container.querySelector('form').submit();
服务端示例
下面使用支付宝 Java SDK 的常见调用方式。具体类名和配置方式应以项目实际使用的 SDK 版本为准。
@PostMapping(
value = "/api/pay/alipay/wap",
produces = "text/html;charset=UTF-8"
)
public ResponseEntity<String> createWapPayment(
@RequestParam String orderId,
HttpServletRequest request) throws AlipayApiException {
Order order = orderService.getPayableOrder(orderId, getCurrentUserId());
if (order == null || !order.isPendingPayment()) {
throw new IllegalStateException("订单不存在或当前状态不可支付");
}
AlipayTradeWapPayRequest payRequest = new AlipayTradeWapPayRequest();
payRequest.setNotifyUrl(alipayNotifyUrl);
payRequest.setReturnUrl(buildReturnUrl(order.getId()));
AlipayTradeWapPayModel model = new AlipayTradeWapPayModel();
model.setOutTradeNo(order.getTradeNo());
model.setTotalAmount(order.getAmount().toPlainString());
model.setSubject(order.getSubject());
model.setProductCode("QUICK_WAP_WAY");
payRequest.setBizModel(model);
// 每次支付当前订单时重新生成,不复用上一笔订单的 body
String payForm = alipayClient.pageExecute(payRequest).getBody();
return ResponseEntity.ok()
.header("Cache-Control", "no-store, no-cache, must-revalidate, max-age=0")
.header("Pragma", "no-cache")
.header("Expires", "0")
.body(payForm);
}
如果服务端使用其他语言,处理原则不变:根据本次提交的 orderId 重新读取订单,再重新构造 alipay.trade.wap.pay 请求。
返回 H5 后刷新订单状态
return_url 只能说明用户的浏览器已经回到商户页面,不能单独证明支付成功。支付结果应以支付宝异步通知的校验结果,或服务端主动查询的结果为准。
window.addEventListener('pageshow', async (event) => {
// persisted 为 true 时,页面可能从 Back-Forward Cache 恢复
if (event.persisted) {
resetPendingPaymentState();
}
await refreshCurrentOrderStatus();
});
切换订单时,还要清理上一笔订单留下的临时状态:
function selectOrder(order) {
currentOrderId = order.id;
sessionStorage.removeItem('pendingPayForm');
sessionStorage.removeItem('pendingPayUrl');
renderSelectedOrder(order);
}
更稳妥的方式是不要在前端持久化支付表单和完整支付 URL,只保存当前选中的业务订单 ID。
排查方法
可以在服务端记录以下字段,以确认旧订单是在哪个环节被复用的:
requestId
userId
orderId
out_trade_no
total_amount
支付请求生成时间
连续选择两笔不同订单发起支付时,重点比较:
- 前端提交的
orderId是否已经改变; - 服务端查询到的订单是否正确;
out_trade_no是否属于当前订单;- 调用支付宝前的
biz_content是否已经更新; - 返回的支付表单是否为本次实时生成;
- 支付入口、反向代理或 CDN 是否命中了缓存。
如果服务端日志中的 out_trade_no 已经属于新订单,但支付宝页面仍显示旧订单,应继续检查前端是否重复提交了旧 DOM 表单、是否复用了旧的中间跳转页,以及业务代码是否打开了历史支付 URL。
注意事项
- 不要只靠修改时间戳解决问题。时间戳只能辅助防止缓存,真正标识订单的是
out_trade_no。 - 生成签名后,不要再修改支付宝请求中的业务参数,否则签名校验会失败。
- 每笔新订单都必须有自己的商户订单号。同一商户订单号对应的金额、标题等核心信息不应随意变化。
- 不要信任前端提交的支付金额。服务端必须根据订单重新读取金额。
- 支付按钮应防止重复点击,但不能因此长期缓存支付表单。
- 用户取消支付后是否关闭上一笔支付宝交易,应根据业务规则决定。关闭旧交易不能作为更新新订单信息的通用解决方案。
备注:内容仅供参考。
