# 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`(用于可观测/调参;客户端可选择性落地) --- ## 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`(可选,用于排查;注意体积) - `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` 注入,避免写死。