fix:小组件- PUSH

This commit is contained in:
吕新雨
2026-02-03 17:43:58 +08:00
parent d742b398ef
commit c1c2c6197d
66 changed files with 4888 additions and 479 deletions

View 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 数据流(高层)
- AppRN
- 写入共享配置(`apiBaseUrl` 等)→ 写入共享画像 →(可选)拉取 `widget` 推荐 → 写入共享每日缓存 → reload Widget
- WidgetSwift
- 读取共享每日缓存,若过期则读取共享配置+画像并请求后端 → 写入共享缓存 → 输出 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 WidgetSwift / 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:1001:00 随机),减少集中刷新与被系统限流概率
### 4.3 网络实现注意事项
- 使用 `URLSession`,超时建议 812 秒(与 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 后显示同一条