31 KiB
31 KiB
通知系统实现方案
创建时间: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 |
NotificationSettings(allowNotifications, 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 已有能力
- Profile + Settings 系统:
profiles.settings是 JSONB,可扩展存储通知偏好 - 用户认证:Supabase Auth 完整
- 数据库迁移框架:Alembic 已建立
- 后台任务:Taskiq (基于 Redis)
- 日志系统:已集成 structlog
- API 模式:Pydantic schemas, RFC7807 错误格式
2.3 当前缺口
| 缺口 | 说明 |
|---|---|
| 通知数据模型 | 无 notifications 表 |
| 推送设备管理 | 无 user_push_devices 表 |
| 通知状态追踪 | 无 user_notifications 状态字段体系 |
| 推送服务集成 | 无 APNs/FCM/备用推送集成 |
| Flutter 通知中心 | 无通知列表页面 |
| Flutter 推送接收 | 无 firebase_messaging 等依赖 |
| Supabase Realtime | 未用于通知同步 |
| 未读数 Badge | 未实现 |
2.4 潜在冲突或风险
- AGENTS.md 约束:
apps/AGENTS.md明确要求通知相关代码放在core/notification/和shared/widgets/notification/,需要遵循 - 现有 Settings 结构:
NotificationSettings只有两个布尔字段,未来需要扩展 - 数据库 JSONB 查询:
profiles.settings使用 GIN 索引,但通知列表不适合放 JSONB
3. 当前架构判断
3.1 推荐架构:混合推送 + 实时同步(Broadcast 主通道)
推荐方案:Supabase Realtime Broadcast(前台)+ APNs/FCM(后台/离线)
原因:
- App 打开时(前台):使用 Broadcast 推送状态变化,避免直接依赖
postgres_changes在高并发下的 RLS 扫描压力 - App 关闭时(后台/离线):通过 APNs/FCM 触达设备
- 两条链路统一写入
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 /read、PATCH /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必须来自 JWTsub,禁止客户端传入- 管理后台发通知接口必须走服务角色鉴权,不暴露给普通用户 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
主要任务:
- 创建
notifications和user_notifications表 - 实现
GET /api/v1/notificationsAPI - 实现
PATCH /api/v1/notifications/{id}/readAPI - Flutter 通知中心页面(列表 + 标记已读)
- 未读数 Badge(在 HomeScreen 通知图标上显示红点)
- 完成协议文档:
docs/protocols/notification/notification-protocol.md - 完成基础测试(见第 7 节)
依赖项:无
风险点:
- 无推送,通知需要后台手动写入 DB
- 不支持实时同步,多设备体验差
验收标准:
- 能看到通知列表
- 点击通知能标记为已读
- 未读通知有 Badge 提示
- 已读/未读状态在下拉刷新后正确
- 越权访问被拒绝(用户 A 无法读写用户 B 通知)
- read 接口重复调用幂等
第二阶段:接入系统推送
目标:支持离线推送通知
改动范围:
- Backend:
user_push_devices表 + Push Service - Flutter: 推送接收 + 设备注册
主要任务:
- 创建
user_push_devices表 - 实现
POST /api/v1/push/devicesAPI(注册 token) - 集成 firebase_messaging(Android)/ apns(iOS)
- 实现 Push Sending Service(APNs/FCM SDK)+ Outbox Worker
- Flutter 端:获取 FCM/APNs token 并注册
- 服务端发送后更新
push_state,客户端仅上报 open/read/seen 回执 - 落地重试与 DLQ
依赖项:
- 第一阶段完成
- Firebase 项目配置(Android)
- APNs 证书/Key(iOS)
风险点:
- APNs/FCM 配置复杂
- 推送送达率不稳定(特别是国内 Android)
验收标准:
- 离线设备能收到推送
- 点击推送能打开对应通知详情
- 推送状态(sent/provider_ack/failed)正确回写
- provider 短时故障时消息可自动重试并可追踪
第三阶段:实时同步与统计
目标:多设备实时同步 + 完整状态追踪
改动范围:
- Backend: Supabase Realtime + 状态统计
- Flutter: Realtime 订阅 + 曝光追踪
主要任务:
- Supabase Realtime Broadcast 私有频道接入
- 实现
seen状态(曝光追踪) - 添加统计接口(送达率、点击率)
- Flutter 端:列表滚动时标记
seen - 版本通知、活动通知的后台写入逻辑
- 断线重连后的增量拉取机制(按
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 回归测试强制项
- 同一通知重复发送不应生成重复
user_notifications mark-all-read与单条read并发时,最终状态一致- Realtime 中断后恢复,未读数与服务端一致
- 无效/过期 token 不应导致消息丢失(进入失败态并可重试)
8. 最终推荐
8.1 推荐总体方案
采用 Supabase Realtime Broadcast(前台)+ APNs/FCM(后台/离线)的混合方案
8.2 推荐原因
- 用户体验最优:前台实时同步,多设备状态一致
- 离线可达:通过 APNs/FCM 触达离线用户
- 扩展性更稳:Broadcast 规避高并发下
postgres_changes的权限扫描放大 - 状态语义更准确:
provider_ack与用户可见状态分离,统计更可信 - 项目适配: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 关键约束提醒
- 遵循 AGENTS.md:通知代码放在
core/notification/和shared/widgets/notification/ - Error Swallowing:所有异常必须传播,禁止静默捕获
- Protocol 先行:新建通知相关协议文档在
docs/protocols/notification/ - 渐进演进:不废弃现有
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 相关配置 |