feat(教师统计): 提交统计面板返修 —— 统计口径、未完成分两栏、多题、自动刷新
排查这个面板时发现的一串问题和缺口,一起修掉。改动集中在 `GET /submissions/statistics` 和 `StatisticsPanel.vue`,两边互相咬着, 拆不成独立的 commit。 **「已解决」数的不是题数**(真在显示错数字)。`count(*) filter (accepted)` 按用户名分组、没有 distinct problem_id,同一道题重复 AC 会重复计数。查单道题时 看不出来,老师查「这节课全班」时「已解决 5」可能是同一道题交了 5 次。新增 `solvedCount = count(distinct problem_id) filter (accepted)`,表格那一列换成它; `acceptedCount` 保留,正确率的分子仍然是提交条数。 **正确率把判题中的算进了分母**。PENDING / JUDGING 进分母不进分子,全班同时交卷的 那几秒正确率凭空掉一截 —— 而老师盯着看的就是这个数。分母改成判完的条数, `UNJUDGED_RESULTS` 提到 judge/status.ts 共用。总提交仍是全部条数(交了就算交过, 否则人数口径会跟着变,正在判的学生会掉进「没交」名单),另外下发 `judgingCount`, 非零时面板多显示一块「判题中」,三个数字才对得上。 **「未完成」实际是「一次没交」**。做了但一次没对的学生既不在「完成人数」也不在 未完成名单,等于从屏幕上消失 —— 而那恰恰是最该去看一眼的人。新增 `dataAttempted`,未完成栏拆成「还没交」/「交了没对」两组,tab 计数是两者之和。 请假隐藏对两组同时生效:他们同样占着班级人数这个分母,只藏一半会把完成度算错。 **点名字能看到错在哪**。`dataAttempted` 带上最近一条提交的题号、状态和 `statistic_info.err_info`(截断 400 字),点名字弹出来,还能一步跳到代码。 老师不用再切到提交列表、翻到这个人、点开代码。 **支持一次查几道题**。题号框接受 `1001,1005,1010`(中英文逗号、分号、空格都当 分隔符,投影前手敲不该因为打了全角逗号就查不出来),有一个题号不存在就整体 404。 **完成 = 这几道全解决**;只填一道时和原来完全等价,不填题号时退回「至少做出一道」。 差一道的人落在「交了没全对」里,名字后面缀 `2/3题`。 **零提交时整块面板消失**。判空条件是 `count.total > 0`,可一节课刚开始一条提交都 没有、后端已经把整份花名册当作「未完成」返回了 —— 最该看名单的时刻反而只显示 「暂无数据」。流程图那边更彻底:`/flowcharts/statistics` 的零提交分支把 `dataUnaccepted` 写死成空数组,名单压根没下发。 **面板不会自己刷新**。打开是空的、要点一次按钮,拿到的是那一刻的快照。改成打开即查 + 每 15 秒滚动重查,页面切到后台就停,关掉面板随组件卸载停掉。上一次没回来就跳过 这一次。班级从手打 `ks251` 换成下拉(选项来自网站配置的 class_list,和登录框同一份), 保留自由输入,查过的班级记进 localStorage 下次带上 —— 机房电脑一台对一个班。 **明细查询没有 LIMIT**。展开行用的 submissionItems 原来把窗口内**全部**提交捞进内存 再原样序列化,「全部时段 + 不填条件」就是十几万条。改成只取有 AC 的人、每人最近 50 条, 截断用窗口函数发生在数据库侧;表格「提交数」仍是真实总数。 **顺带**:`personRate` 前端从来没读过(完成度是前端按「减掉请假人数的分母」自己算的), 从契约里删掉;「语法未过」的题数单列出来(AST_CHECK_FAILED 全站仍然算通过,口径没动, 只是让老师看得见谁是绕过语法要求做出来的,那批人教学上没达标)。 实跑验证(dev 全栈 + 浏览器): - 已解决:student 两条 AC 都在题号 5 上 —— 改前显示 2,改后显示 1 - 正确率:临时把两条改成 PENDING/JUDGING,12 条提交 2 条通过 —— 16.67% → 20%, 面板多出「判题中 2」,表格显示「12(2 条判题中)」 - 未完成两栏:student2 没交、student 交了 12 次没对,请假隐藏 student 后 班级人数 2→1、tab 2→1、出现「恢复 1 位」 - 错因弹层:点 student 弹出「最近一次:1020 · 编译失败 / Test case not found / 看代码」 - 多题:`1004` 完成 1 人;`1004,1005` 完成 0 人、交了没全对 student 1/2题 8次; 全角逗号同上;`1004,9999` 报 `Problem 9999 does not exist` - 自动刷新:打开即发请求,之后 04:44:22 → 04:44:36 → 04:44:51 → 04:45:06 每 15 秒 一次且窗口跟着滚;关掉面板后 20 秒请求数不再增长 - 班级下拉:选「25计算机1班」→ ks251,重开面板自动带上并立刻查 tsc / vue-tsc / check:routes 全过。验证用的 dev 库改动已还原。 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KqjE6qPo67fqVDKn6Bx7yd
This commit is contained in:
@@ -112,20 +112,64 @@ export const submissionListItemSchema = z.object({
|
||||
export const submissionListSchema = paginatedSchema(submissionListItemSchema)
|
||||
|
||||
/**
|
||||
* 未完成学生。`realName` 是从用户名里剥掉 `ks<班级号>` 前缀后剩下的那一段,
|
||||
* **一条都没交**的学生。`realName` 是从用户名里剥掉 `ks<班级号>` 前缀后剩下的那一段,
|
||||
* 不是 user.real_name 列 —— 与 F2「真名默认不下发」不冲突:这里只有教师能看到,
|
||||
* 且教师面板的用途正是点名谁没做。
|
||||
*
|
||||
* 注意它不是「未完成」的全部:交了但一次没对的学生在 `dataAttempted` 里。
|
||||
*/
|
||||
export const unacceptedStudentSchema = z.object({
|
||||
username: z.string(),
|
||||
realName: z.string(),
|
||||
})
|
||||
|
||||
/**
|
||||
* **交了但一次没对**的学生。这批人原来两栏都不在 —— 不在「完成人数」(没 AC),
|
||||
* 也不在「未完成」名单(那一栏只收一条没交的),于是课堂上最该去看一眼的人
|
||||
* 反而从屏幕上消失了。`submissionCount` 是窗口内的提交次数,教师据此判断
|
||||
* 「卡了多久」。
|
||||
*/
|
||||
export const attemptedStudentSchema = unacceptedStudentSchema.extend({
|
||||
submissionCount: z.number().int(),
|
||||
/**
|
||||
* 已经解决的题数。查多道题时这一栏里混着「一道没对」和「三道做出两道」两种人,
|
||||
* 差几道决定了老师先管谁 —— 所以名字后面要缀 `2/3`。
|
||||
*/
|
||||
solvedCount: z.number().int(),
|
||||
/**
|
||||
* 最近一条提交错在哪。教师点名字就能看到「是编译错了还是答案错了」,
|
||||
* 不必再切去提交列表翻这个人。`error` 是判题机写进 statistic_info 的 err_info,
|
||||
* 已截断;没有错误文本(比如答案错误那种)时为 null。
|
||||
*/
|
||||
lastFailure: z
|
||||
.object({
|
||||
id: z.string(),
|
||||
/** 题目的展示编号,用来告诉老师错在哪道题 */
|
||||
problem: z.string(),
|
||||
result: judgeStatusSchema,
|
||||
error: z.string().nullable(),
|
||||
})
|
||||
.nullable(),
|
||||
})
|
||||
|
||||
export const submissionStatisticsUserSchema = z.object({
|
||||
username: z.string(),
|
||||
className: z.string().nullable(),
|
||||
submissionCount: z.number().int(),
|
||||
/** 通过的**提交条数**。correctRate 的分子就是它 */
|
||||
acceptedCount: z.number().int(),
|
||||
/**
|
||||
* 解决的**题数**(同一道题重复 AC 只算一道)。表格「已解决」那一列显示的是它 ——
|
||||
* 不指定题号查「这节课全班」时,条数和题数能差出好几倍。
|
||||
*/
|
||||
solvedCount: z.number().int(),
|
||||
/**
|
||||
* 「答案对了但语法没按要求写」且**最后也没改对**的题数。这些题算在 solvedCount 里
|
||||
* (AST_CHECK_FAILED 全站都算通过),单列出来只是让教师看得见教学上没达标的那几个。
|
||||
*/
|
||||
astOnlyCount: z.number().int(),
|
||||
/** 这个人还在判题队列里的条数。`submissionCount` 含它,`correctRate` 的分母不含 */
|
||||
judgingCount: z.number().int(),
|
||||
// 百分比数值,不带 %。旧后端返回 "85.5%" 字符串,展示格式化交给前端。
|
||||
correctRate: z.number(),
|
||||
submissionItems: z.array(
|
||||
@@ -136,11 +180,21 @@ export const submissionStatisticsUserSchema = z.object({
|
||||
export const submissionStatisticsSchema = z.object({
|
||||
submissionCount: z.number().int(),
|
||||
acceptedCount: z.number().int(),
|
||||
/**
|
||||
* 还没判完的条数(PENDING / JUDGING)。`submissionCount` 把它算在内,
|
||||
* `correctRate` 的分母不算 —— 全班同时交卷的那几秒,分母涨了分子没涨,
|
||||
* 正确率会凭空掉一截。下发它是为了让教师看得出「那几条还在判」。
|
||||
*/
|
||||
judgingCount: z.number().int(),
|
||||
correctRate: z.number(),
|
||||
// 花名册人数(未禁用的普通用户)。**只有这一个分母下发**:完成度由前端算,
|
||||
// 因为「请假隐藏」会把请假的人从分母里减掉,那是后端不知道的浏览器本地状态。
|
||||
personCount: z.number().int(),
|
||||
personRate: z.number(),
|
||||
data: z.array(submissionStatisticsUserSchema),
|
||||
/** 一条都没交的(花名册里的人减去有提交的人) */
|
||||
dataUnaccepted: z.array(unacceptedStudentSchema),
|
||||
/** 交了但一次没对的。和 dataUnaccepted 一样只在传了用户名(有花名册)时才有内容 */
|
||||
dataAttempted: z.array(attemptedStudentSchema),
|
||||
})
|
||||
|
||||
export const formatCodeRequestSchema = z.object({
|
||||
@@ -161,6 +215,7 @@ export type SubmissionStatisticsUser = z.infer<
|
||||
typeof submissionStatisticsUserSchema
|
||||
>
|
||||
export type UnacceptedStudent = z.infer<typeof unacceptedStudentSchema>
|
||||
export type AttemptedStudent = z.infer<typeof attemptedStudentSchema>
|
||||
|
||||
export type SubmissionListItem = z.infer<typeof submissionListItemSchema>
|
||||
export type SubmissionList = z.infer<typeof submissionListSchema>
|
||||
|
||||
Reference in New Issue
Block a user