diff --git a/.gitignore b/.gitignore index 741e713..dedf3b1 100644 --- a/.gitignore +++ b/.gitignore @@ -19,6 +19,9 @@ lerna-debug.log* # OS .DS_Store +# AI Skill 临时缓存(从云存储下载的数据文件) +.cache/ + # Tests /coverage /.nyc_output diff --git a/cloudfunctions/ageRecommendedQuery/index.js b/cloudfunctions/ageRecommendedQuery/index.js new file mode 100644 index 0000000..1278f31 --- /dev/null +++ b/cloudfunctions/ageRecommendedQuery/index.js @@ -0,0 +1,156 @@ +const cloud = require('wx-server-sdk'); + +cloud.init({ env: cloud.DYNAMIC_CURRENT_ENV }); + +const AGE_BANDS = { + '3-4': { minAge: 3, maxAge: 4 }, + '4-5': { minAge: 4, maxAge: 5 }, + '5-6': { minAge: 5, maxAge: 6 }, + '6-7': { minAge: 6, maxAge: 7 }, + '7-8': { minAge: 7, maxAge: 8 }, +}; + +const DIFFICULTY_LABELS = { + 1: '入门', + 2: '基础', + 3: '进阶', + 4: '挑战', +}; + +function normalizeAgeKey(ageKey) { + const key = String(ageKey || '').trim(); + return AGE_BANDS[key] ? key : null; +} + +function ageBand(min, max) { + const minAge = Number(min) || 0; + const maxAge = Number(max) || 0; + return minAge && maxAge ? `${minAge}-${maxAge}岁` : ''; +} + +function difficultyLabel(difficulty) { + if (typeof difficulty === 'string') { + return difficulty; + } + return DIFFICULTY_LABELS[difficulty] || '基础'; +} + +function toRecommendedItem(ws) { + return { + worksheetId: ws._id, + title: ws.title || '', + subtitle: ws.subtitle || '', + previewImg: ws.previewImg || '', + ageBand: ageBand(ws.ageMin, ws.ageMax), + ageMin: Number(ws.ageMin) || 0, + ageMax: Number(ws.ageMax) || 0, + difficulty: difficultyLabel(ws.difficulty), + difficultyLabel: difficultyLabel(ws.difficulty), + path: ws.path || '', + downloads: Number(ws.downloads) || 0, + likes: Number(ws.likes) || 0, + }; +} + +function getAgeMin(ws) { + return Number(ws.ageMin) || 0; +} + +function getAgeMax(ws) { + return Number(ws.ageMax) || 0; +} + +function coversAgeBand(ws, band) { + return getAgeMin(ws) === band.minAge && getAgeMax(ws) >= band.maxAge; +} + +function sortByField(items, field) { + return [...items].sort((a, b) => { + const fieldDiff = (Number(b[field]) || 0) - (Number(a[field]) || 0); + if (fieldDiff !== 0) return fieldDiff; + const aTime = a.updatedAt ? new Date(a.updatedAt).getTime() : 0; + const bTime = b.updatedAt ? new Date(b.updatedAt).getTime() : 0; + return bTime - aTime; + }); +} + +function pickTop(items, field, limit, excludeIds) { + if (limit <= 0) return []; + + const picked = []; + for (const ws of sortByField(items, field)) { + if (excludeIds.has(ws._id)) continue; + picked.push(ws); + excludeIds.add(ws._id); + if (picked.length >= limit) break; + } + return picked; +} + +async function queryActiveWorksheets(db) { + const { data } = await db + .collection('worksheets') + .where({ + status: 'active', + }) + .orderBy('downloads', 'desc') + .orderBy('updatedAt', 'desc') + .limit(100) + .get(); + + return data || []; +} + +/** + * 入参: + * - ageKey: '3-4' | '4-5' | '5-6' | '6-7' | '7-8' + * + * 返回: + * - items: 优先返回 ageMin 等于目标下限且 ageMax 覆盖目标上限的 downloads Top3 + likes Top3,不足 6 个用全站 downloads Top 补足 + */ +exports.main = async (event) => { + try { + const ageKey = normalizeAgeKey(event && event.ageKey); + if (!ageKey) { + return { success: false, message: 'ageKey 不合法' }; + } + + const band = AGE_BANDS[ageKey]; + const db = cloud.database(); + const activeWorksheets = await queryActiveWorksheets(db); + const matchedWorksheets = activeWorksheets.filter((ws) => + coversAgeBand(ws, band), + ); + + const excludeIds = new Set(); + const topDownloads = pickTop( + matchedWorksheets, + 'downloads', + 3, + excludeIds, + ); + const topLikes = pickTop(matchedWorksheets, 'likes', 3, excludeIds); + const fallbackDownloads = pickTop( + activeWorksheets, + 'downloads', + Math.max(0, 6 - topDownloads.length - topLikes.length), + excludeIds, + ); + + return { + success: true, + data: { + ageKey, + items: [...topDownloads, ...topLikes, ...fallbackDownloads] + .slice(0, 6) + .map(toRecommendedItem), + }, + }; + } catch (error) { + return { + success: false, + message: + error instanceof Error ? error.message : '获取分龄推荐失败', + }; + } +}; diff --git a/cloudfunctions/ageRecommendedQuery/package.json b/cloudfunctions/ageRecommendedQuery/package.json new file mode 100644 index 0000000..1c345b5 --- /dev/null +++ b/cloudfunctions/ageRecommendedQuery/package.json @@ -0,0 +1,9 @@ +{ + "name": "age-recommended-query", + "version": "1.0.0", + "main": "index.js", + "dependencies": { + "wx-server-sdk": "^3.0.4" + } +} + diff --git a/cloudfunctions/pageContentBuild/config.json b/cloudfunctions/pageContentBuild/config.json new file mode 100644 index 0000000..3469ef8 --- /dev/null +++ b/cloudfunctions/pageContentBuild/config.json @@ -0,0 +1,3 @@ +{ + "timeout": 20 +} diff --git a/cloudfunctions/pageContentBuild/index.js b/cloudfunctions/pageContentBuild/index.js index a502575..ee62e90 100644 --- a/cloudfunctions/pageContentBuild/index.js +++ b/cloudfunctions/pageContentBuild/index.js @@ -214,17 +214,58 @@ async function buildHomeData(db, event) { return { homeData }; } +function normalizeIdArray(value) { + if (!Array.isArray(value)) return []; + return value.map((x) => String(x || '').trim()).filter(Boolean); +} + +function extractHomeSeedFromCurrentConfig(home) { + const featuredIds = normalizeIdArray(home?.featured?.map((x) => x?.id)); + const hotIds = normalizeIdArray(home?.hot?.map((x) => x?.id)); + const sections = Array.isArray(home?.sections) + ? home.sections + .map((s) => ({ + id: String(s?.id || '').trim(), + subtitle: String(s?.subtitle || '').trim(), + morePath: String(s?.morePath || '').trim(), + worksheetIds: normalizeIdArray(s?.items?.map((it) => it?.id)), + })) + .filter((s) => s.id && Array.isArray(s.worksheetIds)) + : []; + + return { featuredIds, hotIds, sections }; +} + exports.main = async (event) => { try { const db = cloud.database(); const page = String(event.page || '').trim(); - if (page !== 'category' && page !== 'home') { - throw new Error('page 参数不合法,支持 category / home'); + if (page !== 'category' && page !== 'home' && page !== 'all') { + throw new Error('page 参数不合法,支持 category / home / all'); } const config = await readCurrentConfig(); + // 当未提供 featuredIds/hotIds/sections 时,默认从现有 home 配置反推出 worksheetIds + // 用于“只想重建展示数据(比如 ageBand/previewImg/difficultyLabel),但不想手工维护 id 列表”的场景。 + const homeSeed = extractHomeSeedFromCurrentConfig(config.home || {}); + const homeEvent = { + ...event, + featuredIds: + Array.isArray(event.featuredIds) && event.featuredIds.length > 0 + ? event.featuredIds + : homeSeed.featuredIds, + hotIds: + Array.isArray(event.hotIds) && event.hotIds.length > 0 + ? event.hotIds + : homeSeed.hotIds, + sections: + Array.isArray(event.sections) && event.sections.length > 0 + ? event.sections + : homeSeed.sections, + }; + if (page === 'category') { const { categoryData, stats } = await buildCategoryData(db); config.category = categoryData; @@ -244,7 +285,7 @@ exports.main = async (event) => { } if (page === 'home') { - const { homeData } = await buildHomeData(db, event); + const { homeData } = await buildHomeData(db, homeEvent); config.home = homeData; config.version = (config.version || 0) + 1; config.updatedAt = new Date().toISOString(); @@ -262,6 +303,29 @@ exports.main = async (event) => { }, }; } + + if (page === 'all') { + const { categoryData, stats } = await buildCategoryData(db); + const { homeData } = await buildHomeData(db, homeEvent); + config.category = categoryData; + config.home = homeData; + config.version = (config.version || 0) + 1; + config.updatedAt = new Date().toISOString(); + + await saveConfig(config); + + return { + success: true, + data: { + version: config.version, + updatedAt: config.updatedAt, + stats, + featured: homeData.featured.length, + hot: homeData.hot.length, + sections: homeData.sections.length, + }, + }; + } } catch (error) { return { success: false, diff --git a/cloudfunctions/worksheetsBatchPatch/config.json b/cloudfunctions/worksheetsBatchPatch/config.json new file mode 100644 index 0000000..3469ef8 --- /dev/null +++ b/cloudfunctions/worksheetsBatchPatch/config.json @@ -0,0 +1,3 @@ +{ + "timeout": 20 +} diff --git a/cloudfunctions/worksheetsBatchPatch/index.js b/cloudfunctions/worksheetsBatchPatch/index.js new file mode 100644 index 0000000..f9b18e5 --- /dev/null +++ b/cloudfunctions/worksheetsBatchPatch/index.js @@ -0,0 +1,178 @@ +const cloud = require('wx-server-sdk'); + +cloud.init({ env: cloud.DYNAMIC_CURRENT_ENV }); + +const BATCH_SIZE = 10; + +function toInt(value, fallback = 0) { + const n = Number(value); + if (!Number.isFinite(n)) return fallback; + return Math.round(n); +} + +/** 与 miniprogram/utils/debugPublish.inferGradeFromAge 保持一致 */ +function inferGradeFromAge(ageMin, ageMax) { + const centerAge = Math.round((ageMin + ageMax) / 2); + const ageGradeMap = { + 2: -4, + 3: -3, + 4: -2, + 5: -1, + 6: 0, + 7: 1, + 8: 2, + 9: 3, + 10: 4, + 11: 5, + 12: 6, + }; + return ageGradeMap[centerAge] ?? 0; +} + +function normalizeTags(value) { + if (!Array.isArray(value)) return null; + return value.map((t) => String(t || '').trim()).filter(Boolean); +} + +function buildUpdateData(db, patch) { + const id = String(patch.id || '').trim(); + if (!id) return { id: null, ok: false, error: '缺少 id' }; + + const data = { updatedAt: db.serverDate() }; + let hasField = false; + + const hasAgeMin = patch.ageMin !== undefined && patch.ageMin !== null; + const hasAgeMax = patch.ageMax !== undefined && patch.ageMax !== null; + if (hasAgeMin !== hasAgeMax) { + return { + id, + ok: false, + error: 'ageMin 与 ageMax 必须同时提供', + }; + } + if (hasAgeMin && hasAgeMax) { + const ageMin = toInt(patch.ageMin); + const ageMax = toInt(patch.ageMax); + if (ageMin < 0 || ageMax < ageMin) { + return { id, ok: false, error: '年龄范围不合法' }; + } + data.ageMin = ageMin; + data.ageMax = ageMax; + data.grade = + patch.grade !== undefined && patch.grade !== null + ? toInt(patch.grade) + : inferGradeFromAge(ageMin, ageMax); + hasField = true; + } + + if (patch.tags !== undefined) { + const tags = normalizeTags(patch.tags); + if (tags === null) { + return { id, ok: false, error: 'tags 须为数组' }; + } + data.tags = tags; + hasField = true; + } + + if (!hasField) { + return { + id, + ok: false, + error: '未包含可更新字段(ageMin/ageMax 或 tags)', + }; + } + + return { id, ok: true, data }; +} + +async function runInBatches(items, batchSize, worker) { + const results = []; + for (let i = 0; i < items.length; i += batchSize) { + const chunk = items.slice(i, i + batchSize); + results.push(...(await Promise.all(chunk.map(worker)))); + } + return results; +} + +exports.main = async (event) => { + try { + const db = cloud.database(); + const patches = Array.isArray(event.patches) ? event.patches : []; + const dryRun = !!event.dryRun; + const invalidResults = []; + const validUpdates = []; + + for (const p of patches) { + const update = buildUpdateData(db, p); + + if (dryRun) { + invalidResults.push( + update.ok + ? { + id: update.id, + ok: true, + dryRun: true, + wouldSet: { + ...update.data, + updatedAt: '[serverDate]', + }, + } + : update, + ); + continue; + } + + if (!update.ok) { + invalidResults.push(update); + continue; + } + + validUpdates.push(update); + } + + const updateResults = dryRun + ? [] + : await runInBatches( + validUpdates, + BATCH_SIZE, + async ({ id, data }) => { + try { + await db + .collection('worksheets') + .doc(id) + .update({ data }); + return { id, ok: true }; + } catch (error) { + const message = + error instanceof Error + ? error.message + : '更新失败'; + return { + id, + ok: false, + error: /document.*not.*exist|not.*found/i.test( + message, + ) + ? 'worksheet 不存在' + : message, + }; + } + }, + ); + + const results = [...invalidResults, ...updateResults]; + const failed = results.filter((r) => !r.ok).length; + return { + success: failed === 0, + dryRun, + total: results.length, + failed, + results, + }; + } catch (error) { + return { + success: false, + message: error instanceof Error ? error.message : '批量更新失败', + }; + } +}; diff --git a/cloudfunctions/worksheetsBatchPatch/package.json b/cloudfunctions/worksheetsBatchPatch/package.json new file mode 100644 index 0000000..abf6fe4 --- /dev/null +++ b/cloudfunctions/worksheetsBatchPatch/package.json @@ -0,0 +1,8 @@ +{ + "name": "worksheets-batch-patch", + "version": "1.0.0", + "main": "index.js", + "dependencies": { + "wx-server-sdk": "^3.0.4" + } +} diff --git a/cloudfunctions/worksheetsStatsUpdate/index.js b/cloudfunctions/worksheetsStatsUpdate/index.js new file mode 100644 index 0000000..44f0b2c --- /dev/null +++ b/cloudfunctions/worksheetsStatsUpdate/index.js @@ -0,0 +1,37 @@ +const cloud = require('wx-server-sdk'); + +cloud.init({ env: cloud.DYNAMIC_CURRENT_ENV }); + +const VALID_FIELDS = new Set(['likes', 'downloads']); + +exports.main = async (event) => { + try { + const db = cloud.database(); + const command = db.command; + const collection = db.collection('worksheets'); + + const id = String(event.id || '').trim(); + const field = String(event.field || '').trim(); + + if (!id) throw new Error('worksheet id 不能为空'); + if (!VALID_FIELDS.has(field)) throw new Error('统计字段不合法'); + + await collection.doc(id).update({ + data: { + [field]: command.inc(1), + updatedAt: db.serverDate(), + }, + }); + + return { + success: true, + data: { _id: id, field }, + }; + } catch (error) { + return { + success: false, + message: + error instanceof Error ? error.message : '更新统计失败', + }; + } +}; diff --git a/cloudfunctions/worksheetsStatsUpdate/package.json b/cloudfunctions/worksheetsStatsUpdate/package.json new file mode 100644 index 0000000..ef7d3c1 --- /dev/null +++ b/cloudfunctions/worksheetsStatsUpdate/package.json @@ -0,0 +1,8 @@ +{ + "name": "worksheets-stats-update", + "version": "1.0.0", + "main": "index.js", + "dependencies": { + "wx-server-sdk": "^3.0.4" + } +} diff --git a/docs/分龄页内容管理方案.md b/docs/分龄页内容管理方案.md new file mode 100644 index 0000000..454307a --- /dev/null +++ b/docs/分龄页内容管理方案.md @@ -0,0 +1,396 @@ +# 分龄页内容管理方案 + +> 版本:v1.0 | 最后更新:2026-05-06 +> 配套文档:[页面内容管理方案](./页面内容管理方案.md) | [小程序云开发方案](./小程序云开发方案.md) + +--- + +## 一、背景与目标 + +分龄页(`pages/age/age`)按年龄段展示学习内容,目前数据写死在 `age.ts` 内部 Mock。页面内容可拆分为**四个部分**,变更频率各不相同: + +| 部分 | 名称 | 变更频率 | 数据来源 | +| -------- | ---------------- | ----------------------- | ---------------------------------------------------------- | +| 第一部分 | 年龄段设置 | 几乎不变 | 写死在 `age.config.ts` | +| 第二部分 | 能力目标 | 低频变更 | AI Skill 生成 → 写入 `age.config.ts` | +| 第三部分 | 4 周推荐学习路线 | 随 worksheet 增加而更新 | AI Skill 生成 → 写入 `age.config.ts` | +| 第四部分 | 为你推荐 | 动态 | 运行时调用云函数生成:同年龄段 downloads Top3 + likes Top3 | + +**目标:** + +1. 将分龄页配置抽取到独立的 `age.config.ts` 中,分离静态配置与动态数据 +2. 通过 AI Skill(Cursor 本地技能),结合最新 worksheet 数据,以早教专家视角自动生成第二部分(能力目标)和第三部分(学习路线) +3. 第四部分「为你推荐」在运行时通过云函数生成:同年龄段 **downloads Top3 + likes Top3** + +--- + +## 二、页面四部分详解 + +### 2.1 第一部分:年龄段设置(静态) + +年龄段划分几乎不变,直接写死在配置中。 + +```ts +// miniprogram/pages/age/age.config.ts + +export const AGE_TAB_SUB: Record = { + '3-4': '启蒙认知', + '4-5': '基础练习', + '5-6': '能力提升', + '6-7': '幼小衔接', + '7-8': '知识拓展', +}; +``` + +数据来源:`core/data/difficulty.ts` 中的 `AGE_BANDS` 定义了 5 个年龄段 `3-4 | 4-5 | 5-6 | 6-7 | 7-8`。 + +### 2.2 第二部分:能力目标(AI 生成,低频更新) + +每个年龄段 3 个能力维度(数感、书写、思维),描述该年龄段应达到的核心能力。 + +```ts +type AbilityItem = { + icon: string; // emoji 图标 + title: string; // 能力维度名称 + desc: string; // 能力描述 +}; + +// age.config.ts 中的结构 +export const AGE_ABILITIES: Record = { + '3-4': [ + { icon: '🔢', title: '数感', desc: '认读 1-5、点数对应' }, + { icon: '✏️', title: '书写', desc: '涂鸦线条、简单描红' }, + { icon: '🧩', title: '思维', desc: '找相同、简单配对' }, + ], + // ... 其他年龄段 +}; +``` + +**更新方式:** 运行 AI Skill `generate-age-abilities`,基于当前所有 worksheet 内容,以早教专家视角重新生成各年龄段的能力目标,并更新 `age.config.ts`。 + +### 2.3 第三部分:4 周推荐学习路线(AI 生成,中频更新) + +每个年龄段 4 周学习计划,每周一个主题,包含 3 个推荐练习。练习需关联到实际存在的 worksheet。 + +```ts +type WeekExercise = { + _id: string; // 云 worksheets._id,与 page-config category.items[].id 一致 + title: string; // worksheet 标题 + subtitle: string; // 副标题(列表展示) + path: string; // 小程序跳转路径,可含 query;点击后 navigateTo / switchTab +}; + +type WeekPlan = { + week: number; // 第几周 1-4 + theme: string; // 周主题 + exercises: WeekExercise[]; // 3 个练习 +}; + +// age.config.ts 中的结构 +export const AGE_WEEK_PLANS: Record = { + '3-4': [ + { + week: 1, + theme: '数感启蒙', + exercises: [ + { + _id: 'ws_001', + title: '找数字,涂一涂', + subtitle: '在数字方阵中找出目标数字并涂色', + path: '/mathPages/mathDraw/mathDraw?id=number-find', + }, + // ... + ], + }, + // ... week 2-4 + ], + // ... 其他年龄段 +}; +``` + +**更新方式:** 运行 AI Skill `generate-age-weekly-plans`,基于当前所有 worksheet 内容,以早教专家视角为每个年龄段编排 4 周学习路线,并更新 `age.config.ts`。 + +**交互:** 列表展示 `title` + `subtitle`;用户点击「去练习」时,使用 `path` 调用 `wx.navigateTo`(TabBar 页面则 `wx.switchTab` 并不带 query)。 + +### 2.4 第四部分:为你推荐(动态,云函数) + +展示当前年龄段的 6 个推荐 worksheet,规则固定为: + +- **前 3 个**:同年龄段 `downloads` 最高的 3 个(downloads Top3) +- **后 3 个**:同年龄段 `likes` 最高的 3 个(likes Top3) + +> 若两者出现重复,likes Top3 会自动向后补齐,确保总数为 6(在 worksheet 数量充足的前提下)。 + +实现方式: + +- **云函数**:`cloudfunctions/ageRecommendedQuery`,入参 `ageKey`(如 `5-6`),从 `worksheets` 集合查询 `status=active 且 ageMin/ageMax 匹配` 的数据并返回 6 条 +- **分龄页运行时**:在切换年龄段时调用该云函数,拿到 `title + previewImg + worksheetId` 渲染 +- **兜底**:云函数失败或无数据时,前端使用本地占位推荐(后续可替换为固定兜底表) + +--- + +## 三、`age.config.ts` 完整结构 + +```ts +// miniprogram/pages/age/age.config.ts + +import { AgeBandKey } from '../../core/data/difficulty'; + +// ─── 类型定义 ───────────────────────────────── +export type AbilityItem = { + icon: string; + title: string; + desc: string; +}; + +export type WeekExercise = { + _id: string; + title: string; + subtitle: string; + path: string; +}; + +export type WeekPlan = { + week: number; + theme: string; + exercises: WeekExercise[]; +}; + +export type AgeBandConfig = { + key: AgeBandKey; + label: string; + subLabel: string; + abilities: AbilityItem[]; + weeks: WeekPlan[]; +}; + +// ─── 第一部分:年龄段设置(手动维护,几乎不变)──────── +export const AGE_TAB_SUB: Record = { + '3-4': '启蒙认知', + '4-5': '基础练习', + '5-6': '能力提升', + '6-7': '幼小衔接', + '7-8': '知识拓展', +}; + +// ─── 第二部分:能力目标(由 AI Skill generate-age-abilities 生成)──────── +export const AGE_ABILITIES: Record = { + '3-4': [ + /* AI 生成内容 */ + ], + '4-5': [ + /* AI 生成内容 */ + ], + '5-6': [ + /* AI 生成内容 */ + ], + '6-7': [ + /* AI 生成内容 */ + ], + '7-8': [ + /* AI 生成内容 */ + ], +}; + +// ─── 第三部分:4 周学习路线(由 AI Skill generate-age-weekly-plans 生成)──────── +export const AGE_WEEK_PLANS: Record = { + '3-4': [ + /* AI 生成内容 */ + ], + '4-5': [ + /* AI 生成内容 */ + ], + '5-6': [ + /* AI 生成内容 */ + ], + '6-7': [ + /* AI 生成内容 */ + ], + '7-8': [ + /* AI 生成内容 */ + ], +}; +``` + +--- + +## 四、AI Skill 方案 + +### 4.1 设计思路 + +通过两个 Cursor AI Skill 分别管理第二部分和第三部分的内容生成: + +``` +doodle-mini/skills/ + ├── generate-age-abilities/ + │ └── SKILL.md # Skill 1:生成能力目标 + └── generate-age-weekly-plans/ + └── SKILL.md # Skill 2:生成 4 周学习路线 +``` + +两个 Skill 的共同点: + +1. **数据输入**:`page-config.json` 存储在微信云存储(路径:`/content/page-config.json`),本地不存在。运行 Skill 前需先将其下载到 `.cache/page-config.json`(通过微信开发者工具或 `tcb` CLI),Skill 从本地缓存读取 `category` 下所有 worksheet 的标题、描述和内容 +2. **专家视角**:以儿童早教专家(3-8 岁)的专业视角进行内容编排 +3. **输出目标**:生成符合 `age.config.ts` 数据结构的 TypeScript 代码,直接更新配置文件 + +### 4.2 Skill 1:`generate-age-abilities`(生成能力目标) + +**触发场景:** 当 worksheet 内容体系发生较大变化(如新增学科分类、调整难度体系)时运行 + +**生成逻辑:** + +1. 读取所有 active worksheet 的分类、年龄段、难度、标题和描述 +2. 按年龄段分组,分析该年龄段覆盖的知识领域和技能点 +3. 以早教专家视角,为每个年龄段总结 3 个核心能力维度(数感/书写/思维 或根据实际内容调整) +4. 每个能力维度用一句话描述该年龄段应达到的水平 +5. 更新 `age.config.ts` 中的 `AGE_ABILITIES` + +### 4.3 Skill 2:`generate-age-weekly-plans`(生成 4 周学习路线) + +**触发场景:** 每次有新 worksheet 发布上线后运行,确保学习路线推荐最新内容 + +**生成逻辑:** + +1. 读取所有 active worksheet 的完整信息(标题、描述、分类、年龄段、难度) +2. 按年龄段分组,筛选适合该年龄段的 worksheet +3. 以早教专家视角,设计 4 周主题式学习路线: + +- 遵循由易到难、由基础到综合的编排原则 +- 每周一个主题,覆盖不同能力维度 +- 每周 3 个练习,从实际存在的 worksheet 中选取 +- 确保 4 周内容覆盖该年龄段的主要学科和技能 + +4. 生成 `WeekPlan[]` 数据,关联实际 worksheet ID +5. 更新 `age.config.ts` 中的 `AGE_WEEK_PLANS` + +### 4.4 数据获取方式 + +`page-config.json` 存储在微信云存储中(路径:`/content/page-config.json`),Skill 运行在本地 Cursor 环境中,无法直接访问云存储。需先将文件下载到本地: + +**前置步骤:下载 page-config.json 到本地缓存** + +```bash +mkdir -p .cache +``` + +下载方式(任选其一): + +1. **微信开发者工具**:云开发控制台 → 存储 → 找到 `/content/page-config.json` → 下载 → 保存到 `.cache/page-config.json` +2. **tcb CLI**:`tcb storage:download /content/page-config.json .cache/page-config.json` + +**Skill 读取本地缓存** + +``` +读取 .cache/page-config.json + → 解析 category.categories[].items[] + → 提取每个 worksheet 的 id、title、description、category、ageBand、difficulty +``` + +> `.cache/` 目录应加入 `.gitignore`,仅作为 Skill 运行时的临时数据源。 + +--- + +## 五、分龄页管理(Debug 管理页) + +### 5.1 管理页入口 + +`supportPages/ageContentManage/ageContentManage`(待开发) + +### 5.2 管理 UI + +分龄管理页按年龄段切换:`3-4 岁 | 4-5 岁 | 5-6 岁 | 6-7 岁 | 7-8 岁` + +| 模块 | 操作 | 说明 | +| -------- | -------- | ---------------------------------------------------------- | +| 能力目标 | 只读展示 | 来自 `age.config.ts` 的 `AGE_ABILITIES`,由 AI Skill 维护 | +| 四周路线 | 只读展示 | 来自 `age.config.ts` 的 `AGE_WEEK_PLANS`,由 AI Skill 维护 | +| 推荐内容 | 自动计算 | 运行时取收藏/下载最高的 6 个 worksheet | + +> 注:第二、三部分改为由 AI Skill 在开发时生成并提交代码,管理页仅作预览展示,不再需要手动选择 worksheet。 + +### 5.3 更新流程 + +``` +AI Skill 生成流程(开发时): + 0. 从云存储下载 /content/page-config.json → 保存到 .cache/page-config.json + 1. 开发者在 Cursor 中运行 AI Skill + 2. Skill 读取 .cache/page-config.json 中的 worksheet 数据 + 3. 以早教专家视角生成能力目标 / 学习路线 + 4. 自动更新 age.config.ts + 5. 开发者 review 后提交代码 + +运行时数据流: + 分龄页 onLoad / 切换年龄段 + → 从 age.config.ts 读取能力目标 + 学习路线(静态) + → 调用云函数 ageRecommendedQuery(ageKey) 获取推荐(动态) + → 渲染页面 +``` + +--- + +## 六、数据结构 + +### 6.1 分龄页完整数据结构 + +```ts +type AgePageData = { + ageTabs: Array<{ + key: AgeBandKey; + rangeText: string; // '3-4' + subLabel: string; // '启蒙认知' + }>; + bands: Record< + AgeBandKey, + { + abilities: AbilityItem[]; + weeks: WeekPlan[]; + recommended: RecommendedItem[]; // 运行时动态计算 + } + >; +}; + +type RecommendedItem = { + id: string; + title: string; + image: string; + worksheetId: string; +}; +``` + +--- + +## 七、实施计划 + +### Phase 1:配置文件重构 + +1. 创建 `miniprogram/pages/age/age.config.ts`,从 `age.ts` 中抽取类型定义和静态数据 +2. 重构 `age.ts`,改为引用 `age.config.ts` 中的配置 + +### Phase 2:AI Skill 开发 + +1. 创建 `skills/generate-age-abilities/SKILL.md` +2. 创建 `skills/generate-age-weekly-plans/SKILL.md` +3. 运行 Skill 生成初始内容,验证数据结构正确性 + +### Phase 3:动态推荐 + +1. 新增云函数 `ageRecommendedQuery`(downloads Top3 + likes Top3) +2. 分龄页调用云函数并渲染「为你推荐」 + +--- + +## 八、关键文件索引 + +| 文件 / 模块 | 职责 | +| ------------------------------------------- | --------------------------------------------------- | +| `pages/age/age.config.ts` | 分龄页配置(类型定义 + 静态数据 + AI 生成数据) | +| `pages/age/age.ts` | 分龄页逻辑,引用 age.config.ts | +| `skills/generate-age-abilities/SKILL.md` | AI Skill:生成各年龄段能力目标 | +| `skills/generate-age-weekly-plans/SKILL.md` | AI Skill:生成 4 周学习路线 | +| `core/data/difficulty.ts` | AGE_BANDS 年龄段定义 | +| `core/data/categories.ts` | CATEGORY_LIST 分类定义 | +| `.cache/page-config.json` | worksheet 数据本地缓存(运行 Skill 前从云存储下载) | +| 云存储 `/content/page-config.json` | worksheet 数据源(Source of Truth) | +| `cloudfunctions/worksheetsQuery/` | 查询 worksheet 的云函数 | +| `cloudfunctions/ageRecommendedQuery/` | 分龄页推荐:同年龄段 downloads Top3 + likes Top3 | diff --git a/docs/小程序云开发方案.md b/docs/小程序云开发方案和数据库设计.md similarity index 99% rename from docs/小程序云开发方案.md rename to docs/小程序云开发方案和数据库设计.md index 518a34a..130703d 100644 --- a/docs/小程序云开发方案.md +++ b/docs/小程序云开发方案和数据库设计.md @@ -145,9 +145,9 @@ --- -### 3.6 集合:`favorites`(收藏) +### 3.6 集合:`favorites`(收藏日志) -对应 Prisma `Favorite`。 +对应 Prisma `FavoriteLog`。 | 字段 | BSON 类型 | 必填 | 说明 | | ------------- | --------- | ---- | ----------------------- | @@ -171,13 +171,13 @@ 对应 Prisma `DownloadLog`。 -| 字段 | BSON 类型 | 必填 | 说明 | -| ------------- | -------------- | ---- | ----------------------- | -| `_id` | string | 是 | 主键。 | -| `userId` | string | 是 | 关联 `users._id`。 | -| `worksheetId` | string | 是 | 关联 `worksheets._id`。 | -| `params` | object \| null | 否 | 生成参数快照(JSON)。 | -| `createdAt` | date | 是 | 下载时间。 | +| 字段 | BSON 类型 | 必填 | 说明 | +| ------------- | -------------- | ---- | --------------------------------- | +| `_id` | string | 是 | 主键。 | +| `userId` | string | 是 | 关联 `users._id`。 | +| `worksheetId` | string | 是 | 关联 `worksheets._id`。 | +| `params` | object \| null | 否 | 生成参数快照(JSON)。 暂时不使用 | +| `createdAt` | date | 是 | 下载时间。 | **索引建议**:`{ userId: 1, createdAt: -1 }`;`{ worksheetId: 1, createdAt: -1 }`(统计/运营)。 diff --git a/docs/页面内容管理方案.md b/docs/页面内容管理方案.md index 058bcef..e8fcd1e 100644 --- a/docs/页面内容管理方案.md +++ b/docs/页面内容管理方案.md @@ -368,41 +368,18 @@ type CategoryPageData = { ## 五、分龄页内容管理 -### 5.1 新增 `age.config.ts` +> 详细方案已独立为 [分龄页内容管理方案](./分龄页内容管理方案.md) -在 `miniprogram/pages/age/` 下新增 `age.config.ts`,将年龄段划分、能力目标、默认 worksheet 写死在配置中,作为兜底数据和管理页的基础结构。 +分龄页内容分为四部分,通过 `age.config.ts` 统一管理: -```ts -export type AgeBandConfig = { - key: AgeBandKey; - label: string; - subLabel: string; - abilities: AbilityItem[]; - weeks: WeekPlan[]; -}; -``` +| 部分 | 名称 | 管理方式 | +| --- | --- | --- | +| 第一部分 | 年龄段设置 | 写死在 `age.config.ts`,几乎不变 | +| 第二部分 | 能力目标 | AI Skill `generate-age-abilities` 生成 | +| 第三部分 | 4 周学习路线 | AI Skill `generate-age-weekly-plans` 生成 | +| 第四部分 | 为你推荐 | 运行时动态计算(取收藏/下载最高的 6 个 worksheet) | -> `worksheetIds` 初始为空数组,由分龄管理页选择后填入。`worksheetTitles` 作为兜底展示。 - -### 5.2 管理 UI - -分龄管理按年龄段切换:`3-4 岁 | 4-5 岁 | 5-6 岁 | 6-7 岁 | 7-8 岁` - -| 模块 | 操作 | 说明 | -| ---- | ------------ | -------------------------------- | -| 能力目标 | 只读展示 | 来自 `age.config.ts`,不需要在管理页修改 | -| 四周路线 | 选择 worksheet | 每周选择 4 个 worksheet,主题来自 config | -| 推荐内容 | 选择 worksheet | 动态数据,展示 likes 数量最高的 6 个worksheet | - -### 5.3 更新流程 - -``` -分龄管理 → 选择年龄段 → 为每周选择 worksheet → 选择推荐内容 - → 点击「更新分龄数据」 - → 合并 age.config.ts 的能力目标 + 管理页选择的 worksheet 详情 - → 调用 pageContentBuild({ page: 'age', data: ... }) - → 云函数更新 page_configs/current 中的 age 字段 + 同步云存储 -``` +AI Skill 位于 `skills/` 目录,在开发时通过 Cursor 运行,以早教专家视角结合最新 worksheet 数据生成内容。 --- @@ -443,9 +420,11 @@ supportPages/debug/debug ### Phase 4:分龄页管理(待开发) -1. 新增 `age.config.ts` 配置文件 -2. 实现分龄管理页(年龄段切换、周 worksheet 选择) -3. 实现「更新分龄数据」→ 合并 config + worksheet 详情 +> 详见 [分龄页内容管理方案](./分龄页内容管理方案.md) + +1. 新增 `age.config.ts` 配置文件,将静态配置与 AI 生成数据分离 +2. 开发 AI Skill(`generate-age-abilities` + `generate-age-weekly-plans`) +3. 分龄页接入 pageConfig 数据源,实现「为你推荐」动态计算 --- @@ -475,7 +454,9 @@ supportPages/debug/debug | `cloudfunctions/worksheetsQuery/` | 查询 worksheet 列表 | | `cloudfunctions/worksheetsUpdateStatus/` | 更新 worksheet 状态 | | `pages/category/category.ts` | 分类页,支持 `?id=xxx` 参数选中分类 | -| `pages/age/age.config.ts` | 分龄页兜底配置 | +| `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) | diff --git a/miniprogram/app.json b/miniprogram/app.json index 3710321..6f5ebd3 100644 --- a/miniprogram/app.json +++ b/miniprogram/app.json @@ -13,6 +13,7 @@ "pages": [ "guide/guide", "debug/debug", + "worksheetSync/worksheetSync", "categoryManage/categoryManage", "categoryContentManage/categoryContentManage", "homeContentManage/homeContentManage", diff --git a/miniprogram/assets/fonts/demo_index.html b/miniprogram/assets/fonts/demo_index.html index f75c1e0..74b6ba7 100644 --- a/miniprogram/assets/fonts/demo_index.html +++ b/miniprogram/assets/fonts/demo_index.html @@ -54,6 +54,12 @@
    +
  • + +
    loading
    +
    &#xe62e;
    +
  • +
  • task-o
    @@ -409,8 +415,8 @@ >@font-face { font-family: 'iconfont'; src: - url('data:application/x-font-woff;charset=utf-8;base64,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') format('woff'), - url('iconfont.ttf?t=1777354760973') format('truetype'); + url('data:application/x-font-woff;charset=utf-8;base64,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') format('woff'), + url('iconfont.ttf?t=1778054314485') format('truetype'); }

    第二步:定义使用 iconfont 的样式

    @@ -436,6 +442,15 @@
      +
    • + +
      + loading +
      +
      .icon-loading +
      +
    • +
    • @@ -967,6 +982,14 @@
        +
      • + +
        loading
        +
        #icon-loading
        +
      • +