fix: 修复审查发现的 4 个高优先级问题
- plan: getTodayTarget 补 customPlans 参数,timer/index 调用点传值,修复自定义计划每日目标失效 - leaderboard: fetchRank 加请求序列号丢弃过期响应,修复快速切换周期竞态 - leaderboard 云函数:遍历记录加防御(r.date/duration/数组校验),避免单条脏数据崩溃全榜 - tts: _upload cloudPath 补 volume/speed,避免不同音量语速音频互相覆盖 Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -107,15 +107,21 @@ exports.main = async (event) => {
|
|||||||
|
|
||||||
for (const monthKey of Object.keys(records)) {
|
for (const monthKey of Object.keys(records)) {
|
||||||
if (prefix && !monthKey.startsWith(prefix)) continue
|
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') {
|
if (period === 'day') {
|
||||||
// Use startsWith rather than === to tolerate date strings with
|
// Use startsWith rather than === to tolerate date strings with
|
||||||
// trailing time/zone info (e.g. "2026-06-11T09:11:24.587Z" if
|
// trailing time/zone info (e.g. "2026-06-11T09:11:24.587Z" if
|
||||||
// some legacy path stored an ISO date) and to be timezone-
|
// some legacy path stored an ISO date) and to be timezone-
|
||||||
// agnostic when client local date differs from server UTC date.
|
// 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 {
|
} else {
|
||||||
if (r.date.startsWith(prefix)) { duration += r.duration; sessions++ }
|
if (r.date.startsWith(prefix)) { duration += dur; sessions++ }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -61,8 +61,9 @@ const _synthesize = async (text, promptKey, opts) => {
|
|||||||
return Buffer.from(res.Audio, 'base64')
|
return Buffer.from(res.Audio, 'base64')
|
||||||
}
|
}
|
||||||
|
|
||||||
const _upload = async (key, buffer, voiceType) => {
|
const _upload = async (key, buffer, voiceType, volume, speed) => {
|
||||||
const cloudPath = `${CACHE_DIR}/${key}-${voiceType || 'default'}.mp3`
|
// 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/...").
|
// uploadFile returns the canonical fileID (e.g. "cloud://env.xxx/...").
|
||||||
// We must use THAT — not the relative cloudPath we passed in — when
|
// We must use THAT — not the relative cloudPath we passed in — when
|
||||||
// calling getTempFileURL. A self-constructed fileID is invalid.
|
// 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
|
// Slow path: synthesize + upload + return fresh fileID for caching
|
||||||
try {
|
try {
|
||||||
const buffer = await _synthesize(text, promptKey, { voiceType, volume, speed })
|
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 urlRes = await cloud.getTempFileURL({ fileList: [newFileID] })
|
||||||
const url = urlRes.fileList[0].tempFileURL
|
const url = urlRes.fileList[0].tempFileURL
|
||||||
if (!url) {
|
if (!url) {
|
||||||
|
|||||||
@@ -136,7 +136,7 @@ Page({
|
|||||||
const plan = planMod.getPlan(settings.planId, customPlans)
|
const plan = planMod.getPlan(settings.planId, customPlans)
|
||||||
const planDay = planMod.getPlanDay(settings.planId, allRecords, settings.planStartDate, customPlans)
|
const planDay = planMod.getPlanDay(settings.planId, allRecords, settings.planStartDate, customPlans)
|
||||||
const clampedDay = Math.min(planDay, plan.totalDays)
|
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()
|
const theme = themeMod.getCurrentTheme()
|
||||||
// 防御:totalDays 为 0(极端数据损坏)时避免除以 0 得到 NaN
|
// 防御:totalDays 为 0(极端数据损坏)时避免除以 0 得到 NaN
|
||||||
|
|||||||
@@ -54,14 +54,19 @@ Page({
|
|||||||
},
|
},
|
||||||
|
|
||||||
fetchRank(cb) {
|
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 })
|
this.setData({ loading: true, empty: false })
|
||||||
wx.cloud.callFunction({
|
wx.cloud.callFunction({
|
||||||
name: 'leaderboard',
|
name: 'leaderboard',
|
||||||
data: {
|
data: {
|
||||||
period: this.data.activePeriod,
|
period,
|
||||||
maxRank: config.leaderboardMaxRank
|
maxRank: config.leaderboardMaxRank
|
||||||
}
|
}
|
||||||
}).then(res => {
|
}).then(res => {
|
||||||
|
if (seq !== this._fetchSeq) { if (cb) cb(); return } // 过期响应,丢弃
|
||||||
this._applyResult(res.result || {})
|
this._applyResult(res.result || {})
|
||||||
if (cb) cb()
|
if (cb) cb()
|
||||||
}).catch((err) => {
|
}).catch((err) => {
|
||||||
@@ -70,6 +75,7 @@ Page({
|
|||||||
// normal (user didn't train today), and a toast would mislead
|
// normal (user didn't train today), and a toast would mislead
|
||||||
// them into thinking the cloud is broken.
|
// them into thinking the cloud is broken.
|
||||||
console.warn('[leaderboard] cloud function failed, using local fallback:', err)
|
console.warn('[leaderboard] cloud function failed, using local fallback:', err)
|
||||||
|
if (seq !== this._fetchSeq) { if (cb) cb(); return }
|
||||||
this._applyLocal()
|
this._applyLocal()
|
||||||
if (cb) cb()
|
if (cb) cb()
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -76,7 +76,7 @@ Page({
|
|||||||
const customPlans = storage.getCustomPlans()
|
const customPlans = storage.getCustomPlans()
|
||||||
const plan = planMod.getPlan(settings.planId, customPlans)
|
const plan = planMod.getPlan(settings.planId, customPlans)
|
||||||
planDay = planMod.getPlanDay(settings.planId, allRecords, settings.planStartDate, 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({
|
this.setData({
|
||||||
|
|||||||
+2
-2
@@ -73,8 +73,8 @@ const getPlan = (planId, customPlans) => {
|
|||||||
return plans[planId] || plans.beginner
|
return plans[planId] || plans.beginner
|
||||||
}
|
}
|
||||||
|
|
||||||
const getTodayTarget = (planId, currentDay) => {
|
const getTodayTarget = (planId, currentDay, customPlans) => {
|
||||||
const plan = getPlan(planId)
|
const plan = getPlan(planId, customPlans)
|
||||||
// 防御:计划数据损坏(planId 无效 / days 为空)时直接返回 0,
|
// 防御:计划数据损坏(planId 无效 / days 为空)时直接返回 0,
|
||||||
// 避免 plan.days[0] 为 undefined 后访问 .target 抛出崩溃。
|
// 避免 plan.days[0] 为 undefined 后访问 .target 抛出崩溃。
|
||||||
if (!plan.days || plan.days.length === 0) return 0
|
if (!plan.days || plan.days.length === 0) return 0
|
||||||
|
|||||||
Reference in New Issue
Block a user