
简介基于微信小程序的刷题系统是一套面向毕业设计、课程设计与全栈初学者的完整项目源码采用Spring Boot后端与小程序前端结合实现用户登录、题库动态加载、在线答题、自动判分、错题记录与成绩统计等核心功能。资源共773个文件、25.76MB含101个Java后端代码、93个Vue后台管理页面、89个JS脚本、23个WXML和22个WXSS小程序页面另有SQL数据脚本、JAR包、Maven配置及一键安装运行脚本png/svg图片覆盖界面图标与流程示意目录清晰便于按模块学习。前端涉及题目展示、答题提交、错题记录等模块后端包括用户管理、题库管理、答题逻辑与成绩统计可完整体现前后端交互流程。目前已有118人学习下载适合需要快速上手小程序与Spring Boot联调开发的读者。获取后可得到一套可运行方案掌握数据通信、权限校验、评分统计的实现思路便于毕业设计参考或二次扩展。1. 一个刷题系统为什么值得把前后端拆开做如果你在毕业设计或接私活时接到「基于微信小程序的刷题系统」大概率第一反应是这不就是题库加答题页面么但真正动手后会发现难点根本不在画页面而在于「用户做到一半退出、错题怎么记」「每次随机抽题怎么保证不重复」「多人同时刷题时后端能不能撑住」。这套系统本质上是微信小程序做前端交互、Spring Boot 做后端服务、MySQL 存数据的三层结构前后端通过 JSON 接口对话。选择这个方案的人三种最常见毕设需要同时覆盖移动端和服务端技术栈的在校生、给培训机构做内部刷题工具的开发者、想低成本验证刷题类产品 MVP 的创业者。本文会把表结构设计、后端接口、小程序端请求封装、上线部署的坑一步步铺开顺手给出可直接改用的代码骨架。2. 选型与数据建模先把「草稿纸」画对再写代码2.1 为什么是 Spring Boot 微信小程序而不是纯前端或 Node 后端微信小程序天然自带登录体系、UI 组件和发布渠道但它的运行环境对 DOM 操作有限制不适合做重逻辑处理。而刷题系统的核心逻辑——随机抽题、选项判分、错题归集——适合放在服务端理由有三个判分规则要统一不能让前端算完再传结果题库数据不能一次性下发到客户端否则小程序包体积和接口流量都失控后期如果要加统计分析功能后端直接查库更方便。Spring Boot 在这个场景里最大的优势是「起步快」一个 java -jar 就能起服务内置 Tomcat不用单独配 Web 容器配合 MyBatis-Plus 连表查询都很顺手而且网上案例多遇到问题基本都能搜到答案。相比 Node 后端Java 体系在高校和中小公司里更普及接手的人多这也是毕设和外包项目常选它的原因。2.2 核心表结构题库、用户、答题记录、错题本四张表怎么拆磨刀不误砍柴工我一般先把表结构画在纸上再动手。刷题系统最少需要四张表多一张都算过度设计question存题目user存用户基本信息answer_record存每次答题的明细wrong_book存错题。下面是一个可直接执行的 MySQL 建表脚本字段设计考虑到了几个后续一定会踩的坑-- 题目表 CREATE TABLE question ( id BIGINT AUTO_INCREMENT PRIMARY KEY, subject VARCHAR(50) NOT NULL COMMENT 科目如 math/english/java, type TINYINT NOT NULL COMMENT 1单选 2多选 3判断, stem TEXT NOT NULL COMMENT 题干, options_json TEXT COMMENT 选项内容JSON格式如 {A:xxx,B:xxx}, answer VARCHAR(20) NOT NULL COMMENT 正确答案多选用逗号分隔如 A,C, analysis TEXT COMMENT 解析, difficulty TINYINT DEFAULT 1 COMMENT 难度 1-5, created_at DATETIME DEFAULT CURRENT_TIMESTAMP ); -- 用户表 CREATE TABLE user ( id BIGINT AUTO_INCREMENT PRIMARY KEY, openid VARCHAR(64) NOT NULL UNIQUE COMMENT 小程序openid, nickname VARCHAR(50), avatar_url VARCHAR(255), phone VARCHAR(20), created_at DATETIME DEFAULT CURRENT_TIMESTAMP ); -- 答题记录表每次答题一条明细 CREATE TABLE answer_record ( id BIGINT AUTO_INCREMENT PRIMARY KEY, user_id BIGINT NOT NULL, question_id BIGINT NOT NULL, user_answer VARCHAR(20), is_correct TINYINT COMMENT 0错误 1正确, answer_time DATETIME DEFAULT CURRENT_TIMESTAMP, INDEX idx_user_time (user_id, answer_time) ); -- 错题本表 CREATE TABLE wrong_book ( id BIGINT AUTO_INCREMENT PRIMARY KEY, user_id BIGINT NOT NULL, question_id BIGINT NOT NULL, wrong_count INT DEFAULT 1 COMMENT 累计错误次数, last_wrong_time DATETIME, UNIQUE KEY uk_user_question (user_id, question_id) );先解释几个关键设计决策。options_json用 JSON 字符串而不是拆成option_a、option_b多列是因为题库可能随时加选项数量比如从四个选项改成六个用多列的话要改表结构JSON 则不用。answer字段把多选题答案设计成A,C这种逗号分隔的字符串虽然牺牲了一点范式的美感但判分时拿用户的答案字符串直接 split 之后排序再比较代码最简单。answer_record里我特意加了is_correct这个冗余字段——理论上可以通过比对表里的user_answer和question.answer现场算出对不对但每次查错题都要 join 到题目表才能算结果而冗余之后统计正确率就是一条简单的 count 查询这在数据量上来之后差别巨大。2.3 Spring Boot 项目结构和 MyBatis-Plus 配置要点后端代码我习惯按controller / service / mapper / entity四层分包用 MyBatis-Plus 而不是原生 MyBatis因为它内置了BaseMapper单表 CRUD 几乎不用写 XML。下面是pom.xml的关键依赖和application.yml的基础配置这两个文件是每次新建项目的模板基本不需要改dependency groupIdcom.baomidou/groupId artifactIdmybatis-plus-boot-starter/artifactId version3.5.3.1/version /dependency dependency groupIdmysql/groupId artifactIdmysql-connector-java/artifactId scoperuntime/scope /dependency dependency groupIdcn.binarywang/groupId artifactIdweixin-java-miniapp/artifactId version4.5.0/version /dependency dependency groupIdio.jsonwebtoken/groupId artifactIdjjwt/artifactId version0.9.1/version /dependency这里有一个版本选择的坑要注意spring-boot-starter-parent如果用的 3.x那 MyBatis-Plus 必须用 3.5.4 以上版本因为 Spring Boot 3 基于 Jakarta EE旧的javax.*包会直接报 NoClassDefFoundError。我吃过一次亏Spring Boot 2.7 配 MyBatis-Plus 3.4 没问题升级到 3.0 后启动直接失败查了半天才发现是包路径迁移导致。server: port: 8080 spring: datasource: url: jdbc:mysql://localhost:3306/quiz_system?useUnicodetruecharacterEncodingutf8useSSLfalseserverTimezoneAsia/Shanghai username: root password: 123456 driver-class-name: com.mysql.cj.jdbc.Driver mybatis-plus: configuration: log-impl: org.apache.ibatis.logging.stdout.StdOutImpl global-config: db-config: logic-delete-field: deleted logic-delete-value: 1 logic-not-delete-value: 0logic-delete-field这个配置值得多说一句它配置的是逻辑删除也就是说删除错题记录时不是真的 DELETE而是把deleted字段置为 1。刷题系统的场景是用户可能反复做错同一道题如果物理删除错题记录历史统计就断了。我们预留给user表一个deleted字段逻辑删除后查列表自动过滤掉已删除的记录MyBatis-Plus 帮你在 SQL 后面自动追加WHERE deleted0。3. 后端核心接口登录、抽题、判分一次讲透3.1 微信登录换取 openid用 JWT 做无状态会话小程序端wx.login()会拿到一个临时 code后端拿 code 去微信接口换 openid这是每个小程序后端都要走的第一步。微信官方接口jscode2session需要appid secret code三个参数成功后会返回 openid 和 session_key。注意session_key一定不能返回到前端它用于解密手机号等敏感信息泄露等于把用户数据裸奔。我一般用 JWT 生成一个 token 返回给前端前端之后每次请求都把 token 放在 header 里后端用拦截器校验。以下是登录接口的核心逻辑PostMapping(/wx/login) public Result login(RequestBody LoginRequest req) { // 1. 用 code 换 openid WxMaService wxMaService WxMaServiceFactory.get(); WxMaJscode2SessionResult session wxMaService.getUserService() .getSessionInfo(req.getCode()); String openid session.getOpenid(); // 2. 查用户是否存在不存在则注册 User user userMapper.selectOne( new LambdaQueryWrapperUser().eq(User::getOpenid, openid)); if (user null) { user new User(); user.setOpenid(openid); userMapper.insert(user); } // 3. 生成 JWT token有效期 7 天 String token Jwts.builder() .setSubject(user.getId().toString()) .setExpiration(new Date(System.currentTimeMillis() 7 * 24 * 3600 * 1000)) .signWith(SignatureAlgorithm.HS256, SECRET_KEY) .compact(); return Result.success(new LoginResp(token, user.getId())); }这里的SECRET_KEY我建议单独放到配置类里不要硬编码在业务代码中。前后端分离项目里token 在请求头中的传递习惯是Authorization: Bearer {token}而不是自定义一个x-token这是行业通用规范小程序端用wx.request的 header 对象就能设置。每次请求经过拦截器时把 token 解析出的 userId 塞进ThreadLocal后续 service 层直接用不用每次手动传用户 ID这是一种写起来很省事的做法。3.2 随机抽题接口用一条 SQL 解决「每次不重复」的问题刷题系统的核心体验是「抽题」接口设计上要考虑两个场景顺序刷题和随机刷题。顺序刷题就是按题目 ID 从小到大翻随机刷题则要避免用户短时间内抽到重复题。最简单的实现是ORDER BY RAND()但数据量超过几万条后这会让数据库临时建表排序接口响应会明显变慢。我实际用的是「先随机一个偏移量再取一条」的做法见下面的 mapperSelect(SELECT * FROM question WHERE subject #{subject} AND id (SELECT FLOOR(RAND() * (SELECT MAX(id) FROM question WHERE subject #{subject}))) ORDER BY id LIMIT 1) Question getRandomQuestion(String subject);这条 SQL 的思路是先查出该科目下最大的题号再随机生成一个小于它的偏移值取第一个大于等于这个偏移值的题目。它避免了ORDER BY RAND()全表排序的性能问题但有一个边界坑如果偏移值指向的行恰好被删除了id 偏移值会顺延到下一条不会返回空。真正的边界问题是题量少时可能出现连续抽到同一道题所以我加了一层内存去重把用户最近 30 道题的 ID 缓存在 Redis 里抽题时判断是否在集合中是则重新抽一次。对于毕设规模的数据量这条 SQL 完全够用没必要上推荐算法。3.3 判分逻辑与错题本联动事务保证数据一致性判分接口是写入操作最密集的地方它要同时做三件事插入答题记录、更新错题本、更新用户的统计数字。任何一个步骤失败都会造成数据不一致比如答错了但错题本没记上。所以我用Transactional把这些写操作包成一个事务代码如下Transactional(rollbackFor Exception.class) public AnswerResult submitAnswer(AnswerSubmitDTO dto) { Question question questionMapper.selectById(dto.getQuestionId()); boolean correct checkAnswer(question, dto.getUserAnswer()); // 1. 插入答题记录 AnswerRecord record new AnswerRecord(); record.setUserId(dto.getUserId()); record.setQuestionId(dto.getQuestionId()); record.setUserAnswer(dto.getUserAnswer()); record.setIsCorrect(correct ? 1 : 0); answerRecordMapper.insert(record); // 2. 联动错题本 if (!correct) { WrongBook wrongBook wrongBookMapper.selectOne( new LambdaQueryWrapperWrongBook() .eq(WrongBook::getUserId, dto.getUserId()) .eq(WrongBook::getQuestionId, dto.getQuestionId())); if (wrongBook null) { wrongBook new WrongBook(); wrongBook.setUserId(dto.getUserId()); wrongBook.setQuestionId(dto.getQuestionId()); wrongBook.setWrongCount(1); wrongBookMapper.insert(wrongBook); } else { wrongBook.setWrongCount(wrongBook.getWrongCount() 1); wrongBook.setLastWrongTime(new Date()); wrongBookMapper.updateById(wrongBook); } } else { // 答对了就从错题本移除可选策略 wrongBookMapper.delete(new LambdaQueryWrapperWrongBook() .eq(WrongBook::getUserId, dto.getUserId()) .eq(WrongBook::getQuestionId, dto.getQuestionId())); } return new AnswerResult(correct, question.getAnswer(), question.getAnalysis()); }checkAnswer方法的实现有个细节要强调不要用 equals 直接比较字符串。用户可能提交a,c标准答案是A,C大小写不一致就误判了。我一般把两个字符串都转大写后拆成数组排序再 join 比较。多选题的容错策略要提前定好是「少选不算对」还是「少选得半分」我采用「完全一致才算对」判分简单清晰展示答案时也能少解释很多。这里我用Transactional(rollbackFor Exception.class)而不是默认的rollbackFor RuntimeException.class是因为事务里如果抛出 SQL 异常或自定义业务异常默认配置不会回滚数据就脏了。4. 微信小程序端从登录态到刷题页面的完整链路4.1 小程序目录结构与 request 请求封装小程序端代码结构遵循微信官方的约定但我会刻意把utils/request.js、api/目录拆出来避免每个页面都写一遍wx.request。项目结构大概是miniprogram/ ├── pages/ │ ├── index/ // 首页科目选择 │ ├── quiz/ // 刷题页 │ ├── result/ // 答题结果页 │ └── wrong-book/ // 错题本 ├── utils/ │ ├── request.js // wx.request 封装 │ └── auth.js // 登录态管理 ├── api/ │ ├── quiz.js // 抽题/判分接口 │ └── user.js // 登录/个人信息 └── app.jsutils/request.js是所有网络请求的出口我把 token 注入、错误处理、加载动画都收敛到这个文件里页面只管调用。这是前后端分离项目实战里比较重要的一个习惯不然每个页面都要处理 401 重定向和错误提示代码会膨胀到没法维护const request (url, method GET, data {}) { return new Promise((resolve, reject) { const token wx.getStorageSync(token); wx.request({ url: ${BASE_URL}${url}, method, data, header: { Content-Type: application/json, Authorization: token ? Bearer ${token} : }, success(res) { if (res.statusCode 200 res.data.code 0) { resolve(res.data.data); } else if (res.statusCode 401) { // token 过期重新登录后再次请求 wx.removeStorageSync(token); reLoginAndRetry(url, method, data, resolve, reject); } else { wx.showToast({ title: res.data.msg || 请求失败, icon: none }); reject(res.data); } }, fail(err) { wx.showToast({ title: 网络异常请检查后端服务是否启动, icon: none }); reject(err); } }); }); };这段封装里有几个值得注意的地方。BASE_URL在开发阶段用http://localhost:8080但真机上 localhost 指向手机本身必须改成电脑的局域网 IP比如http://192.168.1.100:8080。这个配置我建议放在config.js里单独管理不要散落在各个文件——我们线上出过一次事故某个同事直接把开发环境的 localhost 提交上去了结果所有体验版用户全挂在接口上。reLoginAndRetry是处理 token 过期后静默重登的逻辑避免了用户正在刷题时突然被踢到登录页的糟糕体验。4.2 刷题页核心逻辑选项渲染、提交判分、下一题切换刷题页是小程序端最复杂的页面它涉及选项渲染、提交后的状态切换正确/错误标色、下一题加载三个交互状态。我用data里的currentQuestion、selectedAnswer、submitted三个字段分别控制下面是关键代码片段Page({ data: { currentQuestion: null, selectedOptions: [], // 用户已选项如 [A, C] submitted: false, // 是否已提交本题 correctAnswer: , // 提交后展示的标准答案 optionList: [] // 渲染用的选项数组 }, onLoad(options) { this.loadQuestion(options.subject); }, async loadQuestion(subject) { const question await quizApi.getRandomQuestion(subject); const optionList Object.keys(question.optionsJson).map(key ({ key, value: question.optionsJson[key] })); this.setData({ currentQuestion: question, optionList, selectedOptions: [], submitted: false }); }, handleOptionTap(e) { if (this.data.submitted) return; // 提交后禁止修改答案 const { key } e.currentTarget.dataset; const { selectedOptions, currentQuestion } this.data; if (currentQuestion.type 1) { // 单选直接覆盖 this.setData({ selectedOptions: [key] }); } else if (currentQuestion.type 2) { // 多选toggle 选中状态 let newSelected [...selectedOptions]; const idx newSelected.indexOf(key); if (idx -1) { newSelected.splice(idx, 1); } else { newSelected.push(key); } this.setData({ selectedOptions: newSelected }); } }, async handleSubmit() { if (this.data.selectedOptions.length 0) { wx.showToast({ title: 请先选择答案, icon: none }); return; } const res await quizApi.submitAnswer({ questionId: this.data.currentQuestion.id, userAnswer: this.data.selectedOptions.sort().join(,) }); this.setData({ submitted: true, correctAnswer: res.correctAnswer }); // 答错时自动加入错题本但不用额外提示避免打断刷题节奏 } });注意多选的sort().join(,)是前端先做一次排序再传给后端因为用户点击选项的顺序可能是B, A而后端比较时也会排序两边保持一致能避免判分时出现「用户明明选对了却判错」的诡异问题。单选则没有排序问题但我也统一走了 join 流程保持接口格式一致。submitted字段是一个交互保护锁它在提交后置为 true阻止用户反复点击选项防止在判分结果还没返回时修改答案——这种「没加锁」导致的状态错乱是刷题类小程序最常见的前端翻车点。4.3 微信小程序登录获取手机号授权流程与后端解密刷题系统一般不需要强制绑定手机号但很多培训机构要求「手机号 验证码」登录所以还是得预留这个能力。微信官方从基础库 2.21.2 开始推荐使用getPhoneNumber按钮获取加密数据然后交给后端用session_key解密。前端代码比较简单就是放一个开放能力按钮button open-typegetPhoneNumber bindgetphonenumberhandleGetPhoneNumber 绑定手机号 /button真正的坑在后端的解密环节。e.detail.code换手机号的新逻辑是用 code 调phonenumber.getPhoneNumber接口老逻辑里encryptedData iv解密的方案虽然网上源码还能搜到但微信官方已经逐步收紧。我在对接时遇到过返回码 40029 的情况排查后确认是 code 已经过期——小程序端的 code 只能用一次5 分钟有效如果你在onLoad里先用了 wx.login 的 code再点手机号按钮拿另一个 code顺序和有效期必须理清楚否则就是「代码看起来没问题但实测时频繁报错」的玄学现场。我一般把解密逻辑封装成独立 service返回手机号后走正常的UPDATE user SET phone? WHERE id?流程。5. 联调与部署避坑跑通不是终点跑稳才是交付5.1 跨域、HTTPS 与小程序合法域名校验小程序对网络请求的限制比普通网页严格得多正式版要求所有请求域名必须在小程序后台配置为「合法域名」而且必须是 HTTPS。这意味着你本地用http://localhost:8080能跑通但上传体验版后直接白屏报request:fail。我自己的开发流程是本地开发用「详情 - 本地设置 - 不校验合法域名」这个选项但凡是给客户看 demo 或者上传体验版一定提前准备好一台有备案域名的 HTTPS 服务器。Spring Boot 后端在 Tomcat 层配置 SSL或者用 Nginx 做反向代理 Lets Encrypt 证书两种方案我都试过Nginx 方案改配置不用重新打 jar 包日常维护方便很多。如果后端接口里出现了跨域报错不要慌Spring Boot 加一个 CorsFilter 就能解决小程序端其实不受浏览器同源策略限制跨域问题只出现在 Web 调试工具里Configuration public class CorsConfig { Bean public CorsFilter corsFilter() { CorsConfiguration config new CorsConfiguration(); config.addAllowedOriginPattern(*); config.addAllowedMethod(*); config.addAllowedHeader(*); config.setAllowCredentials(true); UrlBasedCorsConfigurationSource source new UrlBasedCorsConfigurationSource(); source.registerCorsConfiguration(/**, config); return new CorsFilter(source); } }5.2 用 charles 抓包排查联调问题的三个实操场景联调阶段最容易遇到的问题就是「后端说接口没问题前端说没收到数据」。我习惯用 charles 做中间代理把小程序端的请求完整捕获下来。具体操作是电脑和手机连同一个 Wi-Fi手机 HTTP 代理指向电脑的 IP 和 8888 端口安装 charles 的 SSL 证书后就能看到 HTTPS 明文请求。实际排查中三个高频场景分别是CONNECT请求被拦截导致小程序连不上后端、请求头里 Authorization 没传导致 401、后端返回 JSON 中code字段和小程序端解析的不一致导致拿不到data。关于第三点我印象尤其深刻——有的后端团队喜欢用code200表示成功有的用code0前后端没对齐时小程序端会静默失败用户看到的就是「点了开始刷题没有任何反应」。所以接口联调的第一件事是先统一响应体结构而不是急着写业务代码。5.3 Spring Boot 版本太高引发的启动失败一个典型排查过程这是我从 Spring Boot 2.7 升级到 3.x 后踩过最重的坑值得单独列出来。现象是项目启动时报ClassNotFoundException: javax.servlet.Filter原因是 Spring Boot 3 移除了对javax包的支持全面切换到jakarta.*而项目中很多第三方库比如旧版 druid、旧版 mybatis-plus还是按javax编译的。解决方式是逐个升级依赖版本druid 用 1.2.20mybatis-plus 用 3.5.4jjwt 干脆替换成io.jsonwebtoken:jjwt-api/impl/jackson的 0.12.x 版本。如果你对依赖版本控制没把握我建议老老实实留在 Spring Boot 2.7它是 3.x 之前最稳定的版本网上搜到的案例也最匹配。还有一个 springboot 相关的细节banner可以替换成个性文本但这属于锦上添花不用花时间调。5.4 三个必看的日志位置与排查命令后端起不来或者接口报错时不要靠猜按顺序看三处日志Spring Boot 启动日志看端口占用和 Bean 创建失败、MyBatis 的 SQL 执行日志看参数是否传对、SQL 语法是否正确、微信接口调用日志看 code 是否过期、appid 是否匹配。命令上常用的套路是# 查看 Java 进程和端口占用 lsof -i:8080 # 查看 Spring Boot 实时日志 tail -f logs/spring.log # 用 curl 直接调后端接口绕过小程序端定位问题 curl -X POST http://localhost:8080/wx/login -H Content-Type: application/json -d {code:test_code}curl这一招是前后端联调的神器——小程序端报错时先用 curl 打一下同一个接口如果 curl 返回正常就说明问题出在小程序端大概率是请求头或参数格式如果 curl 也报错就直接定位到后端逻辑。这能把「前端问题」「后端问题」「微信平台问题」三选一的排查范围直接缩小比反复在小程序开发者工具里点重试效率高得多。6. 让这个项目从「能用」变成「值得展示」的三个进阶点第一个进阶点给答题记录加一个简单的统计报表。用户刷完一套题之后不会满足于看到「正确率 60%」这个数字。我后来加了一个统计接口返回近七天的每日刷题数和正确率趋势前端用小程序内置的画布组件ec-canvas或者简单的view堆叠柱状图展示。后端实现不难查answer_record表按天分组统计每天的答题总数和正确数SQL 大概是SELECT DATE(answer_time) d, COUNT(*), SUM(is_correct) FROM answer_record WHERE user_id? AND answer_time DATE_SUB(NOW(), INTERVAL 7 DAY) GROUP BY DATE(answer_time)。加了这个功能项目的完整度感觉立刻不一样也容易在答辩时讲出「基于答题数据做个性化学习反馈」的调性。第二个进阶点用 Redis 做刷题排行榜。做题刷题天然有竞技属性一个总榜加一个七日榜能把日活拉起来。实现上用 Redis 的ZADD存储member - scoremember 是 userIdscore 是答题总分每次判分接口执行完后顺手调一次ZINCRBY加分。排行查询用ZREVRANGE key 0 9 WITHSCORES性能比查 MySQL group by 好一个量级。这个功能对毕设来说属于加分项对真实产品来说则是留存的命脉。第三个进阶点上线发布前做一次完整的小程序审核配置检查。微信公众平台的后台需要配置服务器域名、业务域名如果用到wx.getLocation等接口还要在「接口权限」里申请开通。我遇到过最尴尬的事故是项目部署好了、接口全部通了、体验版测试通过结果提交审核时因为类目选的「教育」但资质材料不全被驳回。更隐蔽的是如果刷题内容涉及医考、法考等专业领域平台可能会要求提供对应的内容资质证明。提前把这些材料准备齐比临时抱佛脚从容得多。回到前面说的「从能跑到跑稳」——我自己的习惯是每次改完代码先用curl把登录、抽题、判分三条主链路跑一遍再上小程序模拟器走一遍 UI 流程最后打一个生产环境的 jar 包用java -jar启动验证。这套流程看起来笨但确实让交付后的线上问题少了很多。刷题系统的核心价值说白了就是让用户能在碎片时间里高效地「练 测 复盘」只要这三条链路体验顺滑技术上剩下的问题都是可以修的。希望这些踩坑记录能帮你少走几段弯路祝你的项目一次跑通。本文还有配套的精品资源点击获取