SpringBoot+uni-app仿驾考宝典毕设:前后端分离刷题系统项目拆解

发布时间:2026/10/30 17:21:21
SpringBoot+uni-app仿驾考宝典毕设:前后端分离刷题系统项目拆解

简介:一套基于SpringBoot与uni-app开发的仿驾考宝典答题软件,是完整的毕业设计项目源码,面向计算机相关专业学生,可用于课程设计、大作业或毕业设计参考。该项目为个人毕设作品,评审分达九十五分,代码经调试可运行。资源包共两千个文件,体积约123MB,包含Java后端代码、Vue前端组件、JavaScript脚本、样式表、配置文件、Markdown说明文档,另有大量图片素材、数据库SQL脚本及项目说明,便于快速搭建环境。后端采用SpringBoot、MyBatis、MySQL、Redis、JWT、POI、Thymeleaf等主流框架,前端使用uni-app与colorui组件库,适配H5和App;后台管理账号为admin,短信验证码可在套餐过期时输入“999999”完成验证,腾讯云短信接口可在TxSend处配置。整体功能涵盖后台管理、题库管理、在线答题等核心模块,已有八百余人学习下载,适合需要掌握前后端分离开发、跨端适配和认证鉴权流程的读者,基础较好者可继续扩展统计、社区等新功能。

1. 仿驾考宝典的毕设源码:SpringBoot+uni-app 这套项目到底拆开看什么

很多人拿到这份基于SpringBoot+uni-app开发的仿驾考宝典答题软件源码时,习惯先找一张程序运行截图看看效果,但我更建议你先想清楚一件事:这套代码最值钱的不是那几套驾考题库,而是把“刷题”这个场景完整地做成了一个业务闭环。后端有用户、题库、答题记录、错题统计,前端用 uni-app 包住了 App、H5、小程序三类形态,这才是毕设项目真正需要的东西。它能解决的不只是科目一科目四刷题,把题库一换,它就是考研政治刷题、安全员考证刷题的底子。这篇笔记面向两类人:准备拿它做毕设的同学,以及想从零跑通一个前后端分离项目的老手。后面所有命令我都按实际调试习惯写,你照着做就行。

2. 先把项目跑起来:数据库脚本导入与 SpringBoot 启动参数核对

拿到源码包的第一步不是急着打开 IDEA,而是先确认启动顺序。这套项目的结构是典型的前后端分离:后端依赖数据库,前端依赖后端接口。所以首次启动必须走“先导数据库,再起后端,最后跑前端”这条链路。顺序错了,你会看到后端连不上数据库、前端调不通接口,到处都是报错。下面我会把每一步的关键参数和容易忽略的细节都标出来。

2.1 先拆压缩包:后端、前端、数据库脚本各归其位

把压缩包解压后,先别急着点进去,按工程形态把内容分成三类。后端通常是一个 Maven 工程,根目录放pom.xml,里面按controller/service/mapper/entity分层;前端是 uni-app 工程,特征是有pages.json、manifest.json、App.vue这几个文件;数据库脚本一般单独放在sql或db目录,也可能塞在后端的resources目录下,叫schema.sql或data.sql。项目说明通常是 Word 或 PDF,和源码放在一起。

如果解压后没有找到单独的 SQL 文件,去后端src/main/resources里找,很多毕设项目会把建表语句放在这个位置。看清楚这些之后,再确定 MySQL 版本和 JDK 版本。这套代码在 MySQL 5.7 和 8.0 下都能跑,但 8.0 的驱动和连接参数写法有点区别,后面会专门说到。前端的 uni-app 部分,建议直接用 HBuilderX 打开,这样能省去命令行脚手架的环境问题。

2.2 导入数据库:建库、设置字符集、一次导完所有表

数据库脚本导入是第一个分水岭,这里翻车的人最多。我一般不会用 Navicat 的“运行 SQL 文件”直接一把梭,而是先用命令行把库建好,再导数据,这样报错能看得更清楚。先登录 MySQL,执行下面的建库和导入命令:

CREATE DATABASE IF NOT EXISTS driving_exam DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci; USE driving_exam; SET NAMES utf8mb4; SOURCE /your/path/to/driving_exam.sql;

CREATE DATABASE IF NOT EXISTS确保重复执行不报错,这个在毕设答辩现场很有用;DEFAULT CHARACTER SET utf8mb4不是随便写的,驾考题目里经常出现生僻字和特殊符号,老式的utf8字符集会报 1366 错,换成utf8mb4才能存得住。COLLATE utf8mb4_general_ci是大小写不敏感的排序规则,和后面的 Java 字符串比较逻辑要区分开,数据库排序规则只影响ORDER BY和唯一键判断,不影响你代码里判卷。SET NAMES utf8mb4是让当前客户端连接的字符集和数据库一致,导完以后可以执行下面这条命令验证:

mysql -u root -p -e "USE driving_exam; SHOW TABLES;"

如果你看到十几张表,说明脚本导入完整。常见的报错有两类:一是 1044 权限错误,说明当前 MySQL 用户没有建库权限,换 root 或用授权后的账号;二是 1064 语法错误,多半是 SQL 文件里带了低版本 MySQL 的写法。这里提示一句:需要修改表结构时,不要直接改原 SQL 再重新导入,那会把现有答题记录清空。正确做法是用ALTER TABLE增量改,比如给题目表加难度字段:

ALTER TABLE t_question ADD COLUMN difficulty TINYINT DEFAULT 1 COMMENT '难度1-3';

数据库增删改查只是基本功,真正决定项目质量的是表结构设计,这个下面专门讲。

2.3 配置后端启动参数:JDK、Maven、数据源先对齐

后端启动前,先对着pom.xml看一眼 SpringBoot 版本,这决定了你本机要装哪个 JDK。这是一年能遇到十几次的血泪经验:SpringBoot 2.7.x 用 JDK 8 或 11 最顺畅,SpringBoot 3.x 必须用 JDK 17 或更高。很多人电脑上装了 JDK 21,直接跑 2.7 的项目,启动就报UnsupportedClassVersionError,这就是 springboot 版本太高和本机 JDK 不匹配造成的。先看pom.xml里的 parent 片段:

<parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.7.18</version> <relativePath/> </parent>

确认版本后,再去 IDEA 里调整 Project Structure,把 Project SDK 切到对应版本。接着打开application.yml,核对数据源、端口和时区这三组参数:

server: port: 8080 spring: datasource: url: jdbc:mysql://localhost:3306/driving_exam?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=Asia/Shanghai username: root password: 123456 driver-class-name: com.mysql.cj.jdbc.Driver mybatis-plus: mapper-locations: classpath*:/mapper/**/*.xml type-aliases-package: com.example.entity

server.port=8080是后端服务端口,前端所有接口请求都往这里打;characterEncoding=utf8配合 MySQL 的utf8mb4才能保证中文不乱码;useSSL=false是本地开发必须加的,否则 MySQL 8.0 会默认尝试加密连接导致告警;serverTimezone=Asia/Shanghai解决日期差 8 小时的问题。如果你用新版 IDEA,Run Configuration 里默认会读application.yml的端口,但如果你在 VM options 里手动写了-Dserver.port=8081,那它会覆盖配置文件,排错时先看这里。

Maven 构建我用的是 maven 命令,IDEA 里双击clean再package也一样:

mvn clean package -DskipTests java -jar target/your-artifactId.jar

-DskipTests是跳过单元测试,不是不编译测试代码;如果用-Dmaven.test.skip=true连测试代码编译都跳过,速度更快,但面试问起来你要能说清楚区别。启动成功后日志里会出现Tomcat started on port(s): 8080,这时候后端才算真正立住了。

2.4 用 HBuilderX 打开前端,确认入口文件再连接口

前端是 uni-app 工程,标准做法是用 HBuilderX 导入。打开后先认识三个入口文件:main.js负责创建应用实例,App.vue是全局生命周期,pages.json负责页面注册。uni-app 的入口文件和普通 Vue 项目不一样,main.js里不能直接createApp(App),而是要导出一个createApp函数,这是 uni-app 为跨端做的特殊处理:

import App from './App' import { createSSRApp } from 'vue' export function createApp() { const app = createSSRApp(App) return { app } }

这段代码用createSSRApp是 uni-app 的固定写法,改成createApp的话,微信小程序端可能白屏。接着看pages.json,新增页面必须在这里注册才能被编译出来:

{ "pages": [ { "path": "pages/index/index", "style": { "navigationBarTitleText": "题库列表" } }, { "path": "pages/exam/exam", "style": { "navigationBarTitleText": "模拟考试" } }, { "path": "pages/wrong/wrong", "style": { "navigationBarTitleText": "错题本" } } ] }

pages数组里的第一个元素就是启动页,毕设项目通常会把题库列表作为首页。前端这边先不要急着点题目,先用浏览器直接访问http://localhost:8080/api/xxx,能返回 JSON 再继续。前后端分离开发时 H5 端跨域几乎必然发生,后面第三章我会给出后端 CORS 配置,这样前端就不用额外做跨域转发了。

3. 后端答题模块这样拆:题库、答题记录与自动判卷接口

后端代码如果只做增删改查,答辩老师问两句就露馅。这套项目真正值得讲清楚的是四个能力:题库怎么存、答题记录怎么写、答案怎么判、错题怎么算。把它们串在一起,才是完整的答题业务闭环。本章我会按表设计、选型、判卷逻辑、跨域处理和统一返回体四个部分来拆,每一块都能直接对照你的源码改。

3.1 先定表关系再写接口:题库、答题记录和错题本怎么建表

业务建模是后端的第一步。驾考刷题的核心表有四种:用户表、题目表、答题记录表、错题表。我见过很多毕设项目把所有东西塞进一张大表,后面统计错题率时 SQL 写得极其痛苦。合理的设计是拆开,让每张表只干一件事:

CREATE TABLE t_user ( id BIGINT AUTO_INCREMENT PRIMARY KEY, username VARCHAR(50) NOT NULL UNIQUE, password VARCHAR(100) NOT NULL, nickname VARCHAR(50), create_time DATETIME DEFAULT CURRENT_TIMESTAMP ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; CREATE TABLE t_question ( id BIGINT AUTO_INCREMENT PRIMARY KEY, category VARCHAR(20) NOT NULL COMMENT '科目一、科目四或章节编号', content TEXT NOT NULL, options_json TEXT NOT NULL COMMENT '选项 JSON,如 [{"key":"A","label":"..."}]', answer VARCHAR(10) NOT NULL COMMENT '单选 A/B/C/D,多选 ABD,判断题 T/F', analysis TEXT COMMENT '题目解析', status TINYINT DEFAULT 1, version INT DEFAULT 1 ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; CREATE TABLE t_answer_record ( id BIGINT AUTO_INCREMENT PRIMARY KEY, user_id BIGINT NOT NULL, question_id BIGINT NOT NULL, user_answer VARCHAR(10) NOT NULL, is_correct TINYINT DEFAULT 0, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, INDEX idx_user_question (user_id, question_id) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; CREATE TABLE t_wrong_book ( id BIGINT AUTO_INCREMENT PRIMARY KEY, user_id BIGINT NOT NULL, question_id BIGINT NOT NULL, wrong_count INT DEFAULT 1, last_wrong_time DATETIME DEFAULT CURRENT_TIMESTAMP, UNIQUE KEY uk_user_question (user_id, question_id) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

有几个字段设计值得说明。options_json用 TEXT 存 JSON,而不是单独建一张选项表,这降低了查询复杂度,题目展示时前端一次拿到全部选项;answer用VARCHAR(10)是为了兼容单选和多选,多选答案用逗号分隔,比如A,B,D;t_answer_record里建了复合索引idx_user_question,因为按用户查答题记录是最频繁的查询;t_wrong_book的UNIQUE KEY保证同一道题在错题本里只有一条记录,错误次数靠wrong_count累加。数据库层我没有建外键约束,这不是偷懒,而是毕设项目在开发期经常要删题、改题,外键会让这类操作被卡住,一致性交给后端 Service 层控制。

3.2 持久层选型:MyBatis-Plus 和 Spring Data JPA 怎么选

这套源码的持久层实现,不同版本的项目可能不一样。如果你拿到的版本用的是 MyBatis-Plus,那是国内毕设最常见的选型;如果是 Spring Data JPA,也不用慌张,接口路径是一样的。两者的区别主要在这张表里:

对比维度MyBatis-PlusSpring Data JPA
单表 CRUD 开发速度很快,内置 BaseMapper很快,内置 JpaRepository
复杂 SQL 控制力强,可写 XML 或注解较弱,复杂查询要拼 Specification
上手门槛中,需要理解 Mapper 扫描低,简单继承接口就能用
答辩被追问难度容易被追问 SQL,你必须会写容易被追问 Hibernate 原理

如果你打算拿这套项目去答辩,我建议无论源码里是什么持久层,都先弄懂 MyBatis-Plus 的常见配置,因为面试官问“分页怎么做、条件查询怎么写”的概率很高。SpringBoot 整合 MyBatis-Plus 非常成熟,最常见的问题就是 Mapper 扫描没配好。在启动类上加@MapperScan通常就解决了:

@SpringBootApplication @MapperScan("com.example.mapper") public class Application { public static void main(String[] args) { SpringApplication.run(Application.class, args); } }

@MapperScan后面的包路径必须和你的mapper包实际路径一致,写错的话启动就报Consider defining a bean of type 'xxxMapper'。还有一种做法是给每个 Mapper 接口加@Mapper注解,两者选其一即可,不要叠加,否则可能出现代理冲突。配置完pom.xml里的依赖后,先跑一次mvn compile,看 Mapper 接口能不能被正确扫描到,再来写业务接口。

3.3 判卷接口:单选、多选和判断题的边界处理

自动判卷是整个系统的核心亮点,也是面试官最爱问的地方。很多人实现判卷时直接correctAnswer.equals(userAnswer),遇到多选题顺序不同就翻车。我一般会先看答案里有没有逗号,有逗号就是多选,转成 Set 再比较,忽略顺序;没有逗号就是单选或判断,用忽略大小写的方式比较:

@Transactional public AnswerResultVO submitAnswer(Long userId, SubmitAnswerDTO dto) { Question question = questionMapper.selectById(dto.getQuestionId()); String correctAnswer = question.getAnswer().trim(); String userAnswer = dto.getUserAnswer().trim(); boolean correct; if (correctAnswer.contains(",")) { correct = answerSet(correctAnswer).equals(answerSet(userAnswer)); } else { correct = correctAnswer.equalsIgnoreCase(userAnswer); } AnswerRecord record = new AnswerRecord(); record.setUserId(userId); record.setQuestionId(question.getId()); record.setUserAnswer(userAnswer); record.setIsCorrect(correct ? 1 : 0); answerRecordMapper.insert(record); if (!correct) { WrongBook wrong = wrongBookMapper.selectByUserIdAndQuestionId(userId, question.getId()); if (wrong == null) { wrong = new WrongBook(); wrong.setUserId(userId); wrong.setQuestionId(question.getId()); wrong.setWrongCount(1); wrongBookMapper.insert(wrong); } else { wrong.setWrongCount(wrong.getWrongCount() + 1); wrong.setLastWrongTime(new Date()); wrongBookMapper.updateById(wrong); } } else { wrongBookMapper.deleteByUserIdAndQuestionId(userId, question.getId()); } AnswerResultVO vo = new AnswerResultVO(); vo.setCorrect(correct); vo.setCorrectAnswer(correctAnswer); vo.setAnalysis(question.getAnalysis()); return vo; } private Set<String> answerSet(String answer) { return Arrays.stream(answer.split(",")) .map(String::trim) .collect(Collectors.toSet()); }

这里有两个容易被忽略的点。一是@Transactional,因为提交答案要同时写答题记录表和错题本,事务保证这两步要么都成功,要么都回滚,避免出现答题记录写了但错题没更新的情况;二是trim(),用户从界面上传来的答案可能带空格,不处理会导致明明选对了却判错。判断题的答案在表里统一存T或F,不要存“对”“错”这种中文,不然后端判卷还要做一层映射。数据库里的answer字段设计成逗号分隔,就是为了让answerSet这个方法能同时处理多选和单选。

3.4 跨域配置、统一返回体和异常处理,别让接口调不动

前端 H5 跑在 HBuilderX 的随机端口上,后端跑在 8080,跨域是逃不掉的。有些后端同学的思路是给前端开开发服务器转发,但对于 uni-app 这种多端项目,最省事的做法是在后端直接配跨域过滤器。注意一个细节:allowCredentials(true)时不能再用allowedOrigins("*"),否则浏览器会直接拒绝请求,必须用allowedOriginPatterns("*"):

@Configuration public class CorsConfig implements WebMvcConfigurer { @Override public void addCorsMappings(CorsRegistry registry) { registry.addMapping("/api/**") .allowedOriginPatterns("*") .allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS") .allowedHeaders("*") .allowCredentials(true) .maxAge(3600); } }

addMapping("/api/**")表示只对/api路径下的接口生效,避免把非业务接口暴露出去;allowedMethods里必须加上OPTIONS,因为浏览器跨域请求前会先发一个预检请求,后端不认OPTIONS就会报 403;maxAge(3600)是一小时内不用重复预检,能减少很多无效请求。配合统一返回体,前后端联调时错误信息一目了然:

public class R<T> { private Integer code; private String msg; private T data; public static <T> R<T> ok(T data) { R<T> r = new R<>(); r.setCode(200); r.setMsg("success"); r.setData(data); return r; } public static <T> R<T> fail(Integer code, String msg) { R<T> r = new R<>(); r.setCode(code); r.setMsg(msg); return r; } }

前端统一判断code是否为 200,而不是依赖 HTTP 状态码。为什么?因为跨域预检失败时 HTTP 状态码可能是 200,但浏览器不让你读取响应;统一返回体和 CORS 配置配合,能够把后端真实错误透传给前端。异常处理建议加一个@RestControllerAdvice,把未捕获的异常转成R.fail(500, e.getMessage()),不然前端拿到一堆堆栈信息,体验很差。

4. uni-app 刷题前端:跨端复用、答题状态与本地缓存设计

后端接口齐了,前端的核心任务是把刷题体验做好。uni-app 的价值在于写一套 Vue 代码,编译到 App、H5、微信小程序三个平台。但“一套代码到处跑”并不是说三个平台没有任何差异,而是把差异尽量收敛到 API 层。我在这章里只挑四个最关键的点讲,都是实际开发中一定会碰到的。

4.1 请求封装与 uni-app 前端登录实现

很多毕设项目的前端代码是直接在页面里到处写uni.request,维护起来很痛苦。我一般会在utils/request.js里封装一个请求函数,统一处理baseURL、token和code判断。uni-app 前端用户登录实现,本质就是封装好这个请求后,调后端的/api/login接口:

const BASE_URL = 'http://localhost:8080' export function request(path, method = 'GET', data = {}) { return new Promise((resolve, reject) => { uni.request({ url: `${BASE_URL}/api${path}`, method: method, data: data, header: { 'Content-Type': 'application/json', 'token': uni.getStorageSync('token') || '' }, success: (res) => { if (res.data.code === 200) { resolve(res.data.data) } else { uni.showToast({ title: res.data.msg, icon: 'none' }) reject(new Error(res.data.msg)) } }, fail: (err) => reject(err) }) }) }

BASE_URL是第一个坑。H5 端可以写localhost,但微信小程序里localhost指向的是手机自己,不是你电脑的后端,所以小程序开发时必须改成电脑的局域网 IP。这个参数要单独抽出来,根据编译平台切换。header里的token从本地缓存读,后端过滤器拿到这个值校验登录状态,如果返回 401,前端要跳回登录页。登录接口的响应数据里如果有用户昵称、头像,也要存到storage,方便后面的个人中心直接读取。

4.2 答题页用 vue3 ref 管理状态,选项点击与下一题联动

这套项目的前端如果是 Vue2 版本,改成 Vue3 的 uni-app 项目时会用到组合式 API。答题页的几个状态非常适合用ref来管理:当前题目索引、当前题用户选的答案、所有题的答案数组。用ref而不是普通对象的原因,是模板渲染依赖响应式更新,改普通对象不会触发页面重绘,这在 uni-app vue3 里是常见的白屏元凶:

<script setup> import { ref, computed } from 'vue' import { onLoad } from '@dcloudio/uni-app' import { request } from '@/utils/request' const questions = ref([]) const currentIndex = ref(0) const selectedAnswer = ref('') const answers = ref([]) const currentQuestion = computed(() => questions.value[currentIndex.value]) const progress = computed(() => { if (!questions.value.length) return 0 return Math.round(((currentIndex.value + 1) / questions.value.length) * 100) }) onLoad(() => { loadQuestions() }) async function loadQuestions() { questions.value = await request('/exam/questions', 'GET') } function choose(option) { if (selectedAnswer.value) return selectedAnswer.value = option answers.value[currentIndex.value] = option } function next() { if (currentIndex.value < questions.value.length - 1) { currentIndex.value++ selectedAnswer.value = answers.value[currentIndex.value] || '' } else { submitExam() } } async function submitExam() { const result = await request('/exam/submit', 'POST', { answers: answers.value }) uni.showModal({ title: '得分', content: `${result.score} 分` }) } </script>

这里的currentQuestion用computed计算,而不是在模板里写v-if="index === currentIndex"加v-for,原因很简单:计算属性只会在currentIndex变化时重新求值,性能更好,模板也更干净。choose函数里加if (selectedAnswer.value) return,是防止用户在一道题里反复点选项改了答案,要做到选了就锁定,下一题才解锁。answers数组用来保存所有题的已选答案,这样用户回退到上一题时,能通过answers.value[currentIndex.value]恢复选项高亮,不会丢状态。

4.3 本地缓存与错题收集:用 storage 做后悔药

刷题场景有一个特点:用户可能在答到一半时切走应用,或者小程序被系统回收。这时候如果不做本地缓存,用户的答题进度全丢,体验很差。uni-app 提供了uni.setStorageSync和uni.getStorageSync这对同步 API,数据量不大时直接同步读写就行,不用异步回调:

function saveAnswer(questionId, answer) { const answers = uni.getStorageSync('exam_answers') || {} answers[questionId] = answer uni.setStorageSync('exam_answers', answers) } function collectWrong(questionId, answer) { const wrongList = uni.getStorageSync('wrongList') || [] const exist = wrongList.find(item => item.questionId === questionId) if (!exist) { wrongList.push({ questionId, answer, count: 1 }) uni.setStorageSync('wrongList', wrongList) } }

把错题存到本地而不是每次都请求后端,一是减少接口压力,二是用户离线也能看错题。但这也带来一个问题:本地缓存和远端题库的一致性。如果后端改了题目标题或答案,用户手机里存的仍然可能是旧数据。我一般会在后端加一个题库版本号字段,前端启动时拉一次版本号,发现和本地不一致就清掉旧的答题缓存和错题缓存,重新拉题库。这个“后悔药”机制比直接清缓存更优雅,既保留用户答题进度,又不会让旧题目一直卡在本地。

4.4 从 H5 到微信小程序:一次编译多端运行

在 HBuilderX 里写完整套代码后,运行菜单里可以选“运行到浏览器”“运行到手机模拟器”“运行到微信开发者工具”。同样的代码,编译目标不同,产物也不同。微信小程序端的差异主要体现在三个方面:一是没有window和document对象,任何操作DOM的第三方库都不能直接用;二是不能用localStorage,必须用uni.getStorageSync;三是网络请求要求域名必须配置到微信公众平台的合法域名列表里,开发环境可以在微信开发者工具右上角“详情”里勾选“不校验合法域名”来跳过。

做微信小程序开发 uni-app 项目时,建议从一开始就把request封装好,不要在页面里直接写uni.request,不然每个页面都要处理code判断和环境差异。另外,如果你要打包安卓 App,HBuilderX 里默认是云打包,如果源项目依赖了原生插件,需要准备本地离线打包的 Android SDK,工作量会大不少。毕设场景一般用云打包就够了,不用在本地搭那套环境。

5. 常见问题排查与 4 个避坑记录:环境、接口、缓存与数据一致性

这套项目我帮人排查过不少次,真正的问题集中在五块:Mapper 扫描不到、H5 跨域失败、多选题判卷错误、题库缓存不刷新、JDK 与 SpringBoot 版本冲突。每一条我都按“现象、原因、解决”写清楚,遇到对应情况直接照着处理。

5.1 后端启动报 Mapper 扫描不到,接口 500

现象:SpringBoot 启动时报Consider defining a bean of type 'com.xxx.QuestionMapper' in your configuration.,但QuestionMapper接口确实存在。原因分两种:一是在启动类上没加@MapperScan,二是加了但包路径写得不对。解决方式是在启动类上补上扫描注解,或者在每个 Mapper 接口上加@Mapper。加了注解还是报错,就去检查resources/mapper目录下的 XML 文件,看application.yml里的mybatis-plus.mapper-locations是否指向正确:

mybatis-plus: mapper-locations: classpath*:/mapper/**/*.xml type-aliases-package: com.example.entity

classpath*:后面的路径要和 XML 文件实际位置匹配。如果项目里没有 XML 文件,完全可以去掉mapper-locations配置,用注解 SQL 也能跑。我的习惯是自定义一个简单的 Mapper 接口做测试,注入后直接调用,先确认扫描链路通不通,再往上写 Service。

5.2 H5 端跨域报错,但小程序端正常

现象:同样的接口,小程序里调用正常,H5 浏览器控制台报CORS error,有时还伴随Failed to fetch。原因是浏览器同源策略拦截了跨域响应,后端没有在响应头里加Access-Control-Allow-Origin。解决办法是使用第三章里的CorsConfig,注意allowCredentials(true)时必须配合allowedOriginPatterns("*"),不能用allowedOrigins("*")。这类问题还要看后端是不是用了 Shiro 或 Spring Security,如果用了安全框架,CORS 过滤器必须在安全过滤器之前注册,否则预检请求直接被拦截。H5 端开发期调接口,后端这台电脑的防火墙也要放行 8080 端口,不然局域网里的手机模拟器访问不到。有人问前端打包后能不能放进 springboot 的 static 目录一起部署,可以,但对 uni-app 多端项目不推荐,小程序端仍然要单独部署 API 服务。

5.3 多选题判卷错误:ABD 和 DAB 明明一样却判错

现象:用户选择A,B,D,提交到后端后显示答案错误,但正确答案确实是A,B,D。原因是字符串equals比较完全按顺序匹配,A,B,D和B,D,A在字符串层面不相等。解决方法是后端判卷时把答案拆成Set,用Set.equals比较,忽略顺序。注意一定要先trim()两端空格,因为前端传过来的字符串可能带空字符,特别是用户用键盘输入时。这道题的边界点在于:单选题答案只有一个字母,不需要拆Set,直接equalsIgnoreCase更快;判断题答案T和F同样用忽略大小写比较。这三类题型一定要分清楚,不要一套逻辑通吃。

5.4 题库更新后,用户端还显示旧题目

现象:管理员在后台改了一道题的答案,用户重启 App 后看到的还是旧答案。原因是前端把题目和答案缓存到了本地 storage,没有做版本失效。解决方式是给题库加版本号字段,后端提供/api/exam/version接口,前端启动时对比本地存储的版本号,不一致就清理exam_answers和wrongList,再重新拉题库。版本号怎么变化呢,我一般用数据库里的时间戳或者每次批量更新题库时手动加一,不必用复杂的增量同步。这里要特别注意:清理缓存要放在用户答题之前,如果你在拉完题库之后再清理,用户当前正在做的题就没了,体验很差。

5.5 JDK 版本太高,SpringBoot 2.x 启动直接报 ClassVersionError

现象:启动时日志出现java.lang.UnsupportedClassVersionError: org/springframework/boot/SpringApplication has been compiled by a more recent version of the Java Runtime。原因就是你本机的 JDK 版本比项目编译版本低,或者反过来,JDK 版本太高与 SpringBoot 2.x 依赖不兼容。现在很多同学装的是 JDK 17 或 JDK 21,拿起来就开跑 2.7 的老项目,启动报错的概率非常高。解决方式:先看pom.xml里的<spring-boot.starter.parent.version>,2.7.x 用 JDK 8 或 11,3.x 用 JDK 17。在 IDEA 里打开 Project Structure,把 Project SDK 和 Java Compiler 都切到对应版本,再重启项目。这里补充一点:IDEA 新版本的 Run Configuration 里可以单独给 Spring Boot 配置 JRE 路径,不跟随全局 SDK,多模块项目尤其容易踩,先检查这里。

6. 题库热替换与错题强化:把毕设刷题系统做成答辩亮点

基础功能做完后,想让答辩老师眼前一亮,我建议你在“题库维护”和“错题复习”这两个方向上做深一点。很多毕设的题库是写死在 SQL 脚本里的,用户用起来没问题,但你演示改题目时很尴尬,总不能现场开数据库改。我在做这类系统时,会加一个非常简单的版本表:

CREATE TABLE t_question_version ( id BIGINT AUTO_INCREMENT PRIMARY KEY, version_no INT NOT NULL DEFAULT 1, update_time DATETIME DEFAULT CURRENT_TIMESTAMP ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

管理端每次更新题库,只做两件事:执行UPDATE t_question SET status=0 WHERE id=?下线旧题,再插入新题,最后INSERT INTO t_question_version把版本号加一。用户端启动刷题页面时,先调/api/exam/version拿远端版本号,和本地uni.getStorageSync('questionVersion')比较,不一致才清理缓存拉新题。这套逻辑不依赖消息队列,也不用定时任务,但很好地解决了“题库热替换”的数据一致性问题。

错题强化可以再进一步。普通错题本只是把错题列出来,答辩时要能说出“为什么这么设计”。我在t_wrong_book里存了wrong_count和last_wrong_time,生成强化卷时按错误次数倒序、最近错误时间倒序出十道题。这个排序规则在 SQL 里就是一句话:

SELECT question_id FROM t_wrong_book WHERE user_id = #{userId} ORDER BY wrong_count DESC, last_wrong_time DESC LIMIT 10;

每次都把错题本里错误最多的题拿出来,配合章节分类就能组成一套“易错专项练习”。这个功能做起来不难,但答辩时你能讲清楚“为什么按这两个字段排序”,就已经比大多数只做增删改查的毕设高一截。我最初做类似项目时,也以为把 CRUD 写完就万事大吉,后来才意识到,老师真正认可的是你把“刷题”这个场景里的边界都想清楚了。热替换解决了数据维护的痛点,错题强化解决了用户刷题效率的痛点,这些点不复杂,但很加分,希望帮到你。

本文还有配套的精品资源,点击获取