跳转至

05 · 消息载荷与平台定制

目标读者:要构造跨 Android/iOS/Web 多端推送的开发者 核心问题:同一个 message 怎么在 Android/iOS/Web 上都有最佳体验?哪些字段是必填的? 前置: 01-FCM核心概念 / 02-HTTP-v1-API与认证 相关: 04-批量发送与广播模式 最后更新:2026-04-11


一、消息三层结构

FCM HTTP v1 的 message 对象有三层:

┌────────────────────────────────────────────────┐
│  Layer 1:公共字段(所有平台都解释)             │
│    · notification { title, body, image }       │
│    · data { k: v }(value 必须是 string)       │
├────────────────────────────────────────────────┤
│  Layer 2:平台覆盖块(对应平台读取,会覆盖公共)  │
│    · android { priority, ttl, notification } │
│    · apns { headers, payload }                  │
│    · webpush { headers, notification }          │
├────────────────────────────────────────────────┤
│  Layer 3:元字段                                │
│    · fcm_options.analytics_label                │
│    · token / topic / condition (三选一)         │
└────────────────────────────────────────────────┘

覆盖规则:如果 Layer 1 和 Layer 2 同名字段都存在,Layer 2 的值会覆盖 Layer 1。


二、Notification 消息 vs Data 消息

2.1 核心对比

维度 notification 消息 data 消息
字段 设置 notification 只设置 data
后台行为 FCM SDK 自动显示在系统通知栏 Android:唤醒 service 处理
iOS:需 content-available:1 唤醒 app 短时执行
前台行为 触发 onMessageReceived,不会自动显示,要 app 自己构造通知 触发 onMessageReceived
灵活度 低(系统接管显示) 高(app 完全控制)

2.2 iGaming 推荐做法:混合消息 ⭐

同时设 notification + data: - notification —— 后台时系统自动弹通知 - data —— 携带 promo_codedeeplinkcampaign_id 等结构化字段 - 前台时:app 通过 onMessageReceived 拿到 data,自己构造通知

const message = {
  topic: 'all_users',
  notification: {
    title: '11 点限时福利',
    body: '输入 LUCKY888 领 588 红包',
  },
  data: {
    promo_code: 'LUCKY888',
    expires_at: '1735710000',
    deeplink: 'igaming://promo/LUCKY888',
    campaign_id: 'daily_11am_20260411',
  },
};

2.3 ⚠️ data 字段的严格要求

  • 所有 value 必须是 string
  • "count": 10 ❌ → "count": "10"
  • "is_vip": true ❌ → "is_vip": "true"
  • 不能使用保留 key:
  • from
  • message_type
  • 任何 google. / gcm. / gcm.notification. 开头的 key
  • 总大小:data + notification ≤ 4096 bytes(topic 消息 ≤ 2048 bytes)

三、消息大小与 TTL

单条消息 payload 4,096 bytes(Notification + Data 合计)
Topic 消息 2,048 bytes
TTL 范围 0 ~ 2,419,200 秒(4 周)
TTL 默认值 4 周

iGaming 推荐 TTL:3,600 ~ 14,400 秒(1-4 小时)。超过优惠码截止时间后再送出去反而骚扰用户。


四、Android 平台字段详解

"android": {
  "collapse_key": "promo",      // 同 key 的后到消息会替换前面未送达的
  "priority": "high",            // "high" 才会立即送达;"normal" 会合并/延迟
  "ttl": "14400s",               // 0~2419200 秒(4 周)
  "restricted_package_name": "com.igaming.platforma",
  "data": { "k": "v" },
  "notification": {
    "channel_id": "promo",       // ⚠️ Android 8+ 必填,否则不响
    "sound": "default",
    "click_action": "OPEN_PROMO",
    "icon": "ic_promo",
    "color": "#FFD700",
    "image": "https://cdn.igaming.com/promo/lucky888.png",
    "tag": "promo_2026_04_11",   // 同 tag 会折叠
    "title": "11 点限时福利",
    "body": "输入 LUCKY888 领 588 红包"
  },
  "fcm_options": { "analytics_label": "daily_11am" }
}

4.1 关键字段

字段 必填 说明
priority 推荐 "high" 运营推送必须设 high,否则会被合并/延迟
notification.channel_id Android 8+ 必需 否则通知静默、不响也没声音
notification.image 大图推送,限制 1 MB
ttl 推荐设 过期自动丢弃,避免离线用户收到过期优惠
collapse_key 推荐 同 key 会替换未送达的,避免用户收到一堆同类通知

4.2 ⚠️ Android 8+ 必须设 channel_id

这是 iGaming 最常见的坑。客户端需要先创建 notification channel:

// Android 客户端代码(告知客户端工程师)
val channel = NotificationChannel(
    "promo",                                           // 必须和后端 channel_id 对应
    "促销通知",
    NotificationManager.IMPORTANCE_HIGH,
).apply {
    description = "优惠码和限时福利通知"
    enableLights(true)
    enableVibration(true)
}
NotificationManagerCompat.from(context)
    .createNotificationChannel(channel)

如果客户端没创建 promo channel,发送再多的 channel_id: "promo" 消息都不会响。


五、iOS (APNs) 平台字段详解

"apns": {
  "headers": {
    "apns-priority": "10",       // 10=立即送达;5=节能模式(静默推送必须用 5)
    "apns-push-type": "alert",   // alert / background / voip / ...
    "apns-collapse-id": "promo_daily",
    "apns-expiration": "0"       // 0=不过期就丢;其它是 Unix 秒绝对过期时间
  },
  "payload": {
    "aps": {
      "alert": { "title": "...", "body": "..." },
      "sound": "default",
      "badge": 1,
      "category": "PROMO",
      "thread-id": "promo",
      "content-available": 1,    // 1 = 静默推送(不能有 alert)
      "mutable-content": 1       // 1 = 允许 NSE 修改(富通知/图片必填)
    },
    "promo_code": "LUCKY888"     // 自定义键并列于 aps
  },
  "fcm_options": {
    "image": "https://cdn.igaming.com/promo/lucky888.png",
    "analytics_label": "daily_11am"
  }
}

5.1 关键字段

字段 说明
headers.apns-priority "10" = 立即送达;"5" = 节能模式(静默推送必须用 5)。运营推送一律 "10"
payload.aps.content-available 1 = 静默推送,不能同时有 alert,否则 Apple 限频严重
payload.aps.mutable-content 1 = 允许 Notification Service Extension 修改(富通知带图必填)
payload.aps.category 用于定义通知交互按钮,客户端要预注册 category

5.2 ⚠️ iOS 富通知必须配套两件事

想在 iOS 推送里显示大图:

  1. 后端:apns.payload.aps.mutable-content = 1 + apns.fcm_options.image = "https://..."
  2. 客户端:app 端要实现 Notification Service Extension(告知客户端工程师)

只设置其中一个都没用


六、Web Push 平台字段详解

"webpush": {
  "headers": { "TTL": "14400", "Urgency": "high" },
  "data": { "k": "v" },
  "notification": {
    "title": "11 点限时福利",
    "body": "输入 LUCKY888 领 588 红包",
    "icon": "https://cdn.igaming.com/icon.png",
    "image": "https://cdn.igaming.com/promo/banner.png",
    "badge": "https://cdn.igaming.com/badge.png",
    "actions": [
      { "action": "open", "title": "立即领取" },
      { "action": "dismiss", "title": "稍后" }
    ]
  },
  "fcm_options": {
    "link": "https://igaming.com/promo/LUCKY888",
    "analytics_label": "daily_11am"
  }
}

6.1 关键字段

字段 说明
headers.Urgency very-low / low / normal / high,high 用于优惠码推送
notification.actions 浏览器通知的交互按钮
fcm_options.link 点击通知后打开的 URL(必填,否则点击无反应)

七、完整 iGaming 优惠码消息示例

跨平台完美推送(Android / iOS / Web 都有最佳体验):

// iGaming 日常推送的标准 message 对象
const message = {
  topic: 'all_users',
  notification: {
    title: '11 点限时福利',
    body: '输入 LUCKY888 领 588 红包,限前 1000 名',
  },
  data: {
    promo_code: 'LUCKY888',
    expires_at: '1735710000',
    deeplink: 'igaming://promo/LUCKY888',
    campaign_id: 'daily_11am_20260411',
  },
  android: {
    priority: 'high',
    ttl: '14400s',                   // 4 小时
    notification: {
      channelId: 'promo_high',        // 必填
      sound: 'default',
      color: '#FFD700',
      imageUrl: 'https://cdn.igaming.com/promo/lucky888.png',
      clickAction: 'OPEN_PROMO',
    },
  },
  apns: {
    headers: {
      'apns-priority': '10',
      'apns-push-type': 'alert',
      'apns-collapse-id': 'promo_daily',
    },
    payload: {
      aps: {
        alert: { title: '11 点限时福利', body: '输入 LUCKY888 领 588 红包' },
        sound: 'default',
        badge: 1,
        'mutable-content': 1,         // 允许富通知
        category: 'PROMO',
      },
    },
    fcmOptions: {
      image: 'https://cdn.igaming.com/promo/lucky888.png',
      analyticsLabel: 'daily_11am',
    },
  },
  webpush: {
    headers: { TTL: '14400', Urgency: 'high' },
    notification: {
      icon: 'https://cdn.igaming.com/icon.png',
      image: 'https://cdn.igaming.com/promo/lucky888.png',
    },
    fcmOptions: { link: 'https://igaming.com/promo/LUCKY888' },
  },
  fcmOptions: { analyticsLabel: 'daily_11am' },
};

await getMessaging().send(message);

这就是所有 5 次定时推送可以直接套用的标准模板,只需要换里面的 promo_code / title / body / image


八、陷阱清单

  1. Android 8+ 必须设 channel_id 否则通知静默 —— 是 iGaming 最常见的坑

  2. iOS 富通知必须 mutable-content: 1 + Notification Service Extension 两者缺一不可,后端设了图但客户端没实现 NSE 等于白搭。

  3. data 字段的所有 value 必须是 string 数字 / 布尔 / 对象会被 FCM 直接拒绝 INVALID_ARGUMENT

  4. data 不能用保留字 from / message_type / google.* / gcm.* 都是保留的,用了会报错。

  5. notification 在 iOS 前台不会自动显示 需要 app 端 UNUserNotificationCenterDelegate 配合。

  6. Web Push 的 fcm_options.link 不设就没点击效果 一定要填,否则用户点了通知没反应。

  7. 消息总大小 ≤ 4096 bytes(Topic 消息 ≤ 2048) 超了直接 messaging/payload-size-limit-exceeded


九、权威来源

  • https://firebase.google.com/docs/cloud-messaging/customize-messages/cross-platform
  • https://firebase.google.com/docs/cloud-messaging/customize-messages/set-message-type
  • https://firebase.google.com/docs/reference/fcm/rest/v1/projects.messages
  • https://firebase.google.com/docs/cloud-messaging/concept-options