首页/新闻资讯/正文详情

基于Django的在线考试系统设计与实战:从模型设计到部署全解析

发布时间:2026/9/24 20:54:57 来源:云帆数科 栏目:资讯中心
基于Django的在线考试系统设计与实战:从模型设计到部署全解析
1. 别急着写代码先把在线考试的业务模型拆明白做在线考试系统之前我本来以为不就是把纸质试卷搬到网页上嘛。真正动手才发现这里面的业务逻辑比表面看起来复杂得多。一个完整的考试系统要处理的不是出题—答题—判分这条简单链路而是从题库管理、试卷生成、考试过程监控、异常处理到成绩统计分析的一整套闭环。很多应届生做毕业设计或者简历项目时最容易犯的错就是一上来就建Django项目、写models结果写到一半发现字段对不上、流程走不通推翻重来。先说清楚这套系统能做什么基于Python的Django框架实现一个具备题库管理、随机组卷、在线答题、自动判分、成绩管理、管理员后台等核心功能的在线考试系统。学生端可以登录参加考试、查看成绩和个人错题教师端和管理员端可以维护题库、创建考试、批改主观题、导出成绩报表。这套系统跑通后小到班级测验大到几百人同时在线考试都能支撑住。适合谁来参考正在做Python Web方向毕业设计的同学想丰富简历项目的求职者以及准备把内部培训考核从纸质转向线上的团队。下面我按一个真实项目从零到上线的顺序把关键设计思路和代码骨架都拆开讲。这个项目我前后重构过两版踩了不少坑也总结出一套比较稳定的方案希望能帮你少走冤枉路。1.1 在线考试系统到底要管哪些东西先站在使用者的视角把所有角色盘一遍。系统的用户分成三类管理员、教师、学生。三者权限天然不同这就引出Django生态里绕不开的RBAC权限模型。虽然Django自带User模型和Group权限但考试系统需要更细粒度的控制比如某个老师只能管理自己创建的题库这就要靠自定义权限或者外键归属来约束。从业务流程看系统可以拆成三大模块题库管理模块题型管理、题目CRUD、难度等级、知识点标签、题目导入导出。考试管理模块创建考试、配置考试规则考试时长、总分、及格线、题目数量、是否随机抽题、发布考试、结束考试。答题与判分模块学生参与考试、自动计时、保存答题记录、自动判卷、主观题人工批改、成绩统计。每一块都有自己的坑。比如题库管理看上去简单就是一张Question表但等你想支持选择题、判断题、多选题、填空题、简答题后字段设计就复杂起来了。比如考试管理随机抽题策略不一样有的考试希望每个学生抽到的题完全不一样有的希望抽到的顺序不同就行还有的希望固定题目只打乱顺序。这个需求的差异会直接影响表结构和算法设计。所以我的建议是画业务流程图把自己当学生走一遍登录—进入考试—答题—交卷—看成绩的完整流程再把当老师要做的每一个操作也走一遍。两个端口的流程走完你的表设计基本上就有眉目了。1.2 核心技术决策为什么是Django而不是Flask选型这件事我第一版其实用的是Flask后来才迁移到Django。为什么换因为考试系统的业务模型比普通博客复杂太多Flask灵活是灵活但所有模块都得自己组装ORM也要自己配开发效率低了一倍不止。Django自带Admin后台、ORM、认证系统、表单处理、模板引擎这些都是考试系统的高频刚需。具体来说Django在这几个方面帮我省了大量时间Admin后台题库和考试配置的增删改查几乎不用额外写前端页面Django Admin配置好list_display、search_fields、list_filter就能用。配合自定义action还能实现批量导入题目、批量发布考试这类操作。自带User认证登录、注册、密码修改、session管理全都内置考试系统最基础的用户体系直接就有了。ORM的迁移机制模型改了之后python manage.py makemigrations和migrate就能平滑更新数据库不用手写SQL。表单和CSRF防护Django的Form和ModelForm让我少写大量前端校验代码CSRF中间件天然挡掉了大部分跨站请求伪造攻击。当然Django也不是没缺点最明显的是重对小型项目来说有点杀鸡用牛刀。但考试系统恰恰属于那种你以为很小实际上越做越复杂的项目Django的重反而是优势。尤其是Admin后台真的能省掉管理端80%的页面开发工作量。2. 数据模型设计一套能撑住复杂考试规则的模型模型设计是整个系统的地基。地基没打牢后面每个功能都在补窟窿。我第一版的设计把题目和选项揉在一张表里结果扩充题型时差点把表结构推倒重来。第二版参考了主流在线考试平台的表结构做了一套相对稳定的方案。2.1 模型拆分与关系梳理核心模型我分成这么几张表User用Django的AbstractUser扩展、QuestionCategory题目分类、Question题目、Choice选项仅选择题需要、Exam考试、ExamQuestion考试与题目的关联表、ExamRecord考试记录、AnswerRecord答题明细、Grade成绩表。这里重点说说几个容易设计错的地方。第一题目和选项要不要分表。很多人习惯用JSON字段把选项存进Question表里图省事。短期看确实方便但后续想做选项维度的数据分析、按选项统计错误率时就傻眼了。我的做法是拆出一张Choice表用外键关联Question单选题和多选题的选项都能存。class Question(models.Model): QUESTION_TYPES ( (single, 单选题), (multi, 多选题), (judge, 判断题), (blank, 填空题), (short, 简答题), ) category models.ForeignKey(QuestionCategory, on_deletemodels.CASCADE, verbose_name分类) qtype models.CharField(max_length20, choicesQUESTION_TYPES, verbose_name题型) content models.TextField(verbose_name题干) difficulty models.IntegerField(default3, verbose_name难度(1-5)) analysis models.TextField(blankTrue, verbose_name答案解析) is_public models.BooleanField(defaultTrue, verbose_name是否公开) created_by models.ForeignKey(settings.AUTH_USER_MODEL, on_deletemodels.CASCADE, verbose_name出题人) created_at models.DateTimeField(auto_now_addTrue) class Choice(models.Model): question models.ForeignKey(Question, related_namechoices, on_deletemodels.CASCADE) label models.CharField(max_length10, verbose_name选项标识(A/B/C/D)) content models.TextField(verbose_name选项内容) is_correct models.BooleanField(defaultFalse, verbose_name是否为正确答案)第二正确答案存哪里。判断题和选择题的正确答案可以存在Question表里也可以用Choice表is_correct标记。填空题可以存一个答案列表字段。简答题没法自动判分我在Question里加了一个reference_answer字段给人工批改做参考。统一处理方式是在Question表上增加一个answer_text字段选择题和判断题也把正确答案标识冗余一份这样判卷时不用跨表查性能更好。第三考试和题目的关系要用中间表。很多人图简单直接在Exam表里存一个ManyToMany的题目字段。但这样没法记录每道题在本次考试中的分值也没法固定题目顺序。我加了一个ExamQuestion中间表带上score和sort_order字段class ExamQuestion(models.Model): exam models.ForeignKey(Exam, related_namequestions, on_deletemodels.CASCADE) question models.ForeignKey(Question, on_deletemodels.CASCADE) score models.FloatField(default5, verbose_name本题分值) sort_order models.IntegerField(default0, verbose_name显示顺序)2.2 复杂题型的存储差异判断题型本质上是选择题的变体选项只有正确/错误两个我在Choice表里初始化时自动生成两个选项就行。多选题要注意判卷逻辑学生选的选项必须和正确答案集合完全一致才算对多选、漏选、错选都不得分。这跟单选题不同单选题是选对了就给分选错不给分。这两种判卷逻辑我放在独立的service函数里而不是散落在view里。填空题的答案因为可能有多个空我用一个JSONField来存答案列表判卷时对每个空做规范化比较去掉首尾空格大小写不敏感。简答题走人工批改我在AnswerRecord上增加一个mark_score字段老师批改时填入分值系统自动累加。2.3 用户答题记录的冗余设计答题记录一开始我只设计了一张ExamRecord表记录总分和是否通过。后来发现当老师想看某道题的错误率时必须遍历所有学生的答题明细才查得到。于是补了AnswerRecord表每道题一条记录class AnswerRecord(models.Model): exam_record models.ForeignKey(ExamRecord, related_nameanswer_records, on_deletemodels.CASCADE) question models.ForeignKey(Question, on_deletemodels.CASCADE) student_answer models.JSONField(defaultlist, verbose_name学生答案) is_correct models.BooleanField(nullTrue, blankTrue, verbose_name是否正确) score models.FloatField(default0, verbose_name本题得分) marked_score models.FloatField(nullTrue, blankTrue, verbose_name人工批改得分)这里用JSONField存学生答案适配多种题型选择题存[A,C]判断题存[True]填空题存[答案1,答案2]。判卷时根据题型决定判卷逻辑和得分。之所以要冗余一个score字段而不是判完卷后只存总分是因为老师重新判卷和查看每道题得分都是高频操作明细分数随时要拿出来用。另外ExamRecord表上要冗余考试总分、及格分数和时间消耗避免每次都要join Exam表才能算出来。冗余设计你别嫌丑线上系统性能优化时这些字段都是救命稻草。3. 考试核心流程从开始考试到自动判卷考试流程是最容易出bug的地方。一个完整的考试生命周期是创建考试 → 配置考试规则 → 发布考试 → 考生进入考试 → 开始计时 → 答题 → 交卷 → 判卷 → 出成绩。这一段我讲讲几个关键节点的实现思路特别是别人容易忽略的边界情况。3.1 开始考试与题目确定策略考试规则里得定义一个策略字段random完全随机抽题、fixed固定题目打乱顺序、part_random部分随机部分固定。我用Exam表上的question_generation字段来区分。class Exam(models.Model): title models.CharField(max_length200, verbose_name考试名称) description models.TextField(blankTrue, verbose_name考试说明) duration_minutes models.IntegerField(default60, verbose_name考试时长(分钟)) total_score models.FloatField(default100, verbose_name总分) pass_score models.FloatField(default60, verbose_name及格分) generation_strategy models.CharField( max_length20, choices( (random, 随机抽题), (fixed, 固定题目), (part_random, 部分随机), ), defaultfixed, verbose_name组卷策略 ) start_time models.DateTimeField(verbose_name开始时间) end_time models.DateTimeField(verbose_name结束时间) is_published models.BooleanField(defaultFalse, verbose_name是否发布)随机抽题的实现不能放在学生请求里现抽否则每次刷新页面题目就变了。正确做法是学生第一次点击开始考试时根据策略生成一份题单写入ExamQuestion记录然后学生后续看到的都是这批题目。这样即使刷新、断电重进题单也是稳定的。实现上我写了一个service函数generate_exam_paper(exam, student)逻辑如下检查是否存在已生成的考试记录ExamRecord中status为in_progress存在就直接返回。根据strategy从题库中筛选可用题目按分类、难度、是否公开。如果题库数量不足抛出自定义异常。对题单做洗牌按题型分组后按比例抽题或者全量打乱顺序。把生成的题目关联到ExamQuestion表并创建ExamRecordstatusin_progressstart_timenow。实际考试场景中还要考虑临时加题和部分学生漏考的问题。我的做法是在生成题单之后再锁死试卷配置考试未开始前管理员可以改题一旦有学生开始考试题单就不可变。这块用ExamRecord存在与否来判断就行。3.2 计时逻辑怎么做到掉线也不亏在线考试的计时是老大难。最粗暴的做法是在前端用JavaScript倒计时到时间就自动交卷。这个方案有个致命缺陷学生可以改浏览器时间、刷新页面重置计时器或者关掉页面等倒计时停了再回来。我采用的方案是服务端计时为主前端倒计时为辅。核心思路学生点击开始考试时ExamRecord记录start_time。每次学生提交答题保存时服务端计算剩余时间 start_time duration_minutes - now。交卷时再次校验剩余时间如果超时强制按已答内容判卷。前端倒计时只是一个显示功能通过一个获取剩余时间的接口定时同步。def get_remaining_time(exam_record): 计算剩余时间单位秒。 exam_record.start_time 为考生点击开始考试的时间。 exam.duration_minutes 为考试时长。 deadline exam_record.start_time timedelta(minutesexam_record.exam.duration_minutes) remaining (deadline - timezone.now()).total_seconds() return max(int(remaining), 0)这里有个坑要注意Django的DateTimeField默认存储的是UTC时间而前端页面显示的往往是本地时间。处理不好会出现考试提前几分钟结束的Bug。我的经验是Django全局配置启用USE_TZTrue所有时间比较都用timezone.now()取UTC时间前端展示时转换为北京时间或者其他本地时区。这样前后端各管各的时间戳统一在服务端用UTC比较。另外还有个续考场景学生答到一半浏览器崩溃重新登录后应该还能进入原题单剩余时间从服务器算而不是从上次刷新时间算。所以在进入考试接口里查到已有in_progress状态的ExamRecord直接复用不重新生成试卷。3.3 自动判卷与人工判卷的双轨处理交卷动作我做成一个单独的view不是每个题目单独判分。当学生点击交卷时服务端做这几件事把前端传来的答案字典逐个存入AnswerRecord。遍历ExamRecord对应题单调用判卷函数先自动判所有客观题。遇到简答题这类无法自动判的is_correct置空score暂填0。汇总客观题得分加上主观题等待人工批改的分数算出当前显示的得分。如果考试不含主观题直接得出最终成绩。更新ExamRecord的status为finished、score为最终得分。判断题目的判分需要单独写函数避免在view里堆逻辑def judge_question(question, student_answer): 返回 (is_correct, score, marked_score) 客观题直接判主观题标记为待人工批改。 if student_answer is None: return False, 0, None if question.qtype single: correct question.choices.filter(is_correctTrue).first() is_correct (len(student_answer) 1 and student_answer[0] correct.label) return is_correct, (question.exam_question.score if is_correct else 0), None if question.qtype multi: correct_labels set(question.choices.filter(is_correctTrue).values_list(label, flatTrue)) student_labels set(student_answer) is_correct (correct_labels student_labels) return is_correct, (question.exam_question.score if is_correct else 0), None if question.qtype judge: correct question.choices.filter(is_correctTrue).first() is_correct (len(student_answer) 1 and student_answer[0] correct.label) return is_correct, (question.exam_question.score if is_correct else 0), None if question.qtype blank: # 答案做规范化比较忽略首尾空格和大小写 reference question.answer_text normalized_student [str(item).strip().lower() for item in student_answer] normalized_ref [str(item).strip().lower() for item in reference] is_correct (normalized_student normalized_ref) return is_correct, (question.exam_question.score if is_correct else 0), None # 简答题 return None, 0, None主观题人工批改我借助Django Admin的actions实现。管理员在Admin后台勾选待批改的题目记录点击批量批改进入一个中间页面逐题填入得分保存后自动更新ExamRecord的总分。这块虽然是Admin定制功能但实际使用频率很高值得花时间做好。4. 前端交互与后端安全保障在线考试的页面交互比普通管理后台复杂得多既要保证流畅的答题体验又要防止学生通过前端刷分、作弊、绕过校验。4.1 动态渲染试卷表单答题页我采用Django模板渲染静态页面然后通过AJAX异步提交答案。为什么不用Vue/React考试页面本身不需要复杂的双向绑定Django模板直接渲染题目列表足够用而且渲染速度更快、代码量更少。页面的结构是左侧显示题目列表导航右侧显示当前题目和选项。点击下一题时切换显示同时把当前题目的选择结果保存到本地一个JavaScript对象中。最后一题提交时统一把答案对象通过AJAX POST到服务端。判断题和单选用radio按钮多选用checkbox填空题用input。渲染时注意给每个input设置唯一的name属性比如question_1_single这样的命名规则前端取值时方便后端接收也方便。下面是题单渲染的部分模板{% for eq in exam_questions %} div classquestion-item idquestion-{{ eq.question.id }} styledisplay: none; h4{{ forloop.counter }}. {{ eq.question.content }}/h4 {% if eq.question.qtype single or eq.question.qtype judge %} div classoptions {% for choice in eq.question.choices.all %} label input typeradio nameq_{{ eq.question.id }} value{{ choice.label }} {{ choice.label }}. {{ choice.content }} /label {% endfor %} /div {% elif eq.question.qtype multi %} div classoptions {% for choice in eq.question.choices.all %} label input typecheckbox nameq_{{ eq.question.id }} value{{ choice.label }} {{ choice.label }}. {{ choice.content }} /label {% endfor %} /div {% elif eq.question.qtype blank %} input typetext nameq_{{ eq.question.id }} placeholder多个空用英文逗号分隔 {% endif %} /div {% endfor %}前端这里有个非常值得注意的细节多选框的name重复取出来的值是列表。而单选或文本输入取出来是字符串。提交时统一整理格式多选存数组单选存单元素数组这样后端数据结构干净一致。4.2 防作弊与请求校验在线考试比线下考试更需要防作弊但Web技术能做的防作弊其实有限。我在项目里做了三层防护能挡住大部分顺手作弊的情况。第一层是登录态和权限校验。所有考试相关接口都加login_required装饰器并且视图里检查当前用户是不是学生角色。Django的User模型上我加了一个role字段用自定义装饰器判断。第二层是请求频率和异常行为检测。比如同一道题短时间内反复提交、答题用时异常短30秒交卷且全对、IP频繁切换这些都要做记录。虽然不一定能完全防住但起码留痕。我用Django的signal在ExamRecord保存时记录一个审计日志表后面审计时能直接查。第三层是防csrf攻击。Django默认的CSRF中间件必须开着所有POST请求模板里都要加{% csrf_token %}。这一层不是防学生而是防恶意脚本对考试系统的自动化攻击。有些同学开发时嫌麻烦关掉了CSRF上线后被人写脚本刷接口这锅得自己背。4.3 提交数据的清洗与落库学生提交答案时千万不能直接拿前端传的字典就入库。前端传来的数据是不可信的必须做几层校验数值校验question_id必须是正整数且是本次考试题单中的题目。选项合法性选择题提交的选项label必须在该题Choice表里存在。答案去重多选题提交的选项去重防止[A,A]这种情况。长度限制填空题答案长度限制防止恶意超长文本。我写了一个clean_answers函数来做统一校验def clean_answers(exam_record, raw_answers): raw_answers 形如 {question_id: answer} 返回清洗后的 {question_id: cleaned_answer} valid_question_ids set( ExamQuestion.objects.filter(examexam_record.exam) .values_list(question_id, flatTrue) ) cleaned {} for qid, answer in raw_answers.items(): qid int(qid) if qid not in valid_question_ids: continue question Question.objects.get(idqid) if question.qtype single: cleaned[qid] [answer] if isinstance(answer, str) else answer[:1] elif question.qtype multi: if isinstance(answer, list): cleaned[qid] list(set(answer)) else: cleaned[qid] [answer] else: cleaned[qid] answer return cleaned清洗完的数据才进入AnswerRecord的load阶段。这里还要处理一个边界情况学生没做的题目前端可能不会传过来后端要默认该题未答判0分而不是直接报错。5. Admin后台改造管理端才是真正的生产力考试系统的另一个大头在管理端。Django Admin虽然好用但默认界面和默认功能离能用还有距离。我花了不少时间做Admin定制这里挑几个实用价值最高的讲。5.1 自定义Admin动作批量发布考试默认情况下管理员创建一个Exam之后要在页面上手动去改is_published字段。考试多的时候一次要发布十几场手动改太崩溃。用Django Admin的自定义action可以一键搞定。# admin.py admin.action(description发布选中的考试) def publish_exams(modeladmin, request, queryset): for exam in queryset: if exam.start_time timezone.now() exam.end_time: exam.is_published True exam.save() class ExamAdmin(admin.ModelAdmin): list_display (title, start_time, end_time, duration_minutes, is_published, total_score) list_filter (is_published, start_time) search_fields (title,) actions [publish_exams, unpublish_exams]这里我加了一个校验只能发布当前时间处于考试时间范围内的考试提前发布没有意义反而容易让学生提前进入。5.2 成绩排行榜与导出成绩查看不只在Admin里做学生端也要展示。但管理端的成绩导出才是老师最常用的功能。我写了一个csv_export的action把选中的考试成绩导出成CSV文件。注意CSV导出用utf-8-sig编码不然用Excel打开中文会乱码。这是国内开发者几乎都踩过的坑。admin.action(description导出选中考试的学生成绩(CSV)) def export_scores(modeladmin, request, queryset): from django.http import HttpResponse import csv response HttpResponse(content_typetext/csv; charsetutf-8-sig) response[Content-Disposition] attachment; filenameexam_scores.csv writer csv.writer(response) writer.writerow([考试名称, 学号, 姓名, 得分, 状态, 用时(分钟)]) for exam in queryset: records ExamRecord.objects.filter(examexam).select_related(student) for record in records: used_minutes (record.finish_time - record.start_time).total_seconds() / 60 writer.writerow([ exam.title, record.student.username, record.student.get_full_name(), record.score, 及格 if record.score exam.pass_score else 不及格, round(used_minutes, 1) ]) return response另外一个很实用的小功能是成绩统计卡片。我在Admin的index页面上加了一个自定义的custom admin view展示总考试场次、总考生数、平均分、及格率。虽然也可以装第三方插件但自己写一个几十行的view就能搞定的事没必要引入额外依赖。5.3 题库批量导入功能怎么设计老师手里通常有Excel题库逐条录入浪费生命。我写了一个FAQ导入模板解析函数支持Excel文件上传读取题目、选项、答案、解析后批量创建Question对象。导入时最容易出的问题是编码和格式错误。我的方案是先解析整个文件构建一个待导入的题目列表再统一校验遇到格式错误把错误行号和信息收集起来一次性返回给老师而不是导入到一半中断。这样老师能一次性看到所有问题修好再传一次。6. 上线部署相关Django项目在真实环境中的注意事项开发环境的Django和线上运行的Django完全是两码事。很多同学项目做完了一部署就各种报错。这一章我讲讲把Django在线考试系统真正部署到服务器上时一定要处理的几个细节。6.1 开发环境与生产环境的配置文件拆分Django的settings.py默认只有一份开发时DEBUGTrue没问题线上如果忘了关会直接把堆栈信息暴露给攻击者这对考试系统来说是不可接受的。我的做法是拆分成base.py、development.py、production.pybase.py公共配置比如INSTALLED_APPS、数据库连接、静态文件配置。development.py本地开发配置DEBUGTrue用SQLite数据库或本地PostgreSQL。production.py生产配置DEBUGFalse配置ALLOWED_HOSTS、数据库密码、密钥等从环境变量读取。切换配置靠环境变量DJANGO_SETTINGS_MODULE实现。启动命令变成export DJANGO_SETTINGS_MODULEexam_system.settings.production python manage.py runserver --settingsexam_system.settings.production6.2 数据库、静态文件与并发处理生产环境数据库我选的是PostgreSQL比SQLite稳定得多支持并发读写还能配合Django的select_related优化查询。考试高峰期几百人同时交卷SQLite大概率会报database is locked这个教训我印象深刻。静态文件处理是另一个坑。Django开发模式下自动serve静态文件但生产环境不要用runserver跑正式服务也不要让Django直接serve静态文件。正确做法是python manage.py collectstatic收集静态文件到一个目录然后交给Nginx处理。数据量大的话静态文件也可以丢到云存储但考试系统的静态文件不算多Nginx足够。应用服务器方面我在Windows环境测试时用waitress生产环境也可以继续用waitress或换gunicorn。Waitress是纯Python的WSGI服务器Windows和Linux都能跑性能比runserver强太多。配合Nginx反向代理Nginx负责静态文件和请求转发waitress负责跑Django应用这个组合稳定可靠。我的Nginx核心配置思路是server { listen 80; server_name your-domain.com; # 静态文件由Nginx直接返回 location /static/ { alias /path/to/exam_system/staticfiles/; } # 媒体文件如果有图片试卷等 location /media/ { alias /path/to/exam_system/media/; } # 其他请求转发给waitress location / { proxy_pass http://127.0.0.1:8000; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; } }waitress启动命令waitress-serve --listen127.0.0.1:8000 --threads8 exam_system.wsgi:application需要提醒的是用Nginx反向代理后Django要配置好SECURE_PROXY_SSL_HEADER否则request.is_secure()会判断错误某些依赖HTTPS的跳转逻辑会出问题。6.3 日志与异常监控考试系统最怕学生考试中途报错老师完全不知道。我在生产环境配了文件日志按天切割LOGGING { version: 1, disable_existing_loggers: False, handlers: { file: { level: ERROR, class: logging.handlers.TimedRotatingFileHandler, filename: /var/log/exam_system/error.log, when: D, backupCount: 30, formatter: verbose, }, console: { level: INFO, class: logging.StreamHandler, }, }, loggers: { django: { handlers: [file, console], level: INFO, propagate: True, }, }, }另外我在交卷、开考这些关键动作上加了操作日志模型OperationLog记录谁、什么时候、做了什么操作。这样一旦出现学生说我没交卷但系统显示交卷了的纠纷能迅速定位问题。7. 实测踩坑记录这些细节文档里不会写最后分享几段实际开发中踩坑的记录。这些细节不是看教程能看出来的属于错一次就记住了的宝贵经验。7.1 时区问题导致考试时间错乱的排查第一次测试的时候学生9点开始考试设置60分钟结果8点半就被强制交卷了。我排查了半天发现是settings.py里TIME_ZONE设置成了Asia/Shanghai但USE_TZTrue时Django存储和比较时间用的是UTC。前端传过来的时间被当成UTC存进数据库而页面显示的又经过本地转换一来一回差了8小时。解决办法是所有代码里统一用timezone.now()获取当前时间时间比较也全部基于UTC只在模板渲染时做本地时区转换。前端页面的倒计时用服务端接口的剩余时间不要自己new Date()。7.2 static文件在Django中显示不了的经典问题很多新手用vscode开发Django项目在模板里写img src/static/xxx.png结果图片死活显示不出来。原因基本是这三个settings.py没配置STATIC_URL和STATICFILES_DIRS模板里没加{% load static %}或者开发服务器需要重启才生效。最隐蔽的一个是当你配置了多个STATICFILES_DIRS目录Django按顺序查找如果两个目录下有同名文件只会生效第一个。这时候调整目录顺序就行。还有个隐藏问题如果你用了Nginx做反向代理开发环境的static配置在生产环境是不生效的必须collectstatic之后由Nginx接管。否则你在开发环境能看到的图片部署后就全都404了。7.3 交卷时请求超时与重复提交考试高峰期几百个学生同时交卷每次交卷要遍历几十道题、写入几十条AnswerRecord、更新成绩。如果中间某个数据库操作卡住前端请求可能pending很久学生着急会再点一次交卷按钮结果就会产生两条交卷记录。我的解决思路分两层第一层是前端控制。交卷按钮点击后立即禁用按钮显示正在提交文案防止重复点击。第二层是后端幂等性设计。ExamRecord表加一个唯一约束(exam_id, student_id)并用一个transaction.atomic()包裹整个交卷流程。同时用一个status字段做状态机判断如果状态已经是finished直接返回当前成绩不重复判卷。from django.db import transaction transaction.atomic def submit_exam(exam_record_id, raw_answers): record ExamRecord.objects.select_for_update().get(idexam_record_id) if record.status finished: return record.score # ...清洗答案、判卷、更新成绩... record.status finished record.save() return record.scoreselect_for_update()是这里的关键它会给这行记录加锁防止两个并发请求同时读取in_progress状态然后各自写一遍。这个锁在高并发交卷时至关重要。7.4 随机组卷的性能优化随机抽题在初始实现时我用的是ORDER BY RAND()题目数量一多页面响应明显变慢。原因是数据库要对全表做随机排序数据量大时非常慢。优化方案是先查出符合条件的题目ID列表在Python里用random.sample挑出需要的ID再按ID查询具体题目。这样数据库只需要精确查几十条记录速度翻了几倍。def generate_exam_paper(exam, student): question_ids list( Question.objects.filter( category__inexam.categories.all(), is_publicTrue ).values_list(id, flatTrue) ) if len(question_ids) exam.question_count: raise ExamPaperGenerateError(题库数量不足) selected_ids random.sample(question_ids, exam.question_count) # 再按题目类型分组打乱顺序后写入ExamQuestion7.5 成绩统计的查询优化管理后台做成绩统计时我一开始用了嵌套循环查平均分和及格率数据量到几千条就明显卡顿。后来改成了aggregate和annotate的聚合查询一条SQL搞定from django.db.models import Avg, Count, Q stats ExamRecord.objects.filter(examexam).aggregate( avg_scoreAvg(score), max_scoreMax(score), min_scoreMin(score), pass_countCount(id, filterQ(score__gteexam.pass_score)), total_countCount(id), )及格率 pass_count / total_count。这种聚合查询在索引建好的情况下几万条记录也是毫秒级返回。考试结束后管理员打开成绩统计页面不再卡顿。最后再说几句项目运行了两年从最初的单机版到现在的多班级同时考试最深的体会是在线考试系统真正难的从来不是某个技术点而是把业务规则和异常情况考虑周全。比如学生断网重连怎么办、题目答案有争议怎么处理、老师误操作发布了未完成的考试怎么撤回这些都要在设计阶段留好扩展空间。每次有人问我做这种项目要不要用前后端分离我的回答都是看规模。Django模板渲染对于这种页面数量不多、交互集中的系统完全够用。真到了需要多终端适配、复杂前端交互的规模再把API层抽出来也不迟数据库模型设计好了迁移成本并不高。代码结构上我建议把核心业务逻辑抽成独立的service层比如exam_service.py、paper_service.py不要堆在views.py里。测试起来方便以后扩展API接口时也不用重构。这个习惯我坚持了两年每次改需求都受益。

相关推荐

Mac微信封装方案:Fluid容器化Web微信实战指南
Mac微信封装方案:Fluid容器化Web微信实战指南

1. 项目概述:为什么一个“封装版Web微信”能成为Mac用户的真实刚需你有没有过这样的经历:早上九点刚坐到工位,打开Mac准备处理几条客户消息,结果点开官方微信for Mac——转圈、卡顿、发不出语音、小程序白屏,最后只能切… · 2026/9/24 20:54:57

MATLAB数学建模实战:设备资源配置与减排约束优化求解
MATLAB数学建模实战:设备资源配置与减排约束优化求解

接手这个题目时,我第一反应是:这不就是我前阵子帮朋友做的一个小型能源项目优化方案吗?一组设备、一堆资源、减排目标、成本限制,全部叠加在一块,手算几乎不可能,最后就是靠MATLAB把问题转化成数学模型再求… · 2026/9/24 20:54:57

CPU上126ms解码1080p:PULSE让神经图像编解码器不再倚赖GPU
CPU上126ms解码1080p:PULSE让神经图像编解码器不再倚赖GPU

1. 神经图像编解码器以前被默认是 GPU 专属,这个印象该修正了1.1 为什么神经编解码器一落到 CPU 上就卡成“幻灯片”过去几年我接触过的神经图像编解码器项目,几乎清一色只在 GPU 上做推理验证。数据流大多是这样:加载预训练权重,… · 2026/9/24 20:54:57

ARK手游延迟高丢包?五个实测有效的网络优化方法
ARK手游延迟高丢包?五个实测有效的网络优化方法

玩ARK生存进化手游最让人血压升高的瞬间,不是被霸王龙追得满山跑,也不是驯了一小时的龙被人一箭偷走,而是明明站在安全区,右上角的延迟却从60一路跳到300,转个身都要等两秒,然后眼睁睁看着屏幕上的绿色信号… · 2026/9/24 21:31:33

双膜储气柜与有组织负压隔臭系统:设计、安装与运维全解析
双膜储气柜与有组织负压隔臭系统:设计、安装与运维全解析

双膜储气柜这个设备,我在环保工程里接触了不少年头,说实话,单独看它的储气功能并不算稀奇,真正考功夫的是怎么把它和整个场站的臭气治理衔接起来。这个项目的核心在于,双膜储气柜不只承担沼气储存、压力缓冲的职责&… · 2026/9/24 21:31:26

多智能体AI助手实战:Octop 1.0自托管部署与运维全记录
多智能体AI助手实战:Octop 1.0自托管部署与运维全记录

上个月帮团队搭私有知识库问答助手,需求从“能问答”一路膨胀到“要能自动写周报、能约会议室、能审合同条款”,我一个一个写Agent,写到第三个的时候就开始怀疑人生了。所以当听说腾讯云正式发布AI助手Octop 1.0、主打“一条命令自托管多智能… · 2026/9/24 21:31:20

Python实现JSON转Excel:本地脚本处理数据交付的完整指南
Python实现JSON转Excel:本地脚本处理数据交付的完整指南

我从2019年开始就反复被同一个问题找上门:爬虫跑完了、接口对接完了、数据库导出了,对方开口就是一句“能给我个Excel吗”。JSON数据本身结构清晰、信息密度高,但业务同事、客户、甚至部分开发就是不看JSON文件,只要.xlsx。去网站… · 2026/9/24 21:31:20

深入理解AQS:从ReentrantLock到JUC并发工具的核心原理
深入理解AQS:从ReentrantLock到JUC并发工具的核心原理

1. 为什么并发编程绕不开AQS——从一把可重入锁说起先抛一个很多人都有的疑惑:Java里synchronized用了这么多年,用得好好的,为什么JUC包还要搞出个ReentrantLock?更关键的是,ReentrantLock的实现核心——AbstractQueue… · 2026/9/24 21:31:14

Numba 整数类型推断(NBEP 1):从“最小适配“到“宽度守恒“的可预测整数类型系统
Numba 整数类型推断(NBEP 1):从“最小适配“到“宽度守恒“的可预测整数类型系统

编译器高性能计算 【免费下载链接】numba NumPy aware dynamic Python compiler using LLVM 项目地址: https://gitcode.com/gh_mirrors/nu/numba 点击查看 免费下载 本文基于 Numba 官方增强提案 NBEP 1(integer-typing),系统讲… · 2026/9/24 21:31:13

基于YOLOv8的渔船作业监控系统:从环境搭建到边缘部署全流程
基于YOLOv8的渔船作业监控系统:从环境搭建到边缘部署全流程

简介:这是一套面向计算机、人工智能、自动化等专业学生与教师的毕业设计级项目资源,围绕YOLOv8实现渔船作业监控系统,可用于毕设、课程设计、大作业或项目立项演示。压缩包共97个文件,约24.21MB,以70个Python源码文件为… · 2026/9/24 0:00:13

1D-CNN时间序列建模实战:从Conv1d原理到工业落地
1D-CNN时间序列建模实战:从Conv1d原理到工业落地

简介:面向时间序列数据建模的一维卷积神经网络完整实现,适合深度学习入门者及需要快速验证时序模型的研究者,能够从音频、文本、传感器或股价等序列中挖掘局部特征与时间依赖。压缩包体积很小,只有3KB,内含3个Python脚… · 2026/9/24 0:00:26

柔软的L:汉语语流中被忽视的舌肌张力控制
柔软的L:汉语语流中被忽视的舌肌张力控制

1. 这个“L”不是字母表里的L,而是舌尖上的L最近在几个方言群和语音教学社群里,反复看到有人发一句:“也说字母L:柔软的长舌”。初看以为是英语发音课笔记,点开才发现全是方言爱好者、播音系学生、语言康复师甚至戏曲演… · 2026/9/24 0:00:44

了解更多?预约专属演示

我们的顾问将为您一对一讲解产品与方案

企业微信二维码