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.