Deploy / deploy (push) Canceled after 0s
- 一张榜两个开关:本班 / 本年级 / 全服 × 这周 / 这学期 / 全部,默认本班 · 这学期。 原来最大那张全服 Top100 里,这学期在用的 25 级学生一个都进不去 - 做对一样多的不再按「提交少的在前」排,改成先做到的在前;名次旁边写比周一升降了几名 - 「你」那张卡:第几名、再做对几道上领奖台,要追的人早你几秒、追你的人晚你几秒 - 本年级 / 全服只列前面一段和你附近一段,中间折起来,点开再看 - 班级对抗按人均做对比,另有我和对手的名次走势、这周本班、每周冠军 - 老师不上榜,选班看;每行「⋯」能把怀疑抄代码的学生设成不计入排名(新增 user.rank_hidden_at, 首页周榜同样不算他),随时能恢复 - 去掉全服 Top10、日活两张图、在线人数和班级详情弹框里的统计词,以及它们背后的旧接口 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
54 lines
2.2 KiB
TypeScript
54 lines
2.2 KiB
TypeScript
import type { ChainableCommander } from "ioredis"
|
||
|
||
import { redis } from "../redis"
|
||
|
||
/**
|
||
* 「谁现在在线」。member = userId,score = 最后一次活动的毫秒时间戳。
|
||
*
|
||
* 会话本身判定不了在线:`session:<token>` 的 TTL 是 7 天且每次请求都续期,
|
||
* 「有会话」只说明这人一周内来过。所以这里单独记一个活动时间戳 ——
|
||
* 写入一律搭在已有的 pipeline 上(登录、每个带鉴权的请求、WebSocket 巡检),
|
||
* 不多一趟往返。
|
||
*/
|
||
const PRESENCE_KEY = "online-users"
|
||
|
||
/**
|
||
* 多久没动就算离线。挂着页面不操作的人靠 sweepSessions 每 60 秒续一次
|
||
* (见 websocket.ts),窗口必须明显大于那个间隔,否则开着页面的学生会一闪一闪。
|
||
*/
|
||
const ONLINE_WINDOW_MS = 5 * 60 * 1000
|
||
|
||
/** 记一笔活动。传 pipeline 而不是自己发命令:调用点都在热路径上 */
|
||
export function markOnline(pipeline: ChainableCommander, userId: number) {
|
||
pipeline.zadd(PRESENCE_KEY, Date.now(), String(userId))
|
||
}
|
||
|
||
/**
|
||
* 当前在线的用户 id。
|
||
*
|
||
* 顺手把过期成员删掉 —— 这是唯一的清理时机(整个 key 不能设 TTL:ZADD 不会重置
|
||
* key 的 TTL,到期会把还在线的人一起抹掉)。读这张表的只有后台用户列表,
|
||
* 不清理最坏也就是攒下全站用户数量级的成员,远谈不上要单开一个定时任务。
|
||
*/
|
||
export async function onlineUserIds() {
|
||
const cutoff = Date.now() - ONLINE_WINDOW_MS
|
||
const results = await redis
|
||
.pipeline()
|
||
.zremrangebyscore(PRESENCE_KEY, "-inf", `(${cutoff}`)
|
||
.zrange(PRESENCE_KEY, "0", "-1")
|
||
.exec()
|
||
const members = (results?.[1]?.[1] ?? []) as string[]
|
||
return new Set(members.map(Number).filter(Number.isInteger))
|
||
}
|
||
|
||
/** 登出、被禁用、被踢下线:立刻从在线名单里摘掉,别等窗口自然过期 */
|
||
export async function clearOnline(userId: number) {
|
||
await redis.zrem(PRESENCE_KEY, String(userId))
|
||
}
|
||
|
||
/** 单个用户在不在线。列表页用上面那个,别在循环里调这个 */
|
||
export async function isUserOnline(userId: number) {
|
||
const score = await redis.zscore(PRESENCE_KEY, String(userId))
|
||
return score !== null && Number(score) >= Date.now() - ONLINE_WINDOW_MS
|
||
}
|