基于微信小程序与Spring Boot的考试刷题系统设计与实现 简介《基于微信小程序的考试刷题系统的设计与实现》是一份面向计算机相关专业毕业设计或课程设计的完整论文文档适合需要完成基于微信小程序、Spring Boot 与 MySQL 的考试刷题系统的学生参考。全文围绕传统办公管理在信息控制上的痛点设计了试卷、试题、用户、知识点四大管理模块涵盖在线考试、成绩查看、刷题及管理员后台等功能并结合 MySQL 数据存储方案给出系统架构与实现思路可作为系统性设计参考。资源共 1 个 doc 文件大小 2.14MB内容包含中英文摘要、目录、绪论、系统功能模块设计、数据库设计等核心章节结构清晰便于直接阅读和二次编辑。尤其适合需要撰写毕设文档和答辩材料的学生可快速理解模块划分与数据库设计要点。该文档目前已有 218 人浏览学习是完成相关毕设课题时可借鉴的实用范本。1. 基于微信小程序的考试刷题系统的设计与实现技术选型与整体思路考试刷题系统不是什么新概念但一旦限定“基于微信小程序”和“Spring Boot”两个前缀整个设计的重心就从“题库模型怎么建”转移到了“两端如何低成本协作”。微信小程序的开发模式限制很多比如包体积上限、canvas 能力弱、不能直接连接数据库、支付和登录都有强绑定所以后端只提供 JSON 接口远远不够还要处理好登录态、题目拉取策略、错题同步、章节进度这些状态性问题。常见做法是小程序端负责展示和交互动画Spring Boot 端负责题库管理、答题记录、错题本和数据分析两端的边界按“有状态的全部归后端无状态的展示归前端”来划分。适合走这条技术路线的人有两类一类是拿这个题目做毕业设计或课程项目需要把完整链路跑通另一类是培训机构和在线教育平台的技术人员想快速搭出一个可上线的刷题闭环。无论哪种都要明白这不是一个“题库 CRUD”项目真正的复杂点在于会话管理、题目随机组卷和错题收敛策略。接口设计、数据表结构、前端页面三者是一体的后面所有章节都围绕这一套选型展开。2. 刷题系统的核心链路与 Spring Boot 后端数据设计2.1 考试刷题系统的核心链路拆解从用户体验倒推系统的最小闭环是用户打开小程序 → 微信登录换取 openid → 选择题库/科目 → 做题 → 提交答案 → 查看对错 → 错题进入错题本。这个闭环里的每一步都对应后端一个接口但接口的粒度不是越细越好。比如提交答案和记录答题历史如果拆成两个接口用户断网连续提交时就会出现“答案提交成功但历史没记录”的问题所以这类操作必须合并成一个事务接口或者提供幂等键。常见的做法是后端接口按场景分四组登录组微信登录、获取手机号、用户信息更新、题库组科目列表、章节列表、题目列表、随机组卷、答题组提交答卷、查答题记录、查排行榜、错题组错题列表、移除错题、错题重做。每组再配一个运维层面的健康检查和用户行为埋点这样从上线的第一天就能知道哪个环节耗时最长。2.2 数据库表设计题库表、答题记录表、错题本表数据库是整个系统的地基表设计不对后面每组接口都会别扭。核心表有 8 张左右但如果把题型和选项分开设计表结构会多三张逻辑上更干净。下面给出最小可用的表设计重点看索引和状态位的取舍。CREATE TABLE tb_subject ( id BIGINT PRIMARY KEY AUTO_INCREMENT COMMENT 科目ID, subject_name VARCHAR(64) NOT NULL COMMENT 科目名称, subject_code VARCHAR(32) NOT NULL UNIQUE COMMENT 科目编码, status TINYINT NOT NULL DEFAULT 1 COMMENT 状态0禁用1启用, sort_order INT NOT NULL DEFAULT 0 COMMENT 排序权重, create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ) COMMENT 科目表; CREATE TABLE tb_question ( id BIGINT PRIMARY KEY AUTO_INCREMENT COMMENT 题目ID, subject_id BIGINT NOT NULL COMMENT 所属科目, chapter_id BIGINT NOT NULL DEFAULT 0 COMMENT 章节ID0表示未分类, question_type TINYINT NOT NULL COMMENT 题型1单选2多选3判断, question_content TEXT NOT NULL COMMENT 题干内容, options_json TEXT COMMENT 选项JSON数组格式, answer VARCHAR(255) NOT NULL COMMENT 标准答案多选用逗号分隔, analysis TEXT COMMENT 答案解析, difficulty TINYINT NOT NULL DEFAULT 3 COMMENT 难度1-5, status TINYINT NOT NULL DEFAULT 1 COMMENT 状态0下线1在线, create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, KEY idx_subject_chapter (subject_id, chapter_id), KEY idx_question_type (question_type), KEY idx_difficulty (difficulty) ) COMMENT 题目表;单看题目表三个字段设计需要重点解释。options_json使用 JSON 数组而不是独立的选项表是因为选项结构在不同题型间差异很小单独拆表只在题目量超过 10 万级才有意义。answer对多选题用逗号分隔这样在 Java 里可以用Arrays.asList(answer.split(,))直接和用户提交的答案集合做无序比较这是最简单而且不容易出错的方案。chapter_id默认 0 而不是允许 NULL是为了避免WHERE chapter_id ?时出现三值逻辑的坑MySQL 中 NULL 不会参与等值匹配如果允许 NULL用户按章节刷题时未分类题目永远查不到。答题记录表和错题本表在关联上需要特别小心。每次答题都往答题记录表插一行用户做了几十套卷子后记录量会迅速膨胀。常见设计是只保留用户最后一次答题内容但这样做历史回溯时就拿不到原始答卷。更合理的做法是答题记录表只保存“提交批次 ID、对错统计、耗时”题目明细单独存 JSON 快照这样既保证可以复现答卷又不至于单张表数据量爆炸。CREATE TABLE tb_answer_sheet ( id BIGINT PRIMARY KEY AUTO_INCREMENT COMMENT 答卷ID, user_id BIGINT NOT NULL COMMENT 用户ID, batch_no VARCHAR(64) NOT NULL COMMENT 批次号用于幂等和分组, total_count INT NOT NULL COMMENT 总题数, correct_count INT NOT NULL COMMENT 正确数, duration_seconds INT NOT NULL COMMENT 耗时, snapshot_json MEDIUMTEXT COMMENT 答题明细JSON快照, create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, KEY idx_user_time (user_id, create_time), KEY idx_batch_no (batch_no) ) COMMENT 答卷批次表; CREATE TABLE tb_wrong_book ( id BIGINT PRIMARY KEY AUTO_INCREMENT COMMENT 主键, user_id BIGINT NOT NULL COMMENT 用户ID, question_id BIGINT NOT NULL COMMENT 题目ID, wrong_count INT NOT NULL DEFAULT 1 COMMENT 累计错误次数, last_wrong_time DATETIME NOT NULL COMMENT 最近错误时间, status TINYINT NOT NULL DEFAULT 1 COMMENT 状态1未掌握2已掌握3手动移除, UNIQUE KEY uk_user_question (user_id, question_id) ) COMMENT 错题本表;错题本表用uk_user_question做唯一索引用户重复做错同一道题时只更新wrong_count和last_wrong_time这样“错题数”就是一次计数不会有重复行困扰。status字段承担软状态流转已掌握的题不是删除而是变更为“已掌握”以后统计错题回顾率时有据可查。2.3 Spring Boot 的项目分层与实体对象映射后端工程按常见单体结构拆为 controller / service / mapper 三层实体类用 MyBatis-Plus 的TableName和TableId注解做映射。为什么选 MyBatis-Plus 而不是 JPA一个小型刷题系统的大多数查询都是单表条件查询MyBatis-Plus 的LambdaQueryWrapper可以让代码里不混写 XML 的条件拼接题目的随机组卷和错题分页都能在一行表达式里完成JPA 在实体关系复杂时更省代码但面对阶层分明的题库查询反而需要写大量Query注解或 Specifications调试成本更高。实体类设计遵循一个原则数据库字段下划线Java 字段驼峰实体属性名和 JSON 返回字段尽量一致小程序端拿到数据后不需要再转换一次。比如question_content对应questionContentoptionsJson对应optionsJson只有create_time在小程序端可能需要格式化成字符串后端统一在序列化配置里写好格式避免每个接口重复处理。2.4 缓存与随机组卷Redis 在刷题场景的正确使用刷题系统有一个显著特点题库数据读多写少用户进入章节刷题时加载的是同一批静态数据。这个场景非常适合 Redis 缓存但缓存粒度不能粗到整张表否则任何一道题目更新都会污染整份缓存。常见做法是按subjectId:chapterId:type组合做缓存 key题目列表的 JSON 整体缓存 30 分钟随机组卷时从缓存的题目集合里取随机下标而不是每次请求都去 MySQL 执行ORDER BY RAND()。这一步在代码上体现得很直接Override public ListQuestionVO getRandomQuestions(Long subjectId, Long chapterId, Integer count) { String cacheKey question:random: subjectId : chapterId; ListQuestionVO examQuestions redisTemplate.opsForList().range(cacheKey, 0, -1); if (CollectionUtils.isEmpty(examQuestions)) { ListQuestion questionList questionMapper.selectList( new LambdaQueryWrapperQuestion() .eq(Question::getSubjectId, subjectId) .eq(Question::getChapterId, chapterId) .eq(Question::getStatus, 1)); examQuestions BeanUtil.copyToList(questionList, QuestionVO.class); redisTemplate.opsForList().rightPushAll(cacheKey, examQuestions); redisTemplate.expire(cacheKey, 30, TimeUnit.MINUTES); } Collections.shuffle(examQuestions); return examQuestions.stream().limit(count).collect(Collectors.toList()); }这段代码的逻辑很直接先从 Redis 列表里拿整章题目拿不到就查库并回填缓存然后打乱顺序取前 N 道。这里做了一个显著的取舍把整章题目一次性缓存而不是把每道题单独缓存交换的是内存占用和查询效率。如果一章有 500 道题且每道题平均 1KB30 分钟内每个章节缓存占用也只有 500KB 左右可以接受。更关键的是Collections.shuffle是进程内操作不会因为随机取题造成缓存击穿。题目更新后缓存会存在脏数据问题解决方式是在题目管理接口里同步删除对应章节的缓存 key而不是更新缓存的值。删除有幂等性即使这次删除失败下一次有人访问该章节时也会因为缓存不存在而回源数据库同时把新数据写回缓存。这是缓存策略里“先删缓存再更新数据库”还是“先更新数据库再删缓存”两种做法的取舍刷题场景更倾向于先更新数据库再删缓存因为即使中间有并发请求读到旧数据也不会造成数据大量不一致最多只是看到旧题。3. 微信小程序端页面设计与登录态打通3.1 页面结构与 tabBar 规划小程序端的页面规划要控制在核心功能以内每多一个 tab 就会增加一次审核和兼容负担。一个足够支撑毕设展示的小程序结构包含四个 tab首页练习入口、题库科目列表、错题本复习入口、我的个人信息。页面路径在app.json里定义时注意 tabBar 页面不能带参数需要传参的场景都通过wx.navigateTo打开非 tab 页。具体页面划分为pages/index/index作为首页展示今日推荐、今日做题数、进度条pages/subject/subject展示科目卡片列表pages/exam/exam是答题页是全部代码里交互最复杂的页面pages/wrong/wrong展示错题列表pages/mine/mine展示用户信息和数据统计。每个页面尽量不要直接向后端请求两次以上像“科目列表 今日统计”这类接口可以在后端合并为一个聚合接口返回减少小程序端网络请求次数。3.2 微信登录换取 openid 与 token 的会话设计微信小程序登录的核心机制是前端调用wx.login()获取临时 code然后通过后端请求微信接口换取 openid 和 session_key。这一步是系统所有会话的基础更安全的做法是不让 openid 直接暴露在小程序端而是在首次登录后由后端生成业务 token 返回给前端保存后续所有请求的鉴权都使用该 tokenopenid 只作为后端识别用户的内部标识。后端拿到 code 后用HttpClient调用微信接口代码逻辑模版如下PostMapping(/login) public ResultVOString login(RequestBody LoginRequest request) { String url String.format( https://api.weixin.qq.com/sns/jscode2session?appid%ssecret%sjs_code%sgrant_typeauthorization_code, wxConfig.getAppId(), wxConfig.getAppSecret(), request.getCode()); String result restTemplate.getForObject(url, String.class); WxSession wxSession JSON.parseObject(result, WxSession.class); if (wxSession.getOpenid() null) { return ResultVO.fail(登录失败请稍后再试); } String token UUID.randomUUID().toString().replace(-, ); redisTemplate.opsForValue().set( wx:token: token, wxSession.getOpenid(), 7, TimeUnit.DAYS); return ResultVO.success(token); }代码里已经隐含了一处重点token 与 openid 的绑定关系存放在 Redis 里并设置了 7 天过期而不是直接写进数据库用户表。这样做的好处是登录态可控过期退出或者被顶号时只需要删除对应 key不需要去数据库改用户状态。微信的 session_key 有效期内不能频繁调用wx.login()实践中发现部分手机在弱网环境下会连续调用多次所以前端要做一个登录状态标记只有 token 不存在候选时才调用wx.login()。小程序端存储 token 时不要用wx.setStorageSync这样全局单店存储页面刷新后存储还在但多页面同时读取时会偶尔拿到 undefined 导致请求无鉴权头。常见做法是封装一个request.js模块在请求拦截器里同步读取 token同时缓存到页面级变量里避免重复读取响应码为 401 时统一跳转登录页。3.3 刷题页交互设计单选、多选、判断题的通用渲染方案考试刷题的答题页核心不是题目加载而是用户选择答案后的中间态管理。用户可能从练习模式进入也可能昨日错题进入两种入口的答完行为完全不同因此答题页组件需要接收一个scene参数区分模式。题目数据统一由后端返回结构体题干、选项列表、题型、当前题号、总题数。小程序端用wx:for渲染选项每个选项是自定义组件用户点击时先选中再点“确认”提交。单选和判断题本质上都是选项互斥多选则需要记录多个选中索引。这部分的逻辑与后端数据无关直接在前端用一数组selectedAnswers维护即可提交时转换为字符串。一个容易忽略的交互细节是用户在答完一道题后点击“下一题”前端的选中状态应该立即清空否则下一题会保留上一题的选择但如果从错题本进入重做模式下一题保留上一题的选择反而便于用户对比。所以组件初始化时用scene判断是否开启clearOnNext开关。Page({ data: { questionList: [], currentIndex: 0, selectedAnswers: [], multiSelected: [], currentQuestion: null, }, onLoad(options) { this.mode options.mode || practice; this.loadQuestions(options.subjectId, options.chapterId, options.type); }, onOptionTap(e) { const question this.data.currentQuestion; const idx e.currentTarget.dataset.index; if (question.questionType 1 || question.questionType 3) { this.setData({ selectedAnswers: [idx] }); } else { const current new Set(this.data.multiSelected); current.has(idx) ? current.delete(idx) : current.add(idx); this.setData({ multiSelected: [...current] }); } }, handleNext() { const current this.data.currentIndex; const answers this.data.currentQuestion.questionType 2 ? this.data.multiSelected.join(,) : this.data.selectedAnswers.join(,); this.submitAnswer(this.data.currentQuestion.id, answers); if (current this.data.questionList.length - 1) { this.setData({ currentIndex: current 1 }); this.resetSelection(); } else { this.finishExam(); } }, });这段代码里的一个关键点在handleNext中当前题的答案在“下一题”动作前提交然后前端立即渲染下一道题用户完全没有等待感。但这种方案的潜在缺陷是如果当前题目未做选择就点了下一题提交的是空答案。因此需要在handleNext里增加空值判断禁止未选择答案就进入下一题或者允许提交空答案但提示“本题未作答”。考试刷题系统面向自测场景空答案也应被记录否则数据统计分析会漏掉未答题情况。3.4 顶部导航栏高度适配与单选框样式复用微信小程序在不同手机的顶部导航栏高度不一致尤其是全面屏手机和刘海屏胶囊按钮位置不一样。刷题页顶部的进度条“第 3 题/共 20 题”通常会放置在自定义导航区域此时需要计算状态栏高度和胶囊按钮高度。常见做法是通过wx.getWindowInfo()获取statusBarHeight再通过wx.getMenuButtonBoundingClientRect()拿到胶囊数据封装成一个全局工具函数const getNavBarConfig () { const winInfo wx.getWindowInfo(); const menuRect wx.getMenuButtonBoundingClientRect(); return { statusBarHeight: winInfo.statusBarHeight, navBarHeight: (menuRect.top - winInfo.statusBarHeight) * 2 menuRect.height, menuRect: menuRect, }; };这个函数计算出导航栏总高度后在app.js的onLaunch里全局保存页面样式里用padding-top: {{navBarHeight}}px给进度条留出位置。另一个高频样式问题是单选框原生的radio组件在不同系统上的圆角大小和动画效果不一致刷题场景适合用自定义view模拟单选样式只保留radio的原生能力做无障碍兼容。这样统一视觉样式的同时也避免了原生单选框在部分安卓机型上出现双击选中不灵敏的问题。4. 后端接口实现题目查询、作答校验与错题回收4.1 题目列表与章节类型的接口设计题库列表页的核心接口是聚合科目信息和章节进度用户进来后第一眼看到的是“科目卡片 每个科目的总题数 已完成题数”。后端接口/api/subject/listWithProgress返回以下结构体{ subjectId: 12, subjectName: 数据结构, totalCount: 320, doneCount: 186, accuracy: 68.2 }doneCount的计数逻辑可以依赖答卷批次表和题目表做关联但这样每条科目记录都要额外跑一次聚合查询。更高效的做法是在用户表中冗余一个daily_progress_json字段每次完成一份答卷后由后端统一更新各章节进度前端直接从用户接口拿聚合结果。这个字段虽然存在一定冗余但获取用户进度时只需一次字符串反序列化比实时 JOIN 快很多。在做题目列表接口时需要注意一个规范默认不直接返回所有题目的选项和答案。练习模式的接口返回题干和选项不返回答案和解析前端只有在用户提交后才请求答案错题重做模式则要返回答案和解析因为错题本页面本身需要展示参考答案。同一个列表接口用mode参数区分返回内容而不是设计两个重复接口避免将来修改题目模型时改两处。4.2 答卷提交接口事务、幂等与答案校验答卷提交是刷题系统里唯一需要数据库写操作的业务点也是后面做并发测试时最容易出问题的地方。前端每次提交一题或整卷提交最终后端都应该收到一个批量请求。接口参数设计为{ batchNo: uuid, subjectId: 12, durationSeconds: 360, answerList: [ { questionId: 1001, answer: C }, { questionId: 1002, answer: A,B } ] }后端在提交时先对batchNo做幂等检查。如果答卷批次表里已有同一个batchNo的数据直接返回上次的统计结果。这样即使小程序在弱网环境下连续发送两次提交请求也不会累计双倍做题记录。这一个细节在很多毕设和初版项目中都会漏掉但用户实测时反复点“交卷”同一份卷子被计了多次后面的统计准确率就全部失真。答案校验逻辑按题型分三类单选和判断用字符串相等多选用排序后比较。排序这种细节如果在 SQL 或字符串相等里做很容易忽略选项乱序问题用户多选选了“B,A”和标准答案“A,B”明明是一致的却不给分。后端处理时先把答案字符串拆成数组排序后用String.join再比较这里给出一个多项式对照实现。public boolean checkAnswer(Question question, String userAnswer) { if (question.getQuestionType() 2) { String[] standard question.getAnswer().split(,); String[] current userAnswer.split(,); Arrays.sort(standard); Arrays.sort(current); return String.join(,, standard).equals(String.join(,, current)); } return question.getAnswer().equals(userAnswer); }整体事务上用Transactional统一包裹批次插入、答分明细插入、错题本更新、用户进度更新任何一个步骤异常时整体回滚不会出现“分数更新了但错题本没写入”的半完成状态。4.3 错题本回收机制不会越跑越大的错题表错题本最大的产品设计问题是“越做越多多到最后变成单纯罗列”。很多刷题系统的错题本只是把答错的题不断堆进去用户最后根本不知道从哪里着手复习。设计时引入“掌握度”概念把错题本变成可收敛的集合。核心机制是每道错题在用户连续答对两次且累计做题次数不少于 5 次时状态自动从“未掌握”流转为“已掌握”。这里的“连续答对两次”不是通过状态字段实现而是错题本表额外增加correct_streak字段答对时加 1答错时归 0。达到 2 的阈值时status改为已掌握。用户可以在错题本顶部看到自己还有多少道待复习题目而不是整个列表完全无差别显示。后端回收接口PostMapping(/answer/correct) public ResultVOVoid recordCorrect(RequestBody CorrectRequest request) { WrongBook record wrongBookMapper.selectOne( new LambdaQueryWrapperWrongBook() .eq(WrongBook::getUserId, request.getUserId()) .eq(WrongBook::getQuestionId, request.getQuestionId())); if (record null) { return ResultVO.success(); } record.setCorrectStreak(record.getCorrectStreak() 1); if (record.getCorrectStreak() 2) { record.setStatus(2); } wrongBookMapper.updateById(record); return ResultVO.success(); }参数correctStreak只有在用户再次答错时才会清零平时保持增长即可错题本页面的查询只关心status 1的记录。这个机制可预测、可验证用户做一次错题回顾后错题数明显下降体验比“删除错题”这种手动操作好得多。4.4 Spring Boot 配置参数与部署清单后端部署时的配置比开发环境复杂一个数量级两个最容易出问题的参数是server.servlet.session.timeout和数据库连接池hikari的最小空闲连接数。因为接口请求大部分是无状态的 token 鉴权session 超时时间直接设置为0表示永不失效即可避免默认 30 分钟的会话超时在并发时造成会话重建开销。连接池配置中minimum-idle设为 5、maximum-pool-size设为 20 比较均衡这个系统没有海量并发堆到 50 个连接反而消耗数据库资源。关键配置项整理如下spring.datasource.url记得额外追加useUnicodetruecharacterEncodingutf8useSSLfalseserverTimezoneAsia/Shanghai否则中文题目可能乱码或时区报错Redis 的redisTemplate序列化器要显式使用StringRedisSerializer存 key、GenericJackson2JsonRedisSerializer存 value否则默认 JDK 序列化的数据在 console 里是一堆乱码排查缓存问题极其痛苦。后端代码全部开发完以后建议用mvn clean package -Dmaven.test.skiptrue打包成 fat jar 直接部署到服务器而不是依赖 IDE 启动。Spring Boot 版本避免使用过旧或过新版本常见的2.7.x与微信 SDK 兼容性最稳3.x需要额外处理 Jakarta 包名迁移初版系统没有必要为此增加工作量。5. 前后端联调接口鉴权、异常处理与小程序兼容性排查5.1 统一响应与全局异常处理小程序端与后端通信时接口返回格式必须高度一致否则前端每个请求都要单独做状态判断。统一响应结构{ code: 200, message: success, data: {} }其中code的三位数比boolean更有扩展空间后续增加限流返回429、参数校验失败返回422时可以平滑兼容。全局异常处理器使用RestControllerAdvice统一捕获业务异常、参数异常、空指针异常和兜底异常返回 JSON 而不是跳转错误页。这样用户看到的是“题目加载失败”而不是小程序白屏。前端request.js拦截器统一判断code ! 200时弹出wx.showToast并且对code 401做特殊处理 —— 清除本地 token跳转登录页。这里有个隐蔽问题如果接口同时返回多个请求失败每个请求都会跳一次登录页小程序页面栈会叠加好几层登录页所以 401 跳转前需要加一个全局的防重复跳转标记。5.2 idea 启动微信小程序与 HBuilderX 发行步骤小程序的开发调试流程在开发工具和源码管理之间有多个入口团队协作时容易出现“代码不同步”的情况。常见做法是后端在 IDEA 中直接启动 Spring Boot 工程前端在小程序开发者工具中导入项目目录。需要注意的是微信开发者工具默认会关闭“不校验合法域名”后端本地调试时如果没有关闭域名校验所有请求都会报url not in domain list这一步是新手最容易卡住的地方。如果是用 HBuilderX 开发 vue 语法项目后再发布为微信小程序发行步骤通常为HBuilderX 菜单栏找到“发行 - 小程序 - 微信”设置 appid 与小程序路径然后自动生成dist/build/mp-weixin目录再到微信开发者工具导入该目录。开发期间改代码不必每次重复发行直接在 HBuilderX 运行到小程序模拟器即可只有正式预览或发布时才会走发行流程。5.3 微信小程序抓包与网络异常定位小程序发起的请求不会直接显示在开发者工具的 Network 面板之外的地方定位后端接口问题最直接的方式是在开发者工具里打开 console 看到request:fail的错误。在实际手机调试时真机上的请求不会经过开发工具所以需要借助抓包工具。这里说一个不依赖第三方抓包工具的思路在后端控制台打印每次请求的 URI、参数和返回状态码比对微信端报错时间和后端日志时间能快速定位是网络问题还是后端逻辑问题。更常见的request:fail产生原因是微信小程序要求所有请求域名必须是 HTTPS 且配置过服务器域名测试环境下如果坚持使用 HTTP则必须在开发工具右上角勾选“不校验合法域名”。真机预览时这个选项不存在因此需要临时配置一个与后端联调的域名或者使用微信开发者工具的“真机调试”功能该模式下请求会通过本地代理转发不受真实域名限制。5.4 题库缓存与 Redis 连接异常时的降级策略Redis 在刷题系统中的缓存作用很大但它一旦宕机后端整体不可用就是设计缺陷。降级策略是在从 Redis 读取题目列表失败时直接查询数据库即使每次请求都穿透数据库也比直接抛出异常导致小程序白屏好得多。代码中通过 try-catch 包住 Redis 操作捕获后返回null并走查库逻辑。降级的同时要把耗时容忍阈值放宽。可以设定如果 Redis 连续失败超过 3 次当前请求直接走数据库路径且不尝试写回缓存。第 4 次请求时再尝试连接 Redis 一次如果已恢复再重新写缓存。这个策略用简单的本地计数器和时间戳即可实现不需要引入重试框架。重点在于缓存组件不能成为整个系统的单点故障。6. 在本地跑通验证闭环最小演示数据与关键指标核对6.1 用最小演示数据验证题目名校验逻辑后端工程起来后验证系统是否可用最合适的动作是用最小批量数据跑一遍刷题闭环。在 MySQL 中向tb_question表插入 10 道题分别覆盖单选、多选、判断题三种题型并为其中两道题故意设置答案顺序颠倒然后再调用一次接口确认容器排序比较逻辑生效。INSERT INTO tb_question (subject_id, chapter_id, question_type, question_content, options_json, answer, analysis) VALUES (1, 1, 1, HTTP 协议默认端口是, [80, 443, 8080, 21], 80, HTTP 默认端口为 80), (1, 1, 2, 下列哪些属于关系型数据库, [MySQL, Redis, Oracle, MongoDB], MySQL,Oracle, Redis 和 MongoDB 是非关系型), (1, 1, 3, HTTPS 比 HTTP 更安全。, [正确, 错误], 正确, HTTPS 增加了加密层);插入完成后调用题目列表接口验证参数能返回全部 10 道题且选项顺序保持原样再调用随机组卷接口传 count5确认每次返回的题目集合不完全相同。这一步完成后就能进微信开发者工具做真实 UI 流转测试。6.2 验证错题回收的参数阈值与连续答对逻辑错题本回收机制的正确性是整个刷题系统的关键难点仅靠代码审查不够必须在真实接口层验证。操作顺序为登录用户→作答含 3 道题错误的试卷→确认错题本新增 3 条记录→重新进入错题本重做这 3 道题且全部答对→确认status仍为“未掌握”→第三次启动挑战再次全部答对→确认状态变为“已掌握”且记录数下降为 0。需要特别留意correctStreak的初始值。如果用户第一次答对时初始化为 0 而不是 1那么“连续答对两次”需要三次正确操作才能触发这和产品描述不一致。在后端实现里插入错题本时correctStreak应当初始化为 0答对时先加 1 再比较是否达到 2。这样在代码中表现为record.setCorrectStreak(record.getCorrectStreak() 1); if (record.getCorrectStreak() 2) { record.setStatus(2); }首次答错时correctStreak仍为 0答对一次为 1连续第二次为 2才释放流转条件。这个细节通过接口测试很容易暴露。6.3 使用微信开发者工具检查 top 导航栏与自定义样式小程序兼容性的最后一关是不同机型的顶部导航栏适配。在开发者工具的模式切换里选 iPhone 13 Pro、Android 设备、iPad 各跑一次刷题页检查进度条和返回按钮是否被系统胶囊遮挡。真机预览时顺便快速滑动页面观察是否有滚动穿透答题页如果页面高度超过视口滚动到底部时应该触发“下一题”按钮的自动吸底效果而不是让用户滑到页面外找按钮。这部分的适配本质上是给page设置height: 100vh并在答题内容区使用scroll-view控制纵向滚动避免整个页面一起滚动导致导航栏位移。顶部导航栏高度在wx.getWindowInfo返回的statusBarHeight基础上加上胶囊按钮高度算出可点击区域的精确尺寸再把自定义导航栏高度设置为这个值不同机型的显示就会保持一致。经过以上本地验证后在微信开发者工具上传代码到体验版邀请一两位同学实际答完一套 20 题的试卷观察后端日志中答卷批次号和答案提交记录是否正常写入确认 Redis 连接与降级路径没有报警整个“基于微信小程序的考试刷题系统 Spring Boot 后端”的闭环就完成上线验证了。本文还有配套的精品资源点击获取