PAYATHON 2026

如何在 API 数据更新后向用户推送通知?

支付老李

结论

通知应在后端成功提交 payment remittances 数据后触发,不应依赖某个通用 API 事件。比较稳妥的处理方式是:数据写入成功后生成 PaymentRemittancesUpdated 等业务事件,再由异步任务通过 FCM、APNs 或应用内消息服务通知用户。

触发时机必须在提交成功之后。如果数据库事务还未完成就发送通知,用户收到通知时可能仍看不到最新数据。写入最终失败时,还可能产生错误通知。

推荐流程

完整的数据更新和通知流程可以这样设计:

  1. 后端获取最新的 payment remittances。
  2. 在数据库事务中写入或更新相关记录。
  3. 确认事务提交成功。
  4. 生成 PaymentRemittancesUpdated 业务事件。
  5. 将事件写入消息队列或任务队列。
  6. 通知服务消费事件,并调用 FCM、APNs 等推送服务。
  7. 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、消息队列和独立的通知消费者。

备注:内容仅供参考。