PAYATHON 2026

Angular 7 使用 httpClient POST 对接 Payment gateway

支付阿杰

结论

支付网关通常不适合通过 Angular HttpClient 直接调用。更稳妥的做法是使用原生 HTML 表单,以整页 POST 的方式跳转;也可以先由自己的后端创建支付请求,再让浏览器跳转到网关。

支付完成后的 POST 回调同样不能直接交给 Angular 路由。Angular 是运行在浏览器中的前端程序,无法监听外部 HTTP POST 请求。你需要提供一个公开的后端回调地址,由后端接收请求、验证签名并处理支付结果,最后重定向到 Angular 的成功或失败页面。

为什么请求会落到 localhost

下面使用的是相对 URL:

this.http.post('direcPayment/', formData, options)

浏览器会按照当前站点解析它,例如:

http://localhost:4200/direcPayment/

在 Angular 本地开发中,这种写法可以配合开发代理使用,但启动开发服务器时必须加载代理配置:

ng serve --proxy-config proxy.config.json

也可以在 angular.json 的 serve.options 中设置 proxyConfig。

请求路径最好以 / 开头:

this.http.post('/direcPayment', body, options)

还要注意以下几点:

  • Angular 开发代理只在本地开发环境中生效。
  • 部署到生产环境后,需要使用 Nginx、Apache、API Gateway 或自己的后端提供反向代理。
  • target 应指向网关的实际协议和主机,例如 https://payment.example.com。
  • pathRewrite 处理后的路径必须与网关的真实接口路径一致,不能根据代理名称自行推断。

不要在请求中添加 CORS 响应头

下面这些头不应该由 Angular 设置:

Access-Control-Allow-Origin
Access-Control-Allow-Methods
Access-Control-Allow-Headers

它们是服务器返回给浏览器的 CORS 响应头,并不是客户端请求头。把它们添加到请求中无法解决跨域问题,还可能引发额外的预检请求。

另外,HttpHeaders 是不可变对象,调用 append() 不会修改原对象:

let headers = new HttpHeaders();
headers = headers.append('X-Custom-Header', 'value');

不过在这个场景下,通常不需要手动设置上述 CORS 头。

如果网关允许 HttpClient 调用

只有当支付网关明确支持浏览器跨域请求,并说明支付请求可以通过 AJAX/XHR 发起时,才适合使用 HttpClient。

先确认网关要求的 Content-Type。如果接口要求普通表单编码,一般使用:

application/x-www-form-urlencoded

这时不要使用 FormData,因为它会生成 multipart/form-data 请求。

import { HttpClient, HttpHeaders, HttpParams } from '@angular/common/http';

formSubmit(encryptedString: string): void {
  const requestParameter =
    `MerchantId||CollaboratorID||${encryptedString}`;

  const body = new HttpParams()
    .set('requestParameter', requestParameter);

  this.http.post('/direcPayment', body.toString(), {
    headers: new HttpHeaders({
      'Content-Type': 'application/x-www-form-urlencoded'
    }),
    responseType: 'text'
  }).subscribe({
    next: response => {
      console.log(response);
    },
    error: error => {
      console.error(error);
    }
  });
}

如果传入的 encryptedString 已经是完整的:

MerchantId||CollaboratorID||Encrypted_string

就不要再次拼接,直接把它作为 requestParameter 发送。

即使请求成功,HttpClient 发起的也只是后台 XHR 请求,不会像原生表单一样将当前页面导航到支付网关。许多网关还会返回 HTML 页面或进行连续重定向。这类流程常受 CORS、Cookie、SameSite 和第三方会话限制,一般不适合使用 HttpClient。

推荐使用原生表单跳转

如果网关文档要求“POST 到支付页面”,可以使用原生表单:

<form
  ngNoForm
  method="post"
  action="https://payment.example.com/direcPayment"
>
  <input
    type="hidden"
    name="requestParameter"
    [value]="requestParameter"
  >

  <button type="submit">前往支付</button>
</form>

在组件中准备完整参数:

requestParameter = `MerchantId||CollaboratorID||${this.encryptedString}`;

需要通过代码动态提交时,可以创建一个临时表单:

redirectToGateway(encryptedString: string): void {
  const form = document.createElement('form');
  form.method = 'POST';
  form.action = 'https://payment.example.com/direcPayment';

  const input = document.createElement('input');
  input.type = 'hidden';
  input.name = 'requestParameter';
  input.value = `MerchantId||CollaboratorID||${encryptedString}`;

  form.appendChild(input);
  document.body.appendChild(form);
  form.submit();
}

这样会触发真正的页面导航,能够兼容传统支付页面,也不依赖 AJAX CORS。

Angular 不能直接接收支付网关的 POST

例如,下面的 Angular 路由:

{
  path: 'payment/success',
  component: PaymentSuccessComponent
}

它只能在浏览器已经加载 Angular 应用后处理客户端导航,无法读取外部服务提交给服务器的 POST 请求体。

支付回调应该指向后端,例如:

POST https://api.example.com/payment/callback

后端需要完成以下处理:

  1. 读取网关提交的加密参数。
  2. 验证签名并解密。
  3. 核对订单号、金额、币种和商户信息。
  4. 更新订单状态。
  5. 返回网关要求的确认响应。
  6. 如果请求属于浏览器回跳,再重定向到 Angular 的 GET 页面。

下面是一个 Express 示例:

import express from 'express';

const app = express();

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

app.post('/payment/callback', async (req, res) => {
  const encryptedData = req.body.requestParameter;

  if (!encryptedData) {
    return res.status(400).send('Missing requestParameter');
  }

  try {
    // 按支付网关文档完成解密、验签和订单核对。
    const paymentResult = await verifyPaymentResult(encryptedData);

    // 更新操作应具备幂等性,避免重复回调造成重复处理。
    await updateOrderStatus(paymentResult);

    const orderId = encodeURIComponent(paymentResult.orderId);

    return res.redirect(
      303,
      `https://www.example.com/payment/result?orderId=${orderId}`
    );
  } catch (error) {
    return res.status(400).send('Invalid payment response');
  }
});

Angular 的结果页面只接收订单标识,再向自己的后端查询最终状态:

this.http
  .get(`/api/orders/${orderId}/payment-status`)
  .subscribe(status => {
    this.paymentStatus = status;
  });

不要把回调中的加密数据、支付成功标志或金额直接交给 Angular,并将它们当作可信结果。

回调需要区分两种情况

支付网关所说的 POST 回调可能指两种请求:

  • 浏览器回跳:支付页面通过表单 POST 到指定地址,请求由用户浏览器发出。
  • 服务端通知:支付网关服务器主动请求商户服务器,也叫 webhook 或异步通知。

两种请求都应该先由后端接收。处理服务端通知后,通常需要直接返回网关规定的确认文本。处理浏览器回跳后,可以使用 303 See Other 跳转到 Angular 页面。具体响应格式应以支付网关的接口文档为准。

还需要注意

  • 回调地址必须是支付网关可以访问的公网 HTTPS 地址,正式环境不能使用 localhost:4200。
  • 商户密钥、解密密钥和签名密钥不能放在 Angular 代码中。
  • 不能只根据成功页面判断支付是否成功,应以后端验签结果或服务端查询结果为准。
  • 网关可能重复发送回调,因此订单更新必须具备幂等性。
  • “Session expired” 很可能只是网关的通用错误页面,无法证明问题一定出在会话。还需要检查字段格式、编码方式、接口路径、商户编号、签名、时间戳,以及测试环境和生产环境是否匹配。
  • 如果网关要求 multipart/form-data,可以继续使用 FormData,但不要手动设置 Content-Type。浏览器会自动添加正确的 boundary。最终使用哪种编码格式,应以网关文档为准。

备注:内容仅供参考。