Files
doodle-mini/docs/页面内容管理方案.md
T

500 lines
24 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 页面内容管理方案
> 版本: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` | 管理页配置的 worksheetIdsmorePath 来自 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`(云数据库) | 定时任务开关标记 |