基于Spring Boot+Vue的在线考试系统开发实战与关键设计
1. 项目定位与技术选型拆解在线考试系统在计算机毕设里属于“常青树”级别的题目。它不追求新潮算法不做炫酷图形处理核心价值在于把一条完整的业务链路打通教师出题、组卷、发布考试学生在线答题、交卷系统自动判分、成绩统计。这条链路几乎覆盖了Web系统开发的所有基本功用来做毕业设计既能让答辩时讲清楚每个模块存在的意义又能在开发过程中把前后端分离、权限控制、状态管理这些核心技能练扎实。“基于Spring Boot Vue”并不是拍脑袋选出来的组合。如果拆开来看Spring Boot在后端生态里的地位相当于装修里的“成品房”——自动配置机制把大量繁琐的XML配置和依赖管理消化掉了开发前期不用在环境搭建上耗费太多时间能把精力集中到业务逻辑上。Vue在前端这边的优势则是上手曲线平缓、组件化心智模型清晰一个从没写过前端的后端同学花几天时间就能写出像样的页面交互。这套技术栈真正吸引人的地方在于“前后端分离”的架构模式。以前用JSP或者Thymeleaf做单体应用前端页面和后端逻辑揉在同一个工程里改个样式要重启服务协作开发时还容易互相踩文件。分离之后后端只提供JSON接口前端只负责渲染和交互两边并行开发互不干扰。对于毕设来说这种模式还有一个隐性好处论文里的“系统架构图”会非常清晰业务逻辑层、接口层、前端展示层边界分明写文档的时候都不需要费力解释。不少同学在选择毕设题目时会纠结是做一个“功能堆砌”的系统还是做一个“技术炫技”的系统。我的建议是别走极端。在线考试系统这个题目的正确打开方式是功能围绕考试闭环来设计不多不少技术上把关键点做深比如自动判分的策略、并发交卷时的数据一致性、试卷快照与题目变动的隔离这几个点做好了出彩程度不比用某个冷门中间件差。从答辩评分角度看“能用”永远只是及格线“为什么这么设计”才是拉开分差的地方。比如数据库中为什么要设计“考试记录”和“答题明细”两张表为什么试卷题目关系需要单独一张关联表为什么交卷时要做幂等处理——这些细节不是面试官会问的却是答辩老师大概率会追问的。本篇后续内容就以这些关键决策为主线把整个系统的实现思路完整走一遍。2. 核心业务模型与数据库设计2.1 从业务场景推导出来的核心数据流在线考试系统看起来功能很多剥开外壳后核心业务只有一条主线考试从创建到出成绩的完整生命周期。这条主线上涉及的参与方有三个角色教师负责出题和阅卷学生负责答题系统负责在中间撮合流程并保证数据准确。这三个角色对应的数据实体就是数据库设计的骨架。第一层是用户体系。用户表除了存账号和密码还必须有一个角色字段来区分教师和学生。用Spring Security做权限控制时角色是决定接口访问范围的核心依据。如果管理员角色需要单独存在也可以再增加一种角色类型但底层表结构不需要发生任何变化。第二层是教学基础数据。教师开设课程课程下面挂学生。如果不做课程维度考试系统会退化成“所有学生考同一张试卷”的单机版这在真实教学场景里是不够的。所以课程表和学生选课关系表是必须的这也能让演示过程更有说服力——答辩时现场创建一个课程把几位同学加入课程再发布一场只针对该课程的考试整个流程的完整性一目了然。第三层是试卷与题库。题库是题目的集合题目可以有单选、多选、判断、填空、简答五种类型。试卷是由题目组装出来的组装方式又分为手动选题和按规则抽题两种这两种方式分别对应试卷表上的不同逻辑。这里需要特别注意设计题目与试卷的关系一张试卷包含多道题一道题可以在多张试卷中出现所以它们之间必须用一张关联表来维护。第四层是考试与答题记录。考试表记录某张试卷的考试时间段、时长、总分、及格分考试记录表记录每个学生参加某场考试的报名、状态和最终成绩答题明细表则逐题记录学生的答案。这三张表是系统判分、成绩统计和答案回溯的依据。2.2 表结构设计的几个关键决策详解既然网络考试系统的核心是考试讲解数据库设计时就要抓住考试这个业务状态流转的过程来组织内容。下面这张表清单对应了“建立题库 → 构建试卷 → 发布考试 → 在线答题 → 自动评分 → 成绩统计”的考试闭环可以把数据库设计目标和业务场景对应起来这样后续构建模块时思路会更清晰。核心闭环节点对应数据表数据库设计目标关键字段说明组织架构与用户用户表、课程表、选课关系表建立考试系统的人员与教学组织基础用户账号、密码、角色课程课程名称、授课教师选课课程ID、学生ID出题与题库题库表、题目表实现分类化题库管理与多种题型录入题库名称、所属课程题目题型、难度、题干、选项、答案、解析组卷与试卷试卷表、试卷题目关联表支持手动组卷与自动抽题的规则留存试卷总分、时长、状态关联表试卷ID、题目ID、题目分值、序号考试与答题考试表、考试记录表、答题明细表记录考试发布信息与每位考生的完整答题行为考试表试卷ID、起止时间、考试状态记录表考生ID、试卷快照、交卷状态明细表题目ID、答案、得分自动评分判分规则配置表实现客观题自动评分与主观题人工阅卷题型、判分方式、多选题判分规则漏选得分比例成绩与统计成绩表、班级成绩统计视图实现成绩发布、及格率与分数段统计成绩总分、客观题得分、主观题得分、排名统计班级ID、及格率、最高分、平均分对于毕设级别的开发我的建议是表数量控制在15到20张之间。表太少体现不出系统设计能力表太多又会把自己拖入数据维护的泥潭。以上表格里梳理出的表数量大约在13到14张足够支撑一个完整的考试闭环不会让人觉得功能单薄。在设计上有一个点需要特别强调试卷与题目的关联表上必须保留“分值”和“序号”字段。原因是同一道题进入不同试卷后可能会有不同的分值和排列顺序。如果直接在试卷表里用逗号拼接题目ID组卷和判分时又要做字符串拆分不仅效率低维护起来也极其痛苦。我在给某开发者看他的初版设计时看到他把题目ID塞在一个varchar字段里当场就建议改成关联表方案。这个表设计的取舍答辩时大概率会被问到值得用心准备。2.3 考试快照机制在线考试系统最容易被忽略的设计在线考试系统里有一个隐蔽但是极其重要的数据一致性问题考试一旦发布试卷内容是应该固定不变还是允许教师随时修改如果允许修改正在答题的学生看到的题目和交卷后判分的题目可能就不一致这会产生严重的数据错误。正确做法是在考试记录中保存一份“试卷快照”。快照可以是一套独立的题目副本表也可以是在考试记录中存储一份序列化的题目及标准答案。当学生交卷时系统按照快照内容来判分而不是去查实时题库。这样即使教师在考试中途修改了题库里的题目也不会影响已进行的考试。这个设计在真实在线考试平台里是必需品因为现实世界里教师确实可能发现题目有误需要紧急修正而所有考生的历史答题记录不应该跟着一起被篡改。从实现角度讲把整张试卷的标准答案序列化成JSON存入考试记录表是成本最低的方案。判分时直接解析JSON逐题比对复杂度低且不容易出脏数据。如果为了省这一点存储空间而选择回查关联表等真正出现题目被修改导致成绩作废的事故时再想补救就很被动了。3. Spring Boot后端核心实现3.1 接口设计与工程结构划分后端工程采用经典的分层架构Controller层负责接收请求和参数校验Service层处理业务逻辑Mapper层通过MyBatis-Plus操作数据库。实体类放在entity包里DTO和VO区分输入输出对象统一响应体用Result类包装。这样的分割在毕设论文中可以直接画成三层架构图答辩时也方便按层次讲解。核心接口基本上围绕考试闭环来定义# 认证模块 POST /api/auth/login # 登录返回JWT令牌 GET /api/auth/info # 获取当前登录用户信息 # 题库与题管模块 GET /api/questions # 分页查询题目按题型/难度/题库筛选 POST /api/questions # 新增题目 POST /api/questions/batch # 批量导入题目 PUT /api/questions/{id} # 修改题目 DELETE /api/questions/{id} # 删除题目 # 组卷模块 POST /api/papers # 保存手动组卷的试卷 POST /api/papers/auto # 按规则自动抽题生成试卷 GET /api/papers/{id}/questions # 查看试卷的完整题目列表 # 考试模块 POST /api/exams # 创建并发布考试 GET /api/exams/student # 学生视角获取分配给自己的考试列表 POST /api/exams/{id}/start # 学生开始考试后端记录开始时间 POST /api/exams/{id}/answer # 保存单个题的答题结果自动保存 POST /api/exams/{id}/submit # 交卷后端触发判分 GET /api/exams/{recordId}/result # 查询考试成绩与答题详情3.2 自动判分策略从需求到代码自动判分是整个系统中最有“含金量”的逻辑。客观题单选、多选、判断可以完全由系统判断主观题简答、论述则需要保留给教师人工评分。这里先说客观题的判分策略。单选题的逻辑最简单学生答案等于标准答案得分否则零分。判断题同理。多选题稍微复杂一些常见判分规则有两种——全对才得分或者漏选得一半分。具体采用哪种规则可以在系统设置中进行配置这将作为判分规则表的一行记录来使用。我用一个策略模式来组织判分代码便于后续扩展新的题型。核心代码如下Service public class AutoScoringService { private final StringAnswerCompareUtil compareUtil; public AutoScoringService(StringAnswerCompareUtil compareUtil) { this.compareUtil compareUtil; } Transactional public void scoreObjectiveQuestions(Long examRecordId) { ExamRecord record examRecordMapper.selectById(examRecordId); if (record null || record.getExam() null) { throw new BizException(考试记录不存在或考试信息缺失); } // 只处理客观题对应的答题明细 ListExamAnswer answerList examAnswerMapper.selectList( new LambdaQueryWrapperExamAnswer() .eq(ExamAnswer::getRecordId, examRecordId) .in(ExamAnswer::getQuestionType, QuestionType.SINGLE_CHOICE, QuestionType.MULTI_CHOICE, QuestionType.TRUE_FALSE) ); int objectiveScore 0; for (ExamAnswer answer : answerList) { Question question questionMapper.selectById(answer.getQuestionId()); int score compareUtil.compareAndScore(answer, question); answer.setScore(score); examAnswerMapper.updateById(answer); objectiveScore score; } record.setObjectiveScore(objectiveScore); // 主观题得分由后续人工阅卷回填当前为0 record.setTotalScore(objectiveScore); examRecordMapper.updateById(record); } }判分逻辑中最容易犯错的地方是多选字符串的比对。学生在答题时提交的答案是“A,B,C”标准答案也是存成“A,B,C”如果用字符串equals直接比较顺序不一样就会判错。因此建议将选项字符串拆成集合后先排序再比较。我为了让多选判分支持漏选得分基于一个改进策略设计了判分工具类核心思路是先算出“正确交集大小”再与标准答案数量对比public class StringAnswerCompareUtil { /** * 根据题型判分支持多选漏选得分规则。 */ public int compareAndScore(ExamAnswer answer, Question question) { if (question.getType() QuestionType.SINGLE_CHOICE || question.getType() QuestionType.TRUE_FALSE) { return answer.getAnswer().trim().equals(question.getAnswer().trim()) ? question.getScore() : 0; } if (question.getType() QuestionType.MULTI_CHOICE) { SetString standard splitOptions(question.getAnswer()); SetString student splitOptions(answer.getAnswer()); if (student.equals(standard)) { return question.getScore(); } // 判断是否正确选项的子集即没有选错项 if (standard.containsAll(student) !student.isEmpty()) { return question.getScore() / 2; } return 0; } return 0; } private SetString splitOptions(String answer) { SetString set new HashSet(); if (answer null || answer.isEmpty()) { return set; } String[] parts answer.split(,); for (String part : parts) { set.add(part.trim()); } return set; } }客观题判分之外填空题的判分最容易引起争议。答案是“北京”还是“北京市”在字符串比对中就是两个答案。实际开发中我采用了两种容忍策略如果启用模糊判分就去掉学生答案和标准答案中的所有空格和标点后再比较如果选项条件允许也可以采用包含匹配即学生答案包含标准答案就得分。具体采用哪种方式由教师录入题目时设置。3.3 交卷幂等与并发防重现场演示最容易翻车的地方在线考试系统在交卷环节有一个经典并发问题学生网络卡顿点击交卷按钮没有立即得到响应于是又点了两次。如果后端不做防重处理就会生成两次交卷请求产生两条考试记录成绩翻倍。这个问题不仅仅存在于存档演示里真实场景中也非常常见。解决思路分三层。第一层是前端控制点击交卷按钮后立即禁用弹出确认框。第二层是数据库层在考试记录表上对考试ID学生ID建立唯一约束从数据库层面保证一个学生只可能有一条考试记录。第三层是业务逻辑层交卷方法开始时先检查记录状态如果已经是“已交卷”直接返回“重复提交”的提示不再执行后续判分逻辑。看似简单的三层防御每一层都有实际作用。我在复盘模拟项目时发现过这样一个问题有开发者只做了前端控制认为“用户不会那么快点两次”结果是在答辩演示时恰好网络抖动真的就生成了两条记录。所以从安全可靠的角度来看‘前后端双保险数据库唯一索引兜底’这种方案是更稳妥的做法。提交接口的核心逻辑如下PostMapping(/api/exams/{id}/submit) public Result submitExam(PathVariable Long examId, RequestParam Long recordId) { ExamRecord record examRecordMapper.selectById(recordId); if (record null) { return Result.error(考试记录不存在); } if (ExamStatus.SUBMITTED.equals(record.getStatus())) { return Result.error(试卷已提交请勿重复操作); } record.setStatus(ExamStatus.SUBMITTED); record.setSubmitTime(LocalDateTime.now()); examRecordMapper.updateById(record); autoScoringService.scoreObjectiveQuestions(recordId); return Result.success(); }这里有个实践中的细节判分操作放在交卷请求的同一个事务里会延长接口响应时间。如果试卷有100道题判分可能消耗几十毫秒到一两百毫秒对用户体验稍有影响。更好的做法是交卷接口只做状态变更随后异步触发判分前端轮询或者查询结果接口时再刷新成绩。为了在毕设演示时让最终效果良好我建议先使用同步判分让交卷结果立即返回随后再视情况进行优化未尝不可。3.4 考试状态机设计后端逻辑最值得写进论文的亮点一场考试在整个生命周期中会经历多个状态未开始、进行中、待阅卷、已完成。不同状态下后端接口的可用行为截然不同。例如学生不能在“未开始”状态下答题教师不能在“进行中”状态下修改试卷结构。采用状态机设计把考试状态的流转规则集中管理这样比在各个Service里散落着各种if判断要好维护得多。核心状态转移如下public enum ExamStatus { NOT_STARTED, // 未开始 ONGOING, // 进行中 PENDING_MARK, // 待阅卷存在主观题 COMPLETED // 已完成 public boolean canStart() { return this NOT_STARTED; } public boolean canSubmit() { return this ONGOING; } public boolean canMark() { return this PENDING_MARK; } }需要注意的是考试开始时间是由定时任务驱动的。系统每分钟扫描一次考试表将到达开始时间的考试状态由“未开始”更新为“进行中”将到达结束时间的考试强制标记为“已结束”并自动交卷还没有提交的考生。没有这个定时任务就会出现学生已经过了截止时间仍然能进入考试页面的漏洞。定时任务可以用Spring自带的Scheduled一个方法搞定。但要注意如果部署在多台服务器上每台实例都会执行同一个任务可能重复处理数据。这种场景下建议把定时任务加一个分布式锁或者利用数据库行锁来保证幂等。毕设阶段用单机部署Scheduled就够了但论文中把这个分布式场景讨论一下会显得思考更深入。4. Vue前端核心实现与交互设计4.1 前端工程结构与路由设计前端使用Vue 3 Vite Vue Router Pinia Element Plus的组合。Vue 3的组合式API在组织业务代码时比选项式API更灵活Element Plus组件库可以让后台管理界面快速成型不用手写表格和弹窗。路由设计分为三块登录页、教师端、学生端。教师端路由嵌套在Layout布局下侧边栏包含题库管理、试卷管理、考试管理、成绩统计学生端路由同样嵌套在Layout下侧边栏包含我的考试、我的成绩。通过路由守卫配合JWT令牌进行访问控制未登录跳转到登录页角色不匹配跳转到无权限页。// 全局前置守卫 router.beforeEach((to, from, next) { const token localStorage.getItem(token); if (to.path /login) { next(); return; } if (!token) { next(/login); return; } const userRole localStorage.getItem(userRole); if (to.meta.roles !to.meta.roles.includes(userRole)) { next(/403); return; } next(); });4.2 题库管理与批量导入教师端最核心的日常操作教师端有三个管理页面其中题库管理页是日常使用频率最高的。题目录入页需要支持五种题型每种题型的表单字段不一样单选题和判断题有选项区域填空题有正确答案输入框简答题有参考答案的富文本输入区。批量导入功能我强烈建议做上。真实使用场景是一位教师有几十道题目要录入如果一道一道手输光录入就能浪费一两个小时。实现方案可以是前端解析Excel或Word模板也可以后端解析Excel并逐行转换成题目记录。为了降低实现复杂度可以采用前端解析上传文件取到数据后调用批量接口的方式这样后端不需要引入额外的文件解析依赖。题目列表的筛选条件包括题库分类、题型、难度、关键字。列表一定要做分页否则题库数据到几百条时页面会明显卡顿。操作列提供编辑和删除按钮删除题目需要弹出确认框因为题目可能已经被试卷引用。4.3 组卷流程手动组卷与自动抽题的页面交互逻辑组卷页面是整个教师端交互最复杂的模块。以手动组卷为例左侧是题目筛选区右侧是已选题目的列表。教师按题型筛选题目点击“加入试卷”按钮后题目进入右侧列表同时可以调整分值、排序也可以移除。自动抽题功能的设计让我很满意以下方案可以作为参考教师设置“单选题XX道、多选题XX道、判断题XX道总分期望值XX”系统根据题库中已有的题目按难度比例随机抽取并生成试卷随后教师还可以在题目列表中微调。这里要注意的这个设计要点是“总分会校验”和“数量不足会提示”。例如单选题题库只有20道但教师要求抽25道系统应当给出明确提示而不是静默返回三套可选方案——这就属于那种“正常情况下不易察觉、但实际部署后一定会遇到的问题”所以必须提早处理。4.4 考试页面开发计时器、题目导航与自动保存学生端的答题页是前端技术挑战最大的页面。它必须解决三个问题倒计时与时间同步、题目快速切换定位、答案即时保存。倒计时不能只靠前端setInterval计数因为页面刷新后计时器会归零。正确做法是从后端获取考试截止时间前端根据截止时间与当前时间计算剩余时长。每次倒计时更新时向服务端发送一次“心跳”记录剩余时间在数据库中这样即使学生退出页面再重新进入也能精确恢复自己的答题现场时间不会被“白嫖”掉。题目导航区采用网格化显示所有题号排列成小方块。当前正在作答的题目高亮边框已作答的题目显示为绿色未作答的显示为灰色。通过一个Object类型的answerMap来管理答案状态const answerMap reactive({}); // 题目导航渲染逻辑 const gridClass (questionId) { if (answerMap[questionId] ! undefined answerMap[questionId] ! ) { return answered; } return unanswered; };自动保存采用两种策略配合一是在切换题目时立即保存当前题的答案二是每30秒自动保存一次整个页面所有已变更的答案。每次保存调用后端接口将答案写入答题明细表。保存操作是异步的不阻塞用户操作但对于需要在交卷前同步所有未保存答案的要求则可在交卷期间再次提交当前页答案。这种设计的优点是即使用户中途关闭浏览器已答题目也不会丢失。绕过得注意一个坑千万不要在每次输入或点击选项时都同步调接口保存答案一秒钟可能触发十几二十次请求后端扛得住数据库连接也会被打爆。把保存动作交给“切题”和“定时”两个触发点流量会温和很多。4.5 Axios拦截器与异常提示一家之言的实战经验axios拦截器是前端项目中“性价比”最高的一层封装。统一的请求拦截器需要做三件事把JWT令牌加到请求头、统一设置请求超时时间、统一处理文件上传的Content-Type。而响应拦截器做的是更重要的异常处理。service.interceptors.response.use( (response) { const res response.data; if (res.code 200) { return res; } ElMessage.error(res.message || 请求失败); return Promise.reject(new Error(res.message)); }, (error) { if (error.response error.response.status 401) { ElMessage.error(登录已过期请重新登录); localStorage.removeItem(token); router.push(/login); } else if (error.code ECONNABORTED) { ElMessage.error(请求超时请检查网络); } else { ElMessage.error(error.message || 网络异常); } return Promise.reject(error); } );这套拦截器做好之后业务代码里几乎不用再写try/catch和错误提示。教师创建考试失败时后端返回业务码前端统一弹窗提示体验一致性会非常好。这里有一个很重要的细节不要因为省事就不让错误处理统一封装否则后期调试的难度会成倍增长。5. 核心流程实操演示从启动到跑通完整考试5.1 环境准备与项目初始化在开始之前我先把需要用到的工具版本和配置梳理成清单。这套组合是我实测过多次、踩过版本坑之后固定的工具建议版本说明JDK1.8 或 11Spring Boot 2.x 推荐 JDK83.x 需要使用 JDK17MySQL5.7 或 8.0注意8.0以上需要修改时区配置Maven3.6后端依赖管理Node.js16.x 或 18.x前端构建环境建议16以上npm/yarn任意前端包管理器Vite4.xVue 3 的推荐构建工具Redis不需要毕设阶段可以不用节省部署步骤后端使用Spring Boot 2.7.x配合MyBatis-Plus 3.5.x与Spring Security。主要依赖如下dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId /dependency dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-security/artifactId /dependency dependency groupIdcom.baomidou/groupId artifactIdmybatis-plus-boot-starter/artifactId version3.5.3/version /dependency dependency groupIdio.jsonwebtoken/groupId artifactIdjjwt/artifactId version0.9.1/version /dependency5.2 后端工程目录与配置说明后端工程包名可以设置为com.example.exam目录结构如下src/main/java/com/example/exam/ ├── common/ # 通用类统一返回体、异常处理、常量定义 ├── config/ # 配置类Security配置、CORS配置、MyBatis-Plus配置 ├── controller/ # Controller层按模块拆分 │ ├── AuthController.java │ ├── QuestionController.java │ ├── PaperController.java │ ├── ExamController.java │ └── ScoreController.java ├── service/ # Service层业务接口与实现 ├── mapper/ # MyBatis-Plus Mapper接口 ├── entity/ # 数据库实体类 ├── dto/ # 接收前端参数的DTO └── vo/ # 返回给前端的VOapplication.yml文件的关键配置项如下server: port: 8080 spring: datasource: url: jdbc:mysql://localhost:3306/exam_db?useUnicodetruecharacterEncodingutf8useSSLfalseserverTimezoneAsia/Shanghai username: root password: your_password driver-class-name: com.mysql.cj.jdbc.Driver mybatis-plus: configuration: map-underscore-to-camel-case: true global-config: db-config: logic-delete-field: deleted logic-delete-value: 1 logic-not-delete-value: 0 jwt: secret: your-secret-key expire: 86400000 # 24小时毫秒数一个容易踩的坑是MySQL 8.0以上的时区问题。按上面的URL配置serverTimezoneAsia/Shanghai就不会出现“The server time zone value...的启动报错。字符集用utf8mb4而不是utf8才能正确存储emoji和中文标点。5.3 前端工程初始化与关键配置前端工程使用Vite创建npm create vitelatest exam-web -- --template vue cd exam-web npm install vue-router pinia element-plus axios关键配置是Vite开发服务器的代理转发这样前端开发时请求/api开头的接口会自动转发到后端8080端口避免开发环境的跨域问题// vite.config.js export default defineConfig({ plugins: [vue()], server: { port: 5173, proxy: { /api: { target: http://localhost:8080, changeOrigin: true } } } });5.4 完整考试流程演示环境跑起来后我按正常的业务链路完整走一遍确保所有模块联动正常。第一步使用账号登录。登录成功后在浏览器控制台的Application标签下能看到token。每次请求都会在Authorization请求头中携带JWT令牌后端通过Spring Security的过滤器解析并校验身份。第二步教师创建题库并批量导入题目。进入题库管理页点击“批量导入”选择提前准备好的Excel文件前端解析出题目数据调用批量新增接口列表刷新后能看到所有题目。第三步教师组卷。选择手动组卷从题库中筛选题目加入试卷并设置分值保存试卷。或者切换自动抽题设置数量后系统自动生成试卷。第四步教师发布考试。选择一张试卷设置考试名称、考试时间、时长、及格分选择参与考试的学生班级保存后考试状态为“未开始”。第五步学生登录后进入“我的考试”页面能看到已分配给自己的考试。点击“开始考试”进入答题页面。每答完一题切换题目时自动保存状态栏会记录剩余时间。第六步学生完成所有题目后点击交卷。如果存在未答题目页面会弹出确认提示确认后交卷请求发出后端判分前端跳转到成绩页面。如果是含有主观题的考试成绩页会显示“客观题得分”同时提示“主观题待教师批改”。第七步教师登录后进入成绩管理页查看所有学生的成绩列表对包含主观题的考试进行阅卷打分确认后成绩发布学生端即可看到最终总分。这套流程走通之后系统就是一个完整的可交付项目。我在实际模拟过程中发现流程是否顺畅是最影响答辩体验的环节建议在走查的过程中多注意细节反馈比如交卷后的提示、成绩页的展示、空数据时的占位文案这些细节往往决定了答辩现场的整体观感。6. 常见问题排查与避坑实录在线考试系统虽然业务逻辑不算晦涩但实际开发过程中有不少问题反复出现。我把这些问题整理成一张速查表方便开发时对照排查。问题现象可能原因排查思路与解法前端请求接口返回到后台报错 404前端代理未生效或接口路径写错先看F12网络请求的URL在Network标签确认是否转发到正确端口检查Vite代理与后端拼接路径是否一致。登录成功后访问接口返回401JWT令牌未在请求头携带或令牌过期查看axios请求头里是否有Authorization字段确认令牌签发密钥与校验密钥一致确认token未过期。前端页面加载时所有按钮失效CORS跨域未配置或响应被拦截浏览器控制台若报跨域错误在后端增加CORS配置允许前端域名访问。交卷后成绩等于0判分逻辑未执行或答案字符串比对失败检查答题明细表答案是否为空检查多选答案是A,B还是“A,B ”这种带空格格式统一清洗后再比对。学生重复提交试卷生成多条记录无幂等保护增加数据库唯一索引exam_id student_id并在提交接口中检查记录状态。考试开始时学生无法进入页面考试状态未由定时任务更新检查Scheduled是否配置了fixedDelay/cron表达式检查当前时间与考试开始时间的时区差异。修改题库题目后已结束考试的成绩被影响未使用试卷快照考试发布时将试卷内容序列化存入考试记录判分基于快照。多选题选了A,B但判分为0多选规则设置为全对才得分或答案中包含空格根据判分规则确认把字符串拆分后trim再比较。填空题答案与标准答案“差不多”但判错字符串精确比对在题目层面配置“模糊判分”策略或者录入时使用包含匹配。老师删除题目后试卷中仍显示该题试卷题目关联表未清理或故意保留删除题目时提醒“该题已在N张试卷中被引用”或同步清理关联采用逻辑删除可避免外键约束困扰。前端打包后接口请求地址错误API基础路径配置写死为localhost:8080使用环境变量区分开发与生产环境生产环境Nginx配置反向代理。下面挑三个细节重点展开。第一个是“时间”问题。在线考试系统涉及开始时间、结束时间、交卷时间等多个时间点。如果服务器时区不一致就可能出现学生看到考试已经开始后端却仍然返回“未开始”的情况。我的方案是前后端所有时间统一用时间戳或标准时间字符串避免因为时区偏差导致时间判断出错。第二个是题目删除策略。我强烈建议采用MyBatis-Plus的逻辑删除而不是物理删除。考试已经引用过的题目如果被物理删除考试记录中的题目关联就变成死链。逻辑删除只是打一个deleted标记查询时默认过滤历史数据还能保留完整性。这个设计也和试卷快照机制相辅相成。第三个是MyBatis-Plus的字段自动填充功能。创建时间和更新时间不要在每个Service里手动设置。定义一个MetaObjectHandler实现类在插入时统一填充createTime和updateTime字段既省代码又不会漏填。7. 可扩展方向从合格到优秀的进阶之路如果基础功能已经实现完毕答辩时间允许的情况下可以从以下几个方向给系统加深度加分项。第一个方向是防作弊能力。考试系统最常见的需求是试题顺序与选项顺序随机。当同一张试卷分发给不同学生时系统可以随机打乱题目顺序和选项顺序减少作弊的可能性。虽然这种机制不能完全防止作弊但作为系统的功能亮点展示效果很好技术实现也不复杂在组卷的查询层加入随机排序即可。第二个方向是题库的批量导入模板优化。目前采用前端解析Excel的方式如果想做得更专业可以引入Apache POI做后端解析并提供标准的模板下载。教师在Word里排版题目后导入系统的功能更是企业级系统的常规需求。第三个方向是成绩分析报表。前端的ECharts可以用来展示班级分数分布、及格率、最高分最低分等统计信息。这是一个纯前端展示层的增强后端只需提供聚合统计数据。答辩时图表比表格更能直观呈现系统的价值。第四个方向是知识点维度。给题目增加“所属章节”或“知识点”标识考试结束后生成学生的知识点掌握情况雷达图和薄弱点分析报告。这一项如果做好了在线考试系统就从一个简单的答题工具升级成了一个教学辅助平台其应用价值和社会意义已经有了更高的讨论维度。这些扩展项不需要全部实现挑选一到两个做出来放在论文的“系统特色”章节里答辩打动评委的效果比堆砌十个普通功能模块都要好。8. 前后端部署把项目放到真实服务器上毕设项目演示一般有两种方式本机演示与云服务器演示。本机演示只需要同时启动前端开发服务器和后端Spring Boot应用适合答辩现场环境不稳定的情况。云服务器部署则适合指导教师要求在系统截图里看到“域名访问地址”的毕设。云部署的标准流程是后端打包成JAR前端构建成静态文件由Nginx托管。Nginx在这里承担两个职责托管前端静态页面以及反向代理API请求到后端服务。一个典型配置片段如下server { listen 80; server_name your-domain.com; # 前端静态文件 root /var/www/exam-web/dist; index index.html; # 前端路由支持刷新页面时不出现404 location / { try_files $uri $uri/ /index.html; } # API反向代理 location /api { proxy_pass http://localhost:8080; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; } }这里有一个细节值得注意。如果Nginx使用proxy_pass http://localhost:8080;且不带路径原始请求的/api前缀会原样转发到后端。这就要求后端接口的路径必须保留/api前缀与controller层定义的路径保持一致。如果后端controller路径没有/api前缀Nginx需要做路径剥离location /api { proxy_pass http://localhost:8080/; }尾部的斜杠决定了路径前缀是否被剥离这个细节在实际部署中很容易让人困扰。为了避免反复试错建议统一在后端controller的RequestMapping上加/api前缀这样前端代码里的请求路径与后端接口路径完全对齐部署时不需要做多余处理。数据库部署方面使用source exam_db.sql导入建库脚本即可。注意导入前必须先创建数据库并设置字符集否则中文乱码问题会很快出现。后端启动命令是最基础的nohup java -jar exam-server.jar exam.log 21 启动后检查日志确认无异常后再启动Nginx。之后再用浏览器访问域名走一遍“登录-考试-交卷-查成绩”的完整流程确认前后端联调正常。在数据库导入这块我把具体操作写得更细一些方便照着执行# 进入MySQL客户端 mysql -u root -p # 创建数据库并指定字符集 CREATE DATABASE exam_db DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci; # 导入初始化脚本 USE exam_db; SOURCE /path/to/exam_db.sql;导入成功后验证一下核心表数据SELECT COUNT(*) AS question_count FROM question; SELECT COUNT(*) AS user_count FROM sys_user;如果初始脚本中包含测试数据这两条查询应该能返回大于0的结果说明数据库初始化成功。9. 事后复盘与个人经验分享整个在线考试系统从零到一实现下来最深的体会是这种“经典选题”想要做出彩不在于功能多而在于关键设计想得有多深。考试快照机制保证了历史的不可篡改交卷幂等保证了数据的唯一性状态机让整个业务流转清晰可控这几点在答辩时如果都能讲清楚系统的完成度和思考深度会非常明显。反而是那些把系统堆到二十多张表、十几个管理页面的方案容易在数据维护上疲于奔命。我在带项目的时候经常提醒遇到同样问题的开发者数据库设计阶段要舍得花时间。题目与试卷的关系、试卷与考试记录的关系、考生答题明细的粒度这三个核心模型如果设计清楚了后面的代码基本是水到渠成的状态。如果在这块含糊三天两头改表结构前后端接口也会频繁变动那种痛苦经历过一次就会明白什么叫得不偿失。另外一点是关于前端细节。很多毕设项目的功能逻辑没有问题但页面观感停留在“能用”的层面。在线考试系统的学生答题页尤其需要认真打磨——题号导航的配色、交卷确认弹窗的文案、倒计时变色提醒这些细节叠加起来的效果非常显著。用人与食物之间的类比来说内容是第一位的但摆盘也很重要。简单的横向评分外观看上去的舒适度会直接影响答辩现场的第一印象。最后分享一个实用的小技巧开发阶段用Vite的mock功能模拟部分后端接口可以提前把前端页面全部写完等后端接口稳定后直接切换代理实现并行开发。做法是在Vite配置中增加一个before钩子针对指定接口返回假数据。这样两个人在团队协同的场景下就不会因为后端接口联调进度不一致而互相等待。后端稳定后记得要处理的一个小坑是有些BI工具或者浏览器插件会扫描页面中所有的input造成登录页出现莫名其妙的自动填充行为。如果遇到给登录表单加autocompleteoff即可。如果时间充裕强烈建议把这个项目继续往前推一步给系统加上“导出成绩Excel”的功能用EasyExcel或者POI生成班级成绩表。虽然功能本身不大但它是真实教学场景中教师早晚会用到的基础需求有了它系统的交付完整度会再上一个台阶也让整个系统在“在线考试系统”所聚焦的场景里真正站得住脚。