5.7 KiB
5.7 KiB
Calendar Reminder Alert Lifecycle Protocol
Version
- Current:
1.0 - Status: Draft
Goal
定义日程提醒弹窗在 Android/iOS 的统一行为语义,覆盖提醒触发、用户操作、离线补偿、归档与重装恢复,确保多端一致性和可恢复性。
Canonical Rules
- 提醒弹窗动作语义跨平台一致:
archive、snooze10m。 - 展示文案“取消”必须映射到内部动作
archive,并归档对应日程(status=archived)。 - 归档后的 UI 渲染必须灰色显示;不强制改写原始 metadata 颜色。
- 前端动作上报后端采用最终一致性:本地 outbox + 重放机制。
- 每轮提醒铃声时长默认 15 秒;若用户未点击通知,系统按 10 分钟节奏进入下一轮提醒。
- 点击系统通知必须定位到对应日程,并打开提醒详情页(展示日程详情 + 底部动作:归档/稍后提醒)。
Reminder Payload Contract
{
"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 + 10mdefaultRetry10m: 用户未点击通知时,15 秒铃声结束后自动进入now + 10m下一轮提醒(内部调度语义,不额外上报动作)
UI Label Mapping
- 按钮展示文案“取消”仅为 UI 文案,不作为协议动作值。
- “取消”按钮点击后的上报动作值必须为
archive。
Outbox Contract (Frontend)
{
"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- 当
remindAt <= now < endAt时,启动补偿提醒建议now + 5s - 每轮提醒后均进入 10 分钟节奏,直到超出
endAt - 截止条件:
fireAt > endAt时不再调度后续提醒
Bootstrap/reinstall compensation
启动重建时对每个 active 事件执行:
now < remindAt:按remindAt正常调度。remindAt <= now < endAt:立刻补偿提醒(建议+5s),然后进入 10 分钟节奏。now >= endAt:不再补发提醒;后续状态流转由上层业务策略决定(不在本协议强制范围内)。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_endend_at IS NULL OR end_at >= window_startstatus=active
Platform Notes
Android
- 优先 full-screen intent;系统可能因策略降级为 heads-up/横幅。
- 声音和振动受通知通道及系统设置影响。
- 本协议当前实现不依赖 full-screen intent 权限,采用标准高优先级通知 + 点击跳转提醒详情页。
iOS
- 支持动作按钮与本地提醒语义。
- 不保证 Android 式强制全屏闹钟弹窗;以锁屏/横幅提醒为主。