feat(布置作业): 能按知识点点选题目;一个班一天能布置几节课;数据统计认老师布置的课;后台能管布置过的课
Deploy / deploy (push) Canceled after 0s

- 课堂看板「选题」弹框:左边知识点(加必会题),右边搜题号/题名、点选,按点的先后排;
  只列这次作业的语言能交的题(/problems、/problem-tags 加 ?language=)
- 一天多节课:0040 拿掉 class_lesson (class_name, day) 唯一约束,每次布置一行;
  看板「改」只改当前一节,加「新的一节」;这一节布置超过 100 分钟时先问这节课做哪几道题。
  不带 lessonId/newLesson 的老请求照旧改今天最后一节
- 数据统计「一行一节课」:布置过的课以布置为准(题目和顺序照布置、做完按全班算、标「布置」),
  这些提交不再参与推断切段,也不算零散;详情页「做完」注明是这一天/这节课/这段时间
- 后台「课堂作业」:一节一行,改(班级/日子/时刻/语言/题目)、看统计、删除、补一节;
  看板加「布置过的课」入口。题号校验和「做完」口径抽到 services/lesson.ts,三处共用

⚠️ 机房站点要和服务器一起升级:旧代码布置时 ON CONFLICT (class_name, day) 找不到约束会报错

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
xuyueandClaude Opus 5.5 committed 2026-11-02 20:41:25 +08:00
1 parent 0916015405
commit b344bd1dd6
26 files changed
+5685 -99

No files matched your search

+60 -4
View File
@@ -74,12 +74,20 @@ export const lastVisitSchema = z.object({
/**
* 课堂看板里老师布置这节课的题:按输入顺序给展示题号,最多 20 道,空数组 = 清掉。
* `language` 是这份作业用什么语言做,null = 没选
* `language` 是这份作业用什么语言做,null = 没选。
*
* 一个班一天可以上几节内容不同的课,每次布置是一行(class_lesson.id):
* - `lessonId`:改 / 清掉这一节(看板上的「改」);
* - `newLesson`:今天再开一节(看板上的「新的一节」);
* - 两个都不带:改今天最后布置的那一节,今天还没有就新开 —— 一天只能一份时的老写法,
* 开着旧页面的老师、没跟上的机房站点发的都是这种,别让它们每改一次就多出一节课
*/
export const classLessonRequestSchema = z.object({
className: z.string().trim().min(1),
problemDisplayIds: z.array(z.string().trim().min(1)).max(20),
language: lessonLanguageSchema.nullable(),
lessonId: z.number().int().positive().optional(),
newLesson: z.boolean().optional(),
})
export const classBoardProblemSchema = z.object({
@@ -168,16 +176,19 @@ export const classBoardStudentSchema = z.object({
})
/**
* 课堂看板:一个班、今天、这节课的几道题 × 全班学生。
* 课堂看板:一个班、今天、这节课的几道题 × 全班学生。「这节课」是今天最后布置的那一节,
* 今天没布置过就按今天同班的提交推断。
* className 为 null = 没指定班级、最近两小时也没有哪个班在交(没法猜)。
*/
export const classBoardSchema = z.object({
className: z.string().nullable(),
day: z.string(),
source: z.enum(['teacher', 'inferred']).nullable(),
/** 今天布置的语言(见 lessonLanguageSchema) */
/** 老师布置的这一节的 id(「改」带着它);推断出来的课为 null */
lessonId: z.number().int().nullable(),
/** 这一节布置的语言(见 lessonLanguageSchema) */
language: lessonLanguageSchema,
/** 今天第一次布置的时刻,之后改题不变;推断出来的课为 null */
/** 这一节第一次布置的时刻,之后改题不变;推断出来的课为 null */
assignedAt: z.string().nullable(),
/** 第一次布置的老师(真名,没有真名用用户名);推断出来的课为 null */
assignedBy: z.string().nullable(),
@@ -250,3 +261,48 @@ 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>
/**
* 后台「课堂作业」:布置过的每一节课一行(class_lesson),新的在前。看板上当场布置的、
* 后台补的都在这里,数据统计按它们切课。
*/
export const adminLessonSchema = z.object({
id: z.number().int(),
className: z.string(),
/** 东八区的日子,`2026-10-08` */
day: z.string(),
/** 布置的时刻(看板上第一次布置那一刻;后台补的是填的时刻) */
createdAt: z.string(),
updatedAt: z.string(),
/** 布置的老师:真名,没有真名用用户名(和看板「谁 布置」一样) */
createdBy: z.string(),
language: lessonLanguageSchema,
/** 按布置的顺序;题后来被藏起来的 visible 为 false(统计、学生那边都不再出现) */
problems: z.array(z.object({ problemDisplayId: z.string(), title: z.string(), visible: z.boolean() })),
/** 班里的学生数(STUDENT_ROLES、没禁用),和 doneCount 同一个花名册 */
classSize: z.number().int(),
/** 把布置的题全做对了的人数(那天结束前;口径同看板) */
doneCount: z.number().int(),
})
export const adminLessonListSchema = z.object({
results: z.array(adminLessonSchema),
total: z.number().int().nonnegative(),
/** 布置过课的老师(筛选下拉用),按布置的节数从多到少 */
teachers: z.array(z.object({ id: z.number().int(), name: z.string() })),
})
/**
* 后台新补一节 / 改一节。日子和时刻按东八区填:补的是看板外上的课(没在看板布置、
* 以前的课),时刻决定同一天几节课的先后。题目至少一道 —— 删掉一节走 DELETE
*/
export const adminLessonRequestSchema = z.object({
className: z.string().trim().min(1),
day: z.string().regex(/^\d{4}-\d{2}-\d{2}$/),
time: z.string().regex(/^([01]\d|2[0-3]):[0-5]\d$/),
language: lessonLanguageSchema,
problemDisplayIds: z.array(z.string().trim().min(1)).min(1).max(20),
})
export type AdminLesson = z.infer<typeof adminLessonSchema>
export type AdminLessonList = z.infer<typeof adminLessonListSchema>
export type AdminLessonRequest = z.infer<typeof adminLessonRequestSchema>
+15 -1
View File
@@ -450,6 +450,10 @@ export const NO_CLASS = 'none'
* 没填班级的号合在一起当一个班切(`className` 为 null)。
* 其余的是零散提交(补做、课后自己练)。这样 26计算机0班 9 月 29 日是 14:04–15:38,
* 不会被 16:24、18:42 那两条课后提交拖成「14:04–18:42」。
*
* **老师在课堂看板布置过的课以布置为准**(`assigned`):题目和顺序照布置的,那天这个班
* 交这几道题的提交全算这节课(不论隔了多久、几个人),完成率按全班算。这些提交不再参与上面
* 的切段 —— 同一天没布置的另一节课照旧推断,也不会被算成零散提交。
*/
const lessonFailureSchema = z.object({ result: judgeStatusSchema, count: z.number().int() })
@@ -467,8 +471,18 @@ export const submissionLessonsSchema = z.object({
userCount: z.number().int(),
/** 班里的学生数(未禁用的普通用户)。没填班级的谈不上花名册,给 0 */
classSize: z.number().int(),
/** 这节课做的题(这一段里 5 人以上做过的),做的人多的在前,最多 20 道 —— 点进去就筛这几道 */
/**
* 这节课做的题,最多 20 道 —— 点进去就筛这几道。布置的课是布置的那几道、按布置的顺序;
* 推断的课是这一段里 5 人以上做过的,做的人多的在前
*/
problems: z.array(z.string()),
/** 老师在课堂看板布置的课(不是从提交推断的) */
assigned: z.boolean(),
/**
* 布置的课:班里把布置的题全做对了的人数(那天结束前,以前做对的也算;布置时选了语言的
* 只认那个语言,流程图评到 A / S 也算 —— 和看板同一个口径)。推断的课为 null
*/
doneCount: z.number().int().nullable(),
/** 多半是从题单里交的 —— 「题单 7 道」 */
fromProblemSet: z.boolean(),
/** 这节课的题(上面 problems)交了几次 —— 和点进去看到的对得上 */