基于SpringBoot的在线考试系统,是Java毕业设计里出现频率最高的题目之一。它适合作为毕设,不是因为题目本身简单,而是它能把用户管理、题库管理、组卷、在线答题、自动评分、成绩统计这一整条业务链路串起来,正好覆盖一个前后端分离项目该有的常见模块。这篇内容我会站在“准备交一份能跑、能演示、经得住老师追问的毕设项目”的角度,把系统的模块设计、数据库表、后端流程、前端联调、部署演示和排障经验完整拆一遍。
如果你手里已经拿到了一份带源码、文档报告和代码讲解的项目资源,更建议先看这篇内容,把系统骨架理解清楚,再动手改代码。这类系统在网上很多,命名可能不一样,但核心设计套路是共通的。真正的毕业设计重点不是源码有多少,而是你能不能把“为什么这样设计”讲清楚。
先说我的结论:这个系统最值得花时间的地方,不是考试页面,而是“考试记录如何持久化”和“交卷时怎么评分”。业务上围绕这两条链路走通,其他页面都是补充。
1. 先搞清在线考试系统到底要做什么,再决定从哪开始写
1.1 核心角色和功能边界
一个典型的基于SpringBoot的前后端分离在线考试系统,通常会有三个角色:学生、教师、管理员。有的系统把管理员弱化,只保留学生和教师两个角色,但从毕设评分角度看,保留三类账号会更好,权限区分更完整,论文里也能多写一段权限设计。
学生侧的功能一般是注册登录、查看考试列表、进入考试、按时交卷、查看个人成绩。
教师侧的功能一般是题库维护、手动或自动组卷、创建考试、查看某场考试的参与情况、批改主观题、导出成绩。
管理员侧的功能一般是用户管理、系统参数配置、数据统计、日志查看。
这三块不是平均用力。大多数毕业设计方案里,题库管理和在线考试是核心,成绩统计次之,用户管理最基础。先把这三件事做扎实,比堆一堆只有页面没有逻辑的功能有用得多。
从技术实现角度看,学生侧最容易被低估的是倒计时、交卷防重复提交、题目缓存这些细节。教师侧最容易被低估的是“自动组卷规则”和“主观题转人工批改”。
1.2 前后端分离,对这个系统到底意味着什么
前后端分离不是单纯指“前端一个项目、后端一个项目”,而是让两者通过HTTP接口通信,互不关心对方内部实现。这个系统里,SpringBoot负责提供RESTful接口,前端项目负责渲染页面和调用接口。
大部分毕业设计选的是Vue作为前端框架,配Element UI或者Vue生态下的组件库。也有同学用React,但从参考资料量和上手速度看,Vue是更省事的选择。前端和后端分开部署之后,就必然会遇到跨域、鉴权、静态资源访问三个问题,这三个问题在毕设里最常踩坑,后面我会单独讲。
需要特别提醒的是:很多毕业设计Demo会把前端打包后的文件直接放到SpringBoot的static目录里,这样也能跑起来,但不能叫前后端分离。如果论文里写了“前后端分离”,演示的时候最好让前端走Node或Nginx服务,后端独立监听端口。否则答辩时很容易被发现是伪分离。
2. 数据库设计:这里花时间是最值得的
2.1 建议的核心实体划分
在线考试系统的数据库表,我建议至少包含这些实体:用户表、题目表、试卷表、考试场次表、考试记录表、答题明细表。
用户表保存账号密码和角色字段。题目表保存题干、选项、答案、分值、难度、题型。试卷表保存试卷基本信息,同时用一张试卷题目关联表把生成后的题目持久化下来。
考试场次表用来表达“某套试卷在某段时间内开放给某类学生考试”。考试记录表是学生参与一场考试的动作记录,成绩字段在这里。答题明细表保存学生每一道题的作答内容,是自动评分和人工批改的数据基础。
这里有一个常见错误:只设计了试卷表,但没设计考试场次和答题明细。结果就是学生答完题成绩算出来了,却查不到每一题错在哪,也没有办法重考或者查看历史记录。老师一问就露馅。
一个比较稳妥的核心表结构如下,字段名可以根据自己项目调整:
| 表名(示例) | 核心字段(示例) | 作用说明 |
|---|---|---|
| sys_user | username、password、role | 保存学生、教师、管理员账号 |
| question | type、content、answer、score、difficulty | 题库基础数据 |
| exam_paper | title、status、total_score | 试卷基本信息 |
| exam_paper_question | paper_id、question_id、sort | 试卷与题目的关联 |
| exam_session | paper_id、start_time、end_time、status | 考试场次 |
| exam_record | student_id、session_id、status、score | 一次考试的记录和最终成绩 |
| answer_detail | record_id、question_id、student_answer、is_correct | 每一道题的答题明细 |
这样的表设计,能支撑起“学生进入考试、答题、交卷、查分、教师批改、成绩统计”整条链路。
2.2 考试记录持久化的意义
持久化这个词,很多同学写论文时会写,但没有真正理解。简单说,就是学生交卷之后,他的答案和成绩必须长期保存在数据库里,而不是停留在前端内存或者后端对象里。
我建议答题明细按题号一行一行存,字段包括考试记录ID、题目ID、学生提交的选项或文本、题目标准答案、是否得分、得分值。这样有两个好处:一个是成绩统计时可以直接聚合计算,另一个是学生查看答卷回放时可以直接按成绩记录查询。
数据库初始化脚本一定要完整。市面上很多源码项目只给了结构脚本,没有测试数据,导致前端列表空空如也。要自己补充一些典型的教师账号、学生账号、单选多选判断题数据,演示时才不会冷场。
注意:如果你拿到的脚本只创建了表,没有插入任何数据,建议先补几套完整测试数据。否则演示时点到“考试列表”是空的,你很被动。
2.3 表和表之间怎么关联,看这条链路
在考试系统中,学生要能看到“我有哪些考试”,这个查询依赖 exam_session 和 exam_record 的关联。一张考试场次是否已经开放,状态字段很重要,要能区分“未开始、进行中、已结束”。
进入考试时,前端拿到考试场次ID,后端根据场次ID找到试卷ID,再去试卷题目关联表查出所有题目。这是入题的接口链路。
交卷时,后端把 student_answer 写入 answer_detail,同时逐题判分,把总分写入 exam_record。这是出成绩的链路。
教师端查看某场考试成绩时,直接查 exam_record 表,按场次ID过滤,关联 sys_user 表显示学生姓名。整个系统的查询路径非常清晰,答辩时能拿这张关联图讲清楚整个项目,效果比贴一堆代码好得多。
数据库设计这块,我见过太多项目把“考试成绩”只放在 exam_record 的一个 score 字段里,却没有 answer_detail。这样每次考试结果就是一串数字,学生错了哪一题也查不到。从“系统完整性”角度看,明显少了一块。
3. 后端核心流程:登录认证、组卷、交卷评分
3.1 认证方式:Session 还是 Token
SpringBoot后端做认证,最常见的是两种:Session会话和JWT Token。毕业设计项目里,两者都能用,但前后端分离场景下,JWT更常见,也更容易解释清楚。
用Session的话,前端要在请求里带Cookie,跨域时要额外配置。用JWT的话,用户登录成功后后端返回一个Token,前端存在本地存储里,后续请求在请求头带Authorization字段,后端用拦截器或Spring Security校验。
如果时间紧,不建议一上来就接Spring Security,光是过滤器链、用户细节对象就够折腾。更务实的做法是先用拦截器做一个简单的Token校验过滤器,把不需要校验的路径放行,把需要登录的接口保护起来。等系统跑通了,再在论文里写“如果要生产化,可以接入Spring Security增强”。
这里有个经验:放行路径一定要仔细配置。比如登录、注册、读取考试公告、文件下载这些接口不能拦,否则前端第一次请求就403,排查半天发现是拦截器把所有请求都拦了。
Token校验拦截器的基本思路是这样:
// 自定义拦截器 public class TokenInterceptor implements HandlerInterceptor { @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { // 放行登录、注册等接口 String uri = request.getRequestURI(); if (uri.endsWith("/login") || uri.endsWith("/register")) { return true; } // 检查 Authorization 头里是否有 Token String token = request.getHeader("Authorization"); if (token == null || !JwtUtil.verify(token)) { response.setStatus(401); return false; } return true; } }这段只是思路示例,具体依赖要看你用的JWT库版本。重点是理解:先放行公开接口,再校验受保护接口。
3.2 自动组卷怎么设计
自动组卷是很多在线考试系统里的亮点功能,但不要一开始就做复杂算法。最简单可靠的方案是“按题型和知识点抽取”。
具体步骤是这样的:教师创建试卷时输入规则,比如单选题10道、多选题5道、判断题5题,每题分值若干,难度系数限定在多少以内。后端收到规则后,先按条件查询题库,再用随机方式取出对应数量的题目,最后把这些题目写入试卷题目关联表。
这里要注意边界:题库中符合条件的题目不足时,组卷会失败或者题目重复。代码里要判断实际查询到的题目数量,如果不够,直接返回“题库数量不足”的提示,而不是生成一份残缺试卷。
有些系统还会做“题库覆盖检查”:教师创建试卷前,先按规则统计当前题库能抽出多少题,提前预警。这个功能不复杂,但答辩时很加分,因为它体现了你考虑到了实际使用场景。
如果使用MyBatis Plus或者JPA,随机排序的写法不同。用SQL时常见是“按随机数排序后限制数量”,例如:
SELECT * FROM question WHERE type = 'SINGLE' AND difficulty <= 3 ORDER BY RAND() LIMIT 10;这种方式在数据量小的时候完全够用,不要刻意追求复杂的洗牌算法。毕设演示时数据量不大,随机性足够。
3.3 交卷和自动评分的基本流程
交卷动作很关键,必须防止重复提交。常见做法是:前端在点击交卷时禁用按钮,并提交一份带考试记录ID的请求;后端收到请求后,先判断该考试记录状态是否已经是“已交卷”,如果已经是,则直接返回当前成绩,不重复评分。
自动评分的逻辑按题型区分。
单选题、判断题比较简单,学生提交的答案和标准答案做字符串比较即可。多选题需要处理顺序问题,有的系统要求完全一致才得分,有的系统允许漏选得部分分,具体规则要在代码里明确,不能写得模糊。
填空题比较麻烦。常见做法是允许教师维护多个可接受答案,存储时用分隔符分开,学生答案只要匹配其中一个就算对。
简答题、论述题通常不建议用关键词匹配,准确率太低。更合理的做法是标记为“待人工批改”,教师端出现一个批改列表,教师录入分数后系统自动相加。
交卷评审的核心逻辑,可以用伪代码表达:
for each answerDetail: question = getQuestion(answerDetail.questionId) if question.type in ['SINGLE', 'JUDGE', 'MULTI']: answerDetail.isCorrect = compare(question.answer, studentAnswer) answerDetail.score = isCorrect ? question.score : 0 else: answerDetail.status = 'PENDING' answerDetail.score = null简答题走人工批改后,再把分数回填到答题明细表,并重新汇总考试记录总分。这里最容易出Bug的地方是“更新总分时只更新了某道题,没有重新汇总所有明细”,结果总分错乱。
4. 前端页面与接口联调:最容易卡住半天的地方
4.1 建议的最小页面清单
我不建议为了面子做大量无意义的页面。最实用的前端页面是这些:
- 登录注册页
- 学生端:考试列表页、考试答题页、个人成绩页
- 教师端:题库管理页、试卷和考试场次管理页、成绩管理页
- 管理员:用户管理页、数据统计页
考试答题页是核心页面,里面至少要有题目区、选项区、答题进度条、剩余时间倒计时、交卷按钮。这个页面做不好,演示效果会直接打折。
其他页面可以相对简化,但CRUD的“新增、编辑、删除、分页”这些基本操作要完整。答辩时老师大概率会测试新增一道题库题目,再重新组卷,这个链路一定要提前走通。
4.2 接口风格和请求封装
前后端联调时,接口要有一个明确的约定。建议统一返回格式。后端用一个Result类包装返回数据,包含code、message、data三个字段。code为0表示成功,非0表示不同错误。这样前端封装一个axios请求拦截器,统一处理错误提示,比每个页面单独处理省事得多。
一份简单的后端返回结构可以这样定义:
{ "code": 0, "message": "success", "data": { "token": "xxx", "userInfo": {} } }接口路径建议按模块划分。比如:
POST /api/auth/login GET /api/user/info GET /api/questions POST /api/questions PUT /api/questions/{id} DELETE /api/questions/{id} POST /api/paper/generate GET /api/exam/list POST /api/exam/submit GET /api/exam/records前后端分离项目里,接口文档不一定要写得很正式,但至少应该在README或者接口注释里说清楚每个接口的入参和出参。毕业设计的文档报告里,接口说明表也是比较重要的一节,能在论证工作量时占很多篇幅。
前端请求封装建议单独放到一个request.js或http.js文件里,统一设定baseURL、超时时间、请求拦截器和响应拦截器。不要在每一个页面里直接写axios.get,那样改后端地址时会非常痛苦。
4.3 跨域与鉴权,最容易出问题的两个配置
前端开发服务器默认跑在5173或8080端口,后端跑在8081或8080端口。如果端口不一致,就会出现跨域问题。跨域不是项目的逻辑错误,而是浏览器的同源策略限制,后端需要在跨域配置里处理对应的响应头。
SpringBoot里可以写一个跨域配置类,设置允许来源、允许请求头、允许方法类型。如果使用了Token校验,还要注意把OPTIONS预检请求直接放行,否则前端调用接口时会先发一个OPTIONS请求,被拦截器拦住,真正请求就发不出去。
这里有一个很常见的坑:前端用了代理转发,页面能正常访问,但接口一直提示404或者401。这时候不要只盯着后端代码,先看前端开发服务器的代理配置里,接口路径有没有配到正确的后端地址,有没有加/api前缀。
前端开发模式下Vite的代理配置大致这样:
server: { port: 5173, proxy: { '/api': { target: 'http://localhost:8081', changeOrigin: true } } }代理配置只能解决开发环境,生产环境部署时,如果前端和后端不在同一个端口,还是要依赖Nginx反向代理或者后端跨域配置。
注意:跨域配置和Token校验要配合起来看。很多时候接口报401,不是Token本身错了,而是OPTIONS预检请求被拦截,导致真实请求没有成功发到后端。
5. 本地运行与服务器部署演示
5.1 本地把前后端跑起来
拿到一份源码后,不要急着双击启动。第一步是看项目的目录结构,确认哪部分是后端、哪部分是前端。然后检查三项内容:数据库脚本、后端配置文件、前端接口请求地址。
后端配置文件里主要看数据库连接、Redis(如果有的话)地址、端口号。很多源码默认数据库配置不对,不改就启动失败。建议在本地创建一个同名数据库,导入脚本,再把账号密码改成你本机的。
前端要先执行依赖安装,再确认请求地址。开发环境通常配置的是 http://localhost:后端端口,也可能通过代理方式访问。先跑起来一个最小的链路:登录、查看试题列表、创建一场考试、学生进入考试、提交、看成绩。这样就算初步验证通过。
5.2 后端打包与前端部署
毕业设计最简单稳妥的后端部署方式,是使用Maven打包成jar包,然后通过java -jar命令启动。打包命令是:
mvn clean package -DskipTests启动命令是:
java -jar exam-system.jar --server.port=8081如果你是在Linux服务器上部署,建议配合nohup或systemd来保持进程后台运行。如果只是本地答辩,直接在IDEA里运行主启动类就够了。
前端可以打包成静态文件,放到Nginx中运行。打包命令是:
npm run build打包后在dist目录下会有静态资源。注意:如果服务器上没有Nginx,也可以直接把dist目录放到SpringBoot的classpath下,但这就不是前后端分离了,论文里要小心表述。
5.3 服务器部署的基本顺序
在云服务器上部署这类项目,通常遵循固定顺序:安装JDK、安装MySQL、安装Nginx,然后上传jar包和前端静态文件,配置Nginx反向代理到后端端口。
先说结论:不推荐在答辩前一天才开始部署。第一次部署通常会出现端口占用、数据库外网连接失败、防火墙拦截、静态资源路径错误等一系列问题,至少预留一整天。
Nginx的常见配置思路是这样:
server { listen 80; server_name localhost; root /opt/exam/dist; index index.html; location /api/ { proxy_pass http://127.0.0.1:8081; } }这是示例配置。如果你的后端上下文中已经有 /api 前缀,这里的匹配路径要对上。如果不对应,请求会被Nginx当成静态资源请求处理,结果返回的是前端index.html,而不是后端JSON数据。
部署之后要验证四个点:
- MySQL里的数据能不能被后端读取
- 后端接口能不能返回正常的JSON
- 前端页面能不能通过浏览器访问
- 登录、考试、交卷、成绩展示这一整条链路能不能走通
只要有一个环节不过,就说明部署环境有问题,不要急着改业务代码。
6. 答辩演示和排障经验
6.1 演示顺序建议
答辩演示不要从源码开始讲。建议按这个顺序来:先用1分钟说明系统解决什么问题,再用1分钟说明技术栈和架构,然后进入系统演示。
演示时先登录一个管理员账号,展示用户管理,再登录教师账号,展示题库维护和组卷,最后切到学生账号,完成一次从考试列表到交卷查分的完整流程。这样边界清楚,老师也能看到不同角色之间的分工。
有个细节要注意:演示考试时,如果试卷题量太大,会拉长时间。可以提前准备一场只有5道题的测试考试,时间设长一点,现场演示时迅速答完提交,得分效果一目了然。
6.2 常见报错排查
我先列出几个我见过的高频问题,排查时可以按这个顺序看:
| 现象 | 优先排查项 |
|---|---|
| 启动报数据库连接失败 | 数据库服务是否启动、账号密码、数据库名、端口 |
| 前端接口404 | 接口路径、后端上下文前缀、前端代理配置 |
| 前端接口401 | Authorization头是否存在、Token是否过期、放行路径是否配置 |
| 浏览器报CORS跨域 | 后端跨域配置是否生效、OPTIONS预检是否放行 |
| 自动组卷失败 | 题库数据量是否足够、筛选条件是否太严、是否有重复题 |
| 学生交卷后成绩不显示 | exam_record状态是否更新、成绩是否持久化、查询关联是否写对 |
启动报数据库连接失败,多数情况不是驱动版本问题,而是MySQL服务没开、密码不对、库名写错。先把这三个基础项确认一遍,再看驱动的groupId和artifactId。
前端接口404,要分两种场景:开发环境看代理配置,生产环境看Nginx配置。单独在后端浏览器里访问接口正常,不代表前端部署环境下也能正常访问,因为多了一层转发。
跨域报错时,浏览器控制台会出现CORS字样。先确认请求有没有带OPTIONS预检,再看后端跨域配置里允许的来源是不是写太窄,最后看过滤器有没有把OPTIONS请求误拦截。
6.3 源码拿到手之后应该先看什么
网上这类毕业设计项目,通常会附带源码、文档报告和代码讲解。拿到手之后,不要急着改界面文案和用户名密码。我建议先梳理三份图纸:数据库表结构关系图、后端核心接口列表、前端路由和页面结构。
把这三份图纸搞清楚以后,你其实就已经理解了整个系统。再往后改功能、改表、换主题,都是在这个骨架上做替换。不要面对一堆代码就开始找“哪个类是登录逻辑”,而是从包结构入手,顺着controller、service、mapper、entity这条链路读代码,效率会高很多。
很多同学最后出问题的地方,不是代码本身,而是文档和代码不一致。论文里写了自动组卷算法,代码里却没有;论文里写了Redis缓存,代码里压根没有Redis依赖。这属于信息不一致,答辩时会直接影响印象分。建议在提交前,用代码反向核对文档里的技术点,保留能对上的,把对不上的描述改掉。
如果是从网上下载的源码,尽量先跑一遍基础Demo再决定是否使用。有的项目依赖版本太老,下载困难;有的项目需要特定JDK版本;有的项目会把数据库连接信息写死在代码里,改起来麻烦。先跑通,再看质量,避免快交稿了才发现项目根本启动不起来。
6.4 文档报告怎么写,才能和代码对上
毕业设计的评分重点,通常不只是代码,还有文档报告。在线考试系统的文档逻辑可以这样安排:先写需求分析,再写数据库设计,再写接口设计,最后写实现效果和测试。
每条技术特性,都要在代码里找到对应的类或接口。举例来说,如果写了“系统使用JWT实现无状态鉴权”,那么项目里就应该有JwtUtil类、TokenInterceptor类,并且在Controller层能看到从请求中取用户信息的代码。这样答辩时,老师顺着论文索引去看代码,对得上。
代码讲解放到文档里,可以画一张“一个请求从前端到数据库的流程”图。比如学生交卷这个操作,前端点击按钮后,请求到后端哪个Controller,Service层怎么判分,Mapper层怎么更新题库数据。把这个链路讲清楚,比罗列一堆CRUD代码更有说服力。
6.5 个人经验与判断标准
最后分享几点我做这类项目的判断标准。
第一,不要把考试系统和做题系统混为一谈。在线考试系统必须有考试场次概念、时间限制、交卷状态、成绩持久化。如果只是做一套题库刷题系统,功能深度远远不够。
第二,前后端分离是毕设亮点,但这个亮点需要落地证据做支撑。前端要独立运行,后端要提供可调用的接口,最好保留一份接口调用截图放文档里。
第三,资源占用和性能问题,在毕设阶段其实不用过度焦虑。重点优化高并发,那是生产环境才需要深入考虑的事情。毕设更有价值的表现是逻辑严谨、边界处理完整、文档和代码一致。
我更建议把精力花在组卷规则、自动评分、考试成绩统计这三块上。这三块任何一个做得细,都能在答辩时讲出内容。考试页面再好看,如果交卷和成绩链路有问题,整个系统都会被否定。
最后还是那句话:先把单条链路跑通,再做增量。从登录到交卷到查分,这是一条完整业务闭环;闭环通了,剩下的扩展功能都是时间问题。