基于Python Django的在线考试系统:从表建模到自动评分实战解析

发布时间:2026/10/10 4:19:15
基于Python Django的在线考试系统:从表建模到自动评分实战解析 简介基于Python Django的在线考试系统设计与实现源码包面向计算机相关专业毕业设计、课程设计或需要快速搭建考试平台的开发者。系统采用管理员、教师、学生多角色权限体系实现用户注册与批量导入、班级课程关联、题库管理、手动/随机组卷、在线考试、自动阅卷及成绩统计等核心功能支持单选、多选、判断、填空、简答和编程题编程题可在线运行Python代码。整包含607个文件压缩包大小约21.63MB主要类型包括Vue前端组件与页面、JavaScript脚本、Python后端代码、SQL数据库脚本、图标与样式文件等另附数据库文档及一键安装/运行批处理脚本便于本地环境快速启动。已有67人学习浏览。资源内提供完整项目源码、数据库初始化脚本、批处理启动工具与目录结构说明尤其适合需要理解在线考试系统前后端交互、多角色权限设计或题库抽题算法的读者可作为毕业设计答辩、课程实验扩展或二次开发的实用参考。1. 在线考试系统这份基于 Python Django 的毕设资源半天能不能跑起来如果你正在做毕业设计或者课程作业手上这份「在线考试系统」大概率是你在 GitHub 或资源站里翻到的那一类Django 写后端自带数据库文档看起来五脏俱全。真正的问题是——你把它下下来之后能不能在半天内让它跑起来并且讲清楚每个表、每个接口在干什么。这套系统解决的是最典型的在线考试闭环学生登录、随机抽题、提交答卷、自动判分、教师管理题库和考试记录。它适合两类人一类是拿它当毕设底子需要快速起项目并二次开发另一类是课程作业交了想跑通完整流程给自己留一份可复现的实验记录。下文我按实际拆包顺序来写先说项目骨架再走业务链路、数据库落地最后讲我踩过的坑和一处值得升级的点。2. Django 项目的骨架从创建 app 到核心表的建模这一步决定后面所有代码的走向2.1 为什么选 Django 而不是 Flask 写在线考试在线考试系统的核心诉求是「用户认证 数据增删改查 后台管理」Django 恰好每一项都内置了。Flask 轻但你要自己拼 Flask-Login、Flask-Admin、ORM 选型毕业设计时间耗不起。Django 自带 Admin 后台题目、试卷、成绩单天然有可视化维护界面这在答辩演示时非常加分。另一个关键点是 Django ORM 的迁移机制。模型改完跑python manage.py makemigrations和migrate表结构就能同步到数据库不需要手写 SQL。对课程作业来讲这份基于 Python 在线考试系统的资源里models.py写得越规范你后续答辩时讲「数据表设计」就越省力因为可以拿着模型代码直接画出 ER 图讲关系。2.2 创建 Django 项目和考试应用拿到源码后我一般不会直接开跑而是先把项目结构和 app 对应关系摸清楚。常见做法是这个项目里至少有两个组成部分一个叫exam或online_exam的主应用负责考试业务一个通过startapp生成。如果你是自己从零搭命令如下django-admin startproject exam_project cd exam_project python manage.py startapp exam python manage.py startapp accountsexam应用放题目、试卷、考试记录相关的模型和视图accounts放用户扩展信息。登录认证我建议直接用 Django 自带的django.contrib.auth它已经包含了用户表、会话管理、密码哈希毕设场景不需要自己写注册登录逻辑的加密部分。项目根目录下的settings.py要把这两个 app 注册进INSTALLED_APPS否则迁移时表建不出来这是新手最容易漏的第一步。2.3 核心表建模五张表把在线考试链路串起来考试系统最核心的数据关系是「用户—试卷—题目—答题记录」。下面这份models.py是我在多个 Django 在线考试系统里都会用的结构字段做了精简但覆盖了完整链路from django.db import models from django.contrib.auth.models import User class Course(models.Model): 课程表题目和试卷都挂在课程下面 name models.CharField(课程名称, max_length64) def __str__(self): return self.name class Question(models.Model): 题库表单选客观题答案只存一个字母 course models.ForeignKey(Course, on_deletemodels.CASCADE, related_namequestions) stem models.TextField(题干) choice_a models.CharField(选项A, max_length255) choice_b models.CharField(选项B, max_length255) choice_c models.CharField(选项C, max_length255) choice_d models.CharField(选项D, max_length255) answer models.CharField(正确答案, max_length1, choices[ (A, A), (B, B), (C, C), (D, D)]) score models.IntegerField(分值, default5) class Meta: verbose_name 题目 verbose_name_plural 题目 class ExamPaper(models.Model): 试卷表与题目是多对多支持随机组卷 title models.CharField(试卷名称, max_length64) course models.ForeignKey(Course, on_deletemodels.CASCADE) questions models.ManyToManyField(Question, related_namepapers) duration models.IntegerField(考试时长(分钟), default60) class ExamRecord(models.Model): 考试记录表每次交卷写一条保留答题明细 user models.ForeignKey(User, on_deletemodels.CASCADE, related_namerecords) paper models.ForeignKey(ExamPaper, on_deletemodels.CASCADE) score models.IntegerField(得分, default0) answers models.TextField(答题明细(JSON字符串)) submitted_at models.DateTimeField(交卷时间, auto_now_addTrue)这里的字段选择有几个讲究。Question的answer字段用长度为 1 的CharField而不是IntegerField是为了和选项字母一一对应判分时直接拿用户提交的答案和它比对避免类型转换ExamRecord里存一份answersJSON 文本是为了复盘错误题目时能有原始数据而不是只看到总分。on_deletemodels.CASCADE在课程和题目之间是合理的课程删除时题目跟着走符合直觉。但第五章我会讲到在部分场景下你需要把两个外键改成PROTECT或SET_NULL否则误删考试记录会连坐一堆业务数据。这是 Django 执行查询-删除对象时最容易翻车的地方。2.4 数据库配置项目里那份数据库文档对应的是什么库这套系统既然标注「含数据库文档」核心配套物通常是一个 SQL 导出脚本.sql文件加上一份表关系说明用来描述表之间的外键联系。你需要在settings.py里把默认 SQLite 换成 MySQL 或依然保留 SQLite取决于数据库文档里给的是什么。SQLite 适合演示MySQL 更贴近毕设要求的「真实数据库」。# settings.py 中的数据库配置MySQL 场景 DATABASES { default: { ENGINE: django.db.backends.mysql, NAME: exam_db, USER: root, PASSWORD: 你的密码, HOST: 127.0.0.1, PORT: 3306, OPTIONS: {charset: utf8mb4}, } }utf8mb4是一个必填项。如果漏掉它导入题库后中文十有八九会变成乱码因为 MySQL 默认字符集utf8mb3无法完整表达一些生僻字和表情符号选项内容一旦含特殊字符就会整体报错。数据库文档里如果给的建表脚本注释写着 utf8你在 Django 这一侧统一用 utf8mb4 更保险。3. 核心业务链路登录鉴权、随机抽题与自动评分三个接口把考试闭环跑通3.1 登录鉴权直接用 Django 的 auth 还是另建学生表在线考试系统的用户身份有两类学生和教师。教师进 Admin 后台管理题库学生走前台考试。我的做法是不重写用户表而是用Profile方式扩展from django.db import models from django.contrib.auth.models import User class Profile(models.Model): user models.OneToOneField(User, on_deletemodels.CASCADE) student_id models.CharField(学号, max_length32, blankTrue) role models.CharField(角色, max_length16, defaultstudent, choices[(student, 学生), (teacher, 教师)])OneToOneField关联原生的auth.User这样密码加密、登录 session 全部走 Django 自带机制不用自己处理安全问题。登录视图直接用 Django 提供的login(request, user)函数代码量极少且不容易出逻辑漏洞。有个点需要提前设计如果你继承了别人的项目先看他有没有在settings.py里自定义AUTH_USER_MODEL。如果定义了自定义用户模型数据库文档里的auth_user表就不存在而是替换成了项目自己的用户表此时按原生User导入数据会直接报表不存在。3.2 随机抽题一次查询替你省掉一整个 Python 循环抽题逻辑是这套系统的核心卖点。在线考试系统最常见的组卷方式是指定课程和题量从题库随机抽取 N 道题组成一张临时试卷。用 Django ORM 实现非常短import random from .models import Course, ExamPaper, Question def generate_paper(course_id, question_count20): course Course.objects.get(idcourse_id) # PostgreSQL 场景可以直接用 order_by(?)MySQL 场景同样支持但数据量大时会慢 questions list(Question.objects.filter(coursecourse).order_by(?)[:question_count]) paper ExamPaper.objects.create(titlef{course.name} 随机卷, coursecourse) # 多对多字段用 set() 一次绑定避免循环 add paper.questions.set(questions) return paperorder_by(?)是 ORM 层面的随机排序它会让数据库每次按随机顺序返回记录配合切片[:question_count]就能实现无放回抽题。数据库文档里如果试卷表有多对多关系就一定会有类似exam_exampaper_questions这样的中间表set()方法帮你一次性写入全部关联而不是一条条add性能差别明显。这里的参数要按实际场景调如果题库超过几千条且并发考试多ORDER BY RAND()在 MySQL 上会全表扫性能下降。我一般会加一个前置条件先把符合条件的题目主键取出来随机洗牌再取前 N 个或者按难度比例分层抽。毕设规模下order_by(?)足够但答辩时能主动说出这个性能边界是加分项。3.3 自动评分客观题判分的三个必须注意的边界条件客观题的判分逻辑看着简单实际容易漏三种情况未作答、答案多选、大小写不一致。下面这段评分代码处理了这些边界import json from .models import ExamRecord def score_paper(paper, user_answers, student): total_score 0 detail [] for q in paper.questions.all(): submitted user_answers.get(str(q.id), ).strip().upper() # 未作答时 submitted 为空字符串不能计入正确 if submitted and submitted q.answer.upper(): total_score q.score detail.append({ question_id: q.id, correct_answer: q.answer, student_answer: submitted, is_right: submitted q.answer.upper(), }) record ExamRecord.objects.create( userstudent, paperpaper, scoretotal_score, answersjson.dumps(detail, ensure_asciiFalse) ) return recordstrip()去掉前端传过来的空格upper()统一答案字母大小写这两步能消灭大量人为录入造成的判分错误。detail列表里同时存了正确答案、学生答案和每题正误后期做错题分析直接读record.answers这个 JSON 字段不用再回查题库表。3.4 查询性能select_related 到底要不要用如果你要在试卷详情页同时展示题目所属课程名就会遇见 N1 查询问题。一次循环查 20 道题每道题回查一次课程表总共 21 条 SQL。加一行select_related可以把 21 条变成 1 条questions paper.questions.select_related(course).all()这条关联查询在Question和Course是一对多关系时有效因为外键在Question表上。而ExamPaper和Question是多对多这一侧就无法用select_related直接合并只能配合中间表查询。毕设规模下 impact 不大但如果成绩单页面要列考试标题、课程名、学生名三个关联表的信息用select_related和prefetch_related的实际体感差异会非常明显。4. 数据库文档与数据落地把 ER 关系变成可运行的题库数据4.1 数据库文档到底在帮你省什么事这份资源附带的数据库文档拆开来看核心是三个部分ER 图或关系描述、CREATE TABLE建表语句、字段说明表。它的作用是让你在不读全部代码的前提下先回答答辩老师最常问的三个问题表之间什么关系、主外键怎么约束、核心字段存了什么。看文档时先对号入座auth_user对应学生用户、course对应课程、question对应题目、exam_paper对应试卷、exam_paper_questions是多对多中间表、exam_record是考试记录。这六张表就是整个在线考试系统的数据库骨架。文档里如果有视图View或存储过程通常是成绩统计类逻辑读一下就好不必在 Django 里重写直接用 ORM 聚合查询更简单。4.2 从 SQL 脚本导入 MySQL字符集与主键自增的注意点如果你决定按数据库文档里的 SQL 脚本导入不要直接一把梭执行先处理两个参数。第一是建库时指定字符集第二是确认自增主键没有被写死CREATE DATABASE exam_db DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci; USE exam_db; -- 确认每张表的主键是否为 AUTO_INCREMENT -- 例如id INT NOT NULL AUTO_INCREMENT PRIMARY KEY导入完成后用python manage.py inspectdb反向生成模型代码是一个很实用的验证手段。它能把 MySQL 里的表结构扫描成 Django models你拿生成的代码和项目原有的models.py对比字段对得上说明数据库文档和 Django 模型是一致的对不上说明有一侧改过没同步这是这份资源最容易出现的老化问题。4.3 题库初始化Excel 批量导入脚本数据库文档给你的往往是表结构不包含题目数据。真实交付的在线考试系统一般会附带一份 Excel 题库模板。我习惯写一个独立脚本处理导入不写进视图避免每次启动项目都跑一遍from openpyxl import load_workbook from .models import Course, Question def import_questions_from_excel(file_path): wb load_workbook(file_path) ws wb.active course_cache {} created_count 0 for row in ws.iter_rows(min_row2, values_onlyTrue): # 表格列顺序课程名, 题干, A, B, C, D, 答案, 分值 course_name, stem, a, b, c, d, answer, score row[:8] if course_name not in course_cache: course, _ Course.objects.get_or_create(namecourse_name) course_cache[course_name] course Question.objects.create( coursecourse_cache[course_name], stemstem, choice_aa, choice_bb, choice_cc, choice_dd, answeranswer.strip().upper(), scoreint(score) if score else 5, ) created_count 1 print(f成功导入 {created_count} 道题目)course_cache字典用于缓存课程对象避免几千行数据每行都查一次课程表get_or_create按课程名去重重复执行脚本不会生成重复课程。答案字段统一strip().upper()和第三章评分逻辑保持一致这是整个系统里最容易被忽略的一致性要求。4.4 端到端联调用一条最小链路验证系统可用数据导入完成后不要直接进 Admin 后台逐条录入先走一遍最小链路验证。我的顺序是创建教师账号进 Admin → 创建一门课程 → 导入 Excel 题库 → 创建一张试卷并绑定题目 → 创建学生账号 → 学生登录前台 → 参加考试 → 交卷看分数。任何一步报错都优先去看 Django 日志里最近一次 500 报错的堆栈。注意一个常见误解Admin 后台创建用户时密码选项要有「实际密码」和「密码确认」两栏很多新手只在第一栏输入就保存结果登录时报密码错误。Django 的 User 模型要求密码必须哈希存储Admin 的表单里已经处理好这个逻辑但如果你在图省事直接往数据库里插了一条明文密码记录login函数是无法校验的。5. 避坑清单这份资源最容易翻车的 6 个位置5.1 PyCharm 打开别人的 Django 项目报「No module named django」现象源码在 GitHub 或课程设计包里看起来结构完整用 PyCharm 打开后运行python manage.py runserver直接提示找不到模块。原因PyCharm 默认使用了它自带的虚拟环境而不是项目原来的环境。项目交付方用的 Python 版本、第三方依赖和你当前环境不匹配。解决要求项目里有requirements.txt执行pip install -r requirements.txt安装全部依赖。如果没有这个文件至少需要手动安装 Django、openpyxl、mysqlclient 或 pymysql视项目里settings.py的数据库驱动而定。安装完成后在 PyCharm 右下角把解释器切换到当前虚拟环境。5.2 执行 migration 时提示「table already exists」现象项目自带 SQL 导入脚本你先导入数据库再跑python manage.py migrate结果提示某张表已存在迁移中断。原因Django 的migrate会检查django_migrations表里的迁移记录但你手动导入 SQL 时并没有记录这些历史Django 尝试重新建表。解决两种方案选一。要么不手动导 SQL直接用migrate建表然后用代码或 Admin 数据导入要么导出 SQL 前就把django_migrations里的迁移记录一起还原让 Django 认为这个库已经是迁移过的状态。我更推荐前者因为交付的数据库文档用途是给你看表结构不是替代migrate。5.3 中文乱码题目导入后全是问号现象Excel 导入几百道题前台显示全部是「????」或者干脆导入时报Incorrect string value错误。原因数据库连接字符集没设置MySQL 表实际用的字符集不是 utf8mb4。Excel 里的中文没问题坏在写入数据库的那一环。解决第一步settings.py的OPTIONS里加charset: utf8mb4第二步检查 MySQL 中该库的默认字符集第三步如果你已经建过表把表的字符集也改掉。命令行一条命令搞定ALTER TABLE exam_question CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;5.4 题库重复导入同一道题出现了两遍现象导入脚本跑了两次后台题目翻倍试卷里出现一模一样的题干。原因Question表没有唯一性约束导入脚本每次都无脑create。解决给Question加一个基于题干的唯一约束或在导入脚本里先查重。团队交付场景我推荐在模型上加约束因为单靠脚本无法阻挡多人同时维护数据class Question(models.Model): ... class Meta: constraints [ models.UniqueConstraint(fields[course, stem], nameuq_course_stem) ]加了约束后重复导入会直接报IntegrityError脚本里用get_or_create或捕获异常跳过即可。5.5 考试中途刷新页面答题记录全丢现象学生答到一半点了刷新已选答案全部清空重新进入试卷报错或需要从头再答。原因答题进度存在 session 里而 session 默认保存在数据库的django_session表中刷新时如果 session 过期或被重新生成已存的答案就取不回来了。解决把答题明细设计成「逐题即时保存」或者「刷新后从ExamRecord恢复」。最稳妥的做法是考试开始时就创建一条ExamRecord草稿记录每次提交一道题就update一次answers字段刷新页面后从数据库恢复。session 适合存临时状态不适合当唯一数据源。5.6 删除课程时连坐了全部题和成绩现象在 Admin 后台删了一门测试课程结果该课程下几百道题和几百条考试记录一并消失。原因第三章的模型设计里Question.course和ExamRecord.paper都用了on_deletemodels.CASCADE删除主表自动级联删子表。解决根据业务判断重新选策略。考试记录属于历史数据应当用PROTECT阻止删除题目可以用CASCADE因为课程没了题目确实没有意义。修改模型class ExamRecord(models.Model): user models.ForeignKey(User, on_deletemodels.CASCADE) paper models.ForeignKey(ExamPaper, on_deletemodels.PROTECT)这个改动背后的逻辑是成绩一经产生就属于审计数据不能因为删了一张试卷就连坐消失。这也是 Django 执行查询-删除对象时我强烈建议你重新过一遍所有外键的原因。6. 进阶玩法把题库导入过程写成可复用脚本给答辩和后续扩展留足余地这套资源跑通之后值得花时间做的一件事是给系统增加一套「管理命令式」的批量操作脚本而不是只在函数里写。Django 官方支持自定义manage.py命令你可以把自己写的 Excel 导入、清空题库、成绩导出都封装进去好处是答辩现场演示时不用在 Python shell 里敲一长串代码一行命令就能完成。在exam/management/commands/目录下建立两个文件__init__.py和import_questions.py。命令行运行python manage.py import_questions 题库.xlsx逻辑和第四章那段函数一致但被 Django 统一调度还能通过--dry-run参数预览将要导入多少条数据而不真正写入。在线考试系统在毕业设计中经常被要求演示数据导入能力这个脚本比 Postman 调接口更直观。最后的埋点我建议放在 Admin 后台注册上。项目里模型建好了但 Admin 如果没有注册你在后台看不到课程、题目和试卷的入口只能靠前台硬操作。加一段注册代码整个系统从数据维护到业务演示全部打通from django.contrib import admin from .models import Course, Question, ExamPaper, ExamRecord admin.register(Course) class CourseAdmin(admin.ModelAdmin): list_display (id, name) admin.register(Question) class QuestionAdmin(admin.ModelAdmin): list_display (id, course, stem, answer, score) list_filter (course, answer) search_fields (stem,)从那以后我每次拿到一份 Django 的课程设计资源都会强制走一遍「建库 → 迁移 → 导入 → 跑一次考试 → 后台看数据」的完整闭环再开始改代码。这套动作已经成了我的习惯因为它能在十分钟内暴露资源里百分之八十的坑。希望帮到你。本文还有配套的精品资源点击获取