From 06642b81314eb7a0ce3bef53f912915a9505460a Mon Sep 17 00:00:00 2001 From: yuetsh <517252939@qq.com> Date: Fri, 3 Jul 2026 01:15:58 -0600 Subject: [PATCH] =?UTF-8?q?feat:=20=E4=BF=9D=E5=AD=98=20SQL=20=E9=A2=98?= =?UTF-8?q?=E6=97=B6=E8=87=AA=E5=8A=A8=E7=94=9F=E6=88=90=20sql=5Fdisplay?= =?UTF-8?q?=20=E5=B1=95=E7=A4=BA=E6=95=B0=E6=8D=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Fable 5 --- .../migrations/0012_problem_sql_display.py | 18 ++++++++++ problem/models.py | 3 ++ problem/serializers.py | 6 ++-- problem/views/admin.py | 33 +++++++++++++++++++ 4 files changed, 57 insertions(+), 3 deletions(-) create mode 100644 problem/migrations/0012_problem_sql_display.py diff --git a/problem/migrations/0012_problem_sql_display.py b/problem/migrations/0012_problem_sql_display.py new file mode 100644 index 0000000..2507654 --- /dev/null +++ b/problem/migrations/0012_problem_sql_display.py @@ -0,0 +1,18 @@ +# Generated by Django 6.0.4 on 2026-07-03 07:14 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('problem', '0011_problem_sql_config'), + ] + + operations = [ + migrations.AddField( + model_name='problem', + name='sql_display', + field=models.JSONField(blank=True, default=None, null=True), + ), + ] diff --git a/problem/models.py b/problem/models.py index 309525f..7741671 100644 --- a/problem/models.py +++ b/problem/models.py @@ -89,6 +89,9 @@ class Problem(models.Model): # SQL 题配置: {"mode": "query"|"modify", "order_sensitive": bool},非 SQL 题为 None sql_config = models.JSONField(null=True, blank=True, default=None) + # SQL 题展示数据(保存题目时由测试点1+标准答案自动生成,见 judge/sql_runner.build_display),非 SQL 题为 None + sql_display = models.JSONField(null=True, blank=True, default=None) + class Meta: db_table = "problem" constraints = [ diff --git a/problem/serializers.py b/problem/serializers.py index 76b78a3..e3ff3f9 100644 --- a/problem/serializers.py +++ b/problem/serializers.py @@ -60,9 +60,9 @@ class CreateOrEditProblemSerializer(serializers.Serializer): _id = serializers.CharField(max_length=32, allow_blank=True, allow_null=True) title = serializers.CharField(max_length=1024) description = serializers.CharField() - input_description = serializers.CharField() - output_description = serializers.CharField() - samples = serializers.ListField(child=CreateSampleSerializer(), allow_empty=False) + input_description = serializers.CharField(allow_blank=True) + output_description = serializers.CharField(allow_blank=True) + samples = serializers.ListField(child=CreateSampleSerializer(), allow_empty=True) test_case_id = serializers.CharField(max_length=32) test_case_score = serializers.ListField(child=CreateTestCaseScoreSerializer(), allow_empty=True) time_limit = serializers.IntegerField(min_value=1, max_value=1000 * 60) diff --git a/problem/views/admin.py b/problem/views/admin.py index 3dabeeb..1c5c8dc 100644 --- a/problem/views/admin.py +++ b/problem/views/admin.py @@ -13,6 +13,7 @@ from django.http import StreamingHttpResponse from account.decorators import ensure_created_by, problem_permission_required, teacher_admin_required from contest.models import Contest, ContestStatus +from judge.sql_runner import SQLCaseError, build_display from submission.models import Submission from utils.api import APIError, APIView, CSRFExemptAPIView, validate_serializer from utils.openai import get_ai_client @@ -211,9 +212,41 @@ class ProblemBase(APIView): 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}" class ProblemAPI(ProblemBase):