Flask+Vue在线考试系统实战:从数据库设计到自动判分

发布时间:2026/10/13 18:37:04
Flask+Vue在线考试系统实战:从数据库设计到自动判分 前阵子帮一个做培训业务的朋友搭建在线考试系统技术选型最终敲定 Flask Vue。当时对比过 Django、Spring Boot、Node 这些方案最后让我坚定选择 Flask 的原因是它足够轻、足够灵活配合 Vue 做前后端分离开发效率能比传统全栈方案高出不少。这篇文章我把整个系统的设计思路和核心代码实现梳理一遍涉及数据库建模、随机组卷、自动判分、前端考试页面的倒计时与自动交卷、部署上线等希望对正在做类似项目的人有帮助。1. 整体方案设计为什么是 Flask Vue1.1 需求拆解和核心功能清单在线考试系统听起来简单但只要接触过真实业务就会发现它的坑全藏在细节里。我最初接到的需求只有一句话要能在线出题、答题、出成绩。但和业务方聊了几轮之后需求变成了下面这张表模块需求明细优先级用户管理管理员、教师、学生三种角色支持批量导入账号高题库管理单选、多选、判断、填空四种题型支持按知识点分类高组卷策略支持人工选题和自动随机组卷按题型数量和分值难度配置高考试管理设置考试时间、时长、总分、及格线、考生范围高在线答题倒计时、答题卡切换、未答提醒、自动交卷高判分与统计客观题自动判分成绩统计、分数段分布中防作弊切屏检测、考试期间禁止复制粘贴中这个功能列表基本覆盖了市面上大多数在线考试系统的核心场景。你如果做内部考核或者学校测验直接按这个清单去规划功能就行不需要一上来就设计复杂的分布式架构。1.2 技术栈选型的心路历程选型阶段我认真对比过三组方案服务端方面Django 自带 Admin 后台和 ORM确实省事但模型耦合较重想要全前后端分离反而要绕不少弯子Spring Boot 性能强、生态全但对于一个中小型的考试系统来说团队本身不熟悉 Java 的话学习成本太高。Flask 的优势在于核心精简、扩展自由配合 SQLAlchemy 和蓝图既保持了清晰的工程结构又能在需要时灵活接入各种扩展最终胜出。前端方面Vue 的入门曲线比 React 平滑配合 Element UI 组件库表格、表单、弹窗这些管理系统里高频使用的组件开箱即用。在线考试页面需要的倒计时、答题卡、题目切换这类交互用 Vue 的响应式数据模型实现起来非常顺手。整体架构采用前后端分离Flask 只提供 JSON APIVue 负责渲染与交互。这样后面想换掉任何一端都不会影响另一端。1.3 总体架构和模块划分系统按功能划分成两个端管理端Vue Element UI登录、考试管理、题库管理、考生管理、成绩查看。考生端Vue考试列表、在线答题、交卷、成绩查看。后端 Flask 提供一个 REST API 服务按模块组织蓝图Blueprintauth、user、question、paper、exam、record。数据库采用 MySQL本地联调用 SQLite 也行。缓存用 Redis主要存验验证码、考试令牌、临时答题记录。我把工程目录结构贴出来方便你照着搭examsys/ ├── backend/ │ ├── app.py # 入口 │ ├── config.py # 配置 │ ├── models/ │ │ ├── user.py │ │ ├── question.py │ │ ├── paper.py │ │ ├── exam.py │ │ └── record.py │ ├── api/ │ │ ├── auth.py │ │ ├── question.py │ │ ├── paper.py │ │ ├── exam.py │ │ └── record.py │ ├── utils/ │ │ ├── auth_utils.py │ │ ├── paper_utils.py │ │ └── grade_utils.py │ └── requirements.txt ├── frontend/ │ ├── src/ │ │ ├── api/ # axios 封装 │ │ ├── router/ │ │ ├── store/ │ │ ├── views/ │ │ │ ├── admin/ │ │ │ └── student/ │ │ └── components/ │ └── package.json └── deploy/2. 数据库建模与 API 设计2.1 数据表设计核心逻辑做这个系统时我认为数据库设计是整个项目的灵魂这块如果没想清楚后面写业务逻辑会非常痛苦。核心表有七张我挑几张关键的讲讲设计思路。用户表users区分角色我用一个小技巧解决扩展性问题——用 role 字段区分 admin、teacher、student而不是建立单独的用户扩展表。这么做对于中小型系统来说足够查询效率也高。题库表questions是所有考试系统的地基。设计上支持了题目类型、难度、知识点、分值等多维度的查询条件这样随机组卷时的过滤选项才丰富。字段包括题干、选项 JSON、正确答案我存的是字符串防止多选顺序不对导致判断出错、解析、难度1到5和知识点字段。选项用 JSON 字段存储虽然有人说关系型数据库不该这么干但实际开发中考试题目选项本身就一个整体永远不需要单独查某个选项存 JSON 反而是最省事的方案。试卷表papers采用了主表和明细表分离的设计。paper 主表存总分数、总时长等信息paper_questions 明细表记录每道题的排列顺序方便考试中出现随机乱序的需求。明细表里的 id 同时用作文科题排版和答题卡排序依据。考试记录表exam_records记录了每个考生的考试时间和成绩。考虑到用户可能中途交卷我把答题明细单独拆了张表record_details记录每道题的用户答案和是否得分。草稿的时候我以为可以不用 Redis后来遇到一个实际场景才发现这个想法天真了学生如果在浏览器崩溃后重新进入考试需要恢复之前的答题状态。虽然可以靠每秒往后端发一个心跳保存临时答案实现但频繁写库压力大后来我在 Redis 里做了一个数据结构来临时缓存答题状态到了交卷时间再一次性写库很酸爽。2.2 关键 API 清单API 层我采用了统一的返回结构{code: 0, msg: success, data: {...}}code 为 0 表示成功非 0 表示业务异常。下面列出考试系统最核心的接口方法路径说明POST/api/auth/login登录返回 JWT TokenPOST/api/paper/generate按规则自动生成试卷GET/api/exam/list获取考生可参加的考试列表GET/api/exam/paper/:id获取一份试卷的详情和题目POST/api/exam/start开始考试创建答题记录POST/api/exam/submit提交试卷触发判分GET/api/record/result/:examId查询成绩与答题详情2.3 JWT 登录鉴权实现用户认证没有用 Session而是采用了 JWT。原因很简单前后端分离之后Session 需要处理跨域 Cookie 问题维护成本高用 Token 机制更符合现在的实践。Flask 里我用 PyJWT 手动处理签发和验证没有引入大而全的扩展这样代码更可控。代码实现如下import jwt from datetime import datetime, timedelta from functools import wraps from flask import request, jsonify SECRET_KEY your-secret-key def generate_token(user_id, role): payload { uid: user_id, role: role, exp: datetime.utcnow() timedelta(hours12) } return jwt.encode(payload, SECRET_KEY, algorithmHS256) def login_required(f): wraps(f) def decorated(*args, **kwargs): token request.headers.get(Authorization, ).replace(Bearer , ) if not token: return jsonify({code: 401, msg: 未登录}), 401 try: payload jwt.decode(token, SECRET_KEY, algorithms[HS256]) request.user_id payload[uid] request.role payload[role] except jwt.ExpiredSignatureError: return jsonify({code: 401, msg: 登录过期}), 401 except Exception: return jsonify({code: 401, msg: 无效令牌}), 401 return f(*args, **kwargs) return decorated这里我踩过一个坑前端请求里如果不带Bearer前缀解码时会被当成非法 Token导致用户死循环重定向登录。后来在两个端做了一致约定前端请求拦截器统一加Authorization: Bearer xxx后端只负责校验。建议你也定好这个规范再动工写代码。3. 后端核心业务实现3.1 随机组卷的实现思路随机组卷代码很简单但如果不注意算法设计会埋下很深的坑。看过网上很多版本有的直接用ORDER BY RAND()抽题数据量一大就慢得没法看有的完全不打乱题序考试次数一多题目位置每次一样学生都能背答案了。我的做法分三步第一步拼查询条件。组卷规则里定义了每个题型有几个题每个知识点了录多少道难度分布是几到几。我用字典组合这些条件代码看起来比较清晰。第二步按规则随机抽题。这一块没直接用RAND()而是先查出符合条件的所有题目 ID在 Python 层用random.sample抽取。因为题目的 ID 通常用主键列表返回数据量本身不大内存里操作远比数据库随机排序高效而且代码可测试、可复现。第三步保存试卷快照。组卷成功后要把 paper 和 paper_questions 一次性写入数据库并记录整张试卷的题型分布。这样考生拿到的试卷就是一份“快照”题目不会因为题库后续更新而变化避免出现同一场考试不同考生的试卷题目都不稳定的情况。随机组卷核心代码如下def generate_paper(rule): rule: {single: {count: 10, score: 2, levels: [1,2]}, ...} paper Paper() db.session.add(paper) db.session.flush() total_score 0 order 1 for qtype, cfg in rule.items(): qids Question.query.filter( Question.qtype qtype, Question.level.in_(cfg[levels]) ).with_entities(Question.id).all() qids [i[0] for i in qids] selected random.sample(qids, min(cfg[count], len(qids))) for qid in selected: pq PaperQuestion( paper_idpaper.id, question_idqid, scorecfg[score], orderingorder ) db.session.add(pq) order 1 total_score cfg[score] paper.total_score total_score db.session.commit() return paper.id注意这里分数是试卷快照里定义的score不是题目表里的 score因为同一道题在不同试卷里分值可能不同。3.2 自动判分的完整逻辑自动判分是考试系统最刺激的一环也是踩坑重灾区。大家最容易忽视的是两种特殊场景多选题漏选怎么算判断题选了空字符串怎么处理。我的判分逻辑如下单选用户答案与标准答案完全一致。多选用户答案需要和标准答案完全一致少选、多选都不得分。有些系统支持部分给分我是按全对给分处理的逻辑更简单明确。判断答案转换为 A/B 再比对避免前端传 true/false 造成混乱。填空先做 trim 去空格再逐空精确比较。判分函数如下def grade_question(q, user_answer): correct q.answer # 例如多选 ABC if q.qtype single or q.qtype judge: return 1 if user_answer and user_answer.strip() correct else 0 elif q.qtype multi: if not user_answer: return 0 return 1 if sorted(user_answer) sorted(correct) else 0 elif q.qtype fill: # 每个填空用 || 分隔 if not user_answer: return 0 user_parts [p.strip() for p in user_answer.split(||)] correct_parts [c.strip() for c in correct.split(||)] return 1 if user_parts correct_parts else 0 return 0通过这一版一个收获是一定要在前端对选项顺序做统一化处理比如多选选项都用ABCD这样的字符做 key这样后端才不用管排列问题。真正提交判分的流程是用户点击交卷时后端从 Redis 里取出答题记录逐题比对统计得分一次事务里把exam_records和record_details写入数据库。全程不调用第三方判题机速度非常快。3.3 在线答题的记录与防作弊设计在线答题期间前端每隔 20 秒向后端发送一次心跳后端把答案 JSON 存进 Redis。中途如果浏览器崩溃、断网恢复页面前端首先调用/api/exam/paper/:id获取空卷子再调用恢复接口拿到最近的答案快照继续答题。这个小设计挽救了很多人因为断电差点丢失的答题状态。防作弊我做了三层切屏检测前端监听visibilitychange超过两次切出标签页就触发警告同时把事件上报到后端记录。后端如果发现次数超过设定值可以选择强制交卷。复制粘贴禁用考试页面禁掉复制、粘贴、右键菜单虽然不能完全杜绝但能挡住大部分新手作弊。IP 与账号绑定同一场考试同一个账号只能在固定 IP 上答题防止账号共享。实际用下来这一条最有效不过遇到同一宿舍共用公网 IP 的场景容易误伤要不要开取决于网管和学校网络架构建议开会时让业务方自己定。后端上报切屏事件的接口我用一个简单装饰器冻结了同一考试的答题链路防止并发提交导致的数据错乱。经验是防作弊功能不要写得太重做过头会影响学生考试体验。4. 前端核心页面实现4.1 Vue 工程搭建与路由设计前端工程基于 Vue 2 Vuex Vue Router Element UI。没有用 Vue 3 的原因很朴素Element UI 对 Vue 2 的生态最成熟团队熟悉线上跑得稳如果现在让我重新选可能会换 Vue 3 Ant Design Vue但如果是从零开始且有人能接手Vue 2 也完全够用。路由设计分了两个模块管理端和考生端登录之后根据角色动态加路由const routes [ { path: /login, component: Login }, { path: /, component: Layout, children: [ { path: /admin/exams, component: ExamList, meta: { role: admin } }, { path: /admin/questions, component: QuestionMgr, meta: { role: admin } }, { path: /admin/paper-rule, component: PaperRule, meta: { role: admin } }, { path: /student/exams, component: StudentExamList, meta: { role: student } }, { path: /student/exam/:id, component: ExamRoom, meta: { role: student } }, ]} ]这里最坑的是刷新页面后动态路由丢失。我在 router.beforeEach 判断如果用户信息里有角色但路由没初始化就先调initRoutes()后next({...to, replace: true})否则直接 next。这个坑很多人第一次写 SPA 都会踩建议一开始就把路由初始化逻辑写在登录成功和页面刷新的公共方法里。Axios 封装也是一样的套路响应拦截器统一处理 401 过期弹提示并跳到登录页请求拦截器统一加 Token 和超时时间。我把所有业务接口都收敛到前端/api目录下避免把请求散落在页面代码里。4.2 在线考试倒计时与自动交卷这个页面是整个系统体验最核心的一页做好了给考生的感觉是这系统真专业做不好就是答题体验极为糟糕。倒计时机制不能用前端setInterval累加秒数原因是如果用户切走页面浏览器处于后台时定时器会被节流时间会不准。正确做法是基于服务端下发的考试截止时间用本地时钟动态计算剩余秒数。前端拿到截止时间戳每秒重新计算剩余时间渲染到页面上这样即使切页回来时间依然是准的。this.deadline Date.now() remainingSeconds * 1000; this.timer setInterval(() { const remain Math.floor((this.deadline - Date.now()) / 1000); if (remain 0) { clearInterval(this.timer); this.handleAutoSubmit(); // 自动交卷 } else { this.remainTime remain; this.$store.commit(exam/save, this.paper); } }, 1000);自动交卷调用的就是提交接口没有额外提醒和正常交卷走同一套逻辑。唯一区别是提交前前端会把剩余答案强制提交一次保证后端能拿到最新快照。这里曾经有个很隐蔽的 bug火狐浏览器切到其他 Tab 时setInterval会被放松到每分钟只跑一次倒计时会短暂滞后但当用户切回来时会瞬间回到正确数值。只要用户切回来基本不会误交卷但如果是考试中途长时间切页就会迟交。由于我在循环里同步快了这个 bug 实际影响不大。4.3 答题卡与试卷渲染答题卡组件是主要提体验的地方。试卷渲染用 CSP 数据进行template div classquestion-item v-for(q, idx) in questions :keyq.id div classq-title {{ idx 1 }}. {{ q.content }} span v-ifq.qtype multi多选题/span /div el-radio-group v-modelanswers[q.id] v-ifq.qtype single changesaveAnswer el-radio v-foropt in q.options :labelopt.key{{ opt.label }}/el-radio /el-radio-group /div /template这里要关注两个边界情况第一多选的 v-model 绑定必须是数组answers[q.id] []不能直接用字符串变量以为会自动收集否则一旦题干包含中文选项 key判断就会错乱。第二题目选项渲染必须是稳定的。这里的稳定是同一道题在整场考试期间选项中各内容的顺序不能变。有些第三方组件库默认会按 key 排序渲染会导致考题在倒计时过程中不断闪动极度影响体验。我用数组遍历而非对象遍历就是为了保持选项顺序稳定。答题卡区域是一个网格组件每道题一个方块已答绿色、未答红色、当前定位高亮。点击一题跳到对应题号题号滚动到可视区域。实现时用scrollIntoView同时给顶部留出偏移量避免被 sticky 导航遮住。这个小细节我在图上查了很久后来给题目容器加了scroll-margin-top: 80px一行 CSS 就解决了。4.4 管理后台的关键页面管理后台主要是给教务或者老师用重点关注两个页面考试创建页和成绩统计页。考试创建页要支持两种组卷方式这个交互细节非常关键手工选题从题库里勾选题目组成试卷。自动组卷设置题型数量、分值、难度范围、知识点范围后端生成试卷。我做了个可视化表单题型区间用滑块控制数量难度用五星评分知识点用多选树。提交接口就是前面说的generate_paper的规则对象。成绩统计页是一个成绩表格加上简单的 ECharts 统计图。成绩表格的核心功能点是可以按班级/院系筛选老师把 Excel 导出一套做存档。为了不引入太重框架我直接把 Excel 导出用 60 行 JS 实现前端把表格数据转 CSV再用 Blob 下载。兼容性没问题也不用在后端生成文件省了很多事情。5. 部署、性能与安全细节5.1 部署方案后端用 Gunicorn 跑容器里 Nginx 做反向代理和静态资源服务。前端构建完的 dist 目录直接扔给 Nginx 托管同时通过location /api/把请求转发到 Gunicorn。部署时最容易被忽略的是 CORS。如果 Nginx 同域名部署前端请求走/api就不存在跨域问题不需要配置 CORS 响应头但如果开发环境中后端跑在 5000 端口前端跑在 3000 端口就必须在后端配置 CORS否则所有的请求都被浏览器拦截。我在开发时用 Flask-CORS 扩展配置上线时去掉跨域问题的写法让 Nginx 统一处理最简单。生产环境 Gunicorn 一行命令gunicorn -w 4 -b 127.0.0.1:8000 app:app注意-w 4是 worker 进程数参考 CPU 核数配置。如果试卷生成时用到了 Python 随机函数强烈建议在系统启动时设全局随机种子否则某些场景下并发进程抽题结果可能出现重复我没遇到但看别人的月报项目中间遇到过概率不高但不好排查。5.2 性能优化在线考试系统在普通规模单场 200 人以内下性能不是大瓶颈但有两个场景必须提前考虑考试开始瞬间的集中访问。学生在同一时间点开页面后端大量请求同一份试卷快照虽然查询快但会产生不小的 DB 压力。我做了两个优化参与考试的试卷详情在 Redis 里缓存整份 JSON设置考试开始时间上浮 5 分钟失效同时提前批量生成试卷快照到 Redis避免考场上再去实时查库组卷。成绩导出时的内存占用。一次导出全年级成绩可能出现上百万行不能全查出来转 JSON 爆内存。我用流式查询边查边写 CSV减少一次性内存使用。5.3 安全检查清单与建议安全是一个系统能不能上线的生命线。我最后交给上线同学之前按照一个清单过了一遍密码存储绝不能明文存密码用 Werkzeug 自带的generate_password_hash和check_password_hash。Token 过期JWT 过期时间设置为 12 小时管理端可以设置更短比如 4 小时。SQL 注入全程用 SQLAlchemy 参数化查询严禁拼接 SQL。敏感数据不要在日志里打印完整的用户答案和 Token我见过同事在接口里为了排查 bug 直接把 Body 原样打出来结果 vc 出去了。传输安全生产环境强制 HTTPSWebSocket 也走 WSS否则被中间人抓包直接能被考出试卷答案。这些边界问题和页面功能不同很难在测试阶段全暴露但上线后如果出问题往往就是大问题。6. 常见问题与实战排查记录6.1 高频问题速查表问题现象可能原因解决方案登录后刷新页面白屏动态路由未初始化router.beforeEach 中重新初始化路由并重定向考试倒计时忽快忽慢定时器被浏览器节流改用截止时间戳差值计算而不是秒数累加自动交卷后成绩缺失Redis 快照未落库交卷接口先取 Redis 数据再写库保证最后一份快照被提交随机组卷题目数量不符rand() 抽重了使用random.sample(ids, count)确保不重多选答案怎么判断都对不上选项顺序被前端排序后端以选项 key 比对前端保持数组顺序稳定切屏检测误报页面 title 闪烁也算切屏只监听visibilitychangetitle 闪烁不触发上报这个表我每次做完考试类系统都会更新基本上是实战经验集合很多小问题都要用日志和时间戳去对比排查不能只靠看代码。6.2 两个印象深刻的 BUG 排查第一个 bug并发倒计时导致双重交卷。考试结束瞬间前端组件里有两个地方同时触发交卷setInterval 的 0 哨兵和按钮点击重置的倒计时顺延触发导致后端收到两条提交请求。如果不加锁成绩会写两条统计表直接崩。排查了很久后来在提交接口用 Redis SETNX 做了分布式锁保证同一考试同一用户只能提交一次才算彻底解决。第二个 bug数据库备份时间与考试重叠。因为我在考试期间直接写 exam_records 大表备份任务恰好拖慢了查询200 人的交卷瞬间全部 5xx。后来把备份任务的时间和考试时间错开并把 Redis 快照作为读取优先问题消失。这个坑提醒我在线考试这种峰值型业务一定要把数据库并发峰值和运维任务的时间窗口错开这种经验在压测时很难发现只有真实考试时才会暴露。我自己的经验是在线考试系统这类项目做得久了会发现它其实是一个高并发读、低并发写的小型业务技术挑战不在算法而在各种边界情况和数据的最终一致性。正确的心态是别追求完美关键是先把核心链路打扎实。整套系统从数据库设计到前端考试房间页面的打磨前后花了大约四周时间其中一半时间在调组卷规则和自动交卷的细节。如果你也要做类似的系统建议你先跑通一条最短链路创建考试、随机组卷、学生答题自动判分、结果展示完成这件事以后再去考虑防作弊、断点续考、成绩导出这些进阶功能。以一个能跑通的核心版本为基础去迭代效率会高出很多。

关于本文作者

来自尧图内容编辑团队

尧图内容编辑团队 内容团队

尧图内容编辑团队

本文由尧图网络内容编辑团队执笔。团队由资深项目经理、前端工程师与设计师组成,所有内容均来自亲手交付的真实项目,先讲清问题、再给出可落地的解法。尧图深耕北京网站建设十年,服务过京华建材集团、智造科技等各行业客户,把一线经验沉淀为可复用的行业观察。

  • 十年建站经验,覆盖建材、制造、服务、文创等
  • 项目经理把关选题与事实准确性
  • 工程师与设计师联合撰写专业细节
  • 统一编辑规范,保证文风与排版一致
  • 每月复盘转化数据,迭代选题方向

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

建站决策前值得细读的三篇

网站改版的5个关键决策
2024-08-12

网站改版的5个关键决策

什么时候该改版、改到什么程度、如何避免流量掉光,京华建材集团改版复盘给出答案。

获取专属建站方案

看完文章,把您的行业与预算告诉我们,免费获取一份量身定制的官网建设方案与报价。

立即免费咨询