资讯详情

资讯详情

Spring Boot+微信小程序考研题库:从表设计到错题本聚合实战

简介这份毕业设计资源定位为基于Spring Boot与微信小程序的考研知识题库系统源码适合计算机相关专业学生用于毕业设计、课程实训或小程序开发入门。系统完整覆盖学生管理、讲师管理、申请讲师审核、科目分类、视频课程管理、试题管理、交流论坛、知识测卷、考试记录、错题本、轮播图等模块从后台管理到学生端答题刷题形成完整业务闭环便于理解前后端分离架构与小程序端开发思路。包体共1306个文件压缩包大小约14.52MB以Java、Vue、JavaScript源码为主包含微信小程序前端所需的wxml、wxss、json等页面与配置文档同时有png、svg图标资源和数据库SQL文件各类文件按功能目录分层存放方便按需检索学习。已有270人学习下载尤其适合需要快速搭建考研类题库小程序或参考后台权限管理功能的毕业设计选题。1. 一套 Spring Boot 考研题库小程序先从源码包里看门道拿到这份毕业设计压缩包解压后不是只有一个微信小程序里面还有 vue 管理后台的 .bak 文件、构建脚本和 Spring Boot 后端工程。很多同学一上来就双击1-install.bat页面能打开却说不清一条考试记录是怎样从学生端走到管理端再回写到错题本的。这个项目的重点不是常规的用户 CRUD而是讲师申请、知识测卷、考试记录、错题本这类带状态转换和业务约束的流程。从功能清单看学生、讲师、科目、视频课程、试题、论坛、测卷、考试记录、错题本、轮播图都全了明显是一套面向教育行业的常见后台加小程序前台结构。适合拿来做毕业论文的完整系统也适合想学习 Spring Boot 接口权限和小程序登录链路的人做二次开发。源码里保留了 vue 管理后台的构建产物和备份文件意味着你可以同时看到管理端和小程序端两侧的请求方式。下文不会按界面逐页截图而是沿着数据模型、后端接口、小程序前端、错题本与排错这条主线拆。这样你拿到源码后能少走很多弯路。2. 业务模块拆解与数据库设计从 11 个功能到 9 张核心表2.1 角色边界怎样落成表字段功能清单里的“学生管理、讲师管理、申请讲师管理”不是三套独立页面而是同一张用户表配合不同角色标识和审核状态。常见的设计是在user表增加role字段取值student/teacher/admin再单独建teacher_apply表记录申请记录。设计时容易踩坑的是把讲师申请做成直接往user表插一条teacher数据。更好的做法是teacher_apply表保留status字段0 待审、1 通过、2 拒绝。管理员审核通过后再把申请表里的real_name、title、intro同步到user或teacher_profile同时把user.role改成teacher。源码里的讲师管理如果只有新增和编辑那只是管理端直接维护真正和申请联动时必须依赖这个状态机。科目分类管理也是这样。视频课程和试题都要挂在科目下所以subject表要独立出来不能把科目名直接写死在课程表或题目表里。否则后续调整分类名称时要同时改 video、question、exam_paper 三张表很容易出现脏数据。2.2 题目、测卷、考试记录之间如何关联“试题管理”和“知识测卷管理”是容易混淆的两块。试题是一道题试卷是一组题。试卷表通常有exam_paper与exam_paper_question关联表否则同一道题出现在多张卷子里时会产生冗余。考试记录不能只存总分还要存答题详情否则错题本没有数据来源。下面是我在几个教育项目里常用的核心表设计和这套题的功能基本对得上表名核心字段负责的功能userid, username, password, role, status学生、讲师、管理员统一账号teacher_applyid, user_id, real_name, title, intro, status讲师申请与审核subjectid, name, sort科目分类维护video_courseid, subject_id, teacher_id, title, video_url, cover_url视频课程管理questionid, subject_id, type, content, options, answer, analysis试题录入与题库维护exam_paperid, subject_id, title, total_score, duration, status知识测卷基本信息exam_recordid, user_id, paper_id, total_score, duration, submit_time考试记录主表exam_record_itemid, record_id, question_id, user_answer, is_correct, score答题明细错题数据来源wrong_bookid, user_id, question_id, wrong_count, last_wrong_time错题本聚合这里最关键的是exam_record_item。它记录了每个考生对每道题的具体答案和判分结果。错题本不是另起炉灶去判断对错而是从这张明细表里把is_correct 0的数据聚合出来。CREATE TABLE IF NOT EXISTS exam_record_item ( id BIGINT AUTO_INCREMENT PRIMARY KEY, record_id BIGINT NOT NULL COMMENT 考试记录ID, question_id BIGINT NOT NULL COMMENT 题目ID, user_answer VARCHAR(1000) COMMENT 考生答案选择题存选项编号问答题存文本, is_correct TINYINT DEFAULT 0 COMMENT 0-错误 1-正确 2-待人工判分, score DECIMAL(5,2) DEFAULT 0.00 COMMENT 本题得分, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, KEY idx_record (record_id), KEY idx_question (question_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT考试答题明细;执行这条建表语句后record_id用来反查是哪次考试question_id用来关联题目内容user_answer保存学生提交的原始答案。选择题可以直接比对question.answer自动判分主观题则把is_correct置为 2由讲师在管理端人工评分。错题本功能上线前先确认这张表有数据否则后面所有聚合逻辑都是空转。2.3 自动建表Spring Boot MyBatis 如何初始化表结构许多毕设源码里数据库脚本是单独的.sql文件要手动导入。这个项目用1-install.bat一般会走 Spring Boot 初始化逻辑。MyBatis 本身不会自动建表但 Spring Boot 可以配合spring.sql.init在启动时执行建表脚本。spring: datasource: url: jdbc:mysql://localhost:3306/kaoyan_quiz?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/ShanghaicreateDatabaseIfNotExisttrue username: root password: 123456 sql: init: mode: always schema-locations: classpath:db/schema.sql >Data public class ResultT { private Integer code; private String message; private T data; public static T ResultT success(T data) { ResultT result new Result(); result.setCode(200); result.setMessage(success); result.setData(data); return result; } public static T ResultT error(Integer code, String message) { ResultT result new Result(); result.setCode(code); result.setMessage(message); return result; } }code用 200 表示成功401 表示登录失效403 表示无权限500 表示服务器异常。小程序端封装请求时只需要判断statusCode和code这两个值逻辑会简单很多。不要混用 HTTP 状态码和业务状态码比如登录过期用 200 返回但code为 401这样的设计后面会让你在拦截器里反复加判断。3.2 随机组卷与自动评分知识测卷的核心接口有两个一个是进入考试时获取题目一个是交卷时提交答案。获取题目接口不能把answer字段返回给前端否则学生看一眼接口响应就知道正确答案了。正确做法是只返回题目内容、选项、题型和分值答案只在服务端比对。交卷接口是这套源码里最容易写错的地方。自动评分逻辑要放在 service 层并且用事务包裹保证考试记录和错题数据同时落库。我这里给一个简化的核心代码Service public class ExamService { Transactional public ExamRecord submit(SubmitRequest req, Long userId) { ExamRecord record new ExamRecord(); record.setUserId(userId); record.setPaperId(req.getPaperId()); int totalScore 0; for (AnswerItem item : req.getAnswers()) { Question question questionMapper.selectById(item.getQuestionId()); if (question null) { throw new RuntimeException(题目不存在 item.getQuestionId()); } ExamRecordItem detail new ExamRecordItem(); detail.setRecordId(record.getId()); detail.setQuestionId(question.getId()); detail.setUserAnswer(item.getUserAnswer()); if (question.getType() 1) { // 单选题直接比对答案 boolean correct question.getAnswer().equals(item.getUserAnswer()); detail.setIsCorrect(correct ? 1 : 0); if (correct) { detail.setScore(question.getScore()); totalScore question.getScore(); } } else if (question.getType() 2) { // 多选题答案按逗号分隔顺序不一致也算对 boolean correct sortAnswer(question.getAnswer()) .equals(sortAnswer(item.getUserAnswer())); detail.setIsCorrect(correct ? 1 : 0); if (correct) { detail.setScore(question.getScore()); totalScore question.getScore(); } } else { // 主观题标记待人工判分 detail.setIsCorrect(2); } examRecordItemMapper.insert(detail); } record.setTotalScore(totalScore); examRecordMapper.insert(record); // 同一个事务里收集错题 wrongBookService.collectWrong(record.getId()); return record; } }代码里record.getId()在插入之前是空的所以examRecordItemMapper.insert必须等examRecordMapper.insert(record)拿到主键后再执行否则record_id会写成null。这里用Transactional是因为如果后面写错题时出现异常前面插入的明细和记录都要回滚不然学生会看到自己没交上卷数据库里却已经有答题记录。多选题排序比较逻辑也值得注意B,A和A,B应该算同一份答案建议在sortAnswer方法里拆分后排序再 join。3.3 讲师和管理员的接口权限控制学生端、讲师端、管理端共用同一个后端接口时不能只靠前端隐藏按钮来控制权限。Spring Boot 里可以用 Spring Security 或 Sa-Token 做角色校验。常见的做法是登录成功后返回一个 tokentoken 里包含 userId再用拦截器解析 token 后传入当前用户信息。试题新增、编辑、删除接口应该限制为管理员或讲师RestController RequestMapping(/api/question) public class QuestionController { PreAuthorize(hasAnyRole(ADMIN, TEACHER)) PostMapping public Result? create(RequestBody Valid Question question) { questionService.save(question); return Result.success(null); } PreAuthorize(hasAnyRole(ADMIN, TEACHER)) PutMapping(/{id}) public Result? update(PathVariable Long id, RequestBody Valid Question question) { question.setId(id); questionService.update(question); return Result.success(null); } }PreAuthorize会在进入 controller 之前检查当前登录用户角色没有权限直接抛出AccessDeniedException。如果项目没有引入 Spring Security至少也要在 service 层手动校验currentUser.getRole()避免有人直接绕过 controller 调用 service。考试记录接口只能让学生查自己的记录不能把exam_record表整个返给前端否则学生会看到其他人的成绩和答题详情。典型写法是在 SQL 里加WHERE user_id #{userId}而不是查出来再在 Java 里过滤。4. 微信小程序端登录、请求封装与顶部导航适配4.1 微信 code 换 token 的登录链路微信小程序登录不能把用户名密码直接写在页面里。官方推荐的流程是小程序端调用wx.login获取临时code把code发给后端后端再用这个code调用微信接口换取 openid然后生成自己的 token 返回给小程序。源码里如果看到后端直接接收wx.login返回的 code 再请求微信接口属于标准实现。// pages/login/login.js Page({ handleLogin() { wx.login({ success: (res) { if (!res.code) { wx.showToast({ title: 登录失败, icon: none }); return; } wx.request({ url: https://api.example.com/api/auth/wx-login, method: POST, data: { code: res.code }, success: (resp) { const token resp.data.data.token; wx.setStorageSync(token, token); wx.switchTab({ url: /pages/index/index }); } }); } }); } });code有效期只有五分钟而且只能用一次。如果后端拿到 code 后没有先调code2Session再生成 token而是直接把 code 当成用户凭证存放下次登录会出现 code 已被使用的问题。小程序端拿到 token 后要setStorageSync持久化不要只保存在data里否则页面刷新后登录状态就丢了。4.2 请求封装与 token 过期处理小程序里如果每个页面都直接用wx.request后期改域名、加 token、统一处理 401 会很痛苦。一般会在utils/request.js里封装一个 Promise 风格的请求方法所有页面统一引用。const request (url, method, data) { return new Promise((resolve, reject) { wx.request({ url: getApp().globalData.baseUrl url, method: method || GET, data: data || {}, header: { Authorization: Bearer wx.getStorageSync(token) }, success: (res) { if (res.statusCode 401) { wx.removeStorageSync(token); wx.navigateTo({ url: /pages/login/login }); reject(res); } else if (res.data.code 200) { resolve(res.data.data); } else { wx.showToast({ title: res.data.message || 请求失败, icon: none }); reject(res.data); } }, fail: (err) { reject(err); } }); }); }; module.exports { request };这里的baseUrl在开发环境下通常是http://localhost:8080但微信开发者工具必须勾选“不校验合法域名、web-view业务域名、TLS 版本以及 HTTPS 证书”否则本地请求会被拦。上线时baseUrl必须是 HTTPS 域名并且在小程序后台配置 request 合法域名。Authorization头里的Bearer前缀要和后端拦截器解析逻辑保持一致不要前端加一遍后端又按裸 token 解析。4.3 自定义导航栏高度计算和轮播图尺寸默认导航栏在小程序和 App 上的高度不一样如果项目用了自定义导航栏首页轮播的定位会偏移。网上很多教程让直接写死 64px但带刘海屏的手机状态栏高度有 44px普通手机只有 20px写死必出问题。const systemInfo wx.getSystemInfoSync(); const menuButton wx.getMenuButtonBoundingClientRect(); const statusBarHeight systemInfo.statusBarHeight; const navBarHeight (menuButton.top - statusBarHeight) * 2 menuButton.height;这段代码用胶囊按钮的位置反推导航栏高度。menuButton.top是胶囊按钮到屏幕顶部的距离减掉状态栏高度后乘 2是为了把胶囊上下留白都算进导航栏高度。首页轮播图如果拿不到固定高度可以通过图片原始宽高计算const swiperHeight imageWidth / imageHeight * systemInfo.windowWidth;banner表里最好直接存图片 URL 和跳转链接不要只存一个图片地址。管理端修改轮播图后小程序首页下拉刷新时重新拉取banner列表缓存时间超过五分钟就要清理一次否则用户会以为后台没改。4.4 视频课程与论坛页面的加载状态视频课程列表和论坛帖子列表都是列表页翻页时一定要带上page和size参数。小程序端常见的错误是下拉加载时直接替换数据源而不是 append。下面是一个稳妥的分页追加写法let page 1; const pageSize 10; let list []; function loadCourses(reset) { if (reset) { page 1; list []; } request(/api/course/list?page${page}size${pageSize}, GET) .then((data) { list list.concat(data.records); page 1; }); }reset为 true 是下拉刷新为 false 是上拉加载更多。接口返回的records是当前页数据需要concat才对。如果直接用list data.records上拉加载就会把上一页内容丢掉。视频课程播放页要注意video组件的src必须是 HTTPSiOS 对 HTTP 视频地址会直接拒绝播放。5. 错题本聚合与部署排错把最后一个功能调稳5.1 错题本聚合脚本的设计错题本不是每做错一题就立刻插一条记录而是在考试提交后从exam_record_item里批量聚合。这样做的好处是同一道题错多次错题本里只保留一条记录但wrong_count累加。第一次聚合时插入之后改成更新次数。INSERT INTO wrong_book (user_id, question_id, wrong_count, last_wrong_time) SELECT eri.user_id, eri.question_id, COUNT(*), MAX(eri.create_time) FROM exam_record_item eri JOIN exam_record er ON er.id eri.record_id WHERE eri.is_correct 0 AND NOT EXISTS ( SELECT 1 FROM wrong_book wb WHERE wb.user_id eri.user_id AND wb.question_id eri.question_id ) GROUP BY eri.user_id, eri.question_id;这个 SQL 把每次考试的错误记录汇总到错题本。NOT EXISTS保证只插入还没有进入错题本的题wrong_count用COUNT(*)统计这个学生在所有考试中做错该题的总次数。这里有一个边界如果原错题本里已经有这条题NOT EXISTS会不生效需要额外写一条UPDATE wrong_book SET wrong_count wrong_count 1。我一般会在collectWrong方法里先update返回受影响行数为 0 再insert这样比纯 SQL 更容易调试。如果发现错题本没有数据先用这条 SQL 手动跑一遍确认exam_record_item表里有is_correct 0的记录。5.2 部署前必须处理的三个问题第一个问题是端口冲突。Spring Boot 默认 8080如果本机已经跑了其他服务启动会报Port 8080 was already in use。在application.yml里改掉或者启动命令加参数mvn clean package -DskipTests java -jar target/kaoyan-quiz.jar --server.port9090第二个问题是数据库时区。MySQL 连接串里的serverTimezoneAsia/Shanghai和容器时区不一致时考试记录里的submit_time会差 8 小时。如果用的是 Docker 里的 MySQL启动容器时加上-e TZAsia/Shanghai不要只在 JDBC 连接串里写。第三个问题是小程序线上环境。真机调试可以打开开发者工具的“不校验合法域名”选项但正式版必须在小程序管理后台配置 request 合法域名并且必须是已备案的 HTTPS 域名。Spring Boot 项目如果用自签名证书小程序端会直接fail不会走success回调。排错时先看success里的statusCode和fail里的errMsgerrMsg出现ssl、domain、timeout这三个关键词时分别对应证书问题、域名未配置、后端响应过慢处理方向完全不同。5.3 一行命令查看启动日志启动失败时不要反复重启。先用mvn spring-boot:run在前台跑日志会直接打出来。常见的错是数据库权限不够报Access denied for user这时到application.yml改账号密码还有一种是schema.sql里写了重复的表名启动报Table already exists把建表语句改成CREATE TABLE IF NOT EXISTS就不会继续中断了。本文还有配套的精品资源点击获取
觉得有用,分享给同行:

为您的企业打造数字门面

稳重轻奢商务风格,端正雅致视觉,长效耐看不易过时。

立即咨询 →