news 2026/9/25 1:30:51

微信小程序与Spring Boot刷题系统实战:架构拆解与避坑指南

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
微信小程序与Spring Boot刷题系统实战:架构拆解与避坑指南

简介:一套基于微信小程序与Spring Boot开发的刷题系统完整项目源码,适合Java后端开发者、小程序前端学习者及毕业设计参考。项目实现了题库管理、在线刷题、用户反馈等核心功能,前端使用小程序原生组件与Vue管理端,后端提供RESTful API并配套SQL数据库脚本。资源包共1049个文件,约16MB,涵盖png/svg静态资源、js/vue/wxml/wxss前后端页面、java后端逻辑、json配置及sql脚本等类型,目录结构清晰,便于按模块查阅与二次开发。已有26人学习下载。包内含后台管理端Vue页面、Spring Boot后端工程、小程序前端源码与部署脚本,并附带运行视频与反馈通道,可帮助学习者快速理解前后端交互流程,掌握从环境搭建到接口调试的完整思路,也可作为课程设计与求职作品的基础模板。

1. 基于微信小程序的刷题系统 + Spring Boot:这套代码到底能省多少事

拿到weixin273这个压缩包时,我先确认了一件事:它不是一个“老师布置的作业”级别的半成品,而是一个能直接跑起来的小程序刷题系统——小程序端负责选题、答题、看错题,Spring Boot 后端负责题库管理、答题判定、数据统计,整套链路是通的。对于想自己写一个在线答题小程序、又不想从零抠登录态和接口设计的开发者来说,这份资源能省下至少一周的搭建时间。但它也带着不少实操门槛:你得会改 MySQL 配置、看得懂微信小程序的 request 封装、还得知道端口和域名校验这些坑。这篇笔记就按我拆包复现的顺序,把它说透。

2. 拆包看架构:小程序端、后端接口与数据库表是怎么分工的

2.1 项目目录结构:谁在前端谁在后端

解压后目录大致是这样的:根目录下会有miniprogram(或者叫wechat-app)和springboot(或者直接是jar包和src目录)两个主要文件夹,另外一般还有db或sql目录放初始化脚本,以及一份简单的设计说明文档。别小看这个分类,很多新手第一步就栽在把后端代码往小程序开发工具里拖。

miniprogram里是标准的微信小程序项目,app.json、pages、utils一目了然。pages下通常有index(题库列表)、exam(答题页)、result(结果页)、wrong(错题本)这些子目录。utils里会有一个request.js,负责统一封装wx.request,这是整套前端和后端通信的喉咙,后面我会专门拆它。

springboot那边是标准的 Maven 项目结构,src/main/java下面按controller、service、mapper、entity分层。如果这个包是早期生成的,你可能还会看到dao目录而不是mapper,这不影响理解,核心是 Controller 接 HTTP 请求,Mapper 碰数据库。数据库脚本通常在database/shuati.sql这类路径下,用 Navicat 或命令行执行就能建库建表。

2.2 后端 Spring Boot 分层:Controller-Service-Mapper 的套路

这套资源的技术栈基本是 Spring Boot + MyBatis + MySQL,合情合理,因为刷题系统的数据模型简单,MyBatis 写 SQL 比 JPA 直观多了。接口设计上,我整理了一份典型的接口清单,你对照源码找同名文件就行:

功能接口路径(常见)请求类型作用
微信登录/api/user/loginPOST接收 code,调 wx.code2Session 换 openid,生成 token
获取题库列表/api/bank/listGET分页返回题库列表
获取题目/api/question/listGET按题库 ID 分页获取题目
提交答案/api/record/submitPOST保存答题记录并判定对错
错题列表/api/wrong/listGET查询当前用户错题
统计结果/api/statistics/summaryGET返回做题总数、正确率

Controller 层很薄,基本就是收参数调 Service;Service 层处理业务,比如迭代题目、解析答案;Mapper 层只写 SQL。这套代码的亮点是“答案不在接口里”,而是在数据库question表的answer字段里,提交时用用户答案和字段比对,这种做法比前端判断要稳,因为别人绕过页面直接调接口也刷不了分。

2.3 数据库核心表结构

数据库结构我整理成五张核心表,源码里的sql脚本基本都有:

表名用途关键字段
user用户表id, openid, nickname, avatar, create_time
question_bank题库表id, bank_name, description, question_count
question题目表id, bank_id, question_type, content, option_a, option_b, option_c, option_d, answer, analysis
answer_record答题记录表id, user_id, question_id, bank_id, is_correct, answer_content, create_time
wrong_question错题表id, user_id, question_id, wrong_count, last_wrong_time

注意question表的question_type,一般 1 表示单选题,2 表示多选题。如果这是你第一次看这种表,建议先弄清answer字段的存法:单选是A,多选可能是ABC,甚至用逗号分隔。别拿到多选当单选判,后面我把这个坑展开说。

3. 从 .rar 到能跑起来:环境准备、导入、配置与启动全流程

3.1 环境清单与版本组合建议

先把环境备齐。这套代码是 Spring Boot 项目,JDK 用 1.8 最稳,Maven 3.6 以上就行,MySQL 5.7 或 8.0 都兼容,微信开发者工具用稳定版。这里我不建议你直接用 Spring Boot 3.x 去试,因为老项目的javax包名和 Spring Boot 3 的jakarta不兼容,你会编译出一堆红色报错,纯属折腾。

除了运行环境,你最好准备一个 MySQL 客户端(Navicat 或 DBeaver 都行)和一个能看 JSON 的接口测试工具。为什么强调这两个?因为后端跑起来以后,你要用接口工具先打http://localhost:8080/api/bank/list看返回数据,确认后端正常再调小程序,否则你根本分不清问题出在哪一层。

3.2 数据库初始化:别急着改application.yml,先导脚本

打开数据库脚本,先创建一个名为shuati(或项目自带的名字)的数据库,字符集选utf8mb4,排序规则选utf8mb4_general_ci,然后导入sql文件。导入后别急着跑,先看几行表结构和几条数据,确认题目表里确实有内容。我见过不止一次:脚本本身没有CREATE DATABASE,你直接跑就全执行在默认库里了,后面对不上。

接下来看application.yml(或application.properties),改数据源。常见配置长这样:

server: port: 8080 servlet: context-path: /api spring: datasource: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3306/shuati?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai&useSSL=false username: root password: 123456 mybatis: mapper-locations: classpath:mapper/*.xml type-aliases-package: com.example.shuati.entity

这里有两个参数最容易改错。第一,context-path是/api,那你所有接口都带/api前缀,小程序端request.js里的 baseUrl 如果没带,就会 404。第二,serverTimezone必须要设置,MySQL 8 默认时区和本地不同,不配可能直接报The server time zone value 'Öйú±ê׼ʱ¼ä' is unrecognized。JVM 里的user.timezone不一定管用,直接在连接串写死最省心。

3.3 后端启动:两种方式,本地开发建议用 IDEA

如果你装了 IDEA,直接 File -> Open 选springboot目录,等 Maven 把依赖下载完,然后找到主类(@SpringBootApplication注解的那个),右键 Run。这种方式的好处是你能在控制台看到完整的启动日志,排查问题最方便。

如果你更习惯命令行,也可以进到目录里执行:

mvn clean package -DskipTests java -jar target/shuati-0.0.1-SNAPSHOT.jar

启动成功的标志不是“没有报错”,而是看到类似Tomcat started on port 8080和Started Application in XX seconds。注意-DskipTests是跳过测试,如果项目里有测试类但你本地环境缺东西,不跳会卡在测试阶段,跳过它能让你更快看到结果。

3.4 小程序端导入与配置:先跑通模拟器再上真机

用微信开发者工具导入miniprogram目录,导入时它会要求填 AppID。如果你只是本地验证,可以直接点“测试号”,不需要自己注册小程序账号。这一步很多新手不知道,导致卡在 AppID 上,其实测试号完全够用。

导入后第一件事,打开utils/request.js,找到类似这样的地方:

const BASE_URL = 'http://localhost:8080/api'; function request(url, method, data) { return new Promise((resolve, reject) => { wx.request({ url: BASE_URL + url, method: method || 'GET', data: data || {}, header: { 'Content-Type': 'application/json' }, success: resolve, fail: reject }); }); } module.exports = { request };

BASE_URL就是后端地址。我用的是localhost:8080/api,这个在微信开发者工具里没问题,但真机调试时手机访问不到电脑的 localhost,要改成你电脑在局域网里的 IP,比如http://192.168.1.5:8080/api。另外Content-Type用application/json,如果你的后端方法签名是接收实体类,这个没问题;如果后端接收的是表单参数,这里就得改成application/x-www-form-urlencoded,这是前后端联调最常见的分歧点。

4. 刷题核心链路:题目加载、答题判定、错题记录与进度统计的实现拆解

4.1 小程序端请求封装与登录态:代码的喉咙口

刚才我提了request.js,这里再深入一点。刷题系统里所有页面都会调这个封装,它通常不只是wx.request的简单包装,还会带 token。常见的写法是在请求头里塞一个自定义字段,比如Authorization:

function request(url, method, data) { const token = wx.getStorageSync('token'); return new Promise((resolve, reject) => { wx.request({ url: BASE_URL + url, method: method || 'GET', data: data || {}, header: { 'Content-Type': 'application/json', 'Authorization': token ? `Bearer ${token}` : '' }, success: (res) => { if (res.statusCode === 401) { wx.navigateTo({ url: '/pages/login/login' }); reject(res); } else { resolve(res.data); } }, fail: reject }); }); }

为什么强调这个文件?因为这套系统里“用户身份”完全靠这个 token 识别。你在小程序端登录一次,后端返回 token,存进 storage,之后所有接口都带它。如果后端有拦截器校验 token,你直接调接口不带 header,返回的就不是题目而是 401。所以排查“为什么接口全报错”时,我一般先看这里。

4.2 题库与题目列表的分页加载:别一次捞全量数据

刷题系统的题目量可能不大,但设计上通常会做分页。后端接口常见的实现是 MyBatis 加上 PageHelper 分页插件。先看pom.xml里有没有pagehelper-spring-boot-starter,有的话,Service 层代码大概长这样:

@Override public PageInfo<Question> getQuestionList(Long bankId, int pageNum, int pageSize) { PageHelper.startPage(pageNum, pageSize); List<Question> questions = questionMapper.selectByBankId(bankId); return new PageInfo<>(questions); }

PageHelper.startPage(pageNum, pageSize)是分页开关,它写在查询语句前面,底层会在执行 SQL 前自动拼接LIMIT。注意这里有个经典坑:startPage只对紧接着的下一条查询生效,如果你在它和select之间塞了别的查询,分页就跑到别的语句上去了,结果页面怎么翻都是同一批数据。

小程序端对应的处理也简单:onReachBottom里把pageNum加 1,再请求下一页,把返回的数组concat到原有列表后。微信小程序的页面没有浏览器滚动事件,用onReachBottom是最现成的方案。

4.3 提交答案与判定逻辑:比对答案的时机和事务

题目提交是这套系统的核心。我看过的代码里,判定逻辑大致有两种:一种是前端判,就是提交时直接比对option字段,这种最简单但容易作弊;另一种是后端判,把用户答案传到后端,后端从库里取标准答案再比对。这份资源大概率是后者,代码类似:

@Transactional public SubmitResult submitAnswer(AnswerSubmitDTO dto) { Question question = questionMapper.selectById(dto.getQuestionId()); if (question == null) { throw new ServiceException("题目不存在"); } boolean correct = question.getAnswer().equalsIgnoreCase(dto.getAnswer()); AnswerRecord record = new AnswerRecord(); record.setUserId(dto.getUserId()); record.setQuestionId(dto.getQuestionId()); record.setBankId(question.getBankId()); record.setIsCorrect(correct ? 1 : 0); record.setAnswerContent(dto.getAnswer()); answerRecordMapper.insert(record); if (!correct) { WrongQuestion wrong = new WrongQuestion(); wrong.setUserId(dto.getUserId()); wrong.setQuestionId(dto.getQuestionId()); wrong.setWrongCount(1); wrong.setLastWrongTime(new Date()); wrongQuestionMapper.insert(wrong); } return new SubmitResult(correct, question.getAnswer(), question.getAnalysis()); }

参数说明:AnswerSubmitDTO里必须有userId、questionId、answer三个字段;question.getAnswer()是标准答案,equalsIgnoreCase是为了让你填a或A都算对。@Transactional放在这里很关键——如果插答题记录成功但插错题记录失败,事务回滚,不会出现“这道题明明做错了但错题本里没有”的脏数据。

再看WrongQuestion的插入。很多项目的错题表是有唯一约束的,用user_id和question_id做联合唯一键。如果这套代码里有,那插入错题前应该先查一下是否已存在,存在就update wrong_count而不是继续insert,否则第二次错同一题就主键冲突。你看源码时专门找这个逻辑就行。

4.4 错题本与统计接口:SQL 的聚合和懒加载

错题列表接口不复杂,就是SELECT * FROM wrong_question WHERE user_id = ?。但如果你点进错题详情想看到题目内容,后端会再做一次question_mapper.selectById。这时候要注意 N+1 查询问题:如果错题有 50 条,代码在循环里查 50 次题,接口响应会明显变慢。正常做法是查出错题后收集所有question_id,再用WHERE id IN (...)一次查回来,在内存里组装。

统计接口更依赖 SQL 聚合。比如要算正确率,可能会写这样的 Mapper:

<select id="selectCountByUserId" resultType="map"> SELECT COUNT(*) AS total, SUM(CASE WHEN is_correct = 1 THEN 1 ELSE 0 END) AS correct FROM answer_record WHERE user_id = #{userId} </select>

这里SUM(CASE WHEN ...)是在 MySQL 里做条件计数,返回一个Map,Service 层拿到total和correct,算出正确率。注意total有可能是 null(没有记录时SUM返回 null),拿到 Java 里要先判空再转 int,否则拆箱直接NullPointerException。这段逻辑在小程序端统计页面能直接看到效果,也是我最常建议新手先看的代码。

5. 避坑与排查:把 Spring Boot 刷题系统跑起来最常见的五个翻车点

5.1 小程序请求报错request: fail url not in domain list

现象:后端启动正常,接口工具也能通,但在微信开发者工具里点刷新,控制台报url not in domain list。

原因:微信小程序对网络请求有域名白名单校验,开发工具默认不校验,但如果你勾了“校验合法域名”,它会拦所有http://的本地地址。

解决:在微信开发者工具右上角“详情 -> 本地设置”里,勾上“不校验合法域名、web-view(业务域名)、TLS 版本以及 HTTPS 证书”。这是本地调试的后悔药,真机预览时再按需配置个人域名的 HTTPS 证书。

5.2Tomcat started on port 8080但接口全 404

现象:启动日志没有报错,可访问/api/bank/list返回 404,而不是预期的 JSON。

原因:多半是context-path没对上。后端配置是/api,但你在浏览器里访问的是http://localhost:8080/bank/list,或者小程序端BASE_URL写成http://localhost:8080,漏了/api。接口工具调试时最容易踩这个,因为请求地址是拼接的,少一段就看不出问题。

解决:统一三处的路径:application.yml里的servlet.context-path、小程序端BASE_URL、接口工具的请求前缀。排查时先看浏览器访问http://localhost:8080/api/bank/list是否通,如果通就专心改前端;不通就去看 Swagger 文档里接口的 RequestMapping 有没有额外前缀。

5.3 数据库连上了,但中文全显示成?问号

现象:小程序端题目内容里的中文变成一串问号,或者后端日志插入 MySQL 时出现Incorrect string value报错。

原因:数据库表字符集是latin1,或者连接参数没加characterEncoding=utf8。老项目的sql脚本里可能没有DEFAULT CHARSET=utf8mb4,导入时继承了数据库的默认字符集。

解决:建库时记住用utf8mb4,连接串写死characterEncoding=utf8。如果表已经建了,执行ALTER TABLE question CONVERT TO CHARACTER SET utf8mb4;改表和字段的字符集。注意改完好重新导入数据,否则表中已存坏的问号不会自己恢复。

5.4 模拟器能跑,真机一打开就白屏或接口超时

现象:开发者工具里一切正常,点“真机预览”后,手机上小程序打开,页面加载不出列表,过一会超时。

原因:手机访问localhost:8080访问的是手机自己,不是你的电脑。你电脑上后端监听 8080,手机必须用电脑的局域网 IP 才能访问,同时 Windows 防火墙默认会挡掉来自局域网的入站连接。

解决:先用ipconfig查电脑的 IPv4 地址,把request.js里的BASE_URL改成http://<局域网IP>:8080/api;再检查防火墙,或者直接在弹窗里允许 Java 通过专用网络。同一 Wi-Fi 下手机和电脑必须在一个网段,公司网络如果有 AP 隔离,就改用手机热点来真机调试。

5.5 微信登录报invalid appid或code2Session失败

现象:首次进小程序就卡在登录页,网络请求到/api/user/login,后端日志显示code2Session返回errCode: 40013。

原因:微信小程序的 AppID 和密钥是绑定的,你用测试号发起登录,后端却用自己申请的 AppID 去调微信接口,或者wx.login返回的 code 失效了。

解决:如果你只是验证功能,就统一用测试号,测试号的 AppID 不能调真实微信登录接口,但后端可以做一步“模拟登录”:直接把code当作openid来用,比如openid = "test_" + code。这套系统的源码里通常留着这个开关,你搜code2Session或mock注释就能找到,把 mock 开关打开就行。要是你接到了真实项目,就一定得配成你注册的小程序 AppID 和 Secret,并且把app.js里的 AppID 也改成同一个。

6. 进阶:给这套刷题系统加“错题重刷”和“模拟考试”两个小功能

6.1 错题重刷:把错题表当成随机题库

错题本里的题如果只是列表展示,练的意义就打折了。我一般会给后端加一个randomWrongList接口,从错题表关联题目表随机取 N 道:

@GetMapping("/wrong/random") public List<Question> randomWrong(@RequestParam Long userId, @RequestParam int limit) { return questionMapper.selectRandomWrong(userId, limit); }

对应 Mapper 里的 SQL 关键词就是ORDER BY RAND()加LIMIT。题目量不大时这样写没毛病,量大了要换随机算法,但演示项目完全够用。小程序端刷完这批错题,还可以在提交时调用同一个提交接口,答对了就从错题表删掉,形成“错题清零”的闭环。

6.2 模拟考试:前端倒计时与超时交卷

模拟考试的核心是限制时间。前端页面进入时设置一个总时长,比如 30 分钟,用setInterval每秒减 1,到 0 时触发提交。提交时把所有答案组装成一个数组,批量调提交接口。注意时间组件卸载时要clearInterval,不然页面跳走了定时器还在跑,会出现跳转后依然弹窗的玄学问题。

let timer = null; const startExam = () => { let left = 30 * 60; timer = setInterval(() => { left--; if (left <= 0) { clearInterval(timer); submitAllAnswers(); } }, 1000); };

这套系统本身是完整的,但它的代码风格比较传统,适合学习也适合二次改。你可以顺着它往里面加“每日一题”“排行榜”“刷题日历”这些模块,接口和页面的套路都一样。说句心里话,像这种带前后端完整闭包的项目,最值得做的不是原样跑通,而是先在本地把这条链路摸熟,再按自己的业务改。我每次拿到一个 Spring Boot + 小程序的项目,都会强制自己先把request.js的 baseUrl、数据库脚本、启动日志三步走完,再谈改功能。希望帮到你。

本文还有配套的精品资源,点击获取

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/25 1:30:48

深入SP3232:双通道TTL转RS232电平转换实战解析

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/25 1:30:27

华为AP4050DN FIT转FAT刷机教程:console线+TFTP自救指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/25 1:27:32

嵌入式TIM定时器组件化封装:从原理到实践

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/25 1:27:29

ESP32C3烧录LuatOS固件避坑指南:从环境搭建到点灯实战

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/25 1:26:37

VS Code插件这样配 TaoToken,settings.json 骨架一次到位

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/25 1:26:24

肖特基二极管AM检波电路设计:从选型到ADS仿真与实测

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华