小程序支付后跳转广告页导致支付成功回调无法触发
支付阿杰
结论
从支付宝广告服务助手返回后,原来的 my.tradePay 回调不会再次触发,也不能手动补触发。建议按以下方式处理:
- 支付前保存业务订单号,并将订单标记为”待确认”。
- 正常处理
my.tradePay回调。 - 在小程序
onShow或页面onShow中检查是否有待确认订单。 - 调用自己的服务端查询订单状态。服务端根据支付异步通知或
alipay.trade.query确认支付结果。 - 发货、开通权益等支付成功后的操作必须由服务端幂等执行。
从广告页返回,只能说明需要重新确认支付状态,不能据此认定支付成功。
原因
my.tradePay 回调只属于当前这次支付调用的客户端执行链。支付后跳转到第三方小程序时,当前小程序可能进入后台,页面也可能被系统销毁。用户返回时,可能出现以下情况:
- 原支付页面仍然存在,但支付回调不会重新执行。
- 页面被重新创建,只执行
onLoad和onShow。 - 小程序进程已被回收,再次进入时会执行
App.onLaunch和页面生命周期。 - 支付已经有结果,但客户端因页面跳转、网络异常或进程中断而没有收到结果。
因此,支付回调可以用于改善交互体验,但不能作为判断支付结果的唯一依据。最终状态应以服务端记录和支付宝侧的查询结果为准。
处理步骤
1. 支付前记录待确认订单
发起支付前,先把业务订单号保存到本地。不要只保存支付宝交易号,因为页面恢复后,通常需要使用业务订单号查询自己的服务端。
Page({
startPay() {
const orderNo = this.data.orderNo;
const tradeNO = this.data.tradeNO;
my.setStorageSync({
key: 'pendingPayment',
data: {
orderNo,
createdAt: Date.now()
}
});
my.tradePay({
tradeNO,
success: (result) => {
if (result.resultCode === '9000') {
this.confirmPayment(orderNo);
} else {
// 未明确成功时仍由服务端确认,不要直接认定支付失败
this.confirmPayment(orderNo);
}
},
fail: () => {
// 调用失败、页面跳转或回调丢失时保留待确认记录
this.confirmPayment(orderNo);
}
});
}
});
如果项目通过 orderStr 发起支付,可以用它替换 tradeNO,后续的恢复逻辑不变。
2. 从第三方小程序返回时查询订单
在支付页面的 onShow 中读取待确认订单,然后向自己的服务端查询支付状态。
Page({
onShow() {
const result = my.getStorageSync({
key: 'pendingPayment'
});
const pendingPayment = result && result.data;
if (pendingPayment && pendingPayment.orderNo) {
this.confirmPayment(pendingPayment.orderNo);
}
},
confirmPayment(orderNo) {
if (this.paymentChecking) {
return;
}
this.paymentChecking = true;
my.request({
url: `https://example.com/api/orders/${encodeURIComponent(orderNo)}/payment-status`,
method: 'GET',
success: (response) => {
const paymentStatus = response.data && response.data.status;
if (paymentStatus === 'PAID') {
my.removeStorageSync({
key: 'pendingPayment'
});
this.handlePaymentSuccess(orderNo);
return;
}
if (paymentStatus === 'CLOSED') {
my.removeStorageSync({
key: 'pendingPayment'
});
my.showToast({
content: '订单已关闭'
});
return;
}
// PAYING、UNKNOWN 等非终态暂时保留,后续进入页面时继续确认
},
complete: () => {
this.paymentChecking = false;
}
});
},
handlePaymentSuccess(orderNo) {
my.showToast({
content: '支付成功'
});
// 刷新订单、跳转结果页等
my.redirectTo({
url: `/pages/payment-result/payment-result?orderNo=${encodeURIComponent(orderNo)}`
});
}
});
如果返回后不一定进入原支付页面,也可以在 App.onShow 中检查待确认订单,再通知对应页面刷新。持久化时只保存订单号,不要保存页面实例或回调函数。
3. 由服务端确认支付宝交易状态
服务端查询接口不能直接返回客户端上报的状态。它应先查询本地订单,必要时再调用支付宝交易查询接口,例如 alipay.trade.query。
async function getPaymentStatus(req, res) {
const { orderNo } = req.params;
const order = await orderRepository.findByOrderNo(orderNo);
if (!order) {
return res.status(404).json({
status: 'NOT_FOUND'
});
}
if (order.paymentStatus === 'PAID') {
return res.json({
status: 'PAID'
});
}
const queryResult = await alipayClient.exec('alipay.trade.query', {
bizContent: {
out_trade_no: orderNo
}
});
const paid =
queryResult.tradeStatus === 'TRADE_SUCCESS' ||
queryResult.tradeStatus === 'TRADE_FINISHED';
if (paid) {
await orderService.markPaidIdempotently({
orderNo,
tradeNo: queryResult.tradeNo
});
return res.json({
status: 'PAID'
});
}
return res.json({
status: queryResult.tradeStatus || 'UNKNOWN'
});
}
不同语言和 SDK 版本使用的方法名、字段名可能不同,具体写法应以当前接入的支付宝 SDK 文档为准。服务端应使用 out_trade_no 或支付宝交易号查询真实状态,不能让客户端自行声明支付成功。
优先使用支付异步通知
如果接入的支付产品支持服务端异步通知,应优先通过异步通知更新订单状态:
- 服务端接收支付通知。
- 验证通知签名。
- 校验商户号、应用、订单号、金额、币种等字段。
- 幂等更新订单状态。
- 客户端从广告页返回后,只查询服务端保存的订单状态。
alipay.trade.query 适合用于补偿查询,例如异步通知尚未到达、客户端主动刷新或定时对账。不要在每次 onShow 时无限制地高频调用。
注意事项
onShow只表示小程序重新进入前台,不能说明支付成功。- 从广告服务助手返回后,不要直接发货或开通权益。
- 不能只判断
my.tradePay是否进入success回调。还要检查具体结果,并以服务端状态为准。 - 查询接口和订单更新必须幂等,避免支付回调、异步通知和主动查询同时到达时重复发货。
- 订单处于”处理中”时,不要立即删除本地待确认记录。可以稍后重试,但要限制重试次数并设置合理的时间间隔。
- 订单支付成功或明确关闭后,应清除本地待确认记录。
- 本地缓存可能被清理,因此服务端还应支持用户通过订单列表重新查询支付结果。
- 如果广告页跳转由某个营销产品或支付配置触发,能否关闭取决于对应产品的能力。即使可以关闭,也应保留支付状态恢复机制。
备注:内容仅供参考。
