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。
因此,不要尝试监视支付页面。正确做法是使用支付网关提供的回跳和异步通知功能。
推荐的支付流程
一个比较可靠的流程是:
- AngularJS 前端向自己的后端发送创建支付请求。
- 后端生成本地订单,并向支付网关创建交易。
- 后端创建交易时提交
return_url和 webhook 地址。 - 后端将支付网关返回的
secureUrl交给前端。 - 前端跳转到
secureUrl。 - 用户在支付网关完成支付。
- 支付网关通过 webhook 通知后端。
- 支付网关将浏览器重定向到 AngularJS 应用的返回页面。
- 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
用户可以修改查询参数,浏览器重定向也可能被伪造。后端必须通过可信渠道确认最终支付状态:
- 验证网关 webhook 的签名。
- 核对商户订单号、网关交易号、金额和币种。
- 必要时调用支付网关的交易查询接口复核。
- 确认无误后,再将本地订单改为已支付。
还要处理用户付款后直接关闭页面的情况。此时浏览器不会返回应用,但 webhook 仍应让后端正确更新订单。因此,支付流程不能只依赖 return_url。
如果支付网关不支持返回地址
如果支付接口既不接受 return_url,商户后台也没有回跳配置,前端就无法强制支付页面返回应用。此时只能使用网关实际提供的能力,例如:
- 通过 webhook 通知后端;
- 由后端主动查询交易状态;
- 在应用中提供”我已完成支付”按钮;
- 用户返回应用后,根据订单号查询付款结果。
完整方案不需要读取第三方支付页面。支付网关负责将浏览器带回应用,后端负责通过可信渠道确认交易状态。
备注:内容仅供参考。
