feat: 完成分龄页开发

This commit is contained in:
R524809
2026-05-06 17:28:51 +08:00
parent 50394154f7
commit 9bdf850f20
54 changed files with 3499 additions and 1021 deletions
+156
View File
@@ -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 : '获取分龄推荐失败',
};
}
};
@@ -0,0 +1,9 @@
{
"name": "age-recommended-query",
"version": "1.0.0",
"main": "index.js",
"dependencies": {
"wx-server-sdk": "^3.0.4"
}
}
@@ -0,0 +1,3 @@
{
"timeout": 20
}
+67 -3
View File
@@ -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,
@@ -0,0 +1,3 @@
{
"timeout": 20
}
@@ -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 : '批量更新失败',
};
}
};
@@ -0,0 +1,8 @@
{
"name": "worksheets-batch-patch",
"version": "1.0.0",
"main": "index.js",
"dependencies": {
"wx-server-sdk": "^3.0.4"
}
}
@@ -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 : '更新统计失败',
};
}
};
@@ -0,0 +1,8 @@
{
"name": "worksheets-stats-update",
"version": "1.0.0",
"main": "index.js",
"dependencies": {
"wx-server-sdk": "^3.0.4"
}
}