Files
OnlineJudge/docs/superpowers/plans/2026-07-03-sql-problem-display.md

26 KiB
Raw Permalink Blame History

SQL 题数据化展示实施计划

For agentic workers: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (- [ ]) syntax for tracking.

Goal: SQL 题的题目页展示真实的数据表和期望结果(自动从测试点 1 + 标准答案生成),替代不适用的「输入/输出/例子」文本区块。

Architecture: 后端在 judge/sql_runner.py 新增 build_display()(内存 sqlite 跑初始化脚本 + 标准答案,导出结构化 JSON管理端保存题目时在 ProblemBase.common_checks 中生成并写入新字段 Problem.sql_display。前端学生端用 sql_config 判断 SQL 题,隐藏输入/输出/例子区块,改渲染「数据表」「期望结果」表格;管理端编辑页对 SQL 题隐藏无意义的表单项。

Tech Stack: Django 6 + DRF后端OnlineJudge/ 仓库、Vue 3 + TypeScript + Naive UI前端ojnext/ 仓库)。

Spec: OnlineJudge/docs/superpowers/specs/2026-07-03-sql-problem-display-design.md

Global Constraints

  • 不写任何新测试项目测试政策Do not write new tests。验证靠 ruff checkpython manage.py shell -c 手动执行、npm run build
  • 两个子项目是独立 git 仓库:后端提交在 OnlineJudge/,前端提交在 ojnext/,根目录没有 git。
  • 后端 lintruff check .180 列,双引号)。前端格式化:npm run fmt
  • 展示行数上限 20 行(DISPLAY_ROW_LIMIT = 20);判题行数上限沿用现有 ROW_LIMIT = 10_000
  • sql_display 只由服务端生成,客户端不可提交该字段(不加入 CreateOrEditProblemSerializer)。
  • 错误消息用中文(与 sql_runner.py 现有消息一致),面向出题人。

Task 1: 后端 build_display() 生成展示数据

Files:

  • Modify: OnlineJudge/judge/sql_runner.py(文件末尾追加)

Interfaces:

  • Consumes: 本文件已有的 _new_db_execute_trusted_dump_tablessplit_statements_truncateSQLCaseErrorROW_LIMITPROGRESS_STEP

  • Produces: build_display(init_sql: str, ref_sql: str, mode: str, *, memory_limit_mb: int = 64) -> dict,失败抛 SQLCaseError。返回结构:

    • {"tables": [TableDump, ...], "expected": ExpectedQuery | ExpectedModify}
    • TableDump = {"name": str, "columns": [{"name": str, "type": str}], "rows": [[原始值]], "total_rows": int, "truncated": bool}
    • ExpectedQuery = {"columns": [str], "rows": [[原始值]], "total_rows": int, "truncated": bool}query 模式)
    • ExpectedModify = {"changed_tables": [TableDump, ...]}modify 模式,只含执行标准答案后内容有变化的表)
    • 被标准答案 DROP 的表以 {"dropped": true, "rows": [], "total_rows": 0} 条目出现columns 取初始结构);标准答案未改任何表时抛 SQLCaseError
  • Step 1: 在 sql_runner.py 末尾追加展示数据生成代码

在文件顶部常量区(ERROR_MESSAGE_MAX_LEN = 200 之后)加:

# 题目页展示的行数上限(示例数据/期望结果)
DISPLAY_ROW_LIMIT = 20

在文件末尾追加:

def _display_value(v):
    """转为 JSON 可序列化的展示值bytes 转 hexNone/数值/字符串原样。"""
    if isinstance(v, bytes):
        return v.hex()
    return v


def _dump_display_tables(conn, only=None):
    """按建表顺序 dump 用户表的原始行用于展示(区别于 _dump_tables 的归一化判题态)。

    only 为表名集合时只导出这些表。
    """
    cursor = conn.execute("SELECT name FROM sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite_%'")
    tables = [r[0] for r in cursor.fetchall()]
    result = []
    for table in tables:
        if only is not None and table not in only:
            continue
        quoted = table.replace('"', '""')
        columns = [{"name": r[1], "type": r[2] or ""} for r in conn.execute(f'PRAGMA table_info("{quoted}")').fetchall()]
        total = conn.execute(f'SELECT COUNT(*) FROM "{quoted}"').fetchone()[0]
        rows = conn.execute(f'SELECT * FROM "{quoted}" LIMIT {DISPLAY_ROW_LIMIT}').fetchall()
        result.append(
            {
                "name": table,
                "columns": columns,
                "rows": [[_display_value(v) for v in row] for row in rows],
                "total_rows": total,
                "truncated": total > DISPLAY_ROW_LIMIT,
            }
        )
    return result


def build_display(init_sql, ref_sql, mode, *, memory_limit_mb=64):
    """生成题目页展示数据:初始数据表 + 期望结果。

    供管理端保存题目时调用;任何失败抛 SQLCaseError出题配置问题调用方转为报错信息    """
    trusted_limit_s = 10
    conn = _new_db(memory_limit_mb)
    try:
        _execute_trusted(conn, init_sql, time.monotonic() + trusted_limit_s, "初始化脚本执行失败")
        tables = _dump_display_tables(conn)
        if mode == "query":
            expected = None
            deadline = time.monotonic() + trusted_limit_s
            conn.set_progress_handler(lambda: 1 if time.monotonic() > deadline else 0, PROGRESS_STEP)
            try:
                for stmt in split_statements(ref_sql):
                    cursor = conn.execute(stmt)
                    if cursor.description is not None:
                        columns = [d[0] for d in cursor.description]
                        rows = cursor.fetchmany(ROW_LIMIT + 1)
                        if len(rows) > ROW_LIMIT:
                            raise SQLCaseError(JudgeStatus.SYSTEM_ERROR, f"标准答案结果超过 {ROW_LIMIT} 行")
                        expected = {
                            "columns": columns,
                            "rows": [[_display_value(v) for v in row] for row in rows[:DISPLAY_ROW_LIMIT]],
                            "total_rows": len(rows),
                            "truncated": len(rows) > DISPLAY_ROW_LIMIT,
                        }
                    cursor.close()
            except sqlite3.Error as e:
                msg = "超时" if "interrupted" in str(e) else _truncate(e)
                raise SQLCaseError(JudgeStatus.SYSTEM_ERROR, f"标准答案执行失败: {msg}")
            finally:
                conn.set_progress_handler(None, PROGRESS_STEP)
            if expected is None:
                raise SQLCaseError(JudgeStatus.SYSTEM_ERROR, "标准答案未产生查询结果集")
        else:
            before = _dump_tables(conn)
            _execute_trusted(conn, ref_sql, time.monotonic() + trusted_limit_s, "标准答案执行失败")
            after = _dump_tables(conn)
            changed = {name for name in set(before) | set(after) if before.get(name) != after.get(name)}
            expected = {"changed_tables": _dump_display_tables(conn, only=changed)}
        return {"tables": tables, "expected": expected}
    finally:
        conn.close()
  • Step 2: 手动验证 query 模式与 modify 模式
cd /home/xuyue/Projects/OJ/OnlineJudge && python manage.py shell -c "
from judge.sql_runner import build_display
init = '''CREATE TABLE students(id INTEGER PRIMARY KEY, name TEXT, score REAL);
INSERT INTO students VALUES (1,'小明',95),(2,'小红',88),(3,'小刚',91);'''
import json
r = build_display(init, \"SELECT name, score FROM students WHERE score > 90;\", 'query')
print(json.dumps(r, ensure_ascii=False, indent=1))
r2 = build_display(init, \"DELETE FROM students WHERE score < 90;\", 'modify')
print(json.dumps(r2, ensure_ascii=False, indent=1))
"

Expected

  • query 输出 tables 含 students 表3 行、columns 带 INTEGER/TEXT/REAL 类型),expected.columns == ["name", "score"]expected.rows == [["小明", 95.0], ["小刚", 91.0]]
  • modify 输出 expected.changed_tables 只含 students2 行,小红被删)

再验证错误路径:

cd /home/xuyue/Projects/OJ/OnlineJudge && python manage.py shell -c "
from judge.sql_runner import build_display, SQLCaseError
try:
    build_display('CREATE TABLE t(a);', 'SELEC bad;', 'query')
except SQLCaseError as e:
    print('OK:', e.message)
"

Expected打印 OK: 标准答案执行失败: ...syntax error...

  • Step 3: Lint

Run: cd /home/xuyue/Projects/OJ/OnlineJudge && ruff check judge/sql_runner.py && ruff format --check judge/sql_runner.py Expected: 无报错(有格式问题就跑 ruff format judge/sql_runner.py

  • Step 4: CommitOnlineJudge 仓库)
cd /home/xuyue/Projects/OJ/OnlineJudge && git add judge/sql_runner.py && git commit -m "feat: SQL 题展示数据生成 build_display

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>"

Task 2: 后端 sql_display 字段 + 保存时生成

Files:

  • Modify: OnlineJudge/problem/models.pysql_config 字段之后,约 line 90
  • Create: OnlineJudge/problem/migrations/makemigrations 自动生成)
  • Modify: OnlineJudge/problem/serializers.py:62-65(放宽 input/output/samples 校验)
  • Modify: OnlineJudge/problem/views/admin.pyProblemBase.common_checks,约 line 192-216

Interfaces:

  • Consumes: Task 1 的 build_display(init_sql, ref_sql, mode, *, memory_limit_mb=64)SQLCaseErrore.message 属性)

  • Produces: Problem.sql_displayJSONFieldnull=True结构即 Task 1 返回值);随现有 ProblemSerializer / ProblemSafeSerializer / ProblemAdminSerializer 自动下发(三者均为 ModelSerializer 且未排除该字段)。common_checks 在 SQL 题保存时填充 data["sql_display"],非 SQL 题置 None。

  • Step 1: 模型加字段

problem/models.py,紧跟 sql_config 字段之后加:

    # SQL 题展示数据保存题目时由测试点1+标准答案自动生成,见 judge/sql_runner.build_display非 SQL 题为 None
    sql_display = models.JSONField(null=True, blank=True, default=None)
  • Step 2: 生成并应用迁移

Run: cd /home/xuyue/Projects/OJ/OnlineJudge && python manage.py makemigrations problem && python manage.py migrate problem Expected: 生成一个 Add field sql_display to problem 迁移并应用成功

  • Step 3: 放宽序列化器校验

problem/serializers.pyCreateOrEditProblemSerializer 的三个字段改为(原来 input_description / output_description 不允许空、samples 不允许空列表SQL 题不填这些,改由 common_checks 对非 SQL 题保持强校验):

    input_description = serializers.CharField(allow_blank=True)
    output_description = serializers.CharField(allow_blank=True)
    samples = serializers.ListField(child=CreateSampleSerializer(), allow_empty=True)
  • Step 4: common_checks 接入生成逻辑

problem/views/admin.py 顶部 import 区加:

from judge.sql_runner import SQLCaseError, build_display

ProblemBase 类改为(替换现有 common_checks 的 SQL 分支,并新增 _build_sql_display 方法):

class ProblemBase(APIView):
    def common_checks(self, request):
        data = request.data
        if data["rule_type"] == ProblemRuleType.OI:
            total_score = 0
            for item in data["test_case_score"]:
                if item["score"] <= 0:
                    return "Invalid score"
                else:
                    total_score += item["score"]
            data["total_score"] = total_score
        data["languages"] = list(data["languages"])

        # SQL 题校验:.sql 测试点与 .in/.out 沙箱判题互斥SQL 必须是唯一语言
        if "SQL" in data["languages"]:
            if data["languages"] != ["SQL"]:
                return "SQL problem cannot be mixed with other languages"
            if not data.get("sql_config"):
                return "SQL problem requires sql_config"
            has_sql_answer = any(item.get("language") == "SQL" and item.get("code", "").strip() for item in (data.get("answers") or []))
            if not has_sql_answer:
                return "SQL problem requires a SQL reference answer"
            return self._build_sql_display(data)
        else:
            # 序列化器已放宽SQL 题不填这些),非 SQL 题在此保持原有强校验
            if not data["input_description"] or not data["output_description"]:
                return "Input and output description are required"
            if not data["samples"]:
                return "Samples are required"
            # 防脏数据:非 SQL 题不应携带 SQL 配置
            data["sql_config"] = None
            data["sql_display"] = None

    def _build_sql_display(self, data):
        """SQL 题用测试点1的初始化脚本 + 标准答案生成题目页展示数据。返回错误信息字符串,成功返回 None。"""
        test_case_dir = os.path.join(settings.TEST_CASE_DIR, data["test_case_id"])
        try:
            with open(os.path.join(test_case_dir, "info"), encoding="utf-8") as f:
                info = json.load(f)
        except (OSError, json.JSONDecodeError):
            return "测试点信息读取失败,请重新上传测试点"
        if not info.get("sql"):
            return "测试点不是 SQL 类型,请重新上传 SQL 测试点压缩包"
        keys = sorted(info["test_cases"].keys(), key=natural_sort_key)
        if not keys:
            return "题目没有任何测试点"
        input_name = info["test_cases"][keys[0]]["input_name"]
        try:
            with open(os.path.join(test_case_dir, input_name), encoding="utf-8") as f:
                init_sql = f.read()
        except OSError:
            return f"测试点脚本 {input_name} 读取失败"
        ref_sql = next(item["code"] for item in data["answers"] if item.get("language") == "SQL" and item.get("code", "").strip())
        try:
            data["sql_display"] = build_display(init_sql, ref_sql, data["sql_config"]["mode"])
        except SQLCaseError as e:
            return f"SQL 展示数据生成失败: {e.message}"

说明:osjsonsettingsnatural_sort_key 均已在该文件 import。postProblem.objects.create(**data))和 putsetattr 循环)都直接消费 datasql_display 无需再改动创建/更新代码。ContestProblemAPI 复用同一 common_checks,比赛 SQL 题自动生效。

  • Step 5: 手动验证 _build_sql_display 全链路
cd /home/xuyue/Projects/OJ/OnlineJudge && python manage.py shell -c "
import json, os
from django.conf import settings
from problem.views.admin import ProblemBase

# 造一个假测试点目录
tcid = 'plan_verify_sql_display'
d = os.path.join(settings.TEST_CASE_DIR, tcid)
os.makedirs(d, exist_ok=True)
open(os.path.join(d, '1.sql'), 'w').write(\"CREATE TABLE t(a INTEGER, b TEXT); INSERT INTO t VALUES (1,'x'),(2,'y');\")
open(os.path.join(d, 'info'), 'w').write(json.dumps({'sql': True, 'test_cases': {'1': {'input_name': '1.sql', 'output_name': '1.sql'}}}))

data = {'test_case_id': tcid, 'answers': [{'language': 'SQL', 'code': 'SELECT b FROM t ORDER BY a;'}], 'sql_config': {'mode': 'query'}}
err = ProblemBase()._build_sql_display(data)
print('err:', err)
print(json.dumps(data['sql_display'], ensure_ascii=False))
import shutil; shutil.rmtree(d)
"

Expected: err: Nonesql_displaytables(表 t2 行)与 expectedcolumns ["b"]rows [["x"],["y"]]

  • Step 6: Lint + Django 检查

Run: cd /home/xuyue/Projects/OJ/OnlineJudge && ruff check problem/ && python manage.py check Expected: 无报错

  • Step 7: CommitOnlineJudge 仓库)
cd /home/xuyue/Projects/OJ/OnlineJudge && git add problem/ && git commit -m "feat: 保存 SQL 题时自动生成 sql_display 展示数据

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>"

Task 3: 学生端「数据表 / 期望结果」渲染

Files:

  • Modify: ojnext/src/utils/types.tsSQLConfig 接口之后,约 line 73Problem 接口的 sql_config 字段之后,约 line 175
  • Create: ojnext/src/oj/problem/components/SQLDataTable.vue
  • Modify: ojnext/src/oj/problem/components/ProblemContent.vue

Interfaces:

  • Consumes: 后端 problem.sql_display(结构见 Task 1 Producesproblem.sql_config

  • Produces: SQLDataTable.vue 组件propscolumns: { name: string; type?: string }[]rows: (string | number | null)[][]totalRows?: numbertruncated?: boolean;类型 SQLDisplaySQLDisplayTableSQLDisplayColumnTask 4 不依赖,仅本任务内使用)

  • Step 1: types.ts 加类型

SQLConfig 接口定义之后追加:

export interface SQLDisplayColumn {
  name: string
  type?: string
}

export interface SQLDisplayTable {
  name: string
  columns: SQLDisplayColumn[]
  rows: (string | number | null)[][]
  total_rows: number
  truncated: boolean
}

export interface SQLDisplay {
  tables: SQLDisplayTable[]
  expected:
    | {
        columns: string[]
        rows: (string | number | null)[][]
        total_rows: number
        truncated: boolean
      }
    | { changed_tables: SQLDisplayTable[] }
}

Problem 接口中 sql_config?: SQLConfig | null 之后加:

  // SQL 题展示数据(后端保存题目时自动生成)
  sql_display?: SQLDisplay | null
  • Step 2: 新建 SQLDataTable.vue
<script setup lang="ts">
import type { SQLDisplayColumn } from "utils/types"

defineProps<{
  columns: SQLDisplayColumn[]
  rows: (string | number | null)[][]
  totalRows?: number
  truncated?: boolean
}>()
</script>

<template>
  <n-table class="sqlTable" size="small" :single-line="false">
    <thead>
      <tr>
        <th v-for="(col, i) in columns" :key="i">
          {{ col.name }}
          <span v-if="col.type" class="colType">{{ col.type }}</span>
        </th>
      </tr>
    </thead>
    <tbody>
      <tr v-if="rows.length === 0">
        <td :colspan="columns.length" class="nullCell">空表</td>
      </tr>
      <tr v-for="(row, i) in rows" :key="i">
        <td v-for="(v, j) in row" :key="j" :class="{ nullCell: v === null }">
          {{ v === null ? "NULL" : v }}
        </td>
      </tr>
    </tbody>
  </n-table>
  <p v-if="truncated" class="truncNote">
     {{ totalRows }} 仅展示前 {{ rows.length }} 
  </p>
</template>

<style scoped>
.sqlTable {
  margin-bottom: 8px;
}

.colType {
  font-size: 12px;
  opacity: 0.55;
  margin-left: 4px;
  font-weight: normal;
}

.nullCell {
  opacity: 0.45;
  font-style: italic;
}

.truncNote {
  font-size: 13px;
  opacity: 0.65;
  margin: 0 0 8px;
}
</style>

n-table 由 unplugin 自动导入,无需手动 import。

  • Step 3: ProblemContent.vue 集成

script 部分,在 import { getSimilarProblems } from "oj/api" 之后加:

import SQLDataTable from "./SQLDataTable.vue"

const problemSetId = ... 附近加计算属性:

// SQL 题:隐藏输入/输出/例子,改为渲染数据表与期望结果
const isSQL = computed(() => !!problem.value?.sql_config)
const sqlDisplay = computed(() => problem.value?.sql_display ?? null)
const sqlExpectedQuery = computed(() => {
  const exp = sqlDisplay.value?.expected
  return exp && "columns" in exp ? exp : null
})
const sqlChangedTables = computed(() => {
  const exp = sqlDisplay.value?.expected
  return exp && "changed_tables" in exp ? exp.changed_tables : []
})

template 部分三处修改:

(a) 「输入」「输出」两个区块(<p class="title">…输入…</p> + MdPreview…输出… + MdPreview,现 line 272-294整体包进 <template v-if="!isSQL">…</template>

(b) 例子循环 <div v-for="(sample, index) of samples" :key="index">…</div>(现 line 337-380整体包进 <template v-if="!isSQL">…</template>(注意 v-if 不能与 v-for 放同一元素上)。

(c) 在 (a) 的 </template> 之后(即描述区块后、提示区块前)插入 SQL 区块:

    <template v-if="isSQL && sqlDisplay">
      <p class="title" :style="style">
        <n-flex align="center">
          <Icon icon="devicon:sqlite"></Icon>
          数据表
        </n-flex>
      </p>
      <div v-for="t in sqlDisplay.tables" :key="t.name">
        <p class="sqlTableName">{{ t.name }}</p>
        <SQLDataTable
          :columns="t.columns"
          :rows="t.rows"
          :total-rows="t.total_rows"
          :truncated="t.truncated"
        />
      </div>

      <p class="title" :style="style">
        <n-flex align="center">
          <Icon icon="streamline-ultimate-color:check-button"></Icon>
          期望结果
        </n-flex>
      </p>
      <template v-if="sqlExpectedQuery">
        <SQLDataTable
          :columns="sqlExpectedQuery.columns.map((name) => ({ name }))"
          :rows="sqlExpectedQuery.rows"
          :total-rows="sqlExpectedQuery.total_rows"
          :truncated="sqlExpectedQuery.truncated"
        />
        <p v-if="!problem.sql_config?.order_sensitive" class="sqlNote">
          结果顺序不限
        </p>
      </template>
      <div v-for="t in sqlChangedTables" :key="t.name">
        <p class="sqlTableName">执行后的 {{ t.name }} 表</p>
        <SQLDataTable
          :columns="t.columns"
          :rows="t.rows"
          :total-rows="t.total_rows"
          :truncated="t.truncated"
        />
      </div>
    </template>

style 部分追加:

.sqlTableName {
  font-weight: 600;
  margin: 8px 0 4px;
  font-family: "Monaco";
}

.sqlNote {
  font-size: 13px;
  opacity: 0.65;
  margin: 0 0 8px;
}
  • Step 4: 构建验证

Run: cd /home/xuyue/Projects/OJ/ojnext && npm run build Expected: 构建成功、无 TypeScript 报错

  • Step 5: 格式化 + Commitojnext 仓库)
cd /home/xuyue/Projects/OJ/ojnext && npm run fmt && git add src/utils/types.ts src/oj/problem/components/SQLDataTable.vue src/oj/problem/components/ProblemContent.vue && git commit -m "feat: SQL 题展示数据表与期望结果

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>"

Task 4: 管理端编辑页隐藏 SQL 题无关表单

Files:

  • Modify: ojnext/src/admin/problem/detail.vue

Interfaces:

  • Consumes: 该文件已有的 isSQLProblem computedline 184validateProblem()submit()

  • Produces: 无对外接口SQL 题提交时 samples 置空数组、input_description/output_description 不做必填校验(后端 Task 2 已放宽)

  • Step 1: 模板隐藏三处表单

(a) 「输入的描述」「输出的描述」两个 TextEditorline 594-603v-if="ready" 改为 v-if="ready && !isSQLProblem"

(b) 样例编辑块与「添加用例」按钮line 604-633<div class="box" v-for=...><n-button class="addSamples box" ...>)整体包进 <template v-if="!isSQLProblem">…</template>

  • Step 2: validateProblem() 跳过 SQL 题的相关校验

题目描述校验line 345-352改为

  else if (
    !problem.value.description ||
    (!isSQLProblem.value &&
      (!problem.value.input_description || !problem.value.output_description))
  ) {

样例两处校验line 354、359-363分别加 !isSQLProblem.value && 前缀:

  else if (!isSQLProblem.value && problem.value.samples.length == 0) {
  else if (
    !isSQLProblem.value &&
    problem.value.samples.some(
      (sample) => sample.output === "" || sample.input === "",
    )
  ) {
  • Step 3: 提交时清空 SQL 题的样例

filterAnswers() 函数line 429-433之后加

function filterSamplesForSQL() {
  // SQL 题不展示样例;后端 CreateSampleSerializer 也不接受空字符串样例
  if (isSQLProblem.value) problem.value.samples = []
}

submit() 中在 filterAnswers() 调用之后加一行 filterSamplesForSQL()

  • Step 4: 构建验证

Run: cd /home/xuyue/Projects/OJ/ojnext && npm run build Expected: 构建成功

  • Step 5: 格式化 + Commitojnext 仓库)
cd /home/xuyue/Projects/OJ/ojnext && npm run fmt && git add src/admin/problem/detail.vue && git commit -m "feat: 管理端 SQL 题隐藏输入输出描述与样例表单

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>"

Task 5: 端到端人工验收

Files: 无代码改动,纯验证。

Interfaces:

  • Consumes: Task 1-4 全部成果;本地服务(docker-compose up -d oj-postgres oj-redispython dev.pynpm start

  • Step 1: 起服务

cd /home/xuyue/Projects/OJ/OnlineJudge && docker-compose up -d oj-postgres oj-redis
cd /home/xuyue/Projects/OJ/OnlineJudge && python dev.py   # 后台/另开终端
cd /home/xuyue/Projects/OJ/ojnext && npm start            # 后台/另开终端
  • Step 2: 管理端建一道 SQL 查询题

浏览器(或 agent-browser操作 http://localhost:5173

  1. 管理后台新建题目,语言勾选 SQL → 确认「输入的描述」「输出的描述」「测试样例」表单已隐藏
  2. 上传 SQL 测试点 zip内含 1.sql:建表 + 插数据脚本),填写 SQL 标准答案,保存
  3. Expected: 保存成功故意把标准答案改成语法错误再保存Expected: 报错「SQL 展示数据生成失败: 标准答案执行失败…」
  • Step 3: 学生端查看题目页

打开该题:

  • Expected: 无「输入/输出/例子」区块;有「数据表」区块(表名 + 列类型标注 + 数据行)和「期望结果」区块(结果表 + "结果顺序不限"提示)

  • 提交正确 SQLExpected: Accepted确认判题链路未受影响

  • Step 4: 建一道 modify 题验证 changed_tables

同流程建 mode=modify 的题(标准答案为 UPDATE/DELETEExpected: 题目页「期望结果」显示「执行后的 XX 表」,且只含被修改的表。