首页 / 资讯中心 / 文章详情

基于微信小程序与Spring Boot的刷题系统开发与部署全解析

基于微信小程序与Spring Boot的刷题系统开发与部署全解析 ★ FEATURED ARTICLE
简介基于微信小程序与Spring Boot的在线刷题系统完整源码包适合想要学习前后端分离开发、Java后端接口设计以及小程序页面实现的开发者参考。压缩包一共包含一千零四十九个文件体积大约十六兆字节具体涵盖一百零六个Java源码文件、一百二十二个Vue组件文件、一百三十九个JavaScript脚本以及六十四个wxml与六十个wxss前端页面文件、两百三十一个png图片资源、SQL数据库脚本和启动构建批处理文件整个项目目录清楚便于直接导入开发环境进行学习。目前已有二十六人学习浏览资源覆盖了题库管理、用户交互、后端RESTful接口、数据存储与检索等关键环节同时保留了原有的配置备份与组件样式参考。通过通读源码并执行构建脚本可以快速梳理小程序端与Spring Boot服务端的请求响应流程并在此基础上修改复用搭建面向校园或企业的刷题练习平台。1. 基于微信小程序的刷题系统Spring Boot解压源码后真正要解决的四件事每年毕业设计季节这类“基于微信小程序的刷题系统的设计与实现springboot.rar”资源包会大量出现。解压后通常是三样东西Spring Boot 后端工程、微信小程序前端目录、一份设计文档。不少人以为导入 IDE 就能跑实际上登录、抽题、判分、错题本这几条链路都要自己重新对一遍才能闭环。这个方向解决的不是“写一个刷题工具”而是用微信小程序承载练习交互、用 Spring Boot 提供接口与数据存储的一套前后端分离系统。适合正在做毕设选题、或想低成本验证一个小程序教育产品的开发者。动手前先想清楚四件事题库数据怎么建模、登录态怎么交换、判分放在哪一端、部署后域名怎么过小程序校验。把这四条理顺源码包里的代码才接得住你自己的业务。2. Spring Boot 后端先把题库数据底座立住表设计与接口拆分2.1 刷题系统的数据建模5 张表与答案字段的设计理由刷题系统表面是 CRUD实际上数据关联比普通表单复杂。我一般拆五张表用户表、题库表、题目表、练习记录表、错题表。用户表承接微信 openid题库表只放题库元信息题目表存题干、选项、答案、解析练习记录表记录每次答题明细错题表是冗余表方便个人中心直接展示。题目表是核心字段设计直接决定判分逻辑好不好写。下面是一个常见的建表结构CREATE TABLE question ( id bigint(20) NOT NULL AUTO_INCREMENT, bank_id bigint(20) NOT NULL COMMENT 所属题库ID, type tinyint(4) NOT NULL COMMENT 1单选 2多选 3判断, content text NOT NULL COMMENT 题干, options text COMMENT 选项JSON如[{key:A,text:Java是编译型语言}], answer varchar(20) NOT NULL COMMENT 答案单选填A多选填ABD判断填T或F, analysis text COMMENT 题目解析答完展示, score int(11) NOT NULL DEFAULT 1 COMMENT 分值, create_time datetime NOT NULL, PRIMARY KEY (id), KEY idx_bank (bank_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT题目表;几个容易踩的点多选答案不要用逗号分隔直接存“ABD”前端和后端都按字符串包含关系判断解析成本最低。options 用 JSON 字符串存而不是单独建选项表对刷题系统足够避免 join 太多。题干里可能有富文本图片所以 content 用 text 而不是 varchar。判断题的答案用 T/F 而不是 true/false和单选统一成字符串比较逻辑。题库表保持轻量只维护 name、subject学科分类、question_count 这类冗余统计字段。每次新增题目后更新 question_count 可以做也可以不管首页展示用 count 聚合查询替代。记录表和错题表都带 user_id但重点要记得建联合索引CREATE TABLE record ( id bigint(20) NOT NULL AUTO_INCREMENT, user_id bigint(20) NOT NULL, bank_id bigint(20) NOT NULL, question_id bigint(20) NOT NULL, user_answer varchar(20) DEFAULT NULL COMMENT 用户提交的答案, is_correct tinyint(4) NOT NULL COMMENT 1对 0错, create_time datetime NOT NULL, PRIMARY KEY (id), KEY idx_user_bank (user_id, bank_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT练习记录表;错题表设计上有一个细节不要每次答错都 insert 一条否则错题本越滚越乱。常见做法是加唯一索引uk_user_question(user_id, question_id)答错时做 insert ... on duplicate key update把 wrong_times 加一、update_time 刷新。这样个人中心能看到每道错题错了几次比单纯列一堆重复记录更有价值。2.2 接口按小程序页面反推首页、练习页、错题页各自要什么后端接口数量不必追求多按小程序页面反推最省事。首页要题库列表练习页要随机抽题答题完成要交卷判分个人中心要用户信息与错题本。小程序端每一个页面调用什么接口在写后端时同步列出来前后端联调就不会出现接口对不上的情况。前端页面接口路径Method说明首页/api/wx/bank/listGET返回启用状态的题库列表登录/api/wx/loginPOST用 wx.login 的 code 换后端 token练习页/api/wx/question/randomGET按题库 ID 随机抽 N 道题答题提交/api/wx/record/submitPOST提交答案返回得分与错题列表错题本/api/wx/wrong/listGET分页查询当前用户错题个人中心/api/wx/user/infoGET用户信息与练习统计这里建议把接口路径都放在/api/wx/前缀下管理端接口用另一个前缀/api/admin/后续做权限拦截时按前缀切分即可。练习页的抽题接口最值得花心思。常见源码包喜欢直接写SELECT * FROM question ORDER BY RAND() LIMIT 10数据量小的时候没问题题库到了几千题就会明显变慢而且用户会有很大概率抽到重复题。抽题接口的职责应该包括按题库过滤、排除当前用户已经做过的题、限制每次抽取数量。真正做到这三件事需要拿到用户已做题目 ID 列表再用 NOT IN 排除。后面第五章会专门讲这个坑。2.3 把登录和随机抽题接口写出来Spring Boot 常用实现与参数配置先看登录接口。小程序端调用 wx.login 拿到一个临时 code后端拿这个 code 去微信服务端换取 openid。正式实现里这一步要请求微信的 jscode2session 接口以下代码保留了这个过程方便你替换成自己的真实接口PostMapping(/api/wx/login) public ResultMapString, Object login(RequestBody LoginReq req) { // 用 code 调用微信接口换取 openid // 这里省略 HTTP 请求细节实际替换成 HttpClient 调用即可 WxSession session wxService.code2Session(req.getCode()); User user userMapper.findByOpenid(session.getOpenid()); if (user null) { user new User(); user.setOpenid(session.getOpenid()); user.setNickname(微信用户); user.setCreateTime(new Date()); userMapper.insert(user); } String token JwtUtil.createToken(user.getId(), 7 * 24 * 60 * 60 * 1000L); MapString, Object map new HashMap(); map.put(token, token); map.put(userId, user.getId()); return Result.ok(map); }登录逻辑里最关键的一点是用户第一次进来就自动注册不需要单独走注册流程。这是微信小程序项目和传统 Web 项目的最大区别。token 用 JWT 生成过期时间设为 7 天与小程序本地缓存保持一致。实际源码包可能用 localStorage 或微信 storage无所谓但两端过期时间必须同步否则会出现小程序里显示已登录、后端却返回 401 的玄学问题。随机抽题接口是刷题系统最重要的接口参数设计要能覆盖两种场景正常练习抽新题、错题重刷抽全部题。下面这个实现是常见的合体写法GetMapping(/api/wx/question/random) public ResultListQuestionVO random( RequestParam Long bankId, RequestParam(defaultValue 10) Integer limit, RequestParam(defaultValue false) Boolean reviewMode, RequestHeader(Authorization) String token) { Long userId JwtUtil.getUserId(token); ListLong excludeIds Collections.emptyList(); if (!reviewMode) { // 排除已做过的题避免反复抽到同一批 excludeIds recordMapper.findDoneQuestionIds(userId, bankId); } ListQuestionVO questions questionMapper.selectRandomByBank(bankId, limit, excludeIds); return Result.ok(questions); }对应 Mapper XML 里的 SQL 大概是下面这样注意 NOT IN 的数量如果过多可以改成 NOT EXISTSSELECT * FROM question WHERE bank_id #{bankId} if testexcludeIds ! null and excludeIds.size() 0 AND id NOT IN foreach collectionexcludeIds itemid open( separator, close) #{id} /foreach /if ORDER BY RAND() LIMIT #{limit}参数说明limit是本次练习的题目数量小程序端可以固定传 10 或 15后端要做上限校验建议最大不超过 50否则一次查询压力大且答题体验差reviewMode为 true 时忽略 excludeIds用于错题重刷token 从请求头取后端拦截器要放行/api/wx/login其余接口必须校验。数据库配置上IDEA 新建 Spring Boot 项目时建议选 Spring Boot 2.7.x 加 JDK 8 或 11很多老源码包还在用 javax 命名空间直接配 JDK 17 加 Boot 3 会编译不过这个坑后文专门说。3. 微信小程序端把刷题手感做出来登录、出题与提交判分3.1 小程序登录与 token 缓存wx.login 到 Spring Boot 的完整链路小程序端的登录链路和传统表单登录完全不同。用户打开小程序时不需要输入账号密码前端调用 wx.login 拿到临时 code传给后端后端用 code 换 openid 并返回自定义 token前端把 token 存进 wx.setStorageSync。整个过程用户无感知但开发者要处理三个细节请求封装、token 过期、缓存清理。先封装一个全局请求方法避免每个页面重复写 wx.request。下面这份代码是我在刷题项目里常用的模板统一处理了登录态和错误提示const request (url, method GET, data {}) { const token wx.getStorageSync(token) return new Promise((resolve, reject) { wx.request({ url: getApp().globalData.baseUrl url, method: method, data: data, header: { Content-Type: application/json, Authorization: token ? Bearer ${token} : }, success: (res) { if (res.statusCode 200) { resolve(res.data) } else if (res.statusCode 401) { // token 过期清掉本地缓存并回到登录页 wx.removeStorageSync(token) wx.removeStorageSync(expireTime) wx.navigateTo({ url: /pages/login/login }) reject(res) } else { wx.showToast({ title: 请求失败, icon: none }) reject(res) } }, fail: (err) reject(err) }) }) } module.exports { request }wx.login 的调用时机最好放在 App.onLaunch 里页面 onLoad 时先检查本地 token 是否存在。看到这里你可能会问为什么不直接每次启动都重新登录因为 wx.login 每次生成的 code 只能用一次频繁调用会浪费微信接口配额也没必要。更合理的做法是第一次登录后把 token 和过期时间都存下来下次启动时判断是否在有效期内。缓存时间怎么设我一般在前端存一个 expireTime 字段和后端 JWT 的 7 天保持一致wx.setStorageSync(token, data.token) wx.setStorageSync(expireTime, Date.now() 7 * 24 * 60 * 60 * 1000)在请求封装里加一个前置判断expireTime 小于当前时间就直接跳登录页不发请求。这里要特别注意一个坑有些人只存 token 不存过期时间导致后端 JWT 明明过期了前端还拿着 token 去请求等到 401 再处理体验就多了一次无效请求。设置缓存时间不是为了省事是要让前端和后端的失效时点尽量一致。3.2 刷题页渲染与选项交互单选、多选、切题回填题库列表页比较简单重点在刷题页面。页面结构通常分三块顶部进度条、题目卡片区、底部操作栏。题目卡片区用 swiper 或 scroll-view 呈现我建议用 scroll-view 配合纵向滚动而不是 swiper因为题目内容可能很长用户需要自由滚动看题干swiper 的滑动判断容易误触。题目选项的渲染不要用原生 radio-group。原生单选框的样式很难调刷题场景需要展示选中态、正确态、错误态三种颜色自定义 view 加 bindtap 更灵活。这样可以顺带把“微信小程序单选框”这个交互场景下的样式问题绕开。核心结构如下view classquestion-card wx:for{{questions}} wx:keyid view classq-title{{index 1}}. {{item.content}}/view view classq-options view wx:for{{item.options}} wx:for-itemopt wx:for-indexoptIndex wx:keykey classoption {{answers[item.id] opt.key ? active : }} >onOptionTap(e) { const { qIndex, optIndex } e.currentTarget.dataset const q this.data.questions[qIndex] const opt q.options[optIndex] const questionId q.id if (q.type 2) { // 多选按字母顺序拼接答案如 A B D - ABD const old this.data.answers[questionId] || const keys old ? old.split() : [] const idx keys.indexOf(opt.key) if (idx 0) { keys.splice(idx, 1) } else { keys.push(opt.key) } this.setData({ [answers.${questionId}]: keys.sort().join() }) } else { // 单选/判断直接覆盖 this.setData({ [answers.${questionId}]: opt.key }) } }切题回填的答案存在 data.answers 里key 是题目 IDvalue 是用户已选的答案。这样用户滑动到下一题再回来答案不会丢。页面销毁时如果有未提交的答案可以给个弹窗提示“还有题目未作答”防止用户误退出丢进度。3.3 提交判分与错题写入前后端职责划分判分放前端还是后端答案是后端。前端判分看起来省一次请求但用户只要用开发者工具改一下本地代码就能把 score 改成满分刷题记录就失真了。正确的做法是前端把用户所有答案传给后端后端拿着数据库里的正确答案逐题比对返回得分、正确数、错误题号列表。后端 submit 接口的核心实现PostMapping(/api/wx/record/submit) public ResultSubmitResult submit(RequestBody SubmitReq req, RequestHeader(Authorization) String token) { Long userId JwtUtil.getUserId(token); int totalScore 0; int correctScore 0; ListLong wrongIds new ArrayList(); ListQuestion questions questionMapper.selectBatchIds(req.getQuestionIds()); for (Question q : questions) { totalScore q.getScore(); String userAnswer req.getAnswers().get(q.getId().toString()); boolean correct q.getAnswer().equalsIgnoreCase(userAnswer); if (correct) { correctScore q.getScore(); } else { wrongIds.add(q.getId()); } recordMapper.insert(new Record(userId, q.getBankId(), q.getId(), userAnswer, correct)); } // 答错的题写入错题本用 upsert 方式累计错误次数 if (!wrongIds.isEmpty()) { wrongBookMapper.upsertBatch(userId, wrongIds); } SubmitResult result new SubmitResult(); result.setTotalScore(totalScore); result.setCorrectScore(correctScore); result.setWrongQuestionIds(wrongIds); return Result.ok(result); }这段逻辑有两点值得说明。第一selectBatchIds 查出来的题目列表顺序与传入的 questionIds 不一定一致所以判分时不能依赖遍历顺序但最后返回 wrongQuestionIds 时保持数据库查询顺序无所谓前端是根据题目 ID 去匹配的。第二record 表记录的是每次答题行为而错题本存的是汇总结果。如果用户连续两次答错同一道题record 表会有两条记录wrong_book 表只把 wrong_times 变成 2这是刻意设计的。前端拿到结果后用 wx.showModal 展示得分然后跳转到结果页。结果页除了得分还要把错题解析列出来。这里我习惯把 analysis 字段在抽题接口里一并返回省去结果页再逐题请求的麻烦。刷题的手感好不好很大程度取决于答完能不能立刻看到解析而不是只给一个红色叉号。4. 管理后台、打包部署与发布配置从源码到线上的最后一公里4.1 管理端权限边界题目维护接口为什么要单独拆 admin刷题系统不能只有学生端还得有题目维护入口。但管理端和小程序端不能共用同一套接口否则任何人都能通过小程序接口把题目和答案改掉。常见做法是在后端增加一个简单的管理端模块用账号密码登录签发带有角色的 JWT。接口前缀区分是第一步Configuration public class WebConfig implements WebMvcConfigurer { Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(new AdminAuthInterceptor()) .addPathPatterns(/api/admin/**) .excludePathPatterns(/api/admin/login); } }AdminAuthInterceptor 做的事很直观从请求头取 token解析出 role 字段如果 role 不是 ADMIN 就返回 403。管理端的登录接口可以复用 JwtUtil只是多塞一个 role 进去MapString, Object claims new HashMap(); claims.put(role, ADMIN); String token JwtUtil.createToken(admin.getId(), claims, 2 * 60 * 60 * 1000L);管理端 token 的过期时间我建议设短一点2 小时足够安全边界更好。小程序端的 token 是 7 天。如果你拿到手的源码包把管理端和小程序端接口写在一起优先按路径前缀切分重构而不是在 Controller 里加 if 判断当前用户角色。分离之后管理端的 Swagger 文档也可以单独开一组避免把内部接口暴露给小程序端用户。4.2 打包部署maven 打 jar、Docker 启动和 JVM 参数后端部署的常规路径是 Maven 打包加 Docker 运行。先确认 pom.xml 里没有把源码包自带的本地 JAR 用 systemPath 引用这类问题在网上下载的源码包里很常见一旦用了本地依赖打出来的包换一台机器就起不来。遇到这种情况把本地 JAR 装进本地 Maven 仓库或者改成公共仓库里的坐标。打包命令和启动命令mvn clean package -DskipTests java -jar target/exam-0.0.1-SNAPSHOT.jar --spring.profiles.activeprod生产环境不建议直接 java -jar 挂在前台用 Docker 更便于管理。一个参考 DockerfileFROM openjdk:8-jre-alpine COPY target/exam-0.0.1-SNAPSHOT.jar /app/exam.jar WORKDIR /app EXPOSE 8080 ENTRYPOINT [java, -Xms256m, -Xmx512m, -jar, exam.jar]JVM 参数这里给一个参考值个人毕设项目或小型练习平台1 核 2G 的云服务器足够支撑几十个并发练习场景Xms 和 Xmx 设 256m 到 512m 即可。如果刷题量集中在某一时间段比如晚上八点可以后续接入 Redis 做接口频控而不是一上来就堆机器内存。数据库连接串和 Redis 地址要放到 application-prod.yml 里外置不要在 jar 包里写死。这点听起来是常识但我见过太多源码包把 localhost 数据库地址留在配置里部署到服务器后项目能启动但接口一查数据库就报连接拒绝。外置配置的常见写法是启动时加参数覆盖java -jar exam.jar --spring.datasource.urljdbc:mysql://内网地址:3306/exam?useUnicodetruecharacterEncodingutf8 --spring.datasource.usernameroot --spring.datasource.password***4.3 小程序发布前配置合法域名、HTTPS 证书和导航栏设置小程序端上线前有三个配置绕不开。第一后端必须用 HTTPS 域名且域名要备案。微信公众平台后台的「开发管理 - 开发设置 - 服务器域名」里把 request 合法域名加上例如https://api.example.com。开发阶段可以在微信开发者工具里勾选“不校验合法域名、web-view、TLS 版本以及 HTTPS 证书”但体验版和正式版都会强制校验所以本地跑通后第一件事就是把线上域名配好。第二小程序里的所有请求地址不要写成 IP。微信小程序对 request 域名有严格校验必须是 HTTPS、必须备案、IP 地址直接拒绝。因此开发时如果连本地后端用开发者工具的“不校验合法域名”开关真机预览时要在同一局域网用电脑 IP 加端口但也只能开发调试无法正式发布。第三页面顶部导航栏。小程序默认导航栏高度在不同机型上不一样尤其是有刘海的 iPhone导航栏会更高。如果刷题页顶部有固定进度条建议直接用 navigationStyle 默认导航栏让微信自己处理高度适配省掉自定义导航栏带来的胶囊按钮遮挡问题。如果你确实要自定义导航栏务必用 wx.getMenuButtonBoundingClientRect() 动态获取胶囊位置来计算布局不要写死 44px。app.json 里一个完整的配置片段{ pages: [ pages/index/index, pages/practice/practice, pages/result/result, pages/wrong/wrong, pages/user/user ], window: { navigationBarTitleText: 在线刷题, navigationBarBackgroundColor: #ffffff, navigationBarTextStyle: black } }导航栏标题在 practice 页可以动态改比如进入《Java基础》题库后把标题改成题库名。这个用 wx.setNavigationBarTitle 实现在页面 onLoad 里根据参数调用即可。5. 刷题系统落地避坑5 个让我返工过的典型问题5.1 真机预览请求失败request 合法域名与 HTTPS现象微信开发者工具里接口一切正常一换成真机预览所有 wx.request 全部进入 fail 回调控制台报“不在以下 request 合法域名列表中”。原因开发者工具默认勾选了“不校验合法域名”所以本地调试没问题真机上微信强制读取公众平台配置的合法域名列表没配置就直接拦掉而且是 network 层的拦截代码里根本拿不到后端响应。解决开发期间在公众平台把线上域名加到 request 合法域名列表如果只是临时真机调试可以在开发者工具详情里勾选“不校验合法域名”但扫码预览用的是开发版体验版和正式版仍然强制校验。另一个容易被忽略的点是域名必须是 HTTPS且证书链完整不能用自签名证书。所以部署后端时要提前申请 SSL 证书不能拖到最后一刻。5.2 RequestBody 接收 null小程序 Content-Type 不一致现象后端接口写的是RequestBody LoginReq req小程序端明明传了{code: xxx}后端却打印出 req 为 null或者 req.code 为 null有时干脆报 400 Bad Request。原因这是个黑匣子问题。小程序 wx.request 在部分基础库版本里如果 header 中没有显式指定 content-typePOST 请求会按 application/x-www-form-urlencoded 发送而 Spring Boot 的 RequestBody 期望 application/json两边解析方式不一致body 就变成空的。解决在封装的 request 方法里强制设置请求头header: { Content-Type: application/json, Authorization: token ? Bearer ${token} : }同时后端接口里不要又是 RequestBody 又接收表单参数。统一用 JSON 传参最省事。排查时先在 Network 面板看请求头里的 content-type 是什么再决定改前端还是改后端。5.3 Long 型 ID 精度丢失JSON 序列化成 Number 的坑现象题库列表接口返回的 bankId 是 1656925691822124546点击进入练习页后发现加载出来的题目属于另一个题库或者直接查不到题目。原因Java 后端用 Long 表示主键雪花算法生成的 ID 超过 JavaScript Number 的安全整数范围 2^53 - 1。JSON 序列化时把 Long 当 number 输出小程序拿到后精度已经丢失末尾几位被截断。解决让后端把 Long 类型的 ID 字段序列化成字符串。Jackson 全局配置是对 Long 和 long 类型用 ToStringSerializerConfiguration public class JacksonConfig { Bean public Jackson2ObjectMapperBuilderCustomizer longToStringCustomizer() { return builder - builder.serializerByType(Long.class, ToStringSerializer.instance) .serializerByType(Long.TYPE, ToStringSerializer.instance); } }如果你不想动全局配置也可以在实体类的 id 字段上加JsonSerialize(using ToStringSerializer.class)。注意这个坑在联调时不容易发现因为前端打印 ID 看起来是正常的只有点击跳转后行为不对排查时要先检查两边拿到的 ID 字符串是否一致。5.4 随机抽题又慢又重复ORDER BY RAND 的下限与替代现象题库 5000 道题每次抽 10 道刚做过的题过两题又出现题库越大接口响应越慢从几十毫秒涨到几百毫秒。原因ORDER BY RAND()在 MySQL 里会对全表数据排序生成随机数数据量一大性能直线下降。且抽题逻辑没有排除已做过的题导致重复题目反复出现。解决至少做到按题库过滤加 NOT IN 排除已做题目。如果题库超过一万题可以考虑更高效的随机策略先查出当前题库里未做过的 ID 范围再取随机主键偏移量。只按已完成记录排除的方式还不够因为如果用户做了 4000 题NOT IN 列表会非常长这条 SQL 也会变慢。我通常的做法是在题库表加一个 done_count 字段做冗余计数或者用 Redis 的 SET 存已完成题目 ID查询时直接从 Redis 取差集。对毕设级别的数据量NOT IN 已经够用但不要写成ORDER BY RAND()就交差。5.5 提交按钮被胶囊和底部横条遮挡导航栏与安全区适配现象答题页底部 fixed 定位的“提交”按钮在 iPhone 上被屏幕底部的 home indicator 挡住一部分自定义顶部导航栏时标题文字被右上角胶囊按钮盖住。原因微信小程序的导航栏和底部安全区高度不是固定值。不同机型顶部状态栏高度不同全面屏手机底部还有安全区。写死 px 的布局在特定机型上必翻车。解决顶部导航栏用默认样式标题通过 wx.setNavigationBarTitle 动态设置。底部操作栏预留安全区CSS 里加.submit-bar { position: fixed; left: 0; right: 0; bottom: 0; padding-bottom: env(safe-area-inset-bottom); background-color: #ffffff; }如果页面还需要自定义顶部导航用小程序的wx.getMenuButtonBoundingClientRect()获取胶囊按钮位置动态计算导航栏高度和内容区 padding-top。这个适配代码建议在 App.onLaunch 里计算一次存到 globalData避免每个页面重复调用。卡在 UI 适配上的时间往往比写业务逻辑还多提前做适配能省很多返工。6. 把刷题系统当产品打磨组卷策略、防刷限流与数据闭环验证刷题系统跑通之后最值得投入的是出题策略。固定随机抽题会让用户觉得“题目没有针对性”常见改进是让组卷按难度比例出题。在 question 表增加 difficulty 字段1 简单2 中等3 困难抽题时分别从三个难度等级里取一定比例。后端可以拆成三次查询也可以一次查出候选集后在内存里做权重分配数据量不大时后者更灵活ListQuestion easy questionMapper.selectRandomByDifficulty(bankId, 1, 6, excludeIds); ListQuestion medium questionMapper.selectRandomByDifficulty(bankId, 2, 3, excludeIds); ListQuestion hard questionMapper.selectRandomByDifficulty(bankId, 3, 1, excludeIds);配合组卷策略还要给抽题接口加上频控否则一个用户疯狂调用 random 接口能把题库刷空。轻量做法是在拦截器里用本地缓存记录同一 userId 一分钟内的请求次数超过 30 次直接返回 429。等用户量上来再替换成 Redis 分布式限流。防刷做完最后验证数据闭环登录新用户抽题答对两题答错一题检查 record 表和 wrong_book 表的数据变化答错题在错题本里出现答对两次后错题本对应记录被移除。这套数据核对流程我每次改完判分逻辑都跑一遍比盯着页面看靠谱得多。如果你也在改一套类似的刷题源码先把这三处补上体验会明显比原生源码包好一截希望帮到你。本文还有配套的精品资源点击获取
阅读完成 · 觉得有帮助?
咨询建站