首页 / 资讯中心 / 文章详情

在线考试系统实战:SpringBoot+Vue+MyBatis+MySQL完整实现解析

在线考试系统实战:SpringBoot+Vue+MyBatis+MySQL完整实现解析 ★ FEATURED ARTICLE
在线考试系统算是企业级应用中非常典型的一类业务系统权限模型复杂、并发压力集中、数据一致性要求高做一遍下来几乎能把Java后端和前端的主流技术栈都踩一遍。这套基于SpringBootVueMyBatisMySQL的完整源码项目正好把这套技术栈串成了完整的闭环。我自己前后做过两套考试系统一套是学校机考用的一套是企业内部认证考试用的踩过的坑和沉淀下来的设计经验挺多的这篇就结合这套系统的完整实现把我的实操思路和源码细节拆开讲清楚。先说清楚这套系统能干什么适合谁。从命名就能看出来这是标准的企业级Web考试系统技术栈是SpringBoot做后端服务、Vue做前端页面、MyBatis做数据持久层、MySQL存业务数据。功能上覆盖了考试业务的完整链路用户登录认证、角色权限分配、题库管理、试卷组卷、在线答题、自动判分、成绩统计、考试记录查询。做这套东西的人大概率是两种一种是Java后端想补全前端能力把全栈项目练完整另一种是公司内部或学校真的需要一套可部署的考试系统拿源码改改就能用。不管你是哪种这篇内容都能给你省不少时间。我之前在企业里做内部技能认证考试系统的时候最深的感触是考试系统的难点从来不在“增删改查”而在权限模型的设计、并发交卷时的数据一致性、以及组卷策略的灵活性。这套源码在这些核心环节上处理得比较成熟下面我按模块把设计思路和关键实现逐层讲。1. 整体设计拆解从需求到架构的关键决策1.1 业务模型与角色权限解析考试系统第一个绕不开的问题就是谁在用能干什么。这套系统的用户体系分三层——管理员、教师、考生每一层的能力边界完全不同。管理员负责系统级配置包括用户管理、角色分配、系统参数设置、考试成绩总览。教师负责业务级内容核心是题库维护、试卷创建、考试发布、手动阅卷、成绩复核。考生只关心一件事进入考试、答题、交卷、看成绩。这三类角色如果权限边界没划清楚后续维护就是灾难。权限这块我用的是RBAC基于角色的访问控制模型底层就是用户表、角色表、权限表、用户角色关联表、角色权限关联表这五张标准表。代码层面用Spring Security拦截请求前端用Vue Router动态路由配合按钮级权限控制。具体到实现上登录成功后返回给前端的不只是token还包含当前用户的角色标识和权限码列表前端根据权限码动态生成菜单和可操作的按钮。这样管理员和教师登录后看到的界面完全不一样考生只能看到“我的考试”和“我的成绩”两个模块。确定权限方案时我对比过Shiro和Spring Security。Shiro上手快、配置简单但企业级场景下Spring Security和SpringBoot的整合更平滑而且它的过滤器链路机制在应对OAuth2、JWT扩展时更占优势。这套源码选Spring Security我认为是考虑了后续接入企业统一登录的需求。1.2 前后端分离与RESTful API设计这套系统是标准的前后端分离架构Vue静态资源由Nginx托管后端SpringBoot独立部署通过RESTful API通信。做这种分离架构最关键的就是API设计的规范性我在这套项目里定了几条硬性约定。接口返回统一用Result对象包装结构固定为code、message、data三段。code为0表示成功非0表示业务异常比如参数校验失败、权限不足、考试未开始等。前端axios封装好拦截器统一处理code和HTTP状态码遇到401跳登录页遇到业务异常直接弹出message。接口路径按资源命名标准是模块/功能/操作比如/api/exam/paper/create、/api/exam/record/submit。考试相关的核心接口我列一下获取试卷详情、开始考试、暂存答案、提交试卷、查询考试记录、获取成绩详情。其中最核心的是提交试卷接口后面我会重点讲它在并发场景下的处理。1.3 技术选型为什么是SpringBootVueMyBatisMySQL这套组合可以说是Java全栈最成熟稳妥的组合没有之一的说法虽然绝对但这个搭配确实覆盖了“快速开发、稳定部署、容易招人维护”三大诉求。SpringBoot的核心价值是简化配置内置Tomcat依赖管理用Maven或Gradle统一拉姆打成一个jar包直接跑起来。比起传统SSH那套XML堆配置的方式SpringBoot的自动配置机制帮我把大量环境搭建的时间省掉了。我在这套系统里用的版本是SpringBoot 2.7.x兼容性稳社区资料多遇到问题搜一下基本都有答案。Vue这边用的是Vue2.x版本配合Vue Router和Vuex。选Vue最关键的原因是它的生态成熟Element UI组件库拿来做后台管理系统效率简直离谱。表格、表单、弹窗、分页这些后台高频组件全部开箱即用等于把页面搭建的时间砍半。Vue的双向绑定机制对表单密集型场景特别友好考生答题时选项选择和文本填入都直接绑定数据模型不需要手动操作DOM。MyBatis做持久层的好处是SQL可控性强。考试系统的查询逻辑比较复杂涉及到多表联查、动态条件组合比如按知识点、难度、题型筛选题目组卷MyBatis的XML文件里编写动态SQL比JPA的自动生成SQL更直观可控。这套系统里的题库查询和组卷逻辑我都是手写SQL实现的复杂查询的调优空间比JPA大不少。MySQL作为存储层在这个量级的业务下完全够用。考试系统的瓶颈一般不在单表数据量而在高并发写入。假设一万人在线的考试系统交卷瞬间产生的写入压力MySQL配合合理的表设计和索引优化完全可以扛住。等到并发真的大到需要分库分表时那已经超出这套系统要解决的问题范畴了。另外持久层上来讲MySQL的InnoDB引擎支持事务和行级锁这在对答案自动判分这种“多表更新需要保持一致”的场景里是刚需。2. 数据库设计实战考试系统的表结构与核心字段2.1 核心表结构设计与关联关系考试系统的数据库设计决定了整个项目的上限。我在这套系统里一共设计了十几张表核心的有这么几张。用户相关的是sys_user字段包括id、username、password、real_name、role_id、status。密码存储必须要加密我用的BCrypt算法Spring Security自带支持不用自己实现密码学逻辑。题库相关的是exam_question字段包括id、question_type单选/多选/判断/简答、subject_id所属科目、knowledge_point、difficulty1-5、content、options_json、answer、analysis。这里需要注意选项的存储方式我选择用JSON字符串存options字段这样单选多选判断统一处理不用为每种题型建单独的表。但这里有个取舍JSON存选项对查询统计不友好所以选择在应用层解析。实操下来只要题目量不超过十万级性能完全没问题。试卷相关的是exam_paper和exam_paper_question。exam_paper存试卷的基本信息包括标题、总分、及格分、考试时长、状态。exam_paper_question存试卷和题目的关联关系额外记录该题在试卷中的分值。这两个表拆开的核心原因是同一个题目可以出现在多张试卷中多对多关系必须用中间表维护同时也方便组卷后固定“快照”——一旦试卷被考试使用题目就不能再变动否则已交卷学生的成绩就乱套了。考试过程相关的是exam_record、exam_record_answer。exam_record记录一次考试实例包括user_id、paper_id、start_time、submit_time、score、status。exam_record_answer记录考生的每一道题的作答包括record_id、question_id、answer_content。这两张表是数据一致性关注的重灾区下面我会展开讲。2.2 数据库索引设计的关键考量表结构确定了索引设计就得跟上。考试系统查询频率最高的是考试记录列表和成绩统计我先给exam_record表建了联合索引(user_id, status)因为用户查看自己考试记录是最频繁的查询场景。再给(submit_time)单独建索引用于管理员按时间维度统计考试情况。exam_record_answer表的核心查询是“根据record_id获取某人的全部答案”所以主键策略上我用了自增id加record_id的普通索引。这里有一个我在实际项目中踩过的坑最初我给exam_record_answer设置的联合唯一索引是(record_id, question_id)这是合理且必须的防止同一道题被重复插入作答记录。但如果并发提交时和业务逻辑配合不好这个唯一索引会导致批量插入失败。解决方案是插入前在业务层先删掉该record_id的旧作答记录再统一插入新记录这样唯一索引就是纯兜底保护不会误伤正常流程。另外题库表exam_question的subject_id、question_type、difficulty三个字段大概率会出现在组合查询条件中我建了联合索引(subject_id, question_type, difficulty)。实际组卷时SQL会根据这三个条件筛选题目再配合随机排序联合索引能提升筛选性能。2.3 事务边界与数据一致性方案在线考试系统里最让人头疼的数据一致性问题就是交卷那一下。一次交卷要同时执行几个操作更新exam_record的提交时间和状态、批量插入exam_record_answer、计算主观题以外的得分、更新总分。这四步任何一步失败都会造成“卷子交了但没成绩”这种事故。解决方案是把这个交卷动作放进一个事务方法里用Transactional标注。事务的传播级别默认REQUIRED简单说就是外层有事务就加入没有就新建。我在ServiceImpl层的submitExam方法上加了这个注解方法内先锁住当前考试记录然后执行更新、插入、计算得分等操作任何一个环节抛异常整个事务回滚考生的作答数据不会出现半成品状态。还有一个细节必须提考试时长截止自动提交。后端的定时任务会每分钟扫一次考试记录把超过截止时间且尚未交卷的记录强制提交。这个逻辑必须放在后端不能依赖前端定时器因为用户可能直接关掉浏览器前端定时器就不跑了。我用Spring的Scheduled定时注解实现了一个扫描任务每60秒执行一次保证自动交卷的兜底。2.4 防作弊与考试纪律的技术实现防作弊功能我在这套系统里做了三个层次基础层次是答案随机排序、禁止复制粘贴进阶层次是切屏检测和人脸抓拍高级层次是IP限制和设备绑定。这套源码主要实现了前两个层次我重点讲切屏检测的实现思路。做法是前端监听document的visibilitychange事件和window的blur事件当页面从可见变为不可见时判定为“疑似切换页面”记录切屏次数并给后端发送日志。切屏次数超过一定阈值比如三次系统自动标记该考试记录为“需审核”管理员可以在后台看到这条警示。如果要升级到人脸抓拍思路是调用浏览器的getUserMedia接口采集摄像头画面考试过程中定时抓帧上传到服务器。这个方案用MinIO做文件存储比较合适MinIO是开源的对象存储服务兼容S3协议SpringBoot里集成很方便上传的头像、抓拍图片都可以丢进去。这套源码里没有集成MinIO但如果你要扩展方向很明确。3. 核心功能实现详解从登录认证到自动判分3.1 基于JWT的登录认证与动态路由登录认证这层我没用传统的Session方案而是用了JWTJSON Web Token做无状态认证。核心区别在于Session需要服务器端存储会话状态JWT把用户信息加密放在token里服务器不存状态做到水平扩展时不需要session同步。登录流程是这样的用户输入用户名密码后端校验通过后生成JWT并返回给前端前端把token存到localStorage。后续所有请求都在HTTP请求头携带Authorization字段后端用Spring Security过滤器解析token并校验合法性再把当前用户信息放到上下文里。JWT配置里有两个参数必须要设置合理过期时间和密钥。我给这套系统设置的token过期时间是2小时覆盖单场考试时长上限密钥用的是随机生成的长字符串这个必须放在配置文件的加密配置中不能硬编码在代码里。动态路由这块是配合角色权限的。前端根据登录返回的角色权限码过滤路由表然后通过router.addRoutes动态注册。管理员能看到用户管理、系统设置教师能看到题库管理、试卷管理考生只看到考试列表和成绩查询。这样做的直接好处是前端路由被限制住了就算用户手动输入后台URL没有对应权限的组件也不会被加载配合后端的接口权限拦截才形成双层防护。3.2 题库管理与MyBatis动态SQL实战题库管理的核心操作是“增删改查加批量导入”。这里面最值得展开的是多条件组合查询。管理端的题库列表通常带条件筛选按科目、题型、难度、知识点。这种动态条件的拼接如果在Java代码里写if判断拼SQL字符串又丑又容易出注入风险。MyBatis的 加 标签解决这个问题非常优雅。我截取一段这种实际在源码里用得到的Mapper XML写法select idselectQuestionList resultTypecom.example.entity.ExamQuestion SELECT * FROM exam_question where if testsubjectId ! null AND subject_id #{subjectId} /if if testquestionType ! null AND question_type #{questionType} /if if testdifficulty ! null AND difficulty #{difficulty} /if if testkeyword ! null and keyword ! AND (content LIKE CONCAT(%, #{keyword}, %) OR analysis LIKE CONCAT(%, #{keyword}, %)) /if /where ORDER BY create_time DESC /select这个写法能自动处理“所有条件为空时去掉WHERE关键字”、“第一个条件出现时自动加WHERE后续条件加AND”这些细节。要是自己手写这种SQL拼接边界条件和空指针问题能让你debug到怀疑人生。抓题批量导入我建议用Excel模板后端用EasyExcel解析逐行校验数据后批量插入。这里有一个我刚开始做这系统时踩过的坑批量导入如果每条数据单独insert数据量大时耗时严重。优化方案是分批提交每500条提交一次事务。实测在原系统上5000道题的导入时间从几十秒压缩到几秒。3.3 组卷策略实现固定组卷与随机组卷组卷是考试系统的核心业务逻辑。这套系统实现了两种方式固定组卷和随机组卷。固定组卷的意思是从题库里手工挑选题目确定每道题的分值组成一套固定试卷。所有考生拿到的题目完全一样。这种做法适合企业内部统一认证考试。随机组卷则是按照规则从题库抽取。规则配置包含科目、各题型数量、各题型分值、难度比例。系统按照规则自动组成一套试卷。随机组卷还可以细分两种维度一种是所有考生相同规则生成一套卷另一种是每个考生独立生成的卷子题目不同但难度分布相同。第二种就是常说的A/B卷策略在防作弊上效果明显。实现上随机组卷的SQL核心是“按条件筛选加ORDER BY RAND() LIMIT”。如下select idrandomSelectQuestions resultTypecom.example.entity.ExamQuestion SELECT * FROM exam_question WHERE subject_id #{subjectId} AND question_type #{questionType} AND difficulty #{difficulty} AND status 1 ORDER BY RAND() LIMIT #{limit} /select这里有个性能隐患必须提醒如果题库数据量大ORDER BY RAND()会导致全表扫描加随机排序性能很差。几万条数据还好到了几十万条就等着看超时报警吧。优化方案有两个一个是先用COUNT(*)统计总数再随机生成一个偏移量走LIMIT offset, count另一个是预生成一组随机id集合用IN查询直接取数。第一种在数据量大时offset越深越慢第二种更可控我实际产品里用的是第二种。3.4 在线答题与自动判分实现答题页面是整个系统最核心的前端交互场景。考生进入考试后前端界面顶部固定显示考试倒计时左侧或中间是题目区域右侧是答题卡。答题卡能直观看到哪些题已答、哪些题未答、哪些题标记了疑问点击题号能快速跳转对应题目。答案的存储设计上前端用一个Map对象维护答案key是questionIdvalue是用户选择的选项或输入的文本。每答一题就做一次本地保存同时定期比如每30秒或者切题时自动调后端暂存接口。这样能最大程度防止用户因为断网、误关页面导致答案丢失。暂存接口是普通写入交卷接口做最终的完整落库两套逻辑分开。判断题和单选题的自动判分逻辑很简单比对答案字符串是否一致就行。多选有一个得分策略上的选择完全匹配才得分还是漏选得部分分。我在系统里做的策略是单选、判断答案完全一致得分否则0分多选完全一致给满分漏选给一半分多选错选给0分。这个策略用枚举配置管理员可以在系统参数里调整不需要改代码。自动判分的计算逻辑放在后端执行。交卷事务开始时读取所有客观题的正确答案逐条判断累加分数。主观题分数默认0分等教师在后台手动阅卷后更新总分。3.5 附件上传与文件存储方案这个考试系统里用到了图片上传比如用户头像、问答题的图片附件。文件存哪、怎么存我建议直接从本地目录起步扩展时再切MinIO。本地存储方案配置文件里指定一个上传目录SpringBoot的静态资源映射把该目录暴露为可访问的URL路径上传接口用MultipartFile接收文件写入磁盘数据库存文件的相对路径或完整URL。好处是简单直接单机部署时完全够用坏处是水平扩展时文件不可共享需要迁移到对象存储。如果要上MinIO集成步骤很标准先在服务器部署MinIO服务端默认端口9000控制台端口9001后端引入MinIO SDK依赖配置文件里注入endpoint、accessKey、secretKey上传时把MultipartFile转成InputStream调用putObject存入指定bucket返回文件的访问URL。注意bucket的访问权限要设置好私有bucket需要生成临时访问URL公开bucket直接拼URL即可。企业内部考试系统的附件一般是公开可访问不需要太复杂的权限控制。4. 部署上线与常见问题处理实录4.1 Maven多环境配置与项目打包部署这块我建议直接打包成jar运行简单省事。SpringBoot的Maven插件能打出一个可执行jar内部嵌入了Tomcat服务器上有JDK环境就能跑。关键是环境配置要分离。我在resources目录下按环境做了三种配置文件application-dev.yml开发环境、application-prod.yml生产环境、application.yml主配置。主配置里只放公共项通过spring.profiles.active指定激活哪个环境。生产环境数据库地址、密码这些敏感信息可以放在环境变量或配置中心里不要提交到git仓库。这部分我吃过亏代码库泄露后数据库密码也跟着暴露了企业项目一定要用环境变量注入方式。打包命令用Maven执行mvn clean package -Dmaven.test.skiptrue。跳过测试能加快打包速度特别是考试系统这种测试类里可能包含初始化数据打包时跑测试容易出妖蛾子。打出来的jar放到服务器上执行nohup java -jar exam-system.jar --spring.profiles.activeprod exam.log 21 后台启动日志输出到exam.log文件方便排查。4.2 前端打包与Nginx反向代理配置Vue前端打包是npm run build生成dist静态文件目录。这个目录上传到服务器由Nginx托管。Nginx配置里要做两件事一是静态资源托管location指向dist目录二是API反向代理把/api前缀的请求转发到后端服务的8080端口。我的nginx.conf关键配置片段server { listen 80; server_name exam.example.com; # 静态资源目录 location / { root /var/www/exam/dist; index index.html; try_files $uri $uri/ /index.html; # Vue路由history模式必需的配置 } # API反向代理 location /api/ { proxy_pass http://127.0.0.1:8080; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; } }这里的try_files $uri $uri/ /index.html是为了解决Vue Router的history模式路由刷新后404的问题。不加这一行用户刷新页面直接白屏或404。实际部署中这个配置我几乎每次都要提醒新人注意。4.3 开发部署中常见的十个问题与处理办法考试系统开发中遇到的问题里挑十个高频的整理成速查表遇到问题直接对着看。问题现象根本原因解决方案前端请求接口报跨域错误前后端端口不同浏览器同源策略限制后端配置CORS允许跨域或通过Nginx反向代理同域访问打包后前端刷新404Vue history模式路由刷新后找不到资源Nginx配置try_files指向index.htmlJWT过期导致用户操作中断token有效期设置过短前端在401时自动跳登录页并提示重新登录数据库连接数耗尽连接池配置过小或存在连接泄漏配置HikariCP最大连接数排查事务中连接释放问题大批量插入试题很慢每条数据单独insert导致性能差改为批量插入每500条提交一次事务随机组卷耗时严重ORDER BY RAND()全表扫描改用预生成随机id集合配合IN查询多选答案判分不一致选项顺序比较或存储格式不一致统一选项JSON排序规则判分时先排序再比较图片上传成功但无法访问静态资源映射未配置配置资源映射目录或改用对象存储服务器时间与本地不一致导致考试时间错乱服务器时区未设置为Asia/ShanghaiJDBC连接串加serverTimezoneAsia/Shanghai数据库SSL连接报错MySQL版本默认启用SSL不匹配JDBC连接串加useSSLfalse参数4.4 性能优化与安全加固的进阶建议这套系统上线后还有两件重要的事要做性能优化和安全加固。性能方面先考虑加Redis缓存。考试系统的热点数据特征明显科目列表、常访问的试卷详情、用户基本信息这些都属于访问量大但更新频率低的“读多写少”数据。用Redis做缓存能显著降低MySQL压力。具体缓存策略优先级明确先缓存科目和知识点字典这类几乎不变的数据然后是公开试卷的基本信息最后是用户维度数据。SpringBoot整合Redis很简单引入依赖、配置连接、用Cacheable注解标注方法代码改动量小收益很大。安全方面全站强制HTTPS是必经之路Nginx配置SSL证书让HTTP请求301跳转到HTTPS。API接口需要做限流防止有人写脚本刷接口。SpringBoot里可以基于拦截器或过滤器实现简单的令牌桶限流生产级方案可以用Sentinel或Gateway的限流能力。数据库安全的常规加固也不能忽略MySQL不要用root账号连接业务库单独建一个业务账号只授权业务库的增删改查权限备份策略按天全量加binlog增量做生产环境的数据库密码使用环境变量注入完全没有硬编码的必要。5. 我实际走通这套系统时的经验汇总关于这套SpringBootVueMyBatisMySQL的在线考试系统最后再讲几句我从实操里沉淀下来的经验。这些内容你看完立刻就能用上。第一如果从零开发到部署只有两周时间先做减法。可以把功能范围收缩为核心流程用户登录、题库管理、固定组卷、在线考试、自动判分、成绩查询。不要一上来就做随机组卷、人脸抓拍、复杂报表这些锦上添花的功能。核心链路先跑通能应付真实考试了再迭代增强这是最高效的推进节奏。第二题库数据的初始化一定要重视。开发初期可以用脚本造几千条假数据但上线前必须让业务方提供真实题库并做数据清洗。最常见的问题是选择题选项格式混乱有的带了序号A/B/C/D有的没带导致判分逻辑完全没法统一。我建议在建表字段设计时就用统一的options_json结构前端渲染和后端判分都基于这个结构解析从源头避免格式问题。第三在线考试系统是真的不能等到考试当天才第一次全链路压测的。我见过不止一次考前不测试开考半小时系统崩溃考生答案全部丢失那个场面相当酸爽。我的习惯是在上线前至少做三轮完整演练第一轮功能测试覆盖正常答题交卷全流程第二轮并发测试模拟预期考生数的80%同时交卷第三轮故障演练杀掉数据库进程看系统能否恢复确认恢复后数据有没有丢。这三轮过了心里才有底。第四日志打点要做好。考试系统出问题的时候最怕的就是查不到日志、定位不了问题。我建议至少在后端保证这几个关键节点有日志输出用户登录成功/失败、试卷获取、答案暂存、交卷事务开始/提交/回滚、自动交卷触发、异常捕获。日志级别用info记录正常流程error记录异常用logback按天滚动切割文件保留至少两周。这套系统的完整源码结构上围绕“考”这个核心把基础功能铺开了非常适合拿来当企业级后端的练手项目也适合直接改造成公司内部的培训考试模块。拿到源码后不要急着跑起来先跟着数据库设计理解业务模型再顺着前后端联调流程走一遍核心链路最后根据自己的业务改掉不需要的模块两周内落地是完全可以做到的。
阅读完成 · 觉得有帮助?
咨询建站