Files
social-app/docs/plans/2026-03-20-reminder-overlay-design.md
T

7.2 KiB
Raw Blame History

Reminder Overlay 设计文档

概述

重构日历提醒机制,简化前台/后台判断逻辑,将所有提醒交互统一到独立的 ReminderOverlay 组件处理。

背景

当前实现复杂,涉及:

  • App 启动状态判断(前台/后台)
  • 离线归档请求队列 + 指数退避重试
  • 通知权限降级(Android Timer 模拟)
  • 聚合通知批量操作

新方案利用 iOS/Android 原生通知分组能力,实现:

  • 每条通知独立 payload,点击哪条处理哪条
  • 统一的 ReminderOverlay 处理所有用户交互
  • 操作完成后 app 退到后台

设计决策

决策项 选择
关闭 overlay 后的行为 回到首页,保持缓存状态
同分钟多条通知处理 按点击顺序处理当前,剩余按时间排序
iOS 冷启动 payload 传递 UserDefaultsApp Groups 方案)
Android 通知展示 Full-screen intent(锁屏也弹窗)
稍后提醒时间选项 5 分钟、15 分钟(下拉选项)
"完成"按钮行为 归档 + 关闭 + 退后台
"稍后提醒"按钮行为 弹出选项 + 延后通知 + 关闭 + 退后台
UI 组件 新建 ReminderOverlay(不复用现有)

核心流程

通知到达 → 用户点击通知 → 
    ├─ App 已运行 → 恢复前台 → 直接收到 payload → 打开 ReminderOverlay
    └─ App 未运行 → 
        ├─ iOS: 原生层写入 UserDefaults → Flutter 启动时读取
        └─ Android: full-screen intent 启动 → Flutter 收到 payload
    
ReminderOverlay 显示:
  - 日程标题
  - 当前时间
  - [稍后提醒 ▼] | [完成]
  
用户操作:
  ├─ 完成 → 归档请求 → 关闭 overlay → 退后台
  └─ 稍后提醒 → 选择时间 → 取消当前通知 + 注册新通知 → 关闭 overlay → 退后台

处理完当前 → 检查同分钟是否有多条 → 
  ├─ 有 → 打开下一条的 ReminderOverlay
  └─ 无 → 保持退后台状态

移除的组件

组件 文件路径 移除原因
ReminderColdStartQueue lib/features/calendar/reminders/reminder_cold_start_queue.dart 不需要后台重放机制
ReminderOutboxStore lib/features/calendar/reminders/reminder_outbox_store.dart 不需要离线归档队列
ReminderForegroundPresenter lib/features/calendar/reminders/ui/reminder_foreground_presenter.dart 不需要前台判断
ReminderPresentationCoordinator lib/features/calendar/reminders/ui/reminder_presentation_coordinator.dart 不需要防重复展示
ReminderActionDedupeStore lib/features/calendar/reminders/reminder_action_dedupe_store.dart 通知原生幂等
ReminderOverlapPolicy lib/features/calendar/reminders/reminder_overlap_policy.dart 改为原生分组
Android Timer 模拟逻辑 LocalNotificationService 不需要权限降级

新增组件

ReminderOverlay

独立的状态管理页面,处理提醒交互。

职责

  • 显示日程标题和当前时间
  • 提供"稍后提醒"下拉选项(5分钟/15分钟)
  • 提供"完成"按钮(归档)
  • 处理完成后关闭 overlay

文件位置lib/features/calendar/reminders/ui/reminder_overlay.dart

ReminderQueueManager

管理同分钟多条通知的处理队列。

职责

  • 存储同分钟的通知列表
  • 按点击顺序跟踪当前处理项
  • 处理完当前后调度下一项

文件位置lib/features/calendar/reminders/reminder_queue_manager.dart

IOSNotificationPayloadBridge

iOS 冷启动时从 UserDefaults 读取 notification payload。

职责

  • App 启动时检查是否有待处理的通知 launch
  • 读取 payload 并打开对应的 ReminderOverlay
  • 处理完成后清理 UserDefaults

文件位置lib/core/notifications/ios_notification_payload_bridge.dart

平台差异处理

iOS

  1. 通知点击启动 App

    • 配置 setPluginRegistrantCallback(已有)
    • iOS 原生层将 payload 写入 UserDefaults
    • Flutter 启动时 IOSNotificationPayloadBridge 读取数据
  2. 通知分组

    • 使用 threadIdentifier 分组
    • 同一分钟的通知使用相同的 threadIdentifier

Android

  1. Full-screen intent

    • AndroidNotificationDetails 设置 fullScreenIntent: true
    • 锁屏时直接弹出全屏 overlay
  2. 通知分组

    • 使用 groupKey 分组
    • 同一分钟的通知使用相同的 groupKey

API 变化

归档请求

仍然使用现有的 CalendarService.archiveEvent(),但不再需要失败重试逻辑。

POST /api/v1/calendar/events/{eventId}/archive

通知 Payload

{
  "eventId": "evt_xxx",
  "title": "日程标题",
  "startAt": "2026-03-20T10:00:00Z",
  "endAt": "2026-03-20T11:00:00Z",
  "timezone": "Asia/Shanghai",
  "mode": "single",
  "fireTimeBucket": 1774060800000
}

数据流

通知发送流程(不变)

CalendarService.upsertEventReminder()
  → LocalNotificationService.upsertEventReminder()
  → flutter_local_notifications.zonedSchedule()

通知点击处理流程

用户点击通知
  ├─ App 运行中 → onDidReceiveNotificationResponse(payload)
  └─ App 未运行
      ├─ iOS → 原生写入 UserDefaults → Flutter 启动 → 读取 → 打开 overlay
      └─ Android → full-screen intent → Flutter 收到 payload → 打开 overlay

ReminderOverlay 打开
  ├─ 用户点击"完成" → archiveEvent() → 关闭 → 检查队列 → 有下一条则打开下一条
  └─ 用户点击"稍后提醒" → cancelNotification() + scheduleReminderAt() → 关闭 → 检查队列 → 有下一条则打开下一条

错误处理

场景 处理方式
归档请求失败 显示 toast 提示用户,操作已完成(下次打开 app 时同步)
延后通知注册失败 显示 toast 提示用户,当前提醒已取消
同分钟多条处理时其中一条失败 跳过该条,处理下一条

文件变更清单

删除

  • lib/features/calendar/reminders/reminder_cold_start_queue.dart
  • lib/features/calendar/reminders/reminder_outbox_store.dart
  • lib/features/calendar/reminders/reminder_action_dedupe_store.dart
  • lib/features/calendar/reminders/reminder_overlap_policy.dart
  • lib/features/calendar/reminders/ui/reminder_foreground_presenter.dart
  • lib/features/calendar/reminders/ui/reminder_presentation_coordinator.dart
  • lib/features/calendar/reminders/ui/widgets/reminder_action_sheet.dart
  • 相关测试文件

新增

  • lib/features/calendar/reminders/ui/reminder_overlay.dart
  • lib/features/calendar/reminders/reminder_queue_manager.dart
  • lib/core/notifications/ios_notification_payload_bridge.dart

修改

  • lib/core/notifications/local_notification_service.dart(移除权限降级逻辑)
  • lib/main.dart(集成 IOSNotificationPayloadBridge
  • 相关测试文件

测试策略

单元测试

  • ReminderQueueManager: 队列排序、下一条调度
  • IOSNotificationPayloadBridge: payload 读写

集成测试

  • 通知点击 → overlay 打开 → 操作 → 关闭
  • 同分钟多条通知顺序处理

手动测试

  • iOS 冷启动点击通知
  • Android 锁屏点击 full-screen intent 通知
  • 稍后提醒 5 分钟/15 分钟验证