feat(后台): SQL 题有了自己的出题页——左边写,右边就是学生看到的样子,改答案、改数据马上重跑

- 「新建题目」先选编程题还是 SQL 题,选了就不能改;编辑时按题目本身进对应的页
- SQL 出题页(设计稿「SQL 出题页」方案 B):测试数据一组一行,改完自动重跑,
  跑不通的用中文说原因;保存时才打包上传测试点,不用再手动「预览」「上传」
- 新建时存草稿,误关页面不丢
- 编程题出题页去掉 SQL 那一堆分支(以前勾上 SQL 会静默清掉语法要求和流程图)
- 题目列表能按题型筛,SQL 题标题后面挂标签

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
xuyueandClaude Opus 5.5 committed 2026-10-09 18:59:15 +08:00
1 parent 25d3296410
commit 2cb39fd96b
13 files changed
+1414 -185

No files matched your search

@@ -0,0 +1,62 @@
import type { SqlDisplay } from "utils/types"
import { changedRowCount, tableChanges } from "oj/problem/utils/sqlChanges"
/** 出题页的一组测试数据:一份建表 + 插数据的脚本,和它跑出来的样子 */
export interface ScriptGroup {
key: number
sql: string
/** 跑出来的结果对应的是哪一版(标准答案 + 判题方式 + 脚本),和现在的对不上就是过期了 */
ranFor: string
display: SqlDisplay | null
/** 跑不起来的原因,常见的已经翻成中文(explainSqlError) */
error: string
/** 后端回的原话,翻译过的时候用小字附在后面 */
errorRaw: string
/** 正在跑的那一版 */
running: string
}
export type GroupStatus = "empty" | "no-answer" | "running" | "ok" | "error"
/**
* SQLite 的报错是英文,出题的老师未必看得懂。常见的几种翻成一句中文,
* 认不出来的原样给(后端已经加了「初始化脚本执行失败」这类中文前缀)
*/
export function explainSqlError(message: string) {
const rules: [RegExp, (m: RegExpMatchArray) => string][] = [
[/no such table: (\S+)/, (m) => `没有 ${m[1]} 这张表`],
[/no such column: (\S+)/, (m) => `没有 ${m[1]} 这一列`],
[/table (\S+) already exists/, (m) => `${m[1]} 这张表建了两次`],
[/near "([^"]*)": syntax error/, (m) => `语法有错,在「${m[1]}」附近`],
[/incomplete input/, () => "语句没写完(少了右括号或分号?)"],
[/UNIQUE constraint failed: (\S+)/, (m) => `${m[1]} 有重复的值`],
[/(\d+) values for (\d+) columns/, (m) => `一行给了 ${m[1]} 个值,但表有 ${m[2]} 列`],
]
if (message.includes("标准答案未修改任何表数据")) {
return "标准答案在这组数据上一行都没改:增删改题的每组数据里都要有会被改到的行"
}
for (const [pattern, say] of rules) {
const match = message.match(pattern)
if (match) {
const where = message.startsWith("标准答案") ? "标准答案在这组数据上" : "这组的脚本"
return `${where}跑不起来:${say(match)}`
}
}
return message
}
/** 列表里那一行的小结:几张表各几行 → 期望几行 / 改了几行 */
export function groupSummary(display: SqlDisplay) {
const rows = display.tables.map((t) => t.total_rows).join(" + ")
const expected = display.expected
if ("columns" in expected) return `${rows} 行 → 期望 ${expected.total_rows} 行`
const changed = expected.changed_tables.reduce((sum, table) => {
if (table.dropped) return sum + 1
const changes = tableChanges(
display.tables.find((t) => t.name === table.name),
table,
)
return sum + (changes ? changedRowCount(changes) : 0)
}, 0)
return changed ? `${rows} 行 → 改了 ${changed} 行` : `${rows} 行 → 一行都没改`
}