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

222 lines
7.2 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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 分钟验证