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

+10 -2
View File
@@ -1,5 +1,6 @@
import {
addContestProblemRequestSchema,
isSqlProblem,
createProblemRequestSchema,
generateSqlTestCaseRequestSchema,
makeProblemPublicRequestSchema,
@@ -15,7 +16,7 @@ import {
type SqlTestCaseScript,
type UploadTestCaseResponse,
} from "@oj2/contract"
import { and, count, desc, eq, ilike, inArray, isNull, ne, or, sql } from "drizzle-orm"
import { and, count, desc, eq, ilike, inArray, isNull, ne, not, or, sql } from "drizzle-orm"
import { Hono } from "hono"
import { requireProblemPermission, type AppEnv } from "../../auth/middleware"
@@ -216,7 +217,7 @@ function commonChecks(data: {
}): { error: string } | { sql: boolean } {
const astError = astRulesError(pickAstRules(data.astRules, data.languages))
if (astError) return { error: astError }
if (data.languages.includes("SQL")) {
if (isSqlProblem(data)) {
if (data.languages.length !== 1)
return { error: "SQL problem cannot be mixed with other languages" }
if (!data.sqlConfig) return { error: "SQL problem requires sql_config" }
@@ -397,6 +398,11 @@ adminProblemRoutes.get("/problems", requireProblemPermission, async (c) => {
const author = c.req.query("author")?.trim()
const keyword = c.req.query("keyword")?.trim()
const tagId = c.req.query("tagId")?.trim()
// 题型:sql = SQL 题,code = 编程题(C / C++ / Python)
const kind = c.req.query("kind")
const sqlOnly = sql`${schema.problem.languages} @> '["SQL"]'::jsonb`
if (kind === "sql") filters.push(sqlOnly)
else if (kind === "code") filters.push(not(sqlOnly))
if (author) filters.push(eq(schema.user.username, author))
if (keyword) {
filters.push(
@@ -456,6 +462,7 @@ adminProblemRoutes.get("/problems", requireProblemPermission, async (c) => {
createTime: problem.createTime,
difficulty: problem.difficulty,
tags: tags.get(problem.id) ?? [],
isSql: isSqlProblem(problem),
hasAstRules: problem.astRules !== null,
allowFlowchart: problem.allowFlowchart,
showFlowchart: problem.showFlowchart,
@@ -637,6 +644,7 @@ adminProblemRoutes.get("/contests/:contestId/problems", requireProblemPermission
createTime: problem.createTime,
difficulty: problem.difficulty,
tags: tags.get(problem.id) ?? [],
isSql: isSqlProblem(problem),
hasAstRules: problem.astRules !== null,
allowFlowchart: problem.allowFlowchart,
showFlowchart: problem.showFlowchart,