news 2026/9/15 23:06:34

SpringBoot+Vue+MySQL在线考试系统实战:从数据库设计到部署全流程复盘

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
SpringBoot+Vue+MySQL在线考试系统实战:从数据库设计到部署全流程复盘

从零到一拆解一个 SpringBoot+Vue+MySQL 在线考试系统:项目架构、数据库设计、论文与部署全流程复盘

每年毕业季都会看到一堆人卡在在线考试系统这类题目上。说实话,这个选题确实经典——它不像商城系统那样堆业务模块,也不像管理系统那样偏纯增删改查,而是有真实的状态流转、权限控制、时间约束和自动评分逻辑,能把 SpringBoot、Vue、MySQL 这三件套的核心能力都覆盖到,又不会复杂到毕设阶段做不完。所以我今天就把这套 SpringBoot+Vue+MySQL 在线考试系统的完整实现思路拆开来讲,从功能设计、数据库建模,到后端接口、前端页面,再到论文结构和部署上线,一条线捋清楚。

这篇内容适合正在做同类毕设的同学,也适合想系统了解前后端分离项目完整落地过程的初学者。我会把我常年在项目里验证过、也帮学生避过坑的那套做法直接摆出来,包括表结构设计、核心接口逻辑、前端答题状态管理这些关键细节,以及论文里该写什么、部署时该注意什么,一次性讲透。

1. 项目整体设计与功能模块拆解

先把核心定位说清楚。在线考试系统的本质是:把传统纸面考试的出题、组卷、答题、收卷、批改、成绩统计这几个环节全部线上化。虽然听起来不复杂,但一旦落到系统设计上,你就得同时处理三种不同角色的诉求——学生要顺畅答题、教师要灵活组卷和批改、管理员要维护基础数据和监控考试状态。

1.1 角色权限体系与功能边界

我见过不少起步阶段的同学一上来就急着写代码,结果写到一半发现教师和管理员功能边界含混不清,到处都在改。实际上这套系统只需要划分三种角色,功能边界理清楚之后,代码写起来基本不会返工:

  • 管理员:负责维护系统基础数据,包括班级信息、学生账号管理、教师账号管理,以及全局的考试数据统计。管理员不直接出题,也不参与考试业务细节。
  • 教师:核心业务角色,负责题库维护(单选、多选、判断题)、试卷组卷策略配置、创建并发布考试,以及考试结束后的手动批阅与成绩管理。
  • 学生:参与考试的主体,可以查看待参加考试、在线答题、自动提交,并在教师公布成绩后查看自己的考试结果和错题解析。

这个权限划分看起来很直白,但实际编码时需要注意一个核心细节:按钮级权限。后端接口除了登录校验必须在后端做,前端页面也要根据角色动态渲染菜单和操作按钮,否则只靠后端拦截器控制接口权限,学生在页面上仍然能看到教师的操作入口,体验上就会露怯。

1.2 核心业务模块划分

我把整个系统的功能模块划分为六大块,这也是后面论文目录的核心骨架:

  1. 用户认证模块:登录、注册、登出、密码加密存储、个人信息维护。
  2. 题库管理模块:题目的增删改查,支持单选、多选、判断题三类题型,题目关联知识点标签,便于试卷随机组题。
  3. 试卷组卷模块:教师创建试卷,定义试卷名称、总分、考试时长;组卷方式支持固定选题和按规则随机抽取两种。
  4. 在线考试模块:学生进入考试倒计时、逐题作答、左侧答题卡实时同步、自动保存答案、倒计时结束自动交卷。
  5. 自动评分模块:客观题(单选、判断)由系统实时评分;多选支持严格判分(少选不得分)和按比例给分(少选按选项数量折算)两种策略;成绩自动写入数据库。
  6. 成绩与统计模块:学生查看成绩、试卷回看、错题解析;教师查看班级成绩分布、导出成绩表。

这六个模块是漏斗式递进的:认证是所有模块的地基,题库是试卷的数据来源,试卷是考试的配置载体,考试模块负责运行时流程,评分是考试的结果处理,统计是结果的展示出口。理解了这个依赖关系,你会发现代码组织上其实是有天然顺序的,不容易写乱。

1.3 为什么选 SpringBoot + Vue + MySQL

选题时技术栈不是随便定的,SpringBoot + Vue + MySQL 这个组合能成为毕业设计主流,背后是有实际原因的。

后端用 SpringBoot,是因为它把 Spring MVC、事务管理、数据访问这些基础能力都整合好了,不用像 SSM 时代那套繁琐的 XML 配置,启动一个内嵌 Tomcat 就能跑。更重要的是 SpringBoot 有非常成熟的生态——Spring Security 或 JWT 做认证、MyBatis-Plus 做数据层、Redis 做缓存和分布式锁,都有现成的支持,遇到问题时社区资料几乎能覆盖所有常见场景。

前端用 Vue,是因为它的生态和学习曲线刚好适合毕设周期。Vue 的单文件组件组织方式清晰,Element UI / Element Plus 组件库能快速撑起后台管理系统的界面,配合 Vue Router 管理路由、Axios 处理请求、状态管理用 Pinia 或者简单的 provide/inject,一个完整的管理端页面开发效率极高。

数据库选 MySQL,理由更直接:它足够稳定、资料海量、安装部署方便,而且学生的课程几乎都接触过 MySQL 的 SQL 编写和表设计。在线考试系统的数据关系虽然不少,但量级完全是 MySQL 的舒适区,不需要引入更重的数据库中间件。

2. 数据库设计:五张核心表的建模思路

在线考试系统能不能做好,数据库设计占一半。表结构设计不合理,后面要么写 SQL 写出痛苦面具,要么频繁在 Java 代码里补各种逻辑来兜底。我把这套项目最核心的几张表拆开来逐一说,直接给你能落到项目里的表结构。

2.1 用户表

用户表是最基础的表,注意不要把管理员、教师、学生拆成三张表来存。三种角色统一放一张 user 表,通过 role 字段区分(1=管理员,2=教师,3=学生),这样可以大幅简化登录认证逻辑,也便于未来扩展成更多角色(比如监考老师)。核心字段如下:

CREATE TABLE `user` ( `id` bigint(20) NOT NULL AUTO_INCREMENT, `username` varchar(50) NOT NULL COMMENT '登录账号', `password` varchar(100) NOT NULL COMMENT '密码,BCrypt加密存储', `real_name` varchar(50) DEFAULT NULL COMMENT '真实姓名', `role` tinyint(4) NOT NULL DEFAULT '3' COMMENT '角色:1管理员 2教师 3学生', `class_id` bigint(20) DEFAULT NULL COMMENT '班级ID,仅学生有值', `email` varchar(100) DEFAULT NULL, `phone` varchar(20) DEFAULT NULL, `status` tinyint(4) NOT NULL DEFAULT '1' COMMENT '状态:1启用 0禁用', `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, `update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (`id`), UNIQUE KEY `uk_username` (`username`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户表';

这里有几个细节值得注意。密码字段长度我留了100,是给 BCrypt 加密算法预留的,BCrypt 生成的结果是60位左右,50的长度不够。username 加唯一索引是因为登录时按 username 查询,走索引能避免全表扫描。role 字段用 tinyint 而不是字符串,是为了查询效率,同时配合代码里的枚举类去维护,而不是散落一地的魔法值。class_id 关联班级时,如果学校场景复杂也可以用单独的 class 表维护班级信息,毕设阶段简单外键即可。

2.2 题库表

题库表的设计决定了组卷和后端评分逻辑的复杂度。我见过有人把每道题的选项存JSON字符串塞进一个字段里,这样虽然插入方便,但查询、展示、统计都很痛苦。正确做法是拆开:question 表存题目的公共信息,selection 表(或选项表)存每道题的选项列表。

question 表核心字段如下:

CREATE TABLE `question` ( `id` bigint(20) NOT NULL AUTO_INCREMENT, `type` tinyint(4) NOT NULL COMMENT '题型:1单选 2多选 3判断', `content` text NOT NULL COMMENT '题干内容', `analysis` text COMMENT '答案解析', `difficulty` tinyint(4) DEFAULT '1' COMMENT '难度:1简单 2中等 3困难', `knowledge_point` varchar(100) DEFAULT NULL COMMENT '知识点标签', `creator_id` bigint(20) NOT NULL COMMENT '创建人ID(教师)', `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, `update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (`id`), KEY `idx_type` (`type`), KEY `idx_creator` (`creator_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='题目表';

has_options 这个字段别加。很多设计喜欢加一个标记位表示判断题有没有选项,实际上判断题可以统一为“选项A:正确、选项B:错误”的方式来处理,题型一致性对编码和解码都有好处。判断和单选的选项结构完全一致,多选只是答案数量多于一个而已。

这里有一个关键的建模决策:答案存哪里?我建议把正确答案也放到 question 表里,用两个字段表示:answer(标准答案)和 answer_detail(用于多选的按比例给分策略)。对于单选和判断,answer 就是正确选项的编号;对于多选,answer 就是正确选项的集合字符串(比如“1,3,5”)。判断逻辑在后端统一做,这样才能在并发提交时保证数据一致性。

2.3 试卷表与组卷表

试卷表是考试配置的载体,设计相对直接:

CREATE TABLE `exam` ( `id` bigint(20) NOT NULL AUTO_INCREMENT, `exam_name` varchar(100) NOT NULL COMMENT '考试名称', `creator_id` bigint(20) NOT NULL COMMENT '创建人(教师)', `exam_time_limit` int(11) NOT NULL DEFAULT '60' COMMENT '考试时长(分钟)', `total_score` int(11) NOT NULL DEFAULT '100' COMMENT '试卷总分', `start_time` datetime DEFAULT NULL COMMENT '考试开始时间', `end_time` datetime DEFAULT NULL COMMENT '考试结束时间', `status` tinyint(4) NOT NULL DEFAULT '0' COMMENT '状态:0未发布 1进行中 2已结束', `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (`id`), KEY `idx_status` (`status`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='考试表';

exam_question 关联表用于固定组卷,要存储题目顺序、每题分值,以及随机组卷模式下的随机规则快照。注意“快照”这个概念,后面会细讲。

CREATE TABLE `exam_question` ( `id` bigint(20) NOT NULL AUTO_INCREMENT, `exam_id` bigint(20) NOT NULL, `question_id` bigint(20) NOT NULL, `sort_order` int(11) NOT NULL DEFAULT '0' COMMENT '题目顺序', `score` int(11) NOT NULL DEFAULT '2' COMMENT '该题分值', PRIMARY KEY (`id`), KEY `idx_exam` (`exam_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='考试题目关联表';

为什么需要快照?这是在线考试系统和普通管理系统的最大区别之一。教师创建一场考试后,题目可能被修改,甚至被删除。但如果考试已经发布,学生答卷时的数据必须保持当时的题目状态,否则会出现“题目已经改了但成绩单还对不上”的严重数据事故。解决方式有两种:一是考试发布后把题目复制一份到考试题表;二是在考试题表里加一个 exam_question 的题目内容冗余字段。推荐用第二种的简化版——考试题目关联表里同时保存当时的题干快照和选项快照,这样即使原 question 表被改动,考试回看和成绩统计依然正确。

2.4 考试记录表与答题明细表

这两个表与考试过程强相关,也最容易被忽略。

CREATE TABLE `exam_record` ( `id` bigint(20) NOT NULL AUTO_INCREMENT, `exam_id` bigint(20) NOT NULL COMMENT '考试ID', `student_id` bigint(20) NOT NULL COMMENT '学生ID', `start_time` datetime DEFAULT NULL, `submit_time` datetime DEFAULT NULL, `status` tinyint(4) NOT NULL DEFAULT '0' COMMENT '答卷状态:0未开始 1进行中 2已提交 3已评阅', `score` decimal(5,1) DEFAULT NULL COMMENT '最终得分', `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (`id`), UNIQUE KEY `uk_exam_student` (`exam_id`, `student_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='考试记录表';

exam_record 是学生参与考试的唯一入口记录,每名学生在一场考试中只有一条记录,unique 约束就是防重复入场。它同时承担着保存答卷状态的作用,从“未开始”到“进行中”到“已提交”再到“已评阅”,整个生命周期都靠 status 字段流转。

答题明细表是逐题答案的存储表:

CREATE TABLE `exam_answer` ( `id` bigint(20) NOT NULL AUTO_INCREMENT, `record_id` bigint(20) NOT NULL COMMENT '考试记录ID', `question_id` bigint(20) NOT NULL COMMENT '题目ID', `student_answer` varchar(255) DEFAULT NULL COMMENT '学生作答内容', `correct_answer` varchar(255) DEFAULT NULL COMMENT '标准答案快照', `is_correct` tinyint(4) NOT NULL DEFAULT '0' COMMENT '是否正确:1正确 0错误', `score` decimal(5,2) NOT NULL DEFAULT '0.00' COMMENT '该题得分', PRIMARY KEY (`id`), KEY `idx_record` (`record_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='答卷明细表';

correct_answer 这个字段就是快照思想的又一体现。评分完成后再去 question 表查正确答案,如果题目被删除,成绩记录就崩溃了。把答案快照冗余进来,成绩查询永远独立于题库状态。

2.5 索引设计与常见设计坑

主键和常用的查询外键一定要建索引,比如 exam_answer 的 record_id 和 question 表的 type、creator_id。还要注意 MySQL 外键约束:我建议在物理层面不要强依赖外键约束,保持逻辑外键即可。原因很简单,用外键约束会导致数据插入删除时的锁竞争,而且一旦题目被改动、考试历史记录需要保留,外键反而成了枷锁。逻辑外键配合事务,数据一致性和灵活性都能兼顾。

另一个常见坑是日期时间字段的类型。考试开始时间、结束时间、提交时间建议直接用 datetime,而不要用 timestamp。timestamp 范围到2038年,虽然最近还远,但毕设答辩时评委偶尔会问这个问题,直接用 datetime 一劳永逸。更重要的是,所有的起止时间判断统一用后端服务器时间,不要依赖前端传入的时间,前端时钟是可以被改的。

3. SpringBoot 后端核心实现与避坑细节

后端是整套系统的逻辑大脑。学到 SpringBoot 阶段,大家都能写 CRUD,但考试系统真正的难点在状态控制、自动评分和权限校验这几个点上。我把这几块的实现思路完整过一遍。

3.1 项目结构与分层

我习惯的分层是标准的 Controller-Service-Mapper 三层,加上一个 config 包和一个 common 包。对于毕设规模,不必过度拆分,但至少要保证 Controller 不写业务逻辑、Service 事务边界清晰、Mapper 只做数据访问。项目结构大概是:

src/main/java/com/example/exam/ ├── config // 跨域配置、MyBatis-Plus 配置、JWT 拦截器配置 ├── controller // 用户、题库、试卷、考试、成绩等接口层 ├── service // 业务逻辑层,接口+实现 ├── mapper // MyBatis-Plus 的 Mapper 接口 ├── entity // 数据库实体类 ├── dto // 前端请求/响应对象,避免直接暴露实体 ├── common // 统一返回类、异常处理、常量枚举 └── util // JWT 工具类等

这里有个建议:DTO 层。很多同学喜欢直接把 Entity 返回给前端,图省事,但一旦前端需要的字段和后端实体字段不一致,就会被迫写一堆 @JsonIgnore 或者在实体里塞非表字段,慢慢腐化。用 DTO 接收前端参数、用 VO 返回视图数据,前期多写几个类,后期改接口时你会感谢这个决定。

3.2 JWT 登录认证

在线考试系统的登录认证我用 JWT 而不是传统 Session,原因有两个:一是前后端分离部署时,Session 要处理跨域 Cookie 携带问题,比较麻烦;二是 JWT 无状态,后端服务重启后用户不用重新登录,对考试这种长流程场景更友好。

核心实现是写一个拦截器或者 Spring MVC 的 HandlerInterceptor,拦截所有需要登录的接口,解析请求头里的 Authorization 字段,校验 JWT 合法并且未过期,然后把用户信息存入 ThreadLocal 供后续业务使用。

public class JwtInterceptor implements HandlerInterceptor { @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { String token = request.getHeader("Authorization"); if (token == null || !token.startsWith("Bearer ")) { throw new AuthException("未登录"); } token = token.substring(7); Long userId = JwtUtil.parseToken(token); if (userId == null) { throw new AuthException("登录已过期"); } UserContext.set(userId); return true; } }

需要特别提醒的一点是 JWT 密钥不要硬编码在代码里,尤其不要写“123456”这类密钥,虽然毕设答辩一般不会黑你的系统,但作为习惯还是要把它放到 application.yml 配置文件中,并且使用足够长的随机字符串作为签名密钥。另外,学生和教师两种角色共用一个拦截器即可,但要通过注解或者路径规则区分接口权限,比如 /api/student/** 只能学生访问,/api/teacher/** 只能教师访问,在拦截器里对路径前缀做角色判断即可。

3.3 考试发起与题目快照

考试发布是整个系统的核心接口。学生端进入考试时,后端接口要做的事情按顺序来:

  1. 校验考试是否在有效时间窗口内。
  2. 校验当前用户是学生且未被禁用。
  3. 在 exam_record 表中查到或创建考试记录。
  4. 根据 exam_id 查询 exam_question 表,得到该学生本次考试的试题列表。
  5. 返回给前端的数据中,题目选项和分值来自快照字段,而不是实时查 question 表。

从 security 角度说,学生请求某个考试详情时,如果考试还没到开始时间或者已经结束,后端必须返回明确的错误码,而不是返回题目列表让前端去判断。这个校验一定在后端做,前端只是辅助展示。

还有一点:同一名学生能否多次进入考试?答案是可以,前提是之前没有提交。比如学生做题做了一半关掉浏览器,重新登录后进入考试,应该能够看到自己之前保存的答案,而不是重新开始。这个功能依赖 exam_answer 表中已保存的答案数据。所以在线考试中“保存答案”这个动作必须有:每次切题或点击下一题时,前端把当前题目的答案存入后端的临时保存接口。

3.4 自动评分逻辑

自动评分只针对客观题。后端评分比前端评分可靠得多,不能相信浏览器传过来的得分,所有得分必须在后端根据标准答案重新计算。遍历 exam_answer 表逐题评分,纯 Java 运算即可,不需要什么高级框架。

private double calculateQuestionScore(StudentAnswer answer, Question question) { int questionType = question.getType(); if (questionType == 1 || questionType == 3) { // 单选、判断:完全匹配 return answer.getStudentAnswer().equals(question.getCorrectAnswer()) ? question.getScore() : 0; } else if (questionType == 2) { // 多选:两种策略 List<String> studentSelected = parseOptions(answer.getStudentAnswer()); List<String> correctOptionList = parseOptions(question.getCorrectAnswer()); if (isExactMatch(studentSelected, correctOptionList)) { return question.getScore(); } if (isSubsetMatch(studentSelected, correctOptionList)) { // 少选不选:按比例给分 int correctCount = 0; for (String opt : studentSelected) { if (correctOptionList.contains(opt)) { correctCount++; } } return question.getScore() * correctCount / correctOptionList.size(); } return 0; } return 0; }

注意多选策略的选择必须在考试发布时确定,不能全局统一写死。有的老师要求多选错选不得分,少选给部分分;有的老师要求多选只要有一项错就零分。这两种策略都需要在试卷配置里加一个字段,比如 strict_score 标记位,评分接口按不同策略执行。

3.5 事务与并发处理

考试提交接口是整个系统中并发风险最高的地方,因为考试时间到后大量学生同时点击交卷。这里的关键规避方式有两点:

第一点是防重复提交。用 exam_record 表的唯一索引(exam_id, student_id)来兜底,提交前先 UPDATE ... SET status = 2 WHERE id = ? AND status = 1,如果影响行数为0,说明已经被提交过或者成绩状态不正确,直接拒绝即可。这个 CAS 思路比先查再写要可靠得多,也不会被打到 DB 前端的防重复点击插件干扰。

第二点是事务边界。auto_submit 自动交卷接口和 submit_exam 手动交卷接口都标 @Transactional,防止评分写到一半数据库出错产生半截数据。事务中加一个 Redis 分布式锁或者直接用数据库记录的行级锁也可以,毕设阶段用数据库行级锁(SELECT ... FOR UPDATE)配合唯一索引就能解决大多数并发问题。

3.6 定时任务处理考试过期

学生考试到时间未点击提交怎么办?两个方案:一是后端提供一个当前时间超过考试结束时间就自动提交的检查接口;二是用 Spring 的 @Scheduled 写一个定时任务,每分钟扫一遍 exam_record 表,把 status=1 且截止时间早于当前时间的记录自动提交。

我建议毕设阶段用第二种方案,并且在论文里写清楚“通过定时任务自动结算超时未交卷的答卷”,这是一个加分点。需要注意定时任务的事务传播、每次扫描的数量限制,避免大批量数据一次性提交导致数据库压力抖动。你可以做成每次扫描 100 条、批处理的方式。

4. Vue 前端页面结构与关键交互实现

前端部分,很多人低估了工作量。后端 CRUD 好写,但考试答题页面的倒计时、答题卡、自动保存这些交互如果实现得粗糙,体验会显得不太完整,答辩时也可能被追问。我把前端结构和一个关键细节讲清楚。

4.1 前端工程结构与路由设计

前端用 Vue 3 + Element Plus + Vue Router + Pinia + Axios 是比较主流的选择。如果是要维护 Vue 2 项目,则对应 Element UI。三套角色使用的页面是混在一个前端工程里的,根据 UserInfo.role 动态渲染菜单。

src/ ├── api/ // 按模块封装的 axios 请求 ├── router/ // 路由配置,带前置守卫 ├── store/ // Pinia 状态管理 ├── layout/ // 主布局,侧边栏 + 顶栏 ├── views/ │ ├── login/ │ ├── admin/ │ ├── teacher/ │ │ ├── question-manage/ │ │ ├── exam-manage/ │ │ └── score-manage/ │ └── student/ │ ├── exam-list/ │ ├── exam-taking/ // 考试答题页 │ └── exam-result/

路由守卫是前端权限控制的第一道门槛。核心逻辑是:每次路由跳转前判断有没有 token,没有就强制跳登录页;有 token 再根据路由 meta 里配置的角色判断当前用户是否有权限进入,没有权限就跳回首页。虽然后端接口还有一层校验,但前端路由守卫能极大提升用户体验,避免用户点开一个页面后才弹出“无权限”报错。

4.2 考试答题页的设计要点

考试答题页是前端最复杂的单页,我拆成几个关键点来讲。

答题区设计和答题卡要联动。学生点击左侧答题卡上的题目编号时,路由参数保存当前题的序号,可以只把当前题目数据放到状态里,用 Pinia 的 store 维护当前题号、每题答案、剩余时间等状态。这里有一个比较实用的设计:题目列表一次性加载,答案也一次性加载(或至少把已保存答案一次性加载),切题时不发请求,只是本地状态切换,只有点击“保存下一题”或改变答案并失焦时才触发自动保存。

倒计时是用 setInterval 实现的,初始化时拉取服务器时间,然后不断减秒。注意:倒计时结束时即使前端没有触发提交,后端也要能兜底——因为在定时任务里已经扫描超时记录并自动提交了。前端倒计时和数据库的超时结算相互配合,而不是相互依赖。

自动保存的防抖处理也提醒一下。学生每改一道题的答案就立刻发一次请求,考虑极端情况会产生大量请求。我给的建议是:答题过程中每 10 秒检查一次是否有未同步的答案变更,有则批量提交;切题和交卷时强制同步。这样既保证数据安全性,又不会把后端接口打穿。

Vue 里来实现这个逻辑很简单,但你必须在组合式 API 里把 setInterval 和 beforeunload 事件处理干净,否则跳路由后定时器还在跑,会引发内存泄漏和重复保存。

4.3 数据回显与成绩单

考试提交后学生端查看成绩单,应该展示每道题的题干、自己的答案、正确答案、是否正确、得分、解析。这个页面的数据来自后端返回的已评阅答卷快照,前端不需要任何计算逻辑。也就是说,教师批阅完成的答卷数据是后端组装的,前端只是展示。这里引出一个实现细节:成绩列表需要支持“查看答题卡”“查看具体题目的学生答案和正确答案对比”,这类数据在后端组装 VO 时就要结构清晰:

{ "questionId": 12, "questionType": 2, "content": "以下哪些属于面向对象特性?", "options": ["封装", "继承", "多态", "递归"], "studentAnswer": "1,2,3", "correctAnswer": "1,2,3", "isCorrect": true, "score": 5, "analysis": "递归不是面向对象的特性" }

前端拿到这个结构直接渲染即可,不需要知道判断逻辑,也正因如此,后端评分策略如果变了,前端完全不用改。

5. 论文撰写结构与部署实操

很多人觉得把这个系统写出来已经够辛苦了,但论文在毕业设计中占比同样很高。论文不是代码的流水账,而是要突出“需求分析—系统设计—系统实现—系统测试”这条主线。我把论文框架和部署文档里必须有的内容逐一列清楚。

5.1 论文框架

推荐使用下面这个结构,这也是答辩老师最熟悉的套路:

  1. 绪论:课题背景与研究意义、国内外现状、论文组织结构。
  2. 相关技术介绍:SpringBoot 框架、Vue 框架、MySQL 数据库、JWT 认证、前后端分离架构。技术介绍不要长篇抄书,每项技术 300 字以内,重点说明它在本系统中承担什么作用。
  3. 系统需求分析:角色分析、功能需求分析(用文字描述功能点)、非功能需求分析(安全性、稳定性、响应时间)。
  4. 系统设计:总体架构图(前端、后端、数据库三层)、功能模块设计、数据库设计(ER 图 + 主要表结构描述)、接口设计。
  5. 系统实现:按模块分小节,每个模块写清楚实现思路、关键代码片段和界面截图。建议贴代码时只贴核心片段,不要整段 Service 类贴上去,每个代码片段都要配一段解释。
  6. 系统测试:功能测试用例表 + 测试结果 + 部分性能或者兼容性测试结果。
  7. 总结与展望:完成的工作、遇到的问题及解决、不足之处与后续改进方向。

论文里可以画结构图,但手写工具或者用 draw.io 画就好。答辩时需要解释清楚为什么选择前后端分离,为什么用 JWT 而不是 Session,数据库表为什么这么设计——这些问题在我前面讲的内容里都有答案。

5.2 部署文档写什么

这套项目的部署文档通常需要覆盖两种场景:本地开发环境和服务器生产环境。我自己写部署文档时,会按照下面这个清单来组织:

本地环境部署:

  1. JDK 版本说明(JDK 8 或 JDK 11 均可,不推荐 JDK 17+ 跑老版本 Spring Boot 2.x)。
  2. MySQL 版本要求(5.7 或 8.0),创建数据库 exam_system,设置 utf8mb4 编码。
  3. 后端配置修改 application.yml 里的数据库连接信息、Redis 地址、JWT 密钥。
  4. 启动方式:IDEA 启动 SpringBootApplication 主类,后端默认端口 8080。
  5. 前端配置修改 .env.development 里的 VUE_APP_BASE_URL 指向后端地址。
  6. 前端启动方式:npm install 安装依赖,npm run serve 启动开发服务器,默认端口 8081。

生产环境部署:

  1. 前端执行 npm run build,生成 dist 目录,使用 Nginx 托管 dist 静态文件。
  2. 后端执行 mvn clean package 打成 jar 包,使用 java -jar 启动或者通过 systemd 守护进程管理。
  3. Nginx 配置反向代理,/api 路径转发到后端地址,前端静态资源由 Nginx 直接提供服务。
  4. 数据库使用 MySQL 8.0 生产实例,执行项目附带的 exam_system.sql 初始化脚本。
  5. 服务器防火墙开放端口,域名如果配置了 HTTPS,证书路径要在 Nginx 里正确引用。

提示:部署文档的核心价值在于“照着做能跑通”,所以每一步都要写到具体的命令和文件路径级别,不要只写“配置好数据库”这种话。凡是你能想到初始化数据、测试账号、常见启动报错排查,都往里放。

5.3 常见部署报错的排查思路

这部分内容我建议也写进部署文档的附录,因为实操中一定会遇到。我把我自己踩过的和帮人排过的几个典型问题列出来了:

  • 前端 npm install 失败:多为 Node 版本与依赖版本不兼容。Vue 3 要求 Node 14.18+,npm 7+,Vue 2 用 Node 12 到 14 比较稳。解决方式是 nvm 切换 Node 版本后重装依赖。
  • Nginx 刷新页面 404:这是前端路由 history 模式导致的,需要在 Nginx 配置文件里加 try_files 指令:
    location / { root /usr/share/nginx/html; index index.html; try_files $uri $uri/ /index.html; }
  • MySQL 8 的驱动依赖引入:默认用 com.mysql.cj.jdbc.Driver,不要写成老版本驱动类名,否则会直接报 ClassNotFoundException 或时区异常。
  • 后端启动后端口被占用:在 application.yml 里改 server.port,或者启动时指定 --server.port=8081。
  • 跨域请求报 403 / REDACTED:后端配置跨域过滤器,同时前端 devServer 里配置 proxy 代理。生产环境用 Nginx 代理时,同源访问不存在跨域问题,所以开发环境代理和生产环境反向代理要区分。

6. 从实际项目复盘出的常见问题与解决建议

代码能跑通只是第一步,真正提升项目完成度的是对细节的把控。这一部分我重点整理常见问题,也把答辩可能会问到的点放进来。

6.1 功能层面的易错点

  • 考试成绩什么时候允许查看?如果教师还没批阅完主观题,学生成绩单不能出现成绩字段。所以成绩查询接口要判断 exam_record.status 是否为 3(已评阅),否则只返回“阅卷中”。
  • 试卷发布后还能改题目吗?我的建议是发布后题目变更要回到 draft 状态才允许改,或者干脆锁定期内不能改,否则会导致已经创建答卷的学生看到题目前后不一致。
  • 定时任务自动提交和手动提交不能互撞。手动提交时如果定时任务正好在跑,可能造成重复提交。因此在提交接口里要用前面说的 CAS 更新方式,同时状态字段加版本号或者乐观锁。
  • 教室考试和高并发:如果是全校同时考试,同一时刻几十个请求同时进来,除了数据库事务,还要考虑 Nginx 连接池、Tomcat 最大连接数、数据库 max_connections 这几个配置参数的调整。毕设一般不会压到瓶颈,但答辩老师如果问起来,至少能说清楚怎么调。

6.2 安全层面的易错点

  • 密码不能明文存。Spring Security 的 BCryptPasswordEncoder 或者 jBCrypt 都可以,禁止用 MD5 加盐这种已被证实不安全的方案。
  • 接口权限不能只防君子。学生调教师接口修改成绩这种,如果后端没有角色校验,就是严重安全漏洞。我建议在拦截器中用注解(如 @RequireRole(role = RoleEnum.TEACHER))来控制接口权限。
  • 考试答案不能从前端传回。评分的重要性前面讲了,但要再强调一次:前端传给后端的只能是自己选的答案,不能是“得分”。后端评分并校验答案结构、选项范围合法。

6.3 答辩追问预判

在线考试系统做得人很多,答辩老师问的问题也大多集中在那几个点上。我建议提前准备以下问题的回答:

  1. 为什么用 JWT 而不用 Session?答:前后端分离架构 + 横向扩展友好 + 服务端无状态。
  2. 如何防止学生考试时切换页面查答案?答:这在目前系统里属于非功能需求,可以通过检测页面可见性 API 和考试时间内的页面埋点日志来辅助提醒;但严格防作弊需要更复杂的锁屏和摄像头监控,不在本系统范围内。
  3. 多选评分规则为什么由后端控制?答:前端可以被篡改,评分必须以服务端计算结果为准。
  4. 考试中途断网怎么办?答:答题状态实时保存在服务端,断线重连后重新进入考试页面可以恢复历史答题数据。
  5. 系统能支撑多少并发?答:单机部署下 Tomcat 默认 200 线程池,MySQL 连接池合理配置,可支撑中小规模考试的并发需求;如需更高并发可以引入 Nginx 负载均衡和 Redis 缓存优化。

6.4 我个人的一点实操体会

做完这套在线考试系统,最大的感受是:这个题目“看起来简单,做起来碎”。碎片主要来自几个地方——状态流转特别多(考试有未发布、进行中、已结束,答卷有未开始、进行中、已提交、已评阅);权限边界要理清(三种角色三种菜单,接口要防住);前端答题页交互复杂(倒计时、答题卡、自动保存必须联动);还有论文与代码要互相对应。任何一个环节做得糙,项目就显得松散。

如果时间紧张,我建议实现优先级按这个顺序来卡:先把登录认证和题库管理跑通,然后是试卷创建和考试发布,再是学生答题和自动交卷,最后是成绩查询和统计导出。前面两个模块是地基,中间两个模块是核心功能,最后一个模块是锦上添花。只要这四个阶段的任务都完成了,系统的完整性已经足够支撑毕业设计答辩。

另外,源码里记得写好 README,把测试账号、数据库初始化方式、运行步骤、默认端口写清楚。部署文档和论文里的系统测试章节也是互通的,你把测试用例表整理好,可以直接挪进论文第六章,性价比很高。答辩时不要只会说“我调通了”,要能讲清楚每个设计背后的约束和取舍——这才是评委想听到的。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/15 23:05:15

WorkBuddy本地Agent工作流实战:8个高适配中文Skill深度指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/15 23:04:04

Cursor接入国产大模型低成本配置指南:替换API接口即省90%费用

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/15 23:03:49

信号继电器供应商筛选实战:从文件审查到样品实测的避坑指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/15 23:03:10

Apache Thrift 官方教程实战:从 .thrift IDL 到多语言客户端/服务器

Apache Thrift 官方教程实战&#xff1a;从 .thrift IDL 到多语言客户端/服务器 【免费下载链接】thrift Apache Thrift 项目地址: https://gitcode.com/GitHub_Trending/thr/thrift 本教程是 Apache Thrift 仓库中 tutorial/ 目录的完整实战指南。它以官方 tutorial/RE…

作者头像 李华
网站建设 2026/9/15 23:01:46

MATLAB混合建模实战:数字孪生中的机理与数据驱动融合方案

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华