fix:小组件- PUSH
This commit is contained in:
169
spec_kit/App Push/plan.md
Normal file
169
spec_kit/App Push/plan.md
Normal file
@@ -0,0 +1,169 @@
|
||||
# App Push|每日提醒推送(客户端 + 后端)|Plan
|
||||
|
||||
> 阶段:技术计划(plan)
|
||||
>
|
||||
> 依据:`spec_kit/App Push/spec.md`
|
||||
|
||||
---
|
||||
|
||||
## 1. 总体思路
|
||||
|
||||
本期以 **Expo Push(Expo Push Token + Expo Push Service)** 为推送通道,实现“每日提醒”闭环:
|
||||
|
||||
- **客户端**:负责
|
||||
- 生成/持久化 `client_user_id`(UUID)
|
||||
- 引导用户设置每日次数(0~5,可跳过)
|
||||
- 申请通知权限、获取 Expo Push Token、并上报后端
|
||||
- 在个人主页的“每日提醒”弹窗修改次数或关闭,并同步到后端
|
||||
- 上报 `timezone`(IANA)与 `locale`(用于文案语言)
|
||||
- **后端**:负责
|
||||
- 保存 token 绑定与用户推送偏好
|
||||
- 每日按用户时区在 9:00~24:00 窗口内生成 \(N\in[0,5]\) 个随机抖动时间点
|
||||
- 在这些时间点向用户发送推送(使用后端 `@overview.md` 约定的个性化推荐模板/文案策略)
|
||||
- 幂等防重复(同一用户同一天同一序号只发一次)
|
||||
|
||||
---
|
||||
|
||||
## 2. 客户端设计
|
||||
|
||||
### 2.1 UUID(client_user_id)
|
||||
|
||||
- 复用 `spec_kit/Client User Identity/spec.md` 的结论:
|
||||
- 默认 UUID v4
|
||||
- 本地稳定持久化
|
||||
- 获取时机:
|
||||
- App 启动即确保已生成(便于后续任何上报都可带上)
|
||||
|
||||
### 2.2 Push 权限与 token 获取策略
|
||||
|
||||
- 在 Onboarding 的“每日提醒”设置页(或 push 引导页)进行权限申请:
|
||||
- 先说明 → 再弹系统权限框
|
||||
- token 获取与上报触发:
|
||||
- 权限 granted 后立即获取 token 并上报
|
||||
- 后续冷启动时可“补偿上报”(避免首轮失败导致后端缺 token)
|
||||
|
||||
### 2.3 每日提醒设置(0~5 次)
|
||||
|
||||
#### Onboarding
|
||||
|
||||
- 新增或复用一个 Onboarding 步骤:
|
||||
- 0~5 次选择
|
||||
- “跳过”按钮(跳过不阻塞)
|
||||
- 当用户选择 `times_per_day > 0`:
|
||||
- 引导用户开启系统通知权限
|
||||
- 成功与否都进入主功能
|
||||
|
||||
#### 个人主页弹窗(Daily Reminder)
|
||||
|
||||
- 现有弹窗:
|
||||
- 次数 +/-(限制 0~5)
|
||||
- 开关(关闭等价 `times_per_day=0`)
|
||||
- 权限 denied:给出清晰提示
|
||||
- 修复:移除/关闭“测试模式强制无权限”逻辑(该逻辑会导致永远表现为无法开启)
|
||||
|
||||
### 2.4 客户端与后端接口调用
|
||||
|
||||
- 新增 `client/src/services/pushApi.ts`(参考现有 `legalApi.ts` / `recoApi.ts` 的风格),并使用统一 HTTP 封装 `client/src/utils/http.ts`
|
||||
- 接口调用点:
|
||||
- register:拿到 token 后
|
||||
- preferences:用户完成选择或在个人主页修改后
|
||||
|
||||
---
|
||||
|
||||
## 3. 后端设计
|
||||
|
||||
### 3.1 数据模型(最小可用)
|
||||
|
||||
1) `push_tokens`
|
||||
- 唯一键:`env + app_id + push_token`
|
||||
- 字段:
|
||||
- `client_user_id`(UUID string)
|
||||
- `platform`(ios/android)
|
||||
- `push_token`
|
||||
- `app_id`
|
||||
- `env`
|
||||
- `is_active`
|
||||
- `last_seen_at`
|
||||
|
||||
2) `push_preferences`
|
||||
- 主键:`client_user_id`(或加 `env + app_id` 做隔离)
|
||||
- 字段:
|
||||
- `enabled`
|
||||
- `times_per_day`(0~5)
|
||||
- `timezone`(IANA,来自客户端)
|
||||
- `locale`(用于文案选择)
|
||||
- `updated_at`
|
||||
|
||||
3) `push_send_log`(幂等防重复)
|
||||
- 目的:保证“同一用户同一天第 k 条只发一次”
|
||||
- 唯一键:
|
||||
- `client_user_id + local_date + slot_index`(slot_index=1..times_per_day)
|
||||
- 字段:
|
||||
- `scheduled_at`(用户时区的时间点)
|
||||
- `sent_at`
|
||||
- `status`(scheduled/sent/failed)
|
||||
- `error`(可选)
|
||||
|
||||
> 注:不直接对数据库执行破坏性操作;通过迁移脚本落地表结构,执行迁移需你显式允许后才进行。
|
||||
|
||||
### 3.2 API
|
||||
|
||||
按 `spec_kit/App Push/spec.md`:
|
||||
|
||||
- `POST /v1/push/register`
|
||||
- `PUT /v1/push/preferences`
|
||||
- `GET /v1/push/preferences`
|
||||
- `POST /v1/push/test`(仅 dev)
|
||||
|
||||
### 3.3 推送发送器(Expo)
|
||||
|
||||
- 使用 `https://exp.host/--/api/v2/push/send`
|
||||
- 最小 payload:
|
||||
- `to`:Expo Push Token
|
||||
- `title/body`:来自个性化推荐模板
|
||||
- `data`:深链路参数(可选)
|
||||
- 失败处理:
|
||||
- 对 “DeviceNotRegistered”等不可恢复错误,将 token 标记为 inactive
|
||||
- 记录失败原因到 `push_send_log`
|
||||
|
||||
### 3.4 定时任务(每日 9~24,随机抖动)
|
||||
|
||||
策略:**每日“生成当天计划 + 分发 ETA 任务”**(推荐)
|
||||
|
||||
- 每日固定时刻(例如 UTC 00:10 或服务器本地时间某个点)执行一次“生成计划任务”:
|
||||
- 对每个 `enabled && times_per_day>0` 的用户:
|
||||
- 将用户时区当天的 9:00~24:00 转换为 UTC
|
||||
- 随机生成 \(N\) 个时间点:
|
||||
- 均匀切分窗口为 \(N\) 个区间
|
||||
- 每个区间内随机选一个时间(抖动)
|
||||
- 对每个 slot 写入 `push_send_log`(唯一键保证幂等)
|
||||
- 再投递 Celery ETA 任务,到点调用“发送 push”
|
||||
|
||||
这样可以避免 worker 长时间 sleep,也便于观察“今天会推哪些”。
|
||||
|
||||
---
|
||||
|
||||
## 4. 个性化推荐模板(后端 @overview.md)
|
||||
|
||||
推送文案生成使用后端推荐模块中 “Push 场景模板/策略”:
|
||||
|
||||
- 输入:`client_user_id`、(可选)用户画像/行为统计
|
||||
- 输出:`title/body`(多语言)
|
||||
- 风险控制:Push 场景默认“降个性化/降风险”,避免敏感/医疗类文案(遵守后端已定义的安全规则)
|
||||
|
||||
> plan 阶段仅明确“调用点与输入输出”,具体实现复用后端推荐模块现有能力,避免重复逻辑。
|
||||
|
||||
---
|
||||
|
||||
## 5. 测试与验收(落地检查清单)
|
||||
|
||||
- 客户端:
|
||||
- 首次进入可选择次数/跳过
|
||||
- 权限 granted 时能拿到 Expo token 并上报
|
||||
- 设置页改次数/关闭会调用 preferences 接口并持久化
|
||||
- 后端:
|
||||
- register/preferences 接口幂等可重试
|
||||
- test 接口可在 dev 环境立刻推送到真机
|
||||
- 每日计划生成不会重复排程(`push_send_log` 唯一键)
|
||||
- token 失效可自动停用
|
||||
|
||||
220
spec_kit/App Push/spec.md
Normal file
220
spec_kit/App Push/spec.md
Normal file
@@ -0,0 +1,220 @@
|
||||
# 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 不推)?
|
||||
|
||||
127
spec_kit/App Push/tasks.md
Normal file
127
spec_kit/App Push/tasks.md
Normal file
@@ -0,0 +1,127 @@
|
||||
# App Push|每日提醒推送(客户端 + 后端)|Tasks
|
||||
|
||||
> 阶段:任务清单(tasks)
|
||||
>
|
||||
> 依赖:`spec_kit/App Push/spec.md`、`spec_kit/App Push/plan.md`
|
||||
|
||||
---
|
||||
|
||||
## 0. 约束与共识(本期已确认)
|
||||
|
||||
- 每日次数:**0~5 条/天**
|
||||
- 发送窗口:**9:00~24:00(按客户端上报时区)**
|
||||
- 时间点:窗口内 **随机抖动**
|
||||
- 勿扰:**不做**
|
||||
- 文案:使用后端推荐模块的 **Push 场景个性化模板(降风险)**
|
||||
|
||||
---
|
||||
|
||||
## 1. 客户端(Expo RN)
|
||||
|
||||
### 1.1 UUID(client_user_id)
|
||||
|
||||
- [ ] 在 `client/src/storage/appStorage.ts`(或新模块)实现:
|
||||
- `getOrCreateClientUserId(): Promise<string>`
|
||||
- 首次生成 UUID 并持久化,后续复用
|
||||
|
||||
### 1.2 Push 权限与 token
|
||||
|
||||
- [ ] 接入获取 Expo Push Token 的封装(例如 `src/features/push/`):
|
||||
- 获取权限状态
|
||||
- 请求权限
|
||||
- 获取 Expo Push Token
|
||||
- [ ] 在 Onboarding / push 引导完成后:
|
||||
- granted 时:获取 token + 调 `register`
|
||||
- 未 granted:仅保存本地设置,不阻塞进入主功能
|
||||
|
||||
### 1.3 “每日提醒次数”入口
|
||||
|
||||
- [ ] Onboarding:新增/复用一个步骤页面
|
||||
- 选择 0~5 次(0 表示关闭)
|
||||
- 支持跳过
|
||||
- 完成后写本地存储,并调用后端 `preferences`
|
||||
- [ ] 个人主页弹窗:
|
||||
- 修复“测试模式强制无权限”的逻辑
|
||||
- 次数限制 0~5;关闭等价 0
|
||||
- denied 时提示去系统设置
|
||||
- 修改后写本地存储,并调用后端 `preferences`
|
||||
|
||||
### 1.4 接口封装
|
||||
|
||||
- [ ] 新增 `client/src/services/pushApi.ts`
|
||||
- `registerPushToken(...)`
|
||||
- `setPushPreferences(...)`
|
||||
- `getPushPreferences(...)`(可选)
|
||||
- [ ] 统一使用 `client/src/utils/http.ts` 的请求封装
|
||||
|
||||
### 1.5 联调开关与日志
|
||||
|
||||
- [ ] 开发环境输出必要日志(不打印敏感信息):
|
||||
- client_user_id 生成结果
|
||||
- 权限状态与 token 是否获取成功
|
||||
- register/preferences 的请求是否成功与错误原因
|
||||
|
||||
---
|
||||
|
||||
## 2. 后端(FastAPI)
|
||||
|
||||
### 2.1 数据模型与迁移
|
||||
|
||||
- [ ] 新增表(或等价模型):
|
||||
- `push_tokens`
|
||||
- `push_preferences`
|
||||
- `push_send_log`(幂等防重复)
|
||||
- [ ] 增加迁移脚本(Alembic 或项目现有迁移机制)
|
||||
|
||||
> 注意:不执行任何破坏性数据库操作;运行迁移前若涉及真实库,需要你明确回复“允许操作数据库”。
|
||||
|
||||
### 2.2 API
|
||||
|
||||
- [ ] `POST /v1/push/register`
|
||||
- 幂等:`env + app_id + push_token` 唯一
|
||||
- 更新 `client_user_id` 归属与 `last_seen_at`
|
||||
- [ ] `PUT /v1/push/preferences`
|
||||
- 校验 `times_per_day` ∈ [0,5]
|
||||
- `enabled=false` 或 `times_per_day=0` 时停止后续推送
|
||||
- [ ] `GET /v1/push/preferences`
|
||||
- 返回当前偏好与更新时间
|
||||
- [ ] `POST /v1/push/test`(仅 dev)
|
||||
- 立即向该用户发送一条测试推送(用于真机联调)
|
||||
|
||||
### 2.3 Expo 推送发送器
|
||||
|
||||
- [ ] 实现 `send_expo_push(token, title, body, data?)`
|
||||
- 处理 Expo 返回错误并对不可恢复错误停用 token
|
||||
- 记录发送结果到 `push_send_log`
|
||||
|
||||
### 2.4 推送文案(推荐模板)
|
||||
|
||||
- [ ] 在推送任务中调用后端推荐模块的 Push 场景模板:
|
||||
- 输入:`client_user_id`、语言/时区(可选)
|
||||
- 输出:`title/body`
|
||||
- 默认“降个性化/降风险”
|
||||
|
||||
---
|
||||
|
||||
## 3. 定时任务(每日计划 + ETA 发送)
|
||||
|
||||
- [ ] 每日“计划生成任务”
|
||||
- 扫描 `enabled && times_per_day>0` 用户
|
||||
- 按用户时区在 9:00~24:00 生成 N 个随机抖动时间点
|
||||
- 写入 `push_send_log`(唯一键保证幂等)
|
||||
- 投递 ETA 发送任务(或按项目现有任务系统实现)
|
||||
- [ ] ETA “发送任务”
|
||||
- 拉取当次发送所需 token/偏好
|
||||
- 生成文案(推荐模板)
|
||||
- 调用 Expo push 发送
|
||||
- 更新 `push_send_log` 状态
|
||||
|
||||
---
|
||||
|
||||
## 4. 验收与回归
|
||||
|
||||
- [ ] iOS 真机:权限申请、token 获取、test 推送可达
|
||||
- [ ] Android 真机:权限申请、token 获取、test 推送可达
|
||||
- [ ] 修改次数:后端计划生成正确(一天内不超发)
|
||||
- [ ] 关闭:后端停止后续推送(不再生成计划/不再发送)
|
||||
|
||||
24
spec_kit/Daily Widget Reco/overflow.md
Normal file
24
spec_kit/Daily Widget Reco/overflow.md
Normal file
@@ -0,0 +1,24 @@
|
||||
# Daily Widget Reco(补充说明 / Overflow)
|
||||
|
||||
## 1. 关键配置
|
||||
|
||||
- **App Group suiteName**:`group.com.damer.mindfulness`
|
||||
- **后端鉴权**:无
|
||||
- **多语言**:仅 `en / tc`
|
||||
|
||||
## 2. 共享存储 Key(App ↔ Widget)
|
||||
|
||||
- `widget.config.v1`
|
||||
- 字段:`schema_version=1`、`saved_at`、`apiBaseUrl`
|
||||
- `widget.userProfile.v1_2`
|
||||
- 字段:`schema_version=1`、`saved_at`、`user_profile`(结构对齐 `UserProfileV1_2`)
|
||||
- `widget.dailyReco.v1`
|
||||
- 字段:`schema_version=1`、`saved_at`、`day_key`(本地日 `YYYY-MM-DD`)、`lang`、`item{content_id,text}`、`source`
|
||||
|
||||
## 3. 更新策略(双通道)
|
||||
|
||||
- **Widget 主动拉取**:缓存过期时请求 `POST /v1/reco/widget`,写回 `widget.dailyReco.v1`,并使用 `TimelinePolicy.after` 设定次日刷新
|
||||
- **App 辅助刷新**:
|
||||
- 启动与回到前台时调用 `ensureDailyWidgetRecoUpToDate`
|
||||
- 成功写入缓存后调用 `WidgetCenter.reloadAllTimelines()`(系统仍可能延迟)
|
||||
|
||||
176
spec_kit/Daily Widget Reco/plan.md
Normal file
176
spec_kit/Daily Widget Reco/plan.md
Normal file
@@ -0,0 +1,176 @@
|
||||
# Daily Widget Reco(技术计划)
|
||||
|
||||
> 对应规范:`spec_kit/Daily Widget Reco/spec.md`
|
||||
>
|
||||
> 已确认输入(来自澄清):
|
||||
>
|
||||
> - 更新责任:**App 与 Widget 都需要**(双通道:Widget 主动拉取 + App 辅助刷新)
|
||||
> - App Group suiteName:`group.com.damer.mindfulness`
|
||||
> - 后端鉴权:不需要
|
||||
> - baseURL:由 **App 写入共享区** 提供给 Widget
|
||||
> - “每日”口径:用户本地时区
|
||||
> - 画像来源:App 写入共享区(Onboarding 完成后写入)
|
||||
|
||||
---
|
||||
|
||||
## 1. 计划目标
|
||||
|
||||
- 打通 `POST /v1/reco/widget`,让客户端与 Widget 都能获取每日推荐(Top1)。
|
||||
- 通过 App Group 共享存储,保证 **App 与 Widget 当天展示一致**。
|
||||
- 实现“每日更新(尽力而为)”:用户添加小组件后,即使不打开 App,也能依赖 WidgetKit Timeline 在每日范围内刷新。
|
||||
- 完整回退链路:今日缓存 → 最近缓存 → 兜底文案;任何失败不 crash、不阻塞主流程。
|
||||
|
||||
---
|
||||
|
||||
## 2. 总体方案(双通道更新)
|
||||
|
||||
### 2.1 核心结论
|
||||
|
||||
- **Widget 主动拉取**是满足“添加小组件后每日更新”的必要条件(仅靠 App 前台触发无法覆盖用户不打开 App 的情况)。
|
||||
- 同时保留 **App 辅助拉取与 reload**:
|
||||
- App 启动/进入前台/画像更新后可主动拉取并写缓存,提升即时性与一致性。
|
||||
- App 在写入共享缓存后调用 `WidgetCenter.reloadAllTimelines()`,加速 Widget 读取到新数据(系统仍可能延迟)。
|
||||
|
||||
### 2.2 数据流(高层)
|
||||
|
||||
- App(RN):
|
||||
- 写入共享配置(`apiBaseUrl` 等)→ 写入共享画像 →(可选)拉取 `widget` 推荐 → 写入共享每日缓存 → reload Widget
|
||||
- Widget(Swift):
|
||||
- 读取共享每日缓存,若过期则读取共享配置+画像并请求后端 → 写入共享缓存 → 输出 timeline
|
||||
|
||||
---
|
||||
|
||||
## 3. 客户端(React Native / Expo)改动点
|
||||
|
||||
### 3.1 新增 Widget 场景 API 封装
|
||||
|
||||
- 在 `client/src/services/recoApi.ts` 新增:
|
||||
- `fetchRecoWidget(req: RecoRequest): Promise<RecoEngineResult>`
|
||||
- Header `Accept-Language` 逻辑沿用 Feed(`zh* -> tc` else `en`)
|
||||
- path:`/v1/reco/widget`
|
||||
|
||||
### 3.2 共享存储:新增 App Group 写入能力
|
||||
|
||||
> 目标:RN 侧将必要数据写入 `UserDefaults(suiteName: "group.com.damer.mindfulness")`,供 Widget 读取。
|
||||
|
||||
建议实现路径(两种任选其一,按当前工程实际选型落地):
|
||||
|
||||
- 方案 A(推荐):引入一个“App Group UserDefaults 读写”的 RN 原生桥/插件(Swift/ObjC 模块 + JS 封装)。
|
||||
- 方案 B:若项目已存在可用能力(例如已接入 `react-native-shared-group-preferences` 或自研模块),直接复用并统一 key。
|
||||
|
||||
需要写入的共享 key(与 spec 对齐):
|
||||
|
||||
- `widget.config.v1`:包含 `apiBaseUrl`(以及未来灰度字段)
|
||||
- `widget.userProfile.v1_2`:用户画像快照(JSON)
|
||||
- `widget.dailyReco.v1`:每日推荐缓存(JSON)
|
||||
|
||||
### 3.3 App 侧写入与刷新触发时机
|
||||
|
||||
- **画像写入**:
|
||||
- Onboarding 完成后(或画像更新后)写入 `widget.userProfile.v1_2`
|
||||
- **配置写入**:
|
||||
- App 启动时(或 `API_BASE_URL` 计算完成后)写入 `widget.config.v1`
|
||||
- **主动拉取每日推荐(可选但建议)**:
|
||||
- App 启动或进入前台时:
|
||||
- 读取共享 `widget.dailyReco.v1`,若 `day_key` 不是今天则调用 `fetchRecoWidget` 拉取 Top1
|
||||
- 成功写入共享缓存,失败不影响 UI
|
||||
- **触发 Widget 刷新**:
|
||||
- 当 App 写入 `widget.dailyReco.v1` 成功后,调用 `WidgetCenter.reloadAllTimelines()`(通过原生桥触发)
|
||||
|
||||
> day_key 计算:以用户本地时区生成 `YYYY-MM-DD`(例如 `2026-02-03`)。
|
||||
|
||||
---
|
||||
|
||||
## 4. iOS Widget(Swift / WidgetKit)改动点
|
||||
|
||||
### 4.1 共享存储读取
|
||||
|
||||
- 统一使用:
|
||||
- `UserDefaults(suiteName: "group.com.damer.mindfulness")`
|
||||
- 读取并解析:
|
||||
- `widget.dailyReco.v1`
|
||||
- `widget.userProfile.v1_2`
|
||||
- `widget.config.v1`
|
||||
|
||||
解析失败必须容错:视为缺失,走回退策略。
|
||||
|
||||
### 4.2 Timeline 刷新策略(每日)
|
||||
|
||||
- `getTimeline` 流程:
|
||||
1. 读 `widget.dailyReco.v1`
|
||||
2. 若 `day_key` 为今天且文案存在 → 直接出 timeline
|
||||
3. 否则尝试网络拉取(需要 config + userProfile 均存在):
|
||||
- 请求 `POST {apiBaseUrl}/v1/reco/widget`
|
||||
- `Accept-Language`:从缓存 lang 或系统语言映射到 `en/tc`(优先与 App 一致)
|
||||
- `k=1`
|
||||
4. 成功:写入共享缓存并出 timeline
|
||||
5. 失败:用“最近缓存/兜底文案”出 timeline
|
||||
|
||||
- `policy`:
|
||||
- `TimelinePolicy.after(nextRefreshDate)`
|
||||
- `nextRefreshDate`:下一天本地时间的一个随机刷新点(例如 00:10~01:00 随机),减少集中刷新与被系统限流概率
|
||||
|
||||
### 4.3 网络实现注意事项
|
||||
|
||||
- 使用 `URLSession`,超时建议 8~12 秒(与 RN 对齐即可)
|
||||
- 无鉴权
|
||||
- 失败不抛出 crash:打印有限日志(或仅在 Debug)
|
||||
|
||||
---
|
||||
|
||||
## 5. 共享数据结构与一致性规则
|
||||
|
||||
### 5.1 `widget.dailyReco.v1`(v1)
|
||||
|
||||
按 `spec.md` 定义字段,重点:
|
||||
|
||||
- `day_key`:本地日 key(用户时区)
|
||||
- `lang`:`en|tc`
|
||||
- `item.content_id + item.text`:Widget 展示必须字段
|
||||
- `source`:标记由 `app` 还是 `widget` 写入(便于排查)
|
||||
|
||||
### 5.2 一致性策略(同一天同一条)
|
||||
|
||||
规则:
|
||||
|
||||
- Widget 展示以 `widget.dailyReco.v1` 为准(共享缓存是单一事实来源)。
|
||||
- App 若拉取到新结果,应覆盖写入共享缓存,并触发 reload,保证 Widget 同步。
|
||||
- 若当天出现“不同条”风险(并发写入):
|
||||
- 以最后写入者为准(last-write-wins)
|
||||
- 通过 `saved_at` + `source` 辅助排查
|
||||
|
||||
---
|
||||
|
||||
## 6. 回退策略(必须实现)
|
||||
|
||||
- 今日缓存存在且有效 → 展示
|
||||
- 今日缓存无效但有最近缓存 → 展示最近缓存
|
||||
- 无任何缓存 → 展示兜底安全文案(写死)
|
||||
|
||||
同时:
|
||||
|
||||
- 后端返回 `items=[]` 视为失败
|
||||
- JSON 解析失败视为无缓存
|
||||
|
||||
---
|
||||
|
||||
## 7. 实施顺序(建议)
|
||||
|
||||
1. **补齐客户端 API**:新增 `fetchRecoWidget`,并在本地可通过 Postman/或 RN 调用验证返回。
|
||||
2. **落地 App Group 共享读写(RN → iOS)**:先能写入/读取一个测试 key,确保 suiteName 正确。
|
||||
3. **写入共享配置与画像**:保证 Widget 侧具备发起请求所需输入。
|
||||
4. **Widget 侧读取缓存并展示**:先用共享缓存驱动 UI(不接网络也能跑通)。
|
||||
5. **Widget 侧网络拉取 + 写回缓存**:实现每日更新主链路。
|
||||
6. **App 辅助刷新**:App 前台拉取(可选)+ 写缓存 + reloadAllTimelines。
|
||||
|
||||
---
|
||||
|
||||
## 8. 验收与测试建议
|
||||
|
||||
- **联调验收**:
|
||||
- 手动清空共享缓存 → 添加 Widget → 观察首次拉取并展示(或先展示兜底再更新)
|
||||
- 修改设备日期到次日(或模拟 day_key 变化)→ 触发 `getTimeline`,确认会重新拉取
|
||||
- 断网 → 应展示最近缓存/兜底文案,不崩溃
|
||||
- **一致性验收**:
|
||||
- App 拉取后写入共享缓存,Widget 在 reload 后显示同一条
|
||||
|
||||
197
spec_kit/Daily Widget Reco/spec.md
Normal file
197
spec_kit/Daily Widget Reco/spec.md
Normal file
@@ -0,0 +1,197 @@
|
||||
# Daily Widget Reco(高层规范)
|
||||
|
||||
> 阶段:高层规范(spec)
|
||||
>
|
||||
> 背景引用:
|
||||
>
|
||||
> - `spec_kit/Personalized Reco/overview.md`:后端已实现可复用推荐流程(Feed / Push / Widget)
|
||||
> - `spec_kit/iOS Widget/spec.md`:已完成 WidgetKit Extension V1(写死文案)
|
||||
>
|
||||
> 本需求聚焦:**iOS 小组件“每日推荐”与 App 数据共通**,以及**客户端与后端推荐接口的串联**。
|
||||
|
||||
---
|
||||
|
||||
## 1. 背景与动机
|
||||
|
||||
后端已实现推荐接口(含 `widget` 场景),但客户端目前只接入了 Feed 推荐(`POST /v1/reco/feed`),iOS 小组件仍是 V1 写死文案。
|
||||
|
||||
现在需要实现:
|
||||
|
||||
- iOS 小组件展示“每日推荐”(每天一条),从后端获取。
|
||||
- App 与 Widget 的推荐内容数据共通(同一天展示同一条),并支持在用户添加小组件后开始每日更新。
|
||||
- 推荐链路具备可回退能力:网络失败、画像缺失、接口异常时不崩溃、仍有可展示内容。
|
||||
|
||||
---
|
||||
|
||||
## 2. 目标(Goals)
|
||||
|
||||
- **后端串联**:客户端与 iOS WidgetKit 能稳定调用后端 `widget` 场景推荐接口获取每日文案。
|
||||
- **数据共通**:App 与 Widget 通过 **App Group 共享存储**读写同一份“每日推荐缓存”,保证展示一致。
|
||||
- **每日更新(尽力而为)**:
|
||||
- 小组件添加到桌面后,系统开始调度 Widget Timeline;Widget 能在**每天**刷新一次内容(受系统限制,允许延迟)。
|
||||
- App 在合适时机触发更新与 `WidgetCenter.reload`,提高准时性(但不承诺“严格准点”)。
|
||||
- **安全与回退**:出现失败时按回退梯度展示(今日缓存 → 最近一次缓存 → 兜底安全文案),且不阻塞 App 主流程。
|
||||
|
||||
---
|
||||
|
||||
## 3. 非目标(Non-goals)
|
||||
|
||||
- 不做 Android Widget。
|
||||
- 不在本需求内改动后端推荐算法逻辑与 DB(后端已实现接口)。
|
||||
- 不承诺高频刷新(例如分钟级),遵循 iOS WidgetKit 刷新限制。
|
||||
- 不在本需求内实现复杂的“用户自定义小组件配置”(例如选择主题/类型);仅做“每日一句”。
|
||||
|
||||
---
|
||||
|
||||
## 4. 范围与交付物
|
||||
|
||||
### 4.1 范围
|
||||
|
||||
- 客户端(React Native / Expo):
|
||||
- 增加 `POST /v1/reco/widget` 的请求封装(与 Feed 形态一致)。
|
||||
- 负责把“用户画像快照/必要上下文”写入 App Group(供 Widget 拉取/请求)。
|
||||
- 在合适时机触发“写缓存 + 通知 Widget 刷新”。
|
||||
- iOS Widget(Swift / WidgetKit Extension):
|
||||
- 从 App Group 读取“每日推荐缓存”并展示。
|
||||
- 在需要时(缓存过期/缺失)发起网络请求从后端拉取,并写回缓存(或由 App 拉取并写回,见第 8 节决策)。
|
||||
- 共享数据契约:
|
||||
- 定义 App 与 Widget 共用的数据结构(版本化、可演进)。
|
||||
|
||||
### 4.2 交付物
|
||||
|
||||
- 客户端:推荐接口封装 + 共享存储写入 + 刷新触发。
|
||||
- iOS:Widget Timeline Provider 支持每日推荐(读取共享缓存/必要时拉取)。
|
||||
- 文档:本 `spec.md`。
|
||||
|
||||
---
|
||||
|
||||
## 5. 术语与关键约束
|
||||
|
||||
- **Daily Reco(每日推荐)**:Widget 场景 Top1 文案(每天一条)。
|
||||
- **App Group**:iOS 用于 App 与 Extension 共享数据的机制(建议使用共享 `UserDefaults(suiteName:)`)。
|
||||
- **Widget 刷新限制**:刷新由系统调度,**不可保证准点**;需要设计缓存与兜底展示。
|
||||
|
||||
---
|
||||
|
||||
## 6. 后端接口契约(Client ↔ Server)
|
||||
|
||||
### 6.1 接口
|
||||
|
||||
- `POST /v1/reco/widget`
|
||||
|
||||
### 6.2 Header
|
||||
|
||||
- `Accept-Language`:
|
||||
- 客户端沿用既有策略:当前仅区分 `en/tc`
|
||||
- 规则:`i18n.language` 以 `zh` 开头 → `tc`,否则 `en`
|
||||
|
||||
### 6.3 Request Body(与 Feed 一致)
|
||||
|
||||
- `k?: number`(Widget 默认 1;建议不传或显式传 `1`)
|
||||
- `user_profile: UserProfileV1_2`(必填)
|
||||
- `already_recommended_ids?: (string|number)[]`(可选,默认 `[]`)
|
||||
- `touched_or_viewed_ids?: (string|number)[]`(可选,默认 `[]`)
|
||||
- `now?: string`(ISO8601,可选,用于测试/确定性)
|
||||
|
||||
### 6.4 Response Body
|
||||
|
||||
- `items: RecommendedItem[]`(Widget 预期取 `items[0]`;允许为空)
|
||||
- `meta: Record<string, unknown>`(用于可观测/调参;客户端可选择性落地)
|
||||
|
||||
---
|
||||
|
||||
## 7. 共享数据契约(App ↔ Widget)
|
||||
|
||||
> 原则:结构**版本化**、字段可缺省、读取端容错;避免把整个业务状态塞进共享区。
|
||||
|
||||
### 7.1 共享 Key(建议)
|
||||
|
||||
- `widget.dailyReco.v1`:每日推荐缓存(Widget 展示的主数据)
|
||||
- `widget.userProfile.v1_2`:用户画像快照(供 Widget 发起请求使用)
|
||||
- `widget.recoHistory.v1`:去重/频控所需的最小历史(可选,控制大小)
|
||||
- `widget.config.v1`:Widget 端需要的配置(例如 `apiBaseUrl`、灰度开关等)
|
||||
|
||||
### 7.2 `widget.dailyReco.v1` 数据结构(JSON)
|
||||
|
||||
- `schema_version: 1`
|
||||
- `saved_at: string`(ISO8601)
|
||||
- `day_key: string`(本地日维度 key,例如 `2026-02-03`;用于“今天是否已刷新”的判断)
|
||||
- `lang: "en" | "tc"`
|
||||
- `item`(可为空):
|
||||
- `content_id: number`
|
||||
- `text: string`
|
||||
- `final_score?: number`
|
||||
- `fallback_level_final?: number`
|
||||
- `meta?: Record<string, unknown>`(可选,用于排查;注意体积)
|
||||
- `source: "app" | "widget"`(可选:谁写入的)
|
||||
|
||||
### 7.3 读写规则
|
||||
|
||||
- Widget 展示优先级:
|
||||
1. `day_key` 为今天且 `item.text` 非空 → 直接展示
|
||||
2. 否则展示最近一次缓存(若存在)
|
||||
3. 否则展示兜底安全文案(写死)
|
||||
- 写入要求:
|
||||
- 写入必须原子化(一次性写完整 JSON),避免部分字段缺失导致解析失败
|
||||
- 解析失败时当作“无缓存”,走兜底策略
|
||||
|
||||
---
|
||||
|
||||
## 8. 每日更新策略(刷新触发与责任划分)
|
||||
|
||||
### 8.1 关键结论
|
||||
|
||||
为满足“用户添加小组件后可每日更新”,需要依赖 WidgetKit 自身的 Timeline 调度。仅靠 App 在前台触发更新,无法保证用户不打开 App 时也能更新。
|
||||
|
||||
### 8.2 推荐实现(V1.5:Widget 主动拉取)
|
||||
|
||||
- **Widget Timeline Provider**:
|
||||
- 每次 `getTimeline`:
|
||||
- 先读 `widget.dailyReco.v1`
|
||||
- 若 `day_key` 不是今天 → 尝试从后端请求 `POST /v1/reco/widget` 获取今日内容
|
||||
- 成功后写入 `widget.dailyReco.v1` 并返回 timeline
|
||||
- 失败则返回“最近一次缓存/兜底文案”的 timeline
|
||||
- Timeline 刷新策略:
|
||||
- `policy = .after(nextRefreshDate)`,`nextRefreshDate` 设为“下一天的本地 00:10~01:00 之间随机一个时间”(减少集中刷新)
|
||||
|
||||
- **App(RN)**:
|
||||
- 在 Onboarding 完成或画像更新时,把 `widget.userProfile.v1_2` 写入共享区
|
||||
- 当 App 成功拉取到推荐(可选)时,也可写入 `widget.dailyReco.v1` 并触发 `WidgetCenter.reloadAllTimelines()`,提升即时性
|
||||
|
||||
### 8.3 备选实现(App 主动拉取 + Widget 仅展示)
|
||||
|
||||
若团队希望 Widget 端完全不发网络请求,则:
|
||||
|
||||
- App 负责在“启动/前台/每天首次打开”等时机拉取 `POST /v1/reco/widget` 并写入共享缓存
|
||||
- Widget 仅读取缓存展示
|
||||
|
||||
缺点:用户不打开 App 时,小组件可能长期不更新(不满足“每日更新”的强诉求)
|
||||
|
||||
---
|
||||
|
||||
## 9. 失败回退与稳定性
|
||||
|
||||
- **网络失败**:使用最近一次缓存;若无缓存,展示兜底安全文案。
|
||||
- **接口返回 items 为空**:视为失败,走同样回退。
|
||||
- **画像缺失**:
|
||||
- App 侧应尽量在首次进入完成问卷后写入 `widget.userProfile.v1_2`
|
||||
- 若仍缺失,Widget 端不崩溃,直接展示缓存/兜底文案;并在下一次 timeline 继续尝试
|
||||
- **数据损坏/解析失败**:清空本次读取结果,走兜底策略(不可 crash)。
|
||||
|
||||
---
|
||||
|
||||
## 10. 验收标准(Acceptance Criteria)
|
||||
|
||||
- **接口串联**:客户端/Widget 能成功请求 `POST /v1/reco/widget` 并解析返回。
|
||||
- **数据共通**:同一天内,App 与 Widget 展示的“每日推荐”一致(以共享缓存为准)。
|
||||
- **每日更新**:用户把小组件添加到桌面后,小组件在后续每天能更新到新的推荐(允许系统延迟,但应在一天内更新)。
|
||||
- **可用性**:断网/后端不可用/返回异常时,Widget 仍能展示最近缓存或兜底文案,App 不崩溃。
|
||||
|
||||
---
|
||||
|
||||
## 11. 风险与注意事项
|
||||
|
||||
- iOS Widget 刷新由系统调度,无法承诺“严格 00:00 更新”;需接受“尽力而为 + 缓存兜底”。
|
||||
- App Group 的 suiteName / entitlements 配置错误会导致 Widget 读不到共享数据;必须在实现阶段统一校验。
|
||||
- Widget 端直连后端需要维护 baseURL 与环境切换策略(dev/pro);建议通过 `widget.config.v1` 注入,避免写死。
|
||||
|
||||
144
spec_kit/Daily Widget Reco/tasks.md
Normal file
144
spec_kit/Daily Widget Reco/tasks.md
Normal file
@@ -0,0 +1,144 @@
|
||||
# Daily Widget Reco(任务清单)
|
||||
|
||||
> 对应计划:`spec_kit/Daily Widget Reco/plan.md`
|
||||
>
|
||||
> 标记规则:
|
||||
>
|
||||
> - 执行完成后将对应项从 `- [ ]` 改为 `- [x]`,并把“状态”改为 **已完成**
|
||||
> - 进行中改为 **进行中**
|
||||
> - 阻塞写明原因与解除条件
|
||||
|
||||
---
|
||||
|
||||
## 0. 清单状态说明
|
||||
|
||||
- **状态**:未开始 / 进行中 / 已完成 / 阻塞
|
||||
- **阻塞**:必须写明阻塞点与解除条件
|
||||
|
||||
---
|
||||
|
||||
## 1. 客户端:补齐 Widget 场景 API(RN)
|
||||
|
||||
- [ ] **新增 `fetchRecoWidget`(`POST /v1/reco/widget`)**
|
||||
- **状态**:未开始
|
||||
- **文件**:`client/src/services/recoApi.ts`
|
||||
- **要求**:
|
||||
- Header `Accept-Language` 逻辑沿用 `fetchRecoFeed`
|
||||
- body 结构沿用 `RecoRequest`
|
||||
- timeout 建议 12s
|
||||
- **验收**:在 App 内调用可拿到 `items[0].text`(允许为空但不报错)
|
||||
|
||||
---
|
||||
|
||||
## 2. iOS 原生能力:App Group 共享读写(suiteName 已确认)
|
||||
|
||||
> suiteName:`group.com.damer.mindfulness`
|
||||
|
||||
- [ ] **新增 App Group 共享存储原生模块(RN Bridge)**
|
||||
- **状态**:未开始
|
||||
- **目标**:RN 可写/读共享 `UserDefaults(suiteName:)`
|
||||
- **要求**:
|
||||
- 支持 `setString(key, value)` / `getString(key)`(最小闭环)
|
||||
- 以 JSON 字符串形式存储(上层 JS 自行 `JSON.stringify/parse`)
|
||||
- **验收**:
|
||||
- RN 写入后,Widget 侧能读到同一 key
|
||||
- 解析失败不 crash(返回 `null`/空字符串均可,按上层回退)
|
||||
|
||||
- [ ] **新增触发 Widget 刷新的原生方法(可选但建议)**
|
||||
- **状态**:未开始
|
||||
- **目标**:App 写入缓存后触发 `WidgetCenter.reloadAllTimelines()`
|
||||
- **验收**:App 调用后,Widget 在合理时间内刷新显示新内容(允许系统延迟)
|
||||
|
||||
---
|
||||
|
||||
## 3. 客户端:写入共享配置/画像/每日缓存(App ↔ Widget 共通)
|
||||
|
||||
- [ ] **写入共享配置 `widget.config.v1`(包含 `apiBaseUrl`)**
|
||||
- **状态**:未开始
|
||||
- **要求**:
|
||||
- App 启动后(或 baseURL 计算完成后)写入
|
||||
- JSON 字段最少包含:`schema_version`、`apiBaseUrl`、`saved_at`
|
||||
- **验收**:Widget 侧可读到正确 baseURL(dev/pro 随 App 切换)
|
||||
|
||||
- [ ] **写入共享画像 `widget.userProfile.v1_2`(Onboarding 完成后)**
|
||||
- **状态**:未开始
|
||||
- **依赖**:客户端已能获取/持久化用户画像(本地存储已有 `user.profileScoring`)
|
||||
- **要求**:
|
||||
- 结构以现有 `UserProfileV1_2` 输出为准
|
||||
- JSON 字段建议包含:`schema_version`、`saved_at`、`user_profile`
|
||||
- **验收**:Widget 侧可读到 `user_profile`,且可用于请求后端
|
||||
|
||||
- [ ] **实现 App 前台“可选主动拉取”并写入 `widget.dailyReco.v1`**
|
||||
- **状态**:未开始
|
||||
- **要求**:
|
||||
- 判断 `day_key`(用户时区 `YYYY-MM-DD`)不是今天时才拉取
|
||||
- 调用 `fetchRecoWidget(k=1)`,成功写入缓存
|
||||
- 失败不影响 UI,不阻塞主流程
|
||||
- **验收**:App 打开后能把今日推荐写入共享缓存,Widget reload 后展示一致
|
||||
|
||||
---
|
||||
|
||||
## 4. iOS Widget:读取共享缓存并展示(先不接网络也能跑通)
|
||||
|
||||
- [ ] **Widget 读取 `widget.dailyReco.v1` 并展示**
|
||||
- **状态**:未开始
|
||||
- **要求**:
|
||||
- JSON 解析失败容错:视为无缓存
|
||||
- 展示优先级:今日缓存 → 最近缓存 → 兜底文案
|
||||
- **验收**:手动写入共享缓存后,Widget 立即能展示对应文案(或在下一次 reload 展示)
|
||||
|
||||
---
|
||||
|
||||
## 5. iOS Widget:网络拉取每日推荐 + Timeline 每日刷新(核心)
|
||||
|
||||
- [ ] **Widget 在缓存过期时请求 `POST /v1/reco/widget` 并写回缓存**
|
||||
- **状态**:未开始
|
||||
- **依赖**:可读到 `widget.config.v1`(apiBaseUrl)与 `widget.userProfile.v1_2`(user_profile)
|
||||
- **要求**:
|
||||
- 无鉴权
|
||||
- 超时建议 8~12s
|
||||
- `Accept-Language` 与 App 保持一致(`en/tc`)
|
||||
- `items=[]` 视为失败,走回退
|
||||
- **验收**:清空今日缓存后,Widget 能在一次 timeline 请求内拉取并展示今日推荐(允许先兜底再更新)
|
||||
|
||||
- [ ] **实现每日刷新调度(TimelinePolicy)**
|
||||
- **状态**:未开始
|
||||
- **要求**:
|
||||
- `nextRefreshDate` 设置为“下一天本地时间 00:10~01:00 随机”之一
|
||||
- 不追求严格准点,但确保每日范围内能更新
|
||||
- **验收**:通过修改系统日期/模拟 day_key 变化,可观察到会进入“过期→重新拉取”逻辑
|
||||
|
||||
---
|
||||
|
||||
## 6. 联调与验收验证(必须)
|
||||
|
||||
- [ ] **联调:添加小组件后,未打开 App 也能每日更新(尽力而为)**
|
||||
- **状态**:未开始
|
||||
- **步骤**:
|
||||
- 安装带 Widget 的开发包
|
||||
- 在桌面添加 Widget
|
||||
- 清空今日缓存并等待 Widget 刷新(或手动触发 reload)
|
||||
- **验收**:Widget 能从后端拉取并展示;隔天能更新到新内容(允许系统延迟)
|
||||
|
||||
- [ ] **回退验证:断网/后端不可用/返回异常**
|
||||
- **状态**:未开始
|
||||
- **验收**:
|
||||
- Widget 不崩溃
|
||||
- 展示最近缓存或兜底文案
|
||||
|
||||
- [ ] **一致性验证:App 与 Widget 当天展示一致**
|
||||
- **状态**:未开始
|
||||
- **验收**:App 主动拉取并写入缓存后,Widget reload 后展示同一条 `content_id/text`
|
||||
|
||||
---
|
||||
|
||||
## 7. 文档与总览更新
|
||||
|
||||
- [ ] **补充实现说明(可放入客户端 README 或本需求 overflow)**
|
||||
- **状态**:未开始
|
||||
- **要求**:记录 suiteName、共享 key、如何本地验证
|
||||
|
||||
- [ ] **更新 `spec_kit/overview.md`**
|
||||
- **状态**:未开始
|
||||
- **要求**:在“Spec Kit Overview”中新增本需求摘要(目标/产物/已完成编码变更)
|
||||
|
||||
122
spec_kit/Policy Links/plan.md
Normal file
122
spec_kit/Policy Links/plan.md
Normal file
@@ -0,0 +1,122 @@
|
||||
# Policy Links(技术计划)
|
||||
|
||||
## 1. 目标与原则
|
||||
|
||||
**目标**:让客户端所有「隐私协议 / 使用协议」入口(至少包含:首次同意页 + 个人主页弹窗)统一通过后端接口获取链接,并支持按设备语言分发,缺省回退 EN。
|
||||
|
||||
**原则**:
|
||||
|
||||
- 客户端不写死协议 URL(避免发版更新)
|
||||
- 请求失败不阻塞主流程(尤其是首次同意页)
|
||||
- 语言选择策略一致、可解释、可排查(返回 `resolvedLang`)
|
||||
|
||||
## 2. 接口与契约
|
||||
|
||||
### 2.1 HTTP 接口
|
||||
|
||||
- **方法与路径**:`GET /v1/legal/links`
|
||||
- **请求头**:
|
||||
- `Accept-Language`: `en` / `tc`(当前仅支持 EN / TC;客户端可按 i18n/设备语言映射为其中之一)
|
||||
- **响应**(JSON):
|
||||
- `privacyPolicyUrl: string`
|
||||
- `termsOfUseUrl: string`
|
||||
- `resolvedLang: "en" | "tc"`
|
||||
|
||||
### 2.2 语言回退(服务端决策)
|
||||
|
||||
- 若 `Accept-Language` 缺失/无法解析:回退 `en`
|
||||
- 若目标语言的链接未配置:回退 `en`
|
||||
- `resolvedLang` 表示服务端最终命中的语言(用于排查)
|
||||
|
||||
## 3. 后端实现方案(FastAPI)
|
||||
|
||||
### 3.1 配置来源
|
||||
|
||||
采用环境变量(Pydantic Settings)承载各语言链接配置:
|
||||
|
||||
- 推荐配置(EN):
|
||||
- `LEGAL_PRIVACY_URL_EN`
|
||||
- `LEGAL_TERMS_URL_EN`
|
||||
- 可选:
|
||||
- `LEGAL_PRIVACY_URL_TC` / `LEGAL_TERMS_URL_TC`
|
||||
|
||||
> 补充:若未配置 `LEGAL_*`,后端将使用**内置协议内容页**作为兜底,保证客户端点击后“必有内容”。
|
||||
|
||||
### 3.2 路由与业务逻辑
|
||||
|
||||
- 新增 `v1/legal` 路由模块
|
||||
- 读取 `Accept-Language` 做轻量解析(只关心 en/tc,其他回退 en)
|
||||
- 根据解析结果选择 URL;如缺失则回退到 EN URL
|
||||
|
||||
### 3.3 内置协议内容页(兜底,保证有内容)
|
||||
|
||||
- **页面接口**:
|
||||
- `GET /v1/legal/privacy`:隐私协议(HTML)
|
||||
- `GET /v1/legal/terms`:使用协议(HTML)
|
||||
- **内容来源**:
|
||||
- 直接使用仓库文档:`设计说明文档/隐私协议.md`、`设计说明文档/用户使用协议.md`
|
||||
- **语言策略**:
|
||||
- 仍按 `Accept-Language` 解析 `en/tc`;若文档缺少 tc 段落则回退 en
|
||||
- **链接下发回退**:
|
||||
- 当 `LEGAL_*` 未配置(或显式走内置)时,`GET /v1/legal/links` 返回当前服务域名下的上述内置页面绝对 URL
|
||||
|
||||
### 3.4 兼容性与安全
|
||||
|
||||
- 要求链接为 HTTPS(推荐);返回前做基本 URL 校验(通过响应模型约束)
|
||||
- 不引入数据库变更
|
||||
|
||||
### 3.5 后端测试(建议)
|
||||
|
||||
- 单测覆盖:
|
||||
- `Accept-Language` 缺失 → `resolvedLang=en`
|
||||
- `tc` 命中/缺失回退
|
||||
- `zh-hant/zh-hk/zh-tw` 等输入能解析为 `tc`
|
||||
|
||||
## 4. 客户端实现方案(React Native + Expo)
|
||||
|
||||
### 4.1 统一 service
|
||||
|
||||
- 新增 `src/services/legalApi.ts`:
|
||||
- 统一拼接 `API_BASE_URL + /v1/legal/links`
|
||||
- 统一设置 `Accept-Language`
|
||||
- 统一超时与错误处理(抛错由调用方兜底)
|
||||
|
||||
### 4.2 接入点
|
||||
|
||||
- **首次同意页**(`app/(splash)/splash.tsx`):
|
||||
- 页面展示时拉取链接
|
||||
- 拉取失败:不崩溃、不阻塞「同意并继续」
|
||||
- 点击协议入口:使用 `expo-web-browser` 打开下发链接
|
||||
- **个人主页弹窗**(`components/home/ProfileModal.tsx`):
|
||||
- 弹窗打开时拉取链接
|
||||
- 点击「隐私协议/使用协议」时打开下发链接(不再 `toastTodo`)
|
||||
|
||||
### 4.3 语言映射策略(客户端)
|
||||
|
||||
- 将客户端当前语言映射到 `en / tc` 二选一:
|
||||
- 任意 `zh*` → `tc`
|
||||
- 其他语言 → `en`
|
||||
|
||||
## 5. 可观测与排障
|
||||
|
||||
- 开发环境打印:
|
||||
- 请求地址、请求头(含 `Accept-Language`)
|
||||
- 拉取失败原因(仅 dev)
|
||||
- 线上环境:
|
||||
- 不打印用户相关敏感信息(本接口仅链接配置,风险较低,但仍应克制日志)
|
||||
|
||||
## 6. 发布与回滚策略
|
||||
|
||||
- **灰度**:dev 环境可先不配 `LEGAL_*`,直接验证内置页面能打开;再逐步切到外部托管链接
|
||||
- **补齐多语言**:逐步补齐 `zh-CN` / `zh-TW` 链接配置
|
||||
- **回滚**:
|
||||
- 后端:回滚到仅 EN 配置仍可运行(客户端自动回退)
|
||||
- 客户端:即使接口失败也不阻塞主流程(保持可用)
|
||||
|
||||
## 7. 验收映射(对应 spec)
|
||||
|
||||
- 首次同意页 + 个人主页弹窗均从 API 获取链接
|
||||
- 设备语言为 EN/TC 时分别打开对应链接
|
||||
- 不支持语言回退 EN
|
||||
- 后端不可达时 App 不崩溃,且同意流程可继续
|
||||
|
||||
127
spec_kit/Policy Links/spec.md
Normal file
127
spec_kit/Policy Links/spec.md
Normal file
@@ -0,0 +1,127 @@
|
||||
# Policy Links(高层规范)
|
||||
|
||||
## 1. 背景与目标
|
||||
|
||||
当前客户端首次进入的同意页(Splash Consent)需要在底部提供「隐私协议」「用户使用协议」的查看入口。为避免协议链接写死在客户端、并支持多语言分发,本需求将**协议链接放到后端统一管理与下发**,客户端按设备语言展示对应链接。
|
||||
|
||||
### 目标
|
||||
|
||||
- **首进页补充协议链接**:在客户端首次进入的同意页底部展示「隐私协议」「用户使用协议」链接入口
|
||||
- **后端托管与可配置**:协议链接由后端配置与下发,支持后续更新而无需发版
|
||||
- **按设备语言下发**:后端根据客户端设备语言返回对应语言的链接
|
||||
- **默认 EN**:当语言缺失/不支持/无法识别时,回退为英文(EN)链接
|
||||
|
||||
### 非目标(本阶段不做)
|
||||
|
||||
- 不做复杂的协议版本强制重同意机制(例如协议更新后要求重新同意)
|
||||
- 不规定协议内容必须以何种格式(Markdown/HTML/PDF)存储;本需求仅要求“链接由后端提供并可访问”
|
||||
|
||||
## 2. 需求范围与交付物
|
||||
|
||||
### 范围
|
||||
|
||||
- 客户端:
|
||||
- 首次进入同意页底部展示 2 个协议链接入口,并在点击时打开对应链接
|
||||
- **个人主页弹窗(ProfileModal)**中「隐私协议」「用户使用协议」入口也必须使用同一套后端下发链接(不再写死/不再 TODO)
|
||||
- 后端:提供可被客户端请求的协议链接下发能力,并支持按语言返回不同链接
|
||||
|
||||
### 交付物
|
||||
|
||||
- 后端:新增/扩展一个“协议链接配置与下发”的接口(或纳入现有配置接口),并可按语言返回
|
||||
- 客户端:同意页 UI 补充协议链接入口,且链接来源改为后端下发(含兜底策略)
|
||||
- 文档:本 spec
|
||||
|
||||
## 3. 用户体验与交互(高层)
|
||||
|
||||
### 3.1 展示位置
|
||||
|
||||
- 位置:客户端**首次进入同意页**底部,「同意并继续」按钮下方(或同等位置)展示:
|
||||
- **隐私协议**
|
||||
- **用户使用协议**
|
||||
|
||||
### 3.2 点击行为
|
||||
|
||||
- 点击后打开对应链接内容:
|
||||
- 推荐使用 App 内 WebView(不离开 App),也允许外部浏览器打开(实现阶段在客户端方案中确定)
|
||||
- 若链接无法获取或打开失败:
|
||||
- 仍允许用户继续使用“同意并继续”(不阻塞主流程)
|
||||
- 入口可隐藏或展示为不可点击状态(实现阶段确定,但需保持体验一致且不崩溃)
|
||||
|
||||
## 4. 语言策略
|
||||
|
||||
### 4.1 语言来源
|
||||
|
||||
- 客户端以**设备语言/应用当前语言**作为请求依据(优先级由客户端既有 i18n 策略决定)
|
||||
- 客户端请求应携带语言信息(例如 `Accept-Language` 头或显式参数)
|
||||
|
||||
### 4.2 默认与回退
|
||||
|
||||
- 默认语言:**EN**
|
||||
- 回退策略(必须满足):
|
||||
- 语言缺失/空值 → EN
|
||||
- 语言不支持 → EN
|
||||
- 后端配置缺失对应语言 → EN
|
||||
|
||||
### 4.3 支持语言(建议)
|
||||
|
||||
当前多语言仅支持 **EN / TC**,建议最少覆盖:
|
||||
|
||||
- `en`
|
||||
- `tc`
|
||||
|
||||
> 说明:当前协议文档可能包含英文与繁体版本;当 tc 版本缺失时,系统会回退到 EN。
|
||||
|
||||
## 5. 后端职责(高层)
|
||||
|
||||
### 5.1 配置管理
|
||||
|
||||
- 后端存储并维护协议链接配置,至少包含:
|
||||
- `privacy_policy_url`
|
||||
- `terms_of_use_url`
|
||||
- 配置需支持按语言维度区分(例如每个语言各一套 URL)
|
||||
- **兜底要求**:当外部托管链接未配置或不可用时,后端应提供可访问的内置协议页面(保证客户端点击“必有内容”)
|
||||
|
||||
### 5.2 下发接口(建议形态)
|
||||
|
||||
- 提供一个可被客户端调用的接口,用于获取协议链接(示例,仅表达形态,不强制路径):
|
||||
- `GET /v1/legal/links`
|
||||
- 输入(建议):
|
||||
- 语言:通过 `Accept-Language` 或 `lang` 参数(例如 `en`/`tc`)
|
||||
- 可选:平台、App 版本(用于灰度/差异化配置)
|
||||
- 输出(建议):
|
||||
- `privacyPolicyUrl: string`
|
||||
- `termsOfUseUrl: string`
|
||||
- `resolvedLang: string`(后端最终命中的语言,便于排查)
|
||||
|
||||
### 5.3 可用性与安全
|
||||
|
||||
- 链接必须可被公网访问(或在 App 可访问的网络环境可达)
|
||||
- 推荐使用 HTTPS
|
||||
- 若后端短时不可用,不应导致客户端首进流程崩溃
|
||||
|
||||
## 6. 客户端职责(高层)
|
||||
|
||||
- 在首次进入同意页渲染时,向后端请求协议链接
|
||||
- 根据返回结果渲染「隐私协议」「用户使用协议」入口
|
||||
- 若请求失败或返回缺失:
|
||||
- 按本 spec 的默认回退策略处理(最终保证 EN)
|
||||
- 不阻塞“同意并继续”主流程
|
||||
- (建议)对获取到的链接做轻量校验(非空字符串、看起来是 URL),避免渲染异常
|
||||
|
||||
## 7. 验收标准
|
||||
|
||||
- **首进展示**:首次进入同意页时,底部出现「隐私协议」「用户使用协议」入口
|
||||
- **语言分发**:
|
||||
- 设备语言为 EN 时,打开英文链接
|
||||
- 设备语言为已支持的其他语言(如 `zh-CN`/`zh-TW`)时,打开对应语言链接
|
||||
- 设备语言为不支持语言时,默认回退打开 EN 链接
|
||||
- **稳定性**:后端不可达/返回异常时:
|
||||
- App 不崩溃
|
||||
- “同意并继续”仍可正常进入后续流程
|
||||
|
||||
## 8. 风险与注意事项
|
||||
|
||||
- **法务一致性**:各语言协议内容差异需要法务确认;若部分语言缺失,需明确 `zh-CN` 的回退策略(到 EN 还是 `zh-TW`)
|
||||
- **链接长期可用**:后端配置的 URL 需保证稳定与可访问性(避免过期链接导致合规风险)
|
||||
- **缓存与更新**:若客户端做本地缓存,需要确保更新后能及时刷新(本期不强制,后续可扩展)
|
||||
|
||||
68
spec_kit/Policy Links/tasks.md
Normal file
68
spec_kit/Policy Links/tasks.md
Normal file
@@ -0,0 +1,68 @@
|
||||
# Policy Links(任务清单)
|
||||
|
||||
> 说明:本清单按 `plan.md` 拆分为可执行任务。
|
||||
> 状态含义:`[ ]` 未完成,`[x]` 已完成。
|
||||
|
||||
## 0. 前置检查
|
||||
|
||||
- [x] 确认客户端存在至少两处协议入口:首次同意页、个人主页弹窗(ProfileModal)
|
||||
- [x] 确认后端路由前缀体系为 `/v1/*`
|
||||
|
||||
## 1. 后端(FastAPI)——协议链接下发
|
||||
|
||||
### 1.1 配置项(环境变量)
|
||||
|
||||
- [x] 在后端配置类中新增协议链接配置(EN 兜底 + 可选 TC)
|
||||
- [x] 在 `server/env.example` 补充 `LEGAL_*` 配置示例与注释
|
||||
|
||||
### 1.2 API 路由与返回结构
|
||||
|
||||
- [x] 新增 `GET /v1/legal/links` 接口
|
||||
- [x] 解析 `Accept-Language`(轻量:只关心 `en/tc`,其他回退 `en`)
|
||||
- [x] 支持语言缺失/未配置时回退 EN
|
||||
- [x] 返回 `resolvedLang` 便于排查
|
||||
- [x] 在 `app/main.py` 注册 legal router
|
||||
|
||||
### 1.3 代码质量与最小校验
|
||||
|
||||
- [x] 确保后端代码可被 `python3 -m compileall` 编译通过
|
||||
- [x] (可选)补充单测:覆盖缺省、tc、zh-hant/zh-hk/zh-tw 等输入解析与回退(unittest)
|
||||
|
||||
### 1.4 内置协议内容页(兜底,保证有内容)
|
||||
|
||||
- [x] 新增 `GET /v1/legal/privacy`、`GET /v1/legal/terms` 两个 HTML 页面接口
|
||||
- [x] 未配置 `LEGAL_*` 时,`GET /v1/legal/links` 自动回退到内置页面绝对 URL
|
||||
- [x] 更新后端配置默认值:`LEGAL_*` 未配置时使用内置页面(`__internal__` 哨兵)
|
||||
|
||||
## 2. 客户端(React Native + Expo)——统一获取并打开链接
|
||||
|
||||
### 2.1 新增统一请求封装
|
||||
|
||||
- [x] 新增 `client/src/services/legalApi.ts`
|
||||
- [x] 统一设置 `Accept-Language`(将客户端当前语言映射到 `en/tc`)
|
||||
- [x] 统一超时与错误处理(抛错给调用方兜底)
|
||||
|
||||
### 2.2 首次同意页接入
|
||||
|
||||
- [x] 将 `client/app/(splash)/splash.tsx` 的写死 URL 改为调用 `fetchLegalLinks()`
|
||||
- [x] 拉取失败不崩溃、不阻塞「同意并继续」
|
||||
- [x] 点击协议入口用 `expo-web-browser` 打开后端下发链接
|
||||
|
||||
### 2.3 个人主页弹窗接入
|
||||
|
||||
- [x] 将 `client/components/home/ProfileModal.tsx` 的 `toastTodo` 替换为打开后端下发链接
|
||||
- [x] 弹窗打开时拉取链接(失败不影响其他功能)
|
||||
- [x] 修复 TypeScript 定时器类型(RN 环境兼容)
|
||||
|
||||
### 2.4 客户端最小验证
|
||||
|
||||
- [x] 运行现有 `vitest` 测试通过(`npm test`)
|
||||
- [x] (可选)补充一个轻量测试:`buildAcceptLanguage()` 映射规则(en/tc)
|
||||
|
||||
## 3. 文档与追踪
|
||||
|
||||
- [x] `spec_kit/Policy Links/spec.md` 增补:个人主页弹窗也必须使用同一 API;接口路径统一为 `GET /v1/legal/links`
|
||||
- [x] 生成 `spec_kit/Policy Links/plan.md`
|
||||
- [x] 生成 `spec_kit/Policy Links/tasks.md`
|
||||
- [x] 在 `spec_kit/overview.md` 的 `Policy Links` 条目下标记“任务清单已执行完毕(已完成编码)”
|
||||
|
||||
@@ -28,6 +28,27 @@
|
||||
- **阶段产物**:
|
||||
- `spec_kit/Client User Identity/spec.md`
|
||||
|
||||
## App Push
|
||||
|
||||
- **目标**:实现“每日提醒”推送闭环:首次进入 Onboarding 可选择每日推送次数(0~5,可跳过),个人主页每日提醒弹窗可改次数/关闭;客户端使用首次生成 UUID(`client_user_id`)与后端关联;后端按用户配置定时推送
|
||||
- **关键选型**:客户端继续使用 `expo-notifications`;后端使用 Expo Push Service + 定时任务(Celery/等价方案)按用户偏好发送
|
||||
- **阶段产物**:
|
||||
- `spec_kit/App Push/spec.md`
|
||||
- `spec_kit/App Push/plan.md`
|
||||
- `spec_kit/App Push/tasks.md`
|
||||
- **已完成编码(阶段性)**:
|
||||
- 客户端:新增 `client_user_id`(UUID v4)生成与持久化;每日提醒次数范围修正为 **0~5**(0 表示关闭)
|
||||
- 客户端:Onboarding 结束页(每日提醒)在用户选择次数 > 0 时**直接触发系统权限申请**;授权后获取 Expo Push Token 并调用后端 `register/preferences`(移除单独的 push 引导页)
|
||||
- 客户端:个人主页“每日提醒”弹窗移除测试模式强制无权限逻辑,改为真实读取系统权限;并在开关/点击 OK 时同步后端偏好
|
||||
- 客户端:新增推送接口封装 `client/src/services/pushApi.ts`(token 获取、register/preferences/get、自动上报时区与 locale,并携带用户画像供后端 Push 模板使用)
|
||||
- 后端:新增 Push 数据模型 + Alembic 迁移(`push_tokens` / `push_preferences` / `push_send_log`)
|
||||
- 后端:新增 Push API(`/v1/push/register`、`/v1/push/preferences`、`/v1/push/test`(dev)),并新增基础限流
|
||||
- 后端:实现 Celery 定时任务
|
||||
- 每日生成计划(按用户时区在 9:00~24:00 均匀分段随机抖动生成 0~5 个时间点)
|
||||
- ETA 发送任务(幂等:同一用户同一天同一 slot 只发一次;用户中途关闭/降次数会跳过)
|
||||
- 发送文案复用推荐模块 `scene="push"`(降风险)
|
||||
- 后端:`pytest` 全量通过(27 passed)
|
||||
|
||||
## Project Bootstrap
|
||||
|
||||
- **目标**:完成项目仓库初始化与工程约定落地,明确 client/server 结构、dev/pro 环境隔离、MySQL/Redis 资源命名与访问边界
|
||||
@@ -46,8 +67,8 @@
|
||||
|
||||
## Onboarding App Shell
|
||||
|
||||
- **目标**:落地首次进入体验(Onboarding 3–5 页可跳过 + Push 设置可跳过)与主应用壳(点赞/讨厌、收藏夹入口、通用设置入口)
|
||||
- **核心范围**:Onboarding 多页流程、Push 引导页、主界面反应操作(Like/Dislike)、收藏夹、设置页(版本、iOS 小组件入口/说明)
|
||||
- **目标**:落地首次进入体验(Onboarding 3–5 页可跳过 + 每日提醒可跳过/可关闭)与主应用壳(点赞/讨厌、收藏夹入口、通用设置入口)
|
||||
- **核心范围**:Onboarding 多页流程、主界面反应操作(Like/Dislike)、收藏夹、设置页(版本、iOS 小组件入口/说明)
|
||||
- **阶段产物**:
|
||||
- `spec_kit/Onboarding App Shell/spec.md`
|
||||
- `spec_kit/Onboarding App Shell/plan.md`
|
||||
@@ -66,6 +87,7 @@
|
||||
- 修复 iOS 签名 Team 配置不一致:为主 App(Debug)与 Widget Extension(Debug/Release)显式补齐 `DEVELOPMENT_TEAM=WS92GPX9H2`,避免打包/上传过程中回退到默认 Team 导致显示 “Other Team”
|
||||
- iOS 构建号已提升到 `2`,并将 `client/ios/client/Info.plist` 改为自动跟随 `MARKETING_VERSION` / `CURRENT_PROJECT_VERSION`
|
||||
- 推送 entitlements 的 `aps-environment` 已切到 `production`(用于 TestFlight/线上包)
|
||||
- 清理未接入编译的 WidgetKit 骨架残留:移除磁盘上的 `client/ios/MindfulnessWidget/` 文件,并从 `client/ios/client.xcodeproj/project.pbxproj` 删除对应工程引用(避免 Xcode 显示幽灵文件)
|
||||
|
||||
## Splash Consent
|
||||
|
||||
@@ -76,6 +98,38 @@
|
||||
- `spec_kit/Splash Consent/plan.md`
|
||||
- `spec_kit/Splash Consent/tasks.md`
|
||||
|
||||
## Policy Links
|
||||
|
||||
- **目标**:在客户端首次进入同意页底部补充「隐私协议/用户使用协议」入口,并将协议链接改为**由后端托管与按设备语言下发**(默认回退 EN)
|
||||
- **核心范围**:后端提供协议链接下发接口(可配置、可多语言);客户端按设备语言请求并展示链接,失败不阻塞“同意并继续”
|
||||
- **阶段产物**:
|
||||
- `spec_kit/Policy Links/spec.md`
|
||||
- `spec_kit/Policy Links/plan.md`
|
||||
- `spec_kit/Policy Links/tasks.md`
|
||||
- **已完成编码(任务清单已执行完毕)**:
|
||||
- 后端新增 `GET /v1/legal/links`(按 `Accept-Language` 分发,默认回退 EN,返回 `resolvedLang`)
|
||||
- 后端配置:`LEGAL_*` 环境变量(`server/env.example` 已补齐示例;不配置时走内置协议页兜底)
|
||||
- 后端新增内置协议内容页:
|
||||
- `GET /v1/legal/privacy`
|
||||
- `GET /v1/legal/terms`
|
||||
- 客户端新增协议接口封装 `client/src/services/legalApi.ts`
|
||||
- 客户端工程化:新增统一 HTTP 封装 `client/src/utils/http.ts`(baseURL/超时/JSON/统一错误),并将 `legalApi.ts` / `recoApi.ts` 接入
|
||||
- 客户端接入两处入口:`app/(splash)/splash.tsx`、`components/home/ProfileModal.tsx`
|
||||
- **变更文件清单**:
|
||||
- `server/app/api/v1/legal.py`
|
||||
- `server/app/core/config.py`
|
||||
- `server/app/legal_docs.py`
|
||||
- `server/app/main.py`
|
||||
- `server/env.example`
|
||||
- `server/tests/test_legal_links_unittest.py`
|
||||
- `client/src/services/legalApi.ts`
|
||||
- `client/src/services/__tests__/legalApi.test.ts`
|
||||
- `client/app/(splash)/splash.tsx`
|
||||
- `client/components/home/ProfileModal.tsx`
|
||||
- `spec_kit/Policy Links/spec.md`
|
||||
- `spec_kit/Policy Links/plan.md`
|
||||
- `spec_kit/Policy Links/tasks.md`
|
||||
|
||||
## Card UI
|
||||
|
||||
- **目标**:优化卡片页 UI 与交互,支持右上角主题切换与个人主页弹窗,并将喜欢/讨厌改为 icon + 动效
|
||||
|
||||
Reference in New Issue
Block a user