# App Push｜每日提醒推送（客户端 + 后端）｜Spec

> 阶段：高层规范（spec）
>
> 目标：实现“每日提醒”推送闭环：用户在首次进入 Onboarding 可选择每天推送次数（0～5，可跳过），并可在个人主页的“每日提醒”弹窗随时调整次数或关闭；客户端使用首次生成的 UUID 作为用户标识与后端关联；后端按用户配置定时下发推送。

---

## 1. 背景与动机（摘要）

当前客户端已有 Push 引导页（可跳过）与“每日提醒”设置入口，但尚未形成完整闭环：

- 需要一个稳定的匿名用户标识（UUID）将“提醒配置”与“推送 token”绑定到后端
- 需要后端能够按用户选择的每日次数（0～5）进行定时推送
- 本期不追求高级推送能力（富媒体、复杂分群、到达率分析等）

---

## 2. 目标（Goals）

- **G1：Onboarding 选择**：用户首次进入 Onboarding 可设置“每日提醒次数”（0～5），可跳过且不阻塞进入主功能。
- **G2：设置页可改可关**：用户可在个人主页的“每日提醒”弹窗调整次数（0～5）或关闭推送。
- **G3：UUID 关联**：客户端首次进入生成 `client_user_id`（UUID），用于与后端关联用户配置与推送 token（见 `spec_kit/Client User Identity/spec.md`）。
- **G4：推送闭环**：客户端拿到推送权限与 Push Token 后上报后端；后端存储并按计划每日推送。
- **G5：幂等与可恢复**：重复上报、token 轮换、用户反复开关不应导致重复推送或“僵尸任务”。
- **G6：多语言**：推送文案至少支持当前客户端语言体系（`zh-CN/en/es/pt/zh-TW`），或以安全默认语言回退。

---

## 3. 非目标（Non-goals）

- 不接入高级推送能力（富媒体、通知分类/动作按钮、复杂分群、A/B 实验、到达率看板等）。
- 不引入账号体系与“多设备同人合并”策略（未来可基于 `account_id` 扩展）。
- 不在本阶段做精细化内容策略（例如千人千面的复杂推荐推送）；仅保证“按次数定时发送”与文案安全合规。

---

## 4. 技术选型（高层）

### 4.1 客户端

- 继续使用 **Expo** 的 `expo-notifications`
- Push 权限申请：通过引导页解释后再触发系统弹窗（降低拒绝率）
- Token 获取：使用 Expo Push Token（后续计划阶段细化获取与上报时机）

### 4.2 后端

- 推送通道：**Expo Push Service**（与客户端 `expo-notifications` 配套）
- 定时任务：使用现有后端技术栈约定（Celery + Redis / 或等价调度器），每日按用户时区/策略触发推送

> 说明：若未来需要更高上限（自建 APNs/FCM 直连），可在后续模块升级，不影响本期 API 字段语义（`client_user_id`、`push_token`、偏好配置）的大框架。

---

## 5. 用户流程（User Flows）

### 5.1 首次进入（Onboarding）

1. App 首次启动 → 进入 Onboarding 流程
2. 在 Onboarding 某一页提供“每日提醒次数”选择（0～5）
   - 0 表示不接收每日提醒（等同关闭）
   - 可点击“跳过”（不设置/不打扰）
3. 若用户选择次数 > 0：
   - 展示 Push 引导说明页
   - 用户点击“立即开启”触发系统权限申请
4. 无论是否开启成功，均可进入主功能（不阻塞）

### 5.2 个人主页（每日提醒弹窗）

- 用户可：
  - 调整每日次数（0～5）
  - 开启/关闭提醒（关闭等价次数=0）
  - 若系统权限为 denied：提示用户前往系统设置开启（不做强制跳转要求，按平台能力实现）

### 5.3 后端推送执行

- 后端按用户配置与时区，在每日窗口内发送 \(N\) 次推送（\(N\in[0,5]\)）
- 若用户关闭或次数=0：不再推送

---

## 6. 数据与持久化（高层）

### 6.1 客户端本地存储（最小集合）

- `client_user_id`: string（UUID，稳定持久化；见用户标识 spec）
- `push.permission_state`: `unknown | granted | denied`（用于 UI 显示与引导）
- `daily_reminder.enabled`: boolean（可选；或由次数是否为 0 推导）
- `daily_reminder.times_per_day`: number（0～5）
- `daily_reminder.updated_at`: ISO string（可选，用于排障/幂等）

### 6.2 后端持久化（逻辑约束）

最小需要表达两类信息：

1) **设备/Token 绑定**
- `client_user_id`
- `platform`（ios/android）
- `push_token`（Expo Push Token）
- `app_id`（bundle id / package name）
- `env`（dev/prod）
- `last_seen_at`
- `is_active`

2) **用户推送偏好**
- `client_user_id`
- `enabled`
- `times_per_day`（0～5）
- `timezone`（建议 IANA，如 `Asia/Shanghai`；若拿不到则回退为服务器默认策略）
- `locale`（用于推送文案语言选择；可从客户端上报或推送时推断）
- `updated_at`

---

## 7. API 契约（高层）

> 说明：路由名可在 plan 阶段对齐现有服务结构；此处先固定“字段语义 + 幂等行为”。

### 7.1 注册/更新 Push Token（幂等）

- `POST /v1/push/register`
- 请求体（最小集，继承用户标识 spec）：
  - `client_user_id`: string（UUID）
  - `platform`: `"ios" | "android"`
  - `push_token`: string（Expo Push Token）
  - `app_id`: string
  - `env`: `"dev" | "prod"`
  - `device_meta`（可选）：`{ model, os_version, app_version, locale, timezone }`
- 行为：
  - 幂等：重复上报同一 token 不产生多条“有效绑定”
  - token 变更：同一 `client_user_id` 上报新 token 后，后端应更新“当前有效 token”

### 7.2 设置每日提醒偏好（幂等）

- `PUT /v1/push/preferences`
- 请求体：
  - `client_user_id`: string
  - `enabled`: boolean
  - `times_per_day`: number（0～5；若 enabled=false 则可强制视为 0）
  - `timezone`: string（可选）
  - `locale`: string（可选）
- 行为：
  - 幂等：相同配置重复提交不改变结果
  - `enabled=false` 或 `times_per_day=0`：必须停止后续推送（不再产生新的发送任务）

### 7.3 查询当前偏好（可选但建议）

- `GET /v1/push/preferences?client_user_id=...`
- 返回：
  - `enabled`
  - `times_per_day`
  - `timezone`
  - `locale`
  - `updated_at`

### 7.4 立即测试推送（仅 dev，可选）

- `POST /v1/push/test`
- 请求体：
  - `client_user_id`
  - `title` / `body`（可选）
- 用途：联调排障（token 绑定、证书/通道、Expo 配置）

---

## 8. 推送策略（高层）

### 8.1 发送次数与窗口

- 每日发送次数：0～5
- 建议定义“允许发送的时间窗口”（例如 09:00～21:00），避免深夜打扰
- 当 `times_per_day > 0` 时，在窗口内生成 \(N\) 个时间点并发送

> 时间点生成策略（均匀分布/固定时刻/随机抖动）在 plan 阶段确定；本期只要求“次数正确、用户可控、不会超发”。

### 8.2 文案来源与语言

- 文案可先采用后端配置的模板（按 `locale` 选择，失败回退 `en` 或 `zh-CN`）
- 若未来要接入推荐系统，可在后续迭代按用户画像生成更个性化文案（不在本期范围）

---

## 9. 边界场景与处理原则

- **用户跳过 Onboarding**：不强制开启；可在个人主页再次设置。
- **系统权限 denied**：客户端提示引导去系统设置；后端保存偏好但不保证可推送（无有效 token 时不发送）。
- **token 缺失/过期**：后端发送失败时应标记 token 为不可用，并等待客户端下次上报更新。
- **重复开关/改次数**：后端必须幂等更新，避免同一用户一天内重复排程导致超发。
- **环境隔离**：dev/prod 的 token 与配置必须隔离（`env + app_id` 维度）。
- **卸载/重装**：`client_user_id` 可能变化；视为新用户实例（符合匿名策略）。

---

## 10. 验收标准（Acceptance Criteria）

- 首次进入 Onboarding：
  - 用户可选择每日次数（0～5）或跳过
  - 选择后不阻塞进入主功能
- 个人主页每日提醒弹窗：
  - 可设置次数（0～5）与关闭
  - 权限 denied 时有明确提示
- 客户端：
  - `client_user_id` 稳定持久化
  - 在获得权限与 token 后能上报后端（幂等）
  - 修改偏好会同步到后端（幂等）
- 后端：
  - 能保存 token 与偏好
  - 能按用户配置每日推送（次数正确、不会超发）
  - token 失效可被识别并停止对失效 token 推送

---

## 11. 待确认问题清单（进入 plan/tasks 前必须确认）

1. **“0～5 次”的含义**：是否严格表示“每天发送 0～5 条通知”（看起来是），还是“提醒强度档位”？
2. **发送时间策略**：默认窗口与时间点生成方式（固定时刻 vs 均匀分布 vs 随机抖动）选哪一种？
3. **时区来源**：以客户端上报的 IANA 时区为准吗？若缺失回退到什么策略？
4. **推送文案**：本期文案是否完全由后端模板控制（便于随时调整），还是前端上报“文案 key/参数”由后端拼装？
5. **Push 允许发送的静默规则**：是否需要“勿扰时间段/睡眠模式”开关（例如 22:00～08:00 不推）？

