前后端分离考试系统实战:SpringBoot+Vue+MyBatis从设计到部署

发布时间:2026/10/6 4:46:18
前后端分离考试系统实战:SpringBoot+Vue+MyBatis从设计到部署 很多人拿到一个“前后端分离考试系统”的源码第一反应是先跑起来结果卡在环境配置上要么是端口冲突要么是数据库连不上要么是前端依赖装不完。我见过不少同学在群里问“为什么我npm run dev之后页面是白的”“为什么登录接口一直报404”这些问题九成不是代码问题而是对这套系统的逻辑和部署链路没吃透。这篇文章我打算用一套完整的SpringBootVueMyBatisMySQL前后端分离考试系统作为例子从表结构设计、权限控制、自动评分、部署上线到源码目录梳理把每个关键环节为什么这么设计讲清楚把最容易踩的坑单独拎出来说明白。无论你是想找毕设参考、想练手前后端分离项目还是准备在简历里写一个“在线考试系统”的项目经历这篇文章都值得你花二十分钟从头到尾读一遍——不是教你抄而是帮你看懂一套完整系统是怎么串起来的。1. 考试系统的核心需求拆解不只是“出题做题”那么简单很多人一提到考试系统脑子里浮现的就是“管理员录题学生做题系统判分”。真上手做才发现光是一个“考试”的领域模型就能扯出一堆问题一场考试能考哪些科目试卷是固定题目还是随机抽题学生中途退出能不能接着答交卷以后还能不能改同一道题多选少选怎么给分每个问号背后都直接决定你的数据库表长什么样、接口怎么设计、前端页面要不要做状态恢复。1.1 从使用角色反推功能边界一套常规的前后端分离考试系统通常有三个角色管理员admin、教师teacher、学生student。如果你去看很多开源项目的源码就会发现绝大多数功能都是围绕这三个角色展开的只是深浅程度不一样。管理员负责基础数据维护比如创建学科、注册/禁用账号、查看所有考试的数据统计。教师负责业务数据组织比如创建考试、从题库选题、设置考试时长和及格线、批改主观题、查看成绩分布。学生是纯使用者功能就四个查看可参加的考试、进入考试答题、提交试卷、查看成绩和试卷详情。我见过不少初学者一上来就把管理员和教师的功能合并成一个角色理由是“都是后台嘛”。短期看省了事后期会非常痛苦——权限判断写成一锅粥前端菜单混乱接口没法做精细管控。这三个角色的划分不是产品经理拍脑袋而是从权限最小化原则推导出来的教师不该能删用户学生不该能看题库。1.2 明确核心业务流程考试系统的业务主流程可以抽象成四步考试创建、试卷生成、在线作答、自动评分。这四步是有先后依赖的前端路由和后端接口都挂在这个流程上。考试创建阶段教师填考试名称、关联学科、设定开始/结束时间这个阶段的关键是有“草稿”和“已发布”的状态切换否则一旦创建就生效想改考试时间只能删了重建。试卷生成阶段有固定试卷和随机试卷两种模式固定试卷是人肉选题随机试卷是按照配置条件比如单选题5道、多选题3道、判断题10道难度中等由系统自动抽取这一步直接决定后面的查询SQL怎么写。在线作答阶段要考虑的是答题记录什么时候写入数据库是每答一题就落库还是交卷时一次性提交这个选择直接影响并发压力和防作弊的粒度。自动评分阶段最容易被低估单选判断好说多选和主观题才是麻烦多选要按“全对得分、漏选得部分分、错选不得分”这种规则去写判分逻辑主观题只能先按采分点给参考得分再让教师复核。把这四步想通了你会发现整个系统的架子基本就出来了。1.3 这套技术栈为什么适合这个业务场景选SpringBootVueMyBatisMySQL不是因为它流行而是因为这是性价比最高的组合。SpringBoot负责提供RESTful API能力内置Tomcat打包成jar直接能跑部署成本低社区资料多Vue负责前端页面交互配合vue-router和axios用起来顺手适合表格、表单、弹窗密集的管理类页面MyBatis在复杂查询和动态SQL这一块有天然优势考试系统恰好需要大量按条件拼接查询比如“在某个学科下、排除已选题、按难度比例抽取”这种场景用MyBatis的XML映射器写动态SQL比用JPA优雅得多MySQL则是最稳妥的通用关系型数据库事务支持完整自动评分需要保证“更新分数记录答题详情”的原子性这就非常依赖InnoDB的事务能力。2. 数据库设计考试系统的灵魂在“答题快照”和“自动评分”一次完整的考试系统数据库设计至少要包含学科表subject、用户表user、考试表exam、试卷表paper、试题表question、试卷题目关联表paper_question、答题记录表answer_record和成绩表exam_record。这八张表之间是层层递进的关系由学科创建题库由题库选题组成试卷由试卷实例化出某次考试学生在考试中产生答题记录最后聚合成成绩。2.1 核心表的字段设计与关联逻辑先看最容易被建模错误的“试卷-题目”关系。一张试卷包含多道题一道题可以出现在多张试卷里这是典型的多对多关系所以必须有一张中间表paper_question。中间表里除了paper_id和question_id两个外键还应该存该题的分值score和排序号sort_order。原因很简单同一道题在不同试卷里可能分值不同甚至排序不同如果把分值冗余在试卷表里就自找麻烦把动态属性放进中间表才是对的。此外试题本身要有类型字段question_type1单2多3判4主观因为不同题型的判分规则不同类型字段直接决定评分模块的if-else分支难度字段difficulty也必不可少随机组卷时它就是筛选条件。再看答题记录表。我的建议是每道题在用户作答后落一条记录字段包含exam_record_id关联一次考试实例、question_id考哪道题、user_answer用户答案原文单选题存A多选题存ABD判断题存T/F主观题存文本、is_correct是否答对、score该题实际得分。为了轻量这张表不要存冗余题目内容需要展示试卷详情的时候join回question表。最后再说考试成绩表一次考试一条记录包含总分、客观题得分、主观题得分、及格状态、交卷时间、实际用时。为什么要单独分一张成绩表而不直接在答题记录表上汇总因为成绩表还要存“一次考试一份答卷”的元信息比如交卷时间、作弊标记、教师复核状态这些是答题记录表表达不了的。下面是核心表的字段设计参考表名核心字段用途说明subjectid, name, sort学科/科目基础信息userid, username, password, real_name, role三个角色共用一张账号表examid, subject_id, name, start_time, end_time, duration, status考试场次基本信息paperid, exam_id, total_score, pass_score, group_mode一场考试对应的试卷配置questionid, subject_id, type, content, options_json, answer, difficulty题库选项用JSON存储paper_questionid, paper_id, question_id, score, sort试卷与题目的多对多关系answer_recordid, exam_record_id, question_id, user_answer, is_correct, score每道题的作答快照exam_recordid, user_id, exam_id, objective_score, subjective_score, total_score, submit_time一次考试的成绩主记录2.2 为什么选项用JSON而不用单独的表这个问题我几乎每次都会被问到。标准的关系型数据库教科书会告诉你选项应该用option表通过外键关联到question_id。但在在线考试系统的真实场景里选项是跟随题目创建时定死的后续基本不会单独增删改查把它拆成独立表不仅没有收益反而会让查询和维护变得繁琐。MyBatis查一行题目要额外查一次选项表再组装成对象接口多了效率并不高。我当时的做法是options_json字段存这样的结构 [{label:A,content:中国},{label:B,content:美国},{label:C,content:日本},{label:D,content:俄罗斯}]后端用Jackson转成List前端拿到直接v-for渲染。正确答案answer字段存字符串单选是“A”多选是“ABD”判断是“T”或“F”主观题存参考答案文本。这样的设计看起来不够“范式”但在这个业务里它就是最优解。不要盲目按教科书走模型要服务于业务。2.3 事务在自动评分中的关键作用自动评分不是一个简单的算法函数它涉及多个表的联合更新要更新exam_record的总分、更新answer_record里每道题的得分、更新用户的考试状态。任何一步失败都不能接受——考完试分数没进去这个事故对真实用户来说是灾难性的。所以评分逻辑必须放在同一个事务里Spring的Transactional注解正好干这件事底层交给MySQL的InnoDB引擎行级锁和undo log保证原子性和一致性。注意踩坑点如果你的表是MyISAM引擎事务是不生效的建库时务必确认默认引擎是InnoDB。3. 后端实现JWT鉴权、统一返回与MyBatis持久化的细节后端这部分我着重讲三个大家找源码时最容易忽视但面试最常见的问题登录怎么做鉴权、接口返回的R对象怎么统一、MyBatis的XML文件怎么组织才不乱。3.1 JWT无状态鉴权登录不是“存session”传统单体项目喜欢用HttpSession保存登录状态但前后端分离项目的后端接口可能要同时给Web端、移动端甚至第三方系统调用session的跨域共享问题很麻烦。所以现在的主流方案是JWT无状态鉴权我在这套系统里也是这样做的。流程不复杂用户登录成功后后端生成一个JWT字符串返回给前端前端存在localStorage之后每次请求前端在axios拦截器里把token塞进请求头Authorization后端写一个拦截器从请求头取到并校验token校验通过就放行。这里有一个细节很多人不理解“无状态”的意思就是后端不存token每一次请求都全靠token本身是否有效来决定放行所以即使服务器重启已登录的人也不会掉线。而JWT本身被拆成Header、Payload、Signature三段为了防伪造签名要用一个只有后端知道的密钥。密钥不要硬编码在代码里放进application.yml里会比写在类里好得多至少改配置不用重新编译。为了不让每个接口手动判断token我在项目里写了一个拦截器并在WebMvcConfig里通过addInterceptors方法注册Configuration public class WebMvcConfig implements WebMvcConfigurer { Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(new LoginInterceptor()) .addPathPatterns(/api/**) .excludePathPatterns(/api/user/login, /api/user/register); } }登录和注册接口放行其余接口必须带token。拦截器里解析token后就往request.setAttribute里塞当前用户id后续Controller需要当前用户时直接从request里取省去每个接口反复查数据库。3.2 统一返回对象R前后端联调不吵架如果你做过前后端联调一定经历过这种场景后端返回一个用户对象你直接拿data.name渲染结果用户不存在时后端返回了一段错误文本页面直接白屏。这就是没有统一返回结构造成的。我在项目里定义了规范public class R { private Integer code; // 200成功500失败 private String msg; // 提示信息 private Object data; // 业务数据 }所有接口只要是正常返回一律走R.success(data)任何异常都用全局异常处理器捕获走R.fail(msg)。开发前端的同事拿到这个结构就知道怎么做响应拦截了——code是200就看data否则就弹msg逻辑统一不用每个页面单独判断。还要提醒一个细节全局异常处理器不能只处理业务异常还要处理兜底异常Exception.class否则第三方调用偶发空指针会返回一个不规范的错误前端拿不到msg排查问题只能抓瞎。3.3 MyBatis的XML映射器复杂查询写SQL简单查询用注解MyBatis有两种SQL映射方式注解和XML。小demo用注解确实清爽但在考试系统里随机组卷、成绩列表分页、答题记录的批量插入这些场景注解写起来很吃力动态SQL更是噩梦。我的经验是单表简单查询用注解跨表或动态条件用XML两者结合。比如随机组卷的SQL需要用 拼条件、用 处理难度权重这种场景XML是最合适的写法的select idselectQuestionIds resultTypejava.lang.Long SELECT question_id FROM question WHERE subject_id #{subjectId} AND type #{type} if testdifficulty ! null and difficulty ! AND difficulty #{difficulty} /if ORDER BY RAND() LIMIT #{count} /select还有批量插入答题记录如果一条条insert几百道题就要几百次数据库交互性能很差。改用MyBatis的 批量insert一次请求就能搞定insert idbatchInsertAnswers INSERT INTO answer_record(exam_record_id, question_id, user_answer, is_correct, score) VALUES foreach collectionlist itemitem separator, (#{item.examRecordId}, #{item.questionId}, #{item.userAnswer}, #{item.isCorrect}, #{item.score}) /foreach /insert这里注意MySQL连接参数必须加上allowMultiQueriestrue吗其实不需要批量insert本身就是一条SQL语句只要连接串里useServerPrepStmtsfalse或者不开启预编译默认就能跑但有经验的老手其实在上面的场景里更建议参考MyBatis的ExecutorType.BATCH模式去循环刷新感兴趣可以去查一下这里不再展开。3.4 自动评分的代码怎么写才清晰自动评分是整个后端里逻辑最密集的地方。我用一个独立的ScoreCalculator类来处理避免把判分逻辑堆在Service层里。先说客观题的判分规则这也是最容易起争执的地方单选题答案一致即得满分否则0分。多选题规则要支持“全对满分、漏选得一半分、错选0分”。比如正确答案ABD用户选AB漏选了D按规则可以得一半分。这笔账如果靠字符串equals去做一毫秒就算完了但胜在直观。四类题型的产出物不同规则我分别写放在一个方法里处理。这个方法接收题目、用户答案、该题分值返回该题得分主流程再把它循环调用起来。上线前我整理了一套测试用例漏选、错选、空答案、大小写答案、答案中间带空格这些边界提前测好省的正式考试时出幺蛾子。4. 前端实现Vue工程结构、动态菜单与接口联调前端我用的是Vue 2 Element UI的组合如果你拿到的是一个Vue 3的项目逻辑也是一样的只是写法从options API换成了composition API。前端这块我将拆成三个核心项目结构怎么组织、路由权限怎么做、axios封装和接口联调怎么处理。4.1 目录结构不是用来好看的很多开源项目的前端目录长得乱七八糟组件全堆在views里伸手的人拉下来半天找不到页面。一个规范的Vue工程应该至少分五块api目录按业务聚合接口文件比如exam.js、question.js、user.js、views目录页面组件按角色分包admin/、teacher/、student/、router目录路由表定义包含动态注册逻辑、store目录Vuex的模块比如user模块存token和用户信息、utils目录axios实例、公共函数。我拿到任何一份前端源码第一步一定先看src路径下的目录这个结构比代码本身更能说明系统清晰不清晰。如果你准备借鉴这套代码写简历项目我建议你布局也这么分面试官问你“你在项目里怎么拆分模块”你至少能说出个所以然。4.2 动态菜单不同角色看到不同页面考试系统里学生看到的菜单是“我的考试、成绩查询”教师看到的是“题库管理、考试管理、阅卷管理”管理员多一个“用户管理”。如果让前端按角色配置一份菜单数组写死也不是不行但一旦后端调整了某个角色权限前端不跟着发版就露馅了。所以在稍微正规的项目里菜单都是后端返的前端根据角色ID动态加载路由。在这个项目里后端登录接口返回的数据里带着role字段和menus数组前端拿到后在路由守卫里通过router.addRoutes动态挂载对应路由表。这样每个账号登录后第一眼看到的菜单就和自己的权限对齐了。你还需要注意一个问题直接访问一个无权限的URL怎么办前端要做全局兜底如果用户手动修改地址栏跳到一个没有注册过的路由就重定向到404页面不能白屏或者弹一堆报错。4.3 axios封装和接口对接的坑axios就得封装不封装的话每个页面都要重复写一遍baseURL、token头还得重复处理错误提示。我在utils/request.js里做了统一处理请求拦截器从Vuex里取token如果有就自动加到请求头Authorization。响应拦截器统一判断response.data.code200直接返回data给业务层401就清空本地存储并跳转登录页500就弹出后端返回的msg。超时时间设置为10秒防止某个慢接口把页面请求卡死。接口对接过程中最常见的坑就是跨域。前端跑在localhost:8080后端跑在localhost:8081axios请求发出去直接CORS报错。解决方式有两种后端加CrossOrigin或者配置CorsFilter前端通过webpack的proxy代理转发。我更推荐生产级的前后端分离项目用nginx做反向代理这样跨域在入口层就被消解了后端代码里连CORS配置都不用写。5. 部署上线从本地打包到服务器运行的全流程这部分是你在别的博客里很容易看一篇糊弄过去的环节——很多人教你“npm run dev启动项目”但正经项目没人用dev模式对外提供服务。我理一下从零到能访问的完整部署链路包括前端打包、后端打包、MySQL初始化、Nginx反代。5.1 后端打包的关键配置SpringBoot后端打包通常用Maven命令就一个mvn clean package -DskipTests如果你想跳过测试代码编译可以加-Dmaven.test.skiptrue。打包之前有三件事必须检查一是application.yml里的数据库地址改成分环境配置开发环境和生产环境分开二是打包后的jar包里不能带前端页面因为前后端分离项目前端是独立部署的三是确认项目里没有写死的完整URL路径否则接口请求在生产环境会打到开发服务器的地址上。还有一个老生常谈的坑JDK版本必须一致。项目里如果用的Java 8特性生产服务器也必须装JDK 8否则启动时直接报UnsupportedClassVersionError。5.2 前端打包与Nginx托管前端打包命令npm run build生成dist目录里面是纯静态文件——HTML、CSS、JS。这一步之前要改环境变量文件.env.production把VUE_APP_BASE_API指到你的公网访问路径比如/api。打包完成后把dist目录整体上传到服务器的nginx/html目录下。nginx配置前后端分离项目的要点在于location规则location / { root /usr/share/nginx/html; try_files $uri $uri/ /index.html; } location /api/ { proxy_pass http://127.0.0.1:8081/; }第一条规则让所有前端路由都落到index.html由Vue Router接管否则你直接访问/user/list这种非物理路径会404。第二条规则把/api开头的请求反向代理到后端jar包的端口是解决跨域的最稳妥方式。唯一要注意的是空格、分号别写漏nginx配置错一个符号就启动失败。5.3 MySQL初始化字符集和时区一个都不能错线上环境部署MySQL时建库语句建议这样指定CREATE DATABASE exam_system DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;如果只设置character set而忽略collation默认排序规则可能是utf8mb4_bin在查询时大小写敏感你会遇到“用户名明明输对了却提示不存在”这种诡异问题。时区问题上连接串后面建议加上serverTimezoneAsia/Shanghai否则数据库默认UTC时间考试开始时间和实际时间差8小时安排考试的人会疯的。连接串完整参考jdbc:mysql://127.0.0.1:3306/exam_system?useUnicodetruecharacterEncodingutf8useSSLfalseserverTimezoneAsia/ShanghaiallowPublicKeyRetrievaltrue上面allowPublicKeyRetrievaltrue是给MySQL 8.0用的不加上可能会遇到“Public Key Retrieval is not allowed”的异常。5.4 全流程部署只需要五步我把整套部署过程浓缩成五步你跟着走就能上线服务器装JDK8或11看项目pom里的java.version。服务器安装MySQL 5.7或8.0建数据库导入项目里的exam_system.sql脚本。上传后端jar包执行java -jar exam-system.jar启动默认端口8081。前端npm run build生成dist上传到nginx指定目录。修改nginx配置如上文的location规则执行nginx -s reload然后浏览器访问服务器IP或域名。实测下来这套流程从零到访问成功熟练的话大概十几分钟。不熟练的同学最容易卡在“导入SQL脚本时报错”比如脚本是5.5版本导出的、里面有DISABLE FOREIGN_KEY_CHECKS在新库里执行一般没事如果报错就建议用source命令逐步导入问题能少很多。6. 源码目录结构与上手建议怎么把项目变成自己的很多人的通病是拿到开源项目mv packages mvn clean install npm install npm run dev从头到尾没看过目录最后代码是跑起来了但让他改一个功能就无从下手。我建议你拿到这套源码先做三件事看后端Controller路由清单、看前端router路由表、看数据库表关系。6.1 后端目录怎么快速看懂后端源码通常以包名分包核心的几个包一眼就能识别controller层放接口入口service层放业务逻辑mapper层放数据库访问接口entity层放数据库实体类config层放配置类common放统一返回类和异常处理utils放工具类。打开任何一个Controller比如ExamController你就能看到“创建考试”“发布考试”“查询考试列表”“查看成绩”这一整条业务线的入口。跟着一个接口从Controller走到Service层再走到Mapper层你就对这个项目的数据流过了一遍比任何教程都有效。6.2 建议从这三个功能开始改造如果你单纯是想练手我推荐你从三个功能里挑一个去改造这三个点在改成别的东西时最典型调整判分规则比如多选漏选从“得一半分”改成“不得分”这需要动ScoreCalculator的判分逻辑新增一个角色比如“阅卷教师”只给主观题批改权限增加一种题型比如“填空题”题型枚举、页面渲染、判分逻辑三处都要改。这三个改动足以逼你把整个系统的数据流摸一遍。6.3 SQL脚本导入的顺序问题很多项目的SQL脚本是一个文件直接导入即可。如果脚本拆成多份注意导入顺序必须满足外键依赖——先导学科表、用户表再导考卷表、试题表最后导答题记录表。不然导入时因外键不满足而失败你还会误以为脚本本身有问题在那排查半天。7. 实操心得我从这套系统里悟到的几件事如果你看完上面的内容准备动手实操我再多说几句体己话。第一千万别在本地同时开三个终端窗口去分别启动后端、前端和数据能借助脚本的尽量脚本化。我习惯写一个start.sh把mysql检查、jar包启动、nginx重启串在一起尽早建立自动化部署的意识比你手动一条一条敲命令高效得多。第二考试系统的联调重点测“时间边界”考试开始前能不能进、考试结束后能不能交卷、考试进行中到点了会不会自动收卷这三个边界是产品经理最容易写进需求里但程序员最容易忽略的。我自己被“到点自动收卷”这个需求坑过一次最后是在后端加了一个定时任务每分钟扫描一次进行中的考试过期就直接把未交卷的记录的答题状态置为过期再配合前端倒计时同步处理才解决。第三源码只是起点不是终点。你完全可以用这套考试系统的骨架去改造一套“企业培训答题系统”或者“问卷调研系统”换一套页面风格加一个用户组功能就变成另一个项目了。这种二次改造的锻炼价值远比从头写一个半吊子系统高得多也最接近真实工作里“维护老项目做新需求”的状态。最后送各位一句话前后端分离考试系统这个题目外行看的是热闹的页面内行看的是它的表设计、权限模型和判分事务边界。把这三点弄懂你在这个领域就算真正入门了。

关于本文作者

来自尧图内容编辑团队

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

尧图内容编辑团队

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

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

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

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

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

网站改版的5个关键决策

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

获取专属建站方案

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

立即免费咨询