Flutter应用中Firebase Cloud Functions发送FCM通知失败求助
我需要实现一个Firebase Cloud Functions,功能是监听Firestore中特定集合的变化,匹配另一个集合中文档的字段,从筛选后的文档中获取FCM Token并发送通知。目前Token能正常获取,但通过Functions发送的通知Flutter应用无法接收;而通过Firebase控制台消息推送、单个Token推送、curl请求发送的通知均能正常接收。
相关代码
main.dart代码
import 'package:file_picker/file_picker.dart'; import 'package:firebase_auth/firebase_auth.dart'; import 'package:firebase_core/firebase_core.dart'; import 'package:flutter/material.dart'; import 'package:sreyas_one_hub/screens/home_screen.dart'; import 'package:sreyas_one_hub/screens/login_screen.dart'; import 'package:firebase_messaging/firebase_messaging.dart'; import 'package:sreyas_one_hub/services/notification_service.dart'; void main() async { WidgetsFlutterBinding.ensureInitialized(); const FirebaseOptions firebaseOptions = FirebaseOptions(); FilePicker.platform = FilePicker.platform; await Firebase.initializeApp(options: firebaseOptions); final NotificationService notificationService = NotificationService(); await notificationService.init(); final FirebaseMessaging firebaseMessaging = FirebaseMessaging.instance; await firebaseMessaging.requestPermission( alert: true, badge: true, sound: true, ); FirebaseMessaging.onMessage.listen((RemoteMessage message) { print("onMessage: $message"); RemoteNotification? notification = message.notification; if (notification != null) { notificationService.showNotification( title: notification.title!, body: notification.body!, ); } }); FirebaseMessaging.onBackgroundMessage(_firebaseMessagingBackgroundHandler); runApp(const MyApp()); } Future<void> _firebaseMessagingBackgroundHandler(RemoteMessage message) async { await Firebase.initializeApp(); RemoteNotification? notification = message.notification; print("Handling a background message: ${message.messageId}"); if (notification != null) { print("Notification title: ${notification.title ?? 'No title'}"); print("Notification body: ${notification.body ?? 'No body'}"); } else { print("Notification is null"); } final NotificationService notificationService = NotificationService(); await notificationService.init(); if (notification != null && notification.title != null && notification.body != null) { notificationService.showNotification( title: notification.title!, body: notification.body!, ); } } class MyApp extends StatelessWidget { const MyApp({super.key}); @override Widget build(BuildContext context) { return MaterialApp( debugShowCheckedModeBanner: false, title: 'Sreyas OneHub', theme: ThemeData( primarySwatch: Colors.blue, visualDensity: VisualDensity.adaptivePlatformDensity, ), home: FirebaseAuth.instance.currentUser == null ? const LoginScreen() : HomeScreen( uid: FirebaseAuth.instance.currentUser!.uid, ), ); } }
index.js代码
const functions = require("firebase-functions"); const admin = require("firebase-admin"); admin.initializeApp(); async function sendNotifications(tokens, title, body) { const payload = { notification: { title: title, body: body, }, }; try { console.log("Sending notification: ", payload, " to ", tokens); await admin.messaging().sendToDevice(tokens, payload); console.log("Notification sent successfully"); } catch (error) { console.error("Error sending notification:", error); } } exports.onMarksChange = functions.firestore .document("marks/{markId}") .onWrite(async (change) => { const newValue = change.after.exists ? change.after.data() : null; if (newValue) { const studentId = newValue.studentId; const studentDoc = await admin .firestore() .collection("students") .where("uid", "==", studentId) .get(); if (!studentDoc.empty) { const studentData = studentDoc.docs[0].data(); const tokens = studentData.fcmToken || []; if (tokens.length > 0) { await sendNotifications( tokens, "Marks Updated", "Your marks have been updated." ); } else { console.log("No fcmTokens found for this student"); } } else { console.log("Student document does not exist"); } } }); exports.onStudentChange = functions.firestore .document("students/{studentId}") .onWrite(async (change) => { const newValue = change.after.exists ? change.after.data() : null; if (newValue) { const tokens = newValue.fcmToken || []; if (tokens.length > 0) { await sendNotifications( tokens, "Student Data Updated", "Your student data has been updated." ); } else { console.log("No fcmTokens found for this student"); } } }); exports.onScheduleChange = functions.firestore .document("schedules/{scheduleId}") .onCreate(async (snap) => { const newSchedule = snap.data(); const department = newSchedule.department; const studentsSnapshot = await admin .firestore() .collection("students") .where("department", "==", department) .where("year_of_study", "==", newSchedule.year) .get(); const tokens = []; studentsSnapshot.forEach((doc) => { const studentData = doc.data(); if (studentData.fcmToken && Array.isArray(studentData.fcmToken)) { tokens.push(...studentData.fcmToken); } }); if (tokens.length > 0) { await sendNotifications( tokens, "New Schedule Added", "A new schedule has been added for your department." ); } else { console.log("No fcmTokens found for students in this department"); } }); exports.onExamChange = functions.firestore .document("exams/{examId}") .onCreate(async (snap) => { const newExam = snap.data(); const department = newExam.department; const studentsSnapshot = await admin .firestore() .collection("students") .where("department", "==", department) .where("year_of_study", "==", newExam.year) .get(); const tokens = []; studentsSnapshot.forEach((doc) => { const studentData = doc.data(); if (studentData.fcmToken && Array.isArray(studentData.fcmToken)) { tokens.push(...studentData.fcmToken); } }); if (tokens.length > 0) { await sendNotifications( tokens, "New Exam Added", "A new exam has been added for your department." ); } else { console.log("No fcmTokens found for students in this department"); } });
已尝试的方案
- Firebase控制台消息推送[有效]
- 使用单个FCM Token而非数组[控制台有效,Functions无效]
- bash curl请求[有效]
- 搜索相关问题但未找到匹配的解决方案
若初步建议无效,可提供GitHub仓库权限用于复现问题。
解决建议
完善消息Payload结构
控制台和curl推送的Payload通常包含平台特定配置或data字段,尝试给Functions的Payload补充这些内容,确保通知优先级和行为与控制台一致:const payload = { notification: { title: title, body: body, }, data: { click_action: 'FLUTTER_NOTIFICATION_CLICK', status: 'completed' }, android: { notification: { priority: 'high', sound: 'default' } }, apns: { payload: { aps: { sound: 'default', contentAvailable: true } } } };解析FCM发送响应排查错误
sendToDevice返回的响应包含每个Token的发送状态,添加日志解析响应,找出失败的Token和原因:const response = await admin.messaging().sendToDevice(tokens, payload); console.log("FCM发送响应:", response); const invalidTokens = []; response.results.forEach((result, index) => { const error = result.error; if (error) { console.error(`Token ${tokens[index]} 发送失败:`, error.code, error.message); // 标记无效Token,后续可从Firestore移除 if (['messaging/invalid-registration-token', 'messaging/registration-token-not-registered'].includes(error.code)) { invalidTokens.push(tokens[index]); } } });检查Token存储格式
确认Firestore中存储的fcmToken是字符串数组,而非嵌套数组或格式错误的字符串。在Functions中添加日志打印tokens的具体内容,确保没有空值或无效格式混入。验证Flutter端消息处理逻辑
确保onMessage和onBackgroundMessage覆盖了所有消息类型,比如当消息仅包含data字段时的处理。同时检查NotificationService的showNotification方法是否能正确触发本地通知。确认SDK版本兼容性
检查Cloud Functions使用的firebase-admin版本与Flutter端的Firebase SDK版本是否兼容,避免因版本差异导致的消息解析问题。
内容的提问来源于stack exchange,提问作者Esvin Joshua

