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

Django在线考试系统:从模型设计到部署的完整实战解析

简介面向计算机相关专业学生的Django在线考试系统毕业设计源码包基于Python与MySQL实现个人项目评审得分97分经过严格调试确保可直接运行。这套系统覆盖用户登录、试题管理、在线答题、成绩统计等典型考试模块适合用于毕业设计、期末课程设计或课程大作业也可作为学习Django与MySQL整合开发的完整案例。压缩包共254个文件大小约2.86MB主要包含42个Python源码文件、118个编译后的pyc文件以及前端所需的HTML、CSS、JavaScript、SCSS页面资源另有若干图片与字体图标文件目录结构清晰便于按功能模块查阅。系统界面基于Bootstrap等前端框架构建后台逻辑分层合理读者可从中掌握MTV架构、ORM操作、模板渲染等关键技能也能直接修改题库与考试参数快速搭建自己的在线考试平台。目前已有150人学习下载对有实战参考需求的学生和开发者具有较高借鉴价值。1. Django在线考试系统为什么这组技术栈最值得复刻如果只选一个组合来完成在线考试类毕业设计Django Python MySQL几乎是性价比最高的答案。这套源码评审97分它不是把题目静态堆在页面上而是把组卷、交卷、判分、成绩回写做成了完整链路本地结构清晰改成课程设计或者公司内部培训考试都行。适合计算机相关专业学生直接作为毕设底子也适合想快速搭一套内部考试系统的从业者拿来做二次开发。我建议拿到源码之后不要急着pip install跑起来先从头到尾看一遍数据模型再动手改业务会省掉后面大量返工时间。2. 项目结构与数据模型models.py 决定的表结构边界2.1 拿到源码先看哪几个目录展开压缩包后不要急着装依赖先确认目录结构。这套系统其实就三层配置层、业务 app、模板静态资源。我解压后看到的核心结构是这样online_exam/ ├── manage.py ├── requirements.txt ├── exam/ # 核心业务 app │ ├── models.py │ ├── views.py │ ├── admin.py │ └── migrations/ ├── static/ # bootstrap.css、font-awesome.min.css 等 ├── templates/ │ ├── login.html │ ├── exam_list.html │ └── exam_paper.html └── exam_system/ # Django 项目配置 ├── settings.py └── urls.py重点在exam/models.py和exam/views.pytemplates 里放的是 Bootstrap 4 那套 CSS前端交互主要靠原生 JS 加一点 jQuery没上重框架。这对毕设答辩反而友好老师问前端逻辑时每一行都能解释清楚不会被追问 node_modules 里装了什么。2.2 五个核心模型的设计打开 models.py在线考试系统的表设计基本围绕五个模型展开用户、题库、试卷、考试记录、答题记录。按这个项目的高分定级标准精简且完整的模型层长这样from django.contrib.auth.models import AbstractUser from django.db import models class User(AbstractUser): student_id models.CharField(学号, max_length20, blankTrue) role models.SmallIntegerField(角色, default1, choices((1, 学生), (2, 教师))) class QuestionBank(models.Model): subject models.CharField(科目, max_length64) qtype models.SmallIntegerField(题型, choices((1, 单选), (2, 多选), (3, 判断))) difficulty models.SmallIntegerField(难度, choices((1, 易), (2, 中), (3, 难))) content models.TextField(题干) options models.JSONField(选项, defaultdict) # {A: ..} answer models.CharField(答案, max_length10) score models.PositiveSmallIntegerField(分值, default2) creator models.ForeignKey(User, on_deletemodels.SET_NULL, nullTrue) class ExamPaper(models.Model): title models.CharField(试卷名称, max_length128) duration models.PositiveIntegerField(考试时长/分钟, default60) question_count models.PositiveIntegerField(题目数, default20) questions models.ManyToManyField(QuestionBank, blankTrue) total_score models.PositiveIntegerField(总分, default100) published models.BooleanField(是否发布, defaultFalse) class ExamRecord(models.Model): student models.ForeignKey(User, on_deletemodels.CASCADE) paper models.ForeignKey(ExamPaper, on_deletemodels.CASCADE) score models.FloatField(得分, default0) status models.SmallIntegerField(状态, default1, choices((1, 进行中), (2, 已完成))) start_time models.DateTimeField(auto_now_addTrue) submit_time models.DateTimeField(nullTrue, blankTrue) class AnswerRecord(models.Model): record models.ForeignKey(ExamRecord, on_deletemodels.CASCADE) question models.ForeignKey(QuestionBank, on_deletemodels.CASCADE) user_answer models.CharField(考生答案, max_length20, blankTrue) is_correct models.BooleanField(是否答对, defaultFalse) gained_score models.FloatField(该题得分, default0)几个选型细节值得展开说。options用 JSONField 存选项比单独建一张选项表省掉大量 JOINMySQL 要用 5.7.8 以上版本如果目标环境版本旧改成 TextField 存 JSON 字符串也能跑但查询选项时就要自己json.loads多一道转换。ExamRecord和AnswerRecord拆成两张表是为了交卷判分时不反复读题库尤其是多选题的比对需要把题目正确答案拿出来逐字符处理。question_count在ExamPaper上冗余了一个字段列表页显示「这套卷子有几道题」时不用查中间表虽然多占了点存储但查询快毕设答辩时解释为空间换时间也没问题。为什么ExamPaper.questions用 ManyToManyField 而不是 ForeignKey因为同一道题可以被多套卷子引用而且组卷时经常要先选题目再生成试卷快照多对多中间表天然支持这套逻辑后续加 AB 卷也只需要多建一条 ExamPaper 记录不用动表结构。2.3 初始化数据的正确顺序migrate 之后要先建用户再导题库顺序反了外键会报错。用 admin 创建教师账号然后在题库里录入题目最后建试卷并勾选题目。把题库做成 fixture 是常见做法python manage.py makemigrations exam python manage.py migrate python manage.py createsuperuser python manage.py loaddata questions.jsonquestions.json里每个元素对应QuestionBank的一条记录JSONField 字段在 fixture 里就是普通字典例如{A: 进程, B: 线程, C: 协程}。注意 loaddata 不会清空已有数据重复执行会把题库翻倍所以初始化之前先确认库里没有脏数据。数据库连接配置在exam_system/settings.py里下面的参数是毕设项目里常用的底线配置表格里这几项是评审时追问概率最高的配置项推荐值说明DEBUGFalse部署时关闭否则报错页面会泄露绝对路径ALLOWED_HOSTS[*]毕设演示可放开上生产环境必须改成具体域名DATABASE ENGINEdjango.db.backends.mysql对应 MySQL 5.7字符集用 utf8mb4TIME_ZONEAsia/Shanghai配合USE_TZTrue否则存储时间差 8 小时STATIC_ROOT服务器绝对路径collectstatic 收集后供 nginx 托管TIME_ZONE 和 USE_TZ 的组合是新手最常掉坑的地方。Django 开启USE_TZTrue后写入 MySQL 的时间默认是 UTC模板里直接渲染ExamRecord.start_time会显示比北京时间早 8 小时实际开发中应该在视图里用timezone.localtime(record.start_time)转换后再传给模板或者干脆一次性设置USE_TZFalse配合TIME_ZONEAsia/Shanghai让 Django 直接使用本地时间写入数据库。3. 从组卷到判分核心考试链路如何落地3.1 自动组卷按难度和题型比例抽题很多在线考试系统的组卷逻辑是硬编码的题目顺序固定考前就能背题。这套系统的高分点在于组卷按「题型 难度比例」动态抽取核心思路先确定单选、多选、判断各几道再按难度比例从题库里随机 pick。给出核心代码import random def generate_paper(subject, rule): questions [] for qtype, count in rule[types].items(): qs QuestionBank.objects.filter(subjectsubject, qtypeqtype) hard list(qs.filter(difficulty3)) medium list(qs.filter(difficulty2)) easy list(qs.filter(difficulty1)) picked ( random.sample(hard, kmin(rule[hard_ratio] * count // 100, len(hard))) random.sample(medium, kmin(rule[medium_ratio] * count // 100, len(medium))) random.sample(easy, kmin(rule[easy_ratio] * count // 100, len(easy))) ) questions.extend(picked) paper ExamPaper.objects.create( titlerule[title], question_countlen(questions), total_scoresum(q.score for q in questions) ) paper.questions.set(questions) return paperrandom.sample会在抽样池不够时抛 ValueError所以用min()把抽取数量限制在池子长度以内这是组卷最容易被忽略的边界条件。hard_ratio * count // 100算出该难度应抽几道整数除法保证结果不超总数如果某难度题目不够现在这段代码会少抽而不是从其他难度补足实际项目里一般会在抽完后再统计缺口从剩余题目里按「先中后易」的顺序补齐保证题数达到question_count。调用时机也有讲究。不要在点击「开始考试」时才临时组卷应该在教师发布试卷时就把 ExamPaper 记录建好并绑定题目学生端只读这份快照。这样有两个好处一是组卷过程不会阻塞学生端请求二是如果教师在考试中途往题库里加题已经生成的试卷不会受影响保证所有考生面对的是同一套题。3.2 交卷判分事务边界与答案比对判分入口放在交卷接口里。重点是事务包裹整个判分过程防止判到一半数据库异常导致成绩丢失import json from django.db import transaction from django.utils import timezone from django.http import JsonResponse transaction.atomic def submit_exam(request, record_id): record ExamRecord.objects.select_for_update().get(idrecord_id) if record.status 2: return JsonResponse({code: 1, msg: 试卷已交请勿重复提交}) answers json.loads(request.body).get(answers, {}) total 0.0 for qid, user_ans in answers.items(): q QuestionBank.objects.get(idqid) correct normalize_answer(user_ans) normalize_answer(q.answer) gained q.score if correct else 0.0 AnswerRecord.objects.create( recordrecord, questionq, user_answeruser_ans, is_correctcorrect, gained_scoregained ) total gained record.score round(total, 2) record.status 2 record.submit_time timezone.now() record.save() return JsonResponse({code: 0, score: record.score})select_for_update()在事务内对ExamRecord加行级锁防止学生在两个标签页同时交卷导致成绩被后提交的那次覆盖。Django 里这个锁必须配合transaction.atomic使用否则锁不生效数据库会直接报错。normalize_answer是自定义函数做的事情是把用户答案和标准答案都转成小写、去掉空格、多选按字母排序避免因为「A,B」和「B,A」这种顺序差异误判。判分时逐题读取QuestionBank每个题目查一次库题目量 50 道以内没压力如果题库上万更优的做法是一次性filter(id__inanswers.keys())查出所有题目再组装成 dict内存换速度。浮点累计在题目多的情况下会有精度问题0.1 加 0.2 会得到 0.30000000000000004所以在最后round(total, 2)保留两位小数满足成绩单展示需求也避免数据库里存一长串小数。实际生产场景建议把ExamRecord.score字段类型换成DecimalField(max_digits5, decimal_places2)从根上解决浮点误差。3.3 前端答题交互倒计时和定时保存答题页templates/exam_paper.html的核心逻辑不复杂但坑多。页面加载时先向后端拿试卷快照并渲染题目倒计时用 localStorage 存结束时间戳防止用户刷新页面后重新计时。交卷前用 fetch 把答案 POST 给submit_exam同时保留一份本地备份const duration parseInt(document.getElementById(duration).value); const endTime localStorage.getItem(exam_end) || (Date.now() duration * 60000); localStorage.setItem(exam_end, endTime); const timer setInterval(() { const remain endTime - Date.now(); if (remain 0) { clearInterval(timer); submitExam(); } document.getElementById(timer).innerText formatTime(remain); }, 1000); function submitExam() { const answers {}; document.querySelectorAll(.question-item).forEach(item { const qid item.dataset.qid; const checked item.querySelector(input:checked); if (checked) answers[qid] checked.value; }); fetch(/exam/submit/${recordId}/, { method: POST, headers: {X-CSRFToken: getCookie(csrftoken)}, body: JSON.stringify({answers: answers}) }).then(res res.json()).then(data { if (data.code 0) location.href /exam/result/${recordId}/; }); }用 localStorage 而不是 sessionStorage 的原因localStorage 在浏览器关闭后仍然保留用户误关标签页重新打开exam_end还在继续用同一个截止时间如果用 sessionStorage关掉标签页就丢失重新打开会重新计时相当于考试可以无限续杯。向后端提交时用 fetch 加 JSON 格式比传统的 form 提交省去页面跳转也不影响答题状态。注意 Django 的 CSRF 中间件POST 请求必须带csrftoken从 cookie 里取出来后手动加到 header 里否则返回 403。4. admin 后台定制与防作弊设计让演示环节拿高分4.1 admin 后台的定制点直接裸用 Django admin 也能管理题库和成绩但展示效果偏素。这套项目在 admin.py 里做了定制成绩列表显示学生、试卷、得分、状态和交卷时间再加一个自定义筛选器按分数段过滤答卷。核心代码from django.contrib import admin from .models import ExamRecord admin.register(ExamRecord) class ExamRecordAdmin(admin.ModelAdmin): list_display (student_name, paper, score, status, submit_time) list_filter (paper, status) search_fields (student__username, paper__title) actions [reset_exam] def student_name(self, obj): return obj.student.username student_name.short_description 学生 admin.action(description重置为未考试状态) def reset_exam(self, request, queryset): queryset.filter(status2).update( score0, status1, submit_timeNone )list_display里不能直接写student__username这种跨表路径要自定义方法返回obj.student.username否则 admin 页面直接抛 FieldDoesNotExist。reset_exam这个 action 对毕设演示特别实用学生考完想再演示一遍完整流程直接在后台勾选记录点一下就能重置状态。search_fields里的双下划线写法是跨表搜索的标准写法可以用来按学号或用户名查人。如果觉得原生 admin 的样式不够好看可以引入第三方美化组件但有得必有失。评审时老师更关注功能链路是否完整admin 页面原生风格反而能强调「这是 Django 自带后台没有过度封装」建议保留原生外观把精力花在业务逻辑上。4.2 限时、防重与 IP 留痕在线考试演示环节最怕的场面是学生刷新页面重新做一遍或者开两个标签页同时答题。这两个行为必须在后端拦截不能只靠前端按钮禁用。设计一套状态机操作状态变化拦截策略进入考试无 → 1 进行中record 已存在且 status1 时直接返回试卷快照交卷1 → 2 已完成status2 时抛「已交卷请勿重复提交」重进已交卷考试2 保持不变只返回成绩单不允许重新作答倒计时归零1 → 2 已完成前端触发交卷后端校验 submit_time 不超过时限状态机之外IP 留痕是性价比很高的加分项。在ExamRecord上加一个ip_addr字段交卷时从request.META.get(REMOTE_ADDR)取客户端地址存进去。答辩时可以拿数据说明「系统记录了考生 IP同一 IP 多次登录不同账号会触发异常标记」虽然代码量不大但体现出了防作弊意识。4.3 往中间件里藏日志不改视图代码就能记录所有访问路径这是中间件的优势。在 Django 里写一个简单中间件把每个登录用户的访问行为落盘import logging logger logging.getLogger(exam.access) class ExamAccessLogMiddleware: def __init__(self, get_response): self.get_response get_response def __call__(self, request): if request.user.is_authenticated: logger.info( f{request.user.username} {request.method} {request.path} ) return self.get_response(request)中间件要注册到settings.py的MIDDLEWARE列表里放在最后一位即可。注意 logger 必须先在settings.py里配置 handlers 和 formatter日志级别设成 INFO 才会落盘否则默认只输出到控制台服务器重启后记录全丢答辩时拿不出有效日志就尴尬了。生产部署一般用 TimedRotatingFileHandler 按天切分日志文件避免单个文件无限膨胀。5. 部署瞬间的坑从本地到服务器的迁移记录5.1 连接 MySQL 的版本陷阱本地跑通后换到服务器卡住 90% 的人都是 mysqlclient。Django 连 MySQL 默认找 mysqlclient服务器上编译经常报mysql_config not found。常见做法是改用 pymysql在项目同名目录的__init__.py里做一层适配import pymysql pymysql.install_as_MySQLdb()install_as_MySQLdb()会把 pymysql 注册成 MySQLdb 的替身Django 的 MySQL 后端检测到 MySQLdb 存在就会放行。这个适配放在exam_system/__init__.py里确保项目启动时最先执行。相应的settings.py数据库配置DATABASES { default: { ENGINE: django.db.backends.mysql, NAME: exam_db, USER: root, PASSWORD: 换成你的密码, HOST: 127.0.0.1, PORT: 3306, OPTIONS: {charset: utf8mb4}, } }charset指定utf8mb4而不是utf8因为utf8mb4才能完整支持中文和 emojiMySQL 8.0 默认就是utf8mb4。如果坚持用 mysqlclient在 Ubuntu 服务器上先apt install libmysqlclient-dev再 pip install才是正常路径。5.2 静态文件与 nginx 托管DEBUGFalse之后最大的变化是 Django 不再托管静态文件页面全部裸奔。必须先把静态文件收集到一个统一目录pip install gunicorn python manage.py collectstatic --noinput gunicorn exam_system.wsgi:application -b 0.0.0.0:8000collectstatic会把 static 目录和 admin 自带的静态资源都复制到STATIC_ROOTnginx 再把这些文件直接返回给浏览器不用经过 Django减轻应用负载。nginx 配置里注意alias路径的结尾斜杠location /static/ { alias /www/wwwroot/online_exam/static/; }alias路径最后少写一个/CSS 文件会全部 404这个错误在浏览器控制台里看 Network 面板一眼就能定位。如果用宝塔面板部署 django 项目不要在终端里nohup python manage.py runserver应该用 gunicorn 或 uwsgi 接管否则进程被杀掉后服务就断了。宝塔的 Python 项目管理器会帮你维护 gunicorn 进程填启动命令gunicorn exam_system.wsgi:application -b 0.0.0.0:8000就行。5.3 数据备份与恢复演示完毕业设计库里攒了一堆测试账号和答题记录别让答辩前夜变成数据抢救夜。MySQL 在线考试系统最可靠的备份方式就是 mysqldumpmysqldump -u root -p exam_db exam_backup_$(date %Y%m%d).sql恢复时先建库再导数据注意字符集必须带 utf8mb4否则中文乱码mysql -u root -p -e CREATE DATABASE exam_db DEFAULT CHARSET utf8mb4; mysql -u root -p exam_db exam_backup_20250101.sql恢复完还要检查 Django 的迁移记录是否和数据库表结构同步跑一下python manage.py migrate --fake通常能把不一致的部分补上。然后重启 gunicorn重新登录后台确认题目和成绩列表都在数据备份恢复这套流程才算真正闭环。本文还有配套的精品资源点击获取
分享:

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

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