PAYATHON 2026

AngularJS http post 后跳转支付网关时报错

支付老李

结论

这两个错误的原因不同:

  • $interpolate:interr 通常出现在模板插值 form 的 action 时。AngularJS 会把外部支付地址作为受保护的资源 URL 进行检查,检查失败后便会报错。具体原因要看错误中嵌套的 $sce 信息。
  • element.submit is not a function 是因为指令中的 element 是 AngularJS/jqLite(或 jQuery)包装对象,并非原生 <form> 元素。

建议不要在模板中动态插值支付网关地址。收到事件后,直接设置原生表单属性,创建隐藏字段,再调用原生表单的提交方法。

推荐修改

先在页面中放置一个用于跳转的表单:

<form auto-submit-form event="gateway.redirect"></form>

控制器只需广播一次事件,并传入完整数据:

$scope.transaction = function () {
    var data = $scope.details;

    var formData = {
        redirectUrl: 'https://payment-gateway-url',
        redirectMethod: 'POST',
        redirectData: {
            id: data.id,
            para1: data.para1
        }
    };

    $rootScope.$broadcast('gateway.redirect', formData);
};

对应的指令可以这样写:

app.directive('autoSubmitForm', ['$timeout', function ($timeout) {
    return {
        restrict: 'A',
        link: function ($scope, element, $attrs) {
            var form = element[0];

            $scope.$on($attrs.event, function (event, formData) {
                if (!formData || !formData.redirectUrl) {
                    return;
                }

                // 实际项目中应限制为受信任的支付网关地址。
                form.action = formData.redirectUrl;
                form.method =
                    String(formData.redirectMethod || 'POST').toUpperCase() === 'GET'
                        ? 'GET'
                        : 'POST';

                // 删除上一次提交生成的隐藏字段。
                while (form.firstChild) {
                    form.removeChild(form.firstChild);
                }

                angular.forEach(formData.redirectData, function (value, name) {
                    var input = document.createElement('input');

                    input.type = 'hidden';
                    input.name = name;
                    input.value = value == null ? '' : String(value);

                    form.appendChild(input);
                });

                // 等当前 AngularJS 更新结束后提交。
                $timeout(function () {
                    HTMLFormElement.prototype.submit.call(form);
                }, 0, false);
            });
        }
    };
}]);

为什么不能直接调用 element.submit()

AngularJS 指令传入的 element 是类似下面这样的包装对象:

angular.element(formElement)

真正的 DOM 节点在下标 0 中:

element[0]

所以最简单的修改方式是:

element[0].submit();

更稳妥的写法则是:

HTMLFormElement.prototype.submit.call(element[0]);

如果表单中有一个名为 submit 的控件,例如:

<input name="submit">

这个控件可能会覆盖表单自身的 submit 方法。直接调用原型方法可以避开这一问题。

$interpolate:interr 的常见原因

如果原模板包含类似代码:

<form action="{{formData.redirectUrl}}"
      method="{{formData.redirectMethod}}">

AngularJS 会对 action 执行插值和安全检查。遇到外部支付网关 URL 时,某些 AngularJS 版本或安全配置还可能报告 $sce:insecurl,外层则显示为 $interpolate:interr。

不要为了消除错误就直接信任任意 URL。支付跳转地址通常应由后端生成,或者至少限制在明确的支付网关域名内。例如:

function isAllowedGateway(url) {
    return /^https:\/\/payment-gateway-url(?:\/|$)/.test(url);
}

验证通过后,再设置表单地址:

form.action = formData.redirectUrl;

事件发送也需要修正

原代码连续执行了:

$rootScope.$broadcast('gateway.redirect', formData);
$scope.$emit('gateway.redirect');

第二次调用 $emit() 时没有传入 formData。而且 $emit() 只沿作用域树向上传递,通常无法通知兄弟作用域中的指令。这可能导致重复处理,也可能让监听器收到 undefined。

保留一次广播即可:

$rootScope.$broadcast('gateway.redirect', formData);

如果控制器和指令之间有明确的父子作用域关系,也可以使用局部作用域事件。对于更大的项目,通过服务协调会更合适,可以避免大量依赖 $rootScope。

注意事项

原生 HTML 表单提交会让浏览器离开当前页面,适合用来跳转支付网关。它和 $http.post() 不同,通常不受 Ajax CORS 限制。

还需要注意以下几点:

  • redirectUrl 必须来自可信来源,不能直接使用用户输入。
  • 支付金额、订单号和签名必须由服务端校验,不能信任前端隐藏字段。
  • 如果支付网关要求嵌套对象、数组或特定的签名格式,需要按照接口文档展开字段。
  • 如果页面启用了严格的 Content Security Policy,支付网关域名还必须符合页面的 form-action 策略。
  • replace: true 已被弃用,也没有必要使用。建议把指令直接挂载到真实的 <form> 元素上。

备注:内容仅供参考。