Files
mindfulness/spec_kit/Daily Widget Reco/plan.md
2026-02-03 17:43:58 +08:00

6.9 KiB
Raw Blame History

Daily Widget Reco技术计划

对应规范:spec_kit/Daily Widget Reco/spec.md

已确认输入(来自澄清):

  • 更新责任:App 与 Widget 都需要双通道Widget 主动拉取 + App 辅助刷新)
  • App Group suiteNamegroup.com.damer.mindfulness
  • 后端鉴权:不需要
  • baseURLApp 写入共享区 提供给 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 逻辑沿用 Feedzh* -> 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.v1v1

spec.md 定义字段,重点:

  • day_key:本地日 key用户时区
  • langen|tc
  • item.content_id + item.textWidget 展示必须字段
  • 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 后显示同一条