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

5.7 KiB
Raw Blame History

Calendar Reminder Alert Lifecycle Protocol

Version

  • Current: 1.0
  • Status: Draft

Goal

定义日程提醒弹窗在 Android/iOS 的统一行为语义,覆盖提醒触发、用户操作、离线补偿、归档与重装恢复,确保多端一致性和可恢复性。


Canonical Rules

  1. 提醒弹窗动作语义跨平台一致:archivesnooze10m
  2. 展示文案“取消”必须映射到内部动作 archive,并归档对应日程(status=archived)。
  3. 归档后的 UI 渲染必须灰色显示;不强制改写原始 metadata 颜色。
  4. 前端动作上报后端采用最终一致性:本地 outbox + 重放机制。
  5. 每轮提醒铃声时长默认 15 秒;若用户未点击通知,系统按 10 分钟节奏进入下一轮提醒。
  6. 点击系统通知必须定位到对应日程,并打开提醒详情页(展示日程详情 + 底部动作:归档/稍后提醒)。

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 + 10m
  • defaultRetry10m: 用户未点击通知时,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,取值仅允许 archivesnooze10m
  • fireTimeBucket: 触发时间按“分钟”向下取整后的 epoch minutemillisecondsSinceEpoch / 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 事件执行:

  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/横幅。
  • 声音和振动受通知通道及系统设置影响。
  • 本协议当前实现不依赖 full-screen intent 权限,采用标准高优先级通知 + 点击跳转提醒详情页。

iOS

  • 支持动作按钮与本地提醒语义。
  • 不保证 Android 式强制全屏闹钟弹窗;以锁屏/横幅提醒为主。