PAYATHON 2026

AngularJS 在表单提交前同步调用 server

支付老李

结论

不要把 $http 改成同步请求。正确的处理方式是先取消提交事件,等异步请求完成并填好隐藏字段,再通过原生表单 API 将数据提交到支付网关。

onsubmit 只能立即返回 true 或 false,不能等待 Promise。同步 XHR 会阻塞浏览器主线程,不适合支付流程。

推荐实现

可以用 AngularJS 的 ng-submit 接管表单提交:

<form
    id="bookingForm"
    name="bookingForm"
    method="post"
    action="https://payment-gateway.example/pay"
    ng-submit="submitPayment($event)">

    <!-- 用户填写的字段 -->

    <input
        type="hidden"
        name="paymentToken"
        ng-model="payment.token">

    <input
        type="hidden"
        name="transactionId"
        ng-model="payment.transactionId">

    <button type="submit" ng-disabled="submitting">
        {{ submitting ? '正在处理…' : 'Payment' }}
    </button>
</form>

在 Controller 中先调用自己的服务器,请求成功后再提交表单:

app.controller('BookingController', function ($scope, $http, $timeout) {
    $scope.payment = {};
    $scope.submitting = false;
    $scope.errorMessage = '';

    $scope.submitPayment = function ($event) {
        $event.preventDefault();

        if ($scope.submitting) {
            return;
        }

        $scope.submitting = true;
        $scope.errorMessage = '';

        var requestData = {
            // 从当前表单模型中读取需要提交给后端的数据
        };

        $http.post('/api/payments/create', requestData)
            .then(function (response) {
                if (!response.data ||
                    !response.data.paymentToken ||
                    !response.data.transactionId) {
                    throw new Error('服务器返回的支付信息不完整');
                }

                $scope.payment.token = response.data.paymentToken;
                $scope.payment.transactionId =
                    response.data.transactionId;

                /*
                 * 等待当前 digest 完成,确保 ng-model 已经把值写入
                 * hidden inputs,然后调用原生 submit()。
                 */
                $timeout(function () {
                    var form =
                        document.getElementById('bookingForm');

                    HTMLFormElement.prototype.submit.call(form);
                }, 0);
            })
            .catch(function (error) {
                $scope.submitting = false;
                $scope.errorMessage = '暂时无法创建支付,请稍后重试。';

                // 可在这里记录 error,但不要向用户暴露敏感信息。
                console.error('Unable to create payment', error);
            });
    };
});

这里调用:

HTMLFormElement.prototype.submit.call(form);

不要再次触发 submit button,否则会重新进入 ng-submit,导致 $http 请求重复执行。如果表单里有 name="submit" 的控件,这种写法还能避免该控件覆盖 form.submit 方法。

为什么不能等待 onsubmit 返回

下面的写法拿不到服务器最终返回的布尔值:

var success = scope.createPayment();
return success;

$http 会立即返回 Promise,网络响应稍后才会到达。此时 allowSubmit() 已经执行完毕,浏览器不会暂停提交事件来等待 Promise。

即使改成:

scope.createPayment().then(function () {
    return true;
});

其中的 return true 也只是返回给 .then(),无法返回给已经结束的 onsubmit。

因此,需要把提交过程分成两个阶段:

  1. 第一次触发表单提交时调用 preventDefault(),阻止浏览器直接提交。
  2. $http 请求成功后填充隐藏字段,再主动调用原生 submit()。

如果不想依赖 AngularJS 更新隐藏字段

请求成功后也可以直接给表单控件赋值,然后立即提交:

$http.post('/api/payments/create', requestData)
    .then(function (response) {
        var form = document.getElementById('bookingForm');

        form.elements.paymentToken.value =
            response.data.paymentToken;

        form.elements.transactionId.value =
            response.data.transactionId;

        HTMLFormElement.prototype.submit.call(form);
    });

这种写法不需要等待 digest,但隐藏字段的 name 必须与支付网关要求完全一致。

注意事项

  • 提交期间应禁用按钮或使用状态锁,防止用户连续点击并创建多个支付订单。
  • 只有请求成功且返回字段完整时,才能把表单提交到支付网关。请求失败时应停留在当前页面,并允许用户重试。
  • 金额、订单号、签名和回调地址等重要数据应由服务器生成或校验,不能信任浏览器提交的值。
  • 不要在日志、URL 或错误提示中输出支付密钥、签名原文、信用卡信息等敏感数据。
  • form.submit() 会跳过浏览器的原生表单校验。如果需要校验,应在发送 $http 请求前检查 AngularJS 表单状态,例如:
if ($scope.bookingForm.$invalid) {
    $scope.bookingForm.$setSubmitted();
    return;
}
  • 不建议使用同步 XMLHttpRequest。它会冻结页面,网络较慢时浏览器看起来像失去响应,而且主线程同步请求已经是不推荐的旧式做法。
  • 如果支付网关支持由服务器创建支付会话并返回跳转 URL,可以在 $http 请求成功后直接设置 window.location。这种方式通常更简单,但能否使用取决于支付网关的接口设计。

备注:内容仅供参考。