简介:基于微信小程序的刷题系统是一套面向毕业设计、课程设计与全栈初学者的完整项目源码,采用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> <groupId>com.baomidou</groupId> <artifactId>mybatis-plus-boot-starter</artifactId> <version>3.5.3.1</version> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <scope>runtime</scope> </dependency> <dependency> <groupId>cn.binarywang</groupId> <artifactId>weixin-java-miniapp</artifactId> <version>4.5.0</version> </dependency> <dependency> <groupId>io.jsonwebtoken</groupId> <artifactId>jjwt</artifactId> <version>0.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?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=Asia/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 deleted=0。
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 LambdaQueryWrapper<User>().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(),但数据量超过几万条后这会让数据库临时建表排序,接口响应会明显变慢。我实际用的是「先随机一个偏移量再取一条」的做法,见下面的 mapper:
@Select("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 LambdaQueryWrapper<WrongBook>() .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 LambdaQueryWrapper<WrongBook>() .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-type="getPhoneNumber" bindgetphonenumber="handleGetPhoneNumber"> 绑定手机号 </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 做反向代理 + Let's 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。关于第三点我印象尤其深刻——有的后端团队喜欢用code=200表示成功,有的用code=0,前后端没对齐时小程序端会静默失败,用户看到的就是「点了开始刷题没有任何反应」。所以接口联调的第一件事是先统一响应体结构,而不是急着写业务代码。
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.20+,mybatis-plus 用 3.5.4+,jjwt 干脆替换成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 -> score,member 是 userId,score 是答题总分,每次判分接口执行完后顺手调一次ZINCRBY加分。排行查询用ZREVRANGE key 0 9 WITHSCORES,性能比查 MySQL group by 好一个量级。这个功能对毕设来说属于加分项,对真实产品来说则是留存的命脉。
第三个进阶点:上线发布前做一次完整的小程序审核配置检查。微信公众平台的后台需要配置服务器域名、业务域名,如果用到wx.getLocation等接口还要在「接口权限」里申请开通。我遇到过最尴尬的事故是:项目部署好了、接口全部通了、体验版测试通过,结果提交审核时因为类目选的「教育」但资质材料不全,被驳回。更隐蔽的是,如果刷题内容涉及医考、法考等专业领域,平台可能会要求提供对应的内容资质证明。提前把这些材料准备齐,比临时抱佛脚从容得多。
回到前面说的「从能跑到跑稳」——我自己的习惯是每次改完代码,先用curl把登录、抽题、判分三条主链路跑一遍,再上小程序模拟器走一遍 UI 流程,最后打一个生产环境的 jar 包用java -jar启动验证。这套流程看起来笨,但确实让交付后的线上问题少了很多。刷题系统的核心价值,说白了就是让用户能在碎片时间里高效地「练 + 测 + 复盘」,只要这三条链路体验顺滑,技术上剩下的问题都是可以修的。希望这些踩坑记录能帮你少走几段弯路,祝你的项目一次跑通。
本文还有配套的精品资源,点击获取