考研题库小程序毕设指南:Spring Boot后端与微信小程序全栈实践
2026/9/11 2:39:33 网站建设 项目流程

简介:基于Spring Boot的考研知识题库微信小程序毕业设计,完整包含项目源码与配套教程,面向计算机相关专业毕业生及小程序开发学习者,可作为毕业设计选题或项目实战参考。项目涵盖学生管理、讲师管理、申请讲师审批、科目分类、视频课程、试题管理、交流论坛、知识测卷、考试记录、错题本及轮播图管理等十多个功能模块,形成了从后台数据维护到前台学生使用的完整闭环。资源包共1306个文件,压缩包大小约14.52MB,其中包含130个Java后端源码、143个Vue前端页面、92个WXML小程序页面、186个JavaScript脚本以及大量SVG/PNG图片和JSON配置,同时附带SQL数据库脚本和Windows运行脚本,便于直接导入和启动调试。已有270人学习下载,教程结合源码逐层讲解,有助于读者理解Spring Boot与微信小程序整合的项目架构、权限设计和业务逻辑,适合二次开发或答辩展示。

1. 考研知识题库小程序:这个毕设题目的技术含量在哪

每年毕业设计选题里,考研题库小程序都属于“看起来容易、做出来才知道有坑”的一类。前端是一个微信小程序,用户能刷题、看解析、记录错题;后端是一个 Spring Boot 服务,管题目数据、用户状态和答题记录。很多同学拿到这种题目,第一反应是“不就是 CRUD 吗”,结果做到中期才发现:题目分类怎么设计才能支撑随机抽题,答题记录怎么存才能支撑正确率统计,小程序端登录态怎么和后端 token 对接,每一个点都可能被答辩老师追问。这篇内容按数据模型、后端接口、小程序渲染、交付部署四层展开,把每一步的设计取舍、参数配置和常见坑写清楚。新手可以按章节搭出完整骨架,有经验的人也能在里面看到随机抽题的性能边界、token 失效方案和答辩演示的数据初始化技巧。

2. 从选题到落地:Spring Boot 后端与题库数据模型设计

2.1 先从核心实体关系开始:题目、分类、答题记录怎么落表

考研题库的核心实体有五个:用户、分类、题目、答题记录、错题视图。用户表保存 openid;分类表对应政治、英语、数学、专业课等科目;题目表是题库主体;答题记录表是后面所有统计功能的根基。很多网上的商城模板项目没给你留答题记录表,直接照搬改会造成错题本和正确率统计无从下手。

我在做这类项目时,一般先建这四张表:

CREATE TABLE category ( id BIGINT PRIMARY KEY AUTO_INCREMENT, name VARCHAR(50) NOT NULL COMMENT '分类名:政治/英语/数学/专业课', sort INT DEFAULT 0 COMMENT '排序权重,小的在前', create_time DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间' ) ENGINE=InnoDB COMMENT '题库分类表'; CREATE TABLE question ( id BIGINT PRIMARY KEY AUTO_INCREMENT, category_id BIGINT NOT NULL COMMENT '所属分类', type TINYINT NOT NULL COMMENT '1单选 2多选 3判断', content TEXT NOT NULL COMMENT '题干', options JSON COMMENT '选项,如 {"A":"...","B":"..."}', answer VARCHAR(10) NOT NULL COMMENT '答案:单选填A,多选填ABD,判断填对/错', analysis TEXT COMMENT '答案解析', difficulty TINYINT DEFAULT 1 COMMENT '难度:1易 2中 3难', KEY idx_category (category_id) ) ENGINE=InnoDB COMMENT '题目表'; CREATE TABLE answer_record ( id BIGINT PRIMARY KEY AUTO_INCREMENT, user_id BIGINT NOT NULL COMMENT '用户ID', question_id BIGINT NOT NULL COMMENT '题目ID', user_answer VARCHAR(10) COMMENT '用户提交的答案', is_correct TINYINT DEFAULT 0 COMMENT '0错误 1正确', create_time DATETIME DEFAULT CURRENT_TIMESTAMP, KEY idx_user (user_id), KEY idx_question (question_id) ) ENGINE=InnoDB COMMENT '答题记录表';

这里有两个值得说明的设计决策。第一,question.options用 JSON 类型而不是拆成 option_a、option_b 字段,在 MySQL 5.7 以上可以这样做,改选项数量、打乱选项顺序都不需要动表结构。第二,answer_record独立建表并给 user_id 加索引,因为错题本列表、每日正确率、科目练习统计都要高频查询这张表,如果拿用户表里的一个 JSON 字段存答题历史,后面做聚合查询时只能全表扫描。

候选 key 与业务语义也要在答辩前理清:user表的唯一标识是 openid 而非自增 id;question表不建议直接用题干做唯一索引,因为题干文本较长,索引存储开销大。实际应用中按 (category_id, type) 组合索引来加速分类刷题更常见。

2.2 Spring Boot 项目搭建与版本选择:稳定优先于追新

很多同学的选型矛盾是:学校里教的是 SSM(Spring + Spring MVC + MyBatis),但题目明确写 Spring Boot。这两者的关系在答辩时经常被问“Spring Boot 相比 SSM 有什么优势”,答案落在这三点:自动配置把数据源、事务、MVC 的样板配置收敛成 application.yml 里几行声明;内嵌 Tomcat 让服务以单个 jar 包运行,部署演示省去装独立容器的环节;starter 机制让 MyBatis、Redis、Swagger 等组件都变成一行依赖引入。

实际创建项目时,我建议优先使用 Spring Initializr 生成工程骨架,而不是手写 pom.xml。版本选择上有一个原则:JDK 8 配 Spring Boot 2.7.x,JDK 17 配 Spring Boot 3.x。不要选刚发布的版本,因为你能搜到的参考资料大多滞后于新版本。springboot版本太高带来的典型问题就是 javax 包名变成 jakarta,参考代码大面积失效,这类问题在答辩前一晚出现是非常被动的。

依赖方面,毕设场景的最小集是这些:

<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>com.baomidou</groupId> <artifactId>mybatis-plus-boot-starter</artifactId> <version>3.5.3</version> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <scope>runtime</scope> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis</artifactId> </dependency>

MyBatis-Plus 在毕设场景里几乎是必选,理由很简单:单表 CRUD 不需要写 XML,BaseMapper 已经提供全套方法;分页用 Page 对象,逻辑删除和字段自动填充用注解搞定。原生 MyBatis 的配置成本对毕业设计来说没有必要。

2.3 自动填充与自动建表:用 MyBatis-Plus 和 Flyway 做工程化兜底

表结构设计完成后,下一步是解决两个实操问题:时间字段谁赋值、建表脚本怎么执行。springboot +mybatis 当表不存在自动建表是很多同学在搜的功能,我的建议是不要依赖数据库驱动的 auto-ddl,而是用 Flyway 管理建表脚本,好处是脚本纳入版本控制,答辩时能讲清楚数据库结构如何演进。

时间字段用 MyBatis-Plus 的自动填充统一处理:

@Component public class MyMetaObjectHandler implements MetaObjectHandler { @Override public void insertFill(MetaObject metaObject) { this.strictInsertFill(metaObject, "createTime", LocalDateTime.class, LocalDateTime.now()); } @Override public void updateFill(MetaObject metaObject) { this.strictUpdateFill(metaObject, "updateTime", LocalDateTime.class, LocalDateTime.now()); } }

逻辑说明:插入时自动写 createTime 和 updateTime,更新时只刷新 updateTime,不需要在 service 层每个方法里手动 set。需要注意实体类字段上要加@TableField(fill = FieldFill.INSERT)标注,否则处理器识别不到这个字段。这是一个经常被遗漏的细节,漏掉后的表现是“insert 语句里根本没有 create_time 这一列”。

Flyway 的接入方式是:pom 引入 flyway-core,application.yml 配spring.flyway.locations=classpath:db/migration,启动时会按版本号执行V1__init.sqlV2__xxx.sql这些脚本。这里有一个重要的使用边界:Flyway 只负责 DDL 和基础数据,不要在里面写依赖业务状态的变更,否则团队多人开发时会遇到迁移冲突。

3. 后端接口实现:从登录鉴权到刷题接口

3.1 微信登录的 code2session 流程与 token 方案选择

微信小程序登录的标准路径是:前端调用wx.login()拿到临时 code,后端拿 code 请求微信接口换取 openid,然后以 openid 为业务主键建立用户记录。这个小程序登录接口的完整实现如下:

@RestController @RequestMapping("/api/auth") public class AuthController { @Autowired private StringRedisTemplate redisTemplate; @Autowired private RestTemplate restTemplate; @PostMapping("/login") public Result login(@RequestBody LoginRequest req) { // 1. 使用 code 向微信服务器换取 openid String url = "https://api.weixin.qq.com/sns/jscode2session" + "?appid=" + appId + "&secret=" + appSecret + "&js_code=" + req.getCode() + "&grant_type=authorization_code"; WxSessionResp resp = restTemplate.getForObject(url, WxSessionResp.class); if (resp == null || resp.getOpenid() == null) { return Result.fail("登录失败,请检查 appid 与 secret 配置"); } // 2. 根据 openid 查用户表,不存在则自动注册 User user = userMapper.selectOne( new LambdaQueryWrapper<User>().eq(User::getOpenid, resp.getOpenid())); if (user == null) { user = new User(); user.setOpenid(resp.getOpenid()); user.setNickname("考研人" + resp.getOpenid().substring( resp.getOpenid().length() - 4)); user.setStatus(1); userMapper.insert(user); } // 3. 生成随机 token,写入 redis,有效期 7 天 String token = UUID.randomUUID().toString().replace("-", ""); redisTemplate.opsForValue().set( "login:token:" + token, user.getId().toString(), Duration.ofDays(7)); return Result.success(new LoginResp(token, user.getId())); } }

逻辑说明:code只能使用一次,所以这个接口不能做重试缓存,前端如果请求失败,唯一正确的恢复方式是重新调用wx.login()获取新 code。token 生成使用 UUID 而不是自增 id,是为了防止外部猜测用户标识。Redis 的 key 统一加login:token:前缀,作用是后续做“强制下线”或“全部退出”时能按前缀批量清理。

为什么不用 JWT?毕业设计场景推荐随机 token 存 Redis 而不是 JWT,核心原因是退出登录的立即可失效。JWT 在签发后无法主动作废,而毕设必然要演示登录、退出登录这个基本闭环。另外,token在 Redis 里的过期时间设为 7 天比较合适,考研刷题用户使用频率高但每次使用时间短,太短会导致频繁重新登录,太长会积累过多无效 key。

3.2 核心刷题接口:随机抽题、提交判分与错题记录

随机抽题是题库类小程序的核心体验,对应的接口逻辑是:按分类筛选题目,用数据库随机排序取指定数量。SQL 层面的实现有三种方案,它们的性能边界完全不同:

-- 方案1:ORDER BY RAND(),简单直接,万级数据量可用 SELECT * FROM question WHERE category_id = ? ORDER BY RAND() LIMIT ?; -- 方案2:先随机 id 再回表,数据量大时更稳 SELECT * FROM question WHERE id >= ( SELECT FLOOR(RAND() * (SELECT MAX(id) FROM question)) ) AND category_id = ? LIMIT ?;

方案 1 是毕设最常用的实现,代码清晰、答辩容易解释。如果被追问性能,准备一个答案:MySQL 对ORDER BY RAND()会生成临时表做全表排序,所以当题库量超过几十万时延迟显著上升。方案 2 利用主键索引直接定位随机位置,但要求主键近似连续,如果题目有物理删除,id 空洞会导致取不到足够数量的题。对于考研题库这种万级数据量,方案 1 完全够用,不需要过度设计。

提交判分这一环要注意的坑是:判断逻辑不能只对比字符串相等,因为多选答案的顺序在小程序端可能不一致。我一般把答案统一成“按字母排序后拼接”,后端在判分前也做一次排序归一化,两边约定一致后结果才是稳定的。

public void submitAnswer(AnswerSubmitDTO dto) { String normalized = normalizeAnswer(dto.getUserAnswer()); Question q = questionMapper.selectById(dto.getQuestionId()); boolean correct = normalizeAnswer(q.getAnswer()).equals(normalized); // 写入答题记录表,不直接修改用户表 AnswerRecord record = new AnswerRecord(); record.setUserId(dto.getUserId()); record.setQuestionId(dto.getQuestionId()); record.setUserAnswer(normalized); record.setIsCorrect(correct ? 1 : 0); answerRecordMapper.insert(record); } private String normalizeAnswer(String answer) { char[] chars = answer.replace(",", ",").toUpperCase().toCharArray(); Arrays.sort(chars); return new String(chars); }

逻辑说明:normalizeAnswer先把中文逗号统一成英文逗号,再转大写,最后按字符排序,这样“ADB”和“ABD”会归一化成同一个字符串。判分结束后只插一条答题记录,不实时更新 user 表里的统计字段,因为正确率、刷题数都可以用SELECT COUNT(*) FROM answer_record WHERE ...现算。这种按查询聚合的方式能保证统计口径一致。

3.3 定时任务做每日一题:基于 @Scheduled 的推送设计

做题类小程序的留存功能往往是“每日一题”。后端用 Spring 自带的定时任务就能实现,不需要引入 Quartz:

@Component public class DailyQuestionTask { @Autowired private QuestionMapper questionMapper; @Autowired private StringRedisTemplate redisTemplate; @Scheduled(cron = "0 0 6 * * ?") public void publishDailyQuestion() { Question q = questionMapper.selectOne( new QueryWrapper<Question>() .orderByAsc("RAND()") .last("LIMIT 1")); if (q != null) { redisTemplate.opsForValue().set( "daily:question:" + LocalDate.now(), JSON.toJSONString(q), Duration.ofHours(24)); } } }

注意:启动类上必须有@EnableScheduling,否则定时任务不会生效。cron 表达式0 0 6 * * ?表示每天早上 6 点执行,这个时间点是考研人群刷题高峰之前。答辩演示时,可以把 cron 改成0 0/2 * * * ?(每两分钟一次)方便现场看到效果,演示完再改回正式频率。key 里带日期实现“一天一题”的语义,过期时间 24 小时让旧题自动失效,不需要额外写清理任务。

下面是整个后端模块的接口清单,答辩时可以用这个表来组织演示顺序:

模块接口方法关键参数说明
认证/api/auth/loginPOSTcodecode 换 token
题库/api/question/listGETcategoryId分类题目列表
题库/api/question/randomGETcategoryId, count随机抽题
提交/api/question/submitPOSTquestionId, userAnswer判分入答题记录
统计/api/statistic/summaryGETuserId正确率/总数
每日/api/question/dailyGET获取当日一题

4. 小程序前端:从页面结构到题库渲染

4.1 原生小程序还是 uniapp:毕设场景的选型对比

微信小程序端的技术选型,通常只在原生微信小程序和 uniapp 之间做选择。如果你的毕业设计题目只写了“微信小程序”,我强烈建议选原生,少一层编译链就少一类问题。uniapp 的价值在于一套代码同时输出微信、支付宝、H5 等多个端,但毕业设计没有多端需求,这时候 uniapp 的 HBuilderX 配置、条件编译、平台差异适配反而成了负担。

对比维度原生微信小程序uniapp
学习成本低,官方文档全中,还要理解 Vue 语法
调试体验微信开发者工具直接跑需要 HBuilderX + 微信工具联动
跨端能力仅微信多端
答辩讲解直接讲小程序原生的生命周期还要解释编译链
搜索资料数量最多,问题覆盖最全也多但夹杂大量老版本方案

4.2 请求封装与分类页实现:wx.request 的通用处理

无论选哪种方案,前端的核心都是封装一个统一的wx.request。这里用原生写法演示更直观:

// utils/request.js const BASE_URL = 'http://localhost:8080/api'; function request(path, method = 'GET', data = {}) { return new Promise((resolve, reject) => { wx.request({ url: BASE_URL + path, method, data, header: { 'Content-Type': 'application/json', 'token': wx.getStorageSync('token') }, success: (res) => { if (res.data.code === 200) { resolve(res.data.data); } else { wx.showToast({ title: res.data.msg, icon: 'none' }); reject(res.data); } }, fail: (err) => { wx.showToast({ title: '网络异常', icon: 'none' }); reject(err); } }); }); } module.exports = { request };

逻辑说明:每次请求自动从本地缓存读取 token 并放入 header,后端通过拦截器校验。code === 200是约定的业务成功状态码,HTTP 200 和业务成功是两回事,这一点答辩时经常被问。BASE_URL 在本地开发时要特别注意:微信开发者工具的模拟器里 localhost 指向你电脑本身,但真机预览时手机访问不到 localhost,必须改为电脑的局域网 IP。这也是burpsuite 抓取 PC 端微信小程序这类调试手段出现的原因——先用工具抓包确认请求真的发出了。

分类页的渲染逻辑很直接:onLoad里请求分类列表,scroll-view横向展示分类 tab,点击切换时重新请求对应分类下的题目列表。有一个容易被忽略的适配点:微信小程序顶部导航栏高度在不同机型上不同,如果自定义导航栏,要用wx.getSystemInfoSync().statusBarHeight来计算安全区高度,否则在带刘海的机型上标题会被裁掉。

4.3 答题页的状态管理与答题卡交互

答题页是前端最复杂的部分,核心状态是:当前题目索引、已选答案映射、错题标记。这里不需要引入额外状态库,用 Page 的 data 就可以维护:

Page({ data: { questions: [], currentIndex: 0, selectedMap: {}, }, onLoad(options) { // options.categoryId 从分类页传入 request(`/question/random?categoryId=${options.categoryId}&count=10`) .then(data => this.setData({ questions: data })); }, chooseOption(e) { const { qid, answer } = e.currentTarget.dataset; this.setData({ selectedMap: { ...this.data.selectedMap, [qid]: answer } }); }, nextQuestion() { if (this.data.currentIndex < this.data.questions.length - 1) { this.setData({ currentIndex: this.data.currentIndex + 1 }); } else { this.submitAll(); } }, submitAll() { const { questions, selectedMap } = this.data; const answers = questions .filter(q => selectedMap[q.id]) .map(q => ({ questionId: q.id, userAnswer: selectedMap[q.id] })); request('/question/submit', 'POST', { answers }) .then(() => wx.redirectTo({ url: '/pages/result/result' })); } });

参数说明:selectedMap用对象而不是数组,因为答题卡跳转时需要按题目 id 快速判断是否已作答,对象的取值复杂度是 O(1)。submitAll里先过滤掉未作答题目,只提交有效答案,防止脏数据进入 answer_record 表。答题卡组件用scroll-view横向排列题号,已答和未答用不同背景色区分。这个交互在答辩时能直观展示前端状态管理的层次。

5. 答辩前必做的三件事:数据初始化、部署演示与代码审查

5.1 用 CommandLineRunner 准备演示数据

答辩现场最怕的就是“数据库空空的,评委说刷一道题看看”。用 Spring Boot 的CommandLineRunner在项目启动时自动插入种子数据,能确保每次演示环境都有内容可看:

@Component public class DataInitializer implements CommandLineRunner { @Autowired private QuestionMapper questionMapper; @Override public void run(String... args) { // 已有数据则不重复初始化,幂等处理 if (questionMapper.selectCount(null) > 0) { return; } // 每个分类插入多道题,覆盖单选、多选、判断三种题型 } }

种子数据的覆盖策略是:每个分类至少 5 道题,三种题型都要有,多选题答案至少 3 个选项。这样演示时切换分类、切换题型都不会出现空白页面。

5.2 本地演示的环境检查清单

  • 微信开发者工具:详情-本地设置中勾选“不校验合法域名”,否则本地 http 接口会被拦截
  • 后端服务:确保 8080 端口未被其他进程占用,可用lsof -i:8080netstat -ano检查
  • 数据库:MySQL 8.0 要注意连接串加useSSL=false,否则高版本 MySQL 驱动会报 SSL 警告
  • Redis:确保服务已启动,token 写入失败会表现为登录永远失败
  • 真机预览:BASE_URL 改为电脑局域网 IP,且防火墙需放行后端端口

5.3 源码交付自查

源码目录里建议按这个结构组织:backend/放 Spring Boot 工程,miniprogram/放小程序前端,sql/放建库脚本,README.md写启动步骤和环境要求。代码注释不需要每行都写,关键是类和方法上的设计说明,比如“随机抽题接口,使用 ORDER BY RAND(),适用于万级数据量”这种有效注释。把一个git仓库从项目第一天就建好,提交记录按功能分节点,本身就是答辩时工程素养的直接证据。答辩前一天把 README 里的启动命令完整走一遍,确认从零到演示不需要额外手工步骤。

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

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询