# Reminder Overlay 设计文档 ## 概述 重构日历提醒机制,简化前台/后台判断逻辑,将所有提醒交互统一到独立的 ReminderOverlay 组件处理。 ## 背景 当前实现复杂,涉及: - App 启动状态判断(前台/后台) - 离线归档请求队列 + 指数退避重试 - 通知权限降级(Android Timer 模拟) - 聚合通知批量操作 新方案利用 iOS/Android 原生通知分组能力,实现: - 每条通知独立 payload,点击哪条处理哪条 - 统一的 ReminderOverlay 处理所有用户交互 - 操作完成后 app 退到后台 ## 设计决策 | 决策项 | 选择 | |--------|------| | 关闭 overlay 后的行为 | 回到首页,保持缓存状态 | | 同分钟多条通知处理 | 按点击顺序处理当前,剩余按时间排序 | | iOS 冷启动 payload 传递 | UserDefaults(App 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 ```json { "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 分钟验证