fix(课堂看板): 作业里有 SQL 题就布置不上去(报「不能用 C 交」),也没法布置一节 SQL 课

- 布置作业的语言加上 SQL,数据库课整节选 SQL
- C / Python 的作业里可以夹 SQL 题:SQL 题只收 SQL,不受作业语言限制,交对 SQL 就算做完
  (学生首页课堂条、看板格子、班里做了几个人三处一起改)
- 看板点 SQL 题的格子,提交列表按 SQL 筛,不再是一片空白;提交列表「这节课」遇到夹了
  SQL 题的作业不再按 C 筛

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
xuyueandClaude Opus 5.5 committed 2026-10-09 18:18:10 +08:00
1 parent 98dfcc1e7e
commit fcf63c2ecb
7 files changed
+71 -42

No files matched your search

+4 -4
View File
@@ -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<number[]>(),
/**
* 这份作业用什么语言做(Python / C / C++),老师布置时选。可空:加这一列之前布置的
* 没有它。写入侧在 PUT /classroom/lesson 按 runnableLanguageSchema 校验过
* 这份作业用什么语言做(Python / C / C++ / SQL),老师布置时选。可空:加这一列之前布置的
* 没有它。写入侧在 PUT /classroom/lesson 按 lessonLanguageSchema 校验过
*/
language: text().$type<RunnableLanguage>(),
language: text().$type<LessonLanguage>(),
createdBy: integer("created_by").notNull(),
updatedAt: timestamp("updated_at", {
withTimezone: true,
+26 -11
View File
@@ -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<typeof classMemberIds>
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<ClassDayGroup[]> {
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<number, { id: number; displayId: string; title: string }>()
if (!ids.length)
return new Map<number, { id: number; displayId: string; title: string; isSql: boolean }>()
const rows = await db
.select({
id: schema.problem.id,
displayId: schema.problem.displayId,
title: schema.problem.title,
isSql: sql<boolean>`${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(
+2 -2
View File
@@ -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<null>("classroom/lesson", { className, problemDisplayIds, language })
}
+14 -19
View File
@@ -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<RunnableLanguage>("Python")
const LESSON_LANGUAGES: RunnableLanguage[] = ["Python", "C", "C++"]
const lessonLanguage = ref<LessonLanguage>("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(
<n-text depth="3" class="meta">
<template v-if="board.source === 'teacher'">
学生首页的「班里在做」显示的就是这几道<template v-if="board.language"
>,用 {{ LANGUAGE_SHOW_VALUE[board.language] }} 交对才算做完</template
>,用 {{ LANGUAGE_SHOW_VALUE[board.language] }} 交对才算做完<template
v-if="board.language !== 'SQL' && board.problems.some((p) => p.isSql)"
>(SQL 题交对 SQL 就算)</template
></template
>。清空再点布置就改回自动推断。
</template>
<template v-else-if="board.source === 'inferred'">
@@ -515,13 +516,7 @@ function submissionsHref(
<a
v-if="cell.attempts"
class="cell-link"
:href="
submissionsHref(
student,
board.problems[i]!.problemDisplayId,
!cell.codeAttempts,
)
"
:href="submissionsHref(student, board.problems[i], !cell.codeAttempts)"
target="_blank"
:title="
cell.codeAttempts
+7 -1
View File
@@ -300,7 +300,13 @@ export type { ClassComparison, ClassRankItem, ClassUserRank } from "@oj2/contrac
export type { ClassActivity, ClassActivityProblem, LastVisit } from "@oj2/contract"
/** 课堂看板:一个班今天这节课的题 × 全班学生 */
export type { ClassBoard, ClassBoardStudent, ClassBoardCell, ClassLesson } from "@oj2/contract"
export type {
ClassBoard,
ClassBoardProblem,
ClassBoardStudent,
ClassBoardCell,
ClassLesson,
} from "@oj2/contract"
/** 知识点地图:每个知识点做对几道、在第几档,只给自己看 */
export type { KnowledgeLevel, KnowledgeMap } from "@oj2/contract"
+11 -5
View File
@@ -1,7 +1,7 @@
import { z } from "zod"
import { judgeStatusSchema } from "./judge-status"
import { runnableLanguageSchema } from "./language"
import { lessonLanguageSchema as lessonLanguageValueSchema } from "./language"
export const classRankItemSchema = z.object({
className: z.string(),
@@ -85,10 +85,11 @@ export const classActivityProblemSchema = z.object({
})
/**
* 老师布置这节课时选的语言(Python / C / C++)。null = 没选:推断出来的课、加这一项之前
* 布置的课。选了之后这份作业「做完」只认这个语言交对的,学生打开这几道题编辑器默认就是它
* 老师布置这节课时选的语言(Python / C / C++ / SQL)。null = 没选:推断出来的课、加这一项之前
* 布置的课。选了之后这份作业「做完」只认这个语言交对的(SQL 题例外,交对 SQL 就算),
* 学生打开这几道题编辑器默认就是它
*/
const lessonLanguageSchema = runnableLanguageSchema.nullable()
const lessonLanguageSchema = lessonLanguageValueSchema.nullable()
export const classActivitySchema = z.object({
className: z.string().nullable(),
@@ -147,6 +148,8 @@ 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({
@@ -214,7 +217,10 @@ export const classBoardSchema = z.object({
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() })),
})
+7
View File
@@ -43,7 +43,14 @@ export const problemLanguageSchema = z.enum([...judgeLanguageSchema.options, "SQ
*/
export const runnableLanguageSchema = judgeLanguageSchema.extract(["C", "C++", "Python"])
/**
* 老师布置作业能选的语言:判题机跑得了的三种,加 SQL(数据库课一整节都是 SQL 题)。
* SQL 题只收 SQL,所以 C / Python 的作业里夹 SQL 题也行 —— 那几道不受作业语言限制
*/
export const lessonLanguageSchema = problemLanguageSchema.extract(["Python", "C", "C++", "SQL"])
export type JudgeLanguage = z.infer<typeof judgeLanguageSchema>
export type LessonLanguage = z.infer<typeof lessonLanguageSchema>
export type RunnableLanguage = z.infer<typeof runnableLanguageSchema>
export type ProblemLanguage = z.infer<typeof problemLanguageSchema>