const cloud = require('wx-server-sdk') cloud.init({ env: cloud.DYNAMIC_CURRENT_ENV }) const db = cloud.database() const COLLECTION = 'plank_data' const PAGE_SIZE = 100 /** * One-shot admin tool: deduplicate plank_data docs by _openid. * * Background: before the client-side fix in utils/cloud.js (always locate * by _openid before add()), every cold start of the mini program created * a brand-new cloud doc for the same user. The leaderboard cloud function * then summed duration across N docs for the same _openid, inflating the * board. This function walks the whole collection, groups by _openid, and * deletes every doc except the most-recently-updated one for each user. * * DEPLOYMENT NOTES — read before invoking: * 1. Upload + deploy this function from the WeChat DevTools or `wx-cli`. * 2. The default collection security rule only allows reading docs whose * `doc._openid == auth.openid`, which prevents this function from * seeing other users' docs. Temporarily flip the collection's read * permission to "所有用户可读" in the CloudBase console, OR configure * the function's role to a privileged account that bypasses the * custom rule. * 3. Invoke the function once from the CloudBase console test panel. * 4. **Delete or disable this function immediately afterwards** so it * can't be re-run by a user. It's destructive. */ exports.main = async () => { // 安全闸门:仅允许白名单内的管理员调用,否则直接拒绝,防止被任意用户 // 触发批量删除(该函数以特权身份运行且会删文档)。白名单在云函数环境变量 // ADMIN_OPENIDS 中以逗号分隔配置;未配置时默认拒绝(失败安全),避免误部署后被滥用。 const ctx = cloud.getWXContext() const caller = (ctx && ctx.OPENID) || '' const allowlist = (process.env.ADMIN_OPENIDS || '') .split(',') .map(s => s.trim()) .filter(Boolean) if (!caller || allowlist.length === 0 || !allowlist.includes(caller)) { return { err: 'forbidden', msg: '无权限执行该操作' } } // 1. Page through every doc in the collection. const docs = [] let skip = 0 while (true) { const res = await db.collection(COLLECTION).skip(skip).limit(PAGE_SIZE).get() if (!res.data || res.data.length === 0) break docs.push(...res.data) if (res.data.length < PAGE_SIZE) break skip += PAGE_SIZE } // 2. Sort by updatedAt descending so the freshest doc per openid is // first; that one is the one we keep. const tsOf = (d) => { if (!d.updatedAt) return 0 // CloudBase sometimes returns Date objects, sometimes ISO strings // depending on the SDK version — normalize both. if (d.updatedAt instanceof Date) return d.updatedAt.getTime() const t = new Date(d.updatedAt).getTime() return Number.isFinite(t) ? t : 0 } docs.sort((a, b) => tsOf(b) - tsOf(a)) // 3. Walk the sorted list, marking all but the first occurrence of // each openid for deletion. const seen = new Set() const toDelete = [] const keptByOpenid = {} for (const doc of docs) { const openid = doc._openid || 'unknown' if (seen.has(openid)) { toDelete.push({ id: doc._id, openid }) } else { seen.add(openid) keptByOpenid[openid] = doc._id } } // 4. Delete. Remove one at a time so a single permission/permission // failure doesn't abort the whole cleanup. let deleted = 0 const errors = [] for (const { id, openid } of toDelete) { try { await db.collection(COLLECTION).doc(id).remove() deleted++ } catch (e) { errors.push({ id, openid, msg: (e && e.message) || String(e) }) } } return { scanned: docs.length, uniqueOpenids: seen.size, duplicatesFound: toDelete.length, duplicatesDeleted: deleted, duplicatesFailed: errors.length, errors, sampleKept: Object.fromEntries( Object.entries(keptByOpenid).slice(0, 5) ) } }