PAYATHON 2026

paygate payment transaction 完成后如何返回 AngularJS 应用

支付老李

结论

window.location.href = secureUrl 这行代码本身没有问题。支付完成后能否返回 AngularJS 应用,不取决于前端能否读取支付页面的 HTTP Headers 或 URL,而取决于支付网关是否提供返回地址机制。

完整的支付流程通常需要两个地址:

  • return_url:支付完成后,将用户浏览器带回 AngularJS 应用。
  • notify_url、callback_url 或 webhook:由支付网关直接向后端发送支付结果。

具体参数名称取决于 PayGate 接口,应以支付服务的 API 文档为准。前端既不能跨域读取支付页面,也不能只凭返回 URL 判断付款是否成功。

为什么无法访问支付页面的信息

执行下面的代码后,浏览器就离开了 AngularJS 应用:

window.location.href = secureUrl;

原页面的 JavaScript 无法继续控制支付页面。支付网关通常使用另一个域名,浏览器的同源策略会阻止应用代码读取支付页面的:

  • DOM 内容
  • 当前 URL
  • HTTP Headers
  • JavaScript 变量
  • 支付表单数据

改用 iframe 或 window.open() 也无法绕过这个限制。只要支付页面与应用不同源,应用就不能随意读取其中的内容。很多支付网站还会使用 X-Frame-Options 或 CSP,禁止页面嵌入 iframe。

因此,不要尝试监视支付页面。正确做法是使用支付网关提供的回跳和异步通知功能。

推荐的支付流程

一个比较可靠的流程是:

  1. AngularJS 前端向自己的后端发送创建支付请求。
  2. 后端生成本地订单,并向支付网关创建交易。
  3. 后端创建交易时提交 return_url 和 webhook 地址。
  4. 后端将支付网关返回的 secureUrl 交给前端。
  5. 前端跳转到 secureUrl。
  6. 用户在支付网关完成支付。
  7. 支付网关通过 webhook 通知后端。
  8. 支付网关将浏览器重定向到 AngularJS 应用的返回页面。
  9. AngularJS 返回页面根据订单号向后端查询最终状态。

浏览器回跳用于让用户顺利回到应用,后端通知或主动查询才负责确认支付结果。

AngularJS 前端发起支付

不要让前端直接拼接支付参数、金额或签名。前端只需向自己的服务器请求创建订单:

app.controller('PaymentController', function ($scope, $http, $window) {
    $scope.startPayment = function () {
        $scope.isPaying = true;

        $http.post('/api/payments', {
            productId: $scope.productId
        }).then(function (response) {
            var secureUrl = response.data.secureUrl;

            if (!secureUrl) {
                throw new Error('Payment URL is missing');
            }

            $window.location.assign(secureUrl);
        }).catch(function () {
            $scope.isPaying = false;
            $scope.errorMessage = '暂时无法发起支付,请稍后重试';
        });
    };
});

后端可以返回类似下面的数据:

{
  "orderId": "ORDER_123456",
  "secureUrl": "https://payment.example.com/transaction/..."
}

如果需要在跳转前保存界面状态,可以写入 sessionStorage:

sessionStorage.setItem('pendingOrderId', response.data.orderId);
window.location.assign(response.data.secureUrl);

不要在浏览器中保存密钥、签名密钥或完整的支付凭证。

后端创建支付交易

下面是通用的 Node.js/Express 示意代码。支付网关地址和字段都是占位符,必须替换为 PayGate API 实际规定的参数:

app.post('/api/payments', async function (req, res) {
    const order = await createLocalOrder({
        productId: req.body.productId,
        status: 'PENDING'
    });

    const paymentRequest = {
        merchant_reference: order.id,
        amount: order.amount,
        currency: 'CNY',
        return_url:
            'https://app.example.com/payment/return?orderId=' +
            encodeURIComponent(order.id),
        notify_url: 'https://api.example.com/webhooks/paygate'
    };

    const gatewayResponse = await createPayGateTransaction(paymentRequest);

    await saveGatewayTransaction(order.id, gatewayResponse.transaction_id);

    res.json({
        orderId: order.id,
        secureUrl: gatewayResponse.secure_url
    });
});

有些支付网关要求商户预先在后台配置返回地址,不允许在每次创建交易时传入。另一些网关会使用 redirect_url、success_url、cancel_url 或 callback_url 等参数名。仅从 secureUrl 无法判断这些细节,必须查阅当前网关的接口说明。

后端接收支付通知

支付期间,后端代码仍会正常运行。浏览器跳转不会停止服务器进程,也不会影响已经保存的订单。

但后端无法自行知道用户在支付页面上做了什么,只能通过以下方式取得结果:

  • 接收支付网关的 webhook。
  • 使用交易编号主动查询支付网关。
  • 如果网关支持,轮询交易状态。

Webhook 示例:

app.post('/webhooks/paygate', async function (req, res) {
    const signature = req.get('X-PayGate-Signature');

    if (!verifyPayGateSignature(req, signature)) {
        return res.status(400).send('Invalid signature');
    }

    const event = req.body;
    const order = await findOrder(event.merchant_reference);

    if (!order) {
        return res.status(404).send('Order not found');
    }

    if (event.status === 'PAID') {
        await markOrderAsPaid(order.id, event.transaction_id);
    } else if (event.status === 'FAILED') {
        await markOrderAsFailed(order.id);
    }

    res.status(200).send('OK');
});

签名算法、Header 名称和状态值都必须使用支付网关规定的格式。

Webhook 处理还必须具备幂等性。支付网关可能重复发送同一条通知,因此同一笔交易不能被重复入账、重复发货或重复增加余额。

AngularJS 处理支付返回页面

先为返回地址配置一个 AngularJS 路由:

app.config(function ($routeProvider) {
    $routeProvider.when('/payment/return', {
        templateUrl: 'payment-result.html',
        controller: 'PaymentReturnController'
    });
});

返回页面不能直接相信 URL 中的 status=success。它应先取得订单号,再向自己的后端查询:

app.controller(
    'PaymentReturnController',
    function ($scope, $http, $location) {
        var orderId =
            $location.search().orderId ||
            sessionStorage.getItem('pendingOrderId');

        $scope.status = 'CHECKING';

        if (!orderId) {
            $scope.status = 'UNKNOWN';
            return;
        }

        $http.get('/api/payments/' + encodeURIComponent(orderId))
            .then(function (response) {
                $scope.status = response.data.status;

                if ($scope.status === 'PAID') {
                    sessionStorage.removeItem('pendingOrderId');
                }
            })
            .catch(function () {
                $scope.status = 'UNKNOWN';
            });
    }
);

后端查询接口只需返回必要信息:

{
  "orderId": "ORDER_123456",
  "status": "PAID"
}

如果用户回到应用时 webhook 还没有送达,订单可能仍显示为 PENDING。这是正常情况,可以在短时间内轮询后端:

app.controller(
    'PaymentReturnController',
    function ($scope, $http, $location, $interval) {
        var orderId = $location.search().orderId;
        var attempts = 0;

        function checkStatus() {
            attempts += 1;

            $http.get('/api/payments/' + encodeURIComponent(orderId))
                .then(function (response) {
                    $scope.status = response.data.status;

                    if ($scope.status !== 'PENDING' || attempts >= 10) {
                        $interval.cancel(timer);
                    }
                });
        }

        checkStatus();
        var timer = $interval(checkStatus, 2000);

        $scope.$on('$destroy', function () {
            $interval.cancel(timer);
        });
    }
);

轮询次数和间隔应按业务需求设置,避免无限发送请求。

不要把浏览器返回结果当作支付凭证

下面这样的返回地址无法证明付款已经成功:

https://app.example.com/payment/return?orderId=123&status=success

用户可以修改查询参数,浏览器重定向也可能被伪造。后端必须通过可信渠道确认最终支付状态:

  1. 验证网关 webhook 的签名。
  2. 核对商户订单号、网关交易号、金额和币种。
  3. 必要时调用支付网关的交易查询接口复核。
  4. 确认无误后,再将本地订单改为已支付。

还要处理用户付款后直接关闭页面的情况。此时浏览器不会返回应用,但 webhook 仍应让后端正确更新订单。因此,支付流程不能只依赖 return_url。

如果支付网关不支持返回地址

如果支付接口既不接受 return_url,商户后台也没有回跳配置,前端就无法强制支付页面返回应用。此时只能使用网关实际提供的能力,例如:

  • 通过 webhook 通知后端;
  • 由后端主动查询交易状态;
  • 在应用中提供”我已完成支付”按钮;
  • 用户返回应用后,根据订单号查询付款结果。

完整方案不需要读取第三方支付页面。支付网关负责将浏览器带回应用,后端负责通过可信渠道确认交易状态。

备注:内容仅供参考。