# 页面内容管理方案 > 版本:v3.0 | 最后更新:2026-05-07 > 配套文档:[Worksheet 发布方案](./Worksheet发布方案.md) | [小程序云开发方案](./小程序云开发方案.md) --- ## 一、背景与目标 worksheet 已可通过 Debug 发布进入云数据库,但三个展示页仍使用本地静态数据: | 页面 | 当前数据来源 | 问题 | | --- | --------------------------------- | ---------------------- | | 首页 | `pages/home/home.data.ts` | featured、hot、分区靠代码手动维护 | | 分类页 | `pages/category/category.data.ts` | 新 worksheet 发布后不会自动出现 | | 分龄页 | `pages/age/age.ts` 内 Mock | 推荐内容和周路线无法运营配置 | **目标:** - **首页**:通过 Debug 管理页配置内容,生成配置写入云数据库并同步至云存储,小程序启动时通过数据预拉取获取并渲染 - **分类页**:每次进入时通过云函数实时查询所有已上线 worksheet,前端分组排序展示(v3.0 调整,因需展示收藏/下载等实时数据) - **分龄页**:独立方案(详见分龄页文档) --- ## 二、整体架构 ### 2.1 数据存储与同步 **首页数据:** 通过 `page-config.json` 预拉取方式加载(保持不变) **分类页数据:** 通过云函数接口实时查询(v3.0 调整) 分类页需要展示收藏数和下载数等实时数据,不再适合通过定期更新 `page-config.json` 的方式同步。改为每次进入页面时通过云函数实时查询所有已上线 worksheet,前端本地排序和分类展示。 **数据源(Source of Truth):** 云数据库 `page_configs` 集合,文档 `_id = 'current'`(仅存储 home 配置) **数据交付(Delivery):** 云存储 `content/page-config.json`(仅包含 home 数据,供小程序预拉取使用) 每次首页更新时,云函数先写数据库,再同步上传至云存储。 ```ts // page_configs/current 文档结构 type PageConfig = { home: HomePageData; // 首页配置 // category: 已移除,改为实时接口查询 // age: 已移除,改为独立方案 version: number; // 版本号,每次更新递增 updatedAt: string; // 最后更新时间 }; ``` 读写流程: ``` readCurrentConfig() → 从 page_configs/current 读取当前完整配置 config.home = data → 只覆盖首页字段 saveConfig(config) → 写回数据库 → 上传云存储 content/page-config.json ``` ### 2.2 数据流 ``` ┌──────────────────────────────────────────────────────────────────┐ │ Debug 内容管理页(独立页面) │ │ │ │ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │ │ │ 首页内容管理 │ │ 分类页内容管理 │ │ 分龄页内容管理 │ │ │ │ homeContent │ │ categoryContent│ │ ageContent │ │ │ │ Manage │ │ Manage │ │ Manage │ │ │ └──────┬───────┘ └──────┬───────┘ └──────┬───────┘ │ │ │ │ │ │ │ ▼ ▼ ▼ │ │ 点击「更新首页」→ 仅管理上下线状态 待开发 │ │ 调用 pageContentBuild (不再生成分类页配置) │ │ 只更新 home 配置 │ │ │ │ │ ▼ │ │ ┌─────────────────────────────────────────┐ │ │ │ 1. 从 page_configs/current 读取当前配置 │ │ │ │ 2. 更新 home 字段 │ │ │ │ 3. 清空 category / age 字段(注释保留) │ │ │ │ 4. version++ → 写回数据库 │ │ │ │ 5. 同步上传至云存储 content/page-config.json│ │ │ └─────────────────────────────────────────┘ │ └──────────────────────────────────────────────────────────────────┘ ▼ ┌──────────────────────────────────────────────────────────────────┐ │ 小程序启动 │ │ │ │ app.onLaunch │ │ └─ wx.getBackgroundFetchData('pre') 获取预拉取结果 │ │ ├─ 成功 → 存入 globalData + storage 缓存 │ │ └─ 失败 → 读取 storage 缓存 │ │ ├─ 命中 → 存入 globalData │ │ └─ 未命中 → 调用 pageConfigFetch 云函数兜底 │ │ └─ 成功 → 存入 globalData + storage │ │ │ │ 首页 onLoad │ │ └─ 从 globalData.pageConfig.home 读取 → 渲染 │ │ │ │ 分类页 onShow(每次进入都重新请求) │ │ └─ 调用 worksheetsQuery({ status: 'active' }) │ │ → 前端按分类分组 + 排序 → 渲染 │ └──────────────────────────────────────────────────────────────────┘ ``` ### 2.3 数据预拉取 小程序数据预拉取通过云函数 `pageConfigFetch` 实现(仅用于首页数据): - 在 `app.json` 中配置 `fetchDataUrl` 指向 `pageConfigFetch` 云函数 - 小程序冷启动时由微信客户端自动调用,不占用页面加载时间 - 云函数从 `page_configs/current` 读取配置并返回(主要是 home 字段) ```js // cloudfunctions/pageConfigFetch/index.js exports.main = async () => { const db = cloud.database(); const { data } = await db.collection('page_configs').doc('current').get(); return { success: true, data }; }; ``` ### 2.4 页面加载策略 **首页**:通过预拉取 + storage 缓存加载(保持不变) ``` app.onLaunch │ ├─ Step 1: wx.getBackgroundFetchData('pre') 获取预拉取结果 │ 成功 → 存入 globalData.pageConfig → 写入 storage 缓存 │ ├─ Step 2: 预拉取失败 → wx.getStorageSync('pageConfig') 读取缓存 │ 命中 → 存入 globalData.pageConfig │ └─ Step 3: 缓存也为空 → 调用 pageConfigFetch 云函数兜底 成功 → 存入 globalData.pageConfig + storage 缓存 首页 onLoad └─ 从 globalData.pageConfig.home 读取 → 渲染 ``` **分类页**:每次 onShow 实时查询(v3.0 新方案) ``` 分类页 onShow(每次进入触发) │ ├─ 清空当前列表数据,显示 loading ├─ 调用 worksheetsQuery({ status: 'active' }) 获取所有已上线 worksheet ├─ 前端按分类分组 ├─ 每个分类内排序:updatedAt 最新的 2 个置顶 → 其余按 downloads 降序 └─ setData 渲染,切换分类时无需再次请求 ``` **三级降级策略(首页):** 预拉取 → storage 缓存 → 云函数调用 | 层级 | 数据来源 | 耗时 | 适用场景 | | --- | ----------------------------------- | ------- | -------------------- | | L1 | `wx.getBackgroundFetchData('pre')` | 0ms | 正常冷启动,预拉取已完成 | | L2 | `wx.getStorageSync('pageConfig')` | ~1ms | 预拉取失败,但之前成功过 | | L3 | `pageConfigFetch` 云函数 | 200-500ms | 首次使用或缓存被清理 | ### 2.5 云函数总览 | 云函数 | 职责 | | ----------------------- | ------------------------------------------------- | | `pageContentBuild` | 生成首页配置(仅 home),写入数据库 + 云存储 | | `pageConfigFetch` | 数据预拉取接口,从数据库读取配置返回给客户端(首页用) | | `homeAutoRefresh` | 定时任务,每日 22:00 自动更新 featured 和 hot | | `homeTimerControl` | 暂停/启动首页定时刷新任务(更新 settings 集合标记位) | | `worksheetsQuery` | 查询 worksheet 列表(支持按分类、状态筛选,分类页实时调用)| | `worksheetsUpdateStatus`| 更新单个 worksheet 的状态 | --- ## 三、首页内容管理 ### 3.1 管理页入口 `supportPages/homeContentManage/homeContentManage` 进入页面时并行调用两个云函数加载数据: ``` onLoad → Promise.all([ worksheetsQuery({ status: 'active' }), // 获取所有 active worksheet pageConfigFetch() // 获取当前已保存的配置 ]) → featured: 始终按 downloads desc 取前 5 自动填充 → hot: 始终按 likes desc 取前 5 自动填充 → sections: 从 config.home.sections 回显已保存的分类分区数据 ``` ### 3.2 管理 UI | 区块 | 对应字段 | 数据来源 | | ------ | -------------- | ----------------------------------- | | 定时任务卡片 | — | 显示定时任务状态(运行中/已暂停),可切换 | | 今日推荐 | `featured` | 自动:downloads 最多的 5 个 active worksheet | | 热门推荐 | `hot` | 自动:likes 最多的 5 个 active worksheet | | 分类分区 | `sections` | 从已保存配置回显,支持手动添加/移除 worksheet | ### 3.3 数据回显逻辑 **featured / hot(自动填充,不回显历史配置):** 每次进入管理页,始终从当前 active worksheets 中实时计算: - featured = 按 `downloads` 降序取前 5 - hot = 按 `likes` 降序取前 5 用户可在自动填充基础上手动添加/移除,点击「更新首页数据」后生效。 **sections(从配置回显):** 从 `pageConfigFetch` 返回的 `home.sections` 中恢复已保存的分区数据: ``` CATEGORY_LIST.map(category => { saved = savedSectionMap.get(category.id) items = saved.items → 通过 wsMap 还原为完整 worksheet 对象 morePath = category.path // 从 CATEGORY_LIST 获取,如 /pages/category/category?id=math }) ``` - `name` / `icon` 始终从本地 `CATEGORY_LIST` 取(保证最新) - `morePath` 从 `CATEGORY_LIST` 的 `path` 字段获取,指向对应分类页并自动选中该分类 - `items` 通过 worksheet id 从 active worksheets 中匹配还原,已下架的 worksheet 自动过滤 ### 3.4 自动刷新与定时任务 今日推荐和热门推荐采用**自动化 + 手动微调**的方式: - **定时任务**:云函数 `homeAutoRefresh` 配置定时触发器,每日 22:00 自动执行,查询最新 top5 数据更新 `page-config.json` 中的 `home.featured` 和 `home.hot` - **开关控制**:通过 `settings` 集合中 `homeAutoRefresh` 文档的 `enabled` 字段控制。管理页提供暂停/启动按钮,调用 `homeTimerControl` 云函数切换状态。`homeAutoRefresh` 执行时先检查此标记,`enabled === false` 则跳过 - **手动更新**:用户可在管理页手动添加/移除 worksheet,点击「更新首页数据」调用 `pageContentBuild({ page: 'home', ... })` 生成 ``` 定时任务(每日 22:00) → homeAutoRefresh 云函数 → 检查 settings/homeAutoRefresh.enabled → enabled === false → 跳过执行 → 查询 top5 downloads → featured → 查询 top5 likes → hot → readCurrentConfig() → 只更新 home.featured 和 home.hot → saveConfig() ``` ### 3.5 更新首页数据流程 ``` 点击「更新首页数据」 → pageContentBuild({ page: 'home', featuredIds: [...], hotIds: [...], sections: [ { id: 'math', subtitle: '', morePath: '/pages/category/category?id=math', worksheetIds: [...] }, { id: 'english', subtitle: '', morePath: '/pages/category/category?id=english', worksheetIds: [...] }, ... ] }) → 云函数批量查询所有引用的 worksheet → 构建 home 配置(categoryTabs + ageBands + featured + hot + sections) → readCurrentConfig() → config.home = homeData → config.category = null // 清空分类页配置(注释保留,后期可调整) → config.age = null // 清空分龄页配置(注释保留,后期可调整) → saveConfig() ``` > **注意:** 清空 category 和 age 字段的代码需加注释标记,方便后期对这部分逻辑进行调整或注销。分类页已改为实时接口查询,不再依赖 page-config.json 中的 category 数据。 ### 3.6 数据结构 ```ts type HomePageData = { searchPlaceholder: string; categoryTabs: Array<{ id: string; name: string; path?: string }>; ageBands: Array<{ key: string; label: string; desc: string; path: string }>; featured: HomeDisplayItem[]; hot: HomeDisplayItem[]; sections: HomeDisplaySection[]; }; type HomeDisplaySection = { id: string; // 分类 id title: string; // 分类名称 subtitle: string; morePath: string; // 跳转路径,如 /pages/category/category?id=math items: HomeDisplayItem[]; }; ``` | 模块 | 生成规则 | | -------------- | ---------------------------------------------- | | `categoryTabs` | 由 `CATEGORY_LIST_WITH_ALL` 生成 | | `ageBands` | 由 `AGE_BANDS` 生成 | | `featured` | 自动:downloads 最多的 5 个 active worksheet | | `hot` | 自动:likes 最多的 5 个 active worksheet | | `sections` | 管理页配置的 worksheetIds,morePath 来自 CATEGORY_LIST | --- ## 四、分类页 ### 4.1 数据加载方式(v3.0 调整) 分类页不再通过 `page-config.json` 获取数据,改为**每次 onShow 时实时查询云端数据**: ``` 分类页 onShow │ ├─ 清空 displayItems,显示 loading ├─ 调用 worksheetsQuery({ status: 'active' }) │ → 一次性获取所有分类下已上线的 worksheet ├─ 前端按 CATEGORY_LIST 分组 ├─ 每个分类内排序(含「全部」): │ 1. updatedAt 最新的 2 个置顶(最近修改优先展示) │ 2. 其余按 downloads 降序排列 └─ setData 渲染 → 切换分类时直接从内存数据筛选,无需再次请求 ``` **设计要点:** - 每次进入分类页都重新请求,保证收藏数、下载数等实时数据的准确性 - 一次请求获取全部 active worksheet,切换分类时前端本地筛选,体验流畅 - 排序规则统一:先展示 2 个最近更新的(让用户看到新内容),再按热度(下载量)排序 ### 4.2 排序规则详解 所有分类(包括「全部」)使用相同的排序逻辑: ```ts function sortWorksheets(items: Worksheet[]): Worksheet[] { // 按 updatedAt 降序,取前 2 个作为「最新」 const sorted = [...items].sort((a, b) => new Date(b.updatedAt).getTime() - new Date(a.updatedAt).getTime() ); const recent = sorted.slice(0, 2); const recentIds = new Set(recent.map(w => w._id)); // 剩余按 downloads 降序 const rest = sorted.filter(w => !recentIds.has(w._id)) .sort((a, b) => (b.downloads || 0) - (a.downloads || 0)); return [...recent, ...rest]; } ``` | 位置 | 排序依据 | 说明 | | --- | --- | --- | | 前 2 个 | `updatedAt` 降序 | 最近修改/新上线的 worksheet 优先曝光 | | 第 3 个起 | `downloads` 降序 | 按热度排序,下载多的排前面 | ### 4.3 内容管理页(仅管理上下线) `supportPages/categoryContentManage/categoryContentManage` 分类页内容管理页**仅保留 worksheet 上下线管理功能**,不再提供「更新分类数据」按钮: 分类头部统计: | 指标 | 查询规则 | | --- | ----------------------------- | | 总数 | `worksheets.category == 当前分类` | | 草稿 | `status == draft` | | 线上 | `status == active` | | 已隐藏 | `status == hidden` | 列表展示当前分类下所有 worksheet(按 `draft → hidden → active` 排序),每项显示预览图、标题、ID、年龄/难度/标签、状态、操作按钮。 操作按钮: | 当前状态 | 按钮样式 | 可执行操作 | | -------- | ------- | --------------- | | `draft` | 绿色按钮 | 激活上线 | | `active` | 灰色按钮 | 下架为 hidden | | `hidden` | 黄色按钮 | 恢复为 draft 或直接激活 | > **已移除:**「更新分类数据」按钮及相关代码已注释,分类页数据改为实时接口查询。 ### 4.4 分类页支持 id 参数 分类页 `/pages/category/category` 支持通过 URL 参数 `id` 指定默认选中的分类: ``` /pages/category/category?id=english → 默认选中「英语启蒙」 /pages/category/category?id=math → 默认选中「数感启蒙」 /pages/category/category → 默认选中「全部」 ``` 首页 sections 的「查看更多」链接使用此参数跳转到对应分类。 --- ## 五、分龄页内容管理 > 详细方案已独立为 [分龄页内容管理方案](./分龄页内容管理方案.md) 分龄页内容分为四部分,通过 `age.config.ts` 统一管理: | 部分 | 名称 | 管理方式 | | --- | --- | --- | | 第一部分 | 年龄段设置 | 写死在 `age.config.ts`,几乎不变 | | 第二部分 | 能力目标 | AI Skill `generate-age-abilities` 生成 | | 第三部分 | 4 周学习路线 | AI Skill `generate-age-weekly-plans` 生成 | | 第四部分 | 为你推荐 | 运行时动态计算(取收藏/下载最高的 6 个 worksheet) | AI Skill 位于 `skills/` 目录,在开发时通过 Cursor 运行,以早教专家视角结合最新 worksheet 数据生成内容。 --- ## 六、Debug 页入口结构 ``` supportPages/debug/debug ├─ 分类基础数据管理 (已有:同步 CATEGORY_LIST → categories) ├─ Worksheet 内容池 (已有:状态、分类、上下架、预览) └─ 页面内容管理 (新增) ├─ 📋 分类页内容管理 → supportPages/categoryContentManage ├─ 🏠 首页内容管理 → supportPages/homeContentManage └─ 📅 分龄页内容管理 → supportPages/ageContentManage(待开发) ``` --- ## 七、实施计划 ### Phase 1:基础设施 ✅ 1. 实现 `pageContentBuild` 云函数(读写数据库 + 同步云存储) 2. 实现 `pageConfigFetch` 云函数(数据预拉取接口) 3. 实现 `worksheetsQuery` / `worksheetsUpdateStatus` 云函数 ### Phase 2:分类页管理 ✅(v3.0 调整) 1. 实现分类页管理页(worksheet 列表、状态管理) 2. ~~实现「更新分类数据」→ 生成 category 配置~~(已移除,改为实时接口) 3. 分类页支持 `?id=xxx` 参数默认选中分类 4. 分类页改为 onShow 实时查询 worksheetsQuery + 前端排序 ### Phase 3:首页管理 ✅ 1. 实现首页管理页(featured / hot 自动填充 + sections 配置回显) 2. 实现 worksheet 选择器弹窗 3. 实现定时任务(homeAutoRefresh + homeTimerControl) 4. 实现「更新首页数据」→ 生成 home 配置 ### Phase 4:分龄页管理(待开发) > 详见 [分龄页内容管理方案](./分龄页内容管理方案.md) 1. 新增 `age.config.ts` 配置文件,将静态配置与 AI 生成数据分离 2. 开发 AI Skill(`generate-age-abilities` + `generate-age-weekly-plans`) 3. 分龄页接入 pageConfig 数据源,实现「为你推荐」动态计算 --- ## 八、风险与约束 | 风险 | 处理方式 | | ----------------- | ----------------------------------------- | | 首页预拉取失败 | 依次降级:storage 缓存 → pageConfigFetch 云函数兜底 | | 分类页接口请求失败 | 显示错误提示 + 重试按钮,或降级使用本地 category.data.ts | | 配置生成失败 | 云函数返回错误,管理页提示重试,线上数据不受影响 | | 首页配置引用了 hidden 内容 | sections 回显时通过 wsMap 匹配,已下架的自动过滤 | | 分类页每次请求性能 | 一次查询所有 active worksheet(通常 < 100 条),前端分组排序,耗时可控 | | 分龄内容不适龄 | 选择器按年龄段默认过滤 | | 并发更新冲突 | 云函数使用 version 乐观锁,冲突时提示重新加载后重试 | --- ## 九、关键文件索引 | 文件 / 模块 | 职责 | | -------------------------------------------- | ------------------------------ | | `supportPages/homeContentManage/` | 首页内容管理页 | | `supportPages/categoryContentManage/` | 分类页内容管理页(仅上下线管理) | | `cloudfunctions/pageContentBuild/` | 生成首页配置,写入数据库 + 云存储 | | `cloudfunctions/pageConfigFetch/` | 数据预拉取接口,返回首页配置 | | `cloudfunctions/homeAutoRefresh/` | 定时任务,每日自动更新首页推荐位数据 | | `cloudfunctions/homeTimerControl/` | 暂停/启动首页定时刷新任务 | | `cloudfunctions/worksheetsQuery/` | 查询 worksheet 列表(分类页实时调用) | | `cloudfunctions/worksheetsUpdateStatus/` | 更新 worksheet 状态 | | `pages/category/category.ts` | 分类页,onShow 实时查询 + 前端排序 | | `pages/age/age.config.ts` | 分龄页配置(详见 [分龄页方案](./分龄页内容管理方案.md)) | | `skills/generate-age-abilities/SKILL.md` | AI Skill:生成分龄页能力目标 | | `skills/generate-age-weekly-plans/SKILL.md` | AI Skill:生成分龄页学习路线 | | `pages/home/home.data.ts` | 首页兜底数据 | | `pages/category/category.data.ts` | 分类页兜底数据(接口失败时降级使用) | | `core/data/categories.ts` | CATEGORY_LIST 分类定义(id/name/icon/path) | | `content/page-config.json`(云存储) | 首页配置 JSON(仅 home 字段有效) | | `page_configs/current`(云数据库) | 配置数据源(Source of Truth) | | `settings/homeAutoRefresh`(云数据库) | 定时任务开关标记 |