H5调用支付宝form支付时如何监听支付成功并跳转?
结论
H5 提交支付宝支付表单后,前端无法直接监听支付宝页面中的”支付成功”事件。
通常按以下方式处理:
- 服务端通过支付宝异步通知
notify_url接收并验证支付结果。 - 前端进入支付宝返回页或业务中转页后,轮询服务端的订单状态。
- 服务端确认订单已经支付,前端再跳转到支付成功页。
return_url 只表示用户从支付宝页面返回,不能用来判断支付是否成功。用户可能关闭页面、遇到网络中断,也可能修改返回参数,因此不能仅根据前端回跳认定付款成功。
推荐流程
H5 创建订单
↓
服务端生成支付宝支付表单
↓
前端提交 form,进入支付宝收银台
↓
支付宝异步通知 notify_url
↓
服务端验签、核对订单并更新支付状态
↓
用户进入 return_url 指定的业务中转页
↓
中转页轮询服务端订单状态
↓
确认已支付后跳转支付成功页
异步通知与页面返回的先后顺序并不固定。用户回到业务页面时,服务端可能还没有收到异步通知,所以中转页一般需要进行一段时间的短轮询。
前端提交支付表单
服务端调用支付宝手机网站支付接口后,通常会返回一段可提交的 HTML 表单。前端拿到表单后,将其写入页面并提交:
<div id="alipay-form"></div>
<script>
async function startPay(orderNo) {
const response = await fetch('/api/pay/alipay/create', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
orderNo
})
});
const result = await response.json();
if (!response.ok || !result.formHtml) {
throw new Error(result.message || '创建支付失败');
}
const container = document.getElementById('alipay-form');
container.innerHTML = result.formHtml;
const form = container.querySelector('form');
if (!form) {
throw new Error('支付宝支付表单无效');
}
form.submit();
}
</script>
表单提交后,当前页面一般会跳转到支付宝收银台,无法依靠当前页面中的 JavaScript 回调等待支付完成。
配置支付结果返回地址
创建支付请求时,可以配置:
notify_url:支付宝向服务端发送异步支付通知。return_url:支付流程结束后,用户浏览器可能返回的页面。
字段的具体放置位置取决于所使用的支付宝 SDK,概念上可以按以下结构配置:
const payRequest = {
notify_url: 'https://example.com/api/pay/alipay/notify',
return_url: `https://example.com/pay/wait?orderNo=${encodeURIComponent(orderNo)}`,
biz_content: {
out_trade_no: orderNo,
total_amount: '99.00',
subject: '订单支付',
product_code: 'QUICK_WAP_WAY'
}
};
不要将”支付成功页”直接设为 return_url。可以先返回 /pay/wait 中转页,由该页面查询订单状态,确认支付成功后再进入真正的成功页。
服务端处理异步通知
异步通知接口需要完成以下检查:
- 使用支付宝公钥验证通知签名。
- 检查
app_id是否属于当前应用。 - 检查
out_trade_no是否对应本系统订单。 - 检查
total_amount是否与订单金额一致。 - 根据业务要求检查收款方等信息。
- 判断
trade_status。 - 幂等更新订单,防止重复通知造成重复发货。
- 处理完成后,按支付宝要求返回成功响应。
以下是示意代码,验签方法需要替换为实际 SDK 提供的接口:
app.post(
'/api/pay/alipay/notify',
express.urlencoded({ extended: false }),
async (req, res) => {
const params = req.body;
const verified = verifyAlipaySignature(params);
if (!verified) {
return res.status(400).send('failure');
}
const order = await findOrder(params.out_trade_no);
if (!order) {
return res.status(400).send('failure');
}
if (params.app_id !== process.env.ALIPAY_APP_ID) {
return res.status(400).send('failure');
}
if (String(params.total_amount) !== String(order.payAmount)) {
return res.status(400).send('failure');
}
const paid =
params.trade_status === 'TRADE_SUCCESS' ||
params.trade_status === 'TRADE_FINISHED';
if (paid) {
await markOrderPaidOnce({
orderNo: order.orderNo,
tradeNo: params.trade_no
});
}
res.send('success');
}
);
markOrderPaidOnce 必须是幂等操作。可以通过数据库事务、订单状态条件更新或唯一约束,保证同一订单只处理一次:
UPDATE orders
SET pay_status = 'PAID',
alipay_trade_no = ?
WHERE order_no = ?
AND pay_status = 'UNPAID';
前端轮询订单状态
return_url 指向的中转页可以定时查询服务端。只有服务端明确返回已支付状态,前端才跳转到成功页:
<p id="status-text">正在确认支付结果,请稍候……</p>
<script>
const params = new URLSearchParams(window.location.search);
const orderNo = params.get('orderNo');
const intervalMs = 2000;
const timeoutMs = 60000;
const startedAt = Date.now();
async function checkPayment() {
if (!orderNo) {
document.getElementById('status-text').textContent = '缺少订单号';
return;
}
try {
const response = await fetch(
`/api/orders/${encodeURIComponent(orderNo)}/pay-status`,
{
method: 'GET',
cache: 'no-store',
credentials: 'same-origin'
}
);
const result = await response.json();
if (result.status === 'PAID') {
window.location.replace(
`/pay/success?orderNo=${encodeURIComponent(orderNo)}`
);
return;
}
if (result.status === 'CLOSED' || result.status === 'FAILED') {
document.getElementById('status-text').textContent =
'订单未支付成功,请重新发起支付';
return;
}
} catch (error) {
console.error('查询支付状态失败', error);
}
if (Date.now() - startedAt >= timeoutMs) {
document.getElementById('status-text').textContent =
'支付结果仍在确认中,请稍后查看订单状态';
return;
}
window.setTimeout(checkPayment, intervalMs);
}
checkPayment();
</script>
对应的服务端查询接口只返回本地订单状态:
app.get('/api/orders/:orderNo/pay-status', async (req, res) => {
const order = await findCurrentUserOrder(
req.user.id,
req.params.orderNo
);
if (!order) {
return res.status(404).json({
message: '订单不存在'
});
}
res.json({
status: order.payStatus
});
});
查询接口必须验证当前用户是否有权访问该订单,不能只凭前端传入的订单号返回订单信息。
异步通知尚未到达时的补偿处理
少数情况下,用户已经付款,但服务端暂时没有收到异步通知。服务端可以调用支付宝订单查询接口,主动查询交易状态并更新本地订单。
查询必须由服务端发起,不能将应用私钥或其他敏感配置放入 H5 页面。
常见的处理方式包括:
- 前几次轮询只查询本地数据库。
- 等待数秒仍未确认时,由服务端触发一次支付宝订单查询。
- 查询结果仍不明确时,提示用户稍后到订单列表查看。
- 后台定时任务继续补查长期处于”支付中”状态的订单。
为什么不能只监听 return_url
return_url 无法可靠确认支付结果,原因包括:
- 用户支付后可能直接关闭支付宝页面,没有返回商户页面。
- 用户取消支付后也可能回到业务页面。
- 页面跳转可能早于异步通知。
- 浏览器地址中的参数可能被修改。
- 网络或浏览器限制可能导致回跳失败。
即使返回地址携带了支付宝参数,前端也不应自行判断支付成功。验签和查询支付宝交易状态等操作都应由服务端完成。
注意事项
- 订单金额应以服务端数据库为准,不要直接使用前端传入的金额发起支付。
- 支付成功页也应重新查询服务端订单状态,防止用户手动输入成功页地址。
- 异步通知地址必须能被支付宝服务器通过公网访问。
- 通知验签应使用实际 SDK 推荐的方法,并正确区分支付宝公钥与应用公钥。
- 不要在异步通知接口中执行耗时较长的业务。可以先幂等更新订单,再通过消息队列处理发货、积分等后续流程。
- 轮询需要设置间隔和超时时间,避免无限请求。
- 如果应用允许多端支付,订单状态应以服务端为唯一依据,不能依赖某个浏览器页面的生命周期。
TRADE_SUCCESS和TRADE_FINISHED的具体业务处理差异,需要结合当前接入产品及支付宝接口文档确定,不要在没有核对接口规则时自行推断。
备注:内容仅供参考。
