Files
OJ2/packages/contract/src/classroom.ts
T
xuyueandClaude Opus 5.5 e7b6e27b85 feat(课堂看板): 标出谁在线、谁今天没上过线,请假框里没上线的人分在前面一堆
机房有两人共用一台电脑的,同桌只有一个登着,只看「此刻在不在线」会把旁边的人
当成没来。所以每次记在线时按天再记一笔「今天最后一次上线」:
- 看板「在线」列(原「在写」):开着题的照旧写题号,其余写 在线 / N 分钟前在 / 今天没上线
- 还没交过的人里,今天没上过线的排最前
- 请假框分「今天没上线」「今天上过线」两堆,前一堆可以一键全勾上

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

235 lines
10 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(),
})
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(),
})
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 */
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(),
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.infer<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>