diff --git a/cloudfunctions/leaderboard/index.js b/cloudfunctions/leaderboard/index.js index de0d675..c712edc 100644 --- a/cloudfunctions/leaderboard/index.js +++ b/cloudfunctions/leaderboard/index.js @@ -107,15 +107,21 @@ exports.main = async (event) => { for (const monthKey of Object.keys(records)) { if (prefix && !monthKey.startsWith(prefix)) continue - for (const r of records[monthKey]) { + const arr = records[monthKey] + if (!Array.isArray(arr)) continue + for (const r of arr) { + // 防御脏数据:r.date 缺失/非字符串、duration 非数字时跳过, + // 避免单条坏记录抛 TypeError 导致整个排行榜对所有人失败 + if (!r || typeof r.date !== 'string') continue + const dur = Number(r.duration) || 0 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++ } + if (r.date.startsWith(exact)) { duration += dur; sessions++ } } else { - if (r.date.startsWith(prefix)) { duration += r.duration; sessions++ } + if (r.date.startsWith(prefix)) { duration += dur; sessions++ } } } } diff --git a/cloudfunctions/tts/index.js b/cloudfunctions/tts/index.js index 50a9cc5..0cbe409 100644 --- a/cloudfunctions/tts/index.js +++ b/cloudfunctions/tts/index.js @@ -61,8 +61,9 @@ const _synthesize = async (text, promptKey, opts) => { return Buffer.from(res.Audio, 'base64') } -const _upload = async (key, buffer, voiceType) => { - const cloudPath = `${CACHE_DIR}/${key}-${voiceType || 'default'}.mp3` +const _upload = async (key, buffer, voiceType, volume, speed) => { + // cloudPath 含 volume/speed,避免不同音量/语速的音频互相覆盖(缓存碰撞) + const cloudPath = `${CACHE_DIR}/${key}-${voiceType || 'default'}-v${volume != null ? volume : 0}-s${speed != null ? speed : 0}.mp3` // uploadFile returns the canonical fileID (e.g. "cloud://env.xxx/..."). // We must use THAT — not the relative cloudPath we passed in — when // calling getTempFileURL. A self-constructed fileID is invalid. @@ -94,7 +95,7 @@ exports.main = async (event) => { // Slow path: synthesize + upload + return fresh fileID for caching try { const buffer = await _synthesize(text, promptKey, { voiceType, volume, speed }) - const newFileID = await _upload(promptKey, buffer, voiceType) + const newFileID = await _upload(promptKey, buffer, voiceType, volume, speed) const urlRes = await cloud.getTempFileURL({ fileList: [newFileID] }) const url = urlRes.fileList[0].tempFileURL if (!url) { diff --git a/pages/index/index.js b/pages/index/index.js index 966b0ea..01d1437 100644 --- a/pages/index/index.js +++ b/pages/index/index.js @@ -136,7 +136,7 @@ Page({ const plan = planMod.getPlan(settings.planId, customPlans) const planDay = planMod.getPlanDay(settings.planId, allRecords, settings.planStartDate, customPlans) const clampedDay = Math.min(planDay, plan.totalDays) - const target = planMod.getTodayTarget(settings.planId, clampedDay) + const target = planMod.getTodayTarget(settings.planId, clampedDay, customPlans) const theme = themeMod.getCurrentTheme() // 防御:totalDays 为 0(极端数据损坏)时避免除以 0 得到 NaN diff --git a/pages/leaderboard/leaderboard.js b/pages/leaderboard/leaderboard.js index 69952ac..91e2b0a 100644 --- a/pages/leaderboard/leaderboard.js +++ b/pages/leaderboard/leaderboard.js @@ -54,14 +54,19 @@ Page({ }, fetchRank(cb) { + // 序列号:快速切换周期时丢弃旧响应,避免旧 period 的数据覆盖新 period + this._fetchSeq = (this._fetchSeq || 0) + 1 + const seq = this._fetchSeq + const period = this.data.activePeriod this.setData({ loading: true, empty: false }) wx.cloud.callFunction({ name: 'leaderboard', data: { - period: this.data.activePeriod, + period, maxRank: config.leaderboardMaxRank } }).then(res => { + if (seq !== this._fetchSeq) { if (cb) cb(); return } // 过期响应,丢弃 this._applyResult(res.result || {}) if (cb) cb() }).catch((err) => { @@ -70,6 +75,7 @@ Page({ // normal (user didn't train today), and a toast would mislead // them into thinking the cloud is broken. console.warn('[leaderboard] cloud function failed, using local fallback:', err) + if (seq !== this._fetchSeq) { if (cb) cb(); return } this._applyLocal() if (cb) cb() }) diff --git a/pages/timer/timer.js b/pages/timer/timer.js index f064c7b..0edbcdd 100644 --- a/pages/timer/timer.js +++ b/pages/timer/timer.js @@ -76,7 +76,7 @@ Page({ const customPlans = storage.getCustomPlans() const plan = planMod.getPlan(settings.planId, customPlans) planDay = planMod.getPlanDay(settings.planId, allRecords, settings.planStartDate, customPlans) - target = planMod.getTodayTarget(settings.planId, Math.min(planDay, plan.totalDays)) + target = planMod.getTodayTarget(settings.planId, Math.min(planDay, plan.totalDays), customPlans) } this.setData({ diff --git a/utils/plan.js b/utils/plan.js index ff1159f..106f899 100644 --- a/utils/plan.js +++ b/utils/plan.js @@ -73,8 +73,8 @@ const getPlan = (planId, customPlans) => { return plans[planId] || plans.beginner } -const getTodayTarget = (planId, currentDay) => { - const plan = getPlan(planId) +const getTodayTarget = (planId, currentDay, customPlans) => { + const plan = getPlan(planId, customPlans) // 防御:计划数据损坏(planId 无效 / days 为空)时直接返回 0, // 避免 plan.days[0] 为 undefined 后访问 .target 抛出崩溃。 if (!plan.days || plan.days.length === 0) return 0