feat(班级PK): 班级 PK 只比两个班都布置过的同一批题,按做对的人占几成分输赢;登录了就能看,AI 分析由服务端现算

- 新接口 GET /rankings/pk:一个班过半的人交过算布置过,至少两个班布置过的题才比;
  显示的整数百分比一样算平;口径和排名页一样(这学期、比赛不算、不计入排名的人不算)
- 不给班就用自己的班(老师用默认的班),只给一个班就配同年级一起布置过的题最多的班,
  一样多挑班级对抗排得更前的
- 去掉 POST /classes/comparison(原来不登录也能查,综合分、四分位数、优秀率都按所选几个班算)
- /ai/class-pk-analysis 只收班号和时间段,数字和页面上的是同一份

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
xuyueandClaude Opus 5.5 committed 2026-10-10 09:06:21 +08:00
1 parent 3c6e4a1e7c
commit d83804b814
7 files changed
+390 -240

No files matched your search

+37 -1
View File
@@ -1,4 +1,5 @@
import {
classPkPeriodSchema,
rankHiddenRequestSchema,
rankPeriodSchema,
rankScopeSchema,
@@ -13,7 +14,7 @@ import {
import { and, countDistinct, desc, eq, gte, inArray, isNotNull, ne } from "drizzle-orm"
import { Hono } from "hono"
import { optionalAuth, requireTeacher, type AppEnv } from "../auth/middleware"
import { optionalAuth, requireAuth, requireTeacher, type AppEnv } from "../auth/middleware"
import { db, schema } from "../db"
import { failure, parseBody, success } from "../http"
import { dayStart, termStart, weekStart } from "../time"
@@ -21,7 +22,10 @@ import {
audienceWhere,
classBattle,
classDetail,
classPk,
loadEntrants,
PK_MAX_CLASSES,
pkPartner,
rankedStudents,
standings,
WEEK_MS,
@@ -252,6 +256,38 @@ rankingRoutes.get("/rankings/class-detail", optionalAuth, async (c) => {
return success(c, await classDetail(className, isTeacherOrAbove(c.get("user"))))
})
/**
* 班级 PK(设计稿「班级 PK 重设计」定稿):`classes` 逗号分隔。谁登录了都能看 —— 只有班级
* 层面的数,没有点名到人。只给一个班(或者不给,用自己的班 / 老师默认的班)就配一个对手,
* 见 `pkPartner`。
*/
rankingRoutes.get("/rankings/pk", requireAuth, async (c) => {
const user = c.get("user")!
const period = classPkPeriodSchema.safeParse(c.req.query("period") ?? "term")
if (!period.success) return failure(c, 400, "invalid-query", "时间段不对")
const requested = [
...new Set(
(c.req.query("classes") ?? "")
.split(",")
.map((name) => name.trim())
.filter((name) => name && name.length <= 32),
),
]
if (requested.length > PK_MAX_CLASSES)
return failure(c, 400, "too-many-classes", `一次最多比 ${PK_MAX_CLASSES} 个班`)
const isTeacher = isTeacherOrAbove(user)
const mine = isTeacher ? null : user.className || null
const classNames = requested.length
? requested
: [isTeacher ? await defaultClassFor(user.id) : mine].filter((name): name is string => !!name)
if (classNames.length === 1) {
const partner = await pkPartner(classNames[0]!)
if (partner) classNames.push(partner)
}
return success(c, await classPk(classNames, period.data, mine))
})
/** 本班每周冠军:最近几个已经结束的周,每周新做对最多的那个人(一样多先做到的) */
rankingRoutes.get("/rankings/champions", optionalAuth, async (c) => {
const className = await resolveGroup(c.get("user"), "class", c.req.query("className")?.trim())