SpringBoot+Vue在线考试系统:前后端分离开发与部署指南
做后台管理系统、交毕业设计或者想练手前后端分离项目的人大概率都搜过“在线考试系统”这类关键词。SpringBoot Vue MyBatis MySQL四个词一套基本就代表了目前JavaWeb领域最主流的一整套前后端分离技术栈。这套在线考试系统源码不是什么营销号送的阉割版GitHub搬运货而是一个能跑通全流程、能部署上线、能二次开发的项目。我基于自己复现这类项目的实际经验把这套系统的设计思路、核心表结构、前后端关键代码逻辑、从零到一部署的完整流程以及我踩过的一些坑全部梳理一遍。不管你是拿了源码想本地跑起来看看效果还是打算基于它改造成自己的毕设或者单纯想搞懂“前后端分离在线考试系统”这类项目内部到底怎么设计的这篇文章都值得从头到尾过一遍。文章不会教你怎么点鼠标只会讲清楚那些真正影响项目能不能落地的东西。1. 项目整体设计与架构思路1.1 为什么要用前后端分离而不是传统单体JSP项目先说一个很多新手容易忽略的点。早期JavaWeb项目大多是JSP Servlet甚至SpringMVC直接返回ModelAndView前端页面和后端接口全在一个Tomcat里跑。这种模式最大的痛点不是代码量而是分工和部署上都极其别扭——前端工程师没法独立工作改个页面样式都要重启整个后端服务后端接口稍微变个数据结构前端页面跟着崩。前后端分离以后Vue负责页面渲染、路由跳转、状态管理后端SpringBoot只负责输出JSON数据双方通过HTTP接口通信。这套在线考试系统采用的就是这种模式前端Vue项目可以单独跑在Node环境下开发调试后端SpringBoot单独跑在8080端口联调时通过代理或跨域配置打通。生产部署时前端构建成静态文件丢给Nginx后端打成jar包独立运行甚至可以分开部署在不同服务器上扩展性和容错性都上了一个档次。1.2 技术选型背后的真实考量聊聊这套技术栈为什么是“标配”以及选型时都在平衡什么。SpringBoot选它不是因为“大家都在用”而是因为它解决了一个很实际的问题——配置地狱。传统SSM项目要写web.xml、applicationContext.xml、spring-mvc.xml一堆配置文件互相引入稍微搞错一个标签项目起都起不来。SpringBoot用自动配置加application.yml几行配置就能把项目跑起来。在线考试系统涉及登录鉴权、数据库访问、文件上传比如导入试题ExcelSpringBoot对这三块都有成熟的生态支持。MyBatisORM框架有JPA和MyBatis两大阵营考试系统这种业务更适合MyBatis。原因很直接考试系统有大量复杂查询——按条件筛选题目、多表关联查询考试和成绩、统计通过率这些场景SQL的掌控力比对象关系映射更直观。MyBatis让你手写SQL虽然代码量多一点但每条SQL都在你脑子里排查性能问题的时候心里有底。像“查询某场考试所有学生的总分并排序”这种逻辑手写一条带聚合函数的SQL比JPA的Specification写法顺畅太多了。Vue前端这块无论选Vue还是React都能做但Vue的上手曲线更平缓模板语法直观配套的Element UI / Element Plus组件库对后台管理类页面覆盖度很高。考试系统的页面主体就是表格加表单Vue的组件化开发方式再加上v-model双向绑定写答题页时考生勾选选项、输入文本数据直接同步到状态里开发效率确实快。MySQL考试系统的事务性要求高——交卷时既要写答题记录又要算总分、更新考试状态这必须靠MySQL的事务保证。另外考试系统的数据量级撑死几十万行MySQL完全够用部署和维护成本还低。1.3 功能模块拆分与权限设计整个考试系统从角色维度可以分成三个用户端这是权限设计的基础学生端登录注册、查看可参加的考试列表、在线答题单选题、多选题、判断题、简答题、交卷后查看成绩和答案解析。教师端维护题库增删改查题目、创建考试选择题目组成试卷、设置考试时间、批阅主观题、查看成绩统计。管理端用户管理新增学生、教师账号重置密码、考试数据总览、系统设置。功能模块拆成五个核心板块用户认证、题库管理、考试管理、在线考试、成绩管理。权限这块用最经典的JWTJSON Web Token一种无状态登录令牌方案 角色枚举实现。后端拦截器拦截需要登录的接口从请求头里的Token解析出用户ID和角色再用注解或手动判断角色的访问权限。比如“创建考试”接口只有教师和管理员能调“提交答卷”接口只有学生能调。具体实现细节后面会细讲。2. 数据库设计与核心表结构详解数据库设计是这类项目的根基表结构没设计好后期写接口的时候全是坑。这套系统我拆解下来核心表大概六到八张下面挑重点讲并直接给出建表字段参考。2.1 用户表sys_user用户表要同时容纳三种角色所以不能用角色字段区分三张表。用一个role字段用数字标识角色线上常用0/1/2分别对应管理员、教师、学生。字段名类型说明idbigint主键自增usernamevarchar(50)登录用户名唯一索引passwordvarchar(100)加密后的密码BCrypt加密real_namevarchar(50)真实姓名roletinyint角色0管理员1教师2学生statustinyint账号状态0正常1锁定create_timedatetime创建时间密码为什么不用MD5MD5是快速摘要算法碰见彩虹表基本裸奔。SpringSecurity自带的BCryptPasswordEncoder会对同一个密码加随机盐每次加密结果都不同即使数据库泄露也没法直接反查。这是默认不解释的选择但安全这块不能图省事。2.2 考试表与试题表设计题库表question是整个系统的核心资产设计上要区分题目类型和选项存储字段名类型说明idbigint主键typetinyint1单选2多选3判断4简答contenttext题干optionstextJSON字符串如{A:北京,B:上海...}answervarchar(10)客观题答案如A、ABD、对scoreint每题分值create_bybigint出题教师ID选项用一个JSON字符串字段存储而不是单独建一张选项表原因是出题人和考试系统的题目选项基本不会超过5个存JSON查询时直接解析建单独表反而过度设计。后端用Fastjson或Jackson解析这个字段都很方便。考试表exam记录一次考试的基本信息字段名类型说明idbigint主键titlevarchar(100)考试名称start_timedatetime开考时间end_timedatetime截止时间durationint考试时长分钟total_scoreint总分statustinyint考试状态0未开始1进行中2已结束creatorbigint创建人ID2.3 考试与题目的关联实战中最容易忽略的中间表一场考试有很多题一道题可以被多场考试引用这是典型的多对多关系必须引入中间表exam_question字段名类型说明idbigint主键exam_idbigint考试IDquestion_idbigint题目IDseqint题目序号决定试卷排版顺序这张表存在有一个很重要的业务原因——试卷快照。教师组完卷后题库里的题目可能被改甚至被删但如果通过中间表引用的是题目ID查询时还能JOIN到最新内容就会导致同一场考试中途题目变了。稳妥做法是exam_question里再冗余一份question_content快照字段考试进行中一律读快照保证考生看到的试卷和交卷判分的依据始终一致。这是个非常实战的细节直接决定系统靠不靠谱。答题记录和成绩表exam_record、answer_record这里不展开核心思路是exam_record记录学生某场考试的状态和总分answer_record记录每题作答内容考生选择的选项或写的文本。3. 后端核心实现与关键代码逻辑3.1 后端项目分层结构源码的包结构是标准的三层架构加公共模块但实际开发中我会更细分com.exam ├── controller // 接口层只做参数接收和结果封装 ├── service // 业务层核心逻辑都在这 ├── mapper // MyBatis数据访问层 ├── entity // 数据库实体类 ├── dto // 接口出入参对象 ├── common // 通用工具、常量、异常处理 ├── config // 配置类跨域、拦截器、JWT └── interceptor // 登录拦截器分层的核心原则是Controller不写业务逻辑。考试交卷这种一个接口逻辑很复杂的在Controller里堆代码后续想复用、想测试都困难。3.2 JWT登录鉴权完整流程前后端分离项目登录鉴权一般不推荐用Cookie Session。原因有几点第一前后端分离后前端可能和后端不在同一个域名下Cookie跨域带上是个麻烦事第二Session存在服务器内存里集群部署时要额外引入Redis做Session共享。JWT把用户信息加密放在Token里服务器不存状态天然支持横向扩展。这套系统的登录流程是这样的前端登录页把用户名密码发给/api/auth/login接口后端校验成功后生成Token返回给前端前端把Token存到localStorage之后每次请求都在请求头Authorization: Bearer token带上后端拦截器解析Token并校验是否过期。Java实现JWT的核心代码骨架如下这边用jjwt库演示public class JwtUtil { private static final String SECRET your-secret-key; private static final long EXPIRE_TIME 24 * 60 * 60 * 1000; // 生成Token public static String generateToken(Long userId, Integer role) { return Jwts.builder() .setSubject(String.valueOf(userId)) .claim(role, role) .setExpiration(new Date(System.currentTimeMillis() EXPIRE_TIME)) .signWith(SignatureAlgorithm.HS256, SECRET) .compact(); } // 解析Token public static Claims parseToken(String token) { return Jwts.parser().setSigningKey(SECRET).parseClaimsJws(token).getBody(); } }登录拦截器的核心逻辑放行登录和注册接口其他接口从request头取Token解析失败直接返回401状态码让前端拦截器做统一跳转登录页的处理。3.3 考试业务中最容易出问题的三个环节这套系统真正考验后端功底的地方在三个点抽题组卷、交卷判分、考试状态流转。抽题组卷教师创建考试时一般不手动逐题选择而是按题型和难度随机抽题。后端处理逻辑根据传入的题目数量和题型在题库里按条件查询可用题目集合再用随机数打乱顺序截取。这里要注意不能用一条SQL直接ORDER BY RAND()数据量一旦上千这条SQL会非常慢因为MySQL会对全表每行生成随机数再排序。正确做法是先查出ID列表在Java代码里用Collections.shuffle()打乱再WHERE id IN (...)查出完整题目。交卷判分是核心中的核心。逻辑不能写得太简单因为考试题型里有多选题、判断题和简答题。客观题单选、多选、判断逻辑是比对答案注意只有多选题的选项集合和标准答案完全一致才得分少选多选都算错。简答题要标记为待教师人工批阅不算总分等教师在教师端批阅完后再更新总分。判分全过程要开启数据库事务避免答题记录写了一半、成绩却更新失败导致数据不一致。时间控制也是一道坑。前端做了倒计时但后端交卷接口必须自己校验当前时间是否在考试时间范围内否则前端绕过页面直接调接口就能无限考试。正确的做法是后端根据当前时间和考试起止时间判断“可交卷”区间超过考试时间后后端拒绝接收答卷并强制自动交卷算出当前得分。4. 前端Vue实现与页面交互细节4.1 前端项目结构和路由设计Vue端采用Vue 2 Element UI或Vue 3 Element Plus看源码具体版本做的SPA单页应用。项目目录核心结构如下src ├── api // 接口请求封装 ├── router // 路由配置 ├── store // Vuex状态管理登录信息、用户信息 ├── views // 页面组件 │ ├── login // 登录页 │ ├── student // 学生端相关页面 │ ├── teacher // 教师端相关页面 │ ├── admin // 管理端相关页面 │ └── exam // 在线考试页 ├── utils // 工具函数请求拦截器、Token存取 └── App.vue路由这块难点在权限控制。学生、教师、管理员的页面完全不同前端路由需要配置路由守卫通过Vue Router的beforeEach钩子统一处理如果用户未登录一律跳到登录页登录后根据角色字段判断目标路由是否有访问权限没有就跳到对应角色的首页。动态路由的做法也可以从后端拿当前用户的角色和权限列表用router.addRoutes()动态挂载但这个项目规模用前端写死路由加守卫就够了。4.2 考试答题页的关键交互考试页是前端体验最核心的页面也是难点。我的经验是要做以下几点小题卡片导航左侧或底部是一排题号点击快速跳转已经作答的题号显示不同颜色方便考生知道还有哪些题没做。答案本地暂存考生每选一道题的答案立刻写入Vuex和localStorage防止意外刷新页面导致答案全丢。交卷时再从状态里组织成提交数据一次性发给后端。倒计时强制交卷页面顶部常驻倒计时组件剩余时间到0时弹出Modal提示并自动触发交卷。计时基准不能只靠setInterval因为浏览器切后台会挂起定时器稳妥方案是用后端返回的考试截止时间减去当前时间做差值倒计时。交卷二次确认交卷是重操作必须确认怕考生手滑。确认弹窗里明确显示“还有几题未作答”由考生自己决定是否继续。4.3 接口统一封装axios拦截器前端所有HTTP请求基于axios做了统一封装。这是前后端分离项目必须要养成的习惯不要每个页面单独写ajax请求。import axios from axios const service axios.create({ baseURL: process.env.VUE_APP_BASE_API, // 通过环境变量控制 timeout: 10000 }) // 请求拦截器自动带Token service.interceptors.request.use(config { const token localStorage.getItem(token) if (token) { config.headers[Authorization] Bearer token } return config }) // 响应拦截器统一处理错误码 service.interceptors.response.use( response { const res response.data if (res.code ! 200) { // 业务错误统一提示 if (res.code 401) { // 登录过期跳转登录页 localStorage.removeItem(token) router.push(/login) } return Promise.reject(new Error(res.message)) } return res }, error { // 网络错误处理 console.error(接口请求失败:, error) return Promise.reject(error) } )环境变量这块是个小细节但很实用。开发环境VUE_APP_BASE_API设成/api依赖Vue CLI的devServer代理转发到后端生产环境用Nginx把/api反向代理到后端服务。同一个前端代码换环境只改环境变量不用改代码。5. 部署实战从本地开发环境到服务器上线下面是最抢手的部分完整部署流程。我按从零开始的环境准备到最终上线的顺序捋一遍并附上我在部署过程中遇到过的坑和解决办法。5.1 开发环境准备JDK、Maven、Node、MySQL本地跑这个项目第一步是环境版本要对齐。给出我实测可行的版本组合组件版本说明JDK1.8 / 11SpringBoot 2.x常用JDK83.x必须JDK17Maven3.8用过3.6.3没毛病Node.js16/18Vue CLI要求Node版本别太低MySQL5.7 / 8.0注意驱动配置差异后面有坑IDEIDEA / VSCode后端用IDEA前端用VSCode各有偏科安装过程不多说提两个关键点Maven下载依赖慢在settings.xml里配阿里云镜像不然第一次构建能把人卡死。镜像配置是通用的贴一下核心的部分mirror idaliyunmaven/id mirrorOfcentral/mirrorOf name阿里云公共仓库/name urlhttps://maven.aliyun.com/repository/public/url /mirrorNode.js安装后npm也要换淘宝源npm config set registry https://registry.npmmirror.com。不换源的情况下npm install可能因为网络原因报错或卡住。5.2 数据库初始化与后端启动用Navicat或命令行执行后端源码里的sql目录下的初始化脚本这一步会把上面提到的所有核心表建好并插入初始账号管理员、测试教师、测试学生。后端启动前修改application.yml关键配置server: port: 8080 spring: datasource: url: jdbc:mysql://localhost:3306/exam_system?useUnicodetruecharacterEncodingutf8useSSLfalseserverTimezoneAsia/ShanghaiallowPublicKeyRetrievaltrue username: root password: 你的数据库密码 driver-class-name: com.mysql.cj.jdbc.Driver这里有几个非常容易踩的坑useSSLfalse新版MySQL连接时不加这个参数经常报SSL连接错误。代码经验证生产环境也要加上。serverTimezoneAsia/Shanghai不加时区参数JDBC连MySQL会报时区相关的异常国内服务器特别注意。allowPublicKeyRetrievaltrueMySQL 8.0非SSL连接时不加这个可能报Public Key Retrieval is not allowed。driver-class-nameMySQL 5.7和8.0驱动类名不一样。老版本是com.mysql.jdbc.Driver8.0是com.mysql.cj.jdbc.Driver。如果跑起来报ClassNotFoundException检查这里是关键。配置无误后在项目根目录执行mvn clean package -DskipTests看到BUILD SUCCESS说明后端包打好了target目录下生成一个exam-system.jar。本地直接用IDEA启动SpringBoot主类也行。如果用的是打包方式启动命令java -jar target/exam-system.jar看到SpringBoot的启动日志最后一行出现Started Application in X seconds就算成功了。5.3 前端环境配置与本地启动前端项目代码拿到后先在src/api/request.js或环境变量文件里确认接口地址配置。本地开发时用vue.config.js配置devServer代理是经典的做法module.exports { devServer: { port: 8081, proxy: { /api: { target: http://localhost:8080, changeOrigin: true } } } }这样前端访问/api/xxx开发服务器会把请求转发到localhost:8080前端代码里不用写死后端地址也就绕过了跨域限制。然后依次执行npm install npm run serve浏览器访问http://localhost:8081用初始账号登录如果能看到考试列表页面说明本地联调通了。5.4 生产环境部署后端jar包 Nginx托管前端生产环境部署的思路是把前端构建成静态文件Nginx负责托管静态页面并反向代理后端API。完整步骤如下第一步构建前端产物npm run build构建完成后生成dist目录前端所有代码都在这。第二步打包并上传后端在IDEA里执行mvn clean package用java -jar exam-system.jar在服务器上后台启动。生产环境不能用前台运行否则SSH断开进程就退出了。用nohupnohup java -jar exam-system.jar exam.log 21 看日志排查问题用tail -f exam.log。第三步配置Nginx把dist目录里的文件上传到服务器某个目录比如/usr/share/nginx/html/exam。Nginx配置核心要点静态文件交给Nginx直接返回接口请求从上到下依次匹配接口路径加到后端服务。server { listen 80; server_name your-domain.com; location /api/ { proxy_pass http://127.0.0.1:8080/api/; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; } location / { root /usr/share/nginx/html/exam; index index.html; try_files $uri $uri/ /index.html; } }try_files $uri $uri/ /index.html;这里有一行起关键作用的配置。没有这一行前端Vue路由在浏览器里直接访问比如/exam/detail这种地址刷新页面就是404因为Nginx找不到这个物理文件。加上这一行后所有找不到的路径都回退到index.html让Vue Router接管路由逻辑。改完配置执行nginx -t校验语法再nginx -s reload生效。访问服务器IP能看到登录页输入账号密码能正常跳转、查考试列表部署就完成了。如果域名有备案绑上域名配合HTTPS证书这就是一个能放给真实用户用的系统。6. 常见问题与避坑手册已实测这部分是从实际复现项目中整理的高频问题集合。每一条都真实发生过照着排查能省半天时间。问题现象排查思路解决方案后端启动报Access denied for user数据库账号密码错误或权限不足检查application.yml里的用户名密码用Navicat测试连接授权对应主机访问后端启动报Unknown databaseMySQL里还没创建数据库或库名不匹配先执行CREATE DATABASE exam_system DEFAULT CHARSET utf8mb4;再导入SQL前端npm install报ERESOLVE错误Node版本太高导致依赖依赖冲突删除node_modules和package-lock.json用Node 16版重装或执行npm install --legacy-peer-deps前端接口全部401Token在localStorage里没存住或请求拦截器没带上请求头F12看Network请求头确认Authorization字段有没有前端代码检查headers拼写登录接口报跨域错误前端8081直接访问后端8080导致CORS拦截本地开发用devServer代理生产环境必须用Nginx反向代理后端也可以加CorsFilter兜底考试答题时刷新页面答案全丢前端没有把答案存储到localStorage改前端答题页答题状态实时写入localStorage进页面时优先读取恢复过长的填空题或作文题出现中文乱码数据库表字符集不对或JDBC连接没指定utf8所有表统一用utf8mb4字符集jdbc连接字符串必须加characterEncodingutf8并发交卷导致成绩错乱没有做事务控制或乐观锁缺失Service层加Transactional更新成绩前判断记录版本号或行数用UPDATE ... WHERE status进行中保证并发安全SpringBoot 3 JDK17跑起来报各种不兼容依赖版本没有同步升级检查pom.xmlSpringBoot 3必须配套MyBatis-Spring-Boot-Starter 3.x版本javax.要替换成jakarta.另外还有两个实战中血泪教训单独拎出来说。坑一Maven打包后jar包体积很大但运行报no main manifest attribute。这个八成是pom.xml里缺少SpringBoot Maven插件只用了普通maven-jar-plugin打包。正确做法是在build节点里配置plugins plugin groupIdorg.springframework.boot/groupId artifactIdspring-boot-maven-plugin/artifactId /plugin /plugins坑二服务器内存不够jar包启动即被杀。很多便宜云服务器只有2G内存默认的JVM堆参数可能直接吃满。启动时显式限制堆内存,优先保证稳定运行java -Xms256m -Xmx512m -jar exam-system.jar这样设置后512MB堆 元空间 系统开销基本能在2G内存的服务器上稳定跑起来。7. 项目扩展方向与实际部署经验这套系统框架本身是完整的但真要用于生产环境或者作为毕设亮点有几个扩展方向值得考虑加入导入题库功能教师端用Excel批量导入题目配合EasyExcel库解析文件省手工逐条录入的精力。主观题教师批阅流简答题不能只靠自动判分要做教师批阅面板展示待批阅试卷批完后自动汇总总分。这是从“演示项目”走向“真实可用”的关键一步。考试数据可视化在管理端加成绩分布图、及格率统计、题目正确率排行用ECharts展示能显著提升系统的数据价值也是答辩或演示的加分项。防作弊机制做切屏检测页面失焦超过N次强制交卷、IP限制、考试期间禁止复制粘贴这些在在线考试场景里是刚需。最后再说点个人的实在经验。我每次拿到一套新的前后端分离源码第一步永远不会是直接启动而是先看三样东西SQL脚本里的表结构、pom.xml里的依赖版本、application.yml里的环境配置。这三样东西顺了项目基本能跑起来。跑起来之后再按“登录 - 建题库 - 组卷 - 答题 - 交卷 - 看成绩”的顺序把主流程走一遍确认核心闭环是通的再去看那些边角功能。这套在线考试系统跑通之后我最大的体会是前后端分离项目的难点往往不在某个技术点多高深而在如何把用户认证、数据状态流转、前后端接口约定这些“看不见的骨架”理清楚。考试系统恰好是练习这个能力的好载体——业务规则明确、角色分明、状态流转清晰拿来练手、做毕设、研究源码都再合适不过了。把文章里提到的那些坑避开你手里的这套源码一定能被你真正玩明白。
上一篇/下一篇内容由系统自动关联
返回资讯列表 →