Files
doodle-mini/miniprogram/supportPages/worksheetSync/worksheetSync.ts
T
2026-05-29 10:21:39 +08:00

463 lines
13 KiB
TypeScript
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.
import { MOCK_SEED_COUNTS } from './mockLikes';
import {
MATH_WORKSHEET_DEFINITIONS,
FOCUS_WORKSHEET_DEFINITIONS,
LETTER_TRACING_WORKSHEET_DEFINITIONS,
PINYIN_DICTATION_WORKSHEET_DEFINITIONS,
PEN_CONTROL_WORKSHEET_DEFINITIONS,
WORD_COLORING_WORKSHEET_DEFINITIONS,
type WorksheetDefinition,
} from '../../config/worksheets';
type CloudFunctionResult<T> = {
success?: boolean;
message?: string;
data?: T;
dryRun?: boolean;
total?: number;
failed?: number;
results?: Array<{ id: string | null; ok: boolean; error?: string }>;
};
async function callCloudFunction<T>(
name: string,
data?: Record<string, unknown>,
): Promise<CloudFunctionResult<T>> {
const response = (await wx.cloud.callFunction({
name,
data: data || {},
})) as { result?: CloudFunctionResult<T> };
return response.result || {};
}
function inferGradeFromAge(ageMin: number, ageMax: number): number {
const centerAge = Math.round((ageMin + ageMax) / 2);
const ageGradeMap: Record<number, number> = {
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;
}
type ConfigSource = {
label: string;
data: ReadonlyArray<WorksheetDefinition>;
};
const CONFIG_SOURCES: ConfigSource[] = [
{
label: 'math',
data: MATH_WORKSHEET_DEFINITIONS,
},
{
label: 'focus',
data: FOCUS_WORKSHEET_DEFINITIONS,
},
{
label: 'letterTracing',
data: LETTER_TRACING_WORKSHEET_DEFINITIONS,
},
{
label: 'pinyin',
data: PINYIN_DICTATION_WORKSHEET_DEFINITIONS,
},
{
label: 'penControl',
data: PEN_CONTROL_WORKSHEET_DEFINITIONS,
},
{
label: 'wordColoring',
data: WORD_COLORING_WORKSHEET_DEFINITIONS,
},
];
function isWorksheetDefinition(value: unknown): value is WorksheetDefinition {
const row = value as Partial<WorksheetDefinition>;
return (
!!row &&
typeof row.id === 'string' &&
typeof row.ageMin === 'number' &&
typeof row.ageMax === 'number' &&
Array.isArray(row.tags)
);
}
async function loadConfigSource(
source: ConfigSource,
): Promise<WorksheetDefinition[]> {
const rows = source.data;
if (!Array.isArray(rows) || !rows.every(isWorksheetDefinition)) {
throw new Error(`${source.label} 配置格式不正确`);
}
return [...rows];
}
async function collectAll(): Promise<WorksheetDefinition[]> {
const groups = await Promise.all(CONFIG_SOURCES.map(loadConfigSource));
return groups.flat();
}
function assertUniqueIds(rows: WorksheetDefinition[]) {
const set = new Set<string>();
for (const r of rows) {
if (set.has(r.id)) {
throw new Error(`本地配置存在重复 id${r.id}`);
}
set.add(r.id);
}
}
function summarizeResult(
title: string,
result: CloudFunctionResult<unknown>,
): string {
if (!result.success) {
return `${title}失败:${result.message || '未知错误'}`;
}
const total = result.total ?? 0;
const failed = result.failed ?? 0;
const ok = total - failed;
const firstError = result.results?.find((r) => !r.ok)?.error;
return `${title}完成:成功 ${ok}/${total}${
failed ? `,失败 ${failed}(如:${firstError || '未知原因'}` : ''
}`;
}
Page({
data: {
configLoading: true,
configReady: false,
configError: '',
configTotal: 0,
configSourceCount: CONFIG_SOURCES.length,
loadingAges: false,
loadingTags: false,
loadingRebuild: false,
loadingSeed: false,
loadingResetStats: false,
statusText: '配置加载中,请稍候',
},
onLoad() {
void this.loadConfigs();
},
async onPullDownRefresh() {
await this.loadConfigs();
wx.stopPullDownRefresh();
},
async loadConfigs() {
this.setData({
configLoading: true,
configReady: false,
configError: '',
statusText: '配置加载中,请稍候',
});
try {
const rows = await collectAll();
assertUniqueIds(rows);
this.setData({
configLoading: false,
configReady: true,
configTotal: rows.length,
statusText: `配置已加载:${rows.length} 个 worksheet,可执行同步`,
});
} catch (error) {
const msg = error instanceof Error ? error.message : '配置加载失败';
this.setData({
configLoading: false,
configReady: false,
configError: msg,
statusText: msg,
});
wx.showToast({ title: msg, icon: 'none' });
}
},
async sync(mode: 'ages' | 'tags') {
if (!this.data.configReady) {
wx.showToast({ title: '配置未加载完成', icon: 'none' });
return;
}
const rows = await collectAll();
assertUniqueIds(rows);
const patches =
mode === 'ages'
? rows.map((r) => ({
id: r.id,
ageMin: r.ageMin,
ageMax: r.ageMax,
grade: inferGradeFromAge(r.ageMin, r.ageMax),
}))
: rows.map((r) => ({
id: r.id,
tags: [...r.tags],
}));
wx.showLoading({ title: '同步中...' });
const startedAt = Date.now();
try {
const patchRes = await callCloudFunction('worksheetsBatchPatch', {
patches,
dryRun: false,
});
if (!patchRes.success) {
throw new Error(patchRes.message || '同步失败');
}
const elapsed = Math.round((Date.now() - startedAt) / 1000);
const text = [
summarizeResult(
mode === 'ages'
? '同步年龄(worksheetsBatchPatch'
: '同步标签(worksheetsBatchPatch',
patchRes,
),
'page-config 未自动重建,如需刷新前台展示请点击“重建配置”。',
`耗时:${elapsed}s`,
].join('\n');
this.setData({ statusText: text });
wx.showToast({ title: '已完成', icon: 'success' });
} catch (error) {
const msg = error instanceof Error ? error.message : '同步失败';
this.setData({ statusText: msg });
wx.showToast({ title: msg, icon: 'none' });
} finally {
wx.hideLoading();
}
},
async rebuildPageConfig() {
wx.showLoading({ title: '重建中...' });
const startedAt = Date.now();
try {
const rebuildRes = await callCloudFunction('pageContentBuild', {
page: 'all',
});
const elapsed = Math.round((Date.now() - startedAt) / 1000);
const text = [
summarizeResult(
'重建 page-configpageContentBuild: all',
rebuildRes,
),
`耗时:${elapsed}s`,
].join('\n');
this.setData({ statusText: text });
if (!rebuildRes.success) {
throw new Error(rebuildRes.message || '重建失败');
}
wx.showToast({ title: '已重建', icon: 'success' });
} catch (error) {
const msg = error instanceof Error ? error.message : '重建失败';
this.setData({ statusText: msg });
wx.showToast({ title: msg, icon: 'none' });
} finally {
wx.hideLoading();
}
},
async onSyncAges() {
if (
!this.data.configReady ||
this.data.loadingAges ||
this.data.loadingTags ||
this.data.loadingRebuild
)
return;
this.setData({ loadingAges: true });
try {
await this.sync('ages');
} finally {
this.setData({ loadingAges: false });
}
},
async onSyncTags() {
if (
!this.data.configReady ||
this.data.loadingAges ||
this.data.loadingTags ||
this.data.loadingRebuild
)
return;
this.setData({ loadingTags: true });
try {
await this.sync('tags');
} finally {
this.setData({ loadingTags: false });
}
},
async onRebuildConfig() {
if (
this.data.loadingAges ||
this.data.loadingTags ||
this.data.loadingRebuild
)
return;
this.setData({ loadingRebuild: true });
try {
await this.rebuildPageConfig();
} finally {
this.setData({ loadingRebuild: false });
}
},
async onSyncSeed() {
if (
this.data.loadingAges ||
this.data.loadingTags ||
this.data.loadingRebuild ||
this.data.loadingSeed
)
return;
this.setData({ loadingSeed: true });
wx.showLoading({ title: '同步 Seed...' });
const startedAt = Date.now();
try {
const patches = Object.entries(MOCK_SEED_COUNTS).map(
([id, counts]) => ({
id,
likes_seed: counts.likes,
downloads_seed: counts.downloads,
}),
);
const patchRes = await callCloudFunction('worksheetsBatchPatch', {
patches,
dryRun: false,
});
if (!patchRes.success) {
throw new Error(patchRes.message || '同步 Seed 失败');
}
const elapsed = Math.round((Date.now() - startedAt) / 1000);
const text = [
summarizeResult(
'同步 Seedlikes_seed / downloads_seed',
patchRes,
),
`共 ${patches.length} 条 Seed 数据`,
`耗时:${elapsed}s`,
].join('\n');
this.setData({ statusText: text });
wx.showToast({ title: '已完成', icon: 'success' });
} catch (error) {
const msg =
error instanceof Error ? error.message : '同步 Seed 失败';
this.setData({ statusText: msg });
wx.showToast({ title: msg, icon: 'none' });
} finally {
wx.hideLoading();
this.setData({ loadingSeed: false });
}
},
async onResetStats() {
if (
this.data.loadingAges ||
this.data.loadingTags ||
this.data.loadingRebuild ||
this.data.loadingSeed ||
this.data.loadingResetStats
)
return;
const { confirm } = await wx.showModal({
title: '确认初始化',
content:
'将所有 worksheet 的 likes 和 downloads 重置为 0,此操作不可撤销。确定继续?',
confirmText: '确定重置',
confirmColor: '#e53935',
});
if (!confirm) return;
this.setData({ loadingResetStats: true });
wx.showLoading({ title: '重置中...' });
const startedAt = Date.now();
try {
const queryRes = await callCloudFunction<Array<{ _id: string }>>(
'worksheetsQuery',
{},
);
if (!queryRes.success || !queryRes.data) {
throw new Error(queryRes.message || '查询 worksheet 列表失败');
}
const patches = (queryRes.data as Array<{ _id: string }>).map(
(ws) => ({
id: ws._id,
likes: 0,
downloads: 0,
}),
);
if (patches.length === 0) {
this.setData({ statusText: '没有需要重置的 worksheet' });
wx.showToast({ title: '无数据', icon: 'none' });
return;
}
const patchRes = await callCloudFunction('worksheetsBatchPatch', {
patches,
dryRun: false,
});
if (!patchRes.success) {
throw new Error(patchRes.message || '重置失败');
}
const elapsed = Math.round((Date.now() - startedAt) / 1000);
const text = [
summarizeResult(
'初始化收藏和下载数据(likes / downloads → 0',
patchRes,
),
`共 ${patches.length} 条 worksheet`,
`耗时:${elapsed}s`,
].join('\n');
this.setData({ statusText: text });
wx.showToast({ title: '已重置', icon: 'success' });
} catch (error) {
const msg = error instanceof Error ? error.message : '重置失败';
this.setData({ statusText: msg });
wx.showToast({ title: msg, icon: 'none' });
} finally {
wx.hideLoading();
this.setData({ loadingResetStats: false });
}
},
});