Files
eryao/docs/plans/notification-system-plan.md
T

31 KiB
Raw Blame History

通知系统实现方案

创建时间:2026-04-10 状态:已评审

1. 需求理解

1.1 核心问题

当前项目(觅爻签问 App)没有通知系统,需要从零建设。核心需求包括:

  • 通知中心(通知列表/收件箱)
  • 用户通知存储与读取
  • 已读/已看状态追踪
  • 系统推送触达(APNs/FCM
  • 前台实时通知同步
  • 新版本通知、活动通知等业务通知类型

1.2 通知类型区分

类型 说明 当前状态
应用内通知记录 存储在 DB 的通知数据 不存在
系统推送通知 通过 APNs/FCM 发送 不存在
前台实时同步 App 打开时实时拉取 不存在
本地通知 App 本地触发的通知 不存在
已看状态 用户是否打开过通知详情 未设计
已读状态 用户是否标记为已读 未设计
推送触达 消息是否成功送达设备 未设计

1.3 系统边界

┌─────────────────────────────────────────────────────────┐
│                    Flutter App                           │
│  ┌──────────┐  ┌──────────┐  ┌──────────┐  ┌─────────┐  │
│  │ 本地通知  │  │ 推送接收  │  │ 通知中心  │  │ Badge  │  │
│  └──────────┘  └──────────┘  └──────────┘  └─────────┘  │
└───────────────┬─────────────────────┬───────────────────┘
                │                     │
                │  REST API           │ Supabase Realtime
                ▼                     ▼
┌───────────────────────────────────────────────────────────┐
│                      Backend (FastAPI)                     │
│  ┌────────────┐  ┌────────────┐  ┌────────────────────┐   │
│  │ 通知写入   │  │ Fanout     │  │ 推送发送 (APNs/FCM)│   │
│  └────────────┘  └────────────┘  └────────────────────┘   │
└───────────────┬─────────────────────┬───────────────────────┘
                │                     │
                ▼                     ▼
┌─────────────────────────┐  ┌─────────────────────────────┐
│    PostgreSQL (Supabase) │  │    Redis / Supabase        │
│  ┌──────────────────┐   │  │    Realtime                │
│  │ notifications    │   │  │  ┌─────────────────────┐  │
│  │ notification_    │   │  │  │ user:{id}:notif:new  │  │
│  │ receipts         │   │  │  └─────────────────────┘  │
│  │ user_push_       │   │  └─────────────────────────────┘
│  │ devices           │   │
│  └──────────────────┘   │
└─────────────────────────┘
                │
                ▼
┌─────────────────────────┐
│   Push Provider         │
│  (APNs / FCM / 备用)    │
└─────────────────────────┘

2. 现有代码调研结果

2.1 已发现的相关模块

Flutter 端

路径 说明
apps/lib/features/settings/presentation/screens/privacy_notification_settings_screen.dart 通知设置页面(占位 UI
apps/lib/features/settings/data/models/profile_settings.dart NotificationSettingsallowNotifications, allowVibration
apps/lib/features/home/presentation/screens/home_screen.dart:204 通知图标(点击显示 featurePending

后端

路径 说明
backend/src/schemas/shared/user.py:44 NotificationSettings schema
backend/src/v1/users/service.py 用户服务(含 settings 更新)
backend/src/v1/users/router.py 用户路由
backend/src/services/base/supabase.py Supabase 服务封装
backend/src/core/config/settings.py 配置管理

数据库迁移

文件 说明
backend/alembic/versions/20260403_0002_user_points_chat_schema.py profiles, sessions, messages 表
backend/alembic/versions/20260407_0001_update_notification_settings.py 通知设置默认值

2.2 已有能力

  1. Profile + Settings 系统profiles.settings 是 JSONB,可扩展存储通知偏好
  2. 用户认证Supabase Auth 完整
  3. 数据库迁移框架Alembic 已建立
  4. 后台任务Taskiq (基于 Redis)
  5. 日志系统:已集成 structlog
  6. API 模式Pydantic schemas, RFC7807 错误格式

2.3 当前缺口

缺口 说明
通知数据模型 notifications
推送设备管理 user_push_devices
通知状态追踪 user_notifications 状态字段体系
推送服务集成 无 APNs/FCM/备用推送集成
Flutter 通知中心 无通知列表页面
Flutter 推送接收 无 firebase_messaging 等依赖
Supabase Realtime 未用于通知同步
未读数 Badge 未实现

2.4 潜在冲突或风险

  1. AGENTS.md 约束apps/AGENTS.md 明确要求通知相关代码放在 core/notification/shared/widgets/notification/,需要遵循
  2. 现有 Settings 结构NotificationSettings 只有两个布尔字段,未来需要扩展
  3. 数据库 JSONB 查询profiles.settings 使用 GIN 索引,但通知列表不适合放 JSONB

3. 当前架构判断

3.1 推荐架构:混合推送 + 实时同步(Broadcast 主通道)

推荐方案:Supabase Realtime Broadcast(前台)+ APNs/FCM(后台/离线)

原因

  1. App 打开时(前台):使用 Broadcast 推送状态变化,避免直接依赖 postgres_changes 在高并发下的 RLS 扫描压力
  2. App 关闭时(后台/离线):通过 APNs/FCM 触达设备
  3. 两条链路统一写入 user_notifications,状态由服务端收敛,客户端只上报回执

不适合当前项目的方案

方案 不适合原因
纯轮询 电量/服务器压力大,不优雅
WebSocket 直连 需要自己维护连接,复杂度高
仅本地通知 无法触达离线用户
仅 APNs/FCM 无法查看历史通知列表

3.2 职责划分

组件 职责
notifications 存储通知内容,永久记录
user_notifications 追踪每个用户对每条通知的状态
notification_push_attempts 追踪推送每次尝试和失败原因
user_push_devices 存储设备 token
Supabase Realtime 前台实时通知同步
APNs/FCM 后台/离线推送

4. 推荐实现方案

4.1 数据模型设计

4.1.1 notifications

CREATE TABLE notifications (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    type VARCHAR(32) NOT NULL,          -- 'system'|'activity'|'version'|'social'
    priority SMALLINT DEFAULT 0,        -- 0=normal, 1=high
    title TEXT NOT NULL,
    body TEXT NOT NULL,
    data JSONB,                          -- 透传数据(deeplink 等)
    action_url TEXT,
    expires_at TIMESTAMPTZ,             -- 过期时间,NULL=永不过期
    created_at TIMESTAMPTZ DEFAULT now(),
    deleted_at TIMESTAMPTZ
);

-- 索引
CREATE INDEX ix_notifications_type ON notifications(type);
CREATE INDEX ix_notifications_created_at ON notifications(created_at DESC);

4.1.2 user_notifications 表(用户通知记录 + 状态)

CREATE TABLE user_notifications (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    user_id UUID NOT NULL REFERENCES auth.users(id) ON DELETE CASCADE,
    notification_id UUID NOT NULL REFERENCES notifications(id) ON DELETE CASCADE,
    dedupe_key TEXT NOT NULL,            -- 幂等键(建议: campaign_id:user_id 或业务事件ID

    -- 状态字段
    is_read BOOLEAN DEFAULT FALSE,       -- 已读(点击进入详情)
    is_seen BOOLEAN DEFAULT FALSE,       -- 已看(列表中曝光)
    is_opened BOOLEAN DEFAULT FALSE,     -- 从推送打开(客户端回执)
    read_at TIMESTAMPTZ,
    seen_at TIMESTAMPTZ,
    opened_at TIMESTAMPTZ,

    -- 推送状态(服务端可验证状态,不直接使用 delivered 语义)
    push_state SMALLINT DEFAULT 0,      -- 0=queued, 1=sent, 2=provider_ack, 3=failed
    push_provider VARCHAR(16),          -- apns|fcm
    push_error_code TEXT,
    push_sent_at TIMESTAMPTZ,
    push_provider_ack_at TIMESTAMPTZ,

    created_at TIMESTAMPTZ DEFAULT now(),
    updated_at TIMESTAMPTZ DEFAULT now(),
    deleted_at TIMESTAMPTZ,

    UNIQUE(user_id, dedupe_key)
);

-- 索引
CREATE INDEX ix_user_notifications_user_id ON user_notifications(user_id);
CREATE INDEX ix_user_notifications_user_unread ON user_notifications(user_id, is_read) WHERE deleted_at IS NULL;
CREATE INDEX ix_user_notifications_created_at ON user_notifications(created_at DESC);
CREATE INDEX ix_user_notifications_user_seen ON user_notifications(user_id, is_seen) WHERE deleted_at IS NULL;
CREATE INDEX ix_user_notifications_push_state ON user_notifications(push_state) WHERE deleted_at IS NULL;

4.1.3 user_push_devices

CREATE TABLE user_push_devices (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    user_id UUID NOT NULL REFERENCES auth.users(id) ON DELETE CASCADE,
    device_token TEXT NOT NULL,          -- APNs/FCM token
    device_type VARCHAR(16) NOT NULL,    -- 'ios'|'android'
    push_provider VARCHAR(32) NOT NULL,  -- 'apns'|'fcm'|'huawei'|'xiaomi'
    app_version TEXT,
    locale VARCHAR(16),

    is_active BOOLEAN DEFAULT TRUE,
    last_used_at TIMESTAMPTZ DEFAULT now(),
    created_at TIMESTAMPTZ DEFAULT now(),
    updated_at TIMESTAMPTZ DEFAULT now(),
    deleted_at TIMESTAMPTZ,

    UNIQUE(user_id, device_token, push_provider)
);

-- 索引
CREATE INDEX ix_user_push_devices_user_id ON user_push_devices(user_id);
CREATE INDEX ix_user_push_devices_token ON user_push_devices(device_token);
CREATE INDEX ix_user_push_devices_active ON user_push_devices(user_id, is_active) WHERE deleted_at IS NULL;

4.1.4 notification_push_attempts 表(推送尝试日志)

CREATE TABLE notification_push_attempts (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    user_notification_id UUID NOT NULL REFERENCES user_notifications(id) ON DELETE CASCADE,
    provider VARCHAR(16) NOT NULL,          -- apns|fcm
    attempt_no SMALLINT NOT NULL,
    request_id TEXT,
    result SMALLINT NOT NULL,               -- 0=sent, 1=provider_ack, 2=failed, 3=timeout
    error_code TEXT,
    error_detail TEXT,
    created_at TIMESTAMPTZ DEFAULT now()
);

CREATE INDEX ix_notification_push_attempts_un_id ON notification_push_attempts(user_notification_id, created_at DESC);

4.1.5 状态字段说明

字段 含义 触发条件
is_seen 用户在列表中看到该通知 列表滚动曝光(停留 > 1秒)或拉取
is_read 用户点击进入详情 点击通知项
is_opened 用户从系统通知打开 App 客户端上报 open 回执
push_state=sent 推送请求已发出 服务端调用 APNs/FCM 成功返回
push_state=provider_ack 平台确认接收 APNs/FCM Provider 级确认
push_state=failed 推送失败 服务端重试耗尽或不可恢复错误

注意:provider_ack 不等价于“用户已看到通知”。用户可见性必须以 is_opened/is_seen/is_read 为准。

4.2 后端 API 设计

4.2.1 通知路由 (backend/src/v1/notifications/)

方法 路径 说明
GET /api/v1/notifications 获取当前用户通知列表
GET /api/v1/notifications/unread-count 获取未读数
PATCH /api/v1/notifications/{id}/seen 标记为已看
PATCH /api/v1/notifications/{id}/read 标记为已读
PATCH /api/v1/notifications/mark-all-read 全部已读
DELETE /api/v1/notifications/{id} 删除通知

4.2.2 设备路由 (backend/src/v1/push/)

方法 路径 说明
POST /api/v1/push/devices 注册/更新设备 token
DELETE /api/v1/push/devices/{id} 删除设备
GET /api/v1/push/devices 获取用户设备列表

4.3 Flutter 端设计

4.3.1 目录结构(遵循 AGENTS.md

apps/lib/
├── core/
│   └── notification/                    # NEW
│       ├── models/
│       │   ├── notification.dart
│       │   └── push_device.dart
│       ├── services/
│       │   ├── notification_service.dart
│       │   └── push_service.dart
│       └── repositories/
│           └── notification_repository.dart
├── features/
│   └── notifications/                   # NEW
│       ├── data/
│       │   ├── apis/notification_api.dart
│       │   └── repositories/notification_repository_impl.dart
│       ├── presentation/
│       │   ├── screens/notification_center_screen.dart
│       │   ├── widgets/notification_item.dart
│       │   └── bloc/notification_bloc.dart
├── shared/
│   └── widgets/
│       └── notification/               # NEW
│           ├── notification_badge.dart
│           └── notification_toast.dart

4.3.2 通知中心流程

┌──────────────────────────────────────────────────────────────┐
│ App 打开 / HomeScreen                                        │
│  1. 初始化时连接 Supabase Realtime 私有频道                     │
│  2. 监听 Broadcast 通知事件                                    │
│  3. 实时更新本地列表和未读数 Badge                              │
└──────────────────────────────────────────────────────────────┘
                            │
                            ▼
┌──────────────────────────────────────────────────────────────┐
│ NotificationCenterScreen                                      │
│  1. 首次进入 → 调用 GET /api/v1/notifications                 │
│  2. 滚动曝光 → 调用 PATCH /api/v1/notifications/{id}/seen    │
│  3. 点击通知 → 调用 PATCH /api/v1/notifications/{id}/read    │
│  4. 下拉刷新 → 重新拉取列表                                     │
└──────────────────────────────────────────────────────────────┘

4.4 实时同步方案

Supabase Realtime 职责

  • 当用户在另一设备读取通知时,当前设备实时更新 is_read / is_seen / is_opened 状态
  • 仅做前台状态分发,不承担离线触达

推荐通道:Broadcast(私有频道)

DB update(user_notifications)
  -> trigger 调用 realtime.broadcast_changes(...)
  -> channel: user:{user_id}:notifications
  -> Flutter 收到事件并更新本地缓存

为什么不用 postgres_changes 作为主通道

  • 在订阅规模扩大时,postgres_changes 会放大 RLS 评估开销
  • Broadcast 在通知场景下更可控,可按用户私有频道精准下发
final channel = supabase.channel(
  'user:${userId}:notifications',
  opts: const RealtimeChannelConfig(private: true),
);

channel.onBroadcast(
  event: 'notification_changed',
  callback: (payload) {
    // 更新本地状态和 Badge
  },
).subscribe();

4.5 推送发送流程(Outbox + Worker

┌─────────────┐     ┌──────────────────┐     ┌─────────────────┐
│ 业务触发    │────▶│ DB 事务写入      │────▶│ push_outbox      │
│ (后台任务)  │     │ notifications +  │     │ pending 事件      │
└─────────────┘     │ user_notifications│     └────────┬────────┘
                    └──────────────────┘              │
                                                      ▼
                                           ┌─────────────────┐
                                           │ Fanout Worker   │
                                           │ 读取 outbox      │
                                           └────────┬────────┘
                                                    │
                  ┌─────────────────────────────────┼─────────────────────────────────┐
                  │                                 │                                 │
                  ▼                                 ▼                                 ▼
          ┌──────────────┐                 ┌──────────────┐                 ┌──────────────┐
          │ Broadcast    │                 │ APNs / FCM   │                 │ 重试 / DLQ   │
          │ 前台增量同步  │                 │ 离线触达       │                 │ 指数退避      │
          └──────────────┘                 └──────────────┘                 └──────────────┘

4.6 幂等、重试与失败处理

4.6.1 幂等策略

  • 每次业务发通知必须携带 dedupe_key
  • UNIQUE(user_id, dedupe_key) 保证重复投递不会生成多条记录
  • PATCH /readPATCH /seen 必须幂等,重复调用返回 200 且不重复更新统计

4.6.2 重试策略

  • 可重试错误:超时、5xx、429
  • 不可重试错误:token invalid、payload invalid、认证失败
  • 退避策略:30s -> 2m -> 10m -> 1h,最多 4 次
  • 超过上限进入 DLQ,由运维任务定时重放或人工处理

4.6.3 失败模式清单

场景 风险 方案
推送平台短时不可用 批量失败 Outbox + 指数退避 + DLQ
客户端重复上报 read 状态抖动/统计偏差 幂等更新 + 仅首次写时间戳
多设备并发 read/seen 最终状态不一致 单条记录原子更新 + updated_at 冲突检测
Realtime 断连 UI 未及时同步 前台重连后触发增量拉取(按 updated_at

4.7 版本通知与活动通知

4.7.1 版本通知

  • 触发条件:用户登录时检测到 App 版本低于最新版本
  • 写入方式:后台任务扫描所有用户,批量写入 user_notifications
  • notification.type = 'version'

4.7.2 活动通知

  • 触发条件:运营后台或定时任务触发
  • 目标用户:可按标签/行为筛选
  • notification.type = 'activity'

4.8 权限与安全策略

RLS 设计

-- user_notifications
ALTER TABLE user_notifications ENABLE ROW LEVEL SECURITY;

CREATE POLICY auth_select ON user_notifications FOR SELECT
  USING (auth.uid() = user_id AND deleted_at IS NULL);

CREATE POLICY auth_update ON user_notifications FOR UPDATE
  USING (auth.uid() = user_id)
  WITH CHECK (auth.uid() = user_id);

-- user_push_devices
ALTER TABLE user_push_devices ENABLE ROW LEVEL SECURITY;

CREATE POLICY auth_all ON user_push_devices FOR ALL
  USING (auth.uid() = user_id AND deleted_at IS NULL)
  WITH CHECK (auth.uid() = user_id);

信任边界

  • 客户端不能创建通知主记录,通知内容仅由服务端写入
  • 客户端只能操作自己的 seen/read/open 回执和设备 token
  • owner_id/user_id 必须来自 JWT sub,禁止客户端传入
  • 管理后台发通知接口必须走服务角色鉴权,不暴露给普通用户 token

4.9 可观测性与运行指标

指标 目标/SLO 备注
通知写入成功率 >= 99.9% 5 分钟窗口
推送发送成功率(sent >= 99.5% 按 provider 分维度
推送 provider_ack 延迟 P95 < 3s 仅平台确认链路
前台实时同步延迟 P95 < 1s Broadcast 收包到 UI 更新
read API P95 < 150ms 排除网络抖动

日志要求:每次通知发送链路打通 trace_id(创建 -> outbox -> provider -> 回执)。

4.10 NOT in scope(本阶段不做)

  • 厂商通道(小米/华为)深度集成
  • 通知模板多语言管理后台
  • 通知 A/B 实验平台
  • 全量历史归档/冷热分层

5. 分阶段落地计划

第一阶段:最小可用通知中心(MVP 收敛)

目标:在 App 内显示通知列表,支持标记已读

改动范围

  • Backend: notifications 表 + API
  • Flutter: 通知中心页面 + Repository

主要任务

  1. 创建 notificationsuser_notifications
  2. 实现 GET /api/v1/notifications API
  3. 实现 PATCH /api/v1/notifications/{id}/read API
  4. Flutter 通知中心页面(列表 + 标记已读)
  5. 未读数 Badge(在 HomeScreen 通知图标上显示红点)
  6. 完成协议文档:docs/protocols/notification/notification-protocol.md
  7. 完成基础测试(见第 7 节)

依赖项:无

风险点

  • 无推送,通知需要后台手动写入 DB
  • 不支持实时同步,多设备体验差

验收标准

  • 能看到通知列表
  • 点击通知能标记为已读
  • 未读通知有 Badge 提示
  • 已读/未读状态在下拉刷新后正确
  • 越权访问被拒绝(用户 A 无法读写用户 B 通知)
  • read 接口重复调用幂等

第二阶段:接入系统推送

目标:支持离线推送通知

改动范围

  • Backend: user_push_devices 表 + Push Service
  • Flutter: 推送接收 + 设备注册

主要任务

  1. 创建 user_push_devices
  2. 实现 POST /api/v1/push/devices API(注册 token
  3. 集成 firebase_messagingAndroid/ apnsiOS
  4. 实现 Push Sending ServiceAPNs/FCM SDK+ Outbox Worker
  5. Flutter 端:获取 FCM/APNs token 并注册
  6. 服务端发送后更新 push_state,客户端仅上报 open/read/seen 回执
  7. 落地重试与 DLQ

依赖项

  • 第一阶段完成
  • Firebase 项目配置(Android
  • APNs 证书/KeyiOS

风险点

  • APNs/FCM 配置复杂
  • 推送送达率不稳定(特别是国内 Android)

验收标准

  • 离线设备能收到推送
  • 点击推送能打开对应通知详情
  • 推送状态(sent/provider_ack/failed)正确回写
  • provider 短时故障时消息可自动重试并可追踪

第三阶段:实时同步与统计

目标:多设备实时同步 + 完整状态追踪

改动范围

  • Backend: Supabase Realtime + 状态统计
  • Flutter: Realtime 订阅 + 曝光追踪

主要任务

  1. Supabase Realtime Broadcast 私有频道接入
  2. 实现 seen 状态(曝光追踪)
  3. 添加统计接口(送达率、点击率)
  4. Flutter 端:列表滚动时标记 seen
  5. 版本通知、活动通知的后台写入逻辑
  6. 断线重连后的增量拉取机制(按 updated_at

依赖项

  • 第二阶段完成
  • Supabase Realtime 已启用

风险点

  • Realtime 连接数限制(根据 plan
  • 曝光追踪可能影响性能

验收标准

  • 一设备读取通知,另一设备实时更新
  • 列表曝光能正确标记 seen
  • 能查看送达/点击统计
  • Realtime 断连恢复后不丢状态更新

6. 建议改动清单

6.1 新增/修改的表

表名 操作 说明
notifications 新增 通知模板表
user_notifications 新增 用户通知记录+状态
user_push_devices 新增 设备 token 存储
notification_push_attempts 新增 推送尝试日志
profiles 修改 暂不改,优先在线计算 unread_count

6.2 新增后端模块

路径 说明
backend/src/v1/notifications/ 通知路由+服务+仓库
backend/src/v1/push/ 推送设备路由+服务
backend/src/services/push/ APNs/FCM 发送服务
backend/src/services/push/outbox_worker.py 推送 outbox 消费与重试
backend/src/models/notification.py 通知 ORM 模型
backend/src/models/notification_push_attempt.py 推送尝试日志模型
backend/src/schemas/notification.py Pydantic schemas

6.3 新增 Flutter 模块

路径 说明
apps/lib/core/notification/ 通知核心逻辑
apps/lib/features/notifications/ 通知功能模块
apps/lib/shared/widgets/notification/ 通知 UI 组件

6.4 新增接口

方法 路径 说明
GET /api/v1/notifications 通知列表
GET /api/v1/notifications/unread-count 未读数
PATCH /api/v1/notifications/{id}/seen 标记已看
PATCH /api/v1/notifications/{id}/read 标记已读
PATCH /api/v1/notifications/mark-all-read 全部已读
DELETE /api/v1/notifications/{id} 删除通知
POST /api/v1/notifications/{id}/opened 上报从推送打开
POST /api/v1/push/devices 注册设备
DELETE /api/v1/push/devices/{id} 删除设备

6.5 新增配置项

# .env
ERYAO_PUSH__APNS_KEY_ID=xxx
ERYAO_PUSH__APNS_TEAM_ID=xxx
ERYAO_PUSH__APNS_KEY_PATH=/path/to/key.p8
ERYAO_PUSH__FCM_SERVER_KEY=xxx
ERYAO_PUSH__ENABLED=true

6.6 Flutter 依赖(pubspec.yaml

dependencies:
  firebase_messaging: ^15.0.0     # FCM
  flutter_local_notifications: ^18.0.0  # 本地通知
  supabase_flutter: ^2.5.0      # Supabase 客户端(含 Realtime

6.7 协议文档(先于实现)

docs/protocols/notification/
├── notification-protocol.md          # 数据模型、字段语义、兼容策略
├── notification-api-protocol.md      # API 请求/响应、错误码
└── notification-realtime-protocol.md # Broadcast payload 与版本约束

7. 测试策略(必须随阶段落地)

7.1 测试框架

  • 后端:pytest + pytest-asyncio(见 pyproject.toml
  • Flutter:单测 + Widget 测试 + 关键链路集成测试

7.2 覆盖图(关键分支)

Create Notification
  -> validate payload
    -> invalid type [unit]
    -> expires_at in past [unit]
  -> fanout target users
    -> empty target set [unit]
    -> partial user write failed [integration]
  -> write user_notifications (dedupe)
    -> first write success [unit]
    -> duplicate dedupe_key [unit]
  -> enqueue outbox
    -> enqueue success [integration]
    -> enqueue failed rollback [integration]

Push Worker
  -> send provider
    -> sent/provider_ack [integration]
    -> timeout retry [integration]
    -> permanent failure to DLQ [integration]

User Callback
  -> PATCH seen/read/opened
    -> own record success [api]
    -> cross-user forbidden [api/security]
    -> repeat call idempotent [api]

Realtime
  -> broadcast receive update [integration]
  -> reconnect + incremental pull [e2e]

7.3 最低覆盖要求

  • 新增后端核心分支(通知创建、状态更新、重试)语句覆盖率 >= 90%
  • 安全相关路径(越权、伪造 user_id、非法 token)必须 100% 有测试
  • 每个阶段必须包含至少 1 条回归测试,防止旧行为被破坏

7.4 回归测试强制项

  1. 同一通知重复发送不应生成重复 user_notifications
  2. mark-all-read 与单条 read 并发时,最终状态一致
  3. Realtime 中断后恢复,未读数与服务端一致
  4. 无效/过期 token 不应导致消息丢失(进入失败态并可重试)

8. 最终推荐

8.1 推荐总体方案

采用 Supabase Realtime Broadcast(前台)+ APNs/FCM(后台/离线)的混合方案

8.2 推荐原因

  1. 用户体验最优:前台实时同步,多设备状态一致
  2. 离线可达:通过 APNs/FCM 触达离线用户
  3. 扩展性更稳Broadcast 规避高并发下 postgres_changes 的权限扫描放大
  4. 状态语义更准确provider_ack 与用户可见状态分离,统计更可信
  5. 项目适配Supabase 已在用,Realtime 只是扩展使用

8.3 不确定点

问题 推断依据
是否已有 Firebase 项目 未在代码库中找到 google-services.json 或 Firebase 配置
是否已有 APNs 证书 未在代码库中找到证书文件
国内 Android 推送需求 国内 Android ROM 需要厂商通道(如小米、华为),建议预留

8.4 实施优先级排序

优先级 阶段 说明
P0 第一阶段 MVP:通知列表 + 标记已读
P1 第一阶段 未读数 Badge
P2 第二阶段 推送接入(Android FCM 先,iOS APNs 后)
P3 第三阶段 Realtime 同步
P4 第三阶段 曝光追踪 + 统计

8.5 关键约束提醒

  1. 遵循 AGENTS.md:通知代码放在 core/notification/shared/widgets/notification/
  2. Error Swallowing:所有异常必须传播,禁止静默捕获
  3. Protocol 先行:新建通知相关协议文档在 docs/protocols/notification/
  4. 渐进演进:不废弃现有 NotificationSettings,而是扩展它

附录:推断依据

信息点 推断依据
无通知表 所有 migration 文件中均未发现 notifications 相关表
无推送集成 pubspec.yaml 无 firebase_messaging / apns 相关依赖
无 Realtime 通知 Supabase Service 仅用于 Storage,未配置 Realtime
AGENTS.md 约束 apps/AGENTS.md 明确提到 "Reminder/Notification Rewrite Boundary"
推送服务配置 settings.py 无任何 APNs/FCM 相关配置