PAYATHON 2026

ASP.net MVC 5 中按钮点击后 JavaScript 重定向失效

支付小周

结论

window.location.href 没有按预期生效,是因为按钮触发了两次页面导航:

  1. 点击事件通过 JavaScript 跳转;
  2. 按钮的默认行为继续提交表单,浏览器随后跳转到 SavePayment 的响应页面。

普通表单提交会导航顶层页面。由于表单提交的响应后到,它覆盖了前面的 JavaScript 跳转,最终页面显示的是 Hello。

如果点击后只需立即跳转,应阻止表单的默认提交。不过,这样无法保证 SavePayment 请求成功发出。实际需求如果是“等待支付结果,接近超时时跳回首页”,则应阻止普通表单提交,改用 AJAX 发起支付请求,再由独立计时器控制页面跳转。

只需要立即跳转

监听表单的 submit 事件比只监听按钮的 click 更合适,因为用户按 Enter 提交表单时也能生效。

$(function () {
    $('#paymentForm').on('submit', function (event) {
        event.preventDefault();

        window.location.assign('/Home/Index');
    });
});

对应表单:

<form id="paymentForm" method="post" action="/Payment/SavePayment">
    @Html.AntiForgeryToken()

    <button id="formSubmitButton" type="submit">
        Submit
    </button>
</form>

关键是调用 event.preventDefault()。如果没有这行代码,浏览器仍会按默认行为提交表单。

这段代码取消了普通表单提交,因此不能再认为 SavePayment 一定会被调用。即使先发送请求再跳转,页面导航也可能中断浏览器尚未完成的请求。

支付场景应改用 AJAX

支付场景通常需要完成以下流程:

  • 发起支付请求;
  • 请求及时成功时进入成功页面;
  • 请求等待过久时返回首页;
  • IIS 后续返回的超时响应不能覆盖首页。

可以通过 AJAX 发起支付请求。AJAX 响应不会自动替换当前页面,所以服务器稍后返回错误页面时,也不会像普通表单提交那样覆盖整个浏览器页面。

$(function () {
    $('#paymentForm').on('submit', function (event) {
        event.preventDefault();

        var $form = $(this);
        var finished = false;
        var request;

        // 如果“1 分 58 秒”是指 1:58,则为 118000 毫秒。
        var redirectTimer = window.setTimeout(function () {
            if (finished) {
                return;
            }

            finished = true;

            // 终止客户端等待。服务器端操作不一定会因此停止。
            if (request) {
                request.abort();
            }

            window.location.replace('/Home/Index');
        }, 118000);

        request = $.ajax({
            url: $form.attr('action'),
            type: 'POST',
            data: $form.serialize()
        }).done(function (response) {
            if (finished) {
                return;
            }

            finished = true;
            window.clearTimeout(redirectTimer);

            window.location.assign('/Payment/Hello');
        }).fail(function (xhr, textStatus) {
            if (finished || textStatus === 'abort') {
                return;
            }

            finished = true;
            window.clearTimeout(redirectTimer);

            window.location.replace('/Home/Index');
        });
    });
});

计时器和 AJAX 回调都会检查 finished,确认页面是否已经开始跳转,避免请求成功与计时器几乎同时触发时发生两次导航。

window.location.replace() 不会将当前支付页留在浏览器历史记录中,通常适合超时后返回首页的情况。如果希望用户可以通过“后退”回到支付页,可改用 window.location.assign()。

Controller 最好返回结果,而不是 View

AJAX 请求收到 return View("Hello") 后,只会取得响应中的 HTML,不会自动导航到该页面。更清楚的做法是让 POST 返回 JSON,再由 JavaScript 根据结果决定跳转地址:

[HttpPost]
[ValidateAntiForgeryToken]
public ActionResult SavePayment(PaymentFormMV data)
{
    // 执行或提交支付操作。

    return Json(new
    {
        success = true,
        redirectUrl = Url.Action("Hello", "Payment")
    });
}

[HttpGet]
public ActionResult Hello()
{
    return View();
}

前端读取服务器返回的地址:

request = $.ajax({
    url: $form.attr('action'),
    type: 'POST',
    data: $form.serialize(),
    dataType: 'json'
}).done(function (result) {
    if (finished) {
        return;
    }

    finished = true;
    window.clearTimeout(redirectTimer);

    if (result.success) {
        window.location.assign(result.redirectUrl);
    } else {
        window.location.replace('/Home/Index');
    }
});

这样也能明确区分职责:POST 处理数据,GET 显示结果页面。

为什么 IIS 的超时页面不会再覆盖首页

使用普通表单提交时,SavePayment 的响应直接对应浏览器当前文档,因此它返回的 View 或错误页会成为新页面。

改用 AJAX 后,SavePayment 的响应只是后台请求的结果。计时器跳转到首页后:

  • 浏览器已经离开当前页面;
  • 原页面的 JavaScript 环境会被销毁;
  • 浏览器通常会取消尚未完成的 AJAX 请求;
  • 即使服务器随后继续执行并发生超时,该响应也不是顶层页面导航,无法替换已经打开的首页。

需要注意的是,request.abort() 只会让浏览器停止等待,不能保证服务器停止处理。服务器可能已经开始扣款、写入数据库或调用第三方支付接口。

支付业务还需要注意

浏览器计时器只能改善页面体验,不能作为判断支付状态的最终依据。后台请求超时不代表支付失败。第三方可能已经扣款,只是应用没有及时收到结果。

支付接口至少需要做到:

  • 使用订单号或支付请求 ID 保证幂等,避免重复提交导致重复扣款;
  • 明确保存“处理中”“成功”“失败”等状态;
  • 允许首页或结果页根据订单号查询最终状态;
  • 耗时的支付操作尽量交给后台任务处理,前端通过轮询、回调或消息通知获取结果;
  • 不要把 118000 毫秒当成精确的服务端超时边界。后台标签页、设备休眠或系统调度都可能让浏览器计时器延迟执行;
  • 禁用提交按钮或设置状态标志,防止用户连续点击并发出重复请求。

更可靠的结构是由前端提交支付任务,服务器尽快返回订单或任务标识,前端再查询处理结果。超过前端等待时间后,只结束等待并返回安全页面,不要直接把支付判定为失败。

备注:内容仅供参考。