diff --git a/apps/api/src/db/schema.ts b/apps/api/src/db/schema.ts index 591d808..2528de0 100644 --- a/apps/api/src/db/schema.ts +++ b/apps/api/src/db/schema.ts @@ -44,13 +44,13 @@ import type { FlowchartStatus, HintDiagnosis, JudgeStatus, + LessonLanguage, ProblemDifficulty, ProblemLanguage, ProblemPermission, ProblemSetDifficulty, ProblemSetStatus, ReactionKey, - RunnableLanguage, SqlConfig, SqlDisplay, TagCategory, @@ -1497,10 +1497,10 @@ export const classLesson = pgTable( day: text().notNull(), problemIds: jsonb("problem_ids").notNull().$type(), /** - * 这份作业用什么语言做(Python / C / C++),老师布置时选。可空:加这一列之前布置的 - * 没有它。写入侧在 PUT /classroom/lesson 按 runnableLanguageSchema 校验过 + * 这份作业用什么语言做(Python / C / C++ / SQL),老师布置时选。可空:加这一列之前布置的 + * 没有它。写入侧在 PUT /classroom/lesson 按 lessonLanguageSchema 校验过 */ - language: text().$type(), + language: text().$type(), createdBy: integer("created_by").notNull(), updatedAt: timestamp("updated_at", { withTimezone: true, diff --git a/apps/api/src/routes/classroom.ts b/apps/api/src/routes/classroom.ts index 2232d97..a7bceff 100644 --- a/apps/api/src/routes/classroom.ts +++ b/apps/api/src/routes/classroom.ts @@ -12,7 +12,7 @@ import { type ClassRankItem, type ClassUserRank, type LastVisit, - type RunnableLanguage, + type LessonLanguage, FLOWCHART_PASS_GRADES, } from "@oj2/contract" import { @@ -221,12 +221,21 @@ function classMemberIds(className: string) { * 有的题一节课全班都在画流程图、代码提交个位数,只数代码的话这节课在统计里等于没上。 * 两边都按 `user_id in (...)` + `create_time >=` 走各自的 (user_id, create_time) 索引。 */ +/** + * 老师布置时选了语言:代码只数这个语言交的。SQL 题例外 —— 它只收 SQL,按 C 去数的话, + * C 的作业里夹一道 SQL 题,学生怎么交都算不了做完。非 SQL 题交不进 SQL(提交接口按题目的 + * 语言拦),所以一律放行 SQL 不会把别的题算宽 + */ +function lessonLanguageFilter(language: LessonLanguage | null | undefined) { + return language ? inArray(schema.submission.language, [language, "SQL"]) : undefined +} + function activityRows(options: { since: string users?: number[] | ReturnType problemIds?: number[] - /** 老师布置时选了语言:代码只数这个语言交的,流程图不受影响 */ - language?: RunnableLanguage | null + /** 老师布置时选了语言:代码只数这个语言交的(见 lessonLanguageFilter),流程图不受影响 */ + language?: LessonLanguage | null }) { const { since, users, problemIds, language } = options const code = db @@ -243,7 +252,7 @@ function activityRows(options: { gte(schema.submission.createTime, since), users ? inArray(schema.submission.userId, users) : undefined, problemIds ? inArray(schema.submission.problemId, problemIds) : undefined, - language ? eq(schema.submission.language, language) : undefined, + lessonLanguageFilter(language), ), ) const flowchart = db @@ -272,7 +281,7 @@ function activityRows(options: { async function classDayGroups( className: string, since: string, - options: { minUsers?: number; problemIds?: number[]; language?: RunnableLanguage | null } = {}, + options: { minUsers?: number; problemIds?: number[]; language?: LessonLanguage | null } = {}, ): Promise { const activity = activityRows({ since, @@ -297,12 +306,14 @@ async function classDayGroups( /** 按 id 取题,只留学生看得见的(题库里、visible),被藏起来的点进去也是 404 */ async function visibleProblems(ids: number[]) { - if (!ids.length) return new Map() + if (!ids.length) + return new Map() const rows = await db .select({ id: schema.problem.id, displayId: schema.problem.displayId, title: schema.problem.title, + isSql: sql`${schema.problem.languages} @> '["SQL"]'::jsonb`, }) .from(schema.problem) .where( @@ -533,7 +544,7 @@ classroomRoutes.get("/me/class-activity", requireAuth, async (c) => { inArray(schema.submission.problemId, ids), isNull(schema.submission.contestId), // 布置的是 C:以前用 Python 做对过不算这份作业做完 - lesson.language ? eq(schema.submission.language, lesson.language) : undefined, + lessonLanguageFilter(lesson.language), ), ) .groupBy(schema.submission.problemId) @@ -706,7 +717,7 @@ classroomRoutes.get("/classroom/board", requireTeacher, async (c) => { isNull(schema.submission.contestId), inArray(schema.submission.userId, userIds), inArray(schema.submission.problemId, ids), - lesson.language ? eq(schema.submission.language, lesson.language) : undefined, + lessonLanguageFilter(lesson.language), ), ) .groupBy(schema.submission.userId, schema.submission.problemId) @@ -807,6 +818,7 @@ classroomRoutes.get("/classroom/board", requireTeacher, async (c) => { problemId: problem.id, problemDisplayId: problem.displayId, title: problem.title, + isSql: problem.isSql, })), students: roster.map( (student) => @@ -852,10 +864,12 @@ classroomRoutes.get("/classroom/lesson", requireTeacher, async (c) => { } satisfies ClassLesson) // 和看板一样只回看今天:「这节课」不该冒出昨天的题 const lesson = await classLessonProblems(className, 1) + // C / Python 的作业夹了 SQL 题就不筛语言(见契约 classLessonSchema.language) + const mixed = lesson.problems.some((problem) => problem.isSql !== (lesson.language === "SQL")) return success(c, { className, source: lesson.source, - language: lesson.language, + language: mixed ? null : lesson.language, problems: lesson.problems.map((problem) => ({ problemDisplayId: problem.displayId, title: problem.title, @@ -911,10 +925,11 @@ classroomRoutes.put("/classroom/lesson", requireTeacher, async (c) => { return failure(c, 400, "problem-not-found", `这些题号不存在或没有公开:${missing.join("、")}`) } - // 选了语言就得每道都能用它交,不然学生点开才发现交不了(提交接口会拒:language-not-allowed) + // 选了语言就得每道都能用它交,不然学生点开才发现交不了(提交接口会拒:language-not-allowed)。 + // SQL 题除外:它只收 SQL,C 的作业里夹一道也不会让人选错语言,计数那边也放行了 SQL if (language) { const unsupported = found - .filter((row) => !row.languages.includes(language)) + .filter((row) => !row.languages.includes(language) && !row.languages.includes("SQL")) .map((row) => row.displayId) if (unsupported.length) { return failure( diff --git a/apps/web/src/oj/api.ts b/apps/web/src/oj/api.ts index 4d60d36..cc8be3b 100644 --- a/apps/web/src/oj/api.ts +++ b/apps/web/src/oj/api.ts @@ -42,7 +42,7 @@ import { type FlowchartStatistics, type SubmissionStatistics, type SubmissionLessons, - type RunnableLanguage, + type LessonLanguage, type SubmissionStatisticsGrid, type TodaySubmissionStatistics, } from "@oj2/contract" @@ -261,7 +261,7 @@ export function getClassLesson(className?: string) { export function setClassLesson( className: string, problemDisplayIds: string[], - language: RunnableLanguage | null, + language: LessonLanguage | null, ) { return api.put("classroom/lesson", { className, problemDisplayIds, language }) } diff --git a/apps/web/src/oj/classroom/board.vue b/apps/web/src/oj/classroom/board.vue index 46b9632..c6f249b 100644 --- a/apps/web/src/oj/classroom/board.vue +++ b/apps/web/src/oj/classroom/board.vue @@ -3,11 +3,11 @@ import { getClassBoard, setClassLesson } from "oj/api" import { useLeaveStudents } from "shared/composables/leaveStudents" import { useCollabStore } from "shared/store/collab" import { useConfigStore } from "shared/store/config" -import type { RunnableLanguage } from "@oj2/contract" +import type { LessonLanguage } from "@oj2/contract" import { errorMessage } from "utils/api" import { LANGUAGE_SHOW_VALUE } from "utils/constants" import { parseTime } from "utils/functions" -import type { ClassBoard, ClassBoardCell, ClassBoardStudent } from "utils/types" +import type { ClassBoard, ClassBoardCell, ClassBoardProblem, ClassBoardStudent } from "utils/types" /** * 课堂看板(老师):这个班、今天、这节课的几道题 × 全班学生。 @@ -40,8 +40,8 @@ const lessonInput = ref("") * 这份作业用什么语言做,跟着这一次布置走(用户定的:不是给班级定死的设置)。默认是这个班 * 上一次布置时选的;选了之后看板和学生那边的「做完」都只认这个语言交对的 */ -const lessonLanguage = ref("Python") -const LESSON_LANGUAGES: RunnableLanguage[] = ["Python", "C", "C++"] +const lessonLanguage = ref("Python") +const LESSON_LANGUAGES: LessonLanguage[] = ["Python", "C", "C++", "SQL"] const languageOptions = LESSON_LANGUAGES.map((value) => ({ label: LANGUAGE_SHOW_VALUE[value], value, @@ -291,9 +291,11 @@ const router = useRouter() */ function submissionsHref( student: ClassBoardStudent, - problemDisplayId: string | undefined, + problem: ClassBoardProblem | undefined, flowchart: boolean, ) { + // SQL 题不受布置的语言限制,格子数的是 SQL 交的 + const language = problem?.isSql ? "SQL" : board.value?.language return router.resolve({ name: "submissions", query: { @@ -301,12 +303,8 @@ function submissionsHref( exactUsername: "1", today: "1", // 布置时选了语言的,代码那边只看这个语言交的(和格子里数的一致) - ...(flowchart - ? { language: "Flowchart" } - : board.value?.language - ? { language: board.value.language } - : {}), - ...(problemDisplayId ? { problem: problemDisplayId } : {}), + ...(flowchart ? { language: "Flowchart" } : language ? { language } : {}), + ...(problem ? { problem: problem.problemDisplayId } : {}), }, }).href } @@ -388,7 +386,10 @@ function submissionsHref( 。清空再点布置就改回自动推断。