fix: 修复排行榜数据自增长(冷启动重复创建云端文档)
每次小程序冷启动都会为同一用户创建新的云端文档, 导致 leaderboard 把同一份数据累加 N 次。 改动: - app.js: onLaunch 改为 async,每次启动先 pullAll 填充 _openid / _docId 缓存 - utils/cloud.js: 引入 _ensureOpenid() 单次解析;_doPush 总是先按 openid 定位现有文档;update 失败不再回退到 add()(避免创建重复文档) - utils/storage.js: getTodayRecord 按日期部分比较,修复一天第二次训练
This commit is contained in:
@@ -0,0 +1,93 @@
|
||||
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 () => {
|
||||
// 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)
|
||||
)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user