docs: add reminder overlay design document
This commit is contained in:
@@ -0,0 +1,221 @@
|
||||
# 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 分钟验证
|
||||
Reference in New Issue
Block a user