feat(排行榜): 重写全服 Top100,补上「我的排名」,后台那条挪进 admin
起因是 Top100 的「已解决」「提交数」两列一直空白:列的 key 还是 snake_case(accepted_number / submission_number),而数据早在c2a8120拆掉 转换层后就是 camelCase 了。naive-ui 按 row[key] 取值,取到 undefined 就渲染 空白、不报错 —— 和d3348f9是同一个病根。 顺着这条线把整个端点重写了: **上限不再由调用方传。** `top` 参数原来有三个调用方各传各的(100 / 10 / 0), 而它会覆盖 limit 与 offset、total 却按全量算,正是36e4ac2那个「每页都是同样 100 条」的成因。现在 100 写死在服务端,参数只剩 limit / offset。 「全服 Top10」不需要另一个上限,它就是这个榜的第一页。 **排序补了第三档 asc(user.id)。** 前两个键完全相同的学生在真实数据里成片存在 (都是 0/0),没有稳定兜底键时 postgres 每次返回的顺序可以不同,翻页会看到重复 或漏掉的人。老代码缺这一档。 **新增 me(我的全服名次)。** 名次 = 排在我前面的人数 + 1,三个排序键逐级比较, 与列表的 orderBy 逐字对应 —— 少比一级就会出现「显示第 7 名、实际排在表格第 9 行」。 榜上高亮我那一行,名次超出 100 时在 footer 单独给一行。未登录、教师/超管返回 null。 **后台那条搬去 /api/admin/rankings/users**(requireSuperAdmin,无上限)。 原来它走的是公开端点的 top=0 分支,也就是任何匿名请求都能 ?top=0&limit=250 翻走全校学生名单和个性签名 —— 而 /profiles/:username 恰恰为了收紧枚举面才做了 「匿名一律返回空」,注释里还专门点了 /rankings/users 的名。这条页面本来就是 requiresSuperAdmin,它调的另外两个接口也都是 requireSuperAdmin,守卫对得上。 顺手去掉恒真条件 gte(acceptedNumber, 0):该列是 notNull default 0。 同一次扫了全仓 218 个表格列定义,筛出 70 个没有 render 的(只有这些才靠 key 直接取值),比对全部类型定义里的字段名 —— 除这两处外没有漏网的。`_id` 和 `test_case` 是真字段名,不能改。另外收掉两处同类的雷: admin/setting/config.vue 手写的 `interface Testcase` 字段名和类型都是错的 (真实数据是 createTime: number,不是 create_time: string),改用契约的 OrphanTestCase;serverColumns 里 last_heartbeat / create_time 两个残留 key 有 render 兜着没出事,一并改正。 实测(造 120 个探针用户,含 3 个 AC 与提交数完全相同的并列,验完已清库): 122 人时 total=100;offset=95 末页 5 条;offset=100 越界返回空且不发 SQL; 并列三人稳定占据前三;student(ac=2) 拿到 rank=121 走「不在榜上」分支; 把 ac 调到 450 时 rank=4 且表格第 4 行正是 student(名次与行号对得上); 升成超管后 me 变 null;后台端点 total=121 无上限、keyword=probe_01 命中 10 条、 未登录 401;传 top=1 已被忽略。 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
import { z } from "zod"
|
||||
|
||||
import { achievementRaritySchema } from "./achievement"
|
||||
import { rankProfileSchema } from "./account"
|
||||
import { paginatedSchema, sampleUserSchema } from "./common"
|
||||
import { problemDifficultySchema } from "./problem"
|
||||
|
||||
@@ -269,6 +270,15 @@ export const orphanTestCaseSchema = z.object({
|
||||
createTime: z.number(),
|
||||
})
|
||||
|
||||
/**
|
||||
* 后台的用户排名列表。行的形状与 oj 侧榜单相同,但**没有 100 名的上限**:
|
||||
* 这里不是榜单,是老师按班级前缀翻全班学生的管理视图,要能一直翻到底。
|
||||
*
|
||||
* 独立成一条端点而不是给公开榜单加个「不限量」开关:那个开关一旦存在,
|
||||
* 匿名请求就能拉走全校学生名单和个性签名。
|
||||
*/
|
||||
export const adminUserRankSchema = paginatedSchema(rankProfileSchema)
|
||||
|
||||
export const dashboardInfoSchema = z.object({
|
||||
userCount: z.number().int(),
|
||||
recentContestCount: z.number().int(),
|
||||
@@ -644,6 +654,7 @@ export type JudgeServer = z.infer<typeof judgeServerSchema>
|
||||
export type DashboardInfo = z.infer<typeof dashboardInfoSchema>
|
||||
export type JudgeServerList = z.infer<typeof judgeServerListSchema>
|
||||
export type OrphanTestCase = z.infer<typeof orphanTestCaseSchema>
|
||||
export type AdminUserRank = z.infer<typeof adminUserRankSchema>
|
||||
|
||||
export type AdminAiReportListItem = z.infer<typeof adminAiReportListItemSchema>
|
||||
export type AdminAiReport = z.infer<typeof adminAiReportSchema>
|
||||
|
||||
Reference in New Issue
Block a user