每年到了毕业季,都会有一大批计算机专业的同学在选题和实现之间反复纠结。尤其是“在线考试系统”这类听起来不复杂、但真正动手写才发现细节繁多的题目:用户角色怎么分?试卷怎么生成?客观题自动判分怎么设计?前端和后端到底怎么“分离”?
如果你正在找 SpringBoot 方向的 Java 毕业设计项目,也正在被这些问题卡住,那这篇文章就是为你准备的。我会从系统架构、数据库设计、后端核心代码、前端 Vue 页面到启动排错,完整拆解一个基于 SpringBoot 的前后端分离在线考试系统。看完之后你会明白:这个项目真正的工作量在哪里,哪些点最容易出错,以及怎么在毕业答辩中把系统讲清楚。
全文以项目实践为主线,代码示例尽量保持完整可复制。文中涉及的版本和配置以通用实践为准,具体项目可能使用不同版本,但整体思路是一致的。
1. 在线考试系统到底解决什么问题
很多同学把这个题目理解为“做一套能考试的网页”,但毕业设计不是做产品 Demo,而是要体现你掌握了从需求分析、系统设计、数据库建模、后端开发到前端联调的一整套工程能力。
在线考试系统的核心业务并不复杂,但它天然包含多个角色、多种状态、多条业务链路,非常适合用作 Java 毕业设计:
- 学生端:注册登录、查看考试列表、参加考试、提交答卷、查看成绩和错题。
- 教师端:维护题库、组卷发布、设置考试时间、查看成绩统计、批改主观题。
- 管理员端:用户管理、班级管理、考试数据统计、系统基础配置。
真正考验开发水平的地方在于业务逻辑的交织:
- 一场考试只能从题库中抽取指定数量的题目。
- 学生在考试时间内只能提交一次答卷。
- 客观题要自动评分,主观题要支持人工评分或半自动评分。
- 考试状态要区分未开始、进行中、已结束。
- 学生提交后系统需要重新计算成绩并记录答题明细。
这些逻辑单看每一个都不难,但放在一起就需要合理的表结构设计和清晰的分层编码。这也是为什么“在线考试系统”这个选题虽然常见,却很有区分度的原因。
从技术栈角度看,SpringBoot + Vue 的前后端分离方案已经成为当前 Java 毕业设计的主流选择。后端负责业务逻辑、数据存储和安全认证,前端负责页面渲染和用户交互。两者通过 RESTful API 进行通信。
2. 系统架构与核心技术选型
2.1 前后端分离架构
“前后端分离”可以理解为:前端项目和后端项目是两个独立的工程,可以独立开发、独立部署、独立扩展。
在还没分离的传统开发模式中,前端页面和后端 Java 代码混在同一个 Web 项目中,页面由后端模板引擎渲染。问题在于前后端开发耦合度高,后端同学改页面样式,前端同学改接口逻辑,容易冲突。
前后端分离之后,前端只负责调接口、渲染数据,后端只负责处理业务、返回 JSON。这也是企业级项目的主流开发方式,写在毕业设计里能明显加分。
2.2 后端技术选型
后端核心框架选择 SpringBoot,理由非常现实:
- SpringBoot 简化了 Spring 的基础配置,通过自动配置减少大量 XML 配置。
- 内嵌 Tomcat,可以直接通过
java -jar或 IDE 启动,部署简单。 - 生态成熟,集成 MyBatis、Spring Security、JWT、Redis 等组件都非常方便。
数据访问层选择 MyBatis 或 MyBatis-Plus。MyBatis-Plus 在毕设项目中非常受欢迎,因为内置了单表 CRUD 方法,能减少很多重复代码。
2.3 前端技术选型
前端选择 Vue 2 或 Vue 3 搭配 Element UI / Element Plus。Vue 的核心特点是组件化开发和响应式数据绑定,Element UI 提供了表格、表单、弹窗等现成组件,能大幅缩短页面开发时间。
技术上 Vue 3 是方向,但目前很多毕设项目仍然使用 Vue 2。如果你是从零开始,建议优先 Vue 3 + Element Plus + Vite;如果你要参考已有的源码,就以源码为准。
2.4 数据库与认证方案
数据库使用 MySQL,这是 Java 方向使用最广泛的数据库,资料多、坑少、面试常问。
认证方案推荐 JWT(JSON Web Token)。用户登录成功后,后端生成一个 Token 返回给前端;前端在后续请求中把 Token 放到请求头里;后端拦截器校验 Token 合法性。
JWT 的好处是无状态、适合前后端分离、扩展性比 Session 好。但要注意 Token 的过期时间和密钥管理。
2.5 系统整体流程
前端 Vue 页面 | | RESTful API(JSON) v SpringBoot 后端 - Controller 层:接收请求,参数校验 - Service 层:处理业务逻辑、事务控制 - Mapper 层:数据库访问 | v MySQL 数据库这个分层结构是 SpringBoot 后端项目的经典组织方式,也是毕业设计评阅老师最想看到的层次。
3. 环境准备与项目初始化
3.1 开发环境清单
在开始写代码之前,先准备好以下环境:
| 工具 | 版本建议 | 说明 |
|---|---|---|
| JDK | 1.8 或 11 | Spring Boot 2.x 推荐 JDK 8,Spring Boot 3.x 需要 JDK 17 |
| Maven | 3.6 以上 | 后端依赖管理 |
| Node.js | 14 以上 | 前端构建运行环境 |
| npm | 6 以上 | 前端依赖管理 |
| MySQL | 5.7 或 8.0 | 数据存储 |
| IDEA | 2020 及以上版本 | 后端和前端开发全用 IDEA 也能胜任 |
具体版本以你实际项目为准。这里给你一个判断逻辑:如果你下载的项目是基于 Spring Boot 2.x,很大概率用 JDK 8;如果是 Spring Boot 3.x,那么 JDK 17 是必需的。
3.2 后端项目初始化
最简单的初始化方式是使用 Spring Initializr(start.spring.io)。选择 Maven 工程、Java 版本、Spring Boot 版本,然后添加依赖。
实际项目里常见的核心依赖如下:
<!-- 文件路径:pom.xml --> <dependencies> <!-- Web 启动器,内置 Tomcat --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <!-- MyBatis-Plus 的 Spring Boot 适配 --> <dependency> <groupId>com.baomidou</groupId> <artifactId>mybatis-plus-boot-starter</artifactId> <version>3.5.5</version> </dependency> <!-- MySQL 驱动 --> <dependency> <groupId>com.mysql</groupId> <artifactId>mysql-connector-java</artifactId> <scope>runtime</scope> </dependency> <!-- Lombok,简化实体类代码 --> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <optional>true</optional> </dependency> <!-- Java 后端参数校验 --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-validation</artifactId> </dependency> </dependencies>如果你使用的是新版 Spring Boot,MySQL 驱动的 artifactId 推荐写法可能是com.mysql:mysql-connector-j,不再推荐mysql:mysql-connector-java。这里以你的实际依赖情况为准。
3.3 前端项目初始化
Vue 项目的初始化命令按 Vue 版本区分。
Vue 2 项目使用 Vue CLI:
npm install -g @vue/cli vue create exam-frontVue 3 项目推荐使用 Vite:
npm create vite@latest exam-front -- --template vue初始化完成后安装 UI 组件库:
npm install element-plus axios前端项目初始化好之后,你就可以把一个可运行的空白前端项目先跑起来,再逐步填充页面。
4. 数据库设计与建表脚本
数据库设计决定了业务逻辑的复杂度上限。表设计不好,后面写 Service 层代码时会特别痛苦。
在线考试系统的核心表可以从“用户、题目、考试、答卷、答题明细”五个维度去设计。
4.1 用户表
用户表保存所有角色的账号信息,通过role字段区分管理员、教师、学生。
CREATE TABLE `sys_user` ( `id` bigint(20) NOT NULL AUTO_INCREMENT, `username` varchar(50) NOT NULL COMMENT '登录名', `password` varchar(100) NOT NULL COMMENT '加密后的密码', `real_name` varchar(50) DEFAULT NULL COMMENT '真实姓名', `role` varchar(20) DEFAULT 'student' COMMENT '角色:admin/teacher/student', `status` tinyint(1) DEFAULT 1 COMMENT '账号状态:1启用 0禁用', `create_time` datetime DEFAULT CURRENT_TIMESTAMP, `update_time` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (`id`), UNIQUE KEY `uk_username` (`username`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户表';注意:密码字段绝对不能存明文。实际项目一般使用 BCrypt 加密,具体加密方式以原型代码为准。
4.2 试题表
试题表存储单选题、多选题、判断题、主观题。为了降低设计复杂度,很多毕设系统会先实现单选题和判断题等客观题,再做主观题扩展。
CREATE TABLE `exam_question` ( `id` bigint(20) NOT NULL AUTO_INCREMENT, `question_type` tinyint(4) DEFAULT 1 COMMENT '题型:1单选 2多选 3判断 4主观', `question_content` text COMMENT '题干', `options_json` text COMMENT '选项JSON,例如["A","B","C","D"]', `answer` varchar(255) DEFAULT NULL COMMENT '标准答案', `score` int(11) DEFAULT 5 COMMENT '每题分值', `course_id` bigint(20) DEFAULT NULL COMMENT '所属课程', `create_time` datetime DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='试题表';字段设计有一个关键点:options_json。对于选择题的选项,用 JSON 字符串存储整个选项集合,而不是为每个选项建一张表。这样实现简单,前端解析也容易。
4.3 考试表
考试表存储一场考试的基本信息:名称、学科、时间、状态等。
CREATE TABLE `exam_info` ( `id` bigint(20) NOT NULL AUTO_INCREMENT, `exam_name` varchar(100) NOT NULL COMMENT '考试名称', `course_id` bigint(20) DEFAULT NULL COMMENT '课程ID', `exam_duration` int(11) DEFAULT 60 COMMENT '考试时长(分钟)', `start_time` datetime DEFAULT NULL COMMENT '开始时间', `end_time` datetime DEFAULT NULL COMMENT '结束时间', `total_score` int(11) DEFAULT 100 COMMENT '总分', `pass_score` int(11) DEFAULT 60 COMMENT '及格分', `status` tinyint(1) DEFAULT 0 COMMENT '状态:0未发布 1进行中 2已结束', `create_time` datetime DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='考试表';若要支持“从题库随机抽题”,需要增加一张关联表来维护考试与题目的关系。
CREATE TABLE `exam_paper` ( `id` bigint(20) NOT NULL AUTO_INCREMENT, `exam_id` bigint(20) NOT NULL COMMENT '考试ID', `question_id` bigint(20) NOT NULL COMMENT '题目ID', `question_order` int(11) DEFAULT 0 COMMENT '题目顺序', PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='试卷题目关联表';4.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', `total_score` int(11) DEFAULT 0 COMMENT '总得分', `status` tinyint(1) DEFAULT 0 COMMENT '状态:0未交卷 1已交卷 2已批改', `submit_time` datetime DEFAULT NULL COMMENT '交卷时间', PRIMARY KEY (`id`), KEY `idx_exam_id` (`exam_id`), KEY `idx_student_id` (`student_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='考试记录表';这里建议加索引,因为考试记录表的数据增长比较快,查询学生成绩时会频繁用到student_id和exam_id。
4.5 答题明细表
每个学生每道题的答题情况要单独记录,这是成绩统计和错题回顾的数据基础。
CREATE TABLE `exam_record_detail` ( `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` text COMMENT '学生答案', `correct_answer` text COMMENT '正确答案', `is_correct` tinyint(1) DEFAULT 0 COMMENT '是否正确', `score` int(11) DEFAULT 0 COMMENT '本题得分', PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='答题明细表';有了这些核心表,考试系统的业务闭环就可以跑通了。接下来的代码实现就是围绕这些表展开 CRUD 和业务校验。
5. 后端 SpringBoot 核心功能实现
5.1 后端项目分层结构
我推荐你按照下面的包结构组织代码,这也是企业级 SpringBoot 项目的通用做法:
src/main/java/com/example/exam/ ├── ExamApplication.java // 启动类 ├── common/ // 通用结果封装、异常处理、常量 │ ├── Result.java │ └── GlobalExceptionHandler.java ├── config/ // 配置类:CORS、拦截器、MyBatis-Plus ├── controller/ // 控制层 │ ├── AuthController.java │ ├── ExamController.java │ └── QuestionController.java ├── service/ // 业务层接口 │ └── impl/ // 业务层实现 ├── mapper/ // MyBatis-Plus Mapper 接口 ├── entity/ // 数据库实体类 └── dto/ // 前端传入参数对象5.2 统一返回结果封装
前后端分离项目中,后端接口返回给前端的结构应该保持统一。推荐使用一个Result类封装:
// 文件路径:src/main/java/com/example/exam/common/Result.java package com.example.exam.common; import lombok.Data; @Data public class Result<T> { private Integer code; private String message; private T data; public static <T> Result<T> success(T data) { Result<T> result = new Result<>(); result.setCode(200); result.setMessage("操作成功"); result.setData(data); return result; } public static <T> Result<T> error(String message) { Result<T> result = new Result<>(); result.setCode(500); result.setMessage(message); return result; } }这样做有几个好处:前端解析统一格式,不需要针对不同接口单独处理;后端可以在构造返回结果时统一设置 code、message、data 三段信息;后续接入 Swagger 接口文档也比较直观。
5.3 登录接口实现
登录接口是用户进入系统的第一个入口。核心逻辑是:接收用户名和密码,校验账号是否存在以及密码是否正确,然后生成 Token 返回给前端。
// 文件路径:src/main/java/com/example/exam/controller/AuthController.java @RestController @RequestMapping("/api/auth") public class AuthController { @Autowired private UserService userService; @PostMapping("/login") public Result<String> login(@RequestBody LoginDTO loginDTO) { User user = userService.findByUsername(loginDTO.getUsername()); if (user == null) { return Result.error("用户不存在"); } // 实际项目必须使用 BCrypt 匹配密码,这里简化演示 if (!user.getPassword().equals(loginDTO.getPassword())) { return Result.error("密码错误"); } if (user.getStatus() != 1) { return Result.error("账号已被禁用"); } // 生成 JWT Token String token = JwtUtil.createToken(user.getId(), user.getUsername(), user.getRole()); return Result.success(token); } }在做毕设项目时,登录成功后的 Token 请使用 JWT 工具类生成,不要在前端暴露用户密码。JwtUtil 可以自己实现加密和解析逻辑,也可以用现成的jjwt或hutool工具类。核心思路是:把用户 ID、用户名、角色等信息放入 Token,并设置过期时间。后端通过拦截器从请求头拿到 Token,解析出用户信息后放行。
5.4 教师端创建考试
创建考试是教师角色的核心操作。接口逻辑通常包含:
- 保存考试基本信息。
- 从试题表中按课程、题型、数量条件随机抽取题目。
- 将抽到的题目批量插入
exam_paper表。
这里最容易踩坑的是数据一致性。保存考试信息和插入试卷题目要么同时成功,要么同时失败,必须在 Service 方法上声明@Transactional事务注解。
// 文件路径:src/main/java/com/example/exam/service/impl/ExamServiceImpl.java @Override @Transactional(rollbackFor = Exception.class) public void createExamWithQuestions(CreateExamDTO dto) { // 1. 保存考试基本信息 ExamInfo exam = new ExamInfo(); exam.setExamName(dto.getExamName()); exam.setCourseId(dto.getCourseId()); exam.setExamDuration(dto.getExamDuration()); exam.setStartTime(dto.getStartTime()); exam.setEndTime(dto.getEndTime()); exam.setStatus(0); this.save(exam); // 2. 按需随机抽题,singleCount 为单选数量,judgeCount 为判断数量 List<Long> singleQuestionIds = questionMapper.selectRandomIds( dto.getCourseId(), 1, dto.getSingleCount()); List<Long> judgeQuestionIds = questionMapper.selectRandomIds( dto.getCourseId(), 3, dto.getJudgeCount()); List<ExamPaper> paperList = new ArrayList<>(); int order = 1; for (Long qid : singleQuestionIds) { ExamPaper item = new ExamPaper(); item.setExamId(exam.getId()); item.setQuestionId(qid); item.setQuestionOrder(order++); paperList.add(item); } for (Long qid : judgeQuestionIds) { ExamPaper item = new ExamPaper(); item.setExamId(exam.getId()); item.setQuestionId(qid); item.setQuestionOrder(order++); paperList.add(item); } // 3. 批量插入 paperMapper.insertBatch(paperList); }随机抽题的 SQL 可以这样写:
<!-- 文件路径:src/main/resources/mapper/QuestionMapper.xml --> <select id="selectRandomIds" resultType="java.lang.Long"> SELECT id FROM exam_question WHERE course_id = #{courseId} AND question_type = #{questionType} ORDER BY RAND() LIMIT #{limit} </select>在数据量不大时,ORDER BY RAND()是常见的随机抽取实现。在生产环境大数据量下性能会变差,但用于毕业设计是够用的。如果你在答辩时被问到,完全可以回答“这套方案的数据量瓶颈在哪里,企业级方案一般怎么做”,这会成为一个很好的加分点。
5.5 学生端提交试卷
学生端提交试卷后,后端要做的事情包括:
- 校验考试是否存在且已发布。
- 校验当前时间是否在开考和结束时间范围内。
- 校验该学生是否已经交过卷(防止重复提交)。
- 保存答题明细。
- 自动批改客观题,累加总分。
- 更新考试记录状态。
自动批改的核心逻辑是遍历答卷中的每道题目,将学生答案与标准答案进行比对。
// 文件路径:src/main/java/com/example/exam/service/impl/ExamRecordServiceImpl.java @Override @Transactional(rollbackFor = Exception.class) public void submitExam(SubmitExamDTO dto) { // 1. 查询考试信息 ExamInfo exam = examMapper.selectById(dto.getExamId()); if (exam == null || exam.getStatus() != 1) { throw new RuntimeException("考试不存在或未发布"); } // 2. 防止重复提交 QueryWrapper<ExamRecord> wrapper = new QueryWrapper<>(); wrapper.eq("exam_id", dto.getExamId()) .eq("student_id", dto.getStudentId()); Long count = examRecordMapper.selectCount(wrapper); if (count != null && count > 0) { throw new RuntimeException("请勿重复提交试卷"); } // 3. 创建考试记录 ExamRecord record = new ExamRecord(); record.setExamId(dto.getExamId()); record.setStudentId(dto.getStudentId()); record.setStatus(1); record.setSubmitTime(new Date()); int totalScore = 0; int currentOrder = 1; List<ExamRecordDetail> detailList = new ArrayList<>(); // 4. 遍历学生提交的回答列表 for (SubmitAnswer item : dto.getAnswers()) { Question question = questionMapper.selectById(item.getQuestionId()); if (question == null) { continue; } boolean correct = false; // 客观题直接比对答案 if (question.getQuestionType() == 1 || question.getQuestionType() == 3) { correct = question.getAnswer().trim().equalsIgnoreCase( item.getStudentAnswer().trim()); } ExamRecordDetail detail = new ExamRecordDetail(); detail.setQuestionId(question.getId()); detail.setStudentAnswer(item.getStudentAnswer()); detail.setCorrectAnswer(question.getAnswer()); detail.setCorrect(correct ? 1 : 0); if (correct) { detail.setScore(question.getScore()); totalScore += question.getScore(); } else { detail.setScore(0); } detailList.add(detail); currentOrder++; } record.setTotalScore(totalScore); examRecordMapper.insert(record); // 5. 批量插入答题明细,detailList 的 recordId 需要重新赋值 for (ExamRecordDetail detail : detailList) { detail.setRecordId(record.getId()); examRecordDetailMapper.insert(detail); } }这段代码结构清晰,每一段注释都对应一个业务校验点。在毕设答辩时,老师最喜欢问的问题就是“提交试卷时如何防止重复提交”,这段代码可以直接回答。
5.6 拦截器与跨域配置
前端项目和后端项目开发时使用不同的端口,会触发跨域问题。后端需要配置 CORS(跨域资源共享),才能让前端正常请求接口。
// 文件路径:src/main/java/com/example/exam/config/CorsConfig.java @Configuration public class CorsConfig implements WebMvcConfigurer { @Override public void addCorsMappings(CorsRegistry registry) { registry.addMapping("/api/**") .allowedOrigins("http://localhost:5173") .allowedMethods("GET", "POST", "PUT", "DELETE") .allowedHeaders("*") .allowCredentials(true) .maxAge(3600); } }实际项目中allowedOrigins的地址要与你前端项目启动后的地址保持一致。Vue 2 默认端口是 8080,Vite 项目默认端口是 5173,如果你使用的是其他端口需要自行调整。
6. 前端 Vue 页面与接口对接
6.1 前端项目目录结构
Vue 项目拿到手之后,推荐先看src目录下的结构:
src/ ├── api/ // 后端接口调用封装 ├── assets/ // 静态资源 ├── components/ // 公共组件 ├── router/ // 路由配置 ├── store/ // 全局状态管理(Vuex/Pinia) ├── views/ // 页面组件 │ ├── login/ │ ├── student/ │ ├── teacher/ │ └── admin/ ├── App.vue └── main.js6.2 封装 Axios 请求
前端调后端接口的核心是 Axios。项目里一般会对 Axios 做统一封装,把 baseURL、请求头、响应拦截器集中管理。
// 文件路径:src/api/request.js import axios from 'axios' import { ElMessage } from 'element-plus' const request = axios.create({ baseURL: '/api', timeout: 10000 }) // 请求拦截器:自动携带 Token request.interceptors.request.use(config => { const token = localStorage.getItem('token') if (token) { config.headers.Authorization = token } return config }) // 响应拦截器:统一处理业务错误 request.interceptors.response.use( response => { const res = response.data if (res.code !== 200) { ElMessage.error(res.message || '请求失败') return Promise.reject(new Error(res.message)) } return res }, error => { ElMessage.error(error.message || '网络异常') return Promise.reject(error) } ) export default request这里的关键点是把 Token 放在请求头里,让后端拦截器能够识别用户身份。登录接口返回的 Token 存入localStorage,页面刷新后仍能保持登录状态。
前端调用接口时,页面组件中只需要写业务代码:
// 文件路径:src/api/exam.js import request from './request' // 获取考试列表 export function getExamList(params) { return request({ url: '/exam/list', method: 'get', params }) } // 提交试卷 export function submitExam(data) { return request({ url: '/exam/submit', method: 'post', data }) }6.3 登录页面示例
登录页面是前端开发中最典型的一个例子。表单校验、提交接口、路由跳转、Token 存储都集中在这里。
<!-- 文件路径:src/views/login/Login.vue --> <template> <div class="login-container"> <el-card class="login-card"> <h2>在线考试系统</h2> <el-form ref="loginFormRef" :model="loginForm" :rules="rules"> <el-form-item prop="username"> <el-input v-model="loginForm.username" placeholder="请输入用户名" /> </el-form-item> <el-form-item prop="password"> <el-input v-model="loginForm.password" type="password" placeholder="请输入密码" show-password /> </el-form-item> <el-form-item> <el-button type="primary" style="width: 100%" @click="handleLogin"> 登录 </el-button> </el-form-item> </el-form> </el-card> </div> </template> <script setup> import { ref } from 'vue' import { useRouter } from 'vue-router' import { ElMessage } from 'element-plus' import { login } from '@/api/auth' const router = useRouter() const loginFormRef = ref() const loginForm = ref({ username: 'admin', password: '123456' }) const rules = { username: [{ required: true, message: '请输入用户名', trigger: 'blur' }], password: [{ required: true, message: '请输入密码', trigger: 'blur' }] } async function handleLogin() { await loginFormRef.value.validate() const res = await login(loginForm.value) localStorage.setItem('token', res.data) router.push('/dashboard') } </script>这段代码使用的是 Vue 3 的组合式 API 写法。如果你使用的是 Vue 2,对应的写法是data、methods选项式 API。判断依据仍然是你实际项目的 Vue 版本。
6.4 考试列表与参加考试
学生登录后,在考试列表页面能看到所有已发布且当前时间在考试时间范围内的考试。点击“参加考试”按钮后跳转到答题页面,答题页面加载该考试的试卷题目。
前端在处理答题页面时,有一个关键体验点需要重点处理:倒计时。开考后显示剩余时间,时间到自动提交。这里通常会设置一个定时器,每秒更新剩余分钟数。
// 考试倒计时逻辑伪代码 const remainSeconds = ref(examDuration * 60) const timer = setInterval(() => { remainSeconds.value-- if (remainSeconds.value <= 0) { clearInterval(timer) handleSubmit() } }, 1000)在真实项目中,前端倒计时只是表面控制,真正的考试时间校验必须在后端做。前端绕过倒计时直接调接口,后端也需要拒绝。这个思路在答辩时同样可以展开讲。
7. 项目启动与效果验证
7.1 后端启动
使用 IDEA 打开后端项目,等待 Maven 下载完成依赖后,在项目根目录执行 Maven 命令启动:
mvn spring-boot:run或者直接运行主类ExamApplication.java的main方法。
启动成功后,控制台会看到类似输出:
Started ExamApplication in 4.3 seconds (JVM running for 4.9) Tomcat started on port(s): 8080 (http)此时说明后端服务已经启动成功。
7.2 前端启动
使用 IDEA 或 VS Code 打开前端项目,在项目根目录执行:
npm install npm run dev启动成功后,控制台会输出访问地址。如果端口是 5173,直接访问http://localhost:5173。
7.3 验证流程
建议在写论文和使用演示时,按以下流程验证一遍系统:
- 使用管理员账号登录,创建班级和用户。
- 创建教师账号,并给教师邮箱增加课程和试题。
- 使用教师账号登录,创建一张包含单选题和判断题的考试,设置好考试时间。
- 使用学生账号登录,查看考试列表,进入考试页面答题。
- 提交试卷,查看成绩和答题明细。
- 回到教师端查看成绩统计。
这一套流程走通,说明系统核心业务已经闭环,也证明代码没有大的逻辑问题。
在演示之前,建议你准备一个小型题库,至少包含 10 道单选题和 10 道判断题,这样抽题时效果更明显。
8. 常见报错与排查方法
在线考试系统在开发过程中最容易遇到以下几类问题。
| 问题现象 | 可能原因 | 排查方式 | 解决方案 |
|---|---|---|---|
| 后端启动失败,端口被占用 | 8080 端口已被其他进程占用 | 执行netstat -ano查看端口占用 | 修改application.yml的server.port |
| 前端请求接口报跨域错误 | 后端 CORS 配置未生效 | 查看浏览器 Console 和 Network | 在后端配置 CorsConfig,或使用前端代理 |
| 数据库中文乱码 | MySQL 连接 URL 未指定编码 | 查看数据库库表字符集 | URL 后添加characterEncoding=utf8 |
| MyBatis-Plus 查询结果为空 | 表名或字段名跟实体类不对应 | 打印 SQL 日志,检查表结构 | 添加@TableName或@TableId注解 |
| 登录接口报 401 | JWT Token 无效或过期 | 查看拦截器日志,检查 Token 解析 | 重新登录获取新 Token |
| 提交试卷时一直提示重复提交 | exam_record表中已有记录 | 检查数据库记录和查询条件 | 删除测试记录,或用前端创建考试后再测试 |
如果放大到整个 Java 开发环节,最常见的两个坑是 JDK 和 Maven 环境变量配置问题。第一次运行项目时如果报java: 找不到符号或 Maven 依赖无法下载,优先检查 JDK 版本是否匹配、Maven 镜像源是否配置。你可以在 Maven 安装根目录的conf/settings.xml中配置阿里云镜像,加快依赖下载速度。
8.1 版本兼容问题
SpringBoot 版本高低会直接影响依赖的写法。搜索材料中也反复出现“springboot版本太高”“springboot版本太高 springboot框架介绍”这类问题。
具体来说:
- SpringBoot 2.x 使用 JDK 8,MySQL 驱动 artifactId 是
mysql-connector-java。 - SpringBoot 3.x 使用 JDK 17,MySQL 驱动 artifactId 是
com.mysql:mysql-connector-j,且javax包名变为jakarta。 - Vue 2 使用 Vue CLI 启动,Vue 3 使用 Vite 启动。
如果你的代码在 SpringBoot 2 中能跑,但换到 SpringBoot 3 启动失败,优先检查依赖的 groupId、artifactId 和 javax/jakarta 包名。
9. 毕业设计项目最佳实践
9.1 代码层面的建议
第一,包结构一定要清晰。Controller 层不要出现 SQL 业务逻辑,Service 层不要把大段重复代码直接复制。包结构干净的项目,即使功能一般,答辩老师的第一印象也会好很多。
第二,数据库表要有注释。建表语句中每个字段都要写COMMENT,这样在数据库设计中能直接生成带说明的文档。写毕业设计说明书时,直接引用表格结构就能省很多时间。
第三,统一接口返回格式。不要把成功时返回对象、失败时返回字符串,这种前后端联调会很痛苦。
第四,核心业务必须加事务。创建考试、提交试卷这两个操作涉及多表写入,务必使用@Transactional注解。
9.2 答辩时的讲解重点
在线考试系统答辩时,老师大概率会问以下几类问题,提前准备会更有底气:
- 为什么选 SpringBoot 而不是 SSM 或 Servlet 手写?
- 前后端分离和单体项目的区别?
- 考试时间控制是怎么实现的?如果学生修改前端时间戳能不能绕过?
- Redis 缓存为什么不是必须的?如果需要应对大规模在线考试,哪些环节需要优化?
- JWT 和 Session 的区别是什么?Token 过期了怎么处理?
建议准备一张类似下面的表格,放在毕业设计说明书中,能很直观地表达你的设计思路:
| 角色 | 主要功能 | 核心表 |
|---|---|---|
| 学生 | 注册、查看考试、答题、查看成绩 | exam_record、exam_record_detail |
| 教师 | 题库管理、组卷、发布考试、批阅主观题 | exam_question、exam_paper、exam_info |
| 管理员 | 用户管理、数据统计、系统配置 | sys_user、exam_info |
9.3 生产环境部署建议
毕业设计一般只要求在本地跑通,但很多同学会在答辩前尝试部署到阿里云服务器。如果你有这一步,注意三点:
一是 MySQL 必须修改默认 root 密码,不要使用弱密码。生产环境和本地环境的安全基线不同。
二是前端打包后放在 Nginx 中,后端用java -jar启动,前后端通过 Nginx 反向代理/api地址。部署前最好准备一份简明的部署文档,答辩时老师问到部署你说不出来会很吃亏。
三是部署前要在测试环境完整跑一遍流程,尤其注意数据库初始化脚本和前端请求接口地址。搜索材料里也反复出现“阿里云部署前后端分离项目”的搜索词,说明这是毕业设计的常见方向,值得提前练习。
9.4 善用现成资源但不要失去独立能力
现在网上的毕业设计资源很丰富,也许你手头已经有一份源码了。拿到源码后不要只改个项目名称就交差。建议你花时间做三件事:
一是把数据库表结构全部理清,画出 ER 图。 二是把登录和提交试卷这两条核心链路写清楚,最好能不看源码自己讲出来。 三是尝试改一个功能,比如增加一种新题型、调整自动评分规则。
做到这三件事,答辩时你的表现会比大多数照搬项目的同学好很多。
10. 总结
基于 SpringBoot 的在线考试系统这个选题,技术上覆盖了前后端分离、RESTful 接口设计、MySQL 数据建模、JWT 认证、MyBatis-Plus 数据访问、Vue 页面开发、跨域处理、事务管理等多方面的知识点。从毕业设计角度看,它的业务复杂度适中,不至于做不完,也不会简单到没有内容写。
这篇文章从环境准备、数据库设计、后端核心实现、前端接口联调、启动验证到常见排错和答辩建议,基本覆盖了一个完整毕设项目的落地路径。如果你正在做这个项目,建议先把核心表结构和提交试卷这条业务链路吃透,再逐步完善题库、用户管理和图表统计等功能。代码跑通只是第一步,能在答辩时讲清楚设计思路和关键细节,才是真正有价值的产出。