Files
doodle-mini/docs/页面内容管理方案.md
T
2026-05-06 17:28:51 +08:00

466 lines
22 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.
# 页面内容管理方案
> 版本:v2.0 | 最后更新:2026-04-30
> 配套文档:[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 管理页配置三个页面的内容,生成统一配置写入云数据库并同步至云存储,小程序启动时通过**数据预拉取**获取并渲染。
---
## 二、整体架构
### 2.1 数据存储与同步
**数据源(Source of Truth):** 云数据库 `page_configs` 集合,文档 `_id = 'current'`
**数据交付(Delivery):** 云存储 `content/page-config.json`(供小程序预拉取使用)
每次更新时,云函数先写数据库,再同步上传至云存储,保证两者一致。各管理页更新时只修改配置中自己负责的字段,不影响其他页面数据。
```ts
// page_configs/current 文档结构
type PageConfig = {
home: HomePageData; // 首页配置
category: CategoryPageData; // 分类页配置
age: AgePageData; // 分龄页配置
version: number; // 版本号,每次更新递增
updatedAt: string; // 最后更新时间
};
```
读写流程:
```
readCurrentConfig() → 从 page_configs/current 读取当前完整配置
config[page] = data → 只覆盖对应页面字段(home / category / age
saveConfig(config) → 写回数据库 → 上传云存储 content/page-config.json
```
### 2.2 数据流
```
┌──────────────────────────────────────────────────────────────────┐
│ Debug 内容管理页(独立页面) │
│ │
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │
│ │ 首页内容管理 │ │ 分类页内容管理 │ │ 分龄页内容管理 │ │
│ │ homeContent │ │ categoryContent│ │ ageContent │ │
│ │ Manage │ │ Manage │ │ Manage │ │
│ └──────┬───────┘ └──────┬───────┘ └──────┬───────┘ │
│ │ │ │ │
│ └────────┬─────────┘─────────┬─────────┘ │
│ ▼ ▼ │
│ 点击「更新」按钮 → 调用云函数 pageContentBuild │
│ │ │
│ ▼ │
│ ┌─────────────────────────────────────────┐ │
│ │ 1. 从 page_configs/current 读取当前配置 │ │
│ │ 2. 只更新对应页面字段(home/category/age │ │
│ │ 3. version++ → 写回数据库 │ │
│ │ 4. 同步上传至云存储 content/page-config.json│ │
│ └─────────────────────────────────────────┘ │
└──────────────────────────────────────────────────────────────────┘
┌──────────────────────────────────────────────────────────────────┐
│ 小程序启动 │
│ │
│ app.onLaunch │
│ └─ wx.getBackgroundFetchData('pre') 获取预拉取结果 │
│ ├─ 成功 → 存入 globalData + storage 缓存 │
│ └─ 失败 → 读取 storage 缓存 │
│ ├─ 命中 → 存入 globalData │
│ └─ 未命中 → 调用 pageConfigFetch 云函数兜底 │
│ └─ 成功 → 存入 globalData + storage │
│ │
│ TabBar 页面 onLoad(首页 / 分类页 / 分龄页) │
│ ├─ 配置已就绪 → 从 globalData.pageConfig 读取对应字段 → 渲染 │
│ └─ 配置未就绪 → 显示加载态 → 监听配置就绪回调 → 渲染 │
│ │
│ TabBar 页面 onShow(后续切换) │
│ └─ 直接从 globalData.pageConfig 读取,零延迟 │
└──────────────────────────────────────────────────────────────────┘
```
### 2.3 数据预拉取
小程序数据预拉取通过云函数 `pageConfigFetch` 实现:
-`app.json` 中配置 `fetchDataUrl` 指向 `pageConfigFetch` 云函数
- 小程序冷启动时由微信客户端自动调用,不占用页面加载时间
- 云函数从 `page_configs/current` 读取完整配置并返回
```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 页面加载策略
三个展示页(首页、分类页、分龄页)均为 TabBar 页面,不再使用本地 `.data.ts` 静态数据渲染,改为**统一从内存读取预拉取配置**,并通过 storage 缓存保证离线可用:
```
app.onLaunch
├─ Step 1: wx.getBackgroundFetchData('pre') 获取预拉取结果
│ 成功 → 存入 globalData.pageConfig
│ → wx.setStorageSync('pageConfig', data) 写入缓存
│ → 通知页面就绪
├─ Step 2: 预拉取失败 → wx.getStorageSync('pageConfig') 读取缓存
│ 命中 → 存入 globalData.pageConfig → 通知页面就绪
└─ Step 3: 缓存也为空 → 调用 pageConfigFetch 云函数兜底
成功 → 存入 globalData.pageConfig
→ wx.setStorageSync('pageConfig', data) 写入缓存
→ 通知页面就绪
TabBar 页面 onLoad(仅首次进入触发)
├─ globalData.pageConfig 已就绪
│ → 读取对应字段(home / category / age)→ setData 渲染
└─ globalData.pageConfig 未就绪
→ 显示加载态(骨架屏 / loading)
→ 注册回调,配置就绪后自动渲染
TabBar 页面 onShow(后续每次切换触发)
└─ 直接从 globalData.pageConfig 读取,零延迟,无网络请求
```
**三级降级策略:** 预拉取 → storage 缓存 → 云函数调用
| 层级 | 数据来源 | 耗时 | 适用场景 |
| --- | ----------------------------------- | ------- | -------------------- |
| L1 | `wx.getBackgroundFetchData('pre')` | 0ms | 正常冷启动,预拉取已完成 |
| L2 | `wx.getStorageSync('pageConfig')` | ~1ms | 预拉取失败,但之前成功过 |
| L3 | `pageConfigFetch` 云函数 | 200-500ms | 首次使用或缓存被清理 |
**设计要点:**
- 配置在 `app.onLaunch` 时加载一次,存入 `globalData.pageConfig`,三个页面共享同一份内存数据
- 每次预拉取或云函数获取成功后,同步写入 storage 作为下次启动的缓存兜底
- TabBar 页面 `onLoad` 只执行一次,`onShow` 每次切换都触发,天然适合「启动时加载一次,后续复用」的模式
- 不再维护 `home.data.ts``category.data.ts` 等本地静态数据文件,消除双数据源不一致的问题
- storage 缓存的 key 为 `pageConfig`,使用 `wx.setStorageSync` / `wx.getStorageSync` 同步读写
### 2.5 云函数总览
| 云函数 | 职责 |
| ---------------------- | ------------------------------------------------- |
| `pageContentBuild` | 生成指定页面配置(home / category),写入数据库 + 云存储 |
| `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 → saveConfig()
```
### 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 管理页入口
`supportPages/categoryContentManage/categoryContentManage`
### 4.2 管理 UI
分类页管理顶部展示分类选择器(来源 `CATEGORY_LIST`),默认选中 `math`
分类头部统计:
| 指标 | 查询规则 |
| --- | ----------------------------- |
| 总数 | `worksheets.category == 当前分类` |
| 草稿 | `status == draft` |
| 线上 | `status == active` |
| 已隐藏 | `status == hidden` |
列表展示当前分类下所有 worksheet(按 `draft → hidden → active` 排序),每项显示预览图、标题、ID、年龄/难度/标签、状态、操作按钮。
操作按钮:
| 当前状态 | 按钮样式 | 可执行操作 |
| -------- | ------- | --------------- |
| `draft` | 绿色按钮 | 激活上线 |
| `active` | 灰色按钮 | 下架为 hidden |
| `hidden` | 黄色按钮 | 恢复为 draft 或直接激活 |
### 4.3 分类页支持 id 参数
分类页 `/pages/category/category` 支持通过 URL 参数 `id` 指定默认选中的分类:
```
/pages/category/category?id=english → 默认选中「英语启蒙」
/pages/category/category?id=math → 默认选中「数感启蒙」
/pages/category/category → 默认选中「全部」
```
首页 sections 的「查看更多」链接使用此参数跳转到对应分类。
### 4.4 数据结构
```ts
type CategoryPageData = {
searchPlaceholder: string;
categories: Array<{
id: string;
name: string;
icon: string;
items: CategoryItem[];
}>;
};
```
生成时从 `worksheets` 集合查询 `status == active` 的内容,按分类分组、按 `sortOrder` 排序,转换为 `CategoryItem`
### 4.5 更新流程
```
分类页管理 → 管理 worksheet 状态和排序
→ 点击「更新分类数据」
→ 查询所有 active worksheets,按 category 分组
→ 调用 pageContentBuild({ page: 'category' })
→ 云函数更新 page_configs/current 中的 category 字段 + 同步云存储
```
---
## 五、分龄页内容管理
> 详细方案已独立为 [分龄页内容管理方案](./分龄页内容管理方案.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:分类页管理 ✅
1. 实现分类页管理页(worksheet 列表、状态管理)
2. 实现「更新分类数据」→ 生成 category 配置
3. 分类页支持 `?id=xxx` 参数默认选中分类
### 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 云函数兜底 |
| 配置生成失败 | 云函数返回错误,管理页提示重试,线上数据不受影响 |
| 首页配置引用了 hidden 内容 | sections 回显时通过 wsMap 匹配,已下架的自动过滤 |
| 更新某页数据清空其他页 | 云函数从数据库读取完整配置,只覆盖对应字段,其他页面数据不受影响 |
| 分龄内容不适龄 | 选择器按年龄段默认过滤 |
| 并发更新冲突 | 云函数使用 version 乐观锁,冲突时提示重新加载后重试 |
---
## 九、关键文件索引
| 文件 / 模块 | 职责 |
| -------------------------------------------- | ------------------------------ |
| `supportPages/homeContentManage/` | 首页内容管理页 |
| `supportPages/categoryContentManage/` | 分类页内容管理页 |
| `cloudfunctions/pageContentBuild/` | 生成 page-config 中指定页面的配置 |
| `cloudfunctions/pageConfigFetch/` | 数据预拉取接口,返回完整页面配置 |
| `cloudfunctions/homeAutoRefresh/` | 定时任务,每日自动更新首页推荐位数据 |
| `cloudfunctions/homeTimerControl/` | 暂停/启动首页定时刷新任务 |
| `cloudfunctions/worksheetsQuery/` | 查询 worksheet 列表 |
| `cloudfunctions/worksheetsUpdateStatus/` | 更新 worksheet 状态 |
| `pages/category/category.ts` | 分类页,支持 `?id=xxx` 参数选中分类 |
| `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 |
| `page_configs/current`(云数据库) | 配置数据源(Source of Truth |
| `settings/homeAutoRefresh`(云数据库) | 定时任务开关标记 |