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 %}
+
+
+
+
+
OJplatform
+
+
仪表盘
+
管理后台
+ {% if user.is_authenticated %}
+
+ {% else %}
+
登录
+ {% endif %}
+
+
+ {% 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 %}
+新建作业要求
+
+
+{% 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 %}
+
+ | {{ r.title }} |
+ {{ r.sub_count }} |
+ {{ r.created_at|date:"Y-m-d H:i" }} |
+
+ {% endfor %}
+
+ {% else %}
暂无作业要求。
{% endif %}
+
+
+
最近提交
+ {% if recent_submissions %}
+
+ | 名称 | 状态 | 时间 |
+ {% for s in recent_submissions %}
+
+ | {{ s.name }} |
+ {{ s.get_status_display }} |
+ {{ s.created_at|date:"Y-m-d H:i" }} |
+
+ {% endfor %}
+
+ {% 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 }}
+
+
+
+
提交记录 ({{ submissions|length }})
+ {% if submissions %}
+
+ | 名称 | 提交人 | 状态 | 时间 |
+ {% for s in submissions %}
+
+ | {{ s.name }} |
+ {{ s.submitted_by.username|default:"-" }} |
+ {{ s.get_status_display }} |
+ {{ s.created_at|date:"Y-m-d H:i" }} |
+
+ {% endfor %}
+
+ {% 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 %}
+
+ | {{ c.name }} |
+
+ {% if c.pass == True %}通过
+ {% elif c.pass == False %}未通过
+ {% else %}未判定{% endif %}
+ |
+ {{ c.note|truncatechars:120 }} |
+
+ {% endfor %}
+
+ {% 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 %}
+
+ | {{ i.point }} |
+
+ {% if i.status == "pass" %}pass
+ {% elif i.status == "fail" %}fail
+ {% else %}warn{% endif %}
+ |
+ {{ i.detail }} |
+
+ {% endfor %}
+
+ {% 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)