如何在 API 数据更新后向用户推送通知?
结论
通知应在后端成功提交 payment remittances 数据后触发,不应依赖某个通用 API 事件。比较稳妥的处理方式是:数据写入成功后生成 PaymentRemittancesUpdated 等业务事件,再由异步任务通过 FCM、APNs 或应用内消息服务通知用户。
触发时机必须在提交成功之后。如果数据库事务还未完成就发送通知,用户收到通知时可能仍看不到最新数据。写入最终失败时,还可能产生错误通知。
推荐流程
完整的数据更新和通知流程可以这样设计:
- 后端获取最新的 payment remittances。
- 在数据库事务中写入或更新相关记录。
- 确认事务提交成功。
- 生成
PaymentRemittancesUpdated业务事件。 - 将事件写入消息队列或任务队列。
- 通知服务消费事件,并调用 FCM、APNs 等推送服务。
- App 收到通知后重新调用 API,获取最新数据。
流程如下:
Payment Provider
↓
Backend Sync Job
↓
Database Transaction
↓ commit success
PaymentRemittancesUpdated
↓
Message Queue / Job Queue
↓
Notification Worker
↓
FCM / APNs
↓
User Device
所以,真正的触发条件不是 API 请求已经结束,而是最新的汇款数据已经成功持久化。
简单实现方式
业务量较小时,可以等数据库更新完成后直接调用通知服务:
async function updatePaymentRemittances(userId, remittances) {
await database.transaction(async (transaction) => {
await transaction.paymentRemittance.upsertMany(remittances);
});
await notificationService.sendToUser(userId, {
title: "Payment remittances updated",
body: "Your latest payment remittance information is available.",
data: {
type: "PAYMENT_REMITTANCES_UPDATED"
}
});
}
这里要确保 notificationService.sendToUser() 在数据库事务成功提交后执行。
这种方式实现简单,但数据库更新成功后,如果推送服务暂时不可用,通知就可能发送失败。付款、汇款等重要业务通常还需要重试机制和失败记录。
使用事件和任务队列
可靠性要求较高时,可以把数据更新和推送发送拆开处理:
async function updatePaymentRemittances(userId, remittances) {
await database.transaction(async (transaction) => {
await transaction.paymentRemittance.upsertMany(remittances);
await transaction.outboxEvent.create({
type: "PaymentRemittancesUpdated",
aggregateId: userId,
payload: JSON.stringify({
userId,
updatedAt: new Date().toISOString()
})
});
});
}
后台任务读取尚未处理的 outbox 事件,再将其投递到消息队列:
async function publishOutboxEvents() {
const events = await database.outboxEvent.findPending();
for (const event of events) {
try {
await messageQueue.publish(event.type, JSON.parse(event.payload));
await database.outboxEvent.markPublished(event.id);
} catch (error) {
logger.error("Failed to publish outbox event", {
eventId: event.id,
error
});
}
}
}
通知服务负责消费事件:
messageQueue.subscribe(
"PaymentRemittancesUpdated",
async ({ userId, updatedAt }) => {
const devices = await deviceRepository.findActiveDevices(userId);
for (const device of devices) {
await pushProvider.send(device.pushToken, {
notification: {
title: "Remittance updated",
body: "Your latest payment remittance is ready."
},
data: {
type: "PAYMENT_REMITTANCES_UPDATED",
updatedAt
}
});
}
}
);
Transactional Outbox 模式可以避免两类常见问题:
- 数据库写入失败,通知却已经发出。
- 数据库写入成功,但服务在生成通知事件前崩溃。
消息队列可以使用 RabbitMQ、Kafka、Amazon SQS 或 Google Cloud Pub/Sub,也可以选用 BullMQ、Celery 等任务队列。具体选择取决于现有技术栈和业务规模。
客户端如何处理通知
推送消息中不宜携带完整的汇款明细。客户端收到通知后,应重新请求受保护的 API:
function handlePushNotification(message) {
if (message.data?.type === "PAYMENT_REMITTANCES_UPDATED") {
refreshPaymentRemittances();
}
}
async function refreshPaymentRemittances() {
const response = await api.get("/payment-remittances");
updateRemittanceScreen(response.data);
}
这样既能让客户端展示服务器当前的权威数据,也能防止付款金额、账户信息或交易详情泄露在推送载荷中。
如果更新来自第三方支付平台
如果 payment remittances 由第三方支付平台生成,通常有两种触发方式。
Webhook
第三方平台可以在汇款状态变化时调用后端提供的 webhook。后端验证签名并更新数据库,然后在事务提交后生成通知事件。
app.post("/webhooks/payment-provider", async (req, res) => {
const event = verifyWebhookSignature(req);
if (event.type === "remittance.updated") {
await updatePaymentRemittance(event.data);
}
res.sendStatus(200);
});
remittance.updated 只是示例名称。实际事件名称、签名验证方式和重试规则应以支付平台的官方 API 文档为准。
定时同步
如果第三方平台不提供 webhook,可以通过定时任务轮询 API:
async function syncRemittances() {
const updates = await paymentProvider.fetchLatestRemittances();
for (const update of updates) {
const changed = await hasRemittanceChanged(update);
if (changed) {
await updatePaymentRemittance(update);
}
}
}
只有发现新增记录或有效的状态变化时,才生成通知事件,避免每次同步都重复向用户推送。
API 是否需要返回特殊事件
普通 REST API 不会自动产生推送通知,需要在业务代码中显式发布事件:
POST /internal/remittances/sync
→ 更新数据库
→ 发布 PaymentRemittancesUpdated
→ 返回同步结果
如果客户端只需在页面打开期间实时显示变化,也可以使用 WebSocket 或 Server-Sent Events。但它们无法完全替代系统推送。App 关闭或进入后台后,连接通常不能长期保持,此时仍需使用 FCM 或 APNs。
注意事项
保证幂等性
支付平台可能重复发送 webhook,消息队列也可能重复投递事件。可以使用第三方事件 ID、remittance ID 或业务版本号去重:
if (await eventRepository.exists(providerEventId)) {
return;
}
通知消费者也要能安全地多次处理同一个事件。
设计重试机制
推送失败后应进行有限次数的重试,并采用指数退避。对于无效或已过期的 device token,应及时停用,避免不断发送无效请求。
不要阻塞核心写入流程
推送服务响应缓慢时,不应影响 payment remittances 的写入速度。通知是数据写入后的后续动作,适合交给异步队列。即使推送失败,已经成功入库的数据也不应回滚。
避免敏感信息进入通知
锁屏通知可能被其他人看到。推送内容应使用概括性描述,例如”您的汇款信息已更新”,不要直接写入账号、完整姓名、金额或交易凭证。
记录可观测信息
建议记录这些状态:
- 数据更新时间
- 业务事件 ID
- 用户 ID
- 通知任务状态
- 推送服务返回结果
- 重试次数
- 失败原因
日志中也不应记录完整的支付凭证、访问令牌或其他敏感数据。
最合适的触发点是数据库事务成功提交后产生的业务事件。小型应用可以在提交后直接调用推送服务。对可靠性要求较高的支付场景,应使用 Transactional Outbox、消息队列和独立的通知消费者。
备注:内容仅供参考。
