后端
项目架构
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 22 """Django's command-line utility for administrative tasks.""" import osimport sysdef 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 AppConfigclass 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 AbstractUserfrom django.db import modelsclass 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 adminfrom django.contrib.auth.admin import UserAdminfrom .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 22 from django.contrib.auth import get_user_modelfrom rest_framework import serializersUser = 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 18 from rest_framework import genericsfrom rest_framework.permissions import AllowAny, IsAuthenticatedfrom .serializers import RegisterSerializer, UserSerializerclass 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 pathfrom .views import MeView, RegisterViewurlpatterns = [ 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_cases
models.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 .name
tag表内只有一个字段需要填充,标签名,注意要将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 5 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 21 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, BasePermissionclass 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 11 from rest_framework.routers import DefaultRouterfrom .views import ProblemViewSet, TagViewSet, TestCaseViewSetrouter = 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 返回 JSON
model.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 22 23 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 [临时文件].py
input讲测试点输入传给用户的标准输入
capture_output=True捕获标准输出和标准错误
timeout=timeout_seconds限定运行时间,超时可以抛TLE
cwd=temp_dir让代码在临时目录进行
```python started_at = time.perf_counter() … time_used =
int((time.perf_counter() - started_at) * 1000) 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 用于记录代码执行时间,默认是秒,乘1000转为毫秒 - 用一个try...except捕获超时异常,若超时返回使用时间并返回TLE - `completed.returncode `捕获返回码,若非零表示异常退出,即RE,错误信息从stderr获取,只保留后面2000个字符,防止爆库 - 最后没有运行问题,返回ACCEPT,这里的AC指**运行没有问题,而不代表答案正确** **acquire_pending_submission:** ```PYTHON 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 6 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 22 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" ]: return
BaseCommand是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 settingsfrom django.db import modelsclass 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 adminfrom .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" , )
然后进行迁移
审计中间件
可以自动记录
返回 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 timefrom rest_framework_simplejwt.authentication import JWTAuthenticationfrom .models import OperationLogclass 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 ) 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 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 /
测试
判题机测试:
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 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
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
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