
简介这是一套完整的基于Django框架开发的Python在线考试系统面向高校计算机专业学生、毕业设计开发者及课程作业实践者解决多角色协同考试管理、结构化题库建设与智能组卷落地等核心教学场景需求。资源包共607个文件涵盖56个Python后端逻辑文件、123个Vue前端组件、63个JS交互脚本、53个JPG/PNG界面素材、159个SVG图标资源以及SQL数据库脚本、运行与初始化批处理文件如install.bat、run.bat、初始化hive数据库.bat等整体压缩包大小为21.63MB。目前已有66人学习下载。用户可直接部署运行获得含管理员/教师/学生三端权限体系、支持图文混排题干与Python代码在线判题的完整题库模块、按课程-章节-知识点三级分类的检索能力以及手动随机双模式组卷功能配套提供清晰目录结构、数据库文档与可执行环境脚本显著降低二次开发与教学演示门槛。1. 项目背景与核心需求拆解聊到在线考试系统很多人第一反应是“不就是把纸质试卷搬到网上吗”。真做起来才发现一次完整的线上考试涉及考生认证、试卷生成、答题计时、自动判分、防作弊策略、异常恢复等多个环节任何一个环节掉链子考试现场就是灾难现场。我这次做的是一个基于 Django Python 的在线考试系统整套从零设计包含数据库建模、后端接口、管理后台和考生端最终把数据库文档一并补齐。选 Django 而不是 Flask核心原因是 Django 的 ORM、Admin 后台、认证体系、迁移机制都是现成的考试系统这种重数据、重权限管理的场景Django 能省掉大量重复造轮子的时间。Flask 灵活但灵活意味着你得自己组装太多东西对业务系统来说没必要。这个系统能做什么先简单划个重点管理员端维护题库、创建试卷、发布考试、查看成绩统计考生端登录后查看已发布考试、在线答题、提交试卷、实时查看客观题得分系统端题目乱序、选项乱序、考试计时、自动交卷、防切屏检测、异常中断恢复。适合谁来参考如果你是学生正在做毕业设计或课程项目这套系统的设计思路和代码结构可以作为完整底座如果你是刚转 Python 后端的开发者想了解一个实际业务系统怎么从零落地这篇也能帮你梳理 Django 项目在真实需求下怎么拆模块、怎么设计模型、怎么处理并发和异常。我自己在开发过程中踩了不少坑比如题目乱序和选项乱序的实现细节、并发提交时的状态校验、数据库表的级联关系设计这些在普通教程里基本不会写我会在后面的章节里把思路和代码一起拿出来。先说结论在线考试系统最核心的不是“考试”本身而是考试过程的可靠性。一次线上考试可能同时有几百人参与涉及到计时、提交、断网重连这些才是真正考验系统设计能力的地方。2. 整体架构与核心流程设计2.1 技术选型为什么是 Django SQLite/MySQL技术选型这块我先列一个对比表把我当时筛选的几个方案放在一起看方案优点缺点适用场景Django SQLite零配置、上手快、适合开发调试并发写入能力弱不适合生产环境毕业设计、原型验证Django MySQL通用性强、事务支持好、运维资料多需要额外安装配置数据库服务中小型正式项目Flask SQLAlchemy轻量灵活、自由度高认证、Admin、迁移都要自己搭接口服务、微服务FastAPI Tortoise ORM异步性能好、自动生成API文档生态相对Django弱Admin缺失高并发API服务我最终选了 Django MySQL原因有三第一Django 自带的 User 模型和认证体系可以直接扩展考生和管理员用同一套权限框架省去重复开发。第二Django ORM 的迁移机制能让我随时调整表结构而不丢数据这对开发中途频繁改需求的情况特别友好。第三Django Admin 后台几乎是白送的题库管理界面开发阶段直接用它录入题目等业务稳定后再写定制化前端也不迟。2.2 系统模块划分整个系统我拆成了七大模块这样无论是单人开发还是团队协作分工都很清晰用户模块注册、登录、角色区分管理员/考生、个人信息维护题库模块单选题、多选题、判断题三种题型的增删改查按科目分类试卷模块手动组卷和规则组卷两种模式题目从题库中抽取考试模块发布考试、设置考试时间、设置时长、设置及格分答题模块考生在线答题、实时保存、交卷判分模块客观题自动判分主观题预留人工评分入口统计模块成绩查询、及格率统计、分数分布。每个模块在 Django 里对应一个 app项目结构长这样exam_system/ ├── manage.py ├── config/ # 项目配置settings/urls ├── apps/ │ ├── users/ # 用户模块 │ ├── questions/ # 题库模块 │ ├── papers/ # 试卷模块 │ ├── exams/ # 考试模块 │ ├── answers/ # 答题模块 │ └── statistics/ # 统计模块 ├── static/ # 静态资源 ├── templates/ # 前端模板 └── docs/ └── database_design.md # 数据库设计文档模块化设计的好处是可以独立迭代。比如后期想加一个“随机组卷”的算法只需要改 papers 模块的内部逻辑其他模块完全不用动。这种隔离性在项目后期维护时太重要了。2.3 核心流程时序拆解考试的核心流程比想象中复杂我梳理出两条主链路考生考试链路考生登录 → 查看可参加考试列表 → 点击开始考试 → 生成试卷快照锁定题目版本 → 进入答题页面启动计时 → 逐题作答自动保存答案到缓存表 → 点击交卷 / 时间耗尽自动交卷 → 客观题即时判分 → 成绩写入成绩表 → 返回成绩单管理员组卷与发布链路管理员登录 → 录入题目 → 创建试卷选择题目或设置抽题规则 → 设置考试基本信息名称/时长/开始结束时间/及格分 → 发布考试 → 考生端可见 → 考试结束 → 查看统计报表注意这里有个容易被忽略的点考生点击“开始考试”时系统不是直接引用题库里的题目而是生成一份“试卷快照”。为什么这么做因为如果管理员在考试进行中修改了题目内容或答案考生的作答就会对不上。快照机制保证了考试过程中题目版本的一致性。3. 数据库设计与建模实战3.1 ER 分析与模型设计数据库是整个系统的地基我花的时间最多。先理清楚实体之间的关系一共七个核心表User用户表Django 内置 User 扩展增加角色字段Subject科目表题目所属科目Question题目表题目内容、题型、选项、正确答案、分值Paper试卷表试卷基本信息和总分PaperQuestion试卷题目关联表试卷和题目的多对多关系附带每题分值Exam考试表考试的发布时间范围、时长、及格分ExamPaper考试试卷关联表一场考试关联一份或多份试卷AnswerRecord答题记录表记录考生的答案明细ExamResult考试成绩表记录每场考试考生的总分和状态。这里最关键的设计是PaperQuestion 中间表。为什么不直接在 Question 表里加一个 paper_id 外键因为一道题可以出现在多份试卷里一份试卷里也可以有多个相同题型的题目这是典型的多对多关系。中间表还能额外存储“该题在该试卷中的分值”因为同一道题在不同试卷的分值可能不一样放中间表最合适。各表的核心字段用下面的代码展示这本身也是数据库文档的一个重要部分# apps/questions/models.py from django.db import models from django.contrib.auth.models import AbstractUser class User(AbstractUser): ROLE_CHOICES ((admin, 管理员), (student, 考生)) role models.CharField(max_length10, choicesROLE_CHOICES, defaultstudent) student_no models.CharField(max_length20, blankTrue, nullTrue) class Subject(models.Model): name models.CharField(max_length50, uniqueTrue) description models.TextField(blankTrue) created_at models.DateTimeField(auto_now_addTrue) class Question(models.Model): TYPE_CHOICES ((single, 单选题), (multiple, 多选题), (judge, 判断题)) subject models.ForeignKey(Subject, on_deletemodels.CASCADE, related_namequestions) qtype models.CharField(max_length10, choicesTYPE_CHOICES) content models.TextField(verbose_name题干) options models.JSONField(verbose_name选项格式{A: ..., B: ...}) # 判断题固定A/B answer models.JSONField(verbose_name正确答案单选/判断为[A]多选为[A,B]) score models.IntegerField(default5, verbose_name默认分值) difficulty models.IntegerField(default3, verbose_name难度1-5) created_at models.DateTimeField(auto_now_addTrue)# apps/papers/models.py class Paper(models.Model): name models.CharField(max_length100) total_score models.IntegerField(default100) duration models.IntegerField(help_text推荐时长/分钟仅作为参考) created_at models.DateTimeField(auto_now_addTrue) class PaperQuestion(models.Model): paper models.ForeignKey(Paper, on_deletemodels.CASCADE, related_namequestions) question models.ForeignKey(Question, on_deletemodels.CASCADE) score models.IntegerField(default5, verbose_name该题在试卷中的分值) order models.IntegerField(default0, verbose_name题号顺序) class Meta: unique_together (paper, question)3.2 题库与试卷表的关键设计细节题目表里我用了 JSONField 来存选项和答案这是 Django 3.1 的特性。传统做法是把选项拆成四个字段option_a、option_b但那样扩展性极差——万一题目需要五个选项呢JSON 字段把选项和答案都存成一个结构前端渲染时直接遍历后端判分时直接对比整个逻辑都被简化了。多选题的答案是列表结构存放的时候是 JSON 数组判分时要特别注意顺序问题。考生答案[A,C]和[C,A]应该判对所以判分时不能直接比较字符串要先排序再比对def check_multi_answer(user_answer, correct_answer): return sorted(user_answer) sorted(correct_answer)这个细节我在第一次测试判分时踩了坑。直接用比较两个 JSON 数组发现顺序不同的答案被判错当时还以为是系统 bug。排完序之后就一切正常了。3.3 数据库文档的结构与价值我单独写了一份完整的数据库文档放在项目 docs 目录下内容不只有建表 SQL还包括每个表的业务含义和字段说明表之间的关联关系图文字版关键索引设计常用查询语句示例数据字典和枚举值定义。数据库文档的价值在开发中后期才体现出来。项目里要加一个“按科目统计考试通过率”的功能如果数据库文档里已经有 Subject、Exam、ExamResult 的关联说明和字段含义我五分钟就能写出正确的 ORM 查询如果没有文档就得翻代码看模型定义每张表翻一遍浪费的时间够写三个功能了。索引设计这块我特别提醒一下。考试结果表按exam_id和student_id查询最频繁所以一定要给这两个字段建联合索引class Meta: indexes [ models.Index(fields[exam, student]), ]别小看这个索引。没有索引时一张十万条记录的成绩表做查询可能要几百毫秒加了索引后直接毫秒级返回。考试结束后的成绩统计报表连续查十几场考试差别非常明显。4. 核心功能模块的实现4.1 用户认证与权限控制用户认证直接基于 Django 自带的 AbstractUser 扩展它已经带了 username、password、email、first_name、last_name 等基础字段。我增加了一个 role 字段来区分管理员和考生。但是只加字段不够还需要在权限控制上下功夫。Django 的权限系统是基于模型的默认有 add、change、delete、view 四种权限。我的做法是自定义一个IsAdminUser权限类通过装饰器或 Mixin 控制视图访问# apps/users/decorators.py from django.core.exceptions import PermissionDenied def admin_required(func): def wrapper(request, *args, **kwargs): if request.user.is_authenticated and request.user.role admin: return func(request, *args, **kwargs) raise PermissionDenied return wrapper使用方式很简单login_required admin_required def question_create(request): ...这样管理员后端的每个操作都做了角色校验。这里的关键点是权限校验一定要放在视图入口处而不是依赖前端隐藏按钮。前端可以隐藏入口但后端必须拦截请求否则用户直接构造 URL 就能越权访问。4.2 题库管理模块三种题型的存储与渲染题库管理是考试系统的基础。单选题、多选题、判断题虽然类型不同但存储结构可以统一用 Question 表搞定区别只在于 qtype 字段和 answer 字段的格式。渲染题目时前端根据 qtype 决定控件类型单选题radio 单选框多选题checkbox 复选框判断题radio 单选框选项固定为“正确/错误”。判断题的选项我直接在 options 字段里统一存成{A: 正确, B: 错误}这样渲染逻辑和单选题完全一致前端代码少写一套分支。在管理后台录入题目时通过 Django Admin 自带的 JSON 编辑功能可以方便地录入选项和答案。但要注意Admin 里对 JSON 字段的校验非常弱必须在 Model 的 clean 方法里增加自定义校验def clean(self): if not self.options: raise ValidationError(选项不能为空) if self.qtype judge: required {A: 正确, B: 错误} if self.options ! required: raise ValidationError(判断题选项必须是固定格式)这个细节帮你避免了 90% 的脏数据。不校验的话题目录进去之后发现选项格式是错的考生端直接白屏而且问题很难排查。4.3 试卷生成手动组卷与规则组卷试卷生成我实现了两种模式手动组卷管理员从题库中逐题选择加入试卷可以自定义每题顺序和分值。实现方式是在 PaperQuestion 表里增加一个 order 字段管理员每次添加题目时order 自动递增。规则组卷管理员设定规则科目、题型数量、每种题型分值系统自动从题库抽题。抽取算法要考虑随机性避免每次生成同一套试卷def auto_generate_paper(subject_id, rules): paper Paper.objects.create(namerules[name]) for rule in rules[question_rules]: qtype rule[qtype] count rule[count] score rule[score] questions list( Question.objects.filter(subject_idsubject_id, qtypeqtype) .order_by(?)[:count] ) # order_by(?) 随机取数据量大时性能差可优化为随机ID范围 for idx, q in enumerate(questions): PaperQuestion.objects.create( paperpaper, questionq, scorescore, orderf{rule[section]}_{idx1} ) return paper这里有个性能问题要提一下order_by(?)在数据量小的时候没问题但题库超过几万条时这个查询会让数据库全表扫描并随机排序性能非常差。优化方案是先查出符合条件的 ID 列表然后在 Python 里用 random.sample 抽取再批量查询题目详情import random total_questions Question.objects.filter(subject_idsubject_id, qtypeqtype).values_list(id, flatTrue) selected_ids random.sample(list(total_questions), min(count, len(total_questions))) questions Question.objects.filter(id__inselected_ids)这样就算题库有十万条题查询也是走索引的性能完全可控。4.4 在线考试流程计时、快照、交卷与自动判分在线考试是系统的核心环节我把它拆成了几个状态来管理未开始考生能看到考试信息但不能进入答题页进行中考生进入答题页系统启动倒计时已交卷考生主动交卷或系统自动交卷进入判分已过期考试结束时间已过未提交的试卷按弃考处理。考试过程的数据流设计要重点讲一下。考生每道题作答后前端通过 Ajax 调用保存接口把答案写入 AnswerRecord 表。这样做的目的有三个一是防止考生答题过程中浏览器崩溃、断网导致数据丢失二是服务端随时知道考生的答题进度三是交卷时只需要把 AnswerRecord 里这道题的最新答案取出来判分不用依赖前端传回的数据。前端代码的核心逻辑是这样的// 每答完一题立即保存 function saveAnswer(questionId) { const selected getSelectedOptions(questionId); fetch(/api/exam/save_answer/, { method: POST, headers: { X-CSRFToken: csrftoken }, body: JSON.stringify({ exam_id: examId, question_id: questionId, answer: selected }) }); }后端保存接口只需要做简单的校验和写入login_required def save_answer(request): data json.loads(request.body) exam_id data[exam_id] question_id data[question_id] answer data[answer] # 校验考试是否在进行中 exam Exam.objects.filter(idexam_id, statusongoing).first() if not exam: return JsonResponse({code: 40001, msg: 考试不在进行中}) record, _ AnswerRecord.objects.update_or_create( examexam, studentrequest.user, question_idquestion_id, defaults{answer: answer} ) return JsonResponse({code: 0})交卷时系统遍历这场考试的所有题目对每道题做判分然后汇总总分def grade_exam(exam, student): records AnswerRecord.objects.filter(examexam, studentstudent) records_by_qid {r.question_id: r.answer for r in records} paper_questions PaperQuestion.objects.filter(paperexam.paper) total 0 detail [] for pq in paper_questions: q pq.question user_answer records_by_qid.get(q.id) is_correct check_answer(q, user_answer) if is_correct: total pq.score detail.append({question_id: q.id, correct: is_correct, score: pq.score}) result, _ ExamResult.objects.update_or_create( examexam, studentstudent, defaults{score: total, status: graded, detail: detail} ) return result5. 防作弊、异常恢复与多端适配5.1 防切屏策略为什么不能只靠前端在线考试的作弊防范是个大话题我实现的是一套“前端限制 后端记录”的配合方案。前端在考试页面监听 document 的 visibilitychange 事件当页面被切换到其他标签页时记录一次“切屏记录”document.addEventListener(visibilitychange, () { if (document.hidden) { fetch(/api/exam/record_switch_tab/, { method: POST, headers: { X-CSRFToken: csrftoken }, body: JSON.stringify({ exam_id: examId }) }); } });后端把切屏次数记录到 ExamResult 表的字段里管理员在后台可以查看。如果切屏次数超过系统设置的阈值比如三次标记该考生的试卷为“待人工审核”。这里强调一下防作弊不能只依赖前端。前端检测切屏只是提示和记录真正的限制要在后端做比如考试中限制 IP、限制登录设备数、记录答题时间间隔异常等。我实现了同一个考试账号只允许一个会话登录登录时校验其他会话是否活跃这个功能在防止“多人共用一账号”时很有效。5.2 异常情况处理断网、刷新、超时实操中最常见的场景是考生答到一半断网或者不小心刷新了页面。这时候如果答题记录没有实时保存考生就得重做整张卷子体验非常差。我的处理方式是双保险服务端实时保存每答一题都调用接口保存前面已经讲过前端本地缓存在 localStorage 里再存一份答案作为本地备份。当页面刷新或者断网重连后前端先比较本地缓存和服务端记录如果本地有服务端没有的答案自动补齐window.addEventListener(load, () { const localAnswers JSON.parse(localStorage.getItem(exam_ examId) || {}); Object.entries(localAnswers).forEach(([qid, answer]) { syncAnswerToServer(qid, answer); }); });至于超时自动交卷我在前端实现了倒计时同时后端也有一个定时任务兜底。每次考生进入答题页时服务端记录一个 start_time交卷时如果当前时间超过 start_time duration即使考生没主动交卷后端也会强制交卷并判分。这种双保险防止了前端倒计时出现偏差导致考生超时未交的情况。5.3 题目乱序与选项乱序的实现考试防作弊还有一个常用手段是题目乱序和选项乱序。给不同考生生成同一份试卷的乱序版本邻座考生看到的题目顺序和选项顺序不一样能有效减少偷看。题目乱序在生成试卷快照时实现。每个考生点击“开始考试”时针对这场考试生成一份“考生专属试卷记录”把题目的 order 打乱def generate_student_snapshot(exam, student): paper_questions list(exam.paper.questions.all().order_by(order)) random.shuffle(paper_questions) for idx, pq in enumerate(paper_questions): StudentExamQuestion.objects.create( examexam, studentstudent, paper_questionpq, answer_orderidx 1 )选项乱序稍微复杂一点。因为题目表里的选项存的是 JSON 对象比如{A: 苹果, B: 香蕉}乱序的时候需要把选项键值对打乱并生成一个新的映射然后缓存起来def shuffle_options(question): items list(question.options.items()) random.shuffle(items) shuffled {chr(65 i): val for i, (_, val) in enumerate(items)} # 同时还要保存一份原始选项到乱序选项的映射用于判分时反向转换 return shuffled这里有个坑乱序后考生的答案A已经不代表原来的选项 A 了。所以判分时不能直接用原始答案去比对而是要把考生的选项答案先通过映射关系转换成原始选项再和正确答案比对。我当时的做法是在 StudentExamQuestion 表里额外存一份option_mapping字段记录乱序后的映射关系。5.4 多端适配的技术方案在线考试系统的用户场景很杂有考生用电脑浏览器、有考生用手机浏览器访问。我用了 Bootstrap 做响应式布局考试页面的答题区域、倒计时面板、交卷按钮都做了移动端适配电脑端左侧目录导航显示题目列表右侧显示当前题目移动端导航收起到下拉菜单题目区域占满全屏。后端接口不做区别前端通过媒体查询适配不同屏幕media (max-width: 768px) { .question-nav { display: none; } .question-nav-mobile { display: block; } }一个需要特别注意的细节是移动端下拉框和键盘弹出会影响答题体验。单选题我已经用 radio 解决了比较好点但多选题的勾选体验在手机上还是差点意思后来我把多选题改成了卡片式点选比 checkbox 手感好很多。如果后续接入微信小程序端后端接口基本不用改只需要把 session 认证换成 token 认证就可以。6. 性能优化、部署上线与避坑经验6.1 数据库查询优化与 N1 问题考试结果列表最容易出现 N1 查询问题。比如统计一场考试的所有考生成绩时如果写成这样results ExamResult.objects.filter(examexam) for result in results: student_name result.student.username # 每次循环都要查一次 student 表一次查询 results 只有一条 SQL但循环里取 student_name 会触发 N 次额外查询。考生少的时候没感觉一旦考生有几百人页面加载会明显变慢。解决办法很简单用 select_related 把关联表一次 join 出来results ExamResult.objects.filter(examexam).select_related(student)这个优化对 Django 开发者来说是必修课。考试系统的成绩导出、统计报表、后台列表这些场景都要用到我已经帮你们踩过坑了列表页第一次测试的时候 500 人成绩导出慢到想砸电脑加上 select_related 之后秒开。6.2 部署到 Linux 服务器的完整流程项目在本地开发完成后我部署到了 CentOS 服务器上生产环境用的技术组合是Nginx Gunicorn MySQL Django。这里分享一下部署的关键步骤服务器初始化yum update -y yum install python3 python3-pip nginx mysql-server -y pip3 install django gunicorn pymysql配置 MySQL 数据库CREATE DATABASE exam_system DEFAULT CHARACTER SET utf8mb4; CREATE USER exam_userlocalhost IDENTIFIED BY your_password; GRANT ALL PRIVILEGES ON exam_system.* TO exam_userlocalhost; FLUSH PRIVILEGES;Django settings 里的数据库配置切换DATABASES { default: { ENGINE: django.db.backends.mysql, NAME: exam_system, USER: exam_user, PASSWORD: your_password, HOST: 127.0.0.1, PORT: 3306, } }收集静态文件python3 manage.py collectstatic配置 Gunicorn 启动gunicorn config.wsgi:application -w 4 -b 127.0.0.1:8000 --daemon配置 Nginx 反向代理server { listen 80; server_name your_domain.com; location /static/ { alias /path/to/exam_system/static/; } location / { proxy_pass http://127.0.0.1:8000; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; } }部署过程中最容易出的问题是 MySQL 连接报错pymysql不存在需要在项目根目录的__init__.py里加一行import pymysql pymysql.install_as_MySQLdb()这个坑我每次部署都要踩一次已经形成条件反射了。6.3 并发考试场景下的性能与稳定性保障一场考试如果同时有 500 人参加系统会遇到几个性能挑战第一个是瞬时登录压力。考试开始前十分钟大量考生同时访问登录接口数据库连接数会瞬间飙升可能导致 MySQL “Too many connections”。我的处理办法是给 MySQL 调大 max_connections同时在 Django 层面用连接池比如 django-db-connection-pool复用数据库连接。第二个是提交答案的写入压力。考试过程中每个考生每答一题就会调用保存接口500 人同时答题可能每秒产生几十个写入请求。这时要注意 MySQL 的 innodb_buffer_pool_size 设置不要用默认值。我设置的是物理内存的 60%[mysqld] innodb_buffer_pool_size 2G max_connections 1000第三个是 Redis 的使用。如果后面考生规模继续增长到几千人建议在答题保存、会话管理等高频操作中加入 Redis 做缓存和队列。这一版的系统我用了 Django 自带的缓存机制来缓存考试配置和试卷快照减少数据库读压力。6.4 数据备份与考试记录保存考试系统最怕的是数据丢失。我写了一个定期备份的 cron 脚本每天凌晨备份 MySQL 数据库和上传的附件#!/bin/bash # 每日凌晨 2 点执行 BACKUP_DIR/backup/exam_$(date %Y%m%d) mkdir -p $BACKUP_DIR mysqldump -u exam_user -ppassword exam_system $BACKUP_DIR/db.sql tar -czf $BACKUP_DIR/media.tar.gz /path/to/exam_system/media find /backup -type d -mtime 30 -exec rm -rf {} \; # 保留30天备份这个事别嫌麻烦。我之前就遇到过测试时误删数据的情况没有备份就只能干瞪眼重录。有了自动备份出问题直接恢复成本低得多。6.5 常见问题排查实录我在开发和测试阶段积累了一些典型问题整理成一张速查表方便项目出问题时快速定位问题现象可能原因排查方法解决方案登录后跳转不到对应角色页面权限校验逻辑问题或 URL 路由配置错误检查 Django 日志确认视图是否被正确调用确认自定义的admin_required装饰器在视图函数上的位置是否正确考试页面刷新后答案丢失前端没有从后端恢复已保存答案打开浏览器 Network 面板查看是否有加载答案接口调用页面加载时增加load_saved_answers接口多选题目判分错误答案顺序不一致导致字符串比较失败检查后端判分逻辑是否为逐项对比使用sorted()对答案排序后再比对试卷生成后题目顺序不对题目在中间表里的 order 字段未正确设置检查数据库 PaperQuestion 记录生成试卷时按插入顺序递增 orderMySQL 连接报错pymysql 未安装或未激活启动项目看报错信息在项目__init__.py中执行pymysql.install_as_MySQLdb()前端乱序题目判分不一致选项映射关系未正确保存检查答题记录的 option_mapping 字段提交答案时同步保存临时映射关系考试中途断网重连后状态异常服务端没有做断点恢复检查考试状态字段和答题记录的更新增加重连恢复逻辑从 AnswerRecord 拉取未提交答案成绩统计报表页面卡顿关联查询未优化Django Debug Toolbar 查看 SQL 条数使用select_related和prefetch_related7. 总结项目经验与后续扩展方向这个在线考试系统从设计到完成前后花了两周多时间。我觉得最有价值的不是最终跑通的那一瞬间而是在过程中想清楚的那些权衡和取舍。比如数据模型的设计一开始我偷懒想省掉 PaperQuestion 中间表直接把题目关联到试卷上但后来发现同一道题出现在不同试卷且分值不同是很正常的需求中间表才是正确方案。再比如题目快照机制最初也没有完全靠前端保持页面状态结果测试时管理员在考试中途改了一道题考生端答案和判分全乱了这才下定决心加快照表。还有一个体会是在线考试系统做的是“可靠性”而非“功能数量”。功能再多考试过程中出一两次故障大家对这个系统的信任就没了。所以越到后期我花在异常处理、备份、防作弊这些“看不到”的地方的时间越多。这些看不见的功能才是系统能不能真正上线跑起来的决定性因素。再分享一个小经验整个项目从零开始做的时候建议先把数据库文档写出来。不一定非要用工具画 ER 图用 Markdown 把每个表的字段、关系、索引写清楚就够了。有了数据库设计文档后面的代码开发基本就是填空。后续如果要扩展我建议优先做这几个方向导入导出题库功能支持 Excel 批量导入导出管理员录入题库效率能提升好几倍视频监考接入浏览器摄像头考试过程中定时抓拍存档用于人工复核成绩分析报表统计每道题的正确率、区分度、难度系数方便老师调整教学策略随机组卷的强化支持按知识点权重抽题、按难度比例出题适应更复杂的考试要求。我这个项目已经跑通了完整流程管理员建题库、组卷、发布考试考生登录参与考试、提交试卷、拿到成绩。如果你正在做一个类似的项目希望能帮你少走弯路该优化的部分提前优化该做的防御提前做好踏踏实实把在线考试系统做成一个让人放心的产品。本文还有配套的精品资源点击获取