后端
项目架构
1
2
3
4
5
6
7
8
9
backend/
manage.py
config/
apps/
accounts/
problems/
submissions/
judge/
utils/manage.py
Django项目的执行入口,经常用于各种命令行操作的执行,进入本项目
这个文件里的内容基本固定
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#!/usr/bin/env python
"""Django's command-line utility for administrative tasks."""
import os
import sys
def main():
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "config.settings")
try:
from django.core.management import execute_from_command_line
except ImportError as exc:
raise ImportError(
"Couldn't import Django. Is it installed and available on your "
"PYTHONPATH environment variable? Did you forget to activate a "
"virtual environment?"
) from exc
execute_from_command_line(sys.argv)
if __name__ == "__main__":
main()其中较为重要的是第八行,它设置了本项目的配置文件位于config/setting.py中
config
该文件夹内存储了整个项目的配置文件
setting.py
存储了DJango自带的功能、第三方库、数据库连接、用户模型等各种配置,以及我们自己写的各种业务模块
因此我们在后面每创建一个新app,都要放进该文件的对应位置
其中AUTH_USER_MODEL字段指定了使用的用户模型,格式为app_label.ModelName
urls.py
负责存储API总路由,并进行路由的分发,如目前
1
2
3
4
5
6
7
8
urlpatterns = [
path("admin/", admin.site.urls),
path("api/auth/login/", TokenObtainPairView.as_view(), name="token_obtain_pair"),
path("api/auth/token/refresh/", TokenRefreshView.as_view(), name="token_refresh"),
path("api/auth/", include("apps.accounts.urls")),
path("api/", include("apps.problems.urls")),
path("api/submissions/", include("apps.submissions.urls")),
]分别存储了Django-admin接口,登录接口,accounts这个app的接口,problems这个app的接口,submissions这个app的接口
这里不写具体业务逻辑
apps
写业务代码的目录,对于一个OJ系统,我们有许多个不同业务,需要将他们拆开为独立的模块分别处理
如这里我们拆成了如下系统
1
2
3
4
5
backend/apps/
accounts/
problems/
submissions/
judge/对于每个模块,在Django+DRF中最常见的分层如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
models.py
定义数据库长什么样
serializers.py
定义 API 输入输出长什么样
views.py
定义请求来了以后做什么
urls.py
定义URL对应哪个view
admin.py
定义后台管理页面怎么展示accounts/
负责用户表、注册接口、用户信息接口、Django-admin用户管理
架构:
1
2
3
4
5
6
7
8
9
backend/apps/accounts/
__init__.py
models.py
serializers.py
views.py
urls.py
admin.py
apps.py
migrations/负责的链路
accounts负责了用户的创建和获取,具体链路如下:
注册
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
POST /api/auth/register/
|
v
config/urls.py
|
v
accounts/urls.py
|
v
RegisterView
|
v
RegisterSerializer
|
v
User.objects.create_user()
|
v
accounts_user 表获取当前用户
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
GET /api/auth/me/
Authorization: Bearer xxx
|
v
JWT 认证
|
v
MeView
|
v
request.user
|
v
UserSerializer
|
v
返回用户 JSON__init__.py
告诉Python这个accounts是一个python包
apps.py
1
2
3
4
5
6
from django.apps import AppConfig
class AccountsConfig(AppConfig):
default_auto_field = "django.db.models.BigAutoField"
name = "apps.accounts"
label = "accounts"指出该app的python路径是app.accounts,和settings.py一致
在Django内叫accounts,默认主键为BigAutoField
models.py
用户表
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
from django.contrib.auth.models import AbstractUser
from django.db import models
class User(AbstractUser):
class Role(models.TextChoices):
USER = "user", "普通用户"
ADMIN = "admin", "管理员"
role = models.CharField(max_length=20, choices=Role.choices, default=Role.USER)
solved_count = models.PositiveIntegerField(default=0)
submit_count = models.PositiveIntegerField(default=0)
@property
def is_admin(self):
return self.role == self.Role.ADMIN or self.is_staff可以直接继承Django内的用户系统AbstractUser,内部已经包含如下字段:
1
2
3
4
5
6
7
8
9
10
username
password
email
first_name
last_name
is_staff
is_active
is_superuser
last_login
date_joined继承后可以继续增加自己的字段
下面我们创建了一个Role类,用于定义枚举用户类型,这是Django推荐的枚举写法,用来限制role字段只能在设定的固定值来选
因此定义字段role = models.CharField(max_length=20, choices=Role.choices, default=Role.USER)
后面property定义了一个管理员属性,即判断role是管理员或Django内自带字段是管理员,就默认这个用户是管理员,权限判断时直接调用request.user.is_admin就可以判断是否是管理员
admin.py
负责后台信息展示
1
2
3
4
5
6
7
8
9
10
11
12
from django.contrib import admin
from django.contrib.auth.admin import UserAdmin
from .models import User
@admin.register(User)
class CustomUserAdmin(UserAdmin):
fieldsets = UserAdmin.fieldsets + (
("OJ 信息", {"fields": ("role", "solved_count", "submit_count")}),
)
list_display = ("id", "username", "email", "role", "is_staff", "is_active")
list_filter = ("role", "is_staff", "is_active")首先通过第六行将Uder模型注册到admin后台
在自定义类中继承自带的UserAdmin类,并定义自己的展示区域,如oj信息,可以编辑role、solved_count、submit_count
serializers.py
用于JSON与Django model之间的转换
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
from django.contrib.auth import get_user_model
from rest_framework import serializers
User = get_user_model()
class UserSerializer(serializers.ModelSerializer):
class Meta:
model = User
fields = ("id", "username", "email", "role", "solved_count", "submit_count")
class RegisterSerializer(serializers.ModelSerializer):
password = serializers.CharField(write_only=True, min_length=6)
class Meta:
model = User
fields = ("id", "username", "email", "password")
def create(self, validated_data):
return User.objects.create_user(**validated_data)有两个serializers,第一个UserSerializer用于把用户对象返回给前端,则接口/api/auth/me/返回
1
2
3
4
5
6
7
8
{
"id": 1,
"username": "alice",
"email": "alice@example.com",
"role": "user",
"solved_count": 0,
"submit_count": 0
}第二个RegisterSerializer用于注册,注意重定义了password为writeonly,意思为前端提交password,但是后端响应的时候不会把password返回给前端
创建用户时使用create_user可以自动将明文密码哈希加密
views.py
负责接口逻辑
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
from rest_framework import generics
from rest_framework.permissions import AllowAny, IsAuthenticated
from .serializers import RegisterSerializer, UserSerializer
class RegisterView(generics.CreateAPIView):
serializer_class = RegisterSerializer
permission_classes = [AllowAny]
class MeView(generics.RetrieveAPIView):
serializer_class = UserSerializer
permission_classes = [IsAuthenticated]
def get_object(self):
return self.request.user第一个是负责创建对象的API,对应POST /api/auth/register
serializer_class指定注册时用哪个处理数据
permission_classes指定权限是所有人
第二个是负责获取一个对象的API,对应GET /api/auth/me,指定权限是已登录用户,获取当前登录用户
urls.py
accounts自己的路由
1
2
3
4
5
6
7
8
from django.urls import path
from .views import MeView, RegisterView
urlpatterns = [
path("register/", RegisterView.as_view(), name="register"),
path("me/", MeView.as_view(), name="me"),
]里面定义了自己的路由路径,即最后会存在
1
2
/api/auth/register/
/api/auth/me/这两个路径
登录在哪里?
在config的urls内,我们写了
1
2
path("api/auth/login/", TokenObtainPairView.as_view(), name="token_obtain_pair"),
path("api/auth/token/refresh/", TokenRefreshView.as_view(), name="token_refresh"),这两个views来自于simplejwt的视图
登录时前端传username和password,后端返回refresh和access
problems/
该模块负责题目的管理,包括标签、测试点、题目列表、题目详情、题目管理权限
架构:
1
2
3
4
5
6
7
backend/apps/problems/
models.py
serializers.py
views.py
urls.py
admin.py
permissions.py负责的链路
通过后台添加题目:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
Django Admin 添加题目
|
v
MySQL 保存 problems_problem/problems_testcase
|
v
GET /api/problems/
|
v
ProblemViewSet查询数据库
|
v
ProblemListSerializer转JSON
|
v
浏览器看到题目列表查看题目详情:
1
2
3
4
5
6
7
GET /api/problems/id
|
v
ProblemDetailSerializer
|
v
题目详情+tags+sample_casesmodels.py
向problems相关表内添加数据
tag表:
1
2
3
4
5
6
7
8
class Tag(models.Model):
name = models.CharField(max_length=40, unique=True)
class Meta:
ordering = ["name"]
def __str__(self):
return self.nametag表内只有一个字段需要填充,标签名,注意要将unique设置为True,不能重复
其中meta用于设置默认按name列排序,__str__用于后台调试直接返回标签名,而不是数字
题目表:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
class Problem(models.Model):
class Difficulty(models.TextChoices):
EASY = "easy", "简单"
MEDIUM = "medium", "中等"
HARD = "hard", "困难"
title = models.CharField(max_length=120)
description = models.TextField()
input_description = models.TextField(blank=True)
output_description = models.TextField(blank=True)
difficulty = models.CharField(
max_length=20,
choices=Difficulty.choices,
default=Difficulty.EASY,
)
time_limit = models.PositiveIntegerField(default=2000, help_text="单位:ms")
memory_limit = models.PositiveIntegerField(default=128, help_text="单位:MB")
is_public = models.BooleanField(default=True)
tags = models.ManyToManyField(Tag, blank=True, related_name="problems")
created_by = models.ForeignKey(
settings.AUTH_USER_MODEL,
null=True,
blank=True,
on_delete=models.SET_NULL,
related_name="created_problems",
)
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
class Meta:
ordering = ["id"]
def __str__(self):
return f"{self.id}. {self.title}"题目表内的列会多一点,
前四个为题目与对应描述,描述使用了TextField,适合容纳长文本,且blank设为True,允许为空
第五个为难度,前面有一个difficulty子类,用于设定可供选择的难度枚举,在这里可以直接设为按照该枚举选择
时空限制设为PositiveIntegerField,因为只允许正数
是否公开设为BooleanField,表示是否公开
tags为多对多关系,所以使用ManyToManyField,其中related_name允许进行反查
created_by是一个外键,指向题目作者,使用ForeignKey,
settings.AUTH_USER_MODEL指向我们要用的用户模型,on_delete设置为setnull,使得用户注销后题目还存在,只是将用户设置为null
后面两个时间用DateTimeField,并自动填入
meta设置为按照id排序,str用于调试时按格式返回结果
测试点表:
1
2
3
4
5
6
7
8
9
10
11
12
13
class TestCase(models.Model):
problem = models.ForeignKey(Problem, on_delete=models.CASCADE, related_name="test_cases")
input_data = models.TextField(blank=True)
output_data = models.TextField(blank=True)
is_sample = models.BooleanField(default=False)
score = models.PositiveIntegerField(default=0)
order = models.PositiveIntegerField(default=0)
class Meta:
ordering = ["order", "id"]
def __str__(self):
return f"{self.problem_id} - case {self.id}"用来表示一个测试点
其中problem是外键,表示某个测试点属于哪个题,满足一对多关系,on_delete=models.CASCADE意思是删除后下面的测试点也一起删除,related_name设为test_cases用于从题目反查测试点
is_sample用于表示测试点是否设为样例,详情接口里只会返回该字段为True的
order是测试点的顺序
因此meta里排序首先按照order,再按照id排序
admin.py
用于控制后台添加题目及相关信息
首先说明,装饰器@admin.register(...)用于将括号内的模型注册到DjangoAdmin,并用下面的配置类控制它
problemAdmin:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class TestCaseInline(admin.TabularInline):
model = TestCase
extra = 1
fields = ("order", "input_data", "output_data", "is_sample", "score")
@admin.register(Problem)
class ProblemAdmin(admin.ModelAdmin):
list_display = ("id", "title", "difficulty", "is_public", "created_at")
list_filter = ("difficulty", "is_public", "tags")
search_fields = ("title", "description")
filter_horizontal = ("tags",)
inlines = [TestCaseInline]
def save_model(self, request, obj, form, change):
if not obj.created_by:
obj.created_by = request.user
super().save_model(request, obj, form, change)list_display用于控制题目列表页能显示哪些列
list_filter用于控制用户可以按照哪些字段筛选题目
search_fields控制后台搜索框能搜哪些字段,即你在搜索框里输入的内容,只会在你指定的字段内搜索
filter_horizontal将选择的字段以左右两个备选框的形式呈现,可以用来展示可选与已选
inlines设定了一个列表,里面存储了TestCaseInline类,由于testcase有外键指向problem,所以默认了一个测试点必须属于一道题,因此可以inline在同一个页面直接编辑测试点,
admin.TabularInline指用表格形式展示内联元素- extra意为默认额外显示几个空白表单
- fields意为inline表单内显示哪些字段,以及顺序是什么
save_model函数用于自动设置出题人
TagAdmin:
1
2
3
4
@admin.register(Tag)
class TagAdmin(admin.ModelAdmin):
list_display = ("id", "name")
search_fields = ("name",)标签后台,显示id与name,搜索字段能搜到name
TestCaseAdmin:
1
2
3
4
@admin.register(TestCase)
class TestCaseAdmin(admin.ModelAdmin):
list_display = ("id", "problem", "is_sample", "score", "order")
list_filter = ("is_sample",)单独显示测试样例,可以在后台看所有测试点
右侧可以筛选是否样例
serializers.py
负责五块内容的JSON和Django模型转换
TagSerializer:
1
2
3
4
class TagSerializer(serializers.ModelSerializer):
class Meta:
model = Tag
fields = ("id", "name")将tag模型转换为JSON,其中ModelSerializer是DRF的便捷写法,可以根据Django模型自动生成字段
根据下面的meta类来确定基于的模型与需要的字段
SampleCaseSerializer:
1
2
3
4
class SampleCaseSerializer(serializers.ModelSerializer):
class Meta:
model = TestCase
fields = ("id", "input_data", "output_data", "order")与上面相同,但注意,这里字段只展示了用户需要看到的内容
ProblemListSerializer:
1
2
3
4
5
6
7
8
9
10
11
12
13
class ProblemListSerializer(serializers.ModelSerializer):
tags = TagSerializer(many=True, read_only=True)
class Meta:
model = Problem
fields = (
"id",
"title",
"difficulty",
"time_limit",
"memory_limit",
"tags",
)用于题目列表页,只需要返回需要的字段
tags那行用于获取题目的tag,由于符合一对多的特点,所以要将many设为True,另外要保证该字段只用于输出,而不会写入
ProblemDetailSerialize:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
class ProblemDetailSerializer(serializers.ModelSerializer):
tags = TagSerializer(many=True, read_only=True)
tag_ids = serializers.PrimaryKeyRelatedField(
source="tags",
queryset=Tag.objects.all(),
many=True,
write_only=True,
required=False,
)
sample_cases = serializers.SerializerMethodField()
class Meta:
model = Problem
fields = (
"id",
"title",
"description",
"input_description",
"output_description",
"difficulty",
"time_limit",
"memory_limit",
"is_public",
"tags",
"tag_ids",
"sample_cases",
"created_at",
"updated_at",
)
read_only_fields = ("created_at", "updated_at")
def get_sample_cases(self, obj):
samples = obj.test_cases.filter(is_sample=True)
return SampleCaseSerializer(samples, many=True).data用于查看题目详情,实现创建于修改题目
tags用于展示标签,tag_ids负责处理样例id与题目之间的关联,前端只需要告诉后端该题目关联哪些标签即可
queryset=Tag.objects.all()用于保证传入的tagid要在tag表内找到,many指一道题可以有多个标签,write_only指ids只用于前端提交,不在响应接口内返回
sample_cases是通过一个方法计算出来的字段:
使用filter筛选样例测试点,在用serializers转换为JSON
read_only_fields指定了前端不能修改的字段
TestCaseSerializer:
1
2
3
4
class TestCaseSerializer(serializers.ModelSerializer):
class Meta:
model = TestCase
fields = ("id", "problem", "input_data", "output_data", "is_sample", "score", "order")用于管理测试点,管理员可以通过API创建测试点
views.py
负责判断前端发送请求时,后端如何操作(如何查询数据库,用哪个serializers)
这里一共有三个ViewSet,用于题目、标签和测试点
这里使用了DRF的ViewSet,可以将一组相关接口放到一个类里,默认提供GET、POST等常见方法,这样就不用手写每个接口的函数了
ProblemViewSet:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class ProblemViewSet(viewsets.ModelViewSet):
permission_classes = [IsAdminOrReadOnly]
filterset_fields = ("difficulty", "tags")
search_fields = ("title", "description")
ordering_fields = ("id", "created_at", "difficulty")
def get_queryset(self):
queryset = Problem.objects.prefetch_related("tags", "test_cases")
user = self.request.user
if not (user.is_authenticated and user.is_admin):
queryset = queryset.filter(is_public=True)
return queryset
def get_serializer_class(self):
if self.action == "list":
return ProblemListSerializer
return ProblemDetailSerializer
def perform_create(self, serializer):
serializer.save(created_by=self.request.user)permission_classes 自定义权限,IsAdminOrReadOnly指普通用户只可读,管理员可以进行其他操作
filterset_fields筛选字段,可以通过类似GET /api/problems/?difficulty=easy请求来筛选
search_fields是DRF的搜索功能,可以通过类似GET /api/problems/?search=A+B请求来在给定的字段查询
ordering_fields是排序功能,可以通过类似GET /api/problems/?ordering=id请求来按给定字段排序
get_queryset用于决定这个接口能查到哪些题目
- 其中
prefetch_related用于预加载相关资源,防止频繁查询 - 后面根据用户来过滤公开题,若当前用户不是管理,queryset就需要筛选只公开的题目
get_serializer_class决定当前接口用哪个serializers,如果是list接口则调用problemlist,否则调用problemdetail
perform_create在创建对象时被调用,当管理员请求POST /api/problems/,DRF会用serializer校验前端传来的数据,然后调用perform_create,然后将数据保存到数据库,且额外传入创建者是当前用户
TagViewSet:
1
2
3
4
5
class TagViewSet(viewsets.ModelViewSet):
queryset = Tag.objects.all()
serializer_class = TagSerializer
permission_classes = [IsAdminOrReadOnly]
search_fields = ("name",)标签接口,与上面类似
TestCaseViewSet:
1
2
3
4
5
6
class TestCaseViewSet(viewsets.ModelViewSet):
serializer_class = TestCaseSerializer
permission_classes = [IsAdminUser]
def get_queryset(self):
return TestCase.objects.select_related("problem")测试点管理接口,测试点普通用户不给看,所以设置权限仅管理员
最后返回时用到了select_related,用于外键关系,可以一块将problem查出来
permission.py
该文件负责定义题目和标签接口的访问权限
1
2
3
4
5
6
7
from rest_framework.permissions import SAFE_METHODS, BasePermission
class IsAdminOrReadOnly(BasePermission):
def has_permission(self, request, view):
if request.method in SAFE_METHODS:
return True
return bool(request.user and request.user.is_authenticated and request.user.is_admin)DRF内,每个权限类都会继承BasePermission,并实现has_haspermission,它返回布尔值表示允许或拒绝访问,当返回拒绝时,会报403错误码
SAFE_METHODS是预定义好的一个元组,这里面的方法通常不会修改数据,都是读操作,因此如果请求方法属于这些方法,就可以直接放行
否则就只能是已登录管理员才能用
这里的request.user.is_admin来自model.py
urls.py
用于结合视图变为真正的url
1
2
3
4
5
6
7
8
9
10
from rest_framework.routers import DefaultRouter
from .views import ProblemViewSet, TagViewSet, TestCaseViewSet
router = DefaultRouter()
router.register("problems", ProblemViewSet, basename="problem")
router.register("tags", TagViewSet, basename="tag")
router.register("test-cases", TestCaseViewSet, basename="test-case")
urlpatterns = router.urls可以发现,这里没有使用path显示指定,而是使用router,可以根据viewsset来自动生成url
DefaultRouter()是DRF的路由生成器,可以根据你注册的viewset自动生成路由
如下面使用router.register,指定视图可以直接生成路由,语法如下:
router.register(URL前缀, viewset, 路由名的基础名)
并且这些生成的路由会接上config里的urls.py,根据内部的path("api/", include("apps.problems.urls"))可知最后前面还会加上前缀/api/
最后将router自动生成的url交给Django:urlpatterns = router.urls
注意,前面account里不用router,是因为它们不是简单的CRUD,而且不是viewset,所以可以直接设置path
submissions/
负责用户提交代码、查看判题记录、保存判题结果等
架构:
1
2
3
4
5
6
backend/apps/submissions/
models.py
serializers.py
views.py
urls.py
admin.py分为提交、测试结果两个模型
负责的链路
提交请求入库:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
前端 POST /api/submissions/
Authorization: Bearer xxx
{
"problem": 1,
"language": "python3",
"code": "print(...)"
}
|
v
SubmissionCreateSerializer
|
v
校验 problem 是否存在且公开
校验 language 是否在 choices 里
校验 code 是否存在
|
v
create()
|
v
user = request.user
submit_count + 1
|
v
Submission.objects.create(...)
|
v
数据库新增一条 submissions_submission
status 默认 PENDING提交详情返回:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
GET /api/submissions/1/
Authorization: Bearer xxx
|
v
SubmissionReadSerializer
|
v
读取 Submission
读取 user.username
读取 problem.title
读取 results
|
v
返回 JSONmodel.py
submission:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
class Submission(models.Model):
class Language(models.TextChoices):
PYTHON3 = "python3", "Python 3"
class Status(models.TextChoices):
PENDING = "PENDING", "等待判题"
JUDGING = "JUDGING", "正在判题"
ACCEPTED = "ACCEPTED", "答案正确"
WRONG_ANSWER = "WRONG_ANSWER", "答案错误"
TIME_LIMIT_EXCEEDED = "TIME_LIMIT_EXCEEDED", "运行超时"
RUNTIME_ERROR = "RUNTIME_ERROR", "运行错误"
SYSTEM_ERROR = "SYSTEM_ERROR", "系统错误"
user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE, related_name="submissions")
problem = models.ForeignKey(Problem, on_delete=models.CASCADE, related_name="submissions")
language = models.CharField(max_length=20, choices=Language.choices, default=Language.PYTHON3)
code = models.TextField()
status = models.CharField(max_length=30, choices=Status.choices, default=Status.PENDING)
score = models.PositiveIntegerField(default=0)
time_used = models.PositiveIntegerField(default=0, help_text="单位:ms")
memory_used = models.PositiveIntegerField(default=0, help_text="单位:KB,初版可为空")
error_message = models.TextField(blank=True)
created_at = models.DateTimeField(auto_now_add=True)
judged_at = models.DateTimeField(null=True, blank=True)
class Meta:
ordering = ["-id"]
def __str__(self):
return f"#{self.id} {self.user} {self.problem} {self.status}"language类用于枚举可使用的语言
status类用于枚举判题状态
user是外键,表示提交属于哪个用户,用户一被删除,他的提交记录也被删除
problem是外键,表示这次提交对应哪道题
language表示提交的语言,字段设为上面language类的枚举
status表示提交状态,以上面的status类枚举,这里先默认为pending,后面其他的部分对该字段进行修改
meta设为按照id降序排列,最新提交在前
JudgeResult:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class JudgeResult(models.Model):
submission = models.ForeignKey(Submission, on_delete=models.CASCADE, related_name="results")
testcase = models.ForeignKey(TestCase, on_delete=models.CASCADE, related_name="judge_results")
status = models.CharField(max_length=30, choices=Submission.Status.choices)
time_used = models.PositiveIntegerField(default=0)
memory_used = models.PositiveIntegerField(default=0, help_text="单位:KB")
output = models.TextField(blank=True)
error_message = models.TextField(blank=True)
created_at = models.DateTimeField(auto_now_add=True)
class Meta:
ordering = ["id"]
def __str__(self):
return f"submission {self.submission_id} case {self.testcase_id}: {self.status}"显示某个测试点的判题结果
submission是外键,记录该测试点结果属于哪次提交,若提交被删除,则下面的结果也会被删除
testcase是外键,表明当前结果对应哪个测试点,若测试点被删除,则对应他的提交jie’guo
status使用枚举,每个测试点也可以有状态
serializers.py
将提交记录在JSON和Django model之间转换
JudgeResultSerializer:
1
2
3
4
class JudgeResultSerializer(serializers.ModelSerializer):
class Meta:
model = JudgeResult
fields = ("id", "testcase", "status", "time_used", "memory_used", "output", "error_message")将单条测试点结果转为JSON
SubmissionCreateSerializer:
1
2
3
4
5
6
7
8
9
10
11
12
class SubmissionCreateSerializer(serializers.ModelSerializer):
problem = serializers.PrimaryKeyRelatedField(queryset=Problem.objects.filter(is_public=True))
class Meta:
model = Submission
fields = ("id", "problem", "language", "code")
def create(self, validated_data):
user = self.context["request"].user
user.submit_count += 1
user.save(update_fields=["submit_count"])
return Submission.objects.create(user=user, **validated_data)提交代码时的serializers,前端请求POST /api/submissions/
其中,problem处接受前端提交的problem的ID,后端根据ID找到公开的,对应problem的对象,前端只需要提交problem、language和code即可
create是创建提交时的逻辑,DRF在视图内调用serializers时,会把request放到context里,我们可以通过这个拿到当前用户,没提交一次,用户提交数+1,且更新字段submit_count,返回时创建submission
SubmissionReadSerializer:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
class SubmissionReadSerializer(serializers.ModelSerializer):
username = serializers.CharField(source="user.username", read_only=True)
problem_title = serializers.CharField(source="problem.title", read_only=True)
results = JudgeResultSerializer(many=True, read_only=True)
class Meta:
model = Submission
fields = (
"id",
"username",
"problem",
"problem_title",
"language",
"code",
"status",
"score",
"time_used",
"memory_used",
"error_message",
"results",
"created_at",
"judged_at",
)用于查看提交列表和详情,对应的方法是
1
2
GET /api/submissions/
GET /api/submissions/{id}/user从user模块那里取值,返回的是用户名
problem_title从problem模块那里取值
results从model中的judgeresult拿,且一对多所以要把many设为True
views.py
负责提交接口的访问权限,记录查询范围和serializers的使用
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class SubmissionViewSet(
mixins.CreateModelMixin,
mixins.ListModelMixin,
mixins.RetrieveModelMixin,
viewsets.GenericViewSet,
):
permission_classes = [IsAuthenticated]
filterset_fields = ("problem", "status", "language")
ordering_fields = ("id", "created_at")
def get_queryset(self):
queryset = Submission.objects.select_related("user", "problem").prefetch_related("results")
user = self.request.user
if user.is_admin:
return queryset
return queryset.filter(user=user)
def get_serializer_class(self):
if self.action == "create":
return SubmissionCreateSerializer
return SubmissionReadSerializer这里我们没有用ModelViewSet,因为我们不需要全套CRUD,为了避免出现问题,故自己写
使用mixins添加动作:
Mixin 提供的动作 URL CreateModelMixin create POST /api/submissions/ ListModelMixin list GET /api/submissions/ RetrieveModelMixin retrieve GET /api/submissions/{id}/
permission_classes指明了提交相关接口必须登录使用
filterset_fields允许前端按照条件筛选记录
get_queryset决定了用户能看到哪些提交,前两个是外键,使用select_related,最后一个反向一对多,用prefetch_related,获取当前用户,若是管理员则可以看到所有人的提交,否则普通用户只能看到自己的
若普通用户访问了别人的提交,DRF返回404(并非403,可以避免泄露这个ID是否存在)
get_serializer_class选择使用哪个serializers
urls.py
路由
1
2
3
4
router = DefaultRouter()
router.register("", SubmissionViewSet, basename="submission")
urlpatterns = router.urls这里router里是空字符串,因为总路由里已经包含了api/submissions/
Judge/
找出等待判题的提交、运行代码、对比标准输出、更新提交状态
架构:
1
2
3
backend/apps/judge/
services.py
management/commands/run_judge.py目前只实现python3的支持,因为不需要编译等操作,不需要另外添加docker容器
services.py
判题的核心,负责拿出pending代码,运行代码,对比答案,保存结果,更新判题状态
RunResult:
1
2
3
4
5
6
@dataclass
class RunResult:
status: str
output: str = ""
error_message: str = ""
time_used: int = 0数据容器,保存运行一次代码的结果,包括状态、标准输出、错误信息、运行时间
normalize_output:
1
2
def normalize_output(value):
return value.replace("\r\n", "\n").strip()去掉行尾回车空格,注意Windows和Linux下的换行不同
run_python_code:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
def run_python_code(code, input_data, timeout_seconds):
with tempfile.TemporaryDirectory() as temp_dir:
code_path = Path(temp_dir) / "main.py"
code_path.write_text(code, encoding="utf-8")
started_at = time.perf_counter()
try:
completed = subprocess.run(
[settings.JUDGE_PYTHON_COMMAND, str(code_path)],
input=input_data,
text=True,
capture_output=True,
timeout=timeout_seconds,
cwd=temp_dir,
)
except subprocess.TimeoutExpired:
time_used = int((time.perf_counter() - started_at) * 1000)
return RunResult(status=Submission.Status.TIME_LIMIT_EXCEEDED, time_used=time_used)
time_used = int((time.perf_counter() - started_at) * 1000)
if completed.returncode != 0:
return RunResult(
status=Submission.Status.RUNTIME_ERROR,
output=completed.stdout,
error_message=completed.stderr[-2000:],
time_used=time_used,
)
return RunResult(
status=Submission.Status.ACCEPTED,
output=completed.stdout,
time_used=time_used,
)运行用户代码
首先我们创建一个临时文件,将用户代码写入,在单独的进程里进行,防止在Django进程执行会影响主服务
subprocess.run用于创建子进程并运行代码:
[settings.JUDGE_PYTHON_COMMAND, str(code_path)]执行类似pyhton [临时文件].pyinput讲测试点输入传给用户的标准输入
capture_output=True捕获标准输出和标准错误
timeout=timeout_seconds限定运行时间,超时可以抛TLE
cwd=temp_dir让代码在临时目录进行
1
2
3started_at = time.perf_counter() ... time_used = int((time.perf_counter() - started_at) * 1000)
用于记录代码执行时间,默认是秒,乘1000转为毫秒
- 用一个try…except捕获超时异常,若超时返回使用时间并返回TLE
completed.returncode捕获返回码,若非零表示异常退出,即RE,错误信息从stderr获取,只保留后面2000个字符,防止爆库- 最后没有运行问题,返回ACCEPT,这里的AC指运行没有问题,而不代表答案正确
acquire_pending_submission:
1
2
3
4
5
6
7
8
9
10
11
12
13
def acquire_pending_submission():
with transaction.atomic():
submission = (
Submission.objects.select_for_update(skip_locked=True)
.filter(status=Submission.Status.PENDING)
.order_by("id")
.first()
)
if submission is None:
return None
submission.status = Submission.Status.JUDGING
submission.save(update_fields=["status"])
return submission该函数负责抓取状态为PENDING的提交,改为JUDGING
transaction.atomic()使用了Django的事务管理,保证两个动作是一组操作,防止混乱
select_for_update(skip_locked=True)将选中行上锁,若某行被其他事务锁住,就跳过,防止将来多个判题机同时跑,不会抢同一个提交
按照id排列,遵循FIFO的原则
judge_submission:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
def judge_submission(submission):
test_cases = list(submission.problem.test_cases.all())
if not test_cases:
submission.status = Submission.Status.SYSTEM_ERROR
submission.error_message = "该题目没有测试点。"
submission.judged_at = timezone.now()
submission.save(update_fields=["status", "error_message", "judged_at"])
return submission
timeout_seconds = max(1, submission.problem.time_limit / 1000)
total_score = 0
max_time = 0
final_status = Submission.Status.ACCEPTED
final_error = ""
JudgeResult.objects.filter(submission=submission).delete()
for test_case in test_cases:
run_result = run_python_code(submission.code, test_case.input_data, timeout_seconds)
status = run_result.status
if status == Submission.Status.ACCEPTED:
if normalize_output(run_result.output) == normalize_output(test_case.output_data):
total_score += test_case.score
else:
status = Submission.Status.WRONG_ANSWER
JudgeResult.objects.create(
submission=submission,
testcase=test_case,
status=status,
time_used=run_result.time_used,
memory_used=0,
output=run_result.output[-2000:],
error_message=run_result.error_message,
)
max_time = max(max_time, run_result.time_used)
if status != Submission.Status.ACCEPTED:
final_status = status
final_error = run_result.error_message
break
submission.status = final_status
submission.score = 100 if final_status == Submission.Status.ACCEPTED else total_score
submission.time_used = max_time
submission.error_message = final_error
submission.judged_at = timezone.now()
submission.save(
update_fields=["status", "score", "time_used", "error_message", "judged_at"]
)
return submission核心判题函数
- 首先获取测试点列表,若没有测试点,直接返回SYSTEM_ERROR
- timeout_seconds是时间限制,数据库内单位是ms,需要转化为s
- 判题前先删除旧的测试点结果
- 接下来遍历所有测试点,调用上面的函数使用同一份代码,得到不同输出
- 若状态为运行AC,则判断比较输出,若一致则当前测试点ACCEPT,不一致返回WA
- 创建judgeresult,保存每个测试点的结果
- 最后更新总提交状态,所有结果写回submission,若全部通过为AC,中途失败为其他,
process_next_submission:
取任务用的封装函数,供后面的命令调用
1
2
3
4
5
def process_next_submission():
submission = acquire_pending_submission()
if submission is None:
return None
return judge_submission(submission)run_judge.py
这个文件的作用是判断有无PENDING提交,若有就调用判题机,否则等待,即不断检测提交
放在了backend/apps/judge/management/commands/run_judge.py这个路径,因为是Django自定义命令的固定结构,可以让manage.py调用到该代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class Command(BaseCommand):
help = "Run the simple KouOJ judge worker."
def add_arguments(self, parser):
parser.add_argument("--once", action="store_true", help="Only process one pending submission.")
parser.add_argument("--sleep", type=float, default=1.0, help="Sleep seconds when queue is empty.")
def handle(self, *args, **options):
self.stdout.write(self.style.SUCCESS("Judge worker started."))
while True:
submission = process_next_submission()
if submission:
self.stdout.write(f"Judged submission #{submission.id}: {submission.status}")
elif options["once"]:
self.stdout.write("No pending submission.")
return
else:
time.sleep(options["sleep"])
if options["once"]:
returnBaseCommand是Django自己的命令类,自定义命令都需要继承它,定义自己的类,类名必须是Command
- help是命令说明
- add_argument方法用于添加参数:
--once只处理一个提交--sleep会不断查询判题任务,默认每隔一秒查询一次
- handle执行命令,当我们通过manage运行命令时,Django会调用handle这个函数
self.stdout.write是Django推荐的输出方式- 死循环让判题机一直运行
- 循环内调用
process_next_submission(),位于services内的判题逻辑- 如果判到了提交,则在终端输出状态信息
- 若没有任务且是sleep模式,继续运行,查询间隔为1秒
注意,API服务与判题机要分为两个终端,是两个独立进程
日志功能
流程
1
2
3
4
5
6
7
8
9
OperationLog 数据表
↓
中间件自动记录 API 请求
↓
关键业务主动记录详细操作
↓
Django Admin 查询日志
↓
配置文件日志记录系统异常设计
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
from django.conf import settings
from django.db import models
class OperationLog(models.Model):
user = models.ForeignKey(
settings.AUTH_USER_MODEL,
on_delete=models.SET_NULL,
null=True,
blank=True,
related_name="operation_logs",
)
action = models.CharField(max_length=50)
method = models.CharField(max_length=10)
path = models.CharField(max_length=255)
status_code = models.PositiveSmallIntegerField()
ip_address = models.GenericIPAddressField(null=True, blank=True)
user_agent = models.CharField(max_length=500, blank=True)
object_type = models.CharField(max_length=50, blank=True)
object_id = models.CharField(max_length=50, blank=True)
detail = models.JSONField(default=dict, blank=True)
duration_ms = models.PositiveIntegerField(default=0)
created_at = models.DateTimeField(auto_now_add=True)
class Meta:
ordering = ["-created_at"]
indexes = [
models.Index(fields=["user", "created_at"]),
models.Index(fields=["action", "created_at"]),
models.Index(fields=["status_code"]),
]
def __str__(self):
username = self.user.username if self.user else "anonymous"
return f"{username} {self.action} {self.path}"字段含义:
user:操作用户,允许匿名。action:例如CREATE_SUBMISSION、UPDATE_PROFILE。method/path:请求方法与接口地址。status_code:200、400、500 等。object_type/object_id:操作的题目、提交或题解。detail:安全的补充信息。duration_ms:接口处理时间,便于排查慢请求。indexes:提高后台日志查询速度。
注册后台
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
from django.contrib import admin
from .models import OperationLog
@admin.register(OperationLog)
class OperationLogAdmin(admin.ModelAdmin):
list_display = (
"id",
"user",
"action",
"method",
"path",
"status_code",
"duration_ms",
"created_at",
)
list_filter = ("action", "method", "status_code", "created_at")
search_fields = ("user__username", "path", "object_id")
readonly_fields = (
"user",
"action",
"method",
"path",
"status_code",
"ip_address",
"user_agent",
"object_type",
"object_id",
"detail",
"duration_ms",
"created_at",
)然后进行迁移
审计中间件
可以自动记录
1
POST/PATCH/PUT/DELETE返回 4xx/5xx 的请求
用户、IP、路径、状态码、耗时
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
import time
from rest_framework_simplejwt.authentication import JWTAuthentication
from .models import OperationLog
class OperationLogMiddleware:
def __init__(self, get_response):
self.get_response = get_response
def __call__(self, request):
started_at = time.perf_counter()
response = self.get_response(request)
duration_ms = int(
(time.perf_counter() - started_at) * 1000
)
if self.should_record(request, response):
self.create_log(request, response, duration_ms)
return response
# 记录需要的
def should_record(self, request, response):
if not request.path.startswith("/api/"):
return False
if request.path.startswith("/api/audit/"):
return False
write_methods = {"POST", "PUT", "PATCH", "DELETE"}
return (
request.method in write_methods
or response.status_code >= 400
)
# 获得JWT用户
def get_user(self, request):
if request.user.is_authenticated:
return request.user
try:
authentication = JWTAuthentication()
result = authentication.authenticate(request)
if result is not None:
user, token = result
return user
except Exception:
pass
return None
# 获取ip
def get_ip_address(self, request):
forwarded_for = request.META.get("HTTP_X_FORWARDED_FOR")
if forwarded_for:
return forwarded_for.split(",")[0].strip()
return request.META.get("REMOTE_ADDR")
# 生成操作名称
def get_action(self, request):
action_map = {
"POST": "CREATE",
"PUT": "UPDATE",
"PATCH": "UPDATE",
"DELETE": "DELETE",
}
if request.path == "/api/auth/login/":
return "LOGIN"
if request.path == "/api/auth/register/":
return "REGISTER"
if request.path == "/api/auth/change-password/":
return "CHANGE_PASSWORD"
if request.path == "/api/submissions/":
return "CREATE_SUBMISSION"
return action_map.get(request.method, "REQUEST_ERROR")
# 创建日志
def create_log(self, request, response, duration_ms):
try:
OperationLog.objects.create(
user=self.get_user(request),
action=self.get_action(request),
method=request.method,
path=request.path[:255],
status_code=response.status_code,
ip_address=self.get_ip_address(request),
user_agent=request.META.get(
"HTTP_USER_AGENT",
"",
)[:500],
duration_ms=duration_ms,
detail={
"query": request.GET.dict(),
},
)
except Exception:
# 日志失败不能影响正常业务请求
pass记录过程
1
2
3
4
5
6
记录开始时间
→ 执行真正的接口
→ 获得响应
→ 计算耗时
→ 判断是否需要记录
→ 写入 OperationLog- should_record用于判断哪些请求需要记录,普通的get 200不会记录
- get_user用于单独获取JWT用户
- get_ip_address用于获取ip
- get_action用于生成操作名称
- create_log用于创建日志
然后在settings里注册中间件
请求流程示例
用户注册:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
前端 POST /api/auth/register/
|
v
config/urls.py
|
v
accounts/urls.py
|
v
accounts/views.py
|
v
accounts/serializers.py
|
v
accounts/models.py
|
v
MySQL 用户表查看题目列表:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
前端 GET /api/problems/
|
v
config/urls.py
|
v
problems/urls.py
|
v
problems/views.py
|
v
problems/serializers.py
|
v
problems/models.py
|
v
MySQL 题目表提交代码判题:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
前端 POST /api/submissions/
|
v
submissions/views.py
|
v
Submission 表新增一条 PENDING 记录
|
v
run_judge worker 发现 PENDING
|
v
judge/services.py 运行代码
|
v
更新 Submission/JudgeResult数据库设计
创建数据库:
1
CREATE DATABASE kouoj DEFAULT CHARACTER SET utf8m64给该数据库创建一个专门用户kouoj_user
1
2
CREATE USER 'kouoj_user'@'localhost' IDENTIFIED BY 'password';
GRANT ALL ON kouoj.* TO 'kouoj_user'@'localhost';包含表:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
+--------------------------------+
| Tables_in_kouoj |
+--------------------------------+
| accounts_user |
| accounts_user_groups |
| accounts_user_user_permissions |
| auth_group |
| auth_group_permissions |
| auth_permission |
| django_admin_log |
| django_content_type |
| django_migrations |
| django_session |
| problems_problem |
| problems_problem_tags |
| problems_tag |
| problems_testcase |
| submissions_judgeresult |
| submissions_submission |
+--------------------------------+其中,前三个是我们写的account.User生成的表,负责记录用户,用户组和权限关系
后七个是Django自带的表,负责权限,日志,session等
后四个是存储problems相关信息的表
最后两个是存储submissions相关信息的表
Accounts部分
这部分表记录了用户相关信息,包括一张表:user(用户)
它们的列结构为:
字段 类型 说明 id BigAutoField 主键 password varchar 加密后的密码 last_login datetime 上次登录时间 is_superuser bool 是否超级管理员 username varchar 用户名 first_name varchar 名 last_name varchar 姓 email varchar 邮箱 is_staff bool 是否允许进入 Django Admin is_active bool 账号是否启用 date_joined datetime 注册时间 role varchar 角色,user / admin solved_count int unsigned 通过题目数 submit_count int unsigned 提交次数
Problems部分
这部分的表是最核心的,包括三张表:tag(标签)、problem(题目)、testcase(测试点)
其中tag与problem是多对多,testcase与problem是一对多
拆成三张表有利于灵活设计,不然一个列里要放好几个测试点对应关系很别扭
它们的列结构为:
problems:
字段 类型 说明 id BigAutoField 主键 title varchar(120) 题目标题 description longtext 题目描述 input_description longtext 输入说明 output_description longtext 输出说明 difficulty varchar(20) 难度:easy / medium / hard time_limit int unsigned 时间限制,单位 ms memory_limit int unsigned 内存限制,单位 MB is_public bool 是否公开 created_by_id bigint 创建者用户 ID,外键 created_at datetime 创建时间 updated_at datetime 更新时间
tag:
字段 类型 说明 id BigAutoField 主键 name varchar(40) 标签名,唯一
problems_tags:自动创建的中间表,用于problems和tags之间的多对多关联
字段 类型 说明 id BigAutoField 主键 problem_id bigint 题目 ID,外键 tag_id bigint 标签 ID,外键
testcase:
字段 类型 说明 id BigAutoField 主键 problem_id bigint 所属题目 ID,外键 input_data longtext 输入数据 output_data longtext 标准输出 is_sample bool 是否样例 score int unsigned 测试点分值 order int unsigned 测试点顺序
Submission部分
记录提交相关信息,包括两张表:submission(提交信息)、judgeresult(测试点结果)
关系是submission一对多judgeresult
submission:
字段 类型 说明 id BigAutoField 主键 user_id bigint 提交用户 ID,外键 problem_id bigint 题目 ID,外键 language varchar(20) 语言,目前为 python3 code longtext 用户提交的代码 status varchar(30) 判题状态 score int unsigned 得分 time_used int unsigned 运行时间,单位 ms memory_used int unsigned 内存,单位 KB,初版暂未真正统计 error_message longtext 错误信息 created_at datetime 提交时间 judged_at datetime 判题完成时间
judgeresult:
字段 类型 说明 id BigAutoField 主键 submission_id bigint 所属提交 ID,外键 testcase_id bigint 所属测试点 ID,外键 status varchar(30) 当前测试点状态 time_used int unsigned 当前测试点运行时间,单位 ms output longtext 用户程序输出 error_message longtext 当前测试点错误信息 created_at datetime 创建时间
表间关系
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
accounts_user
id
|
| created_by_id
v
problems_problem
accounts_user
id
|
| user_id
v
submissions_submission
problems_problem
id
|
| problem_id
v
problems_testcase
problems_problem
id
|
| problem_id
v
problems_problem_tags
^
| tag_id
problems_tag
id
submissions_submission
id
|
| submission_id
v
submissions_judgeresult
^
| testcase_id
problems_testcase
id操作示意
以下命令在./backend 路径内进行
创建超管:python manage.py createsuperuser
运行服务器:pyhton manage.py runserver
运行判题机:python manage.py run_judge [--once/--sleep]
URL输入http://localhost:8000/admin/进入Django后台管理
使用curl测试
注册:
1
2
3
curl -X POST http://127.0.0.1:8000/api/auth/register/ `
-H "Content-Type: application/json" `
-d "{\"username\":\"alice\",\"password\":\"123456\",\"email\":\"alice@example.com\"}"登录:
1
2
3
curl -X POST http://127.0.0.1:8000/api/auth/login/ `
-H "Content-Type: application/json" `
-d "{\"username\":\"alice\",\"password\":\"123456\"}"可以获得accesstoken
提交代码:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
$token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ0b2tlbl90eXBlIjoiYWNjZXNzIiwiZXhwIjoxNzc5MjgyOTIxLCJpYXQiOjE3NzkyNzU3MjEsImp0aSI6IjQ0M2VhNmFiNWIzMTQ3ZjZiODc5NWViNDYyZjM0M2Q0IiwidXNlcl9pZCI6IjIifQ.7ieJ8bwhO-a9KfyoWyR8sXJqUroJXc4g5rYhUkY12r8"
$body = @{
problem = 1
language = "python3"
code = "a, b = map(int, input().split())`nprint(a + b)"
} | ConvertTo-Json
Invoke-RestMethod `
-Uri "http://127.0.0.1:8000/api/submissions/" `
-Method Post `
-Headers @{ Authorization = "Bearer $token" } `
-ContentType "application/json" `
-Body $body查看提交详情:
1
2
curl -H "Authorization: Bearer 你的access_token" `
http://127.0.0.1:8000/api/submissions/1/测试
判题机测试:
- ACCEPT
1
2
3
4
5
6
7
8
9
10
11
12
13
14
$token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ0b2tlbl90eXBlIjoiYWNjZXNzIiwiZXhwIjoxNzc5MjgyOTIxLCJpYXQiOjE3NzkyNzU3MjEsImp0aSI6IjQ0M2VhNmFiNWIzMTQ3ZjZiODc5NWViNDYyZjM0M2Q0IiwidXNlcl9pZCI6IjIifQ.7ieJ8bwhO-a9KfyoWyR8sXJqUroJXc4g5rYhUkY12r8"
$body = @{
problem = 1
language = "python3"
code = "a, b = map(int, input().split())`nprint(a + b)"
} | ConvertTo-Json
Invoke-RestMethod `
-Uri "http://127.0.0.1:8000/api/submissions/" `
-Method Post `
-Headers @{ Authorization = "Bearer $token" } `
-ContentType "application/json" `
-Body $body- WRONG ANWSER
1
2
3
4
5
6
7
8
9
10
11
12
13
14
$token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ0b2tlbl90eXBlIjoiYWNjZXNzIiwiZXhwIjoxNzc5MjgyOTIxLCJpYXQiOjE3NzkyNzU3MjEsImp0aSI6IjQ0M2VhNmFiNWIzMTQ3ZjZiODc5NWViNDYyZjM0M2Q0IiwidXNlcl9pZCI6IjIifQ.7ieJ8bwhO-a9KfyoWyR8sXJqUroJXc4g5rYhUkY12r8"
$body = @{
problem = 1
language = "python3"
code = "a, b = map(int, input().split())`nprint(1)"
} | ConvertTo-Json
Invoke-RestMethod `
-Uri "http://127.0.0.1:8000/api/submissions/" `
-Method Post `
-Headers @{ Authorization = "Bearer $token" } `
-ContentType "application/json" `
-Body $body- RUNTIME_ERROR
1
2
3
4
5
6
7
8
9
10
11
12
13
14
$token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ0b2tlbl90eXBlIjoiYWNjZXNzIiwiZXhwIjoxNzc5MjgyOTIxLCJpYXQiOjE3NzkyNzU3MjEsImp0aSI6IjQ0M2VhNmFiNWIzMTQ3ZjZiODc5NWViNDYyZjM0M2Q0IiwidXNlcl9pZCI6IjIifQ.7ieJ8bwhO-a9KfyoWyR8sXJqUroJXc4g5rYhUkY12r8"
$body = @{
problem = 1
language = "python3"
code = "a, b = map(int, input().split())`nprint(1/0)"
} | ConvertTo-Json
Invoke-RestMethod `
-Uri "http://127.0.0.1:8000/api/submissions/" `
-Method Post `
-Headers @{ Authorization = "Bearer $token" } `
-ContentType "application/json" `
-Body $body- TIME_LIMIT_EXCEED
1
2
3
4
5
6
7
8
9
10
11
12
13
14
$token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ0b2tlbl90eXBlIjoiYWNjZXNzIiwiZXhwIjoxNzc5MjgyOTIxLCJpYXQiOjE3NzkyNzU3MjEsImp0aSI6IjQ0M2VhNmFiNWIzMTQ3ZjZiODc5NWViNDYyZjM0M2Q0IiwidXNlcl9pZCI6IjIifQ.7ieJ8bwhO-a9KfyoWyR8sXJqUroJXc4g5rYhUkY12r8"
$body = @{
problem = 1
language = "python3"
code = "while True:`n pass"
} | ConvertTo-Json
Invoke-RestMethod `
-Uri "http://127.0.0.1:8000/api/submissions/" `
-Method Post `
-Headers @{ Authorization = "Bearer $token" } `
-ContentType "application/json" `
-Body $body