拓冰建站拓冰建站
首页 / 资讯中心 / 正文

SpringBoot+Vue3在线考试系统实战:前后端分离架构从零落地

这套系统我前前后后用了大概三周时间从设计到落地后端用Java SpringBoot前端Vue3数据访问层MyBatis数据库MySQL整体走前后端分离架构。整理源码的时候我突然觉得这不仅仅是一个在线考试系统更是一个把日常开发里最常见的Java技术栈完整串起来的实战样本。很多学Java的同学都有过这种体验SpringBoot能写Hello WorldVue3能渲染一个列表但真要独立做一个完整项目就卡在前后端怎么打通、表结构怎么设计、考试这种强并发场景怎么处理这些坎上。这个在线考试系统正好把这些点都覆盖了。它不是那种炫技型项目但麻雀虽小五脏俱全——适合正在准备Java面试的人用来理解框架在实际业务中的配合方式也适合想找一个完整项目练手、扩充简历项目经验的开发者。1. 项目拆解在线考试系统到底在考什么1.1 为什么选用前后端分离架构先说一个很实在的对比。如果是传统的单体架构用JSP加Servlet模板渲染前端每改一个样式后端可能都要重新编译重启前后端代码揉在一起接口也没法复用。而在线考试这类系统天然有多个端的需求——学生要用浏览器页面答题管理员要用管理后台维护试题和查看成绩未来可能还要做小程序端如果接口和后端页面绑死每一次扩展都是一次重构。前后端分离之后后端只负责提供JSON接口前端独立开发和部署两边只需要约定好接口文档就能并行推进。实际开发中的体感也很明显后端这边改一个接口逻辑重启只需要几秒前端在Vite开发服务器里调试热更新几乎是即时的整个调试节奏比传统模式轻松太多。而且这个项目本身也是一个很好的面试话术样本——前后端分离的优缺点、跨域问题怎么解决、接口鉴权怎么做这些问题在面试里出现频率极高亲手做一遍远远比背八股文记得牢。不过前后端分离不等于前端完全独立。线上部署时前端构建出来的dist目录可以直接交由Nginx托管后端API放在独立的服务里前端通过域名区分。这套系统在本地开发时则是前端跑在5173端口、后端跑在8080端口两者之间通过代理转发这样既解决了跨域问题也模拟了生产环境的网络隔离。1.2 核心功能模块与应用场景分析从业务角度看在线考试系统可以拆成两条主线和一条公共线。主线一面向考生包括注册登录、查看考试列表、进入考试、在线答题、交卷、查看成绩。主线二面向管理员包含用户管理、试题管理、试卷管理、考试发布、成绩统计。公共线则是验证码、角色权限控制、操作日志等支撑能力。这里有几个模块值得细说。题库管理必须支持多种题型单选、多选、判断、简答至少这四类是基础。试卷管理要考虑组卷方式简单的做法是管理员手动从题库选题进阶一点的可以做成按规则随机抽题比如指定单选题数量、每种题型分值系统自动生成一套试卷。考试发布则要设置好时间窗口和时长比如早上9点到10点之间可以进入考试进入后必须在30分钟内交卷——这种时间相关逻辑在考试场景里很容易踩坑后面我会详细讲。应用场景其实非常广。学校的院系平时测验、培训机构的结业考核、企业内部的技术认证、各种竞赛的初赛筛选甚至一些招聘笔试环节都能复用这套逻辑。更关键的是这类后台管理系统的骨架——用户管理、角色权限、数据列表、表单编辑——几乎可以无缝迁移到其他业务系统上。你学会的是在线考试但写出来的代码骨架其实是一个标准的Vue3后台管理系统加SpringBoot接口服务的组合。2. 技术选型解析这套技术栈为什么能打2.1 SpringBoot后端开发效率的引擎如果不用SpringBoot传统Spring项目需要手动配置一大堆XML文件项目还没开始写业务代码光配置就要折腾半天。SpringBoot最大的价值是约定优于配置默认帮你完成绝大部分自动装配你只需要在application.yml里写上数据源、端口这些必要信息。选SpringBoot版本时要特别注意。网上教程很多还在用2.x如果你拿到一个2.7.x的项目JDK一般用8或11包名是javax开头如果项目用的是SpringBoot 3.x那JDK至少要17包名换成了jakarta。这两者在导入依赖、写代码时的细节上有不少差别。我整理的这套源码选的是SpringBoot 2.7.x兼容性更好对新手更友好JDK 8也能跑方便大家直接在本地复现。配置方面有几个点值得强调。application.yml里数据源配置如果写错启动阶段就会直接报错所以数据库地址、账号、密码务必确认清楚。项目启动时可能还要区分开发环境、测试环境、生产环境SpringBoot原生支持通过spring.profiles.active切换不同配置文件比如application-dev.yml、application-prod.yml这个机制在真实项目里几乎是标配。2.2 MyBatis与MySQL数据层的搭配逻辑MyBatis是一个半自动ORM框架SQL还是你自己写框架只负责参数映射和结果集映射。有人觉得这不如JPA省事但考试系统这种业务恰恰需要这种可控性——成绩统计、试卷查询这些SQL往往涉及多表联查和动态条件用MyBatis的XML手写SQL反而更清晰。很多同学会问MyBatis和MyBatis-Plus到底怎么选我的看法是如果你的业务以单表CRUD为主MyBatis-Plus确实能省很多事内置的BaseMapper自带增删改查还有分页插件。但考试系统里的题目关联查询、成绩统计、按状态筛选考试列表这些复杂SQL用MyBatis原生XML更好把控不至于被自动生成SQL的规则限制住。MyBatis的动态SQL能力本身也足够强if、where、foreach这些标签组合起来能覆盖绝大多数场景。缓存是另一个高频面试点。MyBatis一级缓存是SqlSession级别的默认开启同一个SqlSession内多次查询同一语句会命中缓存。二级缓存是namespace级别的默认关闭多一个Mapper范围可能会出现脏读问题。考试系统的成绩数据要求强一致所以这套系统我没有开二级缓存避免考生交卷后成绩不能立刻刷新的尴尬。MySQL选8.0很大一部分原因是默认字符集是utf8mb4中文和emoji都能正常存储而且支持窗口函数、JSON字段这种实用特性。说明一下考试系统里的题目选项我用JSON字段存储因为不同题型的选项数量不一样用JSON数组存储灵活性高读取出来直接是结构化数据省去了关联子表的复杂查询。当然这也有代价如果你需要对选项做复杂的SQL统计JSON字段就没那么方便了。对于这套系统来说JSON方案明显更合理。2.3 Vue3前端交互与工程化前端选Vue3而不是Vue2和SpringBoot选版同理——新项目在不考虑历史包袱的情况下没有必要用旧技术。Vue3的组合式API把逻辑按功能组织setup语法糖加上ref、reactive、computed这套响应式方案代码的可复用性比Vue2的选项式API好很多。再加上Vite带来的极速启动和热更新开发体验完全是两个时代。组件库这块后台管理系统用Element Plus是最稳妥的选择。表格、表单、弹窗、消息提示、分页这些后台的高频组件全都现成自己手写一套既不现实也没必要。状态管理用Pinia替代VuexAPI更简洁TypeScript支持也更好。路由用Vue Router配合全局前置守卫做登录校验——没有token就跳转到登录页这个是最基础但也最实用的权限控制方式。还有一个小细节值得说Vue3支持JSX封装复杂组件时确实优雅但常规页面开发里模板语法已经完全够用不必刻意追求JSX。项目里Axios单独封装了一层请求拦截器自动携带token响应拦截器统一处理业务状态码和HTTP异常后端一报错前端就能弹出统一的提示信息这个实践在真实项目里非常关键。3. 从零搭建核心功能模块的设计与实现3.1 数据库设计ER模型与关键表结构在线考试系统的表结构设计核心是四张主表和一张明细表。我直接列出建表语句里最关键的几张表字段不追求冗余但每一张都有它存在的理由。用户表用于存放系统所有账号包括学生和管理员角色字段区分权限CREATE TABLE user ( id bigint(20) NOT NULL AUTO_INCREMENT, username varchar(50) NOT NULL COMMENT 登录名, password varchar(100) NOT NULL COMMENT BCrypt加密后的密码, nickname varchar(50) DEFAULT NULL COMMENT 昵称, phone varchar(20) DEFAULT NULL, role tinyint(4) NOT NULL DEFAULT 0 COMMENT 0-学生 1-管理员, status tinyint(4) NOT NULL DEFAULT 1 COMMENT 1-启用 0-禁用, create_time datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), UNIQUE KEY uk_username (username) ) ENGINEInnoDB AUTO_INCREMENT1000 DEFAULT CHARSETutf8mb4 COMMENT用户表;考试表记录一次考试的基础信息和时间窗口CREATE TABLE exam ( id bigint(20) NOT NULL AUTO_INCREMENT, title varchar(100) NOT NULL COMMENT 考试标题, description varchar(500) DEFAULT NULL, start_time datetime NOT NULL COMMENT 考试开始时间, end_time datetime NOT NULL COMMENT 考试结束时间, duration int(11) NOT NULL COMMENT 考试时长(分钟), total_score int(11) NOT NULL DEFAULT 100, pass_score int(11) NOT NULL DEFAULT 60, status tinyint(4) NOT NULL DEFAULT 0 COMMENT 0-未发布 1-进行中 2-已结束, PRIMARY KEY (id), KEY idx_start_end (start_time,end_time) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT考试表;试题表题目归属到具体考试因为不同考试即使题目相同分值也可能不同直接用exam_id关联最简单CREATE TABLE question ( id bigint(20) NOT NULL AUTO_INCREMENT, exam_id bigint(20) NOT NULL, type tinyint(4) NOT NULL COMMENT 1-单选 2-多选 3-判断 4-简答, content text NOT NULL COMMENT 题干, options json DEFAULT NULL COMMENT 选项JSON数组, answer varchar(500) DEFAULT NULL COMMENT 正确答案, analysis text COMMENT 解析, score int(11) NOT NULL DEFAULT 0, sort_order int(11) NOT NULL DEFAULT 0, PRIMARY KEY (id), KEY idx_exam_id (exam_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT试题表;考试记录表和答题明细表这两张表承担了考试过程中最核心的数据追踪CREATE TABLE exam_record ( id bigint(20) NOT NULL AUTO_INCREMENT, user_id bigint(20) NOT NULL, exam_id bigint(20) NOT NULL, start_time datetime DEFAULT NULL, submit_time datetime DEFAULT NULL, score int(11) DEFAULT NULL, status tinyint(4) NOT NULL DEFAULT 0 COMMENT 0-考试中 1-已交卷, PRIMARY KEY (id), UNIQUE KEY uk_user_exam (user_id,exam_id), KEY idx_exam_id (exam_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT考试记录表; CREATE TABLE exam_record_detail ( id bigint(20) NOT NULL AUTO_INCREMENT, record_id bigint(20) NOT NULL, question_id bigint(20) NOT NULL, user_answer varchar(500) DEFAULT NULL, is_correct tinyint(4) DEFAULT NULL COMMENT 0-错误 1-正确 2-待批改, score int(11) DEFAULT NULL COMMENT 该题得分, PRIMARY KEY (id), KEY idx_record_id (record_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT答题明细表;这里要聊几个设计决策。第一为什么不做物理外键物理外键在并发写入时会有额外的锁开销而且一旦业务上需要分库分表物理外键就是最大的障碍。实际项目里普遍的做法是用逻辑外键在Service层通过事务保证数据一致性考试系统也是这个思路。第二为什么user_id和exam_id要建唯一索引这是为了防止同一考生重复提交数据库层面兜底即使接口没有被正确调用重复数据也插不进去。第三记录明细里为什么存user_answer的原始文本这是为了保留考生的实际作答内容否则考后考生申请复议你连他当时填了什么都不知道。3.2 后端核心接口实现从组卷到判分后端代码按经典的分层结构组织Controller负责接收参数和返回结果Service负责业务逻辑Mapper负责数据库访问。这套结构虽然简单却是真实项目里最实用、最容易维护的分层方式。举一个最有代表性的接口——提交答卷。这是整个系统里业务逻辑最复杂、最容易出错的地方接口背后隐藏着大量细节。第一步校验用户身份。通过请求头的Authorization拿token解析出用户ID和角色确认当前用户是合法考生。第二步校验考试状态。从数据库查出考试信息判断当前时间是否在start_time和end_time之间时间没到或者已经结束都不能提交。这里有个容易被忽视的细节考试时间是管理员配置的时间窗口还是考生进入考试后的倒计时两种逻辑都常见但实现方式不同。我这套系统采用进入后倒计时的方式考生点击开始考试后端记录start_time交卷时判断当前时间是否超过start_time duration。这样即使前端倒计时被绕过后端也能兜底校验。第三步判分。遍历考生提交的答案列表逐题加载题目并比对答案。单选题和判断题只需要逐字符比较多选题需要判断全对才得分或者漏选得一半分——具体逻辑做成可配置项业务方根据需要选择。简答题默认不给分标记为待批改由管理员在后台人工阅卷打分。第四步保存记录。使用Transactional开启事务将exam_record和exam_record_detail两次写入包裹在同一事务中。之所以要事务是因为如果先插入考试记录成功、插入答题明细失败会造成一条没有明细的孤儿记录考生成绩直接丢失。事务保证这两张表的写入要么全部成功要么全部回滚。核心代码大致是这个结构Transactional(rollbackFor Exception.class) public SubmitResult submitAnswer(SubmitRequest request, Long userId) { Exam exam examMapper.selectById(request.getExamId()); if (exam null) { throw new BusinessException(考试不存在); } ExamRecord record examRecordMapper.selectByUserIdAndExamId(userId, request.getExamId()); if (record null || record.getStatus() 1) { throw new BusinessException(考试记录不存在或已交卷); } // 校验答题超时 DateTime now DateTime.now(); if (now.isAfter(record.getStartTime().plusMinutes(exam.getDuration()))) { throw new BusinessException(考试时间已结束); } // 逐题判分 int totalScore 0; ListExamRecordDetail details new ArrayList(); for (AnswerItem item : request.getAnswerItems()) { Question question questionMapper.selectById(item.getQuestionId()); boolean correct checkAnswer(question, item.getUserAnswer()); int questionScore correct ? question.getScore() : 0; totalScore questionScore; details.add(buildDetail(record.getId(), question, item.getUserAnswer(), correct, questionScore)); } // 先更新记录表 record.setStatus(1); record.setSubmitTime(now); record.setScore(totalScore); examRecordMapper.updateById(record); // 再批量插入明细 if (!details.isEmpty()) { examRecordDetailMapper.batchInsert(details); } return SubmitResult.success(totalScore); }另一个值得说的是组卷接口。手工组卷逻辑比较简单管理员勾选题目后端保存试卷快照考生看到的试卷在考试期间不能变。随机抽卷就麻烦一些需要支持按题型指定数量从题库随机抽取确保每次考试生成的试卷不完全相同。为了防止随机抽取性能问题可以先查出符合条件的题目ID列表再用limit加随机偏移量取值或者直接用order by rand()配合limit数据量不大的时候完全够用这里我没有用复杂的分库方案。分页查询是后台管理系统的标配。自己的分页别乱写直接用PageHelper或者MyBatis-Plus的分页插件都行。注意分页查询的结果需要返回总数方便前端渲染分页组件这个看似简单的小问题很多新手项目都会漏掉总数返回导致分页不完整。3.3 前端页面与交互逻辑答题体验是重头戏前端这边登录页、考试列表页、答题页、管理后台的题目管理页、成绩统计页是五个核心页面。其中答题页的交互体验直接决定整套系统的使用感受我在这里花的时间最多。答题页有几个关键交互点。倒计时功能用setInterval实现每秒更新一次剩余时间倒计时归零时自动提交试卷。这里有一个必须处理的坑组件销毁时一定要清理定时器否则页面切走之后计时器还在后台运行浪费资源不说还可能导致意外的自动提交。更稳妥的做法是在beforeUnmount或onUnmounted钩子里调用clearInterval。答题状态管理用Pinia的store存用户每选一题就把答案写入store同时可以定时把未提交的答案备份到localStorage。这样即使考生不小心关闭了浏览器重新打开后还能从本地缓存恢复大部分作答内容。不过本地存储只是辅助手段真正的数据可靠性还得靠前端定时调用后端接口进行答题进度上报把用户答案增量同步到数据库这是考试系统的加分项。交卷前必须做二次确认。用一个Dialog弹窗提示还有哪些题目未作答让考生确认是否继续交卷。这个交互看着简单实际价值很大能有效避免考生因为误触交卷按钮导致成绩作废。管理后台页面则更关注表格和表单的效率。试题管理页用的是Element Plus的el-table加弹窗表单题目列表支持按题型筛选、分页展示编辑试题时单选题和多选题会有动态的选项添加和删除选项数量由前端动态控制。成绩统计页用el-table展示每个考生的得分配合搜索和导出功能导出功能可以用前端把表格数据转成CSV文件也可以用后端POI生成Excel我这边用的是后端导出大数据量时更稳定。4. 实操过程本地运行这套系统的一步步操作4.1 环境准备JDK、Maven、MySQL与Node.js这套系统的本地运行环境其实很常规但不少人恰恰卡在环境配置上。我把每一步的要点都整理出来按顺序操作基本不会有问题。JDK方面因为项目基于SpringBoot 2.7.x推荐JDK 8当然JDK 11也兼容。安装JDK后需要配置环境变量重点就三个JAVA_HOME指向安装目录PATH里加上%JAVA_HOME%\binCLASS_PATH可以设为.,%JAVA_HOME%\lib\dt.jar;%JAVA_HOME%\lib\tools.jar。配置完打开命令行执行java -version能正常显示版本号就说明成功了。Maven下载二进制zip包后解压到本地目录同样需要配置MAVEN_HOME和PATH。这里有一个关键点打开Maven的settings.xml把本地仓库地址改成自己习惯的路径然后配置阿里云镜像不然依赖下载速度会慢到怀疑人生mirror idaliyunmaven/id mirrorOfcentral/mirrorOf name阿里云公共仓库/name urlhttps://maven.aliyun.com/repository/public/url /mirrorMySQL 8.0的安装步骤网上教程很多我只提醒几个容易出错的地方。第一字符集一定要选utf8mb4不然后续中文数据会乱码。第二root密码设置后要记住后面连接数据库要用。第三Windows安装时如果遇到服务启动失败八成是端口被占用用netstat -ano | findstr 3306排查一下。安装完成后创建一个数据库exam然后导入项目的初始化SQL脚本mysql -u root -p exam exam.sqlNode.js建议装16以上的LTS版本npm安装依赖时如果速度太慢同样配置淘宝镜像npm config set registry https://registry.npmmirror.com4.2 启动与联调从源码到跑起来的完整流程环境都准备齐了就可以启动项目了。整个流程大概是四步导入数据库、配置后端、启动后端、启动前端。数据库导入完成后打开后端项目的application.yml把数据源信息改成你自己的数据库地址和账号密码。注意SpringBoot 2.7.x配MySQL 8.0数据库驱动要配成com.mysql.cj.jdbc.Driver连接串里建议加上useUnicodetrue和characterEncodingutf8不然容易出现中文乱码spring: datasource: url: jdbc:mysql://localhost:3306/exam?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/Shanghai username: root password: 你的密码 driver-class-name: com.mysql.cj.jdbc.Driver后端启动有两种方式。在IDEA里直接打开项目等待Maven依赖下载完然后运行启动类里的main方法或者命令行执行mvn spring-boot:run。不管用哪种方式看到类似Started Application in 5.32 seconds的日志就代表后端启动成功。前端启动更简单。进入前端项目目录执行npm install安装依赖这一步可能耗时较长如果某个依赖版本冲突可以先删除node_modules目录再重新安装。依赖装好后执行npm run devVite会启动一个开发服务器控制台输出localhost:5173的访问地址。浏览器打开这个地址如果能看到登录页说明前端已经起来了。联调验证建议走一条完整流程。第一步用管理员账号登录后台新增一个考生用户、创建一场考试录入十几道不同类型的题目设置好时间窗口发布考试。第二步退出再注册一个学生账号进入考试列表点击开始考试做几道题后交卷。第三步回到管理后台查看成绩确认客观题得分正确。第四步到数据库里查exam_record和exam_record_detail两张表核对考生的原始作答记录。整条链路走通这套系统就算真正跑起来了。4.3 常见问题与排查技巧实录本地跑项目遇到报错是常态我这里把最常见的问题和排查思路整理成表格都是群里朋友问过或者我自己踩过的坑问题现象常见原因解决方案后端启动报数据库连接失败MySQL服务没启动或URL、账号密码错误先确认3306端口是否监听再用命令行连接测试前端控制台报跨域请求失败前后端端口不同未配置代理或CORS前端Vite配proxy或后端加CORS配置类中文显示乱码数据库字符集不是utf8mb4或连接串没加编码参数建库时指定utf8mb4连接串加useUnicode和characterEncodingMyBatis报Invalid bound statementXML文件路径与Mapper接口不一致检查mapper-locations配置和XML的namespace依赖下载超时Maven默认走中央仓库太慢配阿里云镜像密码字段存入数据库后变成整串乱码忘记用BCrypt加密应该用BCryptPasswordEncoder加密后存储跨域问题值得单独说。开发环境下最优雅的解法是前端配置代理开发Vite配置文件里加server: { proxy: { /api: { target: http://localhost:8080, changeOrigin: true } } }这样前端请求/api/xxx会被自动转发到8080端口浏览器的同源策略不会拦截因为请求发出去的同源地址是5173。如果不上代理就只能在后端写一个CORS配置类两种方式在本地开发都能解决但生产环境用Nginx反代更规范。排查MyBatis绑定错误我总结了一个固定流程先看application.yml里的mapper-locations路径是否和实际XML目录一致再看XML的namespace是否对应Mapper接口全限定名最后检查接口方法名和XML里select/insert的id是否匹配。三个地方都确认了问题基本就能解决。排查数据库乱码也一样有套路表结构utf8mb4、连接URL带characterEncodingutf8、数据库连接池没有强制其他编码这三层都排除掉中文一般就不会乱。写在最后一些真实的项目体会整套系统从设计到最终跑通我的感受是开发在线考试系统真正的难点不在某个单一技术而在各种边界情况的处理。考试时间怎么校验、重复交卷怎么拦截、断网了怎么恢复、多选漏选怎么打分、并发交卷怎么防穿——这些问题每一个单独拿出来都不复杂但全部凑在一起就是一套能拿得出手的完整项目。最后再分享一个日常调试的小技巧。开发MyBatis项目时把SQL日志打印开关打开在application.yml里加上一行mybatis: configuration: log-impl: org.apache.ibatis.logging.stdout.StdOutImpl这样每次请求控制台都会打印实际执行的SQL语句和参数值排查数据问题比任何调试工具都直观。这个配置对开发阶段极有价值看到SQL心里就有底了。
分享:

看完干货,该让你的企业上线了

免费需求沟通 · 48 小时内出具建站方案 · 河南本地可上门