From 033a245abb822af0baaba915e2300921b9190cea Mon Sep 17 00:00:00 2001 From: yuetsh <517252939@qq.com> Date: Fri, 9 Oct 2026 19:17:42 +0800 Subject: [PATCH] =?UTF-8?q?feat(SQL=20=E9=A2=98):=20=E5=A2=9E=E5=88=A0?= =?UTF-8?q?=E6=94=B9=E9=A2=98=E9=99=A4=E4=BA=86=E7=AC=AC=201=20=E7=BB=84?= =?UTF-8?q?=EF=BC=8C=E5=85=B6=E4=BD=99=E6=B5=8B=E8=AF=95=E6=95=B0=E6=8D=AE?= =?UTF-8?q?=E5=8F=AF=E4=BB=A5=E4=B8=80=E8=A1=8C=E9=83=BD=E4=B8=8D=E6=94=B9?= =?UTF-8?q?=E2=80=94=E2=80=94=E7=94=A8=E6=9D=A5=E6=8A=93=E5=AD=A6=E7=94=9F?= =?UTF-8?q?=E5=A4=9A=E6=94=B9=EF=BC=88WHERE=20=E5=86=99=E6=BC=8F=E3=80=81?= =?UTF-8?q?=E5=86=99=E5=AE=BD=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 原来每组数据都必须有被标准答案改到的行,所以「这组里没有研发部,看学生会不会把别的部门也改了」 这种测试数据建不了。现在只有第 1 组(题面上给学生看的那组)还要求改到行;其余组一行都没改时 出题页写「一行都没改」、预览写「执行后所有表都没有变化」。删掉第 1 组、别的组顶上来时自动按 新位置重查。判题本来就不管这一条,不受影响。 Co-Authored-By: Claude Opus 5.5 --- apps/api/src/judge/sql/child.ts | 9 ++++++++- apps/api/src/judge/sql/engine.ts | 10 +++++++++- apps/api/src/judge/sql/index.ts | 10 ++++++++-- apps/api/src/routes/admin/problem.ts | 7 ++++++- apps/web/src/admin/api.ts | 2 ++ apps/web/src/admin/problem/SqlDetail.vue | 12 +++++++++--- apps/web/src/admin/problem/components/sqlScript.ts | 2 +- .../web/src/oj/problem/components/SqlProblemData.vue | 6 ++++++ packages/contract/src/admin.ts | 5 +++++ 9 files changed, 54 insertions(+), 9 deletions(-) diff --git a/apps/api/src/judge/sql/child.ts b/apps/api/src/judge/sql/child.ts index b40a463..696bd66 100644 --- a/apps/api/src/judge/sql/child.ts +++ b/apps/api/src/judge/sql/child.ts @@ -34,6 +34,7 @@ export type SqlJob = initSql: string refSql: string mode: "query" | "modify" + requireChange: boolean } /** @@ -76,7 +77,13 @@ export async function runSqlChild() { try { if (job.kind === "display") { markPhase("display") - const display = await buildDisplay(job.initSql, job.refSql, job.mode) + const display = await buildDisplay( + job.initSql, + job.refSql, + job.mode, + undefined, + job.requireChange, + ) finish({ ok: true, display }) } // 阶段由 runCase 内部回调标记:prepare(受信脚本)→ student(学生 SQL) diff --git a/apps/api/src/judge/sql/engine.ts b/apps/api/src/judge/sql/engine.ts index 62449a4..e2be058 100644 --- a/apps/api/src/judge/sql/engine.ts +++ b/apps/api/src/judge/sql/engine.ts @@ -584,12 +584,19 @@ function queryResultColumns(names: string[], tables: DisplayTable[]) { return names.map((name) => ({ name, type: types.get(name) ?? "" })) } -/** 生成题目页展示数据:初始数据表 + 期望结果。失败一律抛 SqlCaseError(出题配置问题) */ +/** + * 生成题目页展示数据:初始数据表 + 期望结果。失败一律抛 SqlCaseError(出题配置问题)。 + * + * `requireChange`:增删改题的标准答案必须改到行。题面上展示的那组(第 1 组)必须 —— + * 学生要看着「执行后的表」知道该改什么;其余组可以一行都不改(看学生会不会多改), + * 这时 changed_tables 是空数组。判题(runCase)本来就不管这一条 + */ export async function buildDisplay( initSql: string, refSql: string, mode: "query" | "modify", memoryLimitMb = 64, + requireChange = true, ) { const SQL = await sqlEngine() const db = newDatabase(SQL, memoryLimitMb) @@ -648,6 +655,7 @@ export async function buildDisplay( if (JSON.stringify(before[name]) !== JSON.stringify(after[name])) changed.add(name) } if (changed.size === 0) { + if (!requireChange) return { tables, expected: { changed_tables: [] } } throw new SqlCaseError(JudgeStatus.SYSTEM_ERROR, "标准答案未修改任何表数据,请检查题目配置") } const changedTables = dumpDisplayTables(db, changed) diff --git a/apps/api/src/judge/sql/index.ts b/apps/api/src/judge/sql/index.ts index 709f8ca..336d3d9 100644 --- a/apps/api/src/judge/sql/index.ts +++ b/apps/api/src/judge/sql/index.ts @@ -184,11 +184,17 @@ export function runSqlCase(job: Extract) { }) } -export function buildSqlDisplay(initSql: string, refSql: string, mode: "query" | "modify") { +/** `requireChange` 见 engine.ts 的 buildDisplay:只有题面上展示的那组必须改到行 */ +export function buildSqlDisplay( + initSql: string, + refSql: string, + mode: "query" | "modify", + requireChange = true, +) { // 子进程产出的形状由 engine.ts 的 dumpDisplayTables / runDisplay 决定,就是契约里的 // SqlDisplay —— 同一个仓库里的两端,不在这儿再 parse 一遍 return runJob( - { kind: "display", initSql, refSql, mode }, + { kind: "display", initSql, refSql, mode, requireChange }, { trustedMs: DISPLAY_BUDGET_MS, studentMs: null }, ) } diff --git a/apps/api/src/routes/admin/problem.ts b/apps/api/src/routes/admin/problem.ts index 3eba647..dddeb09 100644 --- a/apps/api/src/routes/admin/problem.ts +++ b/apps/api/src/routes/admin/problem.ts @@ -867,7 +867,12 @@ adminProblemRoutes.get("/problems/:id/sql-scripts", requireProblemPermission, as adminProblemRoutes.post("/sql-test-cases/preview", requireProblemPermission, async (c) => { const parsed = await parseBody(c, sqlPreviewRequestSchema) if (!parsed.success) return parsed.response - const outcome = await buildSqlDisplay(parsed.data.initSql, parsed.data.refSql, parsed.data.mode) + const outcome = await buildSqlDisplay( + parsed.data.initSql, + parsed.data.refSql, + parsed.data.mode, + parsed.data.shown, + ) if (!outcome.ok) return failure(c, 400, "sql-preview-failed", outcome.message) return success(c, outcome.value) }) diff --git a/apps/web/src/admin/api.ts b/apps/web/src/admin/api.ts index 186cccc..6c325b3 100644 --- a/apps/web/src/admin/api.ts +++ b/apps/web/src/admin/api.ts @@ -236,6 +236,8 @@ export function previewSQLTestcase(data: { initSql: string refSql: string mode: "query" | "modify" + /** 是不是题面上展示的那组(第 1 组):增删改题只有它必须改到行 */ + shown: boolean }) { return api.post("admin/sql-test-cases/preview", data) } diff --git a/apps/web/src/admin/problem/SqlDetail.vue b/apps/web/src/admin/problem/SqlDetail.vue index afad4e8..ccb00b4 100644 --- a/apps/web/src/admin/problem/SqlDetail.vue +++ b/apps/web/src/admin/problem/SqlDetail.vue @@ -221,8 +221,12 @@ function onTagsChange(tags: string[]) { // ---------------------------------------------------------------- 测试数据:自动重跑 -/** 一组数据跑出来的结果取决于这三样,任何一样变了,原来的结果就作废 */ -const signatureOf = (g: ScriptGroup) => JSON.stringify([form.answer, form.sqlConfig.mode, g.sql]) +/** + * 一组数据跑出来的结果取决于这几样,任何一样变了,原来的结果就作废。 + * 排在第一也算:删掉第 1 组之后,原来的第 2 组成了题面展示的那组,增删改题得重查它改没改到行 + */ +const signatureOf = (g: ScriptGroup) => + JSON.stringify([form.answer, form.sqlConfig.mode, g.sql, groups.value[0] === g]) function statusOf(g: ScriptGroup): GroupStatus { if (!g.sql.trim()) return "empty" @@ -243,6 +247,7 @@ async function run(g: ScriptGroup) { initSql: g.sql, refSql: form.answer, mode: form.sqlConfig.mode, + shown: groups.value[0] === g, }) } catch (err) { errorRaw = @@ -261,7 +266,8 @@ function runAll() { } watchDebounced( - () => [form.answer, form.sqlConfig.mode, groups.value.map((g) => g.sql)] as const, + // 带上 key:删组、挪了谁排第一也要重跑(见 signatureOf) + () => [form.answer, form.sqlConfig.mode, groups.value.map((g) => `${g.key}:${g.sql}`)] as const, runAll, { debounce: 600, deep: true }, ) diff --git a/apps/web/src/admin/problem/components/sqlScript.ts b/apps/web/src/admin/problem/components/sqlScript.ts index 1f77069..674c911 100644 --- a/apps/web/src/admin/problem/components/sqlScript.ts +++ b/apps/web/src/admin/problem/components/sqlScript.ts @@ -33,7 +33,7 @@ export function explainSqlError(message: string) { [/(\d+) values for (\d+) columns/, (m) => `一行给了 ${m[1]} 个值,但表有 ${m[2]} 列`], ] if (message.includes("标准答案未修改任何表数据")) { - return "标准答案在这组数据上一行都没改:增删改题的每组数据里都要有会被改到的行" + return "标准答案在这组数据上一行都没改:第 1 组是题面上给学生看的,里面要有会被改到的行" } for (const [pattern, say] of rules) { const match = message.match(pattern) diff --git a/apps/web/src/oj/problem/components/SqlProblemData.vue b/apps/web/src/oj/problem/components/SqlProblemData.vue index 6092c74..db9828b 100644 --- a/apps/web/src/oj/problem/components/SqlProblemData.vue +++ b/apps/web/src/oj/problem/components/SqlProblemData.vue @@ -33,6 +33,11 @@ const changedTables = computed(() => { })) }) +/** 增删改题里不是题面那组的测试数据可以一行都不改(看学生会不会多改),出题页预览会碰到 */ +const noChange = computed( + () => "changed_tables" in props.display.expected && !props.display.expected.changed_tables.length, +) + const anyChanged = computed(() => changedTables.value.some( ({ changes }) => changes && (changes.cells.size > 0 || changes.newRows.size > 0), @@ -83,6 +88,7 @@ const anyChanged = computed(() =>

+

执行后所有表都没有变化

执行后变了的格子

diff --git a/packages/contract/src/admin.ts b/packages/contract/src/admin.ts index 4a6bc89..00d2f1b 100644 --- a/packages/contract/src/admin.ts +++ b/packages/contract/src/admin.ts @@ -819,6 +819,11 @@ export const sqlPreviewRequestSchema = z.object({ .min(1) .max(1024 * 1024), mode: z.enum(["query", "modify"]), + /** + * 这组是不是题面上给学生看的那组(第 1 组)。增删改题只有这一组必须改到行 —— + * 其余组可以一行都不改,用来看学生会不会多改(比如 WHERE 写漏了) + */ + shown: z.boolean().default(true), }) export const generateSqlTestCaseRequestSchema = z.object({