最近在搭一套基于Python的《面向对象程序设计》在线题库考试系统技术栈选了Django和Flask搭配的方案。这个项目的核心目标是让课程考核从传统的纸质试卷转成可自动组卷、在线答题、即时评分的线上流程题库按知识点和难度管理考试过程支持自动计时、断点恢复和代码题判分。前前后后踩了不少坑也沉淀出一些能直接复用的设计思路和实现细节这篇就来完整拆一遍这个系统从需求梳理到模块落地的全过程。这套系统适合谁看呢一类是学校实验室或者培训机构里负责课程平台搭建的开发者另一类是正在做在线教育类毕业设计或实训项目的同学。如果你对Django和Flask的配合使用还没形成一套清晰的分工逻辑这篇文章也能帮你省不少试错时间。我会把技术选型的理由、数据库建模的细节、组卷和判题的核心算法思路以及上线后遇到的高频问题全部摊开说。1. 项目整体设计与技术选型思路1.1 为什么一个系统里同时用Django和Flask很多第一次接触这个项目的同学都会问既然都是Python Web框架选一个不就行了为什么非要Django和Flask混着用这个问题其实要回到系统本身的实际诉求来看。这套在线考试系统内部天然分成两个差异很大的模块。管理端需要处理用户管理、题库管理、班级课程关联、试卷模板配置、考试成绩统计这些重业务逻辑而且需要一套成熟的后台界面让管理员快速上手。这种场景下Django的优势非常明显自带的Admin后台、ORM、表单验证、权限体系直接能用开发效率极高不需要从零去搭一套管理界面。但学生考试端又完全是另一种情况。考试过程中最重要的是高并发响应、灵活的接口路由、可定制化的答题交互流程。Flask作为轻量级微框架在构建纯粹的API服务时更轻巧灵活中间件和接口路由可以精确控制响应速度和资源占用也比较可控。尤其在做实时提交答案、倒计时同步、代码题判分回调这类接口时Flask写起来比Django那一套完整框架要顺手得多。再说开发节奏的问题。Django的工程结构虽然清晰但启动和迁移流程相对重如果在考试高峰期临时要调整某个接口的逻辑改动Flask这边的小服务明显更快不需要走Django的整套迁移和启动流程。所以我当时的设计是Django负责整个系统的数据核心和管理后台Flask作为独立的考试服务节点承接学生端的答题请求两者通过共享数据库和RESTful API协作各干各擅长的事。1.2 在线考试系统功能架构的整体拆解要动手写代码之前先把功能边界梳理清楚是绝对必要的。一个能上线用的在线题库考试系统绝不是简单把手写卷子搬到网页上就完事完整的链路非常长。我用一个粗粒度的划分把这个系统分成了六个核心模块用户与权限模块区分管理员、教师、学生三种角色教师可以维护题库和创建考试学生只能参加考试和查看成绩。题库管理模块支持选择题、判断题、填空题、简答题、代码编程题五种题型每道题可以维护知识点、难度系数、所属章节。组卷策略模块支持手工选题和自动组卷两种方式自动组卷需要根据知识点分布和难度比例动态抽取题目。在线考试模块包含考试发布、学生入场校验、倒计时管理、答案暂存、交卷确认、异常断线恢复等功能。判分模块客观题直接比对标准答案简答题和编程题则需要设计关键词匹配或代码运行判分逻辑。成绩与统计模块考试结束后生成成绩报表支持按班级、章节、知识点多维度的得分率分析。这个结构的核心原则是数据解耦。题库、考试实例、答题记录、判分结果是四张独立的数据表考试过程中不对题库做任何修改操作保证一次考试使用的题目快照是固定的。这个设计在后来处理试卷重考和成绩复议时起了大作用因为每次考试都保存了完整的题目快照复查时不需要依赖题库的当前状态。用生活化类比来解释这套系统就像一家标准化的餐厅Django是后厨和店长管理台负责菜品研发、库存管理和员工排班Flask是前厅的服务员团队负责响应顾客的点单、上菜和结账请求而共享数据库就是餐厅中央厨房的食材仓库前后厅虽然各干各的但用的是同一套食材账本。2. 核心功能设计与数据库建模细节2.1 题库表结构设计题型、难度和知识点标签题库是整个系统的地基表结构设计得好不好直接决定后面组卷和统计功能的实现难度。我见过不少项目把题目表设计得过于简单只有题目内容、答案、难度三个字段结果到后面想做按章节知识点维度出卷时完全无能为力只能手工一套套卷子去配。我设计的题目表结构如下这些字段在实践中都验证过是完全够用的id题目唯一标识subject_type题型枚举choice / judge / fill / short / codecontent题干内容选择题会附带选项的JSON数组answer标准答案选择题和判断题是固定字符串简答题是关键词列表JSON代码题是参考代码chapter_id章节编号用于知识点维度组卷knowledge_point知识点标签支持一题多标签difficulty难度系数取值1到5creator_id创建者教师IDcreate_time / update_time时间戳status上下架状态其中知识点标签我单独建了一张关联表而不是直接用一个字符串字段存。原因是同一道题往往涉及多个知识点比如一道关于继承和多态的代码题很可能同时命中“类的定义”“方法重写”“运行时绑定”三个知识点。用关联表才能在成绩统计时准确分析出哪个知识点掌握得薄弱字符串字段做这种多对多关系特别别扭。还有两个容易被忽略的细节。第一是选项内容很多新手喜欢把A、B、C、D选项拼在题干里用分隔符切分这种做法一旦选项里出现特殊字符就会翻车。我用JSON数组存选项列表加个is_choice字段标识是否启用乱序渲染时再在服务端随机打乱选项顺序并重新生成选项标签。第二是标准答案的存储格式选择题和判断题直接存字母或“对/错”标识没有争议但简答题如果只存完整文本判分时关键词匹配就很痛苦所以我对简答题的answer字段结构改成了包含score_points数组每个元素是关键词、匹配权重和得分上限。2.2 考试流程设计从组卷到交卷的状态机考试流程是整个系统里状态最多、最容易出bug的地方。一场考试从教师发布到学生交卷中间要经历草稿、已发布、进行中、已结束、已归档五个状态而每位学生的考试记录又独立拥有未开始、答题中、已交卷、判分中、已完成五个状态。我建议把考试实例和题目快照分开建表。exam表存的是考试的通用信息包括考试名称、考试时长、总分、及格线、开始时间、结束时间、组卷策略参数、是否允许迟到等。exam_paper表存储每一场考试实际用到的题目快照包括题目内容、选项、答案、当前分值。核心逻辑是教师一旦点击“发布考试”系统就把按组卷策略选好的题目复制一份快照存入exam_paper此后无论题库是否被修改这次考试用到的题目都不会再变化。这个设计的好处很多最直观的是出成绩后如果学生申请复查系统能明确告诉学生“你考的就是这一版题目”而不是“题库里这道题现在的样子”。另外一个隐藏好处是支持A、B卷并行教师可以给同一场考试配置两套组卷策略生成两份不同的题目快照系统按学生的学号奇偶自动分配试卷。答题过程中的交卷动作也需要仔细设计。前端默认在考试时间截止前30秒自动触发一次答案暂存交卷按钮点击后会先调用暂存接口把所有未提交答案批量写入答题记录表然后再调用交卷接口更新考试状态。这两个操作必须放在同一个事务里否则可能出现答案存了但状态还是答题中的情况学生端就会一直显示考试未结束后台手动改状态又容易引发成绩统计异常。2.3 面向对象程序设计课程的特殊命题方式《面向对象程序设计》这门课和普通理论课有个显著区别很多考点用选择题和填空题根本无法准确考察。比如“多态的实际应用场景”背概念能答对但让学生真写一段利用多态实现扩展的代码可能完全写不出来。所以这套系统在题库设计上为代码编程题留了很大权重。代码题的表结构比普通题多几个字段题目描述、输入说明、输出说明、样例输入、样例输出、参考解法、运行时限、内存限制、语言类型。判分方式采用输出比对模式也就是学生提交代码后在判题沙箱中编译运行输入预置的测试用例拿到程序的输出结果和标准输出比对完全一致才得分。但这里有个现实问题完全黑盒的输出比对无法区分是逻辑错误还是部分通过。比如一道题有5个测试点学生只对了3个如果只按最终输出判断就是0分这严重打击学习积极性。我后来在判分模块里把测试用例拆成了单测可见的多个用例组每个用例组单独运行单独计分最后汇总得分。这样学生能明确看到自己过了几个用例教师也能根据用例维度分析学生代码中具体哪部分逻辑薄弱。面向对象课程里还有一种特殊题型叫做“读代码写结果”题干给一段包含继承、多态特征的代码让学生手写运行输出结果。这种题本质上是选择题的变体但选项不再是一句简单语句而是一个完整的输出序列。我在设计这类题时要求教师把输出序列的定义规范化每一行输出作为一个独立的判分粒度行间顺序必须一致但允许末尾空格差异判分时先做标准化处理再比对。3. 实操过程与核心环节实现3.1 Django后台管理端的核心模型实现Django端承担的是数据核心职责我把它拆成了三个应用users负责用户和角色question_bank负责题目、知识点和章节维护exam负责考试、试卷、答题记录和成绩。这种按业务域拆应用的方式比把所有模型堆在一个models.py里清爽得多。以题目模型为例当时用Django的Model定义了一个Question基类然后通过多表继承扩展出ChoiceQuestion、JudgeQuestion、CodeQuestion等子类。这个设计让我踩过一次坑Django的多表继承虽然查询方便但每次查询子类都要JOIN基类表题目多的时候性能明显下降。后来我放弃多表继承改成单表加type字段的方案虽然代码里要多写一些类型判断逻辑但查询速度和模型简洁度都好了很多。这个教训建议所有做题库系统的开发者记住题目类型是有限的枚举不要为了那点对象导向的优雅牺牲性能。管理端的组卷逻辑我在Django里用管理命令实现。自动组卷核心是一个参数化的选择算法教师传入总题数、各题型占比、知识点覆盖范围、平均难度系数系统先从题库中过滤出候选题目再按约束条件进行多轮筛选。当时我用的是贪心加随机回退策略先按知识点精准命中做第一轮筛选再按难度分桶确保各组难度比例接近目标最后用随机偏移避免每次生成同一套卷子。这里的细节是要控制相邻两次生成的试卷重复率设置一个repetition_limit参数如果相似度过高就重新抽样一次。3.2 Flask考试服务端的接口设计Flask端是整个系统的门面学生所有的答题交互都走这里。我按照资源维度设计了一组RESTful接口关键路径不多但对状态控制要求很高POST /api/exam/token考试入场令牌发放GET /api/exam/{exam_id}/paper获取当前考生的试卷快照POST /api/exam/{exam_id}/answer提交或暂存答案POST /api/exam/{exam_id}/submit正式交卷GET /api/exam/{exam_id}/status查询考试剩余时间和答题进度POST /api/judge/code代码题提交判分GET /api/result/{record_id}查询判分结果入场令牌这个环节容易被人忽略。学生进入考试前必须调用token接口校验身份和考试资格服务端把考生信息、考试ID、有效期签名到一个JWT令牌里后续所有答题接口都要求携带这个令牌。这样做有两个好处一是防止学生绕过考试页面直接构造请求刷答案二是支持断线自动续考学生刷新页面后只要令牌在有效期内系统能自动恢复到之前保存的答题状态。答题接口的幂等性非常关键。前端在上传答案时为了保证不让用户因为网络抖动丢失输入会做多次重试所以服务端必须支持同一道题重复提交答案只保留最新一份。我在Flask端的answer接口里使用了upsert逻辑以exam_id、student_id、question_id三个字段作为唯一约束每次提交都把对应的答题记录更新为最新内容不会产生多条脏数据。3.3 前后端对接与代码组织形式前端部分我用的是Vue加Element UI构建后生成的静态文件由Django托管Flask只提供API数据不参与页面渲染。整体请求链路是这样的学生浏览器访问Django的考试入口页面 → 前端通过Flask API进行身份验证和数据交互 → 后端两个服务读取同一个MySQL数据库。这种前后端分离的组织方式在部署时带来一个常见问题跨域请求。开发环境里前端跑在8080端口Flask跑在5000端口直接请求会触发浏览器同源策略限制。我那时的处理是给Flask应用安装Flask-CORS扩展允许来自前端开发服务器地址的跨域请求生产环境下则把前端静态文件上传到Django同域托管彻底绕开跨域问题。代码仓库我采用了一个monorepo结构管理两个后端服务目录大致如下project_root/ ├── django_backend/ │ ├── apps/ │ ├── manage.py │ └── config/ ├── flask_service/ │ ├── app.py │ ├── api/ │ └── services/ ├── frontend/ │ ├── src/ │ └── dist/ └── deploy/ ├── docker-compose.yml └── nginx.conf这个结构最明显的好处是共享的数据库迁移脚本和接口文档能放在同一个仓库里统一维护两个服务各自独立启动互不干扰部署时只需要用Docker Compose把它们编排在一起。nginx.conf负责把同一个域名下的/api路径代理到Flask服务其他路径交给Django这样学生访问时只有一个入口体验上完全感知不到后端有两个独立服务。4. 常见问题与排查技巧实录4.1 高并发考试场景的踩坑记录系统第一次用于整年级联考时就出现了在线人数一过两百Flask服务接口响应时间从50毫秒暴涨到3秒的情况。排查发现瓶颈不在Python代码本身而在数据库连接池。Flask每个请求都会创建新的数据库连接没有做连接复用数据库端的连接数被瞬间打满新请求就在排队等连接资源。解决的思路很简单用SQLAlchemy连接池替换裸的MySQL连接。我的具体配置是pool_size设为10max_overflow设为20pool_recycle设为280秒。同时把Flask服务里所有涉及查询的操作都改成通过同一个SQLAlchemy session访问不再手动创建连接。配置完成后用压力测试工具模拟300人同时提交答案接口响应时间稳定在200毫秒以内问题彻底解决。另一个高峰期的问题出在倒计时状态同步上。最初的设计是前端每秒请求一次remaining_time接口同步剩余秒数但考试当日大量请求导致更新频率过快给服务端造成不少无谓压力。我后来改成前端本地倒计时为主每10秒向服务端校准一次校准接口返回当前服务器时间和考试截止时间前端用两个时间戳的差值修正本地倒计时。这样既保证了倒计时的准确性又大幅削减了无效请求。4.2 代码判题模块的边界问题代码题判题是这套系统里最容易出隐蔽bug的地方。最开始设计的判题沙箱是在宿主机上直接运行学生提交的Python代码测试而已看着没有大问题但后来发现两个隐患。一是学生代码里可能写死循环直接把判题服务卡死二是学生代码可能访问文件系统读取标准答案存在严重安全风险。当时把判题模块重构为Docker容器隔离方案每个判题任务拉起一个临时容器设置CPU和内存限制超时时间一旦超过就被强制kill彻底杜绝了这两种隐患。判题本身也有一些输出格式的边界问题。比如一道题要求输出一个整数序列有些学生会多打一个空格在行尾有些学生会用全角逗号。我在比对标准输出时做了三层标准化处理先统一换行符再trim每行首尾空格再去除空行最后才做精确比对。这个处理流程看着简单实际操作中减少的误判比例非常可观强烈建议所有做输出比对判题的系统都加上这一步。还有一个很隐蔽的问题是Python代码运行中的异常信息处理。有些学生代码运行时会抛异常如果直接把异常堆栈返回给前端展示不仅不友好还可能暴露服务器内部路径信息。我的处理方案是把运行异常统一包装成三种类型编译错误、运行超时、输出不匹配前端针对不同类型给出对应的提示文案不给学生展示原始异常堆栈。4.3 数据一致性与异常恢复机制考试系统一旦出现数据不一致轻则成绩错乱重则引发学生投诉。两个最常见的异常场景是学生答完题点交卷时断网以及考试中途刷新页面导致答题状态丢失。对第一种情况我在前端交卷成功后强制跳转到交卷成功页如果网络恢复后学生重新回到考试页系统会先检测当前考试记录状态一旦发现是已交卷状态就自动展示成绩页而不是答题页。对第二种情况学生重新进入时系统会先调用resume接口拉取最近暂存的答案页面恢复到离开前的答题状态已填写的内容不会丢失。数据库端的兜底也很重要。我为答题记录和考试状态表启用了事务保护核心写操作都包在transaction里一旦中途出现异常就整体回滚。另外用Django的Celery异步任务队列处理判分请求交卷后立即返回“已交卷”状态判分任务在后台排队执行学生不必在交卷页面白等几秒钟看判分进度。注意系统上线后一定要配置好日志归档。我最初只记录了接口访问日志没有记录关键操作的完整参数一次学生反馈“我明明提交了第5题答案但系统没保存”时排查半天才发现是前端因为某个字段格式错误静默丢弃了请求。后来我在Flask端为每个答题接口增加了结构化日志记录exam_id、student_id、question_id、提交内容的前128个字符、时间戳和最终处理状态之后类似问题分钟级就能定位。4.4 常见问题速查表问题现象可能原因排查方向解决建议考试高峰期接口响应变慢数据库连接池打满查看连接数监控使用连接池并合理配置pool_size学生交卷后答案缺失前端静默丢弃请求检查浏览器控制台和日志前端增加失败重试后端保证接口幂等代码题判分误报错误输出格式不一致导致比对失败查看判题日志中的实际输出和标准输出增加输出标准化处理流程倒计时与学生本地时间不一致本地时钟偏差计算服务器时间和截止时间差值用服务器时间戳校准前端倒计时学生断线后重新登录无法续考令牌过期或考试状态异常检查令牌有效期和考试状态机延长令牌有效期并提供断线恢复接口自动组卷重复率过高随机采样范围过窄检查抽题算法中的随机偏移逻辑增加随机回退和相似度校验Django管理后台加载题目列表缓慢多表继承导致JOIN过多分析SQL查询语句改用单表加type字段方案5. 项目复盘与可扩展方向这套系统从需求调研到第一版上线用时大约两个月核心功能稳定运行后我最大的感受是框架混用并没有带来额外的维护成本反而因为职责边界清晰让两边团队的开发节奏都更舒服。Django侧管数据和后台需求变化频繁也能快速改Flask侧管API性能敏感又相对独立上线后迭代速度很快。有一个值得说的扩展方向是智能组卷。当前版本的组卷策略是基于固定规则的教师手动指定知识点覆盖范围和难度分布。但如果积累多次考试的学生答题数据后完全可以引入知识点掌握度分析根据班级历史成绩低的知识点自动提高出题权重实现个性化补强训练。这个方向的数据基础在现有系统里已经全部具备只差算法层面的迭代。教师端体验的优化也有不少空间。目前查看考试成绩只能看到总得分和每题得分但有经验的教师更希望看到班级在某一个特定知识点上的整体正确率以及哪些题目错误率异常偏高。基于现有答题记录表的数据这些统计报表已经在后续版本中补齐了按班级、按章节、按知识点三个维度都能一键导出Excel报表。如果还有同学打算从零复刻这个项目我给三点建议。第一数据库设计一定把题目快照和题库分离开千万不要在考试过程中实时读取题库否则后续复查和A/B卷都无从谈起。第二考试相关的所有写操作无论看起来多简单都要保证幂等学生端网络情况千奇百怪后端必须扛得住重复请求。第三代码判题模块一开始就要用容器隔离不要因为开发阶段测试量小就在宿主机上直接跑等上线出问题再改代价就大了。最后再分享一个小技巧考试系统上线前务必备份一次题库数据然后模拟一次全流程的异常考试包括断网续考、重复交卷、多人同时点击交卷和代码判题死循环这几种场景。把异常路径全部跑通之后线上出问题的概率会低非常多这个测试步骤花半天时间能省掉上线后一周的客服工作量。