feat: 增加收藏、下载种子数据功能
This commit is contained in:
@@ -35,6 +35,13 @@ function difficultyLabel(difficulty) {
|
|||||||
return DIFFICULTY_LABELS[difficulty] || '基础';
|
return DIFFICULTY_LABELS[difficulty] || '基础';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function mergeSeedStats(items) {
|
||||||
|
for (const item of items) {
|
||||||
|
item.likes = (item.likes || 0) + (item.likes_seed || 0);
|
||||||
|
item.downloads = (item.downloads || 0) + (item.downloads_seed || 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function toRecommendedItem(ws) {
|
function toRecommendedItem(ws) {
|
||||||
return {
|
return {
|
||||||
worksheetId: ws._id,
|
worksheetId: ws._id,
|
||||||
@@ -118,6 +125,7 @@ exports.main = async (event) => {
|
|||||||
const band = AGE_BANDS[ageKey];
|
const band = AGE_BANDS[ageKey];
|
||||||
const db = cloud.database();
|
const db = cloud.database();
|
||||||
const activeWorksheets = await queryActiveWorksheets(db);
|
const activeWorksheets = await queryActiveWorksheets(db);
|
||||||
|
mergeSeedStats(activeWorksheets);
|
||||||
const matchedWorksheets = activeWorksheets.filter((ws) =>
|
const matchedWorksheets = activeWorksheets.filter((ws) =>
|
||||||
coversAgeBand(ws, band),
|
coversAgeBand(ws, band),
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -15,6 +15,13 @@ function ageBand(min, max) {
|
|||||||
return `${min}-${max}岁`;
|
return `${min}-${max}岁`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function mergeSeedStats(items) {
|
||||||
|
for (const item of items) {
|
||||||
|
item.likes = (item.likes || 0) + (item.likes_seed || 0);
|
||||||
|
item.downloads = (item.downloads || 0) + (item.downloads_seed || 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function toHomeDisplayItem(ws) {
|
function toHomeDisplayItem(ws) {
|
||||||
return {
|
return {
|
||||||
id: ws._id,
|
id: ws._id,
|
||||||
@@ -87,21 +94,21 @@ exports.main = async () => {
|
|||||||
return { success: true, message: '定时任务已暂停,跳过执行' };
|
return { success: true, message: '定时任务已暂停,跳过执行' };
|
||||||
}
|
}
|
||||||
|
|
||||||
// Query top 5 by downloads
|
const { data: allActive } = await db
|
||||||
const { data: topDownloads } = await db
|
|
||||||
.collection('worksheets')
|
.collection('worksheets')
|
||||||
.where({ status: 'active' })
|
.where({ status: 'active' })
|
||||||
.orderBy('downloads', 'desc')
|
.limit(100)
|
||||||
.limit(5)
|
|
||||||
.get();
|
.get();
|
||||||
|
|
||||||
// Query top 5 by likes
|
mergeSeedStats(allActive);
|
||||||
const { data: topLikes } = await db
|
|
||||||
.collection('worksheets')
|
const topDownloads = [...allActive]
|
||||||
.where({ status: 'active' })
|
.sort((a, b) => (b.downloads || 0) - (a.downloads || 0))
|
||||||
.orderBy('likes', 'desc')
|
.slice(0, 5);
|
||||||
.limit(5)
|
|
||||||
.get();
|
const topLikes = [...allActive]
|
||||||
|
.sort((a, b) => (b.likes || 0) - (a.likes || 0))
|
||||||
|
.slice(0, 5);
|
||||||
|
|
||||||
const featured = topDownloads.map(toHomeDisplayItem);
|
const featured = topDownloads.map(toHomeDisplayItem);
|
||||||
const hot = topLikes.map(toHomeDisplayItem);
|
const hot = topLikes.map(toHomeDisplayItem);
|
||||||
|
|||||||
@@ -28,8 +28,8 @@ function toDisplayItem(ws) {
|
|||||||
difficultyLabel: DIFFICULTY_LABELS[ws.difficulty] || '基础',
|
difficultyLabel: DIFFICULTY_LABELS[ws.difficulty] || '基础',
|
||||||
path: ws.path || '',
|
path: ws.path || '',
|
||||||
available: true,
|
available: true,
|
||||||
likes: ws.likes || 0,
|
likes: (ws.likes || 0) + (ws.likes_seed || 0),
|
||||||
downloads: ws.downloads || 0,
|
downloads: (ws.downloads || 0) + (ws.downloads_seed || 0),
|
||||||
date: ws.updatedAt
|
date: ws.updatedAt
|
||||||
? new Date(ws.updatedAt).toISOString().slice(0, 10)
|
? new Date(ws.updatedAt).toISOString().slice(0, 10)
|
||||||
: new Date().toISOString().slice(0, 10),
|
: new Date().toISOString().slice(0, 10),
|
||||||
|
|||||||
@@ -74,11 +74,40 @@ function buildUpdateData(db, patch) {
|
|||||||
hasField = true;
|
hasField = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (patch.likes_seed !== undefined) {
|
||||||
|
const v = toInt(patch.likes_seed, -1);
|
||||||
|
if (v < 0) return { id, ok: false, error: 'likes_seed 须为非负整数' };
|
||||||
|
data.likes_seed = v;
|
||||||
|
hasField = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (patch.downloads_seed !== undefined) {
|
||||||
|
const v = toInt(patch.downloads_seed, -1);
|
||||||
|
if (v < 0)
|
||||||
|
return { id, ok: false, error: 'downloads_seed 须为非负整数' };
|
||||||
|
data.downloads_seed = v;
|
||||||
|
hasField = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (patch.likes !== undefined) {
|
||||||
|
const v = toInt(patch.likes, -1);
|
||||||
|
if (v < 0) return { id, ok: false, error: 'likes 须为非负整数' };
|
||||||
|
data.likes = v;
|
||||||
|
hasField = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (patch.downloads !== undefined) {
|
||||||
|
const v = toInt(patch.downloads, -1);
|
||||||
|
if (v < 0) return { id, ok: false, error: 'downloads 须为非负整数' };
|
||||||
|
data.downloads = v;
|
||||||
|
hasField = true;
|
||||||
|
}
|
||||||
|
|
||||||
if (!hasField) {
|
if (!hasField) {
|
||||||
return {
|
return {
|
||||||
id,
|
id,
|
||||||
ok: false,
|
ok: false,
|
||||||
error: '未包含可更新字段(ageMin/ageMax 或 tags)',
|
error: '未包含可更新字段(ageMin/ageMax、tags、seed 或 stats)',
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -5,6 +5,13 @@ cloud.init({ env: cloud.DYNAMIC_CURRENT_ENV });
|
|||||||
const VALID_STATUS = new Set(['draft', 'active', 'hidden']);
|
const VALID_STATUS = new Set(['draft', 'active', 'hidden']);
|
||||||
const STATUS_ORDER = { draft: 0, hidden: 1, active: 2 };
|
const STATUS_ORDER = { draft: 0, hidden: 1, active: 2 };
|
||||||
|
|
||||||
|
function mergeSeedStats(items) {
|
||||||
|
for (const item of items) {
|
||||||
|
item.likes = (item.likes || 0) + (item.likes_seed || 0);
|
||||||
|
item.downloads = (item.downloads || 0) + (item.downloads_seed || 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
exports.main = async (event) => {
|
exports.main = async (event) => {
|
||||||
try {
|
try {
|
||||||
const db = cloud.database();
|
const db = cloud.database();
|
||||||
@@ -54,6 +61,8 @@ exports.main = async (event) => {
|
|||||||
else if (item.status === 'hidden') stats.hidden++;
|
else if (item.status === 'hidden') stats.hidden++;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
mergeSeedStats(data);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
success: true,
|
success: true,
|
||||||
data,
|
data,
|
||||||
|
|||||||
@@ -0,0 +1,83 @@
|
|||||||
|
# 展示基线方案(Display Seed Baseline)
|
||||||
|
|
||||||
|
## 背景
|
||||||
|
|
||||||
|
worksheet 的 `likes`(收藏)和 `downloads`(下载)初始值为 0,对用户缺乏吸引力。
|
||||||
|
之前的做法是直接修改数据库真实字段,导致无法区分哪些是运营配置的底数、哪些是真实用户行为。
|
||||||
|
|
||||||
|
## 方案概述
|
||||||
|
|
||||||
|
**数据层拆开:真实 + 展示基线。**
|
||||||
|
|
||||||
|
| 字段 | 含义 | 谁在写 | 初始值 |
|
||||||
|
|------|------|--------|--------|
|
||||||
|
| `likes` | 真实用户收藏累加 | 云函数 `worksheetsStatsUpdate` / `userFavorites` 的 `inc(1)` | 0 |
|
||||||
|
| `downloads` | 真实用户下载累加 | 云函数 `worksheetsStatsUpdate` / `userDownloadLogs` 的 `inc(1)` | 0 |
|
||||||
|
| `likes_seed` | 运营配置的收藏基线 | worksheetSync 页面一键同步 | 0 |
|
||||||
|
| `downloads_seed` | 运营配置的下载基线 | worksheetSync 页面一键同步 | 0 |
|
||||||
|
|
||||||
|
**展示值** = `likes + likes_seed`(或 `downloads + downloads_seed`)
|
||||||
|
|
||||||
|
**运营看板** 只看 `likes` / `downloads`(真实值),不受 seed 污染。
|
||||||
|
|
||||||
|
## 涉及改动
|
||||||
|
|
||||||
|
### 1. 数据库 `worksheets` 集合
|
||||||
|
|
||||||
|
每条 worksheet 新增两个字段:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"likes_seed": 20,
|
||||||
|
"downloads_seed": 15
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Seed 数据来源
|
||||||
|
|
||||||
|
文件路径:`miniprogram/supportPages/worksheetSync/mockLikes.ts`
|
||||||
|
|
||||||
|
导出 `MOCK_SEED_COUNTS`,以 worksheet id 为 key,值为 `{ downloads, likes }`。
|
||||||
|
|
||||||
|
### 3. worksheetSync 管理页
|
||||||
|
|
||||||
|
新增「同步 Seed 数据」按钮,点击后:
|
||||||
|
|
||||||
|
1. 读取 `MOCK_SEED_COUNTS`
|
||||||
|
2. 构建 patches:`[{ id, likes_seed, downloads_seed }, ...]`
|
||||||
|
3. 调用 `worksheetsBatchPatch` 批量写入
|
||||||
|
|
||||||
|
### 4. 云函数改动
|
||||||
|
|
||||||
|
所有对外返回 `likes` / `downloads` 的云函数,在返回前合并 seed 值:
|
||||||
|
|
||||||
|
```js
|
||||||
|
item.likes = (item.likes || 0) + (item.likes_seed || 0);
|
||||||
|
item.downloads = (item.downloads || 0) + (item.downloads_seed || 0);
|
||||||
|
```
|
||||||
|
|
||||||
|
涉及的云函数:
|
||||||
|
|
||||||
|
| 云函数 | 改动点 |
|
||||||
|
|--------|--------|
|
||||||
|
| `worksheetsQuery` | 返回数据前合并 seed |
|
||||||
|
| `ageRecommendedQuery` | 合并 seed 后再排序和取 Top N |
|
||||||
|
| `homeAutoRefresh` | 改为全量查询 + 合并 seed + 内存排序 |
|
||||||
|
| `pageContentBuild` | `toDisplayItem` 中合并 seed |
|
||||||
|
|
||||||
|
### 5. 不需要改动的部分
|
||||||
|
|
||||||
|
| 模块 | 原因 |
|
||||||
|
|------|------|
|
||||||
|
| `worksheetsStatsUpdate` | 只 `inc(1)` 真实字段,不涉及 seed |
|
||||||
|
| `userDownloadLogs` | 只 `inc(1)` downloads,不涉及 seed |
|
||||||
|
| `userFavorites` | 只 `inc(1)` likes,不涉及 seed |
|
||||||
|
| `worksheetsPublish` | 发布时设置真实初始值,不涉及 seed |
|
||||||
|
| 用户端收藏/下载交互 | 保持现有行为不变 |
|
||||||
|
|
||||||
|
## 优势
|
||||||
|
|
||||||
|
1. **真实数据干净**:`likes` / `downloads` 永远只反映真实用户行为
|
||||||
|
2. **全站一致**:所有出口在云端统一计算展示值,端上无需额外逻辑
|
||||||
|
3. **调整无需发版**:修改 seed 后通过管理页一键同步,不用重新构建小程序
|
||||||
|
4. **运营可判断**:直接查 DB 的 `likes` / `downloads` 字段即为真实数据
|
||||||
@@ -18,7 +18,8 @@
|
|||||||
"categoryContentManage/categoryContentManage",
|
"categoryContentManage/categoryContentManage",
|
||||||
"homeContentManage/homeContentManage",
|
"homeContentManage/homeContentManage",
|
||||||
"mathIndex/mathIndex",
|
"mathIndex/mathIndex",
|
||||||
"focusIndex/focusIndex"
|
"focusIndex/focusIndex",
|
||||||
|
"worksheetSync/worksheetSync"
|
||||||
],
|
],
|
||||||
"independent": false
|
"independent": false
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -49,6 +49,38 @@ const TAB_BAR_PATHS = new Set([
|
|||||||
// Page-level mutable state (shared across methods)
|
// Page-level mutable state (shared across methods)
|
||||||
let _categoryData: CategoryDataset | null = null;
|
let _categoryData: CategoryDataset | null = null;
|
||||||
|
|
||||||
|
// --- 以下仅为本地调试:拉取分类数据后查看各 worksheet 的 downloads / likes,非业务逻辑 ---
|
||||||
|
|
||||||
|
function debugLogWorksheetStatsAfterCategoryLoad(
|
||||||
|
source:
|
||||||
|
| { kind: 'cloud'; rows: Record<string, any>[] }
|
||||||
|
| { kind: 'static'; data: CategoryDataset },
|
||||||
|
) {
|
||||||
|
let map: Record<string, { downloads: number; likes: number }> = {};
|
||||||
|
if (source.kind === 'cloud') {
|
||||||
|
for (const raw of source.rows) {
|
||||||
|
const id = String(raw._id || '').trim();
|
||||||
|
if (!id) continue;
|
||||||
|
map[id] = {
|
||||||
|
downloads: Number(raw.downloads) || 0,
|
||||||
|
likes: Number(raw.likes) || 0,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
for (const group of source.data.categories) {
|
||||||
|
for (const item of group.items) {
|
||||||
|
map[item.id] = {
|
||||||
|
downloads: Number(item.downloads) || 0,
|
||||||
|
likes: Number(item.likes) || 0,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
console.log('worksheetStatsById', JSON.stringify(map, null, 2));
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- 调试块结束 ---
|
||||||
|
|
||||||
/** 将云端 worksheet 原始数据转换为 CategoryItem */
|
/** 将云端 worksheet 原始数据转换为 CategoryItem */
|
||||||
function toDisplayItem(raw: Record<string, any>): CategoryItem {
|
function toDisplayItem(raw: Record<string, any>): CategoryItem {
|
||||||
const difficulty = (Number(raw.difficulty) || 2) as 1 | 2 | 3 | 4;
|
const difficulty = (Number(raw.difficulty) || 2) as 1 | 2 | 3 | 4;
|
||||||
@@ -129,12 +161,16 @@ const DYNAMIC_LIMIT = 12;
|
|||||||
|
|
||||||
function buildHotItems(): CategoryItem[] {
|
function buildHotItems(): CategoryItem[] {
|
||||||
const all = buildAllItems();
|
const all = buildAllItems();
|
||||||
return [...all].sort((a, b) => (b.likes || 0) - (a.likes || 0)).slice(0, DYNAMIC_LIMIT);
|
return [...all]
|
||||||
|
.sort((a, b) => (b.likes || 0) - (a.likes || 0))
|
||||||
|
.slice(0, DYNAMIC_LIMIT);
|
||||||
}
|
}
|
||||||
|
|
||||||
function buildTopDownloadsItems(): CategoryItem[] {
|
function buildTopDownloadsItems(): CategoryItem[] {
|
||||||
const all = buildAllItems();
|
const all = buildAllItems();
|
||||||
return [...all].sort((a, b) => (b.downloads || 0) - (a.downloads || 0)).slice(0, DYNAMIC_LIMIT);
|
return [...all]
|
||||||
|
.sort((a, b) => (b.downloads || 0) - (a.downloads || 0))
|
||||||
|
.slice(0, DYNAMIC_LIMIT);
|
||||||
}
|
}
|
||||||
|
|
||||||
function getItemsByCategory(categoryId: string): CategoryItem[] {
|
function getItemsByCategory(categoryId: string): CategoryItem[] {
|
||||||
@@ -203,9 +239,15 @@ Page({
|
|||||||
const result = (res?.result as any) || {};
|
const result = (res?.result as any) || {};
|
||||||
if (!result.success) throw new Error(result.message || '查询失败');
|
if (!result.success) throw new Error(result.message || '查询失败');
|
||||||
|
|
||||||
_categoryData = buildCategoryDataFromCloud(result.data || []);
|
const rows = result.data || [];
|
||||||
|
_categoryData = buildCategoryDataFromCloud(rows);
|
||||||
|
debugLogWorksheetStatsAfterCategoryLoad({ kind: 'cloud', rows });
|
||||||
} catch {
|
} catch {
|
||||||
_categoryData = CATEGORY_DATA;
|
_categoryData = CATEGORY_DATA;
|
||||||
|
debugLogWorksheetStatsAfterCategoryLoad({
|
||||||
|
kind: 'static',
|
||||||
|
data: CATEGORY_DATA,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const activeId =
|
const activeId =
|
||||||
|
|||||||
@@ -0,0 +1,169 @@
|
|||||||
|
export const MOCK_SEED_COUNTS: Record<
|
||||||
|
string,
|
||||||
|
{ downloads: number; likes: number }
|
||||||
|
> = {
|
||||||
|
'word-coloring-grid': {
|
||||||
|
downloads: 0,
|
||||||
|
likes: 1,
|
||||||
|
},
|
||||||
|
'word-coloring-find': {
|
||||||
|
downloads: 0,
|
||||||
|
likes: 0,
|
||||||
|
},
|
||||||
|
'handwriting-sheet': {
|
||||||
|
downloads: 0,
|
||||||
|
likes: 0,
|
||||||
|
},
|
||||||
|
'letter-tracing-single': {
|
||||||
|
downloads: 10,
|
||||||
|
likes: 3,
|
||||||
|
},
|
||||||
|
'letter-tracing-two-column': {
|
||||||
|
downloads: 15,
|
||||||
|
likes: 13,
|
||||||
|
},
|
||||||
|
'letter-tracing-upper-lower': {
|
||||||
|
downloads: 3,
|
||||||
|
likes: 7,
|
||||||
|
},
|
||||||
|
'letter-tracing-case-pairing': {
|
||||||
|
downloads: 9,
|
||||||
|
likes: 9,
|
||||||
|
},
|
||||||
|
'letter-tracing-three': {
|
||||||
|
downloads: 12,
|
||||||
|
likes: 7,
|
||||||
|
},
|
||||||
|
'letter-tracing-half': {
|
||||||
|
downloads: 2,
|
||||||
|
likes: 1,
|
||||||
|
},
|
||||||
|
'letter-tracing-daily-checkin': {
|
||||||
|
downloads: 12,
|
||||||
|
likes: 14,
|
||||||
|
},
|
||||||
|
'number-find': {
|
||||||
|
downloads: 20,
|
||||||
|
likes: 23,
|
||||||
|
},
|
||||||
|
'number-coloring': {
|
||||||
|
downloads: 37,
|
||||||
|
likes: 41,
|
||||||
|
},
|
||||||
|
'counting-matching': {
|
||||||
|
downloads: 20,
|
||||||
|
likes: 28,
|
||||||
|
},
|
||||||
|
'number-object-match': {
|
||||||
|
downloads: 12,
|
||||||
|
likes: 17,
|
||||||
|
},
|
||||||
|
'counting-select': {
|
||||||
|
downloads: 10,
|
||||||
|
likes: 9,
|
||||||
|
},
|
||||||
|
compare: {
|
||||||
|
downloads: 15,
|
||||||
|
likes: 10,
|
||||||
|
},
|
||||||
|
'number-sort': {
|
||||||
|
downloads: 35,
|
||||||
|
likes: 47,
|
||||||
|
},
|
||||||
|
'missing-number': {
|
||||||
|
downloads: 13,
|
||||||
|
likes: 10,
|
||||||
|
},
|
||||||
|
'number-decompose': {
|
||||||
|
downloads: 26,
|
||||||
|
likes: 21,
|
||||||
|
},
|
||||||
|
'number-decompose-20': {
|
||||||
|
downloads: 21,
|
||||||
|
likes: 18,
|
||||||
|
},
|
||||||
|
'one-digit-addition': {
|
||||||
|
downloads: 12,
|
||||||
|
likes: 15,
|
||||||
|
},
|
||||||
|
'addition-5': {
|
||||||
|
downloads: 11,
|
||||||
|
likes: 12,
|
||||||
|
},
|
||||||
|
'make-ten': {
|
||||||
|
downloads: 38,
|
||||||
|
likes: 35,
|
||||||
|
},
|
||||||
|
'break-ten': {
|
||||||
|
downloads: 0,
|
||||||
|
likes: 24,
|
||||||
|
},
|
||||||
|
'flat-ten': {
|
||||||
|
downloads: 26,
|
||||||
|
likes: 26,
|
||||||
|
},
|
||||||
|
'borrow-ten': {
|
||||||
|
downloads: 24,
|
||||||
|
likes: 12,
|
||||||
|
},
|
||||||
|
'practice-addition': {
|
||||||
|
downloads: 31,
|
||||||
|
likes: 21,
|
||||||
|
},
|
||||||
|
'practice-subtraction': {
|
||||||
|
downloads: 12,
|
||||||
|
likes: 14,
|
||||||
|
},
|
||||||
|
'practice-mixed': {
|
||||||
|
downloads: 22,
|
||||||
|
likes: 25,
|
||||||
|
},
|
||||||
|
'multiplication-table': {
|
||||||
|
downloads: 2,
|
||||||
|
likes: 5,
|
||||||
|
},
|
||||||
|
'color-shape-match': {
|
||||||
|
downloads: 22,
|
||||||
|
likes: 23,
|
||||||
|
},
|
||||||
|
'shape-symbol': {
|
||||||
|
downloads: 7,
|
||||||
|
likes: 7,
|
||||||
|
},
|
||||||
|
'position-coloring': {
|
||||||
|
downloads: 18,
|
||||||
|
likes: 20,
|
||||||
|
},
|
||||||
|
'color-pattern': {
|
||||||
|
downloads: 15,
|
||||||
|
likes: 15,
|
||||||
|
},
|
||||||
|
'match-connect': {
|
||||||
|
downloads: 10,
|
||||||
|
likes: 10,
|
||||||
|
},
|
||||||
|
'line-recognition': {
|
||||||
|
downloads: 20,
|
||||||
|
likes: 23,
|
||||||
|
},
|
||||||
|
'grid-reasoning': {
|
||||||
|
downloads: 17,
|
||||||
|
likes: 16,
|
||||||
|
},
|
||||||
|
'code-connect': {
|
||||||
|
downloads: 32,
|
||||||
|
likes: 29,
|
||||||
|
},
|
||||||
|
'dot-connect': {
|
||||||
|
downloads: 10,
|
||||||
|
likes: 20,
|
||||||
|
},
|
||||||
|
'grid-drawing-3x3': {
|
||||||
|
downloads: 20,
|
||||||
|
likes: 23,
|
||||||
|
},
|
||||||
|
'grid-drawing-5x5': {
|
||||||
|
downloads: 8,
|
||||||
|
likes: 12,
|
||||||
|
},
|
||||||
|
};
|
||||||
@@ -1,3 +1,5 @@
|
|||||||
|
import { MOCK_SEED_COUNTS } from './mockLikes';
|
||||||
|
|
||||||
type CloudFunctionResult<T> = {
|
type CloudFunctionResult<T> = {
|
||||||
success?: boolean;
|
success?: boolean;
|
||||||
message?: string;
|
message?: string;
|
||||||
@@ -134,6 +136,8 @@ Page({
|
|||||||
loadingAges: false,
|
loadingAges: false,
|
||||||
loadingTags: false,
|
loadingTags: false,
|
||||||
loadingRebuild: false,
|
loadingRebuild: false,
|
||||||
|
loadingSeed: false,
|
||||||
|
loadingResetStats: false,
|
||||||
statusText: '配置加载中,请稍候',
|
statusText: '配置加载中,请稍候',
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -312,4 +316,138 @@ Page({
|
|||||||
this.setData({ loadingRebuild: false });
|
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(
|
||||||
|
'同步 Seed(likes_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 });
|
||||||
|
}
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -52,23 +52,37 @@
|
|||||||
type="primary"
|
type="primary"
|
||||||
width="100%"
|
width="100%"
|
||||||
loading="{{loadingAges}}"
|
loading="{{loadingAges}}"
|
||||||
disabled="{{!configReady || loadingAges || loadingTags || loadingRebuild}}"
|
disabled="{{!configReady || loadingAges || loadingTags || loadingRebuild || loadingSeed || loadingResetStats}}"
|
||||||
bindtap="onSyncAges">
|
bindtap="onSyncAges">
|
||||||
{{loadingAges ? '同步中...' : '同步年龄'}}
|
{{loadingAges ? '同步中...' : '同步年龄'}}
|
||||||
</toy-button>
|
</toy-button>
|
||||||
<toy-button
|
<toy-button
|
||||||
width="100%"
|
width="100%"
|
||||||
loading="{{loadingTags}}"
|
loading="{{loadingTags}}"
|
||||||
disabled="{{!configReady || loadingAges || loadingTags || loadingRebuild}}"
|
disabled="{{!configReady || loadingAges || loadingTags || loadingRebuild || loadingSeed || loadingResetStats}}"
|
||||||
bindtap="onSyncTags">
|
bindtap="onSyncTags">
|
||||||
{{loadingTags ? '同步中...' : '同步标签'}}
|
{{loadingTags ? '同步中...' : '同步标签'}}
|
||||||
</toy-button>
|
</toy-button>
|
||||||
<toy-button
|
<toy-button
|
||||||
width="100%"
|
width="100%"
|
||||||
loading="{{loadingRebuild}}"
|
loading="{{loadingRebuild}}"
|
||||||
disabled="{{loadingAges || loadingTags || loadingRebuild}}"
|
disabled="{{loadingAges || loadingTags || loadingRebuild || loadingSeed || loadingResetStats}}"
|
||||||
bindtap="onRebuildConfig">
|
bindtap="onRebuildConfig">
|
||||||
{{loadingRebuild ? '重建中...' : '重建配置'}}
|
{{loadingRebuild ? '重建中...' : '重建配置'}}
|
||||||
</toy-button>
|
</toy-button>
|
||||||
|
<toy-button
|
||||||
|
width="100%"
|
||||||
|
loading="{{loadingSeed}}"
|
||||||
|
disabled="{{loadingAges || loadingTags || loadingRebuild || loadingSeed || loadingResetStats}}"
|
||||||
|
bindtap="onSyncSeed">
|
||||||
|
{{loadingSeed ? '同步中...' : '同步 Seed 数据'}}
|
||||||
|
</toy-button>
|
||||||
|
<toy-button
|
||||||
|
width="100%"
|
||||||
|
loading="{{loadingResetStats}}"
|
||||||
|
disabled="{{loadingAges || loadingTags || loadingRebuild || loadingSeed || loadingResetStats}}"
|
||||||
|
bindtap="onResetStats">
|
||||||
|
{{loadingResetStats ? '重置中...' : '初始化收藏和下载数据'}}
|
||||||
|
</toy-button>
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
|
|||||||
Reference in New Issue
Block a user