Files
2026-02-03 17:43:58 +08:00

8.4 KiB
Raw Permalink Blame History

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/feediOS 小组件仍是 V1 写死文案。

现在需要实现:

  • iOS 小组件展示“每日推荐”(每天一条),从后端获取。
  • App 与 Widget 的推荐内容数据共通(同一天展示同一条),并支持在用户添加小组件后开始每日更新。
  • 推荐链路具备可回退能力:网络失败、画像缺失、接口异常时不崩溃、仍有可展示内容。

2. 目标Goals

  • 后端串联:客户端与 iOS WidgetKit 能稳定调用后端 widget 场景推荐接口获取每日文案。
  • 数据共通App 与 Widget 通过 App Group 共享存储读写同一份“每日推荐缓存”,保证展示一致。
  • 每日更新(尽力而为)
    • 小组件添加到桌面后,系统开始调度 Widget TimelineWidget 能在每天刷新一次内容(受系统限制,允许延迟)。
    • 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 WidgetSwift / WidgetKit Extension
    • 从 App Group 读取“每日推荐缓存”并展示。
    • 在需要时(缓存过期/缺失)发起网络请求从后端拉取,并写回缓存(或由 App 拉取并写回,见第 8 节决策)。
  • 共享数据契约:
    • 定义 App 与 Widget 共用的数据结构(版本化、可演进)。

4.2 交付物

  • 客户端:推荐接口封装 + 共享存储写入 + 刷新触发。
  • iOSWidget Timeline Provider 支持每日推荐(读取共享缓存/必要时拉取)。
  • 文档:本 spec.md

5. 术语与关键约束

  • Daily Reco每日推荐Widget 场景 Top1 文案(每天一条)。
  • App GroupiOS 用于 App 与 Extension 共享数据的机制(建议使用共享 UserDefaults(suiteName:))。
  • Widget 刷新限制:刷新由系统调度,不可保证准点;需要设计缓存与兜底展示。

6. 后端接口契约Client ↔ Server

6.1 接口

  • POST /v1/reco/widget

6.2 Header

  • Accept-Language
    • 客户端沿用既有策略:当前仅区分 en/tc
    • 规则:i18n.languagezh 开头 → tc,否则 en

6.3 Request Body与 Feed 一致)

  • k?: numberWidget 默认 1建议不传或显式传 1
  • user_profile: UserProfileV1_2(必填)
  • already_recommended_ids?: (string|number)[](可选,默认 []
  • touched_or_viewed_ids?: (string|number)[](可选,默认 []
  • now?: stringISO8601可选用于测试/确定性)

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.v1Widget 端需要的配置(例如 apiBaseUrl、灰度开关等)

7.2 widget.dailyReco.v1 数据结构JSON

  • schema_version: 1
  • saved_at: stringISO8601
  • 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.5Widget 主动拉取)

  • Widget Timeline Provider

    • 每次 getTimeline
      • 先读 widget.dailyReco.v1
      • day_key 不是今天 → 尝试从后端请求 POST /v1/reco/widget 获取今日内容
      • 成功后写入 widget.dailyReco.v1 并返回 timeline
      • 失败则返回“最近一次缓存/兜底文案”的 timeline
    • Timeline 刷新策略:
      • policy = .after(nextRefreshDate)nextRefreshDate 设为“下一天的本地 00:1001:00 之间随机一个时间”(减少集中刷新)
  • AppRN

    • 在 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 注入,避免写死。