基于Flask+Vue的在线考试系统实战:从状态机到并发交卷的完整设计

发布时间:2026/10/18 12:59:10
基于Flask+Vue的在线考试系统实战:从状态机到并发交卷的完整设计 去年接了个项目某公司内部要上一套在线考试系统给员工做技术认证考核。需求不复杂题库管理、随机组卷、在线答题、自动判分、成绩导出。但真正做起来发现网上能查到的“FlaskVue在线考试系统”教程十有八九只是给你看一个能跑起来的CRUD Demo真到了要扛住几百人同时交卷、倒计时不许出错、判分结果必须准确的时候坑一个接一个。这篇文章我会把完整的技术选型、核心实现、踩坑记录、部署压测都摊开讲。项目栈就是标题里的那套Python Flask做后端APIVueVue 3 Vite做前端单页应用MySQL存数据Redis做缓存和会话层。适合正在做课程设计、毕业设计、或者公司内部小型考试平台的开发者参考。我会尽量把每一步为什么要这么设计说清楚而不是只丢代码。1. 为什么是FlaskVue而不是Django全家桶或纯Node方案1.1 先想清楚在线考试系统到底要扛住什么很多人在选型之前根本没想明白考试系统真正的技术压力在哪里。如果只是CRUD那随便什么框架都行。但在线考试系统有一个非常特殊的点考试过程是强状态、强时间约束、强并发写操作的场景。我拆一下需求考试开始后考生长时间停留在答题页面期间需要不断保存答题记录、刷新剩余时间。交卷瞬间大量考生同时提交答案后端要逐个判分并写库这是典型的短时高并发写。判分规则必须一致比如多选题是“全对才得分”还是“漏选给一半分”这直接影响成绩准确性。考试中途可能断网、刷新页面、浏览器崩溃系统要保证考生的答题记录不丢并且能恢复考试。换句话说这个系统的难点不在“展示题目”而在“状态一致性和并发容错”。这就要求后端的业务边界划分清楚所以选型时我优先考虑的不是框架生态有多大而是逻辑是否可控、事务是否好用、部署是否轻量。1.2 三条选型理由当时团队里有人提议用Django因为自带Admin后台管题库似乎很方便。也有人提议直接用NodeExpress前后端都写JavaScript。最后我们还是定了FlaskVue理由有三条第一Flask足够轻业务边界可以自己掌控。考试系统的权限模型和业务模型跟通用后台差异很大。Django Admin确实省事但真正接题目管理、试卷策略、场次管理时你会发现Admin的定制成本比想象中高而且Django的ORM和中间件体系在强事务场景下写法偏重。Flask配合SQLAlchemy事务边界可以控制得很细每一个考试动作都能清晰地落到一个服务函数里出了问题也好排查。第二前端交互复杂度决定了必须前后端分离。不管是用Django的模板还是Express的模板考试页面这种倒计时、题目切换、答题卡联动、草稿箱、异常弹窗的交互后端模板根本写不痛快。Vue的组件化和响应式数据在这种强交互场景下是降维打击。所以从一开始就定了纯API后端前端完全独立。第三团队的语言栈和运维习惯。我们团队是Python背景服务器上已经跑着好几个Python服务。Flask应用可以直接复用已有的部署链路GunicornNginx出问题大家都能排查。这点在实际开发里很重要——选一个团队最舒服的栈比选一个“理论上更好”的栈更划算。2. 数据模型与考试状态机动手前先画清楚业务边界2.1 六张核心表别把“考试”和“试卷”混在一起我第一次做考试系统时犯过一个典型错误把试卷、考试、答题记录都塞进一张大表里结果后面加“补考”“重考”“AB卷”功能时那张表被改得面目全非最后只能推倒重来。这次我老老实实拆表拆完发现业务瞬间清晰了。在线考试系统最小核心模型是六张表试题表questions存储原始试题内容包括题型、题干、选项、答案、分值、难度、科目分类。这里有个要点答案字段建议用JSON存储。单选就是一个字符串多选就是一个数组填空就是有序数组主观题就是参考答案文本。用统一JSON结构可以减少表字段数量判分的时候再按题型走不同逻辑。试卷表papers一张试卷本身不持有题目内容而是通过关联表持有题目列表。试卷只存名称、总分、考试时长、所属科目、创建时间。为什么这么做因为同一道题可以被不同试卷引用而且在不同试卷里单题分值可能不同。试卷试题关联表paper_questions这是最容易忽略的一张表。它要存三个关键字段paper_id、question_id、score该题在本卷中的分值、order_index题目顺序。请注意题目的分值必须在关联表里定义而不是依赖题目表里的默认分值否则你没法实现“同一道题在A卷5分、在B卷10分”这种真实需求。考试场次表exam_sessions这是“一次真实的考试”相当于把试卷和具体时间窗口、考生群体绑定。字段包括paper_id、场次名称、开始时间、结束时间、考试时长、状态未开始/进行中/已结束、监考配置是否允许切屏检测、是否打乱题序。场次是一次实际执行试卷是模板两者必须分开。考生答卷表answer_sheets每个考生参与某一场考试产生一张答卷。字段包括exam_session_id、user_id、开始时间、交卷时间、总分、状态草稿/进行中/已交卷/判分中/已完成。这张表是考试系统的核心状态载体。答题明细表answer_details考生对每一道题的具体作答记录。字段包括answer_sheet_id、question_id、user_answerJSON、是否得分、得分值、判分人主观题人工判分时用到、判分时间。我整理了一张核心数据关系表开发时对着这张表写代码会清晰很多表名职责关键字段与其他表的关系questions原始试题内容type, content, options, answer, score, difficulty被paper_questions引用papers试卷模板name, total_score, duration_minutes, subject_id被exam_sessions引用paper_questions试卷和试题的桥接paper_id, question_id, score, order_index多对多关系实体化exam_sessions一次真实考试场次paper_id, start_time, end_time, status绑定paper和考生answer_sheets考生对场次的答卷exam_session_id, user_id, status, total_score核心状态表answer_details每道题的作答明细answer_sheet_id, question_id, user_answer, obtained_score挂在answer_sheets下这套模型的精髓在于把“题目”“试卷”“考试”“答卷”这四个实体彻底解耦。题目的增删改不影响历史考试试卷的调整不影响已经开始的场次每个考生答卷独立互不干扰。2.2 状态字段考试中途掉线和刷新怎么办状态机是考试系统最容易被忽视的设计。你不可能在考生答题过程中把数据行删掉重来所以每个核心实体都要有状态字段靠状态流转控制逻辑而不是靠“重新创建数据”。对于答卷answer_sheets来说状态至少要有这几个draft还没点“开始考试”可能只是预览了试卷。ongoing已经开始答题计时进行中。submitted考生提交了答卷等待判分。grading正在判分如果存在主观题人工判分流程。finished判分完成成绩可查询。状态机设计里最容易被踩的坑是“恢复考试”。考生中途断网后重连后端要根据状态和考试场次时间来决定是继续答题、还是自动交卷、还是允许重考。这里的逻辑是如果状态是ongoing且当前时间在考试结束时间之前就允许恢复答题已保存的答案继续有效。如果状态是ongoing但当前时间已经超过结束时间自动执行服务端交卷不再允许继续作答。如果状态是submitted说明已经交卷不允许重复交卷只能查看成绩。这套状态机逻辑必须完全放在后端前端只是状态的展示者不能是状态的决策者。我见过一些系统把“是否还在考试时间内”的判断写在前端结果用户改一下本地时间又继续答题了这种低级漏洞绝不能出现。3. Flask后端把组卷、计时、交卷、判分串成可靠流水线3.1 项目结构与蓝图划分Flask项目最忌把所有路由写在一个文件里考试系统的模块边界本来就清晰所以用Blueprint天然合适。我的推荐结构是这样的exam-backend/ ├── app.py # 应用入口初始化扩展注册蓝图 ├── config.py # 配置数据库、Redis、JWT密钥等 ├── models/ │ ├── __init__.py │ ├── question.py # 试题相关模型 │ ├── paper.py # 试卷与试卷试题关联模型 │ ├── exam.py # 场次与答卷模型 │ └── user.py # 用户与角色模型 ├── api/ │ ├── __init__.py │ ├── auth.py # 登录、令牌刷新 │ ├── question.py # 题库管理接口 │ ├── paper.py # 组卷接口 │ ├── exam.py # 考试场次接口 │ └── answer.py # 答题、保存、交卷接口 ├── services/ │ ├── paper_service.py # 组卷逻辑抽题策略 │ ├── exam_service.py # 考试状态机逻辑 │ └── grade_service.py # 自动判分逻辑 ├── utils/ │ ├── jwt_auth.py # JWT装饰器与权限校验 │ └── redis_client.py # Redis连接封装 └── requirements.txt蓝图的划分不是随意的。我的原则是API路由只做参数接收和响应包装核心业务逻辑下沉到services层。比如“交卷”这个操作API层只是调用exam_service的submit方法具体的状态校验、判分、事务提交都在service里完成。这样写的好处是同一个交卷逻辑可以被API调用也可以被定时任务比如考试超时自动交卷调用不会出现“手动交卷和超时交卷行为不一致”的问题。3.2 组卷与AB卷随机不是真随机先说说组卷。考试系统里最常见的需求是从题库里随机抽N道单选题、N道多选题、N道判断题组成一份试卷并且要求相邻考生拿到的题目顺序不一样。这里有一个新手容易犯的错直接SELECT * FROM questions ORDER BY RAND() LIMIT n。这样写在小数据量下没问题但题库一旦上万道RAND()排序会让数据库做全表扫描性能非常差。我的做法是先查出符合条件的题目ID列表然后在Python里用random.sample抽题性能可控逻辑也好测。import random def build_paper(question_pool, rules): selected_questions [] for rule in rules: qtype rule[question_type] count rule[count] candidates [q for q in question_pool if q[type] qtype] picked random.sample(candidates, min(count, len(candidates))) selected_questions.extend(picked) random.shuffle(selected_questions) return selected_questions组卷时还要考虑“AB卷”需求。AB卷的本质是让不同考生看到的题目顺序不同、选项顺序不同甚至题目组合不同。最简单可靠的做法是给每个考生生成独立的题目顺序random.shuffle(paper_questions)但以user_id作为随机种子保证同一考生重新进入考场时顺序不变。对于选择题生成试卷快照时对选项顺序做一次打乱并把打乱后的选项存进答题明细的上下文里。这里要非常小心选项打乱后答案比对不能再用原选项字母必须用选项的唯一ID或内容本身来比对。3.3 交卷事务与幂等处理并发交卷不重不漏交卷是考试系统并发压力最大的环节。几十上百人同时点“交卷”后端要同时处理状态校验、答案判分、总分计算、成绩写入任何一个环节出问题都会引起成绩错误。我踩过最大的坑就是并发重复交卷。那次线上考试一个考生因为网络延迟点了两次交卷按钮后端两个请求同时进入都通过了状态校验结果判分逻辑跑了两遍成绩被写两次数据库里的分数直接翻倍。要解决这个问题核心思路有三个层面第一数据库层面状态约束。在answer_sheets表里加一个submitted_at字段交卷时先执行一条条件更新语句只有状态仍然是ongoing时才更新result db.session.query(AnswerSheet).filter( AnswerSheet.id sheet_id, AnswerSheet.status ongoing ).update({ status: submitted, submit_time: datetime.utcnow() })如果result为0说明记录已被其他请求更新过直接返回“请勿重复交卷”。这一步靠数据库行锁保证了并发安全。第二事务内判分。交卷和判分必须放在同一个事务里。先查询该答卷的所有答题明细逐题计算得分累加总分更新answer_sheets表。整个过程用事务包裹任何异常都回滚不会出现“交卷成功但成绩没算出来”的中间状态。def submit_exam(sheet_id): with db.session.begin(): sheet db.session.query(AnswerSheet).filter( AnswerSheet.id sheet_id, AnswerSheet.status ongoing ).with_for_update().first() if not sheet: raise ExamAlreadySubmitted(已交卷或考试不存在) details db.session.query(AnswerDetail).filter( AnswerDetail.answer_sheet_id sheet_id ).all() total_score 0 for detail in details: detail.obtained_score grade_question(detail) total_score detail.obtained_score sheet.status submitted sheet.total_score total_score sheet.submit_time datetime.utcnow() # 事务提交成功后触发异步判分流程含主观题 trigger_async_grading(sheet_id)第三前端防重。按钮点击后立即置灰并显示加载状态防止用户重复提交。但记住前端防重只是体验优化真正的保障永远在后端。3.4 JWT身份体系为什么不能信前端传来的user_id在线考试系统的鉴权核心是JWT。流程很简单用户登录后后端签发一个包含用户ID和角色考生/管理员/判卷人的JWT前端请求API时在Authorization头里带上。后端每个需要鉴权的接口都用装饰器解析JWT从令牌里拿到当前用户身份。这里有个非常关键的细节任何接口需要“当前用户ID”时都必须从JWT里取不能接受前端通过请求体或URL传过来的user_id。否则任何人都可以传一个别人的user_id代替别人交卷、查看成绩这是严重的安全漏洞。def jwt_required(): def decorator(fn): wraps(fn) def wrapper(*args, **kwargs): token request.headers.get(Authorization, ).replace(Bearer , ) data decode_token(token) if not data: return jsonify({error: 未登录或令牌失效}), 401 g.user_id data[user_id] g.role data[role] return fn(*args, **kwargs) return wrapper return decorator角色权限也要用装饰器统一控制。比如管理员的组卷接口要加admin_required考生的答题接口只允许自己访问自己的答卷。考试系统最怕权限越界考生能访问到其他考生的答案、管理员接口被普通账号调用这些都属于事故级别的问题。另外还有一点题目的答案绝对不能通过题目列表接口泄露给考生。前端需要展示试卷但后端返回试卷内容时必须把answer字段去掉。我见过有系统直接返回整张题目表F12一看答案全暴露了这种错误一次都不能犯。4. Vue前端真正的难点是考生侧的状态维护4.1 答题状态的组件化设计后端把业务边界定清楚之后前端的核心就是管理好“考生当前做到哪一题、每道题答了什么、还剩多少时间”。Vue最大的优势就是响应式数据做这件事非常顺手。我的做法是设计一个useExam组合式函数统一管理考试页面的核心状态const examState reactive({ sheetId: null, status: ongoing, questionList: [], answers: {}, // { questionId: userAnswer } currentIndex: 0, remainingSeconds: 0, })题目切换时只需要把currentIndex改成对应下标组件根据currentIndex渲染当前题目。答题时监听answers的变化自动保存到后端。这里要注意一个问题自动保存不能每改一题就发一次请求否则后端会被请求淹没。我用的是“防抖定时”双保险——用户停止操作5秒后再保存一次同时每30秒强制保存一次当前所有已填答案。watch( () examState.answers, debounce(() saveAnswers(examState.sheetId, examState.answers), 5000), { deep: true } )4.2 倒计时必须服务端校准不能信浏览器时间倒计时这个功能看着简单做起来全是坑。坑点一浏览器标签页切到后台后setInterval会被浏览器降频甚至挂起回来后发现倒计时比实际时间少了十几秒。坑点二用户可以修改电脑的系统时间如果倒计时完全依赖Date.now()计算用户把时间调慢考试时间就无限延长了。解决思路很明确前端只做倒计时的“显示器”真正的计时基准必须在服务端。后端在考试开始时记录start_time并根据场次的duration_minutes算出end_time用服务器时间。前端从接口拿到end_time时间戳本地只负责“用当前时间对比结束时间”来渲染倒计时。那用户改本地时间怎么办很简单前端定期比如每60秒向后端发一个轻量请求重新获取服务器时间戳来校准偏移量let serverOffset 0 async function syncServerTime() { const start Date.now() const res await fetch(/api/time) const end Date.now() const serverTime res.data.timestamp // 用网络往返时间的一半作为近似进行补偿 serverOffset serverTime - (start end) / 2 } function getCurrentServerTime() { return Date.now() serverOffset }页面从后台恢复时监听visibilitychange事件强制同步一次服务器时间然后重新计算剩余秒数。这种做法实测下来已经足够可靠不需要引入复杂的NTP同步协议。4.3 答题卡、断网与异常弹窗答题卡是考试页面的关键交互组件。它的逻辑逻辑不复杂遍历题目列表根据answers里是否有值来判断“已答/未答”点击可跳转到对应题目。但要注意一个细节填空或主观题时如果用户只输入了一个空格也应该视为已答。所以判断已答的条件不只是存在键还要校验值是否有效。还有一个容易被忽略的场景是“主观题答案的本地暂存”。我经历过一次线上考试考生答了半道主观题不小心刷新了页面自动保存又还没触发300字答案直接丢了体验非常糟糕。所以前端在beforeunload事件里要加一个最后的保存请求用navigator.sendBeacon确保页面关闭时数据也能送达后端。window.addEventListener(beforeunload, () { if (examState.status ongoing) { navigator.sendBeacon(/api/exam/${examState.sheetId}/save, new Blob( [JSON.stringify(examState.answers)], { type: application/json } )) } })断网的处理也很关键。考试场景里网络不会一直稳定如果断网时什么都不做考生会以为答案已经保存了实际后端没有收到。前端要用navigator.onLine和HTTP请求失败回调来感知断网弹窗提示“网络异常已切换为本地暂存模式”同时把所有答题数据暂存在localStorage。网络恢复后自动把暂存数据同步到后端。这个功能在移动网络环境下考试时几乎是刚需。5. 实战排坑并发交卷、判分分歧、会话状态5.1 同一条答卷交两次成绩翻倍这个事故前面提到过这里是完整的排查思路供大家复现参考。现象某次考试结束后管理员导出成绩发现有三名考生的成绩异常偏高其中一人总分超过了试卷满分。一看数据库answer_sheets表里出现了两条相同sheet_id的记录当时还没加唯一约束答题明细也写了两遍判分跑了两次分数自然翻倍。排查链路先看Nginx访问日志发现这三名考生在几秒内都有两条POST /api/exam/submit请求。看后端应用日志两次请求的时间差只有不到500毫秒都成功执行了判分逻辑。检查代码发现交卷接口是先查询状态再更新状态查询和更新之间没有加锁并发情况下两个请求都能查到“ongoing”状态。进一步检查数据库发现answer_sheets表没有对(exam_session_id, user_id)加唯一约束所以数据层也无法阻止重复插入。修复方案分两步。第一步在answer_sheets表上为(exam_session_id, user_id)加唯一索引从数据库层面保证每个考生每场考试只能有一份答卷。第二步把交卷逻辑改成“条件更新”只有状态为ongoing的记录才能被更新为submitted影响行数为0就说明已经被交过直接返回提示。这里我还有一个心得任何时候都不要只在代码层做幂等控制数据库的唯一约束是最可靠的兜底。代码可能写漏但数据库约束不会。5.2 判分规则里的边界情况自动判分看起来简单无非是把考生的答案和标准答案做比对。但真实业务里边界情况非常多。最典型的是多选题的判分策略。真实需求里“全对才得分”和“漏选得一半分”是两种常见策略而且还可能是混合的——同一张试卷里简单多选题全对才得分难度高的多选题漏选得一半分。这就要求判分函数必须支持可配置策略。再比如填空题答案往往不是唯一。标准答案是[Flask, Werkzeug]考生写[flask, werkzeug]算不算对答案里加了个空格怎么办我的做法是判分前先做归一化去除首尾空格、统一大小写有些场合要区分、统一全角半角然后再跟标准答案比对。判断题相对简单只有true/false。但要注意前端在展示判断题时选项按钮的value必须跟后端约定的存储值一致否则会出现“界面显示答对了实际库里的值是个错误枚举”。客观题判分我总结了一个原则所有判分逻辑必须后端执行前端只能展示结果。不要在页面上写一段“根据用户选择计算得分”的代码因为你无法信任浏览器端传过来的任何分数结果。5.3 Flask默认Session在考试系统里的局限Flask默认的Session机制是把数据序列化后签个名存在客户端Cookie里。这个机制做简单的登录状态没问题但考试系统的答卷草稿、防作弊记录切屏次数、离开页面次数这类数据如果塞进Cookie Session马上会踩两个坑第一Cookie体积爆炸。浏览器对Cookie有大小限制4KB左右一旦把答题记录和操作日志都放进去请求直接失败。第二数据安全性低。虽然Flask的Session是签名的但内容对客户端是可见的只是防篡改不加密考生可以通过解码工具看到自己Session里的部分信息虽然没有答案也会造成信息泄露风险。我的做法是在Redis里存考试会话数据Cookie里只存一个session_id。答题草稿、倒计时基准、切屏次数全部存Redis键名类似于exam:sheet:{sheet_id}并设置过期时间。这样一来页面刷新后可以从Redis恢复答题上下文分布式的场景下也无缝支持——只要所有后端节点连同一个Redis考生的请求打到哪台机器都能拿到会话数据。这个方案比默认Session可靠得多。6. 部署、压测与容量规划从“能跑”到“扛得住”6.1 Gunicorn Nginx Redis的生产配置开发阶段的flask run服务绝不能直接拿上生产这是基本常识。我的部署方案是Gunicorn启动Flask应用作为API服务Nginx做反向代理和静态文件托管Redis做缓存和会话层。Gunicorn的worker配置要根据服务器CPU核数来定。对于考试系统这种大量I/O等待数据库查询、Redis读写的应用worker数量等于CPU核数的2到4倍通常比较稳妥。比如4核服务器gunicorn -w 8 -k gthread --threads 4 -b 127.0.0.1:8000 app:app这里用gthreadworker模式一个进程内开多个线程比纯多进程更节省内存也够支撑并发。Nginx的核心配置就是两个要点。第一/api/路径反代到Gunicorn/路径直接指向Vue构建后的静态文件目录。第二开启gzip压缩Vue构建后的js/css文件比较大不压缩首屏加载会慢得让人放弃。还有一点值得注意如果服务器有多核MySQL的连接池参数要调大。SQLAlchemy默认的连接池大小是5在线考试交卷的高峰期几十个worker同时查询数据库时连接池会成为瓶颈表现为“MySQL server has gone away”这类错误。我把池大小调到20并设置pool_pre_pingTrue避免数据库连接断开后被复用。6.2 一次120人同时在线考试的压测数据考试系统上线前一定要压测不然你永远不知道交卷那一下会发生什么。我用压测工具模拟过一场120人同时在线、同时交卷的场景记录的数据很有参考价值场景并发人数API平均响应时间数据库CPU峰值是否出现异常定时保存答案平时120人离散请求50ms30%无同时交卷全客观题120人同时800ms70%无同时交卷含主观题120人同时1.2s85%个别请求超时未加唯一索引时的重复交卷120人同时1.5s90%重复记录出现压测暴露了两个问题。第一交卷接口里主观题没有答案时不需要走人工判分队列直接跳过可以减少一些开销。第二最大的瓶颈不在CPU而在数据库连接和事务提交的锁等待加了唯一约束和行锁之后重复提交被挡在数据库层面接口反而更稳定了。我的容量规划经验是对内部考试系统来说量级通常不会超过几百人同时在线这套“1台4核8G服务器 MySQL Redis”的配置足够支撑500人同时在线考试。如果超过这个规模优先做的不是升级服务器而是把题库和答题明细拆到独立的读写节点或者用消息队列削峰——不过大多数场景用不上。6.3 数据库索引优化考试系统的查询模式是非常规律的顺着业务路径就能把索引设计出来。我建的低效索引有一次把交卷查询拖慢到秒级加完索引之后直接降到几十毫秒。核心索引就这几条answer_sheets 表UNIQUE KEY (exam_session_id, user_id)同时服务“查询某场考试的所有答卷”和“防重复交卷”两个需求。answer_details 表KEY (answer_sheet_id)交卷判分时按sheet_id批量拉取答题明细。exam_sessions 表KEY (status, start_time)管理员后台列出“进行中的场次”时常用。paper_questions 表KEY (paper_id)加载试卷时按paper_id取题。写完索引后记得用EXPLAIN看执行计划确认SQL走的是索引而不是全表扫描。这是最简单有效地提升考试系统性能的手段成本最低收益最大。7. 为什么我不推荐一上来就改开源考试系统这个问题被问过很多次。很多人的第一反应是GitHub上搜“在线考试系统”一抓一大把直接clone下来改改不就行了我之前也这么想过但实际评估下来改开源系统往往比从零写更痛苦。最常见的坑是“业务模型不匹配”。开源考试系统大多是为某一种特定场景设计的比如面向学校的固定班级考试、面向培训机构的题库练习它们的权限体系、组卷方式、判分规则都是按自己那套场景写死的。你拿到需求“随机抽题AB卷主观题人工判分成绩一键导出”要去适配一个不匹配的模型改动量可能比重新写还大。另一个坑是“技术债与版本锁定”。不少开源项目用的还是旧版框架、旧版依赖或者根本没有测试覆盖。改到一半想升级依赖发现到处是兼容性问题进程越陷越深。相比之下从零写一套核心模块完全可控的系统虽然工作量不小但每一个逻辑都清楚后续扩展和排障都轻松得多。当然如果需求极其简单就是固定一份试卷给固定一群人答题用一次就完事那用开源项目快速搭建没问题。但“要长期运营、要灵活调整考试规则、要对接内部账号体系”这类场景我强烈建议自研核心流程复用开源项目的UI思路可以核心业务逻辑自己掌控。最后分享一点个人体会做完这套系统我最大的感触是考试系统的技术难度并不在于某个单独的技术点而在于把状态机、并发控制、时间一致性、权限这四件事串成一套可靠的整体。写页面、搭接口谁都会但真正决定系统好不好用的是交卷那一秒的并发处理、断网之后的数据恢复、以及倒计时到底信谁的。这些细节如果不在一开始设计时就想清楚后面填坑的时间和精力会远超写功能的时间。给正准备动手做类似项目的朋友一个建议动手写代码之前先把考试流程的状态流转图画出来把每个状态之间的转换条件、异常分支全部列清楚然后再去写表和接口。你在设计阶段多想清楚一个异常场景上线之后就能少处理一次线上事故。考试系统不追求功能炫技它追求的只有两个字靠谱。