Files
social-app/docs/protocols/calendar/reminder-alert-lifecycle.md
T

159 lines
5.0 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.
# Calendar Reminder Alert Lifecycle Protocol
## Version
- Current: `1.0`
- Status: Draft
---
## Goal
定义日程提醒弹窗在 Android/iOS 的统一行为语义,覆盖提醒触发、用户操作、离线补偿、归档与重装恢复,确保多端一致性和可恢复性。
---
## Canonical Rules
1. 提醒弹窗动作语义跨平台一致:`archive``snooze10m`
2. 展示文案“取消”必须映射到内部动作 `archive`,并归档对应日程(`status=archived`)。
3. 归档后的 UI 渲染必须灰色显示;不强制改写原始 metadata 颜色。
4. 前端动作上报后端采用最终一致性:本地 outbox + 重放机制。
---
## Reminder Payload Contract
```json
{
"eventId": "uuid",
"title": "string",
"startAt": "iso8601-with-offset",
"endAt": "iso8601-with-offset|null",
"timezone": "IANA",
"location": "string|null",
"notes": "string|null",
"color": "#RRGGBB|null",
"mode": "single|aggregate",
"aggregateIds": ["uuid"],
"version": 1
}
```
### Constraints
- `eventId` 必填且为 UUID。
- `startAt` 必须带时区偏移。
- `mode=aggregate` 时,`aggregateIds` 至少包含 2 个 id。
- `version` 必填,用于后续协议升级兼容。
---
## Action Contract
动作枚举:
- `archive`: 内部归档动作(UI 展示文案为“取消”)
- `snooze10m`: 用户点击稍后提醒,重排到 `now + 10m`
### UI Label Mapping
- 按钮展示文案“取消”仅为 UI 文案,不作为协议动作值。
- “取消”按钮点击后的上报动作值必须为 `archive`
---
## Outbox Contract (Frontend)
```json
{
"opId": "uuid",
"eventId": "uuid",
"action": "archive|snooze10m",
"targetStatus": "archived|null",
"occurredAt": "iso8601-with-offset",
"retryCount": 0,
"nextRetryAt": "iso8601-with-offset|null",
"state": "pending|done|dead",
"lastError": "string|null"
}
```
### Constraints
- 幂等键:`actionExecutionId = notificationId + "|" + actionId + "|" + fireTimeBucket`
- `notificationId`: 本地通知唯一 id(整数或其字符串化值)。
- `actionId`: 内部动作 id,取值仅允许 `archive``snooze10m`
- `fireTimeBucket`: 触发时间按“分钟”向下取整后的 epoch minute`millisecondsSinceEpoch / 60000`)。
- 执行前必须先按 `actionExecutionId` 查重;若命中历史执行,直接 ACK,不得产生任何业务副作用。
- 重试策略:指数退避。默认参数:首次重试 0 分钟,之后依次 1/2/4/8/16/32/64 分钟;最大重试次数默认 8(可配置但需跨端一致)。
- `archive` 映射到后端 `PATCH status=archived`
- Outbox 记录必须本地持久化,App 重启后可恢复。
### cold-start Queue Replay Contract
- App cold-start 恢复 outbox/动作队列时,必须按入队顺序进行回放。
- 单条回放失败只记录失败并进入重试,不得阻塞后续条目继续回放。
- 回放过程必须复用 `actionExecutionId` 做去重,防止重复归档或重复稍后提醒。
- 回放执行模型为全局串行(concurrency=1),处理顺序与入队顺序一致。
- ACK 时序:仅在动作被成功执行或命中幂等去重时返回 ACK;失败条目进入重试状态并继续处理下一条。
---
## Scheduling and Compensation Rules
### Normal schedule
- `remindAt = startAt - reminderMinutes`
### Bootstrap/reinstall compensation
启动重建时对每个 active 事件执行:
1. `now < remindAt`:按 `remindAt` 正常调度。
2. `remindAt <= now < endAt`:立刻补偿提醒(建议 `+5s`),然后进入 10 分钟节奏。
3. `now >= endAt`:不再补发提醒;后续状态流转由上层业务策略决定(不在本协议强制范围内)。
4. `endAt = null`:视为无结束时间,沿用规则 1/2,不适用规则 3。
---
## Uniqueness and Dedupe Rules
- 通知唯一键:`hash(eventId + cycleStartEpochMinutes + mode)`
- `cycleStartEpochMinutes` 定义为“该轮提醒触发时间(fireAt)按分钟向下取整后的 epoch minute”。
- 每次创建提醒前必须取消同 dedupe key 的旧提醒(upsert 语义)。
- 补偿提醒在同一 cycle 窗口内最多触发一次。
- 启动恢复时要同时参考 pending notification 和 outbox 状态,避免重复调度。
---
## Overlap Rule
- 同一分钟内多个提醒合并为一个 aggregate 弹窗。
- aggregate 弹窗默认操作作用于全部成员事件。
- aggregate 负载中必须包含 `aggregateIds` 以支持后续批处理。
---
## Backend Contract Reuse
- 复用现有接口:`PATCH /schedule-items/{item_id}`,请求体传 `{"status":"archived"}`
- 建议提供/改造 overlap 查询语义用于启动补偿:
- `start_at <= window_end`
- `end_at IS NULL OR end_at >= window_start`
- `status=active`
---
## Platform Notes
### Android
- 优先 full-screen intent;系统可能因策略降级为 heads-up/横幅。
- 声音和振动受通知通道及系统设置影响。
### iOS
- 支持动作按钮与本地提醒语义。
- 不保证 Android 式强制全屏闹钟弹窗;以锁屏/横幅提醒为主。