ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

Spring Boot + Vue在线考试系统全栈项目实战:从架构设计到部署

Spring Boot + Vue在线考试系统全栈项目实战:从架构设计到部署 1. 项目概述与核心需求解析这几年只要是做管理系统、业务后台、毕设项目的人几乎都绕不开 Spring Boot Vue 这对组合。市面上能看到的在线考试系统demo不少但真正把“登录鉴权、考试状态流转、自动判分、成绩统计”这些核心环节都跑通并且附带可直接导入的数据库脚本和说明文档的项目其实没有想象中那么多。这个“基于 Spring Boot Vue 在线考试系统源码数据库文档”的项目就是一套典型的全栈分离结构后端用 Spring Boot 提供 REST API前端用 Vue 实现单页应用数据库采用 MySQL 来承载用户、试题、试卷、考试记录和成绩等核心数据。拆开来看这个项目能解决的核心问题其实就一句话把线下的“出题、发卷、答题、收卷、判分”完整流程搬到线上并且让管理员、教师、考生三类角色都能在一套系统里各取所需。对开发者来说它更大的价值在于提供了一个可复用的前后端分离脚手架登录状态怎么保持、考试倒计时怎么处理、试卷随机组题怎么做、事务边界画在哪里这些都能从源码里找到现成答案。适合看这个项目的人大致分三类。第一类是刚学完 Spring Boot 和 Vue 基础、想找一个完整项目练手的学生这套系统的代码量不大不小刚好能把框架知识串起来。第二类是准备做毕业设计或课程项目的开发者因为项目自带数据库脚本和文档省去了从头设计表结构的痛苦。第三类是实际有在线考试需求的小团队比如培训机构、企业内部测评拿这套代码做二次开发比从零造轮子快得多。我在接触这个项目时最看重的是它把“考试”这个业务场景里的特殊状态处理得怎么样因为考试系统和普通 CRUD 系统有个本质区别考试过程中不能随意刷新、不能重复提交、倒计时到了必须强制交卷。这些细节如果只在页面层做后端不考虑并发和状态校验线上跑起来一定会出问题。所以后面我会重点拆这几个环节而不是只停留在“某某表怎么建、某个接口怎么调用”的层面。2. 技术选型与整体架构设计2.1 后端为什么锁死 Spring Boot 而不是 SSM 或 Spring Cloud很多人会问既然项目不大用 SSMSpring Spring MVC MyBatis是不是更简单实际上Spring Boot 相比传统 SSM 最大的优势是“自动配置”和“起步依赖”机制。它把以前需要手动配的一大堆 XML 配置收敛成了几个注解和配置文件项尤其适合快速搭建单体应用。这个在线考试系统本质上就是一个典型的单体 Web 应用用户管理、考试管理、成绩管理都集中在同一个进程里没有复杂的分布式诉求所以引入 Spring Cloud 那套注册中心、网关、配置中心的组件反而是过度设计。选 Spring Boot 的另一个现实原因是生态里现成的组件足够顺手。持久层可以用 Spring Data JPA 快速完成实体映射也可以用 MyBatis 写灵活 SQL这套项目两种方式都有参考价值。安全方面可以直接引入 Spring Security 或结合 JWT / Token 机制来处理身份认证不用自己从零写加密和会话管理。再加上内置 Tomcat打包成一个可执行 jar 丢到服务器上就能跑部署成本很低。这里要注意一个高频问题Spring Boot 版本太高导致兼容性翻车。我自己就踩过不少次因为项目依赖的某些第三方库还没有适配最新版本一升级就报各种诡异的 bean 加载失败或配置绑定错误。如果你拿到手的源码是基于 Spring Boot 2.x 开发的就不要轻易升到 3.x因为 3.x 不仅要求 JDK 17还把很多 API 做了模块化调整默认的spring.factories加载机制也换成了AutoConfiguration.imports。除非你对源码结构非常熟否则保持与项目原始版本一致是最稳的做法。2.2 前端为什么选择 Vue 而不是 jQuery 或 React在线考试系统需要频繁操作 DOM、实时渲染倒计时、动态切换题目用传统 jQuery 拼 HTML 字符串会非常痛苦。Vue 的响应式数据绑定天然适合这种场景页面只是数据的状态映射考生点击下一题、上一题只需修改当前题目索引视图自动跟着变。Vue 2 和 Vue 3 的选择需要单独说。老项目很多是 Vue 2 Element UI因为当时 Vue 3 刚出来生态还不成熟Element Plus 也还没稳定。新项目则推荐直接上 Vue 3 Vite Element Plus因为组合式 API 写业务逻辑更干净Vite 的开发服务器启动速度也比 Webpack 时代快一个量级。不过如果你拿到的源码是 Vue 2 的最好别强行升级到 Vue 3项目里的路由写法、组件通信、UI 库引用方式都有差异官方虽然有迁移工具但迁移完通常还要手动改不少地方工作量不小。Vue 项目最容易卡住新人的其实是环境配置。装 Node、配 npm 镜像、创建项目这几个步骤网上教程说法很多版本一不对就会报错。我习惯的做法是直接用npm create vitelatest生成 Vue 3 项目然后根据实际情况安装 Vue Router 和 Pinia。依赖安装完先跑一次npm run dev把基础环境打通再往项目里复制业务代码这样能把环境问题和业务代码问题分开排查。2.3 数据库选型与核心表结构设计MySQL 是这个项目最合理的选择因为考试系统的数据关系比较清晰用关系型数据库管理事务和一致性优势明显。比如一场考试正式开始后考生交卷时后端需要同时更新考试记录状态、写入每道题的作答明细、累加成绩这个动作必须在一个事务里完成MySQL 的 ACID 特性可以保证不会出现“答题记录写了但成绩没算出来”的脏数据。从表设计角度一个在线考试系统至少需要这几张表用户表区分管理员、教师、学生角色、试题表存放题干、选项、正确答案、题型、试卷表定义试卷基本信息、试卷题目关联表或者直接用 JSON 字段存储选到的题目 ID、考试记录表记录某位考生某场考试的状态、答题明细表记录每个题目的作答内容和对错。如果还要支持题库分类、知识点标签、成绩导出等功能再额外扩展相关字段即可。这里我想特别强调一下为什么要给“考试记录表”加状态字段而不是简单地用删除操作。因为考试场景里“已开始”“进行中”“已交卷”“已判分”“已过期”这些状态不是用户随随便便就能改的后端必须根据业务逻辑严格流转。比如考生从未进入考场时状态可能是未开始点了开始考试按钮才生成一条进行中的记录倒计时结束自动交卷后更新为已交卷如果试卷里有主观题还需要由教师手动评分评分完成后状态再变为已判分。只有把状态建模清楚前端页面和后端接口的行为才能保持一致。2.4 前后端分离架构下接口和权限到底怎么划前端单独占用一个端口后端单独占用一个端口开发环境下通常让 Vite 代理后端接口生产环境下则用 Nginx 反向代理。这个项目如果按标准方式搭应该是前端只负责渲染和提交用户操作后端只负责校验权限和业务逻辑两边通过 JSON 格式交互。权限划分是这类系统最容易做模糊的地方。比如学生只能看到自己参与或可参加的考试教师只能出题和改卷管理员能管理用户和所有考试。后端的 Controller 层必须做角色校验不能依赖前端把按钮隐藏了就算安全。我以前见过不少项目前端把管理入口隐藏了但后端的接口只要知道路径随便一个人就能访问这是非常危险的做法。所以这套系统里你如果看到PreAuthorize或自定义拦截器对/admin/**、/teacher/**路径做角色限制属于正常且必须的设计。3. 核心模块设计与实现要点3.1 登录鉴权从 Session 到 Token 的落地方案在线考试系统涉及三类角色后端必须能够识别出当前请求到底是谁、属于什么角色。传统做法是 Session Cookie后端把登录状态存在内存前端带 Cookie 访问。但在前后端分离架构下尤其是接口还可能被小程序或移动端复用时基于 Token 的方式会更通用。这套项目里常见的做法是登录成功后返回一个自定义 Token前端存到本地存储或内存里每次请求带上这个 Token后端通过拦截器解析出用户 ID 和角色信息。与 JWT 结合的方案也很常见JWT 的好处是服务器无状态Token 本身携带过期时间和用户信息但坏处是一旦签发在过期前很难主动吊销。如果要实现“管理员可以把某个人踢下线”这种功能还得引入 Redis 来维护一个黑名单或在线状态。实操里给新手的建议先别把鉴权想得太复杂把登录接口调通、Token 能带在请求头里、后端拦截器能放行已登录用户这三件事搞定系统的权限骨架就立住了。至于刷新 Token、并发登录限制、密码加密策略这些属于后续安全加固阶段的事可以放在第二版再做。3.2 试卷生成手动组卷还是随机抽题做在线考试系统绕不开的问题是怎么把题目装进一张试卷。最简单的做法是教师手工从题库里一道道选题固定下来考生所有人都考同一张卷子。这个方案开发最简单维护也直观但有个明显缺陷相邻考生之间可能互相传答案不同批次考试的人也会用同一套题。更好的做法是支持“固定题 随机题”混合组卷。比如一套试卷配置为单选题从指定题库中随机抽 20 道、多选题随机抽 10 道、判断题随机抽 10 道、主观题由教师固定指定 2 道。这套逻辑在后端实现时通常就是按题型分组随机抽取拼装成一份试卷存到考试记录里。需要注意一定要在考试开始那一刻把本次考试的题目快照保存下来而不是每次进入考试都重新随机加载一次。否则考生刷新页面后看到的是另一套题成绩就完全没法算了。从性能角度考虑随机抽题的 SQL 不能写得太大。低于几千道题的题库用ORDER BY RAND()勉强可以但对于几万条数据这种方式会全表扫描排序非常不推荐。更好的做法是先算出只包含当前题型和难度条件的 ID 列表然后用程序生成一个不重复的随机下标集合再去按主键查询完整题目信息。3.3 考试状态与倒计时一刷新就回到上一步才算合格考生开始考试后会遇到“网络断了”“浏览器崩溃”“不小心关掉页面”这些突发情况。如果一刷新作答记录全部丢失需要重新开始这种体验肯定不会有人接受。所以后端接口在设计上必须支持断点续考考试记录里保存好已经开始的时间、截止时间、已经答过的题目明细考生重新进入时后端根据当前时间和截止时间算出剩余时长把已答内容回显给前端。倒计时的实现也要分两层。前端用setInterval每秒更新一次剩余时间到 0 时自动触发交卷接口。但真正靠得住的还是后端的时间校验后端在保存答案或提交试卷时会校验当前时间是否已经超过考试截止时间如果超了就拒绝继续作答或者直接标记为超时交卷。这样做的好处是防止有人故意改本地电脑时间延长考试或者绕过前端逻辑直接调用后端接口修改作答记录。把状态机和倒计时的校验逻辑放在后端是我在这个项目里最看重的一点。3.4 自动判分与手动评分如何和谐共存客观题单选题、多选题、判断题判分逻辑不复杂后端拿考生提交的答案和标准答案做比较即可。但这里有两个容易踩坑的细节第一多选题的判分要区分“完全一致才算对”还是“漏选、错选按比例得分”第二答案的存储格式必须统一不能有的存AB有的存A,B有的又存成[A,B]否则解析逻辑会越写越乱。一套比较稳妥的存储方案是选项用A|B|C|D这种带分隔符的字符串存储提交答案也用相同格式判分时先转成 Set 再比较顺序不一致也不影响结果。主观题则需要教师手动评分所以试卷表里要加一个字段表示总分和及格分判分时只把客观题分数算出来主观题分数先空着等教师后台评分后再汇总计算最终成绩。成绩统计在数据库中应该尽量用一次查询汇总而不是在 Java 里循环几十个学生算成绩。比如按考试 ID 分组查询每个考生的客观题得分再关联主观题评分表用一条 SQL 完成大部分统计比逐个调接口快得多。4. 实操从零搭建并跑通这个项目4.1 环境准备先弄清楚你要面对的版本组合跑这种“源码数据库文档”的项目目录里通常都会有README.md或者部署文档第一步一定不要跳过去直接启动代码。先把文档里写的 JDK 版本、Maven 版本、Node 版本、MySQL 版本记下来。我推荐的基础组合是JDK 8 或 JDK 11对应 Spring Boot 2.xMaven 3.6配好阿里云镜像仓库避免首次下载依赖慢到怀疑人生MySQL 5.7 或 8.0字符集用utf8mb4Node.js 16 或 18npm 源切换到国内镜像如果是 Vue 3 ViteNode 版本不要低于 16如果项目文档只写了“JDK8”但代码里其实引用了 Spring Boot 2.7那你本地装了 JDK 17 也大概率能跑因为 2.7 支持到 JDK 17 了。真正麻烦的是 Spring Boot 3.x 必须 JDK 17你把源码按 2.x 的思路改成 3.x很多依赖会集体报错所以尽量别在版本上搞创新。4.2 导入数据库看 SQL 脚本的顺序比执行本身更重要数据库脚本一般会拆成两类文件一类是school_student.sql这种建库建表和基础数据脚本另一类是bill.sql这种带模拟数据的脚本。有的项目还会把初始化数据单独放在data.sql里。实际操作时我先在 MySQL 里手动创建一个空数据库CREATE DATABASE exam_system DEFAULT CHARACTER SET utf8mb4;然后执行源码包里的 SQL 文件。执行顺序如果乱了可能出现“某张表不存在”的报错因为后执行的脚本里可能有外键关联或依赖基础数据。建议用 Navicat 或命令行按文件名顺序逐个执行看到每个文件执行成功的提示后再进行下一步。导入之后一定要看一眼后端配置文件里的数据源连接信息。默认配置往往写的是localhost:3306用户名密码是root/root如果你的本地环境不是这个设置不改配置就直接启动跑起来以后报的错都是“数据库连接失败”或者“Access denied for user”非常浪费时间。4.3 启动后端Maven 命令的先后顺序别搞反后端代码拿到手之后我并不建议直接在 IDE 里点运行而是先在命令行执行一次打包这样做能提前暴露很多问题。在项目根目录运行mvn clean package -DskipTests如果项目是分模块的比如有exam-common、exam-system、exam-web这样的多模块结构就需要先确认你所在的目录是根模块。打包成功后会生成target/*.jar直接用java -jar启动也能跑但日常开发调试还是在 IDEA 里找到启动类直接运行更方便。启动日志里重点看三个信息Tomcat 监听端口默认 8080、数据库连接是否成功、Mapper 有没有扫描到。如果启动一闪而过或者日志里出现红色异常多半是配置文件里的 IP、端口、账号密码不对或者某些依赖库版本冲突。启动成功后在浏览器访问http://localhost:8080/如果能出现 Spring Boot 默认的欢迎页或项目自定义的接口文档页说明后端基本没大问题了。4.4 配置并启动 Vue 前端端口不同才是正常的前端项目一般在前端目录或vue-exam下面。先把package.json里的依赖安装好# 进入前端目录 npm install这个命令的执行时间取决于网络状况和依赖包数量有的项目依赖了几百个包装起来很慢。如果装了很长时间还卡住建议把 npm 镜像换成国内源或者删掉node_modules重新装一次。安装完成之后启动开发服务器npm run devVite 默认端口通常是 5173Vue CLI 项目默认是 8080。后端的 8080 和前端开发服务器的 8080 如果冲突了就不是小问题了。我遇到过一次后端和前端都用 8080结果后端先启动前端再怎么编译都访问不到页面白屏。解决办法很简单给 Vue CLI 项目改端口在vue.config.js里设置devServer.port 8081Vite 项目则在vite.config.js里配置server.port。前端能打开后先验证一下登录页用管理员账号登录如果能跳转到首页且菜单能点开说明前后端联调基本通了。4.5 联调与演示别卡在最后的接口代理上开发环境下前端页面请求/api/login但后端接口实际是http://localhost:8080/api/login这个映射关系必须在 Vite 或 webpack 的代理配置里写好。如果代理没配浏览器控制台会报跨域错误接口请求发不出去页面看着像是登录功能坏了。常见的跨域处理有两种后端配置 CORSCrossOrigin或全局 CorsFilter或者前端开发服务器配置代理。我个人的习惯是开发阶段用前端代理生产阶段用 Nginx 代理后端一般不全局放开跨域因为那样等于关闭了浏览器同源策略的保护安全性会打折扣。联调完成后可以走一条完整演示路径管理员创建一场考试指定教师出题给学生分配考试学生登录进入考试页面答完题交卷教师登录后台对主观题评分最后管理员或学生查看成绩报表。如果你拿到手的项目功能没那么全也可以只走“管理员发布考试 - 学生考试 - 系统自动判分并展示成绩”这一段核心链路跑通项目的价值就已经体现出来了。5. 常见问题与排查技巧实录5.1 前端请求接口报跨域到底先查哪个方向跨域报错是前后端分离项目的头号敌人。浏览器控制台出现CORS policy或No Access-Control-Allow-Origin header时我的排查顺序是先看请求到底发到了哪个地址。打开开发者工具 Network 面板看请求的 URL 是不是localhost:8080如果发现请求还指向 5173 前端端口那十有八九是代理没生效或者 vite 配置改了没重启。第二步看后端是否真的收到了请求。如果后端收到了但响应头里没有跨域信息那就是后端没配置 CORS或者配置了但被拦截器提前截住了。有些项目的安全拦截器会在进入 Controller 之前直接返回 401导致前端连跨域响应头都看不到这时候需要先放行 OPTIONS 预检请求。5.2 考试倒计时和交卷时间对不上前端倒计时归零自动调交卷接口结果后端记录的时间比前端显示的时间多了几秒这其实是正常现象因为网络请求本身有延迟。但如果差异太大比如前端显示还有 20 分钟后端却提示考试已截止那就要检查系统时间是不是不一致了。在后端起服务的那台机器上用date命令查看系统时间如果服务器时区设的不是Asia/ShanghaiJackson 序列化时间的格式就会出问题考试时间可能被转成 UTC 导致差 8 小时。处理办法是在后端配置文件里加spring.jackson.time-zoneGMT8数据库连接串里也可以加上serverTimezoneAsia/Shanghai来避免 MySQL 驱动解析时间异常。5.3 数据库连接失败但账号密码明明没问题这种问题通常出在 MySQL 驱动版本或者连接参数上。比如 MySQL 8.0 需要使用com.mysql.cj.jdbc.Driver而老项目里写的是com.mysql.jdbc.Driver就会启动直接报驱动类找不到。另外还要确认 MySQL 服务是不是真的启动了如果是 Docker 跑的数据库还要看端口有没有映射到宿主机。一个容易被忽略的坑是数据库字符集。如果建表时用了错误的字符集或者排序规则插入中文数据可能报Incorrect string value错误所以创建数据库时我都会显式指定utf8mb4而不是依赖默认配置。5.4 Spring Boot 版本太高、依赖冲突怎么处理这类问题在源码项目里太常见了。拿到项目第一件事看pom.xml里用的 Spring Boot 版本是多少再看本机 Maven 仓库里下载的依赖是什么。如果本地之前装过别的项目依赖Maven 仓库可能缓存了版本信息有时候删掉~/.m2/repository里对应目录重新下载才能解决奇怪的冲突。遇到ClassNotFoundException或NoSuchMethodError别急着怀疑代码逻辑先查是不是某个依赖的版本被 Maven 仲裁成了不兼容的版本。在 IDEA 里按Ctrl Shift F10运行项目时控制台如果出现APPLICATION FAILED TO START通常会明确提示是哪个 bean 创建失败或哪个配置属性找不到照着提示去排查基本上都能解决。5.5 Vue 项目启动报 tsconfig 找不到或者依赖缺失如果你拿到的前端源码是用 TypeScript 写的启动时可能会报Failed to load tsconfig vue/tsconfig/tsconfig.web.json这种错误原因是项目里引用的 tsconfig 依赖包没装全。解决方式是在前端目录执行npm install或者单独安装npm install -D vue/tsconfigNode 版本和依赖不匹配也可能导致启动直接崩尤其是sass、node-sass这类原生模块。优先使用sass因为它支持最新的 Node 版本如果项目锁死了node-sass大概率需要按照项目文档里指定的 Node 版本重新安装。6. 对二次开发的一些实际想法项目跑通只是第一步真正把它用到实际考试场景里还有不少值得完善的地方。如果让我在这个源码基础上继续做我第一优先会加 Redis 缓存和分布式锁尤其是在“同一考生同时提交试卷”这种极端并发下后端必须保证同一场考试同一考生只生成一条有效的交卷记录否则成绩数据会重复或错乱。用 Redis 的SETNX做简单防重提交再配合数据库唯一索引兜底是比较稳妥的思路。第二优先是试卷的安全设计。现在的系统如果直接按固定试卷考题库泄漏风险很大。改进方向包括按考生维度打乱选项顺序、同一套题每个人看到的选项顺序不同以及限制考生的切屏次数和复制粘贴行为。这些功能看起来很小但对实际使用的安全感提升明显。第三把成绩分析做厚一点。考试系统如果只有总分展示价值比较有限。可以基于答题明细按题型、知识点维度统计正确率为教师调整教学重点提供数据依据。这部分功能并不复杂后端多几个统计接口前端用 ECharts 画图表即可。对于网上提到的那些“源码怎么发给别人”的问题我的建议是把后端打成 jar、前端构建出 dist 目录连同 SQL 脚本和部署说明一起打包对方只需要准备环境和数据库就能跑起来。千万记得把配置文件里的数据库密码和 Token 密钥在生产环境重新生成别把测试环境的敏感信息直接带到线上。我个人在实际操作中的体会是这种“源码数据库文档”的项目价值不在于代码多高深而在于它把一整套完整业务闭环串了起来。先别急着改功能按原样跑通一遍了解每条数据是怎么流转的再去调整、优化、二次开发会比直接上手改代码高效得多。最后再分享一个小技巧拿到任何新项目先在本地建一个专门的环境变量文件或配置 profile把所有本地环境和项目的默认配置区分开这样无论怎么折腾都不会弄脏原始代码排查问题也能下手更快。
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进