diff --git a/ojplatform/settings.py b/ojplatform/settings.py index 0901453..c92ca29 100644 --- a/ojplatform/settings.py +++ b/ojplatform/settings.py @@ -30,6 +30,7 @@ INSTALLED_APPS = [ 'corsheaders', 'requirements', 'submissions', + 'web', ] MIDDLEWARE = [ diff --git a/ojplatform/urls.py b/ojplatform/urls.py index cb4e613..3dc7e9d 100644 --- a/ojplatform/urls.py +++ b/ojplatform/urls.py @@ -6,6 +6,7 @@ from django.urls import include, path urlpatterns = [ path('admin/', admin.site.urls), + path('', include('web.urls')), path('api/', include('requirements.urls')), path('api/auth/login/', include('rest_framework.urls')), ] diff --git a/web/__init__.py b/web/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/web/admin.py b/web/admin.py new file mode 100644 index 0000000..8c38f3f --- /dev/null +++ b/web/admin.py @@ -0,0 +1,3 @@ +from django.contrib import admin + +# Register your models here. diff --git a/web/apps.py b/web/apps.py new file mode 100644 index 0000000..560f56c --- /dev/null +++ b/web/apps.py @@ -0,0 +1,5 @@ +from django.apps import AppConfig + + +class WebConfig(AppConfig): + name = 'web' diff --git a/web/migrations/__init__.py b/web/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/web/models.py b/web/models.py new file mode 100644 index 0000000..71a8362 --- /dev/null +++ b/web/models.py @@ -0,0 +1,3 @@ +from django.db import models + +# Create your models here. diff --git a/web/templates/web/base.html b/web/templates/web/base.html new file mode 100644 index 0000000..2f2211d --- /dev/null +++ b/web/templates/web/base.html @@ -0,0 +1,59 @@ + + + + + +{% block title %}OJplatform{% endblock %} + + + + +
+ {% if messages %}{% for m in messages %}
{{ m }}
{% endfor %}{% endif %} + {% block content %}{% endblock %} +
+ + diff --git a/web/templates/web/create_requirement.html b/web/templates/web/create_requirement.html new file mode 100644 index 0000000..199ed1a --- /dev/null +++ b/web/templates/web/create_requirement.html @@ -0,0 +1,35 @@ +{% extends "web/base.html" %} +{% block title %}新建作业要求 · OJplatform{% endblock %} +{% block content %} +

新建作业要求

+
+
+ {% csrf_token %} + + + + + + + + + +

+
+
+ +{% endblock %} diff --git a/web/templates/web/dashboard.html b/web/templates/web/dashboard.html new file mode 100644 index 0000000..0b54f61 --- /dev/null +++ b/web/templates/web/dashboard.html @@ -0,0 +1,38 @@ +{% extends "web/base.html" %} +{% block title %}仪表盘 · OJplatform{% endblock %} +{% block content %} +

仪表盘

+

+ 新建作业要求

+
+
+

作业要求

+ {% if requirements %} + + + {% for r in requirements %} + + + + + + {% endfor %} +
标题提交数创建时间
{{ r.title }}{{ r.sub_count }}{{ r.created_at|date:"Y-m-d H:i" }}
+ {% else %}

暂无作业要求。

{% endif %} +
+
+

最近提交

+ {% if recent_submissions %} + + + {% for s in recent_submissions %} + + + + + + {% endfor %} +
名称状态时间
{{ s.name }}{{ s.get_status_display }}{{ s.created_at|date:"Y-m-d H:i" }}
+ {% else %}

暂无提交。

{% endif %} +
+
+{% endblock %} diff --git a/web/templates/web/requirement.html b/web/templates/web/requirement.html new file mode 100644 index 0000000..a762c06 --- /dev/null +++ b/web/templates/web/requirement.html @@ -0,0 +1,41 @@ +{% extends "web/base.html" %} +{% load extras %} +{% block title %}{{ requirement.title }} · OJplatform{% endblock %} +{% block content %} +

{{ requirement.title }}

+
+

作业要求

+

{{ requirement.content }}

+
+
+

AI 分析出的检测维度

+
{{ requirement.spec|json_filter }}
+
+
+

上传项目包

+
+ {% csrf_token %} + + + + +

+
+
+
+

提交记录 ({{ submissions|length }})

+ {% if submissions %} + + + {% for s in submissions %} + + + + + + + {% endfor %} +
名称提交人状态时间
{{ s.name }}{{ s.submitted_by.username|default:"-" }}{{ s.get_status_display }}{{ s.created_at|date:"Y-m-d H:i" }}
+ {% else %}

暂无提交。

{% endif %} +
+{% endblock %} diff --git a/web/templates/web/submission.html b/web/templates/web/submission.html new file mode 100644 index 0000000..fd1de7e --- /dev/null +++ b/web/templates/web/submission.html @@ -0,0 +1,90 @@ +{% extends "web/base.html" %} +{% load extras %} +{% block title %}{{ submission.name }} · OJplatform{% endblock %} +{% block content %} +

{{ submission.name }}

+

作业要求: {{ submission.requirement.title }} + · 提交人: {{ submission.submitted_by.username|default:"-" }} + · 状态: {{ submission.get_status_display }}

+ +{% if submission.report %} +
+

检测总览

+ {% with o=submission.report.overall %} + {% if o %} +

通过 {{ o.passed_checks }} / {{ o.decided_checks }} 项已判定检查

+ + + {% for c in o.checks %} + + + + + + {% endfor %} +
检查项结果说明
{{ c.name }} + {% if c.pass == True %}通过 + {% elif c.pass == False %}未通过 + {% else %}未判定{% endif %} + {{ c.note|truncatechars:120 }}
+ {% endif %} + {% endwith %} +
+ +
+

稳定度(压力测试)

+ {% with s=submission.report.stability %} + {% if s.enabled %} +

要求: {{ s.requirements }}

+ + + + + + + + +
基准错误率加压崩溃率总请求数判定
{{ s.base_error_rate|default_if_none:"—" }}{{ s.rampup_crash_rate|default_if_none:"—" }}{{ s.total_requests|default_if_none:"—" }} + {% if s.pass == True %}达标 + {% elif s.pass == False %}未达标 + {% else %}未判定: {{ s.reason|default:"" }}{% endif %} +
+ {% else %}

本次未启用稳定度检测。

{% endif %} + {% endwith %} +
+ +
+

AI 检测

+ {% with a=submission.report.ai_inspection %} + {% if a %} +

{{ a.summary }}

+ {% if a.issues %} + + + {% for i in a.issues %} + + + + + + {% endfor %} +
检查点状态说明
{{ i.point }} + {% if i.status == "pass" %}pass + {% elif i.status == "fail" %}fail + {% else %}warn{% endif %} + {{ i.detail }}
+ {% endif %} + {% else %}

无 AI 检测结果。

{% endif %} + {% endwith %} +
+ +
+

完整报告 (JSON)

+
{{ submission.report|json_filter }}
+
+{% else %} +
+

报告尚未生成(当前状态: {{ submission.get_status_display }})。刷新

+
+{% endif %} +{% endblock %} diff --git a/web/templatetags/__init__.py b/web/templatetags/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/web/templatetags/extras.py b/web/templatetags/extras.py new file mode 100644 index 0000000..b59a0ee --- /dev/null +++ b/web/templatetags/extras.py @@ -0,0 +1,10 @@ +import json + +from django import template + +register = template.Library() + + +@register.filter +def json_filter(value): + return json.dumps(value, ensure_ascii=False, indent=2) diff --git a/web/tests.py b/web/tests.py new file mode 100644 index 0000000..7ce503c --- /dev/null +++ b/web/tests.py @@ -0,0 +1,3 @@ +from django.test import TestCase + +# Create your tests here. diff --git a/web/urls.py b/web/urls.py new file mode 100644 index 0000000..98e8cbb --- /dev/null +++ b/web/urls.py @@ -0,0 +1,15 @@ +from django.contrib.auth import views as auth_views +from django.urls import path + +from . import views + +urlpatterns = [ + path('', views.dashboard, name='dashboard'), + path('login/', auth_views.LoginView.as_view(), name='login'), + path('logout/', auth_views.LogoutView.as_view(), name='logout'), + path('create/', views.create_requirement, name='create-requirement'), + path('requirement//', views.requirement_detail, name='requirement-detail'), + path('requirement//upload/', views.upload_submission, name='upload-submission'), + path('submission//', views.submission_detail, name='submission-detail'), + path('password_change/', auth_views.PasswordChangeView.as_view(), name='password_change'), +] diff --git a/web/views.py b/web/views.py new file mode 100644 index 0000000..362f24f --- /dev/null +++ b/web/views.py @@ -0,0 +1,100 @@ +import json + +from django.contrib import messages +from django.contrib.auth.decorators import login_required +from django.db import transaction +from django.db.models import Count +from django.shortcuts import get_object_or_404, redirect, render + +from requirements.llm import analyze_requirements, _default_spec +from requirements.models import Requirement, Submission +from requirements.tasks import run_submission + + +@login_required +def dashboard(request): + requirements = Requirement.objects.annotate(sub_count=Count("submissions")).order_by("-created_at") + recent_submissions = Submission.objects.select_related("requirement").order_by("-created_at")[:15] + return render(request, "web/dashboard.html", { + "requirements": requirements, + "recent_submissions": recent_submissions, + }) + + +@login_required +def requirement_detail(request, pk): + requirement = get_object_or_404(Requirement, pk=pk) + submissions = requirement.submissions.all() + return render(request, "web/requirement.html", { + "requirement": requirement, + "submissions": submissions, + }) + + +@login_required +def submission_detail(request, pk): + submission = get_object_or_404(Submission, pk=pk) + return render(request, "web/submission.html", { + "submission": submission, + "report_json": json.dumps(submission.report or {}, ensure_ascii=False, indent=2), + }) + + +@login_required +def create_requirement(request): + from django.conf import settings + if request.method != "POST": + return render(request, "web/create_requirement.html", { + "llm_base_url": settings.LLM_BASE_URL, + "llm_model": settings.LLM_MODEL, + }) + title = (request.POST.get("title") or "").strip() + content = (request.POST.get("content") or "").strip() + if not title or not content: + messages.error(request, "标题和内容不能为空") + return redirect("create-requirement") + llm_mode = request.POST.get("llm_mode", "default") + override = {} + if llm_mode == "custom": + override = { + "mode": "custom", + "base_url": (request.POST.get("llm_base_url") or "").strip(), + "api_key": (request.POST.get("llm_api_key") or "").strip(), + "model": (request.POST.get("llm_model") or "").strip(), + } + if not override["base_url"] or not override["model"]: + messages.error(request, "custom 模式必须填写 LLM 地址和模型名") + return redirect("create-requirement") + try: + spec = analyze_requirements(content, override) + except Exception: + spec = _default_spec() + with transaction.atomic(): + requirement = Requirement.objects.create( + title=title, content=content, spec=spec, llm_config=override, + ) + messages.success(request, f"已创建作业要求「{title}」,AI 分析完成") + return redirect("requirement-detail", pk=requirement.pk) + + +@login_required +def upload_submission(request, pk): + requirement = get_object_or_404(Requirement, pk=pk) + if request.method != "POST": + return redirect("requirement-detail", pk=pk) + file = request.FILES.get("file") + if not file: + messages.error(request, "请选择要上传的项目包(tar/tar.gz/zip)") + return redirect("requirement-detail", pk=pk) + name = (request.POST.get("name") or "").strip() or file.name + with transaction.atomic(): + submission = Submission.objects.create( + requirement=requirement, name=name, file=file, submitted_by=request.user, + ) + try: + run_submission(submission.pk) + except Exception: + submission.status = "failed" + submission.save(update_fields=["status"]) + messages.success(request, f"已提交「{name}」,检测完成") + return redirect("submission-detail", pk=submission.pk)