Files
OJ2/packages/contract/src/classroom.ts
T
xuyueandClaude Opus 5.5 0faf78ad8f feat(挑战): 做对之后可以加一条写法上的限制再做一遍,判题时记下谁做到了
借小丑牌的赌注档:同一道题加一条限制(「整首歌只用 1 次 print」)再做一遍,
给课上做得快的学生找事做(做完一节课的题,中位要空等 13 分钟)。
判定沿用 AST 规则,挑战配在 problem.challenges,做到的记在 challenge_success;
做对的提交顺手判一遍,挑战没做到也照样算做对。没过的规则带上是第几行。
0039 灌进第一批 56 道常用题的 73 条挑战,api 起来后拿历史上做对的代码补一遍谁已经做到了。
出题页能配挑战、从做对的代码里找候选;课堂看板的格子带上挑战状态。

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-26 08:52:50 +08:00

253 lines
11 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { z } from "zod"
import { judgeStatusSchema } from "./judge-status"
import { lessonLanguageSchema as lessonLanguageValueSchema } from "./language"
import { statisticInfoSchema } from "./submission"
/**
* 「班里在做」:同班同一天有好几个人做的题,基本就是老师在课上点名的那几道。
* `day` 是东八区日历日;最近 7 天都没有这样的题时为 null,`problems` 为空。
*/
export const classActivityProblemSchema = z.object({
problemDisplayId: z.string(),
title: z.string(),
/** 那天做过这道题的同班人数(含自己) */
userCount: z.number().int(),
/** 其中那天通过了的人数 */
acceptedCount: z.number().int(),
/**
* 我自己在题库里(不含比赛)做这道题的状态,不限那一天。老师布置时选了语言的,
* 代码只认用那个语言交的(流程图照旧算)
*/
myStatus: z.enum(["accepted", "tried", "none"]),
})
/**
* 老师布置这节课时选的语言(Python / C / C++ / SQL)。null = 没选:推断出来的课、加这一项之前
* 布置的课。选了之后这份作业「做完」只认这个语言交对的(SQL 题例外,交对 SQL 就算),
* 学生打开这几道题编辑器默认就是它
*/
const lessonLanguageSchema = lessonLanguageValueSchema.nullable()
export const classActivitySchema = z.object({
className: z.string().nullable(),
day: z.string().nullable(),
/** teacher = 老师在课堂看板里布置的;inferred = 从同班提交记录推断的 */
source: z.enum(["teacher", "inferred"]).nullable(),
language: lessonLanguageSchema,
problems: z.array(classActivityProblemSchema),
})
/**
* 首页的「上次来」卡(原来登录后弹的「登录速报」并进了首页,AI 那两句不要了)。
* 窗口是**上次登录到这次登录之间**,不是到现在 —— 这次登录期间卡片内容不变,
* 今天做的题不会混进「上次」里。只算题库提交,比赛的题号在题库里点不开。
*/
export const lastVisitSchema = z.object({
/** 上次登录的时刻。null = 头一回登录(或会话早于这个字段) */
previousLogin: z.string().nullable(),
/** null = 上次来已经是 30 天以前,或那次一条代码都没交 */
summary: z
.object({
submissionCount: z.number().int(),
/** 那次做对的题数(去重) */
solvedCount: z.number().int(),
/** 那次最后一条提交的时刻,前端拿它和「班里那天」比是不是同一天 */
lastSubmitTime: z.string(),
/**
* 那次交过、**到现在都还没做对**的题(这次登录后补做对了的不算),
* 按最后一次提交倒序,最多 3 道
*/
unsolved: z.array(
z.object({
problemDisplayId: z.string(),
title: z.string(),
attempts: z.number().int(),
lastResult: judgeStatusSchema,
}),
),
/** 那次做对的题,按第一次做对的先后,最多 12 道(总数看 solvedCount) */
solved: z.array(z.object({ problemDisplayId: z.string(), title: z.string() })),
})
.nullable(),
})
/**
* 课堂看板里老师布置这节课的题:按输入顺序给展示题号,最多 20 道,空数组 = 清掉。
* `language` 是这份作业用什么语言做;不传当 null(上线时还开着的旧看板页面不带它)
*/
export const classLessonRequestSchema = z.object({
className: z.string().trim().min(1),
problemDisplayIds: z.array(z.string().trim().min(1)).max(20),
language: lessonLanguageSchema.optional(),
})
export const classBoardProblemSchema = z.object({
problemId: z.number().int(),
problemDisplayId: z.string(),
title: z.string(),
/** SQL 题不受布置的语言限制:格子数的是 SQL 交的,点进提交列表也按 SQL 筛 */
isSql: z.boolean(),
/** 这道题配了挑战(见 problem.ts 的 challengeSchema):题卡片写挑战成功几人 */
hasChallenges: z.boolean(),
})
export const classBoardCellSchema = z.object({
/**
* accepted 看的是题库里(不含比赛)有没有通过过,不限今天 —— 以前做过的也算做完。
* 布置时选了语言的,代码的次数和通过都只数那个语言(流程图照旧)
*/
status: z.enum(["accepted", "tried", "none"]),
/** 今天在这道题上交了几次(代码 + 流程图) */
attempts: z.number().int(),
/**
* 上面那个数拆开:代码几次、流程图几次。点格子要知道该跳到提交列表的哪一边 ——
* 这节课整班在画流程图时,跳到代码那边是一片空
*/
codeAttempts: z.number().int(),
flowchartAttempts: z.number().int(),
/** 第一次通过的时刻;今天之前通过的,前端显示成「之前」 */
acceptedAt: z.string().nullable(),
/**
* 还没做对时,今天最后一次**代码**提交:点格子打开它讲错误,题卡片下面按错法归堆。
* 做对了、今天没交代码(只画了流程图)的为 null。
*
* 只带归类要用的那几样(runtime_error / sample_check 和提交里存的同一份),
* 不带代码和原始报错 —— 一班四五十人,真要看代码的时候按 submissionId 再拉
*/
latest: z
.object({
submissionId: z.string(),
result: judgeStatusSchema,
runtimeError: statisticInfoSchema.shape.runtime_error,
sampleCheck: statisticInfoSchema.shape.sample_check,
})
.nullable(),
/**
* 做对了的格子:今天交的每一次代码,从早到晚(范围和 latest 一样)。点格子看他是怎么一次次
* 做对的,停在哪一次看哪次的代码。没做对的为空 —— 那边点开是讲错误,靠 latest
*/
trail: z.array(z.object({ id: z.string(), result: judgeStatusSchema, createTime: z.string() })),
/** 今天第几次代码提交做对的,1 = 一次就对;今天之前就做对了、今天没靠代码做对的为 null */
acceptedTry: z.number().int().nullable(),
/**
* 挑战:done = 做到了这道题的一条挑战(不限今天);doing = 做对之后今天又交了、还没做到 ——
* 做完了的学生是在挑战还是闲着,老师一眼看得出。没配挑战的题、没做对的格子为 null
*/
challenge: z.enum(["done", "doing"]).nullable(),
})
export const classBoardStudentSchema = z.object({
userId: z.number().int(),
username: z.string(),
realName: z.string().nullable(),
/** 和 problems 同序 */
cells: z.array(classBoardCellSchema),
/** 今天最后一次提交(任何题),没交过为 null */
lastSubmitAt: z.string().nullable(),
/**
* 今天交过**非比赛**的代码 / 画过流程图没有。点名字跳提交列表用:lastSubmitAt 把比赛提交
* 也算进去了,而提交列表只列非比赛的,只看它会跳到一个空列表
*/
codeToday: z.boolean(),
drawnToday: z.boolean(),
/** 最近几节课(不含今天,个数见 ClassBoard.recentLessons)里交过题的有几节 */
recentAttended: z.number().int(),
/**
* 他此刻开着哪道题的代码编辑器(展示题号),没开着为 null。老师点它就能打开他正在写的
* 代码,不用等他举手。只认连着**本站**的(和课堂求助一样,在 serve 进程的内存里)
*/
editing: z.string().nullable(),
/**
* 此刻在线(5 分钟内有动静)、今天最后一次上线的时刻(今天没上过线为 null)。
* 机房两人共用一台电脑时同桌只有一个登着,所以「谁没来」看 seenAt,不看 online。
* 和 editing 一样只认**本站**:服务器和机房各记各的(在各自的 Redis 里)
*/
online: z.boolean(),
seenAt: z.string().nullable(),
})
/**
* 课堂看板:一个班、今天、这节课的几道题 × 全班学生。
* className 为 null = 没指定班级、最近两小时也没有哪个班在交(没法猜)。
*/
export const classBoardSchema = z.object({
className: z.string().nullable(),
day: z.string(),
source: z.enum(["teacher", "inferred"]).nullable(),
/** 今天布置的语言(见 lessonLanguageSchema) */
language: lessonLanguageSchema,
/** 今天第一次布置的时刻,之后改题不变;推断出来的课为 null */
assignedAt: z.string().nullable(),
/** 第一次布置的老师(真名,没有真名用用户名);推断出来的课为 null */
assignedBy: z.string().nullable(),
/** 这个班最近一次布置选的语言(含今天),给语言下拉当默认值;从没选过为 null */
lastLanguage: lessonLanguageSchema,
problems: z.array(classBoardProblemSchema),
students: z.array(classBoardStudentSchema),
/**
* 「最近几节课」一共几节(最多 5,不含今天)。一节课 = 这个班同学一起做题的一天
* (同班同一天 ≥ 5 人做同一道题,和「班里在做」同一个判据)。新班、开学头几周不满 5。
*/
recentLessons: z.number().int(),
})
/**
* 提交列表「这节课」那颗按钮要的:哪个班、这节课是哪几道题。和课堂看板同一套取法
* (老师布置过就用布置的,否则按今天同班提交推断),但不带花名册 —— 列表只拿它当筛选条件。
* className 为 null = 没指定、最近两小时也没有哪个班在交。
*/
export const classLessonSchema = z.object({
className: z.string().nullable(),
source: z.enum(["teacher", "inferred"]).nullable(),
/**
* 选了语言时,列表那边连语言一起筛。C / Python 的作业里夹了 SQL 题就是 null:
* 列表只能筛一种语言,按 C 筛会把 SQL 题交的全藏起来,宁可多列一些
*/
language: lessonLanguageSchema,
problems: z.array(z.object({ problemDisplayId: z.string(), title: z.string() })),
})
/**
* 老师首页「今天的课」:今天在上课的每个班一行(同班 ≥5 人一起交题,或者老师在看板上布置过)。
* 题目的人数是今天的(和看板同一套取法),「全做对」看的是这几道题有没有做对过,不限今天
*/
export const classTodaySchema = z.object({
classes: z.array(
z.object({
className: z.string(),
source: z.enum(["teacher", "inferred"]).nullable(),
/** 老师布置的时刻;推断出来的为 null */
assignedAt: z.string().nullable(),
/** 布置的老师;推断出来的为 null */
assignedBy: z.string().nullable(),
problems: z.array(
z.object({
problemDisplayId: z.string(),
title: z.string(),
userCount: z.number().int(),
acceptedCount: z.number().int(),
}),
),
/** 今天交过题的人数 / 全班人数 */
activeCount: z.number().int(),
rosterCount: z.number().int(),
/** 这节课的题全做对了的人数 */
allDoneCount: z.number().int(),
/** 今天最后一次有人交题;还没人交(只布置了)为 null */
lastSubmitAt: z.string().nullable(),
}),
),
})
export type ClassActivity = z.infer<typeof classActivitySchema>
export type ClassToday = z.infer<typeof classTodaySchema>
export type LastVisit = z.infer<typeof lastVisitSchema>
export type ClassLessonRequest = z.input<typeof classLessonRequestSchema>
export type ClassBoard = z.infer<typeof classBoardSchema>
export type ClassLesson = z.infer<typeof classLessonSchema>
export type ClassBoardProblem = z.infer<typeof classBoardProblemSchema>
export type ClassBoardStudent = z.infer<typeof classBoardStudentSchema>
export type ClassBoardCell = z.infer<typeof classBoardCellSchema>
export type ClassActivityProblem = z.infer<typeof classActivityProblemSchema>