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

重启NodeJS服务器后FCM Admin推送失败及注册令牌失效问题求助

问题排查与解决方案

我碰到过类似的推送问题,你的两个症状其实大概率是同一个核心问题导致的——服务端Firebase Admin SDK的初始化逻辑不规范,再加上客户端令牌管理的小漏洞。下面给你一步步拆解和解决:

一、先解决服务端的核心问题:确保Admin SDK单例初始化

Firebase Admin SDK在Node.js环境中必须只初始化一次,如果每次服务重启或者处理请求时重复调用initializeApp(),会导致SDK内部状态混乱,推送通道失效,甚至让Firebase标记旧令牌为无效。

错误示例(你可能踩了这个坑)

如果你的初始化代码写在路由处理函数或者每次请求都会执行的地方,就会重复初始化:

// ❌ 错误:每次请求都重新初始化SDK
app.post('/send-notification', async (req, res) => {
  const admin = require('firebase-admin');
  const serviceAccount = require('./serviceAccountKey.json');
  
  admin.initializeApp({
    credential: admin.credential.cert(serviceAccount),
    databaseURL: "https://hungphongbk-1812.firebaseio.com"
  });
  
  // 发送推送...
});

正确写法:全局单例初始化

把SDK初始化放在项目入口文件(比如app.js或bin/www的最顶部),只执行一次:

// ✅ 正确:全局初始化一次,所有路由复用这个实例
const admin = require('firebase-admin');
const serviceAccount = require('./path/to/serviceAccountKey.json');

// 初始化前先检查是否已存在实例,避免重复初始化报错
if (!admin.apps.length) {
  admin.initializeApp({
    credential: admin.credential.cert(serviceAccount),
    databaseURL: "https://hungphongbk-1812.firebaseio.com"
  });
}

// 后续路由直接使用已初始化的admin实例
app.post('/send-notification', async (req, res) => {
  // 发送推送逻辑...
});

二、修复客户端令牌重复生成的问题

你的客户端代码每次加载都会直接注册Service Worker并重新获取令牌,没有复用已有注册和令牌,这会导致每次页面刷新或重启服务后生成新令牌,旧令牌被Firebase标记为失效。

修改后的客户端代码

const self = this;
if ('serviceWorker' in navigator) {
  if (!firebase.apps.length) firebase.initializeApp({
    apiKey: "AIzaSyDu888R1nyP_W_xFX7aATSB0U5irf79lrM",
    authDomain: "hungphongbk-1812.firebaseapp.com",
    databaseURL: "https://hungphongbk-1812.firebaseio.com",
    projectId: "hungphongbk-1812",
    storageBucket: "hungphongbk-1812.appspot.com",
    messagingSenderId: "1926697148"
  });
  const messaging = firebase.messaging();
  
  messaging.onMessage(function (payload) {
    console.log("Message received. ", payload);
    const {notification} = payload;
    new Notification(notification.title, { body: notification.body });
  });

  (async function () {
    try {
      // 第一步:复用已有的Service Worker注册,避免重复注册
      let registration;
      const existingRegistrations = await navigator.serviceWorker.getRegistrations();
      const existingSw = existingRegistrations.find(reg => reg.scope === '/'); // 匹配你的SW scope
      
      if (existingSw) {
        registration = existingSw;
        console.log('复用已有的Service Worker注册');
      } else {
        registration = await navigator.serviceWorker.register('/sw.js');
        console.log('注册新的Service Worker');
      }
      
      messaging.useServiceWorker(registration);

      // 第二步:检查权限状态,避免重复请求
      const permission = await Notification.requestPermission();
      if (permission !== 'granted') {
        throw new Error('用户未授权通知');
      }

      // 第三步:获取当前令牌,对比本地存储,仅变化时发送到服务端
      const currentToken = await messaging.getToken();
      const savedToken = localStorage.getItem('fcm_token');
      
      if (currentToken) {
        self.token = currentToken;
        if (currentToken !== savedToken) {
          console.log('令牌更新,发送到服务端:', currentToken);
          await post('/noti/register', {token: currentToken});
          localStorage.setItem('fcm_token', currentToken);
        } else {
          console.log('令牌未变化,无需重复发送');
        }
      } else {
        // 极端情况:权限已授权但无令牌,重新获取
        const newToken = await messaging.getToken();
        self.token = newToken;
        await post('/noti/register', {token: newToken});
        localStorage.setItem('fcm_token', newToken);
      }
    } catch (e) {
      alert(e.message);
    }
  })()
}

三、服务端添加推送错误处理,自动清理失效令牌

重启服务后发送推送无响应,可能是因为旧令牌已经被Firebase标记为无效,但服务端没有捕获错误继续发送。添加错误处理可以帮你定位问题,同时清理无效令牌:

// 服务端发送推送的工具函数
async function sendPushNotification(token, payload) {
  try {
    const response = await admin.messaging().sendToDevice(token, payload);
    
    // 检查失效令牌并清理
    const invalidTokens = [];
    response.results.forEach((result, index) => {
      const error = result.error;
      if (error) {
        console.error(`推送失败到令牌 ${token}:`, error.code);
        // 标记无效令牌的错误码
        if (['messaging/invalid-registration-token', 'messaging/registration-token-not-registered'].includes(error.code)) {
          invalidTokens.push(token);
        }
      }
    });

    // 从数据库删除无效令牌
    if (invalidTokens.length > 0) {
      // 这里替换成你的数据库删除逻辑
      await db.collection('notification_tokens').where('token', 'in', invalidTokens).delete();
      console.log(`已清理 ${invalidTokens.length} 个无效令牌`);
    }

    return response;
  } catch (err) {
    console.error('推送发送失败:', err);
    throw err;
  }
}

四、验证步骤

  1. 重启服务端,确保Admin SDK只初始化一次(查看日志有没有重复初始化的报错)。
  2. 打开Chrome开发者工具,查看Application -> Service Workers,确认SW是激活状态且没有重复注册。
  3. 调用服务端的测试推送接口,查看服务端日志的推送响应,确认没有错误。
  4. 重新加载客户端页面,查看控制台是否复用了旧令牌,没有生成新令牌。

按照这几步调整后,应该能解决重启服务端后的推送失效和令牌重复生成问题。

内容的提问来源于stack exchange,提问作者Hùng Phong

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.05.15 04:00:55