Django在线考试系统开发实战:从架构设计到部署优化
简介这是一套基于Python Django框架开发的在线考试系统完整源码面向Web后端开发者、高校计算机专业学生及课程设计实践者用于快速构建支持题库管理、在线组卷、限时答题、自动阅卷与成绩统计的教育类应用。资源包共405个文件涵盖54个核心Python后端逻辑文件、70个HTML前端页面、83个JavaScript交互脚本、70个PNG/GIF等静态资源以及25个CSS样式文件整体结构清晰包含用户认证、试题CRUD、考试会话控制等典型Django模块压缩包仅3.68MB轻量易部署。已有1585人学习下载提供开箱即用的SQLite数据库、完整前后端联动逻辑、富文本编辑器UEditor集成及响应式界面适合二次开发、毕业设计或教学演示使用。1. 项目概述一个Django在线考试系统的核心价值最近在整理硬盘翻出来一个几年前用Django写的在线考试系统源码。当时是为了给一个内部培训项目救急从零开始搭的没想到后来这套东西成了团队里好几个项目的模板。今天正好有空把这个项目的核心思路、踩过的坑以及怎么从零开始把它跑起来系统地梳理一遍。如果你正在找一个能快速上手的、结构清晰的Django实战项目来学习或者你们团队内部也需要一个轻量级的考试、测评工具那这个源码和接下来的内容应该能给你不少启发。这个系统麻雀虽小五脏俱全。它不是一个玩具Demo而是一个具备完整前后端逻辑、用户角色管理、题库管理、在线考试、自动阅卷和成绩统计功能的Web应用。核心就是用Python的Django框架来驱动前端用了经典的Bootstrap数据库是MySQL。它的价值不在于用了多炫酷的技术栈而在于其架构的清晰性和功能的实用性。对于初学者你能看到一个标准的Django项目是如何组织模型Models、视图Views、模板Templates和路由URLs的对于有经验的开发者里面关于事务处理、并发控制、自动评分算法的一些实现细节也值得讨论和优化。为什么是Django从这些热搜词就能看出来python django国内使用广泛么答案是肯定的。Django以其“开箱即用”的特性、完善的后台管理、清晰的设计哲学MTV模式和强大的ORM在国内的中小型Web项目、内部管理系统、教育类应用中非常流行。它帮你处理了Web开发中大量繁琐的细节比如用户认证、表单处理、安全防护让你能更专注于业务逻辑。这个考试系统就是这种“生产力”的典型体现。2. 系统架构与核心模块拆解拿到一个源码包直接一头扎进代码里是最低效的。我们先从高处俯瞰理解整个系统的骨架。这个在线考试系统采用了典型的MVC在Django里叫MTV架构主要分为以下几个核心模块。2.1 用户与权限管理模块任何多用户系统的基础。这里定义了三种核心角色超级管理员SuperAdmin、教师Teacher和学生Student。Django自带的django.contrib.auth模块是基石但我们在此基础上做了扩展。首先我们没有直接使用Django的默认User模型而是通过AbstractUser进行继承和扩展。这是因为默认的User模型字段username, first_name, last_name, email, password...对于考试系统来说不够用。我们创建了一个自定义的UserProfile模型。# models.py 节选 from django.contrib.auth.models import AbstractUser from django.db import models class UserProfile(AbstractUser): USER_TYPE_CHOICES ( (0, 超级管理员), (1, 教师), (2, 学生), ) user_type models.IntegerField(choicesUSER_TYPE_CHOICES, default2, verbose_name用户类型) real_name models.CharField(max_length20, verbose_name真实姓名) student_id models.CharField(max_length20, blankTrue, nullTrue, verbose_name学号/工号) phone models.CharField(max_length11, blankTrue, nullTrue, verbose_name手机号) avatar models.ImageField(upload_toavatars/, defaultavatars/default.png, verbose_name头像) class Meta: verbose_name 用户信息 verbose_name_plural verbose_name def __str__(self): return self.username为什么这么做直接修改Django的User模型在项目中期会是一场灾难而使用AbstractUser进行一对一关联又显得冗余。继承AbstractUser是最干净、最Django的方式。它保留了所有原生的认证机制同时无缝添加了我们需要的字段。user_type字段是权限控制的核心通过它我们可以在视图层和模板层轻松判断用户角色展示不同的菜单和功能。权限控制上我们没有使用Django复杂的Permission和Group因为对于这个相对固定的三权分立模型用user_type进行逻辑判断更简单直接。例如在视图函数中def some_view(request): if not request.user.is_authenticated: return redirect(login) if request.user.user_type ! 1: # 不是教师 return HttpResponseForbidden(无权访问) # ... 教师才能执行的逻辑踩坑点在settings.py中一定要记得告诉Django我们使用了自定义的用户模型这是很多初学者会忘记的一步会导致数据库迁移和admin后台出现各种诡异问题。# settings.py AUTH_USER_MODEL your_app_name.UserProfile2.2 题库与试卷管理模块这是系统的业务核心。设计的好坏直接决定了系统的灵活性和性能。我们的设计遵循了从大到小的层次课程(Course) - 试卷(Paper) - 试题(Question)。课程Course一个顶层分类比如“Python程序设计”、“数据结构”。教师可以管理自己负责的课程。试题Question最基本的元素。我们支持了单选题、多选题、判断题和填空题。每种题型用一个Question模型表示但通过question_type字段区分。题目的选项对于选择题存储在一个JSON字段options中答案answer字段根据题型存储不同内容如单选题存选项ID多选题存ID列表判断题存True/False填空题存关键词列表。class Question(models.Model): QUESTION_TYPE ( (single_choice, 单选题), (multi_choice, 多选题), (judgment, 判断题), (fill_blank, 填空题), ) course models.ForeignKey(Course, on_deletemodels.CASCADE, verbose_name所属课程) content models.TextField(verbose_name题干) question_type models.CharField(max_length20, choicesQUESTION_TYPE, verbose_name题型) options models.JSONField(defaultdict, blankTrue, verbose_name选项JSON格式) # 例如{A: 选项A内容, B: ...} answer models.TextField(verbose_name标准答案) # 存储方式依题型而定 score models.PositiveIntegerField(default5, verbose_name分值) created_by models.ForeignKey(UserProfile, on_deletemodels.CASCADE, verbose_name创建者)使用JSONField的考量Django 3.1原生支持了JSONField对于像选项这种结构固定但内容灵活的数据用它比再拆一个Option表更简单查询和更新也更方便。但要注意数据库兼容性我们用的是MySQL 5.7支持JSON类型。试卷Paper试卷不是简单的问题列表。我们设计了Paper模型存储试卷元信息如名称、总分、考试时间和PaperQuestion中间模型。PaperQuestion记录了某道题在某张试卷中的顺序order和实际分值score。这样同一道题在不同的试卷中可以有不同的分值和位置灵活性极高。class Paper(models.Model): title models.CharField(max_length200, verbose_name试卷名称) course models.ForeignKey(Course, on_deletemodels.CASCADE, verbose_name所属课程) total_score models.PositiveIntegerField(verbose_name总分) duration models.PositiveIntegerField(verbose_name考试时长分钟) is_published models.BooleanField(defaultFalse, verbose_name是否发布) class PaperQuestion(models.Model): paper models.ForeignKey(Paper, on_deletemodels.CASCADE, related_namepaper_questions) question models.ForeignKey(Question, on_deletemodels.CASCADE) order models.PositiveIntegerField(default0, verbose_name题目序号) score models.PositiveIntegerField(verbose_name本题分值)这种设计避免了在Question模型里硬编码分值也使得“从题库中抽题组卷”的功能更容易实现——你只需要创建新的Paper和对应的PaperQuestion记录即可。2.3 考试过程与成绩管理模块这是最复杂的部分涉及状态机、定时任务和事务处理。考试记录ExamRecord学生每次进入一场考试就会生成一条ExamRecord记录。它关键字段包括关联的学生、试卷、开始时间、提交时间、状态未开始进行中已提交超时已批阅、最终得分等。开始时间在学生点击“开始考试”时写入是计算剩余时间和判断是否超时的依据。学生答案StudentAnswer学生每答一题就对应一条StudentAnswer记录关联到ExamRecord和具体的PaperQuestion。它存储学生的原始答案answer_text可能是选项ID、True/False或文本以及该题是否得分is_correct和实际得分got_score。这里采用实时保存的策略学生每做一题前端就通过Ajax将答案提交到后台保存防止页面意外关闭导致答案丢失。技术细节答案保存的视图函数必须用transaction.atomic装饰器包裹确保在并发提交时数据的一致性。同时要处理好前端频繁请求可能带来的性能压力我们做了简单的防抖Debounce处理并使用了Django的Q对象进行高效查询。自动阅卷当学生提交试卷或考试时间到时系统触发自动阅卷流程。这是一个后台任务核心逻辑是一个函数遍历该次考试的所有StudentAnswer与对应Question的标准答案进行比对。单选题/判断题直接比较字符串或布尔值。多选题比较学生提交的选项ID列表和标准答案列表是否完全一致顺序无关。这里有个细节我们判定全对才得分漏选、错选均不得分。填空题这是最麻烦的。我们采用“关键词匹配”加“相似度计算”的策略。标准答案里会存储一个关键词列表如[Python, 解释型]。系统会先对学生的答案进行简单的分词和清洗然后检查是否包含这些关键词。对于更复杂的情况可以引入difflib或jieba库进行相似度分析但初期为了简单我们只做了精确关键词匹配和同义词替换预定义一个小型同义词表。 阅卷完成后更新ExamRecord的状态为已批阅并计算总分写入total_score。超时处理我们最初用JavaScript在前端做倒计时但这是不可靠的用户可以修改系统时间或调试工具。可靠的做法必须是服务端校验。我们在两个地方做校验一是在学生每次保存答案或提交试卷时视图函数会检查ExamRecord.start_time duration是否小于当前时间二是使用Django的定时任务框架如django-celery-beat设置一个周期性任务每隔一分钟扫描所有状态为进行中的考试将超时的考试状态自动改为超时并触发自动阅卷。这是保证考试公平性的关键。3. 关键功能的技术实现细节理解了模块我们深入到几个关键功能的具体实现这里面的“坑”和技巧最多。3.1 前后端交互与实时保存考试页面的核心需求是稳定、防丢失、体验流畅。我们放弃了传统的表单一次性提交采用Ajax分题保存。前端基于jQuery和Bootstrap大概逻辑如下// 假设每题有一个class为‘save-answer’的按钮或输入框失去焦点事件 $(.question-input).on(blur, function() { var questionId $(this).data(question-id); var answer $(this).val(); var examRecordId $(#exam-record-id).val(); // 隐藏域存储本次考试记录ID // 简单防抖避免失焦事件过于频繁 clearTimeout(window.saveTimer); window.saveTimer setTimeout(function() { $.ajax({ url: /api/save_answer/, type: POST, data: { exam_record_id: examRecordId, paper_question_id: questionId, answer: answer, csrfmiddlewaretoken: $(input[namecsrfmiddlewaretoken]).val() }, success: function(response) { if(response.status success) { // 可以在题号旁显示一个“已保存”的小图标持续2秒后消失 showTempHint(答案已自动保存); } }, error: function() { alert(答案保存失败请检查网络后重试); } }); }, 800); // 延迟800毫秒 });后端对应的Django视图需要处理并发和重复提交import json from django.db import transaction from django.views.decorators.csrf import csrf_exempt from django.http import JsonResponse csrf_exempt transaction.atomic def save_student_answer(request): if request.method ! POST: return JsonResponse({status: error, msg: 方法不允许}) exam_record_id request.POST.get(exam_record_id) paper_question_id request.POST.get(paper_question_id) answer request.POST.get(answer, ) try: exam_record ExamRecord.objects.select_for_update().get(idexam_record_id, studentrequest.user) # 检查考试是否已结束或超时 if exam_record.status not in [进行中]: return JsonResponse({status: error, msg: 考试已结束无法保存答案}) # 使用get_or_create避免同一题多次保存产生多条记录 student_answer, created StudentAnswer.objects.get_or_create( exam_recordexam_record, paper_question_idpaper_question_id, defaults{answer_text: answer} ) if not created: student_answer.answer_text answer student_answer.save() return JsonResponse({status: success}) except Exception as e: return JsonResponse({status: error, msg: str(e)})注意这里用了select_for_update()对ExamRecord进行了行级锁是为了防止在极短时间内同一个考试记录被并发修改状态。transaction.atomic确保了保存答案和检查状态的原子性。csrf_exempt是因为我们用了Ajax POST但更安全的做法是在前端配置携带CSRF token。3.2 自动阅卷算法的优化最初的阅卷函数是简单的循环比对当一场考试有100道题时就要进行100次数据库查询N1问题非常慢。我们对此进行了优化。优化后的批阅函数核心逻辑def auto_grade_exam(exam_record_id): from django.db.models import Prefetch # 1. 一次性取出考试记录、关联的试卷、试卷题目、及对应的原始题目和学生的答案 exam_record ExamRecord.objects.select_related(paper).prefetch_related( Prefetch(paper__paper_questions, querysetPaperQuestion.objects.select_related(question).all()), Prefetch(student_answers, querysetStudentAnswer.objects.all()) ).get(idexam_record_id) # 2. 将学生的答案按题目ID整理成字典方便快速查找 answer_dict {sa.paper_question_id: sa for sa in exam_record.student_answers.all()} total_score 0 details [] # 3. 遍历试卷中的每一道题 for pq in exam_record.paper.paper_questions.all(): student_answer answer_dict.get(pq.id) got_score 0 is_correct False if student_answer: # 4. 调用评分函数根据题型分发 got_score, is_correct grade_single_question( pq.question.question_type, pq.question.answer, student_answer.answer_text, pq.score # 传入本题分值 ) # 5. 更新学生答案记录 student_answer.got_score got_score student_answer.is_correct is_correct student_answer.save(update_fields[got_score, is_correct]) else: # 学生未作答得0分 got_score 0 is_correct False total_score got_score details.append({pq_id: pq.id, score: got_score}) # 6. 更新考试记录 exam_record.total_score total_score exam_record.status 已批阅 exam_record.save(update_fields[total_score, status]) return total_score, details def grade_single_question(q_type, standard_answer, student_answer, full_score): # 根据题型评分 if q_type single_choice or q_type judgment: return (full_score, True) if standard_answer student_answer else (0, False) elif q_type multi_choice: # 将字符串形式的答案列表如[A,C]解析成集合再比较 std_set set(json.loads(standard_answer)) stu_set set(json.loads(student_answer)) if student_answer else set() return (full_score, True) if std_set stu_set else (0, False) elif q_type fill_blank: # 填空题关键词匹配逻辑 keywords json.loads(standard_answer) # 标准答案是一个关键词列表 student_text student_answer.lower().strip() matched_count sum(1 for kw in keywords if kw.lower() in student_text) # 假设匹配度超过70%则给全分可以调整 ratio matched_count / len(keywords) if keywords else 0 return (full_score, True) if ratio 0.7 else (0, False) return (0, False)优化点使用select_related和prefetch_related一次性拉取所有相关数据避免了N1查询问题这是Django ORM性能优化的关键。将学生答案预先处理成字典将循环内的查询操作降为O(1)的字典查找。评分逻辑独立成函数清晰且易于扩展比如未来增加简答题评分。3.3 定时任务与考试状态同步我们使用Celerydjango-celery-beat来处理定时任务。具体配置不在此赘述网上教程很多。关键是在tasks.py中定义一个周期性任务。# tasks.py from celery import shared_task from django.utils import timezone from .models import ExamRecord shared_task def check_exam_timeout(): 检查并处理超时的考试 now timezone.now() # 查找所有‘进行中’且已超时的考试 timeout_exams ExamRecord.objects.filter( status进行中 ).extra( where[start_time INTERVAL duration MINUTE %s], params[now] ) for exam in timeout_exams: exam.status 超时 exam.save(update_fields[status]) # 异步触发自动阅卷避免阻塞定时任务 auto_grade_exam.delay(exam.id) return f处理了 {len(timeout_exams)} 场超时考试然后在django-celery-beat的后台设置这个任务每分钟执行一次。这样无论前端发生什么学生浏览器崩溃、网络断开服务端都能保证考试在规定时间结束。4. 部署、配置与常见问题排查4.1 本地开发环境搭建假设你已经按照python安装详细步骤装好了Python 3.8和MySQL。创建虚拟环境并安装依赖这是隔离项目环境的好习惯。python -m venv venv # Windows: venv\Scripts\activate # Linux/Mac: source venv/bin/activate pip install django3.2.* pymysql pillow celery django-celery-beat redisPillow用于处理图片上传头像Celery系列用于异步任务Redis作为Celery的消息代理和缓存后端可选但推荐。数据库配置在settings.py中配置MySQL。DATABASES { default: { ENGINE: django.db.backends.mysql, NAME: exam_system_db, USER: your_username, PASSWORD: your_password, HOST: localhost, PORT: 3306, OPTIONS: {charset: utf8mb4}, } }记得先用MySQL客户端创建数据库exam_system_db并确保字符集是utf8mb4支持Emoji等四字节字符。应用数据迁移与创建超级用户python manage.py makemigrations python manage.py migrate python manage.py createsuperuser按照提示输入管理员账号信息。运行并测试python manage.py runserver访问http://127.0.0.1:8000/admin用刚创建的超级用户登录你就能看到Django强大的后台管理界面里面已经包含了我们定义的所有模型。你可以在这里直接管理用户、课程、试题和试卷。4.2 生产环境部署要点本地跑起来只是第一步要上线还需要做很多工作。关闭Debug模式这是最重要的安全步骤。在settings.py中DEBUG False ALLOWED_HOSTS [your_domain.com, www.your_domain.com, 服务器IP] # 必须设置静态文件收集Django开发服务器能伺服静态文件但生产环境不行。你需要运行python manage.py collectstatic将所有静态文件收集到STATIC_ROOT指定的目录然后用Nginx或Apache来提供这些文件。使用WSGI服务器不要用runserver上线。使用Gunicorn或uWSGI。例如用Gunicornpip install gunicorn gunicorn --workers 3 your_project.wsgi:application -b 0.0.0.0:8000前端代理与HTTPS使用Nginx作为反向代理处理静态文件并将动态请求转发给Gunicorn。同时务必配置SSL证书启用HTTPS保护用户登录信息和考试数据的安全。Celery Worker与Beat在生产服务器上你需要分别启动Celery的工作进程和调度进程。# 启动worker celery -A your_project worker --loglevelinfo # 启动beat调度器 celery -A your_project beat --loglevelinfo --scheduler django_celery_beat.schedulers:DatabaseScheduler通常使用supervisor或systemd来管理这些进程确保它们意外退出后能自动重启。4.3 开发与部署中的常见“坑”静态文件404这是Django新手部署时遇到最多的问题。确保settings.py中的STATIC_URL和STATIC_ROOT配置正确并且运行了collectstatic命令。在Nginx配置中正确设置location /static/的别名alias指向STATIC_ROOT目录。数据库连接错误确保生产环境的数据库用户有远程连接权限如果数据库和应用不在同一台服务器并且防火墙开放了数据库端口默认3306。在Django的DATABASES配置中HOST要写对。时区问题考试开始时间、结束时间的判断强烈依赖服务器时间。务必在settings.py中设置正确的时区。TIME_ZONE Asia/Shanghai USE_TZ True # 建议设置为True让Django处理时区感知的datetime对象在代码中始终使用django.utils.timezone.now()来获取当前时间而不是Python标准的datetime.now()。并发提交导致数据错乱正如前面提到的在保存答案、提交试卷等关键操作中一定要使用数据库事务transaction.atomic和行锁select_for_update来保证数据一致性。特别是在计算最终成绩并更新ExamRecord时。性能瓶颈当题库和考试记录数量巨大时一些列表查询如教师查看所有学生成绩可能会变慢。要考虑使用数据库索引在models.Meta中定义indexes对复杂查询使用select_related和prefetch_related并合理引入缓存Django的缓存框架。例如课程列表、固定的题型说明等不常变的数据非常适合缓存。这个项目源码虽然基础但涵盖了Django Web开发的绝大部分核心概念模型设计、视图逻辑、模板渲染、表单处理、用户认证、静态文件、异步任务、生产部署。通过研究和修改它你不仅能得到一个可用的在线考试系统更能深入理解一个真实Django项目的构建脉络。我建议你在本地运行起来后尝试添加一些新功能比如“考试倒计时实时显示在页面标题栏”、“主观题人工批阅功能”、“成绩分析图表”这会让你的学习更有成效。本文还有配套的精品资源点击获取
上一篇/下一篇内容由系统自动关联
返回资讯列表 →