PAYATHON 2026

H5 唤起支付宝支付时重复支付上一笔订单如何解决?

支付老李

结论

每次选择新订单,都要由服务端重新调用 alipay.trade.wap.pay,使用新的商户订单号生成支付表单,再让浏览器加载并提交这份表单。上一笔支付的表单 HTML、跳转地址、签名参数和中间页面都不能复用。

出现这种情况,通常不是支付宝 APP 自动选中了旧订单,而是 H5 页面、业务服务端或浏览器历史缓存仍在使用上一笔订单的支付请求。

常见原因

建议依次检查以下环节:

  1. 复用了 out_trade_no

    out_trade_no 是商户订单号。同一笔业务订单再次发起支付时,可以继续使用原订单号;切换到另一笔订单后,则必须传入该订单对应的唯一编号。

  2. 复用了支付表单或跳转地址

    alipay.trade.wap.pay 返回的表单、签名和跳转参数都与当前订单绑定。不要提前生成后长期保存在变量、DOM、缓存或会话中,也不要反复提交同一份表单。

  3. 支付接口被缓存

    如果生成支付表单的接口使用 GET,或者请求经过浏览器、CDN、网关缓存,相同 URL 可能直接返回上一笔订单的支付页面。

  4. 页面恢复了旧状态

    用户从支付宝 APP 返回浏览器时,部分浏览器会通过页面历史记录或 Back-Forward Cache 恢复原页面。如果代码继续读取旧的全局变量、隐藏字段或 DOM 节点,就可能再次提交上一笔订单。

  5. 服务端按用户或 Session 缓存了支付参数

    如果服务端将 payForm、out_trade_no 等支付数据保存在 Session 中,切换订单后又没有及时覆盖,就可能生成或返回旧请求。

推荐处理流程

建议按下面的流程处理:

  1. 用户选择订单。
  2. 前端只向服务端提交订单 ID,不自行拼接金额和支付宝签名参数。
  3. 服务端重新查询订单,并校验订单所属用户、金额和支付状态。
  4. 服务端取得当前订单唯一的 out_trade_no。
  5. 服务端实时调用 alipay.trade.wap.pay 生成支付表单。
  6. 浏览器提交新表单并唤起支付宝。
  7. 用户返回后,前端重新查询订单状态,不直接根据前端页面状态判断支付结果。

前端示例

可以通过普通 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。
  • 生成签名后,不要再修改支付宝请求中的业务参数,否则签名校验会失败。
  • 每笔新订单都必须有自己的商户订单号。同一商户订单号对应的金额、标题等核心信息不应随意变化。
  • 不要信任前端提交的支付金额。服务端必须根据订单重新读取金额。
  • 支付按钮应防止重复点击,但不能因此长期缓存支付表单。
  • 用户取消支付后是否关闭上一笔支付宝交易,应根据业务规则决定。关闭旧交易不能作为更新新订单信息的通用解决方案。

备注:内容仅供参考。