feat(leaderboard): 训练完强刷 + 下拉刷新 force

- 云函数 _fetchLatestByOpenid 支持 force 跳过实例缓存重新扫描
- 训练完成设 _lb_force_refresh 标志,下次打开排行榜强刷,新数据立即可见
- 下拉刷新带 force=true,拉取最新数据
- 平时常规打开仍走 5min 缓存 + 客户端乐观缓存,不增加无谓扫描

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2026-07-28 08:47:41 +08:00
parent e2becc24aa
commit b42d080826
4 changed files with 22 additions and 15 deletions
+5 -5
View File
@@ -43,11 +43,11 @@ const tsOf = (d) => {
// per openid. Uses cursor pagination (_id > lastId) instead of skip(N):
// skip is O(N) and degrades as the collection grows; a cursor is constant
// cost per page, and default get() order (_id ascending) won't drop/dup.
const _fetchLatestByOpenid = async () => {
if (_docsCache && (Date.now() - _docsCache.ts) < CACHE_TTL) {
const _fetchLatestByOpenid = async (force) => {
if (!force && _docsCache && (Date.now() - _docsCache.ts) < CACHE_TTL) {
return _docsCache.latestByOpenid
}
// 缓存过期:复用 in-flight promise,避免并发请求各自全表扫描(缓存击穿)
// 缓存过期或强刷(force):复用 in-flight promise,避免并发请求各自全表扫描(缓存击穿)
if (_docsCachePromise) return _docsCachePromise
_docsCachePromise = (async () => {
const latestByOpenid = new Map()
@@ -81,7 +81,7 @@ const _fetchLatestByOpenid = async () => {
}
exports.main = async (event) => {
const { period, maxRank } = event || {}
const { period, maxRank, force } = event || {}
const limit = Math.max(1, Math.min(parseInt(maxRank) || DEFAULT_MAX_RANK, 500))
if (!period) return { err: 'missing period' }
@@ -107,7 +107,7 @@ exports.main = async (event) => {
// start add() bug; only the latest updatedAt counts, else we'd sum the
// same records N times and inflate the board. Latest (not first) also
// gives the freshest profile.nickname before admin-dedupe runs.
const latestByOpenid = await _fetchLatestByOpenid()
const latestByOpenid = await _fetchLatestByOpenid(force)
// Second pass: each openid appears exactly once, so the accumulation
// logic doesn't need any dedup guards.
+1 -1
View File
@@ -7,7 +7,7 @@ module.exports = {
version: 'v3.01',
/** 最后更新日期,外显在设置页「关于」 */
updatedAt: '2026-07-27',
updatedAt: '2026-07-28',
/** 开发者名称 / 微信号,设置页点击可复制 */
developer: '刘承',
+13 -8
View File
@@ -35,16 +35,20 @@ Page({
const tb = this.getTabBar()
if (tb) tb.setData({ selected: 2 })
} catch (e) {}
if (this._firstShow) {
this._firstShow = false
this.fetchRank()
return
// 训练完设的强刷标志:下次进排行榜跳过云函数缓存,立刻拿到含新记录的数据
let force = false
try {
if (wx.getStorageSync('_lb_force_refresh')) {
force = true
wx.removeStorageSync('_lb_force_refresh')
}
this.fetchRank()
} catch (e) {}
this._firstShow = false
this.fetchRank(undefined, force)
},
onPullDownRefresh() {
this.fetchRank(() => wx.stopPullDownRefresh())
this.fetchRank(() => wx.stopPullDownRefresh(), true)
},
switchPeriod(e) {
@@ -54,7 +58,7 @@ Page({
this.fetchRank()
},
fetchRank(cb) {
fetchRank(cb, force) {
// 序列号:快速切换周期时丢弃旧响应,避免旧 period 的数据覆盖新 period
this._fetchSeq = (this._fetchSeq || 0) + 1
const seq = this._fetchSeq
@@ -81,7 +85,8 @@ Page({
name: 'leaderboard',
data: {
period,
maxRank: config.leaderboardMaxRank
maxRank: config.leaderboardMaxRank,
force: !!force
}
}).then(res => {
if (seq !== this._fetchSeq) { if (cb) cb(); return } // 过期响应,丢弃
+2
View File
@@ -403,6 +403,8 @@ Page({
// Flag for the index page's "训练完成" highlight
try { wx.setStorageSync('_last_train_at', Date.now()) } catch (e) {}
// 标记排行榜强刷:下次打开排行榜跳过云函数缓存,让刚训练的数据立即可见
try { wx.setStorageSync('_lb_force_refresh', true) } catch (e) {}
const finalStreak = storage.getStreak().count