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 (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) { return { id, ok: false, error: '未包含可更新字段(ageMin/ageMax、tags、seed 或 stats)', }; } 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 : '批量更新失败', }; } };