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:
@@ -38,47 +38,38 @@ exports.main = async (event) => {
|
||||
const userMap = new Map()
|
||||
let skip = 0
|
||||
|
||||
// Defense-in-depth: if a user somehow ends up with multiple docs in the
|
||||
// collection (e.g. before the client-side fix to _doPush landed, or via
|
||||
// a buggy data migration), only the doc with the latest `updatedAt`
|
||||
// counts for each openid. Otherwise we'd sum the same records N times
|
||||
// and inflate the board.
|
||||
//
|
||||
// Why "latest updatedAt" instead of "first one we see": paginated reads
|
||||
// come back in `_id` ascending order, so "first one" is the OLDEST doc.
|
||||
// Records content is identical across duplicates (both came from the
|
||||
// same local state), so duration/sessions are unaffected — but the
|
||||
// oldest doc carries the OLDEST profile.nickname. Picking the latest
|
||||
// doc gives the freshest nickname even before admin-dedupe runs.
|
||||
const tsOf = (d) => {
|
||||
if (!d.updatedAt) return 0
|
||||
// CloudBase returns Date objects in the SDK, but be defensive in case
|
||||
// a stringified ISO timestamp sneaks through (older SDKs / migrations).
|
||||
if (d.updatedAt instanceof Date) return d.updatedAt.getTime()
|
||||
const t = new Date(d.updatedAt).getTime()
|
||||
return Number.isFinite(t) ? t : 0
|
||||
}
|
||||
|
||||
const latestByOpenid = new Map()
|
||||
|
||||
while (true) {
|
||||
const res = await db.collection(COLLECTION).skip(skip).limit(PAGE_SIZE).get()
|
||||
if (!res.data || res.data.length === 0) break
|
||||
|
||||
for (const doc of res.data) {
|
||||
const records = doc.records || {}
|
||||
let duration = 0
|
||||
let sessions = 0
|
||||
|
||||
for (const monthKey of Object.keys(records)) {
|
||||
if (prefix && !monthKey.startsWith(prefix)) continue
|
||||
for (const r of records[monthKey]) {
|
||||
if (period === 'day') {
|
||||
// Use startsWith rather than === to tolerate date strings with
|
||||
// trailing time/zone info (e.g. "2026-06-11T09:11:24.587Z" if
|
||||
// some legacy path stored an ISO date) and to be timezone-
|
||||
// agnostic when client local date differs from server UTC date.
|
||||
if (r.date.startsWith(exact)) { duration += r.duration; sessions++ }
|
||||
} else {
|
||||
if (r.date.startsWith(prefix)) { duration += r.duration; sessions++ }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (duration > 0) {
|
||||
const openid = doc._openid || 'unknown'
|
||||
const entry = userMap.get(openid)
|
||||
const profile = doc.profile || {}
|
||||
if (entry) {
|
||||
entry.duration += duration
|
||||
entry.sessions += sessions
|
||||
// Refresh nickname in case the user updated it since last write
|
||||
if (profile.nickname) entry.nickname = profile.nickname
|
||||
} else {
|
||||
userMap.set(openid, {
|
||||
openid,
|
||||
duration,
|
||||
sessions,
|
||||
nickname: profile.nickname || ''
|
||||
})
|
||||
}
|
||||
const openid = doc._openid || 'unknown'
|
||||
const current = latestByOpenid.get(openid)
|
||||
if (!current || tsOf(doc) > tsOf(current)) {
|
||||
latestByOpenid.set(openid, doc)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -86,6 +77,40 @@ exports.main = async (event) => {
|
||||
skip += PAGE_SIZE
|
||||
}
|
||||
|
||||
// Second pass: each openid appears exactly once, so the accumulation
|
||||
// logic doesn't need any dedup guards.
|
||||
for (const doc of latestByOpenid.values()) {
|
||||
const openid = doc._openid || 'unknown'
|
||||
const records = doc.records || {}
|
||||
let duration = 0
|
||||
let sessions = 0
|
||||
|
||||
for (const monthKey of Object.keys(records)) {
|
||||
if (prefix && !monthKey.startsWith(prefix)) continue
|
||||
for (const r of records[monthKey]) {
|
||||
if (period === 'day') {
|
||||
// Use startsWith rather than === to tolerate date strings with
|
||||
// trailing time/zone info (e.g. "2026-06-11T09:11:24.587Z" if
|
||||
// some legacy path stored an ISO date) and to be timezone-
|
||||
// agnostic when client local date differs from server UTC date.
|
||||
if (r.date.startsWith(exact)) { duration += r.duration; sessions++ }
|
||||
} else {
|
||||
if (r.date.startsWith(prefix)) { duration += r.duration; sessions++ }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (duration > 0) {
|
||||
const profile = doc.profile || {}
|
||||
userMap.set(openid, {
|
||||
openid,
|
||||
duration,
|
||||
sessions,
|
||||
nickname: profile.nickname || ''
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Full sorted list (for ranking)
|
||||
const allSorted = Array.from(userMap.values())
|
||||
.sort((a, b) => b.duration - a.duration)
|
||||
|
||||
Reference in New Issue
Block a user