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

Flutter应用中Firebase Cloud Functions发送FCM通知失败求助

问题:Firebase Cloud Functions发送的FCM通知无法被Flutter应用接收

我需要实现一个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仓库权限用于复现问题。

解决建议

  1. 完善消息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
          }
        }
      }
    };
    
  2. 解析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]);
        }
      }
    });
    
  3. 检查Token存储格式
    确认Firestore中存储的fcmToken是字符串数组,而非嵌套数组或格式错误的字符串。在Functions中添加日志打印tokens的具体内容,确保没有空值或无效格式混入。

  4. 验证Flutter端消息处理逻辑
    确保onMessage和onBackgroundMessage覆盖了所有消息类型,比如当消息仅包含data字段时的处理。同时检查NotificationService的showNotification方法是否能正确触发本地通知。

  5. 确认SDK版本兼容性
    检查Cloud Functions使用的firebase-admin版本与Flutter端的Firebase SDK版本是否兼容,避免因版本差异导致的消息解析问题。


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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.17 14:03:10