feat(题目页): 「统计」页签重做——学生看难不难、几次做对、常错在哪,老师按班看谁没做对

- 全部按人算:原来的「通过数」是 AC 条数、「通过率」按提交算;击败用户、饼图、历年 AC 率去掉
- 错得最多的那种给一句该怎么办(都指向「运行例子」),自己交了 3 次以上还没对再多一张卡
- 老师选一个班:做对 / 交了没对(次数、最后结果)/ 没交;班里错得和全站不一样时点出来
- 题库入口工具栏的「课堂统计」并进这里(题单没有页签,按钮留着)
- 新接口 /problems/:id/stats(按内部题号,比赛题不再查到同号公开题),删掉 beat-count、yearly-ac

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
xuyueandClaude Opus 5.5 committed 2026-09-30 00:39:57 +08:00
1 parent 91d9c9c6d7
commit 99319fdcc5
10 files changed
+1017 -317

No files matched your search

+61 -6
View File
@@ -1,6 +1,7 @@
import { z } from "zod"
import { paginatedSchema, sampleUserSchema } from "./common"
import { judgeStatusSchema } from "./judge-status"
import { problemLanguageSchema } from "./language"
/**
@@ -446,11 +447,64 @@ export const problemAuthorSchema = z.object({
problemCount: z.number().int().nonnegative(),
})
export const yearlyAcSchema = z.object({
year: z.number().int(),
total: z.number().int().nonnegative(),
accepted: z.number().int().nonnegative(),
acRate: z.number(),
const failureCountSchema = z.object({ result: judgeStatusSchema, count: z.number().int() })
/** 老师选中一个班时的明细:谁做对了、谁交了没对(卡在哪)、谁还没交 */
export const problemClassDetailSchema = z.object({
className: z.string(),
/** 班里的学生数(学生角色、没禁用) */
roster: z.number().int(),
/** 这个班一起做这道题的那天(东八区 YYYY-MM-DD,交的人最多的那天)—— 「2025年12月5日做的」 */
day: z.string(),
failures: z.array(failureCountSchema),
/** 交了没对,次数多的在前。名字是剥掉 ks<班级号> 前缀的那一段 */
unsolved: z.array(
z.object({ realName: z.string(), attempts: z.number().int(), lastResult: judgeStatusSchema }),
),
solved: z.array(z.string()),
/** 班里还没交过的 */
untouched: z.array(z.string()),
})
/**
* 题目页「统计」页签。全部**按人**算,不按提交条数 —— 原来的「通过数」是 AC 条数
* (3092 是 219 条、202 个人),「通过率」是按提交算的,学生看了不知道这题难不难。
* 原来的「击败用户」(分母是近两年所有登录过的人)、饼图、历年 AC 率一起去掉了。
*/
export const problemStatsSchema = z.object({
/** 比赛还没结束(又不是比赛管理员):别人的情况一律不给,下面的数字都是 0 / 空 */
locked: z.boolean(),
/** 交过的人 */
tried: z.number().int(),
solved: z.number().int(),
/** 做对的人用了几次:1 次 / 2–3 次 / 4 次以上 */
tries: z.object({ one: z.number().int(), few: z.number().int(), many: z.number().int() }),
/** 没通过的提交按结果分,多的在前(不含判题中) */
failures: z.array(failureCountSchema),
/** 答案错误里第 1 个测试点就没过的条数 —— 多半是例子都没对上,提示先点「运行例子」 */
wrongAnswerFirstCase: z.number().int(),
/** 我自己:交了几次、做对没有。没登录为 null */
me: z.object({ attempts: z.number().int(), solved: z.boolean() }).nullable(),
/** 学生自己班这题 x / y 人做对(y 是班里交过的人)。没班级、比赛里为 null */
myClass: z
.object({ className: z.string(), tried: z.number().int(), solved: z.number().int() })
.nullable(),
/**
* 老师:一起做过这题的班(同一天至少 5 个人交过 —— 零星几个人自己刷到、补做的不算),
* 按那天倒序。原来工具栏上的「课堂统计」并进了这里。学生、比赛里为 null
*/
classes: z
.array(
z.object({
className: z.string(),
tried: z.number().int(),
solved: z.number().int(),
day: z.string(),
}),
)
.nullable(),
/** 老师选中的那个班(默认 classes 第一个)的明细 */
classDetail: problemClassDetailSchema.nullable(),
})
export type AstRuleEngine = z.infer<typeof astRuleEngineSchema>
@@ -468,4 +522,5 @@ export type SqlConfig = z.infer<typeof sqlConfigSchema>
export type SqlDisplay = z.infer<typeof sqlDisplaySchema>
export type SqlDisplayTable = z.infer<typeof sqlDisplayTableSchema>
export type SqlDisplayColumn = z.infer<typeof sqlDisplayColumnSchema>
export type YearlyAc = z.infer<typeof yearlyAcSchema>
export type ProblemStats = z.infer<typeof problemStatsSchema>
export type ProblemClassDetail = z.infer<typeof problemClassDetailSchema>