update
Some checks failed
Deploy / deploy (build, debian, 22, /root/OJDeploy/data/clientnext) (push) Has been cancelled
Deploy / deploy (build:staging, school, 8822, /root/OJ/data/dist) (push) Has been cancelled

This commit is contained in:
2026-07-04 11:06:41 -06:00
parent bda28b8400
commit ff5daf8afe
3 changed files with 72 additions and 30 deletions

View File

@@ -163,6 +163,14 @@ export function getSQLTestcaseScripts(problemId: number) {
)
}
// AI 根据标准答案生成一个 SQL 测试点初始化脚本
export function generateSQLTestcase(data: {
ref_sql: string
mode: "query" | "modify"
}) {
return http.post<{ sql: string }>("admin/sql_test_case_ai_gen", data)
}
export function createProblem(problem: BlankProblem) {
return http.post("admin/problem", problem)
}

View File

@@ -3,6 +3,7 @@ import { downloadZip } from "client-zip"
import type { LANGUAGE, SQLDisplay, Testcase } from "utils/types"
import SQLDataTable from "oj/problem/components/SQLDataTable.vue"
import {
generateSQLTestcase,
getSQLTestcaseScripts,
previewSQLTestcase,
uploadTestcases,
@@ -35,7 +36,7 @@ function blankEntry(): ScriptEntry {
return { id: nextId++, sql: "", display: null, error: "", stale: false }
}
const scripts = ref<ScriptEntry[]>([blankEntry(), blankEntry()])
const scripts = ref<ScriptEntry[]>([blankEntry(), blankEntry(), blankEntry()])
const refSQL = computed(
() =>
@@ -45,8 +46,10 @@ const refSQL = computed(
const isPreviewing = ref(false)
const isUploading = ref(false)
const isGenerating = ref(false)
const hasAnyScript = computed(() => scripts.value.some((s) => s.sql.trim()))
const hasBlankScript = computed(() => scripts.value.some((s) => !s.sql.trim()))
const canUpload = computed(() => {
const filled = scripts.value.filter((s) => s.sql.trim())
@@ -83,7 +86,7 @@ function remove(index: number) {
}
function reset() {
scripts.value = [blankEntry(), blankEntry()]
scripts.value = [blankEntry(), blankEntry(), blankEntry()]
}
function expectedQuery(d: SQLDisplay) {
@@ -94,6 +97,28 @@ function changedTables(d: SQLDisplay) {
return "changed_tables" in d.expected ? d.expected.changed_tables : []
}
async function generate() {
const blanks = scripts.value.filter((s) => !s.sql.trim())
if (!blanks.length) return
isGenerating.value = true
await Promise.all(
blanks.map(async (s) => {
try {
const res = await generateSQLTestcase({
ref_sql: refSQL.value,
mode: props.mode,
})
s.sql = res.data.sql
} catch (err) {
const data = (err as { data?: unknown })?.data
message.error(typeof data === "string" ? data : "AI 生成失败")
}
}),
)
isGenerating.value = false
await preview()
}
async function preview() {
// 丢弃空脚本
scripts.value = scripts.value.filter((s) => s.sql.trim())
@@ -168,15 +193,33 @@ async function upload() {
还没有填写 SQL 标准答案请先在上方"本题参考答案"中填写再来编写测试点
</n-alert>
<n-flex align="center" wrap>
<n-button :disabled="isPreviewing" @click="reset">清空</n-button>
<n-button :disabled="isPreviewing" @click="add">+1</n-button>
<n-button :disabled="isPreviewing || isGenerating" @click="reset">
清空
</n-button>
<n-button :disabled="isPreviewing || isGenerating" @click="add">
+1
</n-button>
<n-tooltip :disabled="!!refSQL && hasBlankScript">
<template #trigger>
<span>
<n-button
:loading="isGenerating"
:disabled="!refSQL || !hasBlankScript || isPreviewing"
@click="generate"
>
AI 生成
</n-button>
</span>
</template>
{{ !refSQL ? "请先填写 SQL 标准答案" : "所有脚本都写好了,无需生成" }}
</n-tooltip>
<n-tooltip :disabled="!!refSQL && hasAnyScript">
<template #trigger>
<span>
<n-button
type="success"
:loading="isPreviewing"
:disabled="!refSQL || !hasAnyScript"
:disabled="!refSQL || !hasAnyScript || isGenerating"
@click="preview"
>
预览验证
@@ -191,7 +234,7 @@ async function upload() {
<n-button
type="primary"
:loading="isUploading"
:disabled="!canUpload"
:disabled="!canUpload || isGenerating"
@click="upload"
>
上传
@@ -207,7 +250,7 @@ async function upload() {
<strong>{{ index + 1 }}.sql</strong>
<n-button
size="small"
:disabled="scripts.length === 1 || isPreviewing"
:disabled="scripts.length === 1 || isPreviewing || isGenerating"
@click="remove(index)"
>
删除

View File

@@ -784,10 +784,10 @@ watch(
<h2 class="title">测试用例区域</h2>
<n-flex align="center" style="margin-bottom: 12px">
<n-flex v-if="!isSQLProblem" align="center" style="margin-bottom: 12px">
<div>
<n-button type="success" @click="showGeneratorModal = true">
{{ isSQLProblem ? "在线编写" : "直接生成" }}
直接生成
</n-button>
</div>
<div>
@@ -796,26 +796,25 @@ watch(
accept=".zip"
:custom-request="handleUploadTestcases"
>
<n-button type="info">
{{ isSQLProblem ? "上传数据脚本压缩包" : "(老)手动上传" }}
</n-button>
<n-button type="info">手动上传</n-button>
</n-upload>
</div>
<n-tooltip placement="right" style="max-width: 320px; white-space: normal">
<template #trigger>
<n-button text>温馨提醒</n-button>
</template>
<template v-if="isSQLProblem">
压缩包内放 1.sql2.sql每个文件是一个测试点的建表+插入数据脚本
判题时对每个测试点分别运行标准答案和学生 SQL 比对结果 建议至少 2-3
个数据不同的测试点防止学生硬编码答案
</template>
<template v-else>
测试用例最好要有10个要考虑边界情况且不要跟测试样例一模一样
</template>
测试用例最好要有10个要考虑边界情况且不要跟测试样例一模一样
</n-tooltip>
</n-flex>
<SQLTestcaseEditor
v-if="isSQLProblem"
:answers="problem.answers"
:mode="problem.sql_config?.mode ?? 'query'"
:problem-id="problem.id"
@uploaded="handleTestcasesGenerated"
/>
<n-alert
class="box"
v-if="problem.test_case_score.length"
@@ -845,20 +844,12 @@ watch(
<n-modal
v-model:show="showGeneratorModal"
preset="card"
:title="isSQLProblem ? 'SQL 测试点编辑器' : '测试用例生成器'"
title="测试用例生成器"
style="width: 80vw; max-width: 900px"
:mask-closable="false"
display-directive="show"
>
<SQLTestcaseEditor
v-if="isSQLProblem"
:answers="problem.answers"
:mode="problem.sql_config?.mode ?? 'query'"
:problem-id="problem.id"
@uploaded="handleTestcasesGenerated"
/>
<TestcaseGenerator
v-else
:answers="problem.answers"
:samples="problem.samples"
@uploaded="handleTestcasesGenerated"
@@ -909,7 +900,7 @@ watch(
</n-form-item>
</n-form>
</template>
<n-flex style="margin-bottom: 120px" align="center" justify="end">
<n-flex style="margin: 16px 0 120px" align="center" justify="end">
<n-button type="primary" @click="submit">提交</n-button>
</n-flex>
</template>