> ## Documentation Index
> Fetch the complete documentation index at: https://documentation.onesignal.com/llms.txt
> Use this file to discover all available pages before exploring further.

# 重复通知

> 排查由 SDK 冲突、服务器端重试、多应用环境和平台特定错误导致的重复推送通知问题。

重复通知是指同一设备多次收到相同内容的消息。本指南涵盖最常见的原因及解决方法。

如果同一用户在多台设备（手机、平板、桌面）上看到通知，这是基于您的定向配置（细分群组、外部 ID 等）的预期行为。如果您遇到重复的应用内消息，请参阅[应用内消息故障排除](./in-app-message-troubleshooting#duplicated-in-app-messages)指南。

<Info>
  Apple 确认了 iOS 17 中导致重复的错误。此问题已在 iOS 17.3 中修复。[了解更多](https://forums.developer.apple.com/forums/thread/747044)。
</Info>

## 从这里开始

选择最符合您情况的部分：

* 您的服务器重试失败的 API 调用，或后端管道可能重复发送 → [多次发送相同消息](#same-message-sent-multiple-times)
* 您的应用与 OneSignal 一起使用 Firebase 或其他推送 SDK → [第三方通知处理程序](#third-party-notification-handlers)
* 您的 Android 应用在代码中自定义通知（服务扩展或前台监听器）→ [Android 通知处理程序](#android-notification-handlers)
* 您的 iOS 应用实现了自己的 `UNUserNotificationCenterDelegate` → [iOS 前台处理程序](#ios-foreground-handler)
* 您只在开发构建或多次安装时看到重复 → [多个应用实例](#multiple-app-instances)

## 多次发送相同消息

重复通知的最常见原因是通过 OneSignal API 多次发送相同的通知载荷。常见原因：

* 您的服务器重试请求而不检查第一次是否成功。
* 后端管道中的逻辑重复发送了同一通知。
* 您正在迁移到 OneSignal 但仍从之前的提供商发送通知。避免同时从两个系统发送。

<Card title="幂等 API 请求" icon="rotate" href="/reference/idempotent-notification-requests">
  通过在重试时复用幂等键来防止重复消息。
</Card>

## 第三方通知处理程序

当应用中的其他代码处理 OneSignal 的推送载荷并在 OneSignal 之外额外显示通知时，就会出现重复。包括：

* 其他接收相同载荷的远程推送 SDK（例如 Firebase Cloud Messaging）。
* 自定义消息处理程序，例如 Android 上的 `FirebaseMessagingService` 或 iOS 上的 `UNUserNotificationCenterDelegate`，它们读取传入载荷并从中构建本地通知。

当 OneSignal SDK 是唯一的处理程序时，会自动过滤 OneSignal 载荷。第三方代码不知道要过滤它们，除非您告诉它。

### 识别 OneSignal 通知

每条 OneSignal 通知都包含一个带有 `i` 键的 `custom` 对象，其中包含 OneSignal 通知 UUID：

```json JSON theme={null}
{
  "custom": {
    "i": "the-notification-id"
  }
}
```

有关完整的载荷参考，请参阅[自定义 OneSignal 载荷结构](./osnotification-payload#custom-onesignal-payload-structure)。

### 从第三方处理程序中过滤 OneSignal 通知

在任何非 OneSignal 的处理程序中，检查 `custom.i` 键，并在其存在时提前返回。OneSignal 随后处理自己的载荷，而您的其他代码继续处理自己的载荷。

<Tabs>
  <Tab title="Android">
    在自定义 `FirebaseMessagingService` 或其他远程消息处理程序中：

    ```kotlin Kotlin theme={null}
    override fun onMessageReceived(remoteMessage: RemoteMessage) {
        val customJson = remoteMessage.data["custom"]
        if (customJson != null) {
            try {
                val custom = JSONObject(customJson)
                if (custom.has("i")) {
                    // OneSignal 通知——让 OneSignal SDK 处理它
                    return
                }
            } catch (e: JSONException) {
                // 不是 OneSignal 载荷，继续执行
            }
        }

        // 在这里处理您自己的通知
    }
    ```
  </Tab>

  <Tab title="iOS">
    在您的 `UNUserNotificationCenterDelegate` 或自定义通知处理程序中：

    ```swift Swift theme={null}
    func userNotificationCenter(_ center: UNUserNotificationCenter,
                                willPresent notification: UNNotification,
                                withCompletionHandler completionHandler: @escaping (UNNotificationPresentationOptions) -> Void) {
        let userInfo = notification.request.content.userInfo

        if let custom = userInfo["custom"] as? [String: Any], custom["i"] != nil {
            // OneSignal 通知——让 OneSignal SDK 处理它
            completionHandler([])
            return
        }

        // 在这里处理您自己的通知
    }
    ```
  </Tab>
</Tabs>

<Tip>
  如果您控制从其他提供商发送的载荷结构，请添加一个独特的标记键（例如 `my_app_notification: true`），并在两个方向上进行过滤——在您自己的代码中只处理包含您标记的通知，让 OneSignal 处理包含 `custom.i` 键的通知。
</Tip>

## Android 通知处理程序

OneSignal 提供了两个 Android 回调，允许您在通知显示之前拦截和自定义：

* [通知服务扩展](./service-extensions)中的 `onNotificationReceived`——在每次推送时运行，包括应用在后台时。
* [前台生命周期监听器](./mobile-sdk-reference#addforegroundlifecyclelistener-push)中的 `onWillDisplay`——仅在应用处于前台时运行。

当这些回调除了 OneSignal 的自动显示之外还显示通知时，就会出现重复。

### `preventDefault()` 和 `display()` 规则

OneSignal 会自动显示每条通知，除非您通过 `event.preventDefault()` 抑制它。两个常见错误会导致重复：

* 在未首先调用 `event.preventDefault()` 的情况下调用 `event.getNotification().display()`——会导致通知显示两次。
* 在 OneSignal 也显示原始通知时，使用 [`NotificationManagerCompat.notify()`](https://developer.android.com/reference/androidx/core/app/NotificationManagerCompat) 另外发布一条通知。

只能有一条显示路径是活动的——要么是 OneSignal 的 `display()`，要么是您自己的 `NotificationManagerCompat.notify()`，不能同时使用两者。

<CodeGroup>
  ```kotlin Kotlin theme={null}
  override fun onNotificationReceived(event: INotificationReceivedEvent) {
      event.preventDefault()

      val notification = event.notification
      notification.setExtender { builder ->
          builder.setColor(0xFF0000FF.toInt())
      }
      notification.display()
  }
  ```

  ```java Java theme={null}
  @Override
  public void onNotificationReceived(INotificationReceivedEvent event) {
      event.preventDefault();

      IDisplayableMutableNotification notification = event.getNotification();
      notification.setExtender(builder ->
          builder.setColor(0xFF0000FF)
      );
      notification.display();
  }
  ```
</CodeGroup>

同样的规则适用于前台生命周期监听器中的 `onWillDisplay`：

```kotlin Kotlin theme={null}
OneSignal.Notifications.addForegroundLifecycleListener(object : INotificationLifecycleListener {
    override fun onWillDisplay(event: INotificationWillDisplayEvent) {
        event.preventDefault()

        // 渲染您自己的 UI 或调用 event.notification.display() 显示 OneSignal 通知
    }
})
```

<Warning>
  如果您调用了 `preventDefault()` 但从未调用 `display()`，通知将被静默丢弃，用户不会看到它。确保每条代码路径要么恰好调用一次 `display()`，要么有意抑制通知。
</Warning>

<Warning>
  如果您同时使用通知服务扩展和前台生命周期监听器，请确保对于给定的应用状态只有一个处理程序显示通知。从两者都调用 `display()` 会产生重复。
</Warning>

### 回调中的异步工作

当您在显示之前执行后台工作（网络调用、数据库查找）时，请在回调中同步调用 `preventDefault()`，并仅在异步工作完成后调用 `display()`。在不调用 `preventDefault()` 的情况下从回调返回会让 OneSignal 显示通知，之后再调用 `display()` 则会产生重复。

## iOS 前台处理程序

OneSignal 在 SDK 初始化期间将自身设置为 `UNUserNotificationCenterDelegate`。如果您的应用也实现了前台通知处理，通知可能会显示两次——一次来自 OneSignal，一次来自您的代码。

导致重复的常见模式：

* 实现了自己的 `UNUserNotificationCenterDelegate` 而不将调用转发给 OneSignal，然后在同一方法中调用 `completionHandler([.banner, .sound])` 并显示自定义的应用内警报。
* 在 OneSignal 也在显示传入推送的同时调度本地通知（`UNUserNotificationCenter.current().add(...)`）。

要在不导致重复的情况下控制前台显示，请使用 OneSignal 的[前台生命周期监听器](./mobile-sdk-reference#addforegroundlifecyclelistener-push)，并在您想自己渲染通知时调用 `event.preventDefault()`：

```swift Swift theme={null}
OneSignal.Notifications.addForegroundLifecycleListener(self)

func onWillDisplay(event: OSNotificationWillDisplayEvent) {
    event.preventDefault()

    // 渲染您自己的 UI 或调用 event.notification.display() 显示 OneSignal 通知
}
```

## 多个应用实例

<Tabs>
  <Tab title="Android">
    当您同时安装了应用的生产版本和开发版本时，可能会出现重复。每个应用都有唯一的包名并接收自己的推送令牌。

    长按通知以确认是哪个应用实例发送的。
  </Tab>

  <Tab title="iOS">
    如果 OneSignal 中的两个设备记录共享相同的推送令牌，设备可能会收到两个通知。这可能在导入或多次注册相同令牌时发生。OneSignal 包含防止此情况的检查，但边缘情况可能绕过它们。
  </Tab>

  <Tab title="Web">
    Web 推送重复是由于使用相同的 OneSignal 应用 ID 订阅多个来源造成的。[来源](https://developer.mozilla.org/en-US/docs/Web/Security/Same-origin_policy)是浏览器用于限定推送订阅范围的协议、主机和端口的组合。

    例如，订阅了 `https://example.com` 和 `https://sub.example.com` 的用户算作两个订阅，使用相同应用 ID 发送时两者都会收到。

    修复方法：

    1. 按来源[重置浏览器数据和推送权限](./troubleshooting-web-push)。
    2. 如果许多用户受到影响，创建一个新的 OneSignal 应用，并在您的网站初始化代码中使用新的应用 ID。
    3. 让用户重新访问网站并重新订阅。

    在多个浏览器或浏览器配置文件中订阅也会导致多个通知。
  </Tab>
</Tabs>

## 诊断提示

为了更快地调试重复问题，请收集以下信息并发送给 OneSignal 支持：

* 收到重复通知的设备的 OneSignal 订阅 ID
* OneSignal 消息 ID 或仪表板中消息的链接
* 应用中其他库或插件的列表
* [重现问题的调试日志](./capturing-a-debug-log)
* 详细的复现步骤

## 常见问题

### 如果我的应用中有两个通知 SDK 会怎样？

两个 SDK 可能会独立处理并显示同一通知。OneSignal 会自动过滤自己的通知，但其他 SDK 不会。通过检查 `custom.i` 键，从其他 SDK 的处理程序中过滤 OneSignal 载荷。有关代码示例，请参阅[第三方通知处理程序](#third-party-notification-handlers)。

### 如何同时从之前的提供商和 OneSignal 发送推送？

您可以逐步过渡，但要避免从两个提供商发送相同的消息。

* **Android**：在集成 OneSignal 并发布应用时删除旧 SDK 通知处理代码。随着用户更新，他们将停止接收来自旧提供商的推送。
* **iOS**：在用户更新期间，您可以暂时继续从旧提供商发送。完全过渡后，仅从 OneSignal 发送以避免重复。

### 如何防止快速变化更新的多条通知？

在[创建通知](/reference/push-notification) API 中使用 [`collapse_id`](/reference/push-notification#body-parameters) 来替换之前的通知而不是堆叠它们。当多条通知共享相同的 `collapse_id` 时，每条新通知都会替换通知栏中的上一条。这对于股票价格更新、实时比分、配送预计到达时间和类似的频繁更新非常有用。

## 相关页面

<Columns cols={2}>
  <Card title="移动 SDK 故障排除" icon="mobile" href="./mobile-troubleshooting">
    解决 iOS 和 Android 上推送通知投递的常见问题。
  </Card>

  <Card title="Web 推送故障排除" icon="browser" href="./troubleshooting-web-push">
    调试 Web 推送订阅、Service Worker 和投递问题。
  </Card>

  <Card title="应用内消息故障排除" icon="message" href="./in-app-message-troubleshooting">
    修复应用内消息显示、触发器和重复问题。
  </Card>

  <Card title="移动服务扩展" icon="gear" href="./service-extensions">
    在 iOS 和 Android 上显示之前拦截和自定义推送通知。
  </Card>

  <Card title="未显示的通知（Web）" icon="bell-slash" href="./notifications-not-shown-web-push">
    对已发送但未显示的 Web 推送通知进行故障排除。
  </Card>
</Columns>
