docs: add reminder overlay design document

This commit is contained in:
qzl
2026-03-20 18:22:14 +08:00
parent e4fa7980cc
commit 5e2ade614f
@@ -0,0 +1,221 @@
# 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
```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 分钟验证