PAYATHON 2026

如何在 AngularJS 页面接收 POST request?

支付阿杰

结论

AngularJS 的 $routeProvider 只处理浏览器加载应用后的客户端路由,无法接收 Payment Gateway 发往服务器的 HTTP POST 请求。

应按下面的方式处理:

  1. 在后端创建真实的 Callback 接口,接收支付平台发来的 POST。
  2. 验证回调签名,并根据订单号向支付平台核验支付状态。
  3. 保存支付结果。
  4. 如果请求是浏览器的前台回跳,后端返回 303 See Other,把浏览器重定向到 AngularJS 的 GET 页面。
  5. AngularJS 成功页通过后端 API 查询订单的最终状态。

支付回调和 AngularJS 页面路由需要分开处理。

为什么 $routeProvider 没有生效

以下配置注册的是 AngularJS 客户端路由:

$routeProvider.when('/booking-success', {
  templateUrl: 'views/booking-success.html',
  controller: 'BookingSuccessController'
});

它只有在以下条件都满足时才会运行:

  • 浏览器已经通过 GET 加载 AngularJS 应用;
  • AngularJS 已经启动;
  • 客户端路由检测到当前路径为 /booking-success。

Payment Gateway 发送的请求则是:

POST /booking-success HTTP/1.1
Content-Type: application/x-www-form-urlencoded

这个请求会先到达 Web 服务器或后端应用。此时 AngularJS 还没有运行,服务器必须决定如何处理这个 POST。如果服务器没有对应的 POST 路由,通常会返回 404、405 Method Not Allowed,或者直接拒绝请求。

在浏览器地址栏中直接打开该地址可以成功,因为地址栏发出的是 GET /booking-success。它和支付平台发送的 POST /booking-success 是两种不同的请求。

推荐的请求流程

可以为支付通知和用户回跳分别设置地址:

支付平台服务端
    POST /api/payments/callback
              |
              v
      验签、核验并更新订单

用户浏览器
    GET /booking-success?orderId=...
              |
              v
       AngularJS 成功页面
              |
              v
    GET /api/orders/:orderId/status

如果支付平台只能通过表单 POST 把用户带回网站,可以使用以下流程:

POST /payment/return
        |
        v
后端处理并返回 303
        |
        v
GET /booking-success?orderId=...

不要让 AngularJS 直接处理 POST 回调。

后端接收回调并重定向

下面以 Express 为例。实际使用的字段名、签名算法和响应内容,必须以对应 Payment Gateway 的文档为准。

const express = require('express');

const app = express();

app.use(express.urlencoded({ extended: false }));
app.use(express.json());

app.post('/payment/return', async (req, res) => {
  const paymentData = req.body;

  try {
    const signatureValid = verifyGatewaySignature(paymentData);

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

    // 不应只相信回调中的 success 字段。
    // 在支付平台支持的情况下,应通过其服务端 API 核验交易状态。
    const payment = await queryPaymentFromGateway(
      paymentData.transactionId
    );

    await updateOrderPaymentStatus({
      orderId: paymentData.orderId,
      transactionId: payment.transactionId,
      status: payment.status
    });

    const orderId = encodeURIComponent(paymentData.orderId);

    // POST/Redirect/GET:明确要求浏览器下一步使用 GET。
    return res.redirect(303, `/booking-success?orderId=${orderId}`);
  } catch (error) {
    console.error('Payment return processing failed:', error);
    return res.redirect(303, '/booking-failed');
  }
});

这里使用 303 See Other,是为了让浏览器在重定向后发起新的 GET 请求,避免再次提交原来的 POST。

不建议依赖默认的 302 行为来处理支付回跳,因为不同客户端处理 POST 重定向的方式可能不同。307 和 308 会保留原请求方法,也不适合把 POST 转成成功页的 GET。

AngularJS 路由配置

如果应用使用 HTML5 路由模式,可以这样配置:

angular.module('bookingApp', ['ngRoute'])
  .config(function ($routeProvider, $locationProvider) {
    $routeProvider
      .when('/booking', {
        templateUrl: 'views/booking.html',
        controller: 'BookingController'
      })
      .when('/booking-success', {
        templateUrl: 'views/booking-success.html',
        controller: 'BookingSuccessController'
      })
      .when('/booking-failed', {
        templateUrl: 'views/booking-failed.html'
      })
      .otherwise({
        redirectTo: '/booking'
      });

    $locationProvider.html5Mode(true);
  });

成功页根据订单号向后端查询最终结果:

angular.module('bookingApp')
  .controller('BookingSuccessController', function (
    $scope,
    $http,
    $location
  ) {
    const orderId = $location.search().orderId;

    $scope.loading = true;

    $http.get(
      '/api/orders/' + encodeURIComponent(orderId) + '/payment-status'
    ).then(function (response) {
      $scope.payment = response.data;
    }).catch(function () {
      $scope.error = '暂时无法获取支付结果,请稍后刷新页面。';
    }).finally(function () {
      $scope.loading = false;
    });
  });

后端状态接口只需返回页面展示所需的信息:

app.get('/api/orders/:orderId/payment-status', async (req, res) => {
  const order = await findOrderVisibleToCurrentUser(
    req.params.orderId,
    req.user
  );

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

  return res.json({
    orderId: order.id,
    paymentStatus: order.paymentStatus
  });
});

配置服务器回退到 AngularJS 入口页

在 HTML5 路由模式下,浏览器访问:

GET /booking-success?orderId=123

请求仍会先到达服务器。服务器需要为这个 GET 返回 AngularJS 应用的 index.html,然后由 $routeProvider 渲染成功页面。

Express 可以在 API 和支付回调路由之后添加以下配置:

const path = require('path');

app.use(express.static(path.join(__dirname, 'public')));

// 必须放在 API、支付回调和静态资源路由之后。
app.get('*', function (req, res) {
  res.sendFile(path.join(__dirname, 'public', 'index.html'));
});

Nginx 的典型配置如下:

location / {
    try_files $uri $uri/ /index.html;
}

SPA 回退规则不能截获支付回调接口。以下请求必须由后端接口处理:

POST /api/payments/callback
POST /payment/return

使用 Hash 路由时的处理方式

如果 AngularJS 使用默认的 Hash 路由,成功页地址可能是:

https://xyz.com/#/booking-success?orderId=123

后端可以这样重定向:

res.redirect(
  303,
  '/#/booking-success?orderId=' + encodeURIComponent(orderId)
);

URL 中 # 后面的内容不会发送给服务器。因此,Payment Gateway 的 Callback URL 不能设置为:

https://xyz.com/#/payment-callback

服务器实际只能收到对 / 的请求,无法根据 Fragment 处理支付回调。Callback URL 必须指向真实的后端地址,例如:

https://xyz.com/api/payments/callback

区分服务端通知和浏览器回跳

支付平台通常会提供两类 URL:

  • notify_url、webhook_url 或 Callback URL:支付平台服务器向商户服务器发送通知。
  • return_url、success_url:支付完成后,把用户浏览器带回商户页面。

服务端通知不一定会把用户浏览器带回来。支付平台可能只在后台向 Callback URL 发送 HTTP 请求,并要求接口返回固定文本或指定状态码。这种情况下,即使后端返回重定向,执行跳转的也是支付平台的 HTTP 客户端,而不是用户浏览器。

如果平台同时支持这两类地址,应分别配置:

Callback URL:
https://xyz.com/api/payments/callback

Return URL:
https://xyz.com/booking-success

具体名称、请求格式和回调响应要求,应以相应支付平台的接口文档为准。

安全和可靠性注意事项

支付成功页不能只根据回调参数或 URL 参数判断付款是否成功。例如,用户可以自行构造以下地址:

/booking-success?status=paid&orderId=123

处理支付回调时,至少需要注意以下事项:

  • 按照支付平台规定的方式验证签名。
  • 校验支付金额、币种、商户号和订单号。
  • 如果条件允许,通过支付平台的服务端 API 再次查询交易状态。
  • 回调处理必须支持幂等。同一通知重复到达时,不能重复发货或重复记账。
  • 订单状态应保存在后端,不能只存放在 AngularJS 内存或 localStorage 中。
  • 不要把签名密钥、API Secret 等凭据放入 AngularJS 前端代码。
  • Callback URL 应使用 HTTPS。
  • 成功页应从后端查询状态,并处理”支付结果确认中”的情况。
  • 如果回跳依赖登录 Cookie,需要检查 Cookie 的 SameSite 设置。跨站表单 POST 可能不会携带设置为 SameSite=Lax 或 Strict 的 Cookie。

整体结构是:后端接收并验证支付回调,AngularJS 通过 GET 打开结果页面,再展示后端保存的订单状态。

备注:内容仅供参考。