拓冰建站拓冰建站
首页 / 资讯中心 / 正文

在线考试系统设计与实现:基于Django的完整实战指南

简介这是一套完整的基于Django框架开发的Python在线考试系统适用于本科毕业设计、课程大作业及教学实践项目聚焦多角色协同的考试全流程管理。系统支持管理员、教师、学生三级权限体系覆盖用户管理、班级课程绑定、题库建设含单选/多选/判断/填空/简答/编程题、智能组卷手动随机、在线考试、自动判分编程题支持Python沙箱运行、成绩统计与错题解析等核心功能。资源包共607个文件包含56个Python后端逻辑文件、123个Vue前端组件、63个JS交互脚本、159个SVG图标资源以及SQL数据库脚本、BAT自动化部署脚本如install.bat、run.bat和配套文档整体压缩包大小为21.63MB。已有66人学习下载提供可直接运行的完整工程结构、清晰的模块划分如exam、question、user应用、初始化数据库脚本及图文混排题干支持能力便于快速部署、二次开发与教学演示。 做在线考试系统这个项目我前后折腾了两轮踩了不少坑也总结出一套比较顺的路径。这篇文章围绕基于Python的Django在线考试系统设计与实现来写把数据库设计、核心功能、实操步骤和常见问题一次讲清楚希望能帮你少走弯路。这个系统解决的问题很直接把传统纸质考试的出题、发卷、答题、判分、成绩统计全流程搬到线上。教师可以维护题库、创建考试、发布试卷学生登录后参加考试、交卷后系统自动判分成绩实时可查。整个系统涉及用户认证、角色权限、试卷管理、在线答题、自动判分、成绩统计等多个模块是一条非常完整的Web开发链路既能作为毕业设计/课程设计也能用于企业培训考核或学校测验。如果你正在做类似项目或者刚学完Python基础想找一个综合实战练手这篇文章会从技术选型、数据库设计、核心逻辑到部署带你完整走一遍。即使你只是想把Django的知识点串起来后面几节关于权限设计、并发处理、时间校验的内容也值得一看。1. 项目概述与技术选型思路1.1 系统整体需求拆解开始写代码之前先把需求摸清楚。在线考试系统通常面向三类角色学生、教师、管理员。管理员管理用户账号、课程分类、系统基础配置教师创建考试、维护题库、查看所有考生成绩并导出学生查看可参加的考试、在线答题、提交试卷、查看自己的成绩从业务场景来看一场考试的生命周期是教师创建草稿考试 → 添加题目 → 发布考试 → 学生在规定时间内进入答题 → 自动判分 → 成绩汇总统计。这个流程看起来简单但每个节点都有隐藏的边界条件比如考试未开始能不能看到试卷、交卷后还能不能修改答案、教师改了题目分数会不会影响已经交卷的学生这些问题在后面几章会逐个展开。1.2 为什么选Django而不是Flask或Spring Boot很多初学者都会纠结框架选型。就我个人的实践经验来说做这类“管理后台 用户登录 业务表单”的系统Django确实是最省心的选择原因有四条。第一Django自带Admin后台。题库管理、用户管理这类操作配置好Django Admin后几乎不用写前端页面就能直接使用项目开发周期能缩短很多。第二Django内置用户认证系统。登录、登出、Session管理、权限分组都是现成的不需要自己实现密码加密和会话保持。第三Django的ORM非常成熟。在线考试系统的核心就是数据模型和关系映射用ORM写模型类直观清晰还自带数据库迁移工具改完模型执行一条命令就能同步表结构。第四安全性有基础保障CSRF防护、SQL注入防护默认开启对经验不太多的开发者来说等于自带了一面盾牌。作为对比Flask确实小巧灵活但需要自己拼装数据库扩展、表单扩展、登录扩展项目一复杂到处都是重复代码。Spring Boot性能和生态都很好但Java体系对小型项目来说太重了学习成本高开发效率反而不如Python。如果你预计考试并发量不超过几千人Django完全够用如果要做几十万人在线的大型考试系统那是另一个话题需要引入缓存、消息队列和专门的压力测试但作为课程设计或企业内部系统Django是“能力中庸但恰好够用”的选择。1.3 整体技术方案后端框架是Django数据库起步用SQLite上线换MySQL模板渲染作为主方案配合少量AJAX实现切题和暂存答案。之所以不直接上前后端分离是因为这类系统的核心是业务逻辑和数据处理模板渲染可以大大减少开发量一套代码搞定服务端渲染不用维护两套联调接口。如果你的目标是学习Django REST Framework或者想做成前后端分离项目可以在完成基础版本后改造这个下面会提到。2. 数据库设计整个系统的地基2.1 核心表结构拆解在线考试系统能不能做好一半取决于数据库设计。我最终定下来的表有7张核心业务表再加上Django自带的用户表。第一张是用户表继承Django的AbstractUser增加role字段区分学生、教师、管理员增加real_name字段存真实姓名。为什么不用一张独立的Profile表去存角色和姓名对于这个项目来说直接加字段更简单。Django的OneToOneField和Signal机制虽然能做扩展但新手很容易搞混项目规模不大时没必要增加复杂度。第二张是课程表用来给考试归类字段就是课程名和课程简介表里至少要有个外键关联到教师表示这门课的负责人。第三张是考试表字段设计和示例可以参考这个结构字段名类型说明titleCharField考试名称courseForeignKey关联课程start_timeDateTimeField考试开始时间end_timeDateTimeField考试结束时间durationIntegerField答题时长分钟total_scoreIntegerField总分pass_scoreIntegerField及格分statusCharField草稿/已发布/已关闭created_byForeignKey创建人这里有个关键设计考试状态不要只靠status字段还要结合start_time和end_time来判断。数据库里只存草稿、已发布、已关闭三个显式状态至于考试是未开始、进行中还是已结束用当前时间和开始结束时间比较就能算出来。这样省掉了定时任务去刷新状态也避免服务器休眠导致状态没刷新。第四张是试题表包含题号类型单选题、判断题、题干、分值、正确答案。第五张是选项表一个选择题对应多个选项。第六张是考试记录表记录某个学生参加某场考试的状态、得分、开始交卷时间。第七张是答题明细表记录学生每道题的作答情况和判分结果。2.2 表关系设计的三个关键决策第一选择题的选项一定要独立建表不要用逗号分隔存在一个字段里。很多新手图省事把ABC三个选项拼成一个字符串存进Question表的options字段读取时split一下。这个方案前期写起来很爽后期改选项、统计选项分布时会非常痛苦而且如果某个选项本身包含逗号split后数据就错乱了。正确的做法是建Choice表通过外键关联到Question。第二标准答案和学生答案要分开存。这个问题我问过不少人第一次做的人很容易把“正确答案”和“考生的回答”混在一起。正确答案是题目属性存在Question表的correct_answer字段学生的作答结果跟着人走一个人在多个考试中有多个答案所以学生答案必须存到AnswerDetail表。如果错把学生答案放在Question表里那这道题所有考生的答案都会互相覆盖。第三要在交卷时就固化判分结果。我的方案是交卷时遍历答题明细每道题判完分存进AnswerDetail的score字段最后汇总总分存进ExamRecord。这个思路和电商的“订单快照”是一个道理考生交卷时的价格得分已经确定后续教师修改题目分值已经交卷的学生成绩不受影响。如果交卷后不保存分数每次都实时计算一旦教师改了题目分数历史成绩就会悄悄跟着变数据就不对了。2.3 数据库文档的整理方法项目标题里特别提了数据库文档这部分多说几句。一份合格的数据库文档应该包含四个部分ER图、表结构说明、状态流转说明、核心查询SQL示例。ER图可以手动用draw.io画也可以直接用django-extensions的graph_models自动生成。手动画图的好处是美观规范自动生成的好处是省时准确。我建议先用自动生成拿到一张准确的底图再用draw.io整理出最终版。表结构说明要逐表列出字段名、类型、允许为空、默认值、索引、外键关联。这里有一个容易被忽视的点很多人的数据库文档只写了字段和类型但没写业务含义。比如ExamRecord表的status字段要解释清楚ongoing表示进行中、finished表示已完成以及状态转换是由哪个动作触发的。状态流转说明是我强烈建议补充的内容。经验教训是如果文档里不把状态机写清楚开发时很容易被“考试记录从进行中变成已完成”这类流转逻辑绕晕。比如考试记录的status从ongoing到finished触发条件是交卷接口用户不能查看自己未参加的考试记录接口层要过滤。核心SQL示例也很重要。比如“查询某场考试的平均分、最高分、及格率”这个SQL或ORM写法在答辩或交接时经常被问到提前整理好省得现场翻代码。3. 核心功能模块设计与实现3.1 用户认证与三角色权限控制用户认证直接复用Django内置的LoginView和Session机制这部分不需要自己实现。角色字段建议存字符串不存数字。为什么存数字“1、2、3”写代码时总要回头注释才知道1代表学生还是教师可读性太差直接存student、teacher、admin一眼看懂数据库多占几个字节无所谓。权限控制做两层。第一层是视图层装饰器用login_required保证必须登录再定义role_required装饰器校验角色是否允许访问from functools import wraps from django.core.exceptions import PermissionDenied def role_required(roles): def decorator(view_func): wraps(view_func) def _wrapped_view(request, *args, **kwargs): if request.user.is_authenticated and request.user.role in roles: return view_func(request, *args, **kwargs) raise PermissionDenied return _wrapped_view return decorator用法很简单login_required role_required([teacher, admin]) def create_exam(request): ...第二层是数据级权限也就是按数据归属来控制。Django自带的Permission系统是“动词名词”的粗粒度模型比如可以控制谁能add_exam但控制不了“教师只能修改自己创建的考试管理员可以修改所有考试”这种细分场景。所以数据级权限在视图里通过外键过滤实现。教师查看自己的考试列表查询时加一个created_byrequest.user管理员查看所有考试不加这个过滤条件。Django内置的admin后台也要在admin.py里做同样的过滤否则教师登录admin后能看到并修改别人的考试。这个问题我见过很多人踩坑只改了前端页面权限没改admin后台结果学生在admin后台里看到了所有答案。3.2 考试流程从创建考试到发布教师的操作路径是填写考试基本信息 → 设置题目 → 发布考试。创建考试的表单非常简单考试名称、选择课程、设置起止时间和时长、设置总分和及格分。这一步会插入一条Exam记录status是draft。接着进入题目配置页面。题目的添加有两种方式一种是逐道手工录入适合小型考试另一种是从题库批量选择导入适合题目量大的场景。为了支持批量复用Exam和Question之间用ManyToManyField关联。这里Django会自动生成一张中间表不需要手动建。但如果需要控制每道题在试卷里的顺序或者不同学生看到不同的题目版本就要手动建中间表加一个order字段。发布考试时把status从draft改成published即可。前端学生列表的查询条件是statuspublished且当前时间在start_time和end_time之间。这里有个设计细节值得强调未开始的考试只显示考试名称和开始时间不显示题目内容点击进入答题页面时后端也要校验时间不能只靠前端隐藏入口否则懂点技术的学生直接构造URL就能进入答题页。3.3 在线答题的核心逻辑答题页是这个系统交互最复杂的地方有几个问题在动手前必须想清楚。第一个问题是倒计时怎么实现。不要想复杂了前端倒计时只是体验层后端时间校验才是真正的逻辑保证。我的做法是学生点击开始考试时后端创建一个ExamRecord记录started_at时间返回试卷数据。前端拿到duration和started_at用duration减掉已经过去的时间得到剩余秒数本地做倒计时。倒计时归零时自动调用交卷接口。同时后端交卷接口里再做一次校验如果服务器时间已经超过started_at duration就强制交卷不管前端有没有自动提交。from django.utils import timezone from datetime import timedelta deadline record.started_at timedelta(minutesrecord.exam.duration) if timezone.now() deadline: # 强制交卷第二个问题是防止重复交卷。学生第一次提交成功后后端把ExamRecord状态从ongoing改成finished。交卷接口里先查状态如果不是ongoing直接拒绝。这里要用事务和select_for_update锁行防止两个并发请求同时通过状态判断后重复提交这个细节在第5章会详细展开。第三个问题是学生中途关掉浏览器怎么办。我建议在答题页加一个“保存”功能学生切换题目时通过AJAX把当前题目选中的答案暂存到AnswerDetail。这样即使中途退出下次进来还能接着做不会因为页面刷新导致全部答案丢失。很多初学者第一版不做暂存功能结果学生答了30道题不小心刷新页面全部白答体验非常差。第四个问题是题目和选项顺序要不要打乱。如果考试需要防作弊最简单的做法是每个学生进入答题页时随机打乱题目顺序如果选项也要防就随机打乱选项顺序。注意打乱选项顺序时必须同步更新正确选项的标识逻辑。更省事的做法是给答题页面生成一次随机顺序后存到中间表刷新后顺序不变否则学生刷新一次题目顺序就变一次容易把学生搞晕。3.4 自动判分与成绩统计选择题和判断题是自动判分的主流题型。选择题的判分逻辑很简单比较学生提交的答案和正确答案的标识一致给分不一致给零分。判断题本质上只有一个选项答案存True或False逻辑完全一样。交卷时的判分核心代码def grade_exam(record): details AnswerDetail.objects.filter(recordrecord) total 0 for detail in details: if detail.student_answer and detail.student_answer detail.question.correct_answer: detail.is_correct True detail.score detail.question.score total detail.question.score else: detail.is_correct False detail.score 0 detail.save() record.score total record.finished_at timezone.now() record.save()成绩统计页面用Django ORM的aggregate方法就能完成from django.db.models import Avg, Max, Min, Count, Q stats ExamRecord.objects.filter(examexam, statusfinished).aggregate( avg_scoreAvg(score), max_scoreMax(score), min_scoreMin(score), pass_countCount(id, filterQ(score__gteexam.pass_score)), total_countCount(id), )导出成绩用CSV或者Excel。如果是中文环境建议用openpyxl直接生成xlsx文件因为CSV用Excel打开时经常遇到中文乱码xlsx格式谁拿到都能正常打开。如果后续要支持简答题、论述题这类主观题自动判分做不了需要在答案表中加一个status字段记录待判分/已判分状态教师手动打分后更新得分。这个扩展不复杂核心思路是主观题不在交卷时判分而是在教师端待办列表里逐题批改。4. 实操过程从零搭建一个可运行的考试系统4.1 环境准备与项目初始化动手敲代码的第一步是准备环境。我用Python 3.10以上版本和Django 4.x版本演示。python -m venv venv venv\Scripts\activate # Windows source venv/bin/activate # Linux/macOS pip install django django-admin startproject exam_project cd exam_project python manage.py startapp exampython -m venv venv这一步是关键一定要给项目单独建虚拟环境避免和其他项目的包版本打架。我见过太多人图省事直接用系统全局Python结果不同项目依赖的Django版本冲突最后只能逐个重装。然后修改settings.py把exam加到INSTALLED_APPS配置数据库。开发阶段用SQLite零配置起步DATABASES { default: { ENGINE: django.db.backends.sqlite3, NAME: BASE_DIR / db.sqlite3, } }要切换MySQL就把ENGINE改成django.db.backends.mysql加上HOST、PORT、USER、PASSWORD等参数再安装pymysql并在项目的__init__.py里import pymysql; pymysql.install_as_MySQLdb()。这个细节如果不做Django连MySQL会报错很多第一次连的人都会卡在这一步。4.2 核心模型代码编写在写任何模型之前第一件事是在settings.py里设置AUTH_USER_MODELAUTH_USER_MODEL exam.User先配置这个字段的原因是Django的用户模型一旦执行过数据库迁移再想换自定义用户模型就非常麻烦。一开始就把它配好避免后面改了User表后整个数据库需要重来。用户模型from django.contrib.auth.models import AbstractUser from django.db import models class User(AbstractUser): ROLE_CHOICES ( (student, 学生), (teacher, 教师), (admin, 管理员), ) role models.CharField(max_length20, choicesROLE_CHOICES, defaultstudent) real_name models.CharField(max_length50, blankTrue)考试模型class Exam(models.Model): STATUS_CHOICES ( (draft, 草稿), (published, 已发布), (closed, 已关闭), ) title models.CharField(max_length200) course models.ForeignKey(Course, on_deletemodels.CASCADE) start_time models.DateTimeField() end_time models.DateTimeField() duration models.IntegerField(help_text考试时长分钟) total_score models.IntegerField(default100) pass_score models.IntegerField(default60) status models.CharField(max_length20, choicesSTATUS_CHOICES, defaultdraft) created_by models.ForeignKey(User, on_deletemodels.CASCADE) question_pool models.ManyToManyField(Question, throughExamQuestion)试题模型和选项模型class Question(models.Model): TYPE_CHOICES ( (single, 单选题), (judge, 判断题), ) question_type models.CharField(max_length20, choicesTYPE_CHOICES) content models.TextField() score models.IntegerField(default5) correct_answer models.CharField(max_length10) created_by models.ForeignKey(User, on_deletemodels.SET_NULL, nullTrue) class Choice(models.Model): question models.ForeignKey(Question, on_deletemodels.CASCADE, related_namechoices) label models.CharField(max_length10) content models.CharField(max_length200)这里注意related_namechoices的作用配了以后通过question.choices.all()就能取到所有选项代码写起来非常顺手。考试记录和答题明细class ExamRecord(models.Model): STATUS_CHOICES ( (ongoing, 进行中), (finished, 已完成), ) student models.ForeignKey(User, on_deletemodels.CASCADE) exam models.ForeignKey(Exam, on_deletemodels.CASCADE) status models.CharField(max_length20, choicesSTATUS_CHOICES, defaultongoing) score models.IntegerField(nullTrue, blankTrue) started_at models.DateTimeField(auto_now_addTrue) submitted_at models.DateTimeField(nullTrue, blankTrue) class AnswerDetail(models.Model): record models.ForeignKey(ExamRecord, on_deletemodels.CASCADE, related_namedetails) question models.ForeignKey(Question, on_deletemodels.CASCADE) student_answer models.CharField(max_length10, blankTrue) is_correct models.BooleanField(nullTrue) score models.IntegerField(default0)写完后执行迁移命令python manage.py makemigrations python manage.py migrate python manage.py createsuperuser4.3 核心视图与模板落地学生查看考试列表的视图from django.contrib.auth.decorators import login_required from django.utils import timezone login_required role_required([student]) def exam_list(request): now timezone.now() exams Exam.objects.filter( statuspublished, start_time__ltenow, end_time__gtenow, ) return render(request, exam_list.html, {exams: exams})进入答题页的视图主要工作是初始化考试记录和读取题目列表login_required role_required([student]) def exam_detail(request, exam_id): exam get_object_or_404(Exam, idexam_id, statuspublished) now timezone.now() if now exam.start_time or now exam.end_time: messages.error(request, 考试不在进行中) return redirect(exam_list) record, created ExamRecord.objects.get_or_create( studentrequest.user, examexam, defaults{status: ongoing} ) if record.status finished: return render(request, exam_result.html, {record: record}) questions exam.question_pool.all() return render(request, exam_detail.html, { exam: exam, questions: questions, record: record, })这里get_or_create很实用一个学生进入一场考试只会初始化一条记录不会重复创建。交卷的AJAX视图from django.http import JsonResponse from django.db import transaction login_required role_required([student]) def submit_exam(request, record_id): if request.method ! POST: return JsonResponse({code: 1, msg: 请求方式错误}) try: with transaction.atomic(): record ExamRecord.objects.select_for_update().get( idrecord_id, studentrequest.user ) if record.status finished: return JsonResponse({code: 1, msg: 试卷已提交请勿重复提交}) # 保存答案并判分 ... record.status finished record.save() return JsonResponse({code: 0, msg: 交卷成功}) except ExamRecord.DoesNotExist: return JsonResponse({code: 1, msg: 考试记录不存在})模板里表单的csrf_token不能少AJAX提交要带X-CSRFToken头否则Django直接403拒绝。这个坑几乎每个月都有人踩一次具体做法是先引入function getCookie(name) { let cookieValue null; if (document.cookie document.cookie ! ) { const cookies document.cookie.split(;); for (let i 0; i cookies.length; i) { const cookie cookies[i].trim(); if (cookie.substring(0, name.length 1) (name )) { cookieValue decodeURIComponent(cookie.substring(name.length 1)); break; } } } return cookieValue; } const csrftoken getCookie(csrftoken);4.4 数据库文档的生成与整理开发过程中整理数据库文档最省时的方案是用django-extensions自动生成ER图和模型描述pip install django-extensions pydot python manage.py graph_models -a -o erd.png这条命令从模型自动生成ER图虽然不是特别美观但用来梳理表关系和撰写项目文档足够用。如果在图片上还需要细调可以导出dot格式再用draw.io加工。另外注意把AUTH_USER_MODEL设置好后Django自带的admin后台也会自动使用自定义User模型不需要额外配置登录后台后就能看到用户管理。4.5 单元测试与基础验证写完后不要急着部署先跑一遍核心功能测试。Django的TestCase自带测试数据库写几个关键用例比手工测试靠谱太多from django.test import TestCase from django.utils import timezone from datetime import timedelta from .models import Exam, ExamRecord, Question class ExamTestCase(TestCase): def setUp(self): self.student User.objects.create_user( usernamestu1, password123456, rolestudent ) self.teacher User.objects.create_user( usernametea1, password123456, roleteacher ) self.exam Exam.objects.create( title期中测试, start_timetimezone.now() - timedelta(hours1), end_timetimezone.now() timedelta(hours1), duration60, statuspublished, created_byself.teacher ) def test_student_can_join_exam(self): self.client.login(usernamestu1, password123456) response self.client.get(f/exam/{self.exam.id}/) self.assertEqual(response.status_code, 200) self.assertTrue( ExamRecord.objects.filter(student__usernamestu1, examself.exam).exists() )测试用例覆盖三个最核心的场景学生能正常进入考试、交卷后不能重复提交、考试时间未到不能进入答题页。这3个测试跑通了系统的核心逻辑就有了基本保证。5. 常见问题与排查技巧实录5.1 时区问题考试时间总是不对这是在线考试系统最经典的坑。Django的settings.py里TIME_ZONE默认是UTCUSE_TZ默认是True。如果你直接调用datetime.now()得到的是UTC时间和北京时间差8小时界面显示的时间自然不对。解决办法是在settings.py里把时区配置好TIME_ZONE Asia/Shanghai USE_TZ True然后代码里统一用django.utils.timezone.now()生成时间不要用datetime.now()。模板显示时间的时候用localtime过滤器或模板标签这样会自动把UTC时间转成配置的本地时区。判断考试是否在有效期内时注意不要拿naive datetime和Django返回的aware datetime直接比较会报“cant compare offset-naive and offset-aware datetimes”的错。5.2 交卷并发与重复提交考试系统的交卷接口是非常典型的并发场景。学生可能双击交卷按钮也可能网络超时后手动再次点击。如果后端不做保护同一份试卷会生成两条交卷记录成绩覆盖关系不确定数据就乱了。解法有两层。第一层是前端交卷按钮点击后立即置灰并显示“提交中”从源头减少重复请求。第二层是后端用事务加行锁保护状态判断from django.db import transaction with transaction.atomic(): record ExamRecord.objects.select_for_update().get( idrecord_id, studentrequest.user ) if record.status finished: return JsonResponse({code: 1, msg: 试卷已提交请勿重复提交}) # 判分逻辑select_for_update的作用是在当前事务内对该行加锁另一个并发事务要修改这行时会被阻塞只有当前事务提交后另一个事务才能继续。这样就保证了两个交卷请求即使同时到达第一个执行完提交后第二个也会看到status已经是finished从而被拒绝。5.3 查询性能列表页越来越慢如果考试记录列表变得很慢多半是N1查询导致的。加入学生姓名和考试名称都要显示常规写法是循环里再查一次关联表一个页面发了上百条SQL。解决办法是用select_related和prefetch_related# 查询考试记录时一次性把关联的exam和student取出来 records ExamRecord.objects.filter( examexam ).select_related(exam, student)select_related适用于ForeignKey和OneToOneFieldprefetch_related适用于ManyToManyField和反向查询。答题明细这类一对多场景用prefetch_related一次性加载所有题目的答案避免循环查询。排查SQL执行效率最直观的方式是安装django-debug-toolbar页面右侧会展示每个请求执行的SQL语句、参数和耗时初学者可以很轻松定位到慢查询位置。5.4 部署时的静态文件问题本地开发DEBUGTrue时Django自动处理静态文件看不出来问题一部署到服务器CSS、JS全丢了。解决方法是部署前把settings.py里的DEBUG改成False配置STATIC_ROOT和STATIC_URLDEBUG False ALLOWED_HOSTS [your-domain.com] STATIC_ROOT BASE_DIR / staticfiles STATIC_URL /static/然后在服务器上执行python manage.py collectstaticcollectstatic会把所有app里的静态文件收集到STATIC_ROOT目录然后由nginx或IIS把这个目录映射到/static/路径。Django只处理动态请求静态文件交给Web服务器处理。5.5 删除对象的级联陷阱Django删除对象时外键关联的数据默认有几种行为CASCADE是级联删除PROTECT是保护禁止删除SET_NULL是删除后置空SET_DEFAULT是设为默认值。在考试系统里很多地方一上来就全用CASCADE会造成一个风险删除一个教师账号他创建的所有考试、所有学生考试记录、所有答题明细全部被级联删除这个后果非常严重。我的建议是删除课程时如果还有考试关联应该PROTECT禁止删除提示先把该课程下的考试迁移或删除删除教师账号时他创建的考试用SET_NULL把created_by置空保留考试数据删除考试时学生考试记录通常会级联删除这个要谨慎如果有成绩留存需求考试只能关闭不能删除这个设计在写模型时就要想清楚别等数据跑起来了再改外键行为改起来很麻烦。5.6 一个容易被忽略的细节考试结束后还能不能看试卷考试结束后学生能不能查看自己做过的试卷和答案这个需求要看学校规定。如果允许学生查看答题页要做只读处理隐藏所有表单控件只显示题目和学生答案、正确答案、得分。我建议不要让学生随便看全部正确答案否则后面重考时题目就没法复用了。一个折中的方案是只显示得分和错题数量不展示正确答案详情这个权限控制可以作为一个开关放到Exam表里。6. 项目扩展与经验总结最后分享一点个人体会。在线考试系统做完第一版后我最大的感受是这个项目看起来简单但真正把各种边界情况想清楚需要平衡状态机、时间边界、并发控制、数据快照这四个点。很多第一次做这个选题的人容易把注意力放在页面好不好看上反而忽略了核心业务逻辑结果答辩时被老师一问“交卷时并发怎么处理”“教师改了题目分数怎么办”就答不上来。推荐的是先把业务闭环走通再考虑美化UI。如果你后续想扩展优先级建议这样排先加简答题和人工判分这是功能性的补齐再把Redis引入做答题进度缓存和倒计时这是性能的提升然后可以尝试做Vue3前后端分离用Django REST Framework提供接口这是架构层面的升级最后如果考试人数上来了再考虑用Celery做异步判分和邮件通知这是规模化的方向。在做这个项目时我还有一个踩过的坑想提醒你不要把数据库文档留到最后才补。先花一两天把ER图和状态流转图画清楚写代码时会顺利很多改模型的次数也会大大减少。文档和代码同步迭代这才是做项目的正确节奏。本文还有配套的精品资源点击获取
分享:

看完干货,该让你的企业上线了

免费需求沟通 · 48 小时内出具建站方案 · 河南本地可上门