资讯动态

Spring Boot+Vue+MySQL在线考试系统完整可运行实现与避坑指南

发布时间:2026/10/6 18:56:20 来源:尧图企业网站定制
简介这是一份面向高校计算机及相关专业毕业设计场景的在线考试系统完整项目覆盖用户注册登录、考试创建与发布、题库维护、自动判分、成绩统计等核心流程适合需要完成课程设计、毕业设计或希望系统学习Web开发的学生参考。压缩包共67个文件整体大小约3.31MB文件类型以ASP动态页面、DOC设计文档、JPG/GIF界面预览图、HTM页面、MDB数据库、CSS样式及TXT说明为主其中ASP文件实现后端业务逻辑MDB数据库存储用户、试题与成绩信息DOC文档可辅助理解系统架构与模块划分。系统从前端交互、后端服务到数据库操作均有完整实现且源码可直接运行便于在此基础上进行功能扩展或代码研究。目前已有172人学习浏览内容紧凑、目录结构清晰适合作为全栈开发入门实践以及毕业答辩材料准备的有力参考。1. 在线考试系统不是增删改查完整可运行的项目才是毕业设计该有的样子做毕业设计最怕的一句话不是“你这里写错了”而是“你这个系统能跑吗”。很多同学交上去的在线考试系统截图里页面齐全演示时却连试卷都加载不出来。根本原因不是代码量不够而是设计与实现压根没闭环后端接口没有和前端页面连通数据库建表只覆盖了用户和题目漏掉了考试记录、答题明细这些关键实体。“在线考试系统的设计与实现完整源码可运行”这个标题讲的就是把考试业务从前端答题页到后端判分逻辑完整跑通的一整套方案。它面向两类人需要交毕业设计的学生以及想用一套可运行代码做课程设计或期末项目的人。核心难点不在写代码而在三件事——数据模型覆盖完整考试流程、自动组卷和自动判分逻辑严谨、系统能在单机上稳定跑起来给老师演示。本文围绕一个最常见的完整技术组合展开Spring Boot Vue MySQL这也是相关热词里“基于springboot vue”的毕业设计最集中的技术栈选型。从表结构设计到判分逻辑再到部署演示的坑一次讲透。2. 技术选型为什么是 Spring Boot Vue MySQL从“能跑”到“好演示”的现实考量2.1 技术栈定的不是代码是答辩时的“求生欲”在线考试系统这个题目可以用的技术组合非常多Java Web JSP、Python Flask、PHP ThinkPHP甚至纯 Servlet 都能做完。但在“毕业设计”这个具体场景里选型的第一原则不是技术最先进而是“老师看了不皱眉、自己改得动代码、演示时不翻车”。我见过不少选 JSP 的老项目功能完整但页面老旧答辩老师第一印象就打了折扣。反过来一味追求微服务架构、Redis 缓存、消息队列在单机演示环境里反而容易暴露配置错误。Spring Boot Vue 是目前中等偏上本科毕业设计的常见选择Spring Boot 处理后端接口、Vue 管理前端页面、MySQL 存数据三者分工清晰横跨前后端工作量足够技术栈也新。2.2 前后端分离的工程结构项目一打开就知道你会不会干活一个能拿到较高评分的在线考试系统工程目录应该是前后端分离的这也直接对应答辩时的“项目结构介绍”环节。我一般建议的目录划分如下exam-system/ ├── backend/ # Spring Boot 后端 │ ├── src/main/java/ │ │ ├── controller/ # 接口层登录、试卷、考试、判分 │ │ ├── service/ # 业务层组卷逻辑、自动判分 │ │ ├── mapper/ # MyBatis 数据访问层 │ │ ├── entity/ # 实体类 │ │ └── config/ # 跨域等配置 │ └── src/main/resources/ │ ├── mapper/ # MyBatis XML 文件 │ └── application.yml # 数据库、端口等配置 └── frontend/ # Vue 前端 ├── src/ │ ├── views/ # 页面登录、考试列表、答题页、成绩页 │ ├── router/ # 路由配置 │ └── api/ # axios 接口封装 └── package.json这里最容易被忽略的是config包下的跨域配置。前端开发服务器运行在 8080后端跑在 8081两者端口不同浏览器默认拦截跨域请求。不配这一层前端页面永远调不通后端接口。常见做法是在后端加一个配置类Configuration public class CorsConfig implements WebMvcConfigurer { Override public void addCorsMappings(Registry registry) { registry.addMapping(/**) .allowedOriginPatterns(*) .allowedMethods(GET, POST, PUT, DELETE) .allowCredentials(true) .maxAge(3600); } }这段代码的意思很直接允许所有来源跨域访问、允许四种 HTTP 方法、携带 Cookie 的跨域请求也放行。开发环境里我一般不开allowedOriginPatterns(*)而是精确写前端地址http://localhost:8080避免安全问题——但这个坑放到后面单独说。2.3 数据库选 MySQL 的理由很简单老师电脑上一定有MySQL 不是性能最优的数据库但在毕业设计场景里它是容错率最高的安装包到处都是、Navicat 连上就能看表、老师想验证数据也方便。相比 PostgreSQL 或 SQL ServerMySQL 的教程资料最全踩坑的答案一搜就有。需要说明的是这套系统对数据库版本不敏感MySQL 5.7 或 8.0 都能跑只要注意 8.0 的驱动类名和密码加密方式变化即可。具体会在避坑章节展开。3. 数据库设计两张表撑不起在线考试系统最少要六张3.1 从考试流程反推数据模型很多初学者设计在线考试系统的数据库第一反应是“用户表 试题表”就完了。这是用管理系统的思维做考试系统漏掉了考试业务最关键的两个环节——考试实例和答题记录。一个完整的在线考试流程是这样的管理员创建试卷、录入题目 → 学生选择试卷开始考试 → 系统记录学生每道题的答案 → 交卷时系统判分 → 学生查看成绩和答案。这里面涉及三类核心数据用户学生和教师、题库与试卷、考试记录与答题明细。我一般这样设计六张核心表表名用途关键字段user学生、教师、管理员账号id, username, password, rolequestion试题库选择题、判断题id, type, content, options, answer, scoreexam考试场次id, title, start_time, end_time, duration, total_scoreexam_question试卷与题目的关联表id, exam_id, question_id, scoreexam_record考试记录id, exam_id, user_id, start_time, submit_time, score, statusanswer_detail答题明细id, record_id, question_id, user_answer, is_correct这六张表缺一不可。最容易被忽略的是exam_question表——它承担“组卷”的职责。如果只在question表里加一个exam_id字段那就意味着每道题只能属于一张试卷题库和试卷就直接绑死了系统根本谈不上“在线考试系统”的完整性。3.2 建表 SQL直接用这套结构省两小时返工以下是精简过的建表脚本保留核心字段你可以在此基础上扩展CREATE TABLE user ( id int(11) NOT NULL AUTO_INCREMENT, username varchar(50) NOT NULL COMMENT 登录名, password varchar(100) NOT NULL COMMENT 加密后的密码, real_name varchar(50) DEFAULT NULL COMMENT 姓名, role tinyint(4) NOT NULL DEFAULT 0 COMMENT 0学生 1教师 2管理员, PRIMARY KEY (id), UNIQUE KEY uk_username (username) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; CREATE TABLE question ( id int(11) NOT NULL AUTO_INCREMENT, type tinyint(4) NOT NULL COMMENT 1单选 2判断, content varchar(500) NOT NULL COMMENT 题干, options varchar(500) DEFAULT NULL COMMENT 选项JSON数组, answer varchar(10) NOT NULL COMMENT 正确答案, score int(11) NOT NULL DEFAULT 5 COMMENT 分值, PRIMARY KEY (id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; CREATE TABLE exam ( id int(11) NOT NULL AUTO_INCREMENT, title varchar(100) NOT NULL COMMENT 考试名称, start_time datetime DEFAULT NULL, end_time datetime DEFAULT NULL, duration int(11) NOT NULL DEFAULT 60 COMMENT 考试时长分钟, total_score int(11) NOT NULL DEFAULT 100, PRIMARY KEY (id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; CREATE TABLE exam_question ( id int(11) NOT NULL AUTO_INCREMENT, exam_id int(11) NOT NULL, question_id int(11) NOT NULL, score int(11) NOT NULL DEFAULT 5, PRIMARY KEY (id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; CREATE TABLE exam_record ( id int(11) NOT NULL AUTO_INCREMENT, exam_id int(11) NOT NULL, user_id int(11) NOT NULL, start_time datetime DEFAULT NULL, submit_time datetime DEFAULT NULL, score int(11) DEFAULT 0, status tinyint(4) NOT NULL DEFAULT 0 COMMENT 0考试中 1已交卷, PRIMARY KEY (id), KEY idx_user_exam (user_id, exam_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; CREATE TABLE answer_detail ( id int(11) NOT NULL AUTO_INCREMENT, record_id int(11) NOT NULL, question_id int(11) NOT NULL, user_answer varchar(10) DEFAULT NULL, is_correct tinyint(4) DEFAULT 0, PRIMARY KEY (id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;这个脚本有几个值得注意的细节。user表用了反引号包起来因为user是 MySQL 的保留字不包会直接报语法错误。options字段用 JSON 字符串存储选择题选项避免了单独建选项表的冗余。所有表名采用单数形式exam_record故意加了id主键而没有用(exam_id, user_id)联合主键——因为用户可能考同一张试卷多次比如补考场景。answer_detail表是整个系统判分的核心依据。它记录的是“谁在哪次考试中、对哪道题、答了什么、对了没有”而不是只存一个总分。有了这个表才能实现成绩查看、错题回顾、管理员查看每道题的正确率。3.3 试卷和考试要分开建模这是“设计与实现”的隐藏得分点exam表和exam_question表的拆分是数据设计上区分“高分设计”和“凑合实现”的分水岭。exam_question是一个多对多关联表一份试卷包含多道题一道题可以被多份试卷引用。这样设计的好处至少有三层第一层是组卷教师从题库中勾选题目创建试卷不需要复制题目内容。第二层是试卷复用同一个题库可以生成多套试卷期末 A/B 卷只需要建两个exam记录引用不同的exam_question组合。第三层是扩展将来要支持随机组卷只需要在exam_question里加一个question_count或category_id字段在组卷逻辑里按条件查询即可不需要改表结构。这个拆分逻辑在论文的数据库设计章节写清楚排版都能省不少功夫。答辩时问“试卷和题库为什么分开建表”这个问题答好了基本上数据层面就稳了。4. 核心模块实现自动组卷、自动判分与考试流程闭环4.1 自动组卷接口把“选题”变成“选条件”在线考试系统和传统考试系统最大的区别在于组卷是否能自动化。一套完整的系统教师应该可以通过指定题型数量、知识点占比来自动生成试卷。最简陋但可用的实现方式是“按条件随机抽取题目插入 exam_question 表”。后端 Service 层的组卷逻辑用 MyBatis 查询题号列表再逐个插入关联表public void generateExamQuestions(Integer examId, Integer singleCount, Integer judgeCount) { // 从题库随机抽取指定数量的单选题题号 ListInteger singleIds questionMapper.selectRandomIds(1, singleCount); // 从题库随机抽取指定数量的判断题题号 ListInteger judgeIds questionMapper.selectRandomIds(2, judgeCount); ListInteger allIds new ArrayList(); allIds.addAll(singleIds); allIds.addAll(judgeIds); for (Integer questionId : allIds) { Question question questionMapper.selectById(questionId); ExamQuestion eq new ExamQuestion(); eq.setExamId(examId); eq.setQuestionId(questionId); eq.setScore(question.getScore()); examQuestionMapper.insert(eq); } }这里有几个关键点需要解释。selectRandomIds在 XML 里对应这样的查询select idselectRandomIds resultTypejava.lang.Integer SELECT id FROM question WHERE type #{type} ORDER BY RAND() LIMIT #{count} /selectORDER BY RAND()在题目量小时性能尚可但对于毕业设计完全够用不需要优化成复杂的分区随机算法。LIMIT #{count}里的 count 不是查询参数而是截断数量所以即使题库里该题型不足数量SQL 也不会报错只会返回更少的结果——这是一个需要业务层显式校验的边界点。第二个边界是分值处理。我在组卷时读取的是question表里题型默认的分值而不是让教师在页面里逐个设置这样可以减少前端传参复杂度。如果要求每张试卷的题目分值和题库默认值可区分那exam_question表里单独存score字段的设计就派上用场了。第三这个组卷方式有一个天生的缺陷随机抽取可能重复。如果题库里只有 5 道判断题而教师要求生成 8 道SQL 只会返回 5 条业务层应该捕获这个不足量并向前端提示“题库题目数量不足”。我见过真实的项目这里直接报空指针就是因为没做数量校验。添加校验其实很简单在方法开头先查总数Integer totalCount questionMapper.countByTypeAndCategory(type, categoryId); if (totalCount requireCount) { throw new ServiceException(题库题目不足单选题缺口 (requireCount - totalCount)); }4.2 考试进行中的核心状态机开始考试和交卷两个动作在线考试需要精确控制“谁在什么时候考哪场试”这里有一个常见的状态流转未开始 → 考试中 → 已交卷。后端只需要维护两个动作学生点击“开始考试”时创建exam_record点击“交卷”时更新记录状态并触发判分。开始考试的后端逻辑是这样的public ExamRecord startExam(Integer examId, Integer userId) { // 查询考试信息 Exam exam examMapper.selectById(examId); // 校验当前时间是否在考试时间段内 Date now new Date(); if (now.before(exam.getStartTime())) { throw new ServiceException(考试尚未开始); } if (now.after(exam.getEndTime())) { throw new ServiceException(考试已结束); } // 检查是否已有进行中的考试记录 ExamRecord existing examRecordMapper.selectActiveRecord(userId, examId); if (existing ! null) { return existing; } // 创建新记录 ExamRecord record new ExamRecord(); record.setExamId(examId); record.setUserId(userId); record.setStartTime(now); record.setStatus(0); examRecordMapper.insert(record); return record; }这里有两个思考点。第一时间段校验放在 Service 层而不是前端是为了防止学生直接改本地时间跳过限制——当然这只防普通用户防不了专业攻击但对毕业设计已经足够。第二selectActiveRecord查询条件是user_id exam_id status0防止学生刷新页面时重复创建考试记录。这两个细节在答辩时被问到的概率极高。值得注意的是start_time和duration的约束关系没有在这里强校验。比如考试时长 60 分钟学生 10:00 开始那么 11:00 必须强制交卷。这个逻辑的实现可以放到交卷接口里统一处理也可以写一个定时任务扫描超时未交卷的记录。我一般会在交卷接口里做双保险校验后面展开。4.3 自动判分逻辑别用 if-else 堆先设计判分抽象自动判分是在线考试系统的技术亮点也是最容易写出烂代码的地方。题型只有单选和判断两种时很多人直接用两个 if 分支但一个真正完整的在线考试系统判分逻辑应该抽象成“判断对错 计算得分”两个步骤。我的做法是定义一个判分接口各题型实现自己的逻辑public interface AnswerChecker { boolean check(String correctAnswer, String userAnswer); } Component public class SingleChoiceChecker implements AnswerChecker { Override public boolean check(String correctAnswer, String userAnswer) { return correctAnswer.equalsIgnoreCase(userAnswer); } } Component public class JudgeChecker implements AnswerChecker { Override public boolean check(String correctAnswer, String userAnswer) { // 判断题答案统一存储为 正确/错误容错处理为空值 if (userAnswer null || userAnswer.isEmpty()) { return false; } return correctAnswer.equals(userAnswer.trim()); } }加上一个工厂类根据题型返回对应的判定器Component public class CheckerFactory { private final MapInteger, AnswerChecker checkerMap; public CheckerFactory(ListAnswerChecker checkers) { checkerMap new HashMap(); for (AnswerChecker checker : checkers) { if (checker instanceof SingleChoiceChecker) { checkerMap.put(1, checker); } else if (checker instanceof JudgeChecker) { checkerMap.put(2, checker); } } } public AnswerChecker getChecker(Integer questionType) { AnswerChecker checker checkerMap.get(questionType); if (checker null) { throw new ServiceException(不支持的题型 questionType); } return checker; } }判分主流程在交卷时执行遍历answer_detail表中的答题记录逐题判定并累加总分Transactional public Integer submitExam(Integer recordId) { ExamRecord record examRecordMapper.selectById(recordId); // 幂等校验已经交过卷的不允许重复交卷 if (record.getStatus() 1) { throw new ServiceException(试卷已交卷请勿重复提交); } ListAnswerDetail details answerDetailMapper.selectByRecordId(recordId); int totalScore 0; for (AnswerDetail detail : details) { Question question questionMapper.selectById(detail.getQuestionId()); AnswerChecker checker checkerFactory.getChecker(question.getType()); boolean correct checker.check(question.getAnswer(), detail.getUserAnswer()); detail.setIsCorrect(correct ? 1 : 0); if (correct) { totalScore question.getScore(); } answerDetailMapper.updateCorrectStatus(detail); } record.setScore(totalScore); record.setSubmitTime(new Date()); record.setStatus(1); examRecordMapper.update(record); return totalScore; }这段代码里有一个容易踩坑的点在循环里逐条update答案状态性能不优雅但毕业设计的数据量下完全可行。另一个更重要的点是Transactional注解——交卷必须保证“判分 更新记录”的原子性否则判到一半报错成绩和答案状态就错乱了。判分逻辑设计成策略模式的好处在后续扩展题型时体现得最明显。要加多选题只需要新建一个MultiChoiceChecker实现AnswerChecker并把类型映射加到工厂里原有代码一行不用改。论文里写“系统基于策略模式设计了可扩展的判分框架”这个含金量面试官和答辩老师都认。4.4 考试时间控制后台校验 前端倒计时双保险考试系统的“时间”是业务正确性的命脉。前端倒计时只是体验层后台必须强制校验。我见过的最严重翻车案例是前端页面清除倒计时就能无限续考本质是后端完全没有时间校验。交卷接口里应该加这样的校验逻辑public Integer submitExam(Integer recordId) { ExamRecord record examRecordMapper.selectById(recordId); Exam exam examMapper.selectById(record.getExamId()); Date now new Date(); long startMillis record.getStartTime().getTime(); long nowMillis now.getTime(); long durationMillis exam.getDuration() * 60 * 1000L; // 超时强制交卷 if (nowMillis - startMillis durationMillis) { throw new ServiceException(考试时间已到系统自动交卷); } // ... 判分逻辑 }前端在 Vue 里的倒计时我一般用setInterval实现每秒递减剩余秒数到 0 时直接调用交卷接口startTimer() { const totalSeconds this.exam.duration * 60 this.remaining totalSeconds this.timer setInterval(() { this.remaining-- if (this.remaining 0) { clearInterval(this.timer) this.handleSubmit() // 自动交卷 } }, 1000) }这里有两个细节要注意第一前端倒计时的基准应该是record.startTime加上duration而不是从页面加载时重新计时——否则学生刷新页面就能重置倒计时。第二setInterval在组件销毁时一定要清理否则切走页面再回来会出现多个计时器叠加剩余时间加速倒数。这两个都是血泪经验换来的。5. 完整可运行的部署链路从源码 RAR 到老师电脑上能演示5.1 解压源码的第一步RAR 里的项目结构比代码先“通关”标题里写的是“.rar”压缩包这里要先处理一个问题——毕业设计经常遇到 RAR 压缩包解压异常尤其是从百度网盘或 QQ 邮箱下载的场景。首先需要明确标准 RAR 文件用 WinRAR 或 7-Zip 都可以正常解压。如果你遇到压缩包损坏一个常见做法是先用 7-Zip 尝试打开它能自动跳过损坏的头部信息读取文件其次才是用 WinRAR 的“修复压缩文件”功能。这些都是常规操作但每年都有同学卡在这一步。更值得关注的是解压后的目录结构。完整的毕业设计源码包不应该只有一个后端代码而应该包含sql目录数据库脚本、backend目录后端工程、frontend目录前端工程和一份README.md。如果解压后发现只有代码没有 SQL 脚本那就说明这个“完整”源码是不完整的——数据库表结构都没有怎么跑都是空指针。5.2 后端启动三步走改配置、导数据、跑 Maven后端工程拿到手第一步不是急着mvn spring-boot:run而是检查application.yml里的数据库配置server: port: 8081 spring: datasource: url: jdbc:mysql://localhost:3306/exam_system?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/Shanghai username: root password: 123456 driver-class-name: com.mysql.cj.jdbc.Driver mybatis: mapper-locations: classpath:mapper/*.xml configuration: map-underscore-to-camel-case: true这里最容易出错的有三个点。第一是serverTimezoneMySQL 8.0 连接 URL 必须带时区参数否则报The server time zone value异常直接指定Asia/Shanghai是最省事的。第二是driver-class-nameMySQL 5.7 用com.mysql.jdbc.Driver8.0 用com.mysql.cj.jdbc.Driver版本不对连启动都过不去。第三是map-underscore-to-camel-case: true没有这个配置数据库里的user_name映射不到 Java 里的userName查询结果全部为 null。数据库导入用命令或工具都行命令更直观mysql -u root -p exam_system exam_system.sql执行后进入 MySQL 验证数据mysql -u root -p USE exam_system; SHOW TABLES; SELECT COUNT(*) FROM question;如果question表和其他表都存在且数据量大于 0数据库这关就算过了。这里顺便提一个我常提醒的问题导入 SQL 前确认建表语句里没有DROP DATABASE或DROP TABLE IF EXISTS。有些网上下载的 SQL 脚本开头直接删库一执行就把你电脑上同名的其他库给洗掉了。这个“后悔药”是没有的。5.3 前端跑通的关键一条命令npm install 的三种翻车现场Vue 前端项目的启动流程相对简单但翻车率极高cd frontend npm install npm run devnpm install最常见的问题是网络慢导致的安装失败。我一般会建议先检查 Node.js 版本Vue 2 项目建议 Node 14 或 16Vue 3 项目建议 Node 16 及以上。版本不对装依赖时会报node-sass相关的错——这个错几乎成了前端项目最大的“玄学”问题。node-sass安装失败的常见解决方法是换用sass或dart-sass或者设置镜像源后重新安装npm config set registry https://registry.npmmirror.com npm install如果项目用的是npm-shrinkwrap.json锁定版本则要注意package-lock.json和它的版本冲突问题最稳妥的是删掉node_modules和package-lock.json两个文件后重新安装。5.4 前后端联调的硬核验证方法不只看页面要看数据启动后端8081 端口和前端8080 端口后浏览器打开http://localhost:8080用数据库中已有的测试账号登录。如果页面能展示试卷列表并进入考试说明基本链路通了。但“能打开页面”和“完整可运行”是两回事。我一般会按下面这个清单逐项验证少一项都有可能在答辩时出丑用管理员账号创建一份新试卷添加几道题目。用学生账号参加考试正常答题后提交查看成绩是否正确。再次登录学生账号确认不能重复参加同一场考试。修改系统时间或用超时后提交看系统是否正确拒绝。查看数据库exam_record表确认交卷记录和分数都写入成功。第五步最容易被忽略但它是证明系统“完整”的关键证据。答辩时打开 Navicat当场查询answer_detail表展示每一道题的答题明细比任何 PPT 截图都有说服力。老师问“怎么验证判分正确性”直接把这条 SQL 拍出来就完事。6. 在线考试系统避坑指南四个高频翻车点和排查命令6.1 数据库连不上报错信息三连位置各不相同现象后端启动时控制台报警Cannot create PoolableConnectionException或Access denied for user。原因排查分三个方向。第一是数据库服务没启动Windows 下直接去服务管理确认 MySQL 服务状态第二是密码和application.yml里不一致注意区分 root 密码是安装时自定义的还是默认的空密码第三是数据库名错误MySQL 里根本没有exam_system这个库。解决方式# 检查 MySQL 服务状态 systemctl status mysql # 或 Windows 下services.msc 查找 MySQL 服务 # 测试数据库连接 mysql -u root -p # 查看已有数据库 SHOW DATABASES;如果是密码问题最省事的做法是在application.yml里改成实际密码而不是去重置 MySQL 密码。如果 URL 里的库名写错了用CREATE DATABASE exam_system DEFAULT CHARACTER SET utf8mb4;重建再重新导入 SQL。6.2 中文乱码一个配置项解决 90% 的问题现象页面上显示中文正常但数据库里存的是???或者在浏览器和数据库之间传输后显示乱码。原因数据库连接 URL 少了编码参数或者表本身不是 utf8mb4 字符集。解决URL 里加characterEncodingutf8并且建库指定utf8mb4。还有一个跨端设置的坑前端页面里请求 JSON 数据axios 默认按 UTF-8 解析只要后端接口返回的Content-Type带了charsetUTF-8一般不会出问题。spring: datasource: url: jdbc:mysql://localhost:3306/exam_system?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/Shanghai如果还乱码就用 Navicat 打开对应的表右键选择“表属性”检查字符集。个别老库是latin1需要转成utf8mb4ALTER TABLE question CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;这条 SQL 会转换整张表的字符集执行前确认没有重要数据。6.3 前端调不通接口F12 控制台的报错是唯一真相现象页面正常打开但登录点击后无反应或报Network Error、Request failed with status code 404。原因分两类Network Error一般是后端没启动或跨域没配404 则可能是接口路径不对。排查方式很简单按 F12 打开开发者工具切到 Network 标签页点击登录按钮看请求发出去的 URL 和状态码。如果请求根本没有发出去检查 axios 封装的baseURLimport axios from axios const request axios.create({ baseURL: http://localhost:8081/api, timeout: 10000 }) export default requestbaseURL里的端口号和application.yml里的server.port必须一致。一个真实常见的翻车现场是后端配了8081前端baseURL写了8080结果请求全部打到前端开发服务器上自然全是 404。这个错误用 F12 一眼就能看出来但经常被当成后端 Bug 查半天属于典型的“低级但高发”。跨域问题则会直接看浏览器控制台的 CORS 报错。如果后端已经加了跨域配置但仍然报错检查是不是用了CrossOrigin注解和全局配置重复了。6.4 表单提交后 500MyBatis 映射不到的深层原因现象新增题目或创建试卷时点击提交后端报 500控制台提示nested exception is org.apache.ibatis.binding.BindingException。原因通常是实体类的createTime之类字段在数据库表里不存在或者 MyBatis 的resultType映射到了不存在的列。解决方式是检查 XML 里的 SQL 语句和实体类字段一一对应insert idinsertQuestion parameterTypecom.example.entity.Question INSERT INTO question (type, content, options, answer, score) VALUES (#{type}, #{content}, #{options}, #{answer}, #{score}) /insert注意这里故意没有插入id字段因为它是自增主键。如果表里还有create_time字段而实体类没有对应属性插入 SQL 里也别写这个字段两边保持一致就不会报错。7. 从“能跑”到“好演示”成绩分析和试卷导出的加分项技巧一套在线考试系统做到能跑只是及格线想拿优秀成绩关键在于两个“加分”能力成绩统计分析和试卷导出。这两个功能对代码量要求不大但演示时效果极好。成绩分析我一般做一个简单的统计页用 SQL 聚合即可SELECT exam_id, COUNT(*) AS student_count, AVG(score) AS avg_score, MAX(score) AS max_score, MIN(score) AS min_score, SUM(CASE WHEN score 60 THEN 1 ELSE 0 END) / COUNT(*) AS pass_rate FROM exam_record WHERE status 1 GROUP BY exam_id;这条 SQL 一次查出参考人数、平均分、最高最低分和及格率后端接口直接返回给前端渲染成表格或 ECharts 图表。整个功能从开发到联调大概一小时但答辩时你向老师展示“系统具备教学统计能力”比单纯演示考试流程的层次要高出一截。试卷导出功能则是实用性最强的。我推荐用 Apache POI 生成 Excel 导出考试成绩关键代码很短public void exportExamScore(Integer examId, HttpServletResponse response) { ListExamRecord records examRecordMapper.selectByExamId(examId); try (XSSFWorkbook workbook new XSSFWorkbook()) { Sheet sheet workbook.createSheet(考试成绩); Row header sheet.createRow(0); header.createCell(0).setCellValue(学生姓名); header.createCell(1).setCellValue(开始时间); header.createCell(2).setCellValue(交卷时间); header.createCell(3).setCellValue(得分); for (int i 0; i records.size(); i) { Row row sheet.createRow(i 1); row.createCell(0).setCellValue(records.get(i).getUserName()); row.createCell(1).setCellValue(records.get(i).getStartTime().toString()); row.createCell(2).setCellValue(records.get(i).getSubmitTime().toString()); row.createCell(3).setCellValue(records.get(i).getScore()); } response.setContentType(application/vnd.openxmlformats-officedocument.spreadsheetml.sheet); response.setHeader(Content-Disposition, attachment; filenameexam_score.xlsx); workbook.write(response.getOutputStream()); } catch (IOException e) { throw new ServiceException(导出失败); } }这段代码用到了 try-with-resourcesworkbook会自动关闭不需要手动 flush。response.setHeader里的文件名没有做 URL 编码如果文件名是中文浏览器可能显示乱码——一个经验做法是文件名用英文或者用URLEncoder.encode处理。最后说一个我自己的习惯。每次给项目写 README我都坚持包含三个内容启动步骤数据库导入 → 后端 → 前端、默认账号密码、以及“常见启动失败原因”一章。这件事在答辩前帮过我太多次因为到答辩现场你根本来不及查代码。记不住的地方打开 README 就能看到比临时翻文档强多了。做一个完整可运行的在线考试系统代码只是三分之一让老师三分钟内在他的电脑上跑起来才是“设计与实现”最有力的交付证据。希望这些经验能帮到你少走弯路。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑