移动应用支付成功后如何发送 Push notification
明确结论
支付成功后的 Push notification 应由服务端发送,不能由移动应用根据客户端的支付结果直接触发。
正确的流程是:支付平台通过异步回调(Webhook)通知业务服务端。服务端验证签名、核对金额并进行幂等处理,将订单状态更新为“已支付”,然后通过消息队列或后台任务调用 FCM、APNs 等推送服务。
为什么不能由客户端直接发送
客户端显示“支付成功”,不等于服务端已经确认到账,可能存在以下情况:
- 支付完成后网络中断,结果未能同步到服务端。
- 客户端回调可能遭到篡改或伪造。
- 支付平台可能延迟确认交易。
- 同一个支付回调可能被重复发送。
- 将推送服务密钥放进移动应用会带来严重的安全风险。
订单状态应以支付平台的服务端通知为准。客户端返回的结果只适合用来更新界面,例如显示“正在确认支付”。
推荐处理流程
用户发起支付
↓
支付平台处理交易
↓
支付平台调用服务端 Webhook
↓
服务端验证签名、订单号、金额和币种
↓
以幂等方式更新订单状态
↓
写入推送任务或消息队列
↓
推送服务发送通知
↓
用户点击通知后进入订单详情页
具体步骤如下:
- 用户登录应用时,将设备的 FCM registration token 或 APNs device token 上传到服务端,并与用户账号绑定。
- 创建支付交易时,由服务端保存业务订单号、支付平台交易号、金额、币种和初始状态。
- 配置支付平台的 Webhook 地址。
- 收到回调后,使用支付平台提供的 SDK 或官方方法验证签名。
- 查询并锁定订单,检查订单号、金额、币种和支付状态。
- 只有在订单第一次从未支付状态变为已支付时,才创建推送任务。
- 由后台任务调用 FCM 或 APNs 发送通知。
- 保存推送结果,并清理已经失效的设备 token。
服务端示例
以下示例使用 Node.js、Express 和 Firebase Admin SDK。不同支付平台的回调验签方式不同,因此 verifyPaymentWebhook() 只是一个接口占位,必须替换为对应平台的官方实现。
import express from "express";
import admin from "firebase-admin";
const app = express();
// 某些支付平台要求使用原始请求体进行验签。
// 是否能使用 express.json(),应以支付平台的 Webhook 文档为准。
app.use(express.json());
async function verifyPaymentWebhook(req) {
// 必须使用支付平台提供的签名验证机制。
// 验证失败时应抛出异常或返回 false。
throw new Error("Implement verification with the payment provider SDK");
}
app.post("/webhooks/payment", async (req, res) => {
let event;
try {
event = await verifyPaymentWebhook(req);
} catch (error) {
return res.status(400).send("Invalid webhook signature");
}
if (event.type !== "payment.succeeded") {
return res.sendStatus(200);
}
const {
transactionId,
orderId,
amount,
currency
} = event.data;
try {
const notificationJob = await database.transaction(async (tx) => {
const order = await tx.orders.findForUpdate(orderId);
if (!order) {
throw new Error("Order not found");
}
if (order.amount !== amount || order.currency !== currency) {
throw new Error("Payment data does not match the order");
}
// 支付平台可能重复发送同一事件。
if (order.status === "PAID") {
return null;
}
await tx.orders.update(orderId, {
status: "PAID",
transactionId,
paidAt: new Date()
});
// 建议使用 Outbox Pattern,保证订单更新和消息创建处于同一事务。
return tx.notificationJobs.create({
userId: order.userId,
orderId: order.id,
type: "PAYMENT_SUCCEEDED",
status: "PENDING"
});
});
// Webhook 应尽快返回成功。
// 实际项目中通常由队列消费者处理 notificationJob。
if (notificationJob) {
await enqueueNotification(notificationJob.id);
}
return res.sendStatus(200);
} catch (error) {
console.error("Payment webhook processing failed", error);
return res.sendStatus(500);
}
});
发送 FCM Push notification
后台任务获取用户仍然有效的设备 token 后,可以通过 Firebase Admin SDK 发送通知:
async function sendPaymentNotification(job) {
const order = await database.orders.findById(job.orderId);
const tokens = await database.deviceTokens.findActiveByUserId(job.userId);
if (tokens.length === 0) {
await database.notificationJobs.markCompleted(job.id);
return;
}
const message = {
notification: {
title: "支付成功",
body: `订单 ${order.id} 已支付成功`
},
data: {
type: "PAYMENT_SUCCEEDED",
orderId: String(order.id)
},
tokens
};
const result = await admin.messaging().sendEachForMulticast(message);
for (let index = 0; index < result.responses.length; index += 1) {
const response = result.responses[index];
if (!response.success) {
const code = response.error?.code;
if (
code === "messaging/registration-token-not-registered" ||
code === "messaging/invalid-registration-token"
) {
await database.deviceTokens.disable(tokens[index]);
}
}
}
await database.notificationJobs.markCompleted(job.id);
}
初始化 Firebase Admin SDK 时,应在服务端安全加载凭据:
admin.initializeApp({
credential: admin.credential.applicationDefault()
});
不要将 Firebase 服务账号私钥、APNs signing key 或其他服务端凭据打包进移动应用。
移动端需要做什么
移动端主要负责申请通知权限、获取设备 token、将 token 上传到服务端,以及在用户点击通知后跳转到相应页面。
上传 token 时,还要记录平台和应用环境:
{
"token": "DEVICE_PUSH_TOKEN",
"platform": "ios",
"environment": "production"
}
重新安装应用、恢复设备数据或推送服务刷新,都可能导致设备 token 发生变化。应用获取新 token 后,应及时更新服务端记录。用户退出登录时,也要解除当前 token 与用户账号的绑定,避免后续登录该设备的其他账号收到错误通知。
通知内容建议
Push notification 可以用来提示支付结果,但不能将其视为支付凭证或唯一的通知渠道。通知内容宜保持简洁:
{
"title": "支付成功",
"body": "您的订单已支付成功,点击查看详情。",
"data": {
"type": "PAYMENT_SUCCEEDED",
"orderId": "ORDER_123"
}
}
为保护用户隐私,锁屏通知中不要包含完整银行卡号、身份证号、详细商品信息或其他敏感数据。是否展示金额,应根据业务场景和用户的隐私设置决定。
需要特别注意的问题
Webhook 必须验签
不能只根据请求体中的 status: "success" 更新订单,还要验证:
- Webhook 签名是否合法。
- 事件是否来自预期的支付账户。
- 订单号是否存在。
- 金额和币种是否一致。
- 交易是否确实处于成功状态。
- 交易号是否已经处理过。
保证幂等性
支付平台可能多次发送同一个回调,网络重试也可能造成重复处理。可以将支付平台的事件 ID 或交易 ID 设为唯一键,同时限制订单只能发生一次有效的状态转换。
不要让推送阻塞支付回调
如果直接在 Webhook 请求中调用 FCM 或 APNs,推送服务一旦超时,支付平台可能会重复发送回调。更稳妥的做法是先提交数据库事务,再将通知任务交给消息队列处理。
正确处理失败和重试
遇到网络错误、限流或临时服务异常时,可以进行有限次数的指数退避重试。对于无效或已经注销的设备 token,不应反复重试,而应及时禁用或删除。
客户端仍要主动查询订单
用户可能关闭通知,设备也可能处于离线状态。省电策略或 token 失效同样会导致 Push notification 无法送达。因此,应用回到前台或打开订单页时,仍要调用服务端接口查询最新的订单状态,不能依赖 Push notification 更新核心业务数据。
备注:内容仅供参考。
