8.4 KiB
8.4 KiB
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: 1saved_at: string(ISO8601)day_key: string(本地日维度 key,例如2026-02-03;用于“今天是否已刷新”的判断)lang: "en" | "tc"item(可为空):content_id: numbertext: stringfinal_score?: numberfallback_level_final?: number
meta?: Record<string, unknown>(可选,用于排查;注意体积)source: "app" | "widget"(可选:谁写入的)
7.3 读写规则
- Widget 展示优先级:
day_key为今天且item.text非空 → 直接展示- 否则展示最近一次缓存(若存在)
- 否则展示兜底安全文案(写死)
- 写入要求:
- 写入必须原子化(一次性写完整 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(),提升即时性
- 在 Onboarding 完成或画像更新时,把
8.3 备选实现(App 主动拉取 + Widget 仅展示)
若团队希望 Widget 端完全不发网络请求,则:
- App 负责在“启动/前台/每天首次打开”等时机拉取
POST /v1/reco/widget并写入共享缓存 - Widget 仅读取缓存展示
缺点:用户不打开 App 时,小组件可能长期不更新(不满足“每日更新”的强诉求)
9. 失败回退与稳定性
- 网络失败:使用最近一次缓存;若无缓存,展示兜底安全文案。
- 接口返回 items 为空:视为失败,走同样回退。
- 画像缺失:
- App 侧应尽量在首次进入完成问卷后写入
widget.userProfile.v1_2 - 若仍缺失,Widget 端不崩溃,直接展示缓存/兜底文案;并在下一次 timeline 继续尝试
- App 侧应尽量在首次进入完成问卷后写入
- 数据损坏/解析失败:清空本次读取结果,走兜底策略(不可 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注入,避免写死。