You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Firebase云函数重复推送通知问题排查与解决咨询

解决Firebase重复推送通知的问题

嘿,我帮你梳理下当前遇到的重复通知问题,结合你贴的代码,咱们一步步来解决:

一、重复通知的核心原因

先看你贴的Cloud Function代码,这里有个关键错误:你在sendPushNotificationAdded里用了admin.database().ref('/tokens').on('value', ...)——这是持续监听数据库的变化,而不是一次性读取数据!在Cloud Function这种无服务器环境里,用on('value')会注册一个永久监听器,导致函数重复执行,反复发送相同的通知。

另外你提到登录/退出修改logged_in字段时触发问题,虽然你没贴对应的监听函数,但大概率是同样的问题:要么触发器设置太宽泛(比如用onWrite而不是onUpdate),要么读取数据时用了持续监听,或者每次登录时多次更新用户节点(比如你的Android代码里多次调用setValue),导致触发器多次触发。

至于Firebase“存储/保留通知”:FCM本身不会长期存储通知,但如果你的函数重复触发发送,或者设备离线时FCM会暂时保存通知直到设备上线,就会出现重复收到的情况;另外如果函数逻辑有问题,导致多次写入触发条件,也会重复发送。

二、分步解决

1. 修复Cloud Function的监听逻辑

把on('value')改成once('value'),一次性读取tokens数据,避免函数持续运行重复发送:

const functions = require('firebase-functions'); 
const admin = require('firebase-admin'); 
admin.initializeApp(functions.config().firebase); 

exports.sendPushNotificationAdded = functions.database.ref('/emergencies/{id}').onCreate(async (event) => { 
  // 改用once('value')一次性读取tokens数据
  const snapshot = await admin.database().ref('/tokens').once('value'); 
  const efarArray = snapshotToArray(snapshot, event.data.child('latitude').val(), event.data.child('longitude').val()); 
  efarArray.sort((a, b) => a.distance - b.distance); 

  const payload = { 
    notification: { 
      title: "New stuff :)", 
      body: "Info given: " + event.data.child('other_info').val(), 
      sound: 'default', 
    } 
  }; 

  let tokens_to_send_to = []; 
  // 取最近的5个,简化循环逻辑
  const sendCount = Math.min(efarArray.length, 5);
  for (let i = 0; i < sendCount; i++) { 
    tokens_to_send_to.push(efarArray[i].token); 
  } 

  if (tokens_to_send_to.length === 0) {
    return null; // 没有目标token,直接结束
  }

  const response = await admin.messaging().sendToDevice(tokens_to_send_to, payload); 
  // 可选:清理无效token,避免下次发送失败
  const cleanupPromises = [];
  response.results.forEach((result, index) => {
    const error = result.error;
    if (error) {
      console.error('发送失败到token:', tokens_to_send_to[index], error);
      // 如果是无效token,从数据库移除
      if (error.code === 'messaging/invalid-registration-token' ||
          error.code === 'messaging/registration-token-not-registered') {
        cleanupPromises.push(admin.database().ref('/tokens').child(tokens_to_send_to[index]).remove());
      }
    }
  });
  return Promise.all(cleanupPromises);
}); 

function snapshotToArray(snapshot, incoming_latitude, incoming_longitude) { 
  var returnArr = []; 
  snapshot.forEach(function(childSnapshot) { 
    var distance_to_efar = distance(childSnapshot.child('latitude').val(), childSnapshot.child('longitude').val(), incoming_latitude, incoming_longitude); 
    var item = { 
      latitude: childSnapshot.child('latitude').val(), 
      longitude: childSnapshot.child('longitude').val(), 
      token: childSnapshot.key, 
      distance: distance_to_efar 
    }; 
    returnArr.push(item); 
  }); 
  return returnArr; 
}; 

2. 处理登录/退出的触发器(关键!)

假设你有监听/users/{id}/logged_in的Cloud Function,需要做这几点:

  • 只在logged_in字段实际变化时触发,而不是用户节点任何更新都触发(比如登录时更新位置、token)
  • 同样用once('value')读取数据,避免持续监听
  • 用onUpdate替代onWrite,减少不必要的触发

示例代码:

exports.sendLoginStatusAlert = functions.database.ref('/users/{userId}').onUpdate(async (change) => {
  // 检查logged_in字段是否真的变化了
  const wasLoggedIn = change.before.child('logged_in').val();
  const isLoggedIn = change.after.child('logged_in').val();
  if (wasLoggedIn === isLoggedIn) {
    return null; // 字段没变化,直接返回
  }

  // 一次性读取需要通知的目标token(比如管理员或其他用户)
  const tokensSnapshot = await admin.database().ref('/target_tokens').once('value');
  const targetTokens = [];
  tokensSnapshot.forEach(child => {
    targetTokens.push(child.key);
  });

  if (targetTokens.length === 0) return null;

  const payload = {
    notification: {
      title: "用户状态更新",
      body: `${change.after.child('name').val()} 现在${isLoggedIn ? '在线' : '离线'}了`,
      sound: 'default'
    },
    // 设置collapse_key,让相同类型的通知合并,避免重复显示
    data: {
      collapse_key: "login_status"
    }
  };

  return admin.messaging().sendToDevice(targetTokens, payload);
});

3. 让通知只发一次且不重复显示

  • 设置collapse_key:在payload里添加collapse_key,FCM会合并相同key的通知,不会重复弹出
  • Android端控制通知显示:用固定的notificationId显示通知,新通知会替换旧的:
    NotificationManager notificationManager = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);
    // 用固定ID,新通知覆盖旧的
    notificationManager.notify(1001, yourNotificationBuilder.build());
    

4. Android登录代码优化

你的登录代码里多次调用setValue更新用户节点,这会多次触发数据库触发器!改成批量更新,只触发一次:

// 批量更新用户信息,替换多次setValue
Map<String, Object> userUpdates = new HashMap<>();
userUpdates.put("name", name);
userUpdates.put("token", refreshedToken);
userUpdates.put("latitude", my_lat);
userUpdates.put("longitude", my_long);
userUpdates.put("logged_in", true);

userRef.child(id).updateChildren(userUpdates);

三、为什么Firebase会“保留”通知?

  • 你的Cloud Function重复触发:比如用了on('value')持续监听,导致函数反复执行发送通知
  • FCM临时缓存:当设备离线时,FCM会暂时保存通知直到设备上线,这是正常的离线消息机制
  • 触发器触发过于频繁:比如多次更新用户节点,导致每次更新都触发通知发送逻辑

内容的提问来源于stack exchange,提问作者MooseMan55

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.15 06:58:29