FCM多设备令牌管理方法及新手遇到的异常问题问询
嘿,刚搞定FCM又碰到多设备令牌管理的问题,确实挺挠头的!结合我在Stack Overflow上处理过的大量类似案例,给你整理一套实用的方案,不管你后续的用户A/B场景是多设备登录、账号共享还是其他情况,这套方案都能hold住:
处理FCM多设备令牌管理的核心方案
一、先把存储逻辑捋顺——别搞一对一绑定
很多新手容易踩的坑就是把用户和令牌做一对一关联,这根本没法适配多设备场景。你得在MongoDB里单独建一个类似user_fcm_tokens的集合,每条记录对应一个设备的令牌,字段至少要包含:
userId:关联用户的唯一IDtoken:FCM生成的设备令牌deviceInfo:设备型号、系统版本、设备类型(手机/平板/Web),方便后续排查和定向推送createdAt:令牌创建时间lastActiveAt:最后一次上报活跃的时间isValid:标记令牌是否有效(默认true)
而且每次客户端上报令牌时,先查库中有没有相同userId+token的记录,有就更新lastActiveAt和isValid,没有再插入,避免冗余存储。
二、常见场景的具体处理方案
1. 用户在多设备登录(比如用户A同时用手机和平板登同一个账号)
- 客户端每次启动时,都要检查本地存储的令牌:如果没有就生成新令牌上报后端;如果有,也定期(比如每周)上报一次,更新
lastActiveAt,让后端知道这个设备还在活跃。 - 推送通知时,根据
userId查询该用户下所有isValid=true的令牌,调用FCM的批量推送接口,把通知推送到所有活跃设备。
2. 令牌失效/过期的情况
FCM推送失败时会返回明确的错误码,比如InvalidRegistration或NotRegistered,这说明令牌已经失效了,你要:
- 捕获这些错误,把对应的令牌标记为
isValid=false(建议标记而非直接删除,方便后续排查问题) - 加个定时任务,定期清理超过30天未活跃且失效的令牌,减少无效推送的请求量
3. 用户注销/退出登录
- 客户端退出登录时,主动调用后端接口,把当前设备的令牌标记为
isValid=false - 如果客户端没主动通知(比如卸载APP),后端可以通过
lastActiveAt判断,超过15天未活跃的令牌,标记为待清理状态
三、代码示例(MongoDB + Node.js)
存储/更新令牌的接口逻辑
async function saveOrRefreshToken(userId, token, deviceInfo) { // 先查有没有已存在的相同令牌 const existingToken = await db.collection('user_fcm_tokens').findOne({ userId, token }); if (existingToken) { // 更新活跃时间和有效性 return db.collection('user_fcm_tokens').updateOne( { _id: existingToken._id }, { $set: { lastActiveAt: new Date(), isValid: true } } ); } else { // 插入新的令牌记录 return db.collection('user_fcm_tokens').insertOne({ userId, token, deviceInfo, createdAt: new Date(), lastActiveAt: new Date(), isValid: true }); } }
给用户批量推送通知的逻辑
async function sendNotificationToUser(userId, notificationContent) { // 获取该用户下所有有效令牌 const validTokens = await db.collection('user_fcm_tokens') .find({ userId, isValid: true }) .project({ token: 1 }) .toArray(); if (validTokens.length === 0) return; // 调用FCM批量推送API const pushResponse = await fcm.messaging().sendEachForMulticast({ tokens: validTokens.map(item => item.token), notification: notificationContent }); // 处理推送失败的失效令牌 const invalidTokenList = pushResponse.responses .filter(res => !res.success && (res.error.code === 'InvalidRegistration' || res.error.code === 'NotRegistered')) .map((res, index) => validTokens[index].token); if (invalidTokenList.length > 0) { await db.collection('user_fcm_tokens').updateMany( { userId, token: { $in: invalidTokenList } }, { $set: { isValid: false } } ); } }
四、额外小建议
- 客户端一定要监听令牌刷新:FCM在令牌更新时会触发回调(比如Android的
onNewToken、iOS的didReceiveRegistrationToken),一旦拿到新令牌,立刻上报后端替换旧的,避免推送失败。 - 利用
deviceInfo做定向推送:比如用户只想在手机上收通知,就可以筛选出手机类型的令牌推送,提升用户体验。
内容的提问来源于stack exchange,提问作者Srinivas Nahak
相关产品推荐
相关产品推荐

