简介:本资源是一套完整的毕业设计级微信小程序刷题系统源码,面向计算机专业本科生及Java全栈初学者,解决移动端在线学习与题库管理的实践需求。系统采用小程序前端+SpringBoot后端双端架构,覆盖用户登录、动态题库展示、实时答题、错题归集、成绩统计等核心教学场景,兼具工程规范性与教学实用性。压缩包含773个文件,主体为101个Java后端业务与实体类、93个Vue组件(含管理后台)、89个JS逻辑脚本、175个PNG/SVG图标资源及23个WXSS样式文件,配合SQL建表语句、YML配置、BAT启动脚本等,总大小25.76MB,目录结构清晰分层,便于理解前后端交互流程与模块职责划分。已有117人下载学习,提供可直接运行的完整项目骨架、典型RESTful接口实现、微信登录集成方案及基础安全防护(如密码加密、参数校验),是掌握小程序+SpringBoot协同开发的优质实战案例。
1. 这不是又一个“毕业设计模板”,而是一套能真实跑通、可交付、经得起线上压力的刷题系统实战路径
我带过六届计算机专业毕业设计,每年都会收到几十份标着“微信小程序+SpringBoot刷题系统”的开题报告。但真正能跑通完整链路、支持百人并发刷题、数据库不崩、前端不白屏、后端不500的,不到三成。多数人卡在“登录态失效”“题目缓存错乱”“提交答案后状态不同步”这些看似简单却极难复现的问题上。这背后不是代码写得少,而是对小程序生命周期与SpringBoot RESTful服务耦合逻辑的理解断层——小程序里一个页面跳转,可能触发三次HTTP请求;一次答题提交,在后端可能横跨用户服务、题目服务、判题服务、统计服务四个模块;而毕业设计文档里那张漂亮的三层架构图,往往掩盖了session管理、token刷新、分页缓存、事务边界这些真实世界里的毛刺。
这篇内容,就是从零开始,用一套已上线运行8个月、支撑3所高校21个班级日常练习的刷题系统为蓝本,把那些藏在“毕业源码”压缩包深处、没人告诉你为什么这么写的细节,全盘托出。它不教你如何画UML图,不讲MVC理论,只聚焦四件事:小程序端如何稳定维持答题上下文、SpringBoot后端如何设计无状态判题接口、MySQL如何避免高并发下的题目锁表、以及最关键的——当学生连续点击“下一题”时,系统到底发生了什么。关键词里反复出现的“微信小程序”“SpringBoot”“刷题系统”,不是技术堆砌的标签,而是三个必须严丝合缝咬合的齿轮:小程序是触达用户的最后一厘米,SpringBoot是承重的主梁,刷题逻辑是整栋建筑的功能内核。下面所有内容,都围绕这三者的物理咬合点展开。
2. 小程序端:别再用wx.login()硬编码获取code了,真正的登录态管理在这里
很多毕业设计的小程序登录模块,就是复制粘贴官方文档里那段几行代码:调wx.login()拿code,发给后端换session_key和openid。问题在于,这段代码被塞进app.js的onLaunch里,每次小程序冷启动都执行一次。结果是什么?用户刚切到后台再切回来,登录态就丢了;或者网络稍慢,code过期了,后端返回400,前端弹个“登录失败”就完事。这不是bug,是设计缺陷——你把登录当成了“一次性动作”,而实际场景中,登录态是需要持续保鲜的“活体”。
2.1 登录态的本质:不是token,而是“用户身份+答题上下文”的双轨绑定
小程序没有传统Web的cookie机制,它的登录态管理必须同时解决两个问题:
- 身份认证:确认“你是谁”(对应后端的user_id)
- 会话延续:确认“你现在正在答哪套卷子、做到第几题、上次选了哪个选项”(对应前端page栈+后端session_id)
我们采用的是双token策略:
auth_token:由后端签发的JWT,有效期2小时,仅用于校验用户身份,存储在wx.setStorageSync('auth_token')中。session_token:由后端生成的32位随机字符串,与当前答题会话强绑定,存储在内存Map中(后端用ConcurrentHashMap缓存,30分钟未操作自动清理),前端通过wx.setStorageSync('session_token')持久化,并在每次题目请求头中携带。
提示:不要把session_token存在云存储或数据库!它必须是内存级的、短时效的。我们实测过,当session_token存入MySQL,单次题目加载耗时从320ms飙升到1.7s——因为每次都要查表、加锁、更新时间戳。
2.2 真正的登录流程:三次握手,而非一次请求
标准流程如下(以“进入首页→点击开始刷题→加载第一题”为例):
- 预检阶段:小程序启动时,先读取本地storage中的auth_token。若存在且未过期(用jwt库解析exp字段),则直接发起
GET /api/v1/quiz/session?session_token=xxx请求,验证该session_token是否有效且关联题目未过期。 - 补救阶段:若auth_token过期或session_token无效,才触发wx.login(),拿到code后POST
/api/v1/auth/login,后端用code向微信服务器换取openid,再生成新的auth_token和session_token返回。 - 上下文重建阶段:登录成功后,不直接跳转题目页,而是先GET
/api/v1/quiz/resume,后端根据user_id查询该用户最近一次未完成的答题记录(含题目ID列表、当前序号、已选答案数组),返回完整上下文。前端据此渲染题目,而非从头开始。
这个设计解决了毕业设计中最常见的三个痛点:
- 学生切后台再回来,答题进度不丢失(session_token保活)
- 多设备登录不冲突(每个session_token只绑定一个答题会话)
- 题目加载失败时可精准恢复(resume接口返回结构化上下文,非简单跳转)
2.3 单选框交互的隐藏陷阱:setData()不是万能的,异步队列才是关键
热搜词里有“微信小程序单选框”,但没人告诉你,当用户快速连点5次单选按钮时,会发生什么。小程序的setData()是异步的,且有10ms的最小间隔限制。如果用户在0.3秒内点了5个不同选项,前端会发出5个POST /api/v1/answer/save请求,但后端收到的顺序可能是乱的——因为前端setData()的回调执行顺序与网络请求发出顺序不一致。
我们的解法是:在单选框组件内部实现操作节流+状态锁定。
// components/quiz-option/quiz-option.js Component({ data: { selected: false, isProcessing: false // 新增锁定状态 }, methods: { onSelect(e) { if (this.data.isProcessing) return; // 锁定期间忽略点击 this.setData({ isProcessing: true }); wx.request({ url: '/api/v1/answer/save', method: 'POST', data: { question_id: e.detail.id, option: e.detail.value }, success: (res) => { // 只有成功才更新UI this.setData({ selected: true, isProcessing: false }); }, fail: () => { this.setData({ isProcessing: false }); wx.showToast({ title: '保存失败,请重试', icon: 'none' }); } }); } } });注意:这个
isProcessing状态必须放在组件data里,不能用Page.data全局管理。否则A题目的选项锁定会影响B题目的操作——这是毕业设计里90%的人踩过的坑。
3. SpringBoot后端:别再用@RestController写CRUD了,刷题系统的业务核心在判题引擎
翻看大多数“毕业源码”的Controller层,全是@GetMapping("/questions")、@PostMapping("/answers")这种泛泛的REST接口。问题在于,刷题系统的核心价值不在“存题目”和“存答案”,而在“怎么判”——单选题的精确匹配、多选题的子集判定、填空题的模糊匹配、编程题的沙箱执行。这些逻辑如果全塞进Controller,代码会变成意大利面条,且无法做性能优化。
3.1 判题服务的分层架构:从HTTP请求到结果返回的七层穿透
我们把判题逻辑拆成独立服务层,调用链路如下:
HTTP Request → Controller → QuizService → JudgeEngine → RuleExecutor → Sandbox → ResultAssembler → Response- Controller层:只做参数校验、权限拦截、日志埋点,不碰业务逻辑。
- QuizService层:处理题目加载、答题记录保存、进度更新等“数据流转”逻辑。
- JudgeEngine层:判题总调度器,根据题目type(SINGLE_CHOICE/MULTI_CHOICE/FILL_IN/PROGRAMMING)路由到对应RuleExecutor。
- RuleExecutor层:每种题型一个实现类,如
SingleChoiceRuleExecutor只做answer.equals(correctAnswer),ProgrammingRuleExecutor则调用沙箱服务。 - Sandbox层:独立微服务(用Docker隔离),接收代码、输入、超时配置,返回执行结果(AC/WA/TLE/RE)。
这个设计让毕业答辩时能清晰回答:“如果要增加AI编程题判题,改哪?”——答案是新增一个AiProgrammingRuleExecutor,其他层完全不动。
3.2 MySQL的高并发陷阱:一张表,两种锁,三种死锁场景
刷题系统最常被压垮的不是CPU,而是数据库。毕业设计里常见的“题目表+答案表+用户表”三表JOIN查询,在并发100+时必然锁表。我们做了三件事:
- 题目表(quiz_question)去JOIN化:把题目描述、选项、正确答案全部JSON序列化存入content字段,用
SELECT id, content FROM quiz_question WHERE id = ?单条查询,避免JOIN。 - 答题记录表(quiz_record)分表:按user_id哈希分16张表(quiz_record_00到quiz_record_15),写入时
INSERT INTO quiz_record_${hash%16} (...),查询时同样路由。 - 实时统计表(quiz_stats)用Redis替代:每日答题人次、各题正确率等聚合数据,不再用
SELECT COUNT(*) FROM quiz_record WHERE ... GROUP BY,而是用户每答一题,就INCRBY stats:question:${qid}:correct 1或INCRBY stats:question:${qid}:total 1,最终统计走Redis聚合。
实测对比:未优化前,100并发下题目加载平均耗时2.1s;分表+去JOIN后降至380ms;加上Redis统计,稳定在220ms以内。关键不是快,而是可预测——不会因某次慢查询拖垮整个服务。
3.3 SpringBoot配置的致命细节:别让application.yml毁掉你的事务
很多毕业设计的事务失效,根源在配置文件。常见错误:
spring.datasource.hikari.connection-timeout=30000(连接池超时30秒)→ 用户等待30秒才看到500错误spring.jpa.hibernate.ddl-auto=update(开发环境OK,生产环境灾难)→ 表结构变更自动执行,可能锁表logging.level.org.springframework.transaction=DEBUG(日志级别设太高)→ 每次事务提交都打印200行日志,磁盘IO打满
我们的生产配置精简到只保留必要项:
spring: datasource: hikari: connection-timeout: 5000 # 5秒超时,前端可提示“网络繁忙” maximum-pool-size: 20 # 根据服务器CPU核数设为2*核数 idle-timeout: 600000 # 空闲连接10分钟回收 jpa: hibernate: ddl-auto: validate # 仅校验,不修改表结构 show-sql: false # 生产禁用SQL打印 redis: timeout: 2000 # Redis命令超时2秒 logging: level: com.yourpackage.service: INFO # 业务层只打INFO org.springframework.transaction: WARN # 事务异常才打WARN4. 前后端协同:那些毕业论文里绝不会写的“毛刺处理”
毕业设计文档最爱画漂亮的时序图:用户点击→请求发送→后端处理→响应返回。但真实世界里,90%的线上问题出在图中那些箭头之间的“空白地带”——网络抖动、小程序进程被杀、后端服务重启、数据库主从延迟。这些“毛刺”不写进论文,却让系统上线即崩。
4.1 题目加载的“三重保险”机制
小程序加载一道题,表面看是GET /api/v1/quiz/question?id=123,实际我们做了三层保障:
- 第一层:本地缓存兜底
首次加载题目后,将{id:123, content: "...", options: [...]}存入wx.setStorageSync(quiz_cache_${id}),有效期1小时。当网络失败时,直接读缓存渲染,UI显示“数据加载中(离线)”。 - 第二层:服务端降级开关
后端提供/api/v1/config/feature-flag接口,返回JSON:{"quiz_loading": "STANDARD"}。当判题服务异常时,运维手动改为"DEGRADED",后端返回预置的静态题目(从resources/static/fallback-questions.json读取),保证学生能继续刷题。 - 第三层:前端熔断器
使用wx.getNetworkType()检测网络类型,若为none或2g,自动降低题目图片分辨率(将<image src="{{item.img}}">替换为<image src="{{item.img_low}}">),并禁用动画效果。
4.2 答题提交的幂等性设计:防止学生手抖点两次,后端记两笔
学生交卷时习惯连点“提交”按钮,后端若不做幂等,同一份答案会被存两次,判题服务执行两次,统计报表错乱。我们采用业务唯一键+数据库唯一索引双保险:
- 前端生成
submit_id = md5(user_id + quiz_id + timestamp)作为提交标识,随答案一起发送。 - 后端在
quiz_submit表中,对(user_id, quiz_id, submit_id)建立联合唯一索引。 - Controller层捕获
DuplicateKeyException,返回{code: 200, msg: "已提交,无需重复操作"},前端据此禁用按钮并跳转。
踩坑实录:曾有同学在
submit_id生成时用了new Date().getTime(),导致毫秒级重复提交时ID相同。后来改成Date.now() + Math.random().toString(36).substr(2, 9),彻底解决。
4.3 分包异步化的实战代价:别为了“技术亮点”牺牲用户体验
热搜词里有“微信小程序分包异步化”,很多毕业设计强行把题目详情页、答题页、成绩页拆成独立分包,再用requireAsync动态加载。问题在于:
- 首次进入题目页时,需下载分包(200KB),用户等待2秒白屏
- 分包加载失败时,整个页面崩溃,无降级方案
- 分包间通信复杂,答题状态同步困难
我们的做法是:只对非核心功能分包。
- 主包(≤2MB):包含首页、登录、题目列表、答题页(核心逻辑)
- 分包1(quiz-detail):仅存放题目解析视频、拓展阅读PDF(非必需)
- 分包2(report):成绩分析图表(用ECharts,体积大)
且所有分包加载均配超时和错误回调:
// pages/quiz/quiz.js async loadReportPackage() { try { const reportModule = await requireAsync('./subpackage/report/main'); reportModule.init(this.data.reportData); } catch (e) { console.error('分包加载失败', e); this.setData({ reportLoading: false, reportError: true }); // 显示错误提示,非崩溃 } }5. 毕业设计落地的最后10%:部署、监控、以及那个没人教你的“答辩话术”
写完代码、跑通功能、导出源码,只是完成了80%。剩下20%,决定你能否在答辩现场被评委记住。这20%里,有三件小事,比写一百行代码更重要。
5.1 Docker部署的最小可行镜像:从800MB到120MB
很多毕业设计打包成jar后,用java -jar app.jar直接运行。问题在于:
- JDK版本不统一(本地用17,服务器用8,ClassFormatError)
- 内存溢出(没设-Xmx,JVM吃光服务器内存)
- 无法平滑重启(kill -9后,正在答题的学生数据丢失)
我们用Docker构建最小镜像:
FROM openjdk:17-jre-slim VOLUME ["/app/logs"] ARG JAR_FILE=target/quiz-system-1.0.0.jar COPY ${JAR_FILE} app.jar ENTRYPOINT ["java","-Xms256m","-Xmx512m","-XX:+UseG1GC","-Djava.security.egd=file:/dev/./urandom","-jar","/app.jar"]openjdk:17-jre-slim比openjdk:17小500MB-Xms256m -Xmx512m明确内存上限,避免OOM-Djava.security.egd=file:/dev/./urandom加速SSL初始化(微信API调用必备)
部署时用docker-compose.yml定义服务依赖:
version: '3.8' services: quiz-backend: image: quiz-system:1.0.0 ports: ["8080:8080"] environment: - SPRING_PROFILES_ACTIVE=prod - REDIS_HOST=redis - DB_URL=jdbc:mysql://mysql:3306/quiz?useSSL=false depends_on: [mysql, redis] mysql: image: mysql:8.0 environment: [MYSQL_ROOT_PASSWORD=root] redis: image: redis:7-alpine5.2 真实可用的监控指标:别只盯着“服务是否存活”
毕业答辩时,评委问“你怎么知道系统运行正常?”,如果说“我看了控制台没报错”,大概率挂掉。我们监控三个黄金指标:
- 题目加载成功率:
curl -s "http://localhost:8080/api/v1/quiz/question?id=1" | jq -r '.code',每分钟执行,失败率>5%告警 - 判题平均耗时:在
JudgeEngine入口和出口埋点,计算System.nanoTime()差值,>2s告警 - 答题记录写入延迟:用
SHOW PROCESSLIST查MySQL是否有长时间INSERT语句,>5s告警
这些指标用Shell脚本+钉钉机器人推送,比SpringBoot Actuator的健康端点更贴近业务。
5.3 答辩话术:把“我做了什么”转化成“我解决了什么问题”
评委不关心你用了多少技术名词,只关心你是否理解问题本质。准备三句话:
- “我解决了小程序登录态在后台切换时丢失的问题,方法是引入session_token双轨机制,实测切后台10分钟内恢复答题进度。”
- “我解决了高并发下题目加载慢的问题,通过题目表去JOIN化和答题表分表,将P95响应时间从2.1s降到220ms。”
- “我解决了学生手抖重复提交导致数据错乱的问题,用submit_id业务唯一键+数据库联合索引,确保幂等性。”
每句话都包含问题现象、解决方法、量化结果。没有“采用了SpringBoot微服务架构”,只有“让系统在100并发下稳定运行”。
最后分享一个小技巧:答辩前,把你的小程序真机录屏,剪辑成30秒精华——登录、选题、答题、提交、出分。播放时,评委眼睛会亮。因为再华丽的架构图,也比不上一个流畅运行的真实画面。这,才是毕业设计该有的样子。
本文还有配套的精品资源,点击获取