简介:面向计算机及相关专业毕业设计,这是一套完整的MHK在线学习微信小程序项目,基于微信开发者工具、Java与MySQL实现,包含学生端和管理员端双角色。学生可注册登录后使用听力、阅读、书面表达、口试材料等在线训练,完成三级或四级模拟考试并自动记录分数、生成错题本;管理员能管理用户、资讯、试题、考试系统与在线训练,后台采用Vue界面,功能覆盖实际在线学习场景。压缩包共1289个文件,约44MB,主要包含小程序页面(wxml/wxss)、JS交互逻辑、Vue管理后台、Java后端及SQL数据库脚本,附有演示录像和说明文档,并提供了安装、运行、构建批处理脚本,可快速启动项目。已有327人学习下载,大量图片素材便于界面替换,适合作为毕业设计选题参考,也能帮助开发者快速熟悉小程序与Java后端整合的开发流程。
1. 别把 MHK 在线学习小程序做成“视频网站”:先想清楚毕设要交什么
「MHK 在线学习小程序」最容易翻车的做法,是把它做成一个塞满课程视频的“视频网站”。我见过不少毕设作品,页面堆了十几个,真到答辩时评委一句「你的核心数据从哪来?」就冷场。MHK(少数民族汉语水平等级考试)考生的真实需求其实很朴素:查词、刷题、看错题。一个能完整跑起来的词表学习 + 专项练习 + 模拟考试 + 错题本闭环,远比花哨的 UI 更能扛住追问。这篇文章从源码导入、数据库设计、答题判分到演示录像,一条线讲清怎么把毕设做出「能答辩」的完成度。适合正在做微信小程序毕设、又不想在答辩现场翻车的同学。
2. MHK 在线学习小程序的模块与表结构:先定功能边界,再动手建集合
2.1 从考生行为反推功能:四个模块串成一个数据闭环
「在线学习」这四个字太宽泛,落到 MHK 这个场景里,考生打开小程序真正想做的事只有四件:查词、刷题、模拟考、看错题。课程列表、视频播放、收藏、评论这类功能可以做,但不应该是主角。做毕业设计最忌功能铺太开,页面写了不少,核心路径反而没跑通,答辩时被问「你的系统解决了什么具体问题」就答不上来了。
我一般会先画一张考生使用路径图:打开小程序,按等级查词,做专项练习,发现错题,进入模拟考试,考完回到错题本重做。顺着这条路径,页面只需要四类,但每一步都在为下一步产生数据。这四类模块的设计目标如下表:
| 模块 | 对应页面 | 核心能力 | 产出的数据 |
|---|---|---|---|
| 词表学习 | 词汇页 / 搜索页 | 按三级、四级分组浏览,支持拼音搜索 | 搜索记录、收藏词表 |
| 专项练习 | 练习页 | 按听力、阅读、书写等题型刷题,即时判分 | 答题结果、错题记录 |
| 模拟考试 | 考试页 | 限时答题、自动交卷、生成成绩单 | 考试记录、错题明细 |
| 个人中心 | 我的页 | 错题本、学习统计、数据清理 | 学习统计 |
这张表对应的就是后面要设计的四个数据集合。数据闭环是整套设计的灵魂:练习答错,wrong_book 集合加一条;模拟考再错,wrongTimes 加一;重做正确,记录改为已掌握。答辩时把这个闭环画在说明书的架构图里,比放一大堆页面截图有说服力得多。哪怕界面像素级普通,只要数据闭环是真跑通的,评委就挑不出「功能是死的」这种毛病。
2.2 选型:云开发与自建后端的取舍
微信小程序的后端选型,常见路径就两条:一条是自建后端,自己写接口,小程序里用 wx.request 调用;另一条是微信云开发,用云函数当接口层,用集合当数据表。对毕业设计来说,这个选择直接决定答辩当天会不会因为环境问题翻车。
我见过太多翻车现场:本地后端没启动,数据库连不上,演示时接口超时,画面转圈圈。云开发的价值恰恰在于把这类环境风险降到最低。它不需要你自己部署服务,数据在云端,答辩现场只要手机能联网就能演示。两者对比如下:
| 维度 | 云开发 | 自建后端 |
|---|---|---|
| 环境准备 | 开发者工具里点开通即可 | 需要自己部署运行环境 |
| 演示稳定性 | 数据在云端,不怕本地服务挂掉 | 后端进程没起来就直接黑屏 |
| 用户体系 | 云开发自动带出用户标识 | 需要自己实现登录态和鉴权 |
| 代码工作量 | 页面逻辑为主,集合即表 | 额外写接口层、数据表、鉴权 |
| 评委观感 | 可能被认为偏“低代码” | 更能体现后端设计能力 |
我的建议是:除非导师明确要求必须自建后端,否则用云开发。它只是把后端能力包装成了集合和云函数,数据建模、业务逻辑、接口设计这些毕设要考察的东西一样都没少。为了打消“太简单”的顾虑,把每个云函数在说明书里按接口文档的格式写清楚——请求参数、返回结构、异常码都列出来,答辩时老师说“后端在哪”,直接翻说明书给他看即可。
注意:云开发不是黑匣子,集合权限、云函数日志都能在控制台看到。开通后先建空集合,跑通一次读写,再写业务代码,别一上来就堆逻辑。
2.3 集合设计:题库、词表、错题本与考试记录
数据库设计是这份源码里最值得写进说明书的部分。我按集合维度拆开讲,顺便说清每个字段为什么这样设计。题库集合直接决定前端渲染,设计时要想明白“这道题怎么存、怎么判分”。
// data/questions.example.js module.exports = [ { _id: 'q001', type: 'single', // single: 单选;multi: 多选;judge: 判断 subject: '听力', // 听力 / 阅读 / 书写 level: '四级', // 三级 / 四级 stem: '听录音,选择正确的答案。', options: ['图书馆', '食堂', '宿舍', '教学楼'], answer: 0, // 单选题答案用下标;多选题答案用数组,如 [0, 2] analysis: '原文提到“去图书馆借书”,故选 A' } ]这里有三个设计决策。第一,options 为什么不拆成 option_a、option_b 四列?因为 MHK 题目有单选、多选、判断,选项个数不固定,数组存起来最灵活,新增题型不用改表结构。第二,answer 为什么用下标而不是选项文本?判断题、单选题用下标判分最简单;多选题答案就是一个数组,判断逻辑后面会讲到。第三,分析字段 analysis 一定要有,这是「学习」功能的体现,答题后展示解析,功能就比普通刷题小程序完整了一大截。
词表集合和错题本集合的示例结构如下:
// data/words.example.js module.exports = [ { _id: 'w001', word: '锻炼', pinyin: 'duànliàn', meaning: '通过体育运动使身体强壮', level: '三级', example: '他每天早晨都去操场锻炼。' } ]// data/wrong_book.example.js module.exports = [ { _id: 'wrong001', _openid: '用户openid', // 云开发自动写入,标记错题归属 questionId: 'q001', wrongAnswer: 2, wrongTimes: 3, correctTimes: 0, lastWrongTime: '2024-05-01 10:00:00' } ]考试记录集合 exam_record 则记录每次模拟考的完整答卷:openid、总成绩、每题选项、每题对错、开始和交卷时间。这个集合的价值在于个人中心的学习统计页,能画出「最近一周做题量」曲线,属于便宜又出效果的亮点功能。最后提一个容易踩的细节:集合名全部用小写字母,云数据库集合名对大小写敏感,真机和模拟器环境不一致时,大小写命名会让排查变得很痛苦。
3. 用微信开发者工具跑通源码:导入、云开发配置与真机预览
3.1 导入源码:目录结构、AppID 与编译检查
拿到源码压缩包后,第一步不是急着看代码,而是确认工程结构完整。常见的小程序工程会按下面这个目录组织,解压后对照检查即可。
mhk-study-miniprogram/ ├── project.config.json # 开发者工具项目配置,含 AppID ├── miniprogram/ │ ├── app.js # 小程序入口,云开发初始化在这里 │ ├── app.json # 页面注册、窗口样式、tabBar 配置 │ ├── app.wxss # 全局样式 │ ├── pages/ │ │ ├── index/ # 首页:学习中心 │ │ ├── word/ # 词表学习 │ │ ├── practice/ # 专项练习 │ │ ├── exam/ # 模拟考试 │ │ ├── wrong/ # 错题本 │ │ └── mine/ # 个人中心 │ ├── data/ │ │ ├── questions.js # 本地题库备份 │ │ └── words.js # 本地词表备份 │ └── images/ │ └── tabbar/ # 底部导航图标,只能用本地 png ├── cloudfunctions/ │ └── importData/ # 一次性批量导入题库的云函数 └── 说明文档.md # 毕业设计说明书思路稿检查完目录,打开微信开发者工具,选择「导入项目」,目录指向解压后的文件夹。AppID 可以先选「测试号」,把界面跑通后再换成自己注册的小程序 AppID。这里有个常见误区:换 AppID 后 project.config.json 里的 appid 字段会被工具自动改写,这不会影响源码结构,不需要手动去改文件。
编译完成后,如果模拟器没有正常出界面,先看调试器 Console 面板有没有红色报错。最常见的两种情况:app.json 里注册了页面但在 pages 目录下找不到对应文件,或者 tabBar 的路径写错了。页面路径是按 app.json 的 pages 数组加载的,新增页面却忘了注册时,编译器会直接报「页面未找到」,思路是先查 app.json 再查目录。
3.2 云开发配置:环境 ID、集合权限与批量导入脚本
模拟器跑通后,下一步开通云开发。在开发者工具顶部点「云开发」按钮,创建环境,环境名称可以用 mhk-demo 这类便于识别的名字。创建完成后,把环境 ID 复制到 app.js 的初始化代码里,这是整个配置流程里最容易被忽略的一步。
// miniprogram/app.js App({ onLaunch() { if (!wx.cloud) { console.error('请使用 2.2.3 及以上基础库以使用云能力'); } else { wx.cloud.init({ env: 'mhk-demo', // 云开发环境 ID,控制台复制过来 traceUser: true // 追踪用户访问,控制台可查 }); } } })不传 env 的情况下,云开发会用默认环境。本地只有一套环境时没问题,一旦控制台里建了多个环境,真机预览就可能连到错误的环境,报一堆集合不存在。显式指定 env 是最省心的习惯。云开发初始化时,如果基础库版本过低,wx.cloud 会是 undefined,代码里的判断就是为了在低版本基础库上给出明确提示,而不是白屏。
集合权限也在这里一起配掉。控制台里创建 question、word、wrong_book、exam_record 四个集合,权限建议选「所有用户可读,仅创建者可读写」。这样所有考生都能读到题库,但错题本和考试记录只有本人能改。题库的批量导入可以写一个一次性云函数,把本地题库 JSON 传上去循环插入,避免在控制台一条条手录。
// cloudfunctions/importData/index.js const cloud = require('wx-server-sdk') cloud.init({ env: cloud.DYNAMIC_CURRENT_ENV }) const db = cloud.database() exports.main = async (event, context) => { const questions = require('./questions.json') const count = await db.collection('question').count() if (count.total > 0) { return { skipped: true, reason: '题库已存在,防止重复导入' } } for (const item of questions) { await db.collection('question').add({ data: item }) } return { inserted: questions.length } }这段云函数先用 count 判断集合是否已有数据,防止每次部署都重复插入。循环里逐条 add,是因为云数据库的单次 add 只接受一条记录,批量插入最稳的方式就是循环。题库规模不大,几百条数据完全够用,云函数默认超时时间也足够。部署后在控制台运行一次,返回 inserted 数字就说明导入成功。
3.3 真机预览与界面配置:二维码、tabBar 与日志排查
配置完成后,点工具栏的「预览」,开发者工具会生成一个二维码,手机扫码就能在真机上打开小程序。真机预览和模拟器的最大差别在于网络环境、基础库版本和触摸交互,所以毕设演示前至少要扫一次二维码,确认在真实手机上不白屏。
真机白屏时,优先看手机端调试面板,也就是微信里的「真机调试」入口,而不是盯着模拟器。云开发相关的报错会出现在 Console 里,常见的提示是 environment not found 或者 collection not exists。前者就是环境 ID 没配好,后者是集合没创建或者名字写错。日志是最好的排错工具,别靠猜。
底部导航是这类小程序的门面,配置集中在 app.json 的 tabBar 字段。我给一份最小配置,拿到源码后对照你的页面路径改:四个 tab 分别对应学习、考试、错题、我的。
{ "tabBar": { "color": "#999999", "selectedColor": "#1296db", "list": [ { "pagePath": "pages/index/index", "text": "学习", "iconPath": "images/tabbar/study.png", "selectedIconPath": "images/tabbar/study-active.png" }, { "pagePath": "pages/exam/exam", "text": "考试", "iconPath": "images/tabbar/exam.png", "selectedIconPath": "images/tabbar/exam-active.png" }, { "pagePath": "pages/wrong/wrong", "text": "错题", "iconPath": "images/tabbar/wrong.png", "selectedIconPath": "images/tabbar/wrong-active.png" }, { "pagePath": "pages/mine/mine", "text": "我的", "iconPath": "images/tabbar/mine.png", "selectedIconPath": "images/tabbar/mine-active.png" } ] } }tabBar 的 list 至少 2 项、最多 5 项,iconPath 必须指向本地 png 文件,官方建议尺寸 81×81 像素、40KB 以内。图标不显示是 tabBar 配置里最典型的坑,通常不是代码逻辑问题,而是路径写错或者用了网络图片。修改完 tabBar 配置后一定要重新编译,如果放任不处理,演示时底部导航缺图标,看上去就像半成品。
4. 把题库“活”起来:刷题、判分与错题本的核心实现
4.1 题目数据源:云数据库优先,本地 JSON 兜底
题库是 MHK 在线学习小程序的心脏。数据源的选择直接影响演示效果:全部放云数据库,只要网络抖动就加载不出来;全部放本地,评委会觉得数据是写死的。我的习惯是两层都做,云数据库优先,本地 JSON 兜底,代码里加缓存。
// utils/quiz.js const localQuestions = require('../data/questions.js') async function getQuestions(subject, level) { // 缓存优先,避免每次进页面都拉一次网络 const cache = wx.getStorageSync(`questions_${subject}_${level}`) if (cache && cache.length > 0) { return cache } try { const db = wx.cloud.database() const res = await db.collection('question') .where({ subject, level }) .limit(20) // 小程序端单次最多返回 20 条 .get() wx.setStorageSync(`questions_${subject}_${level}`, res.data) return res.data } catch (e) { console.error('云端题库加载失败,退回本地题库', e) return localQuestions.filter(q => q.subject === subject && q.level === level) } }这段代码做了三件事:缓存优先减少网络请求,云端失败自动退回本地,每个题型按科目和等级分开缓存。云开发小程序端查询单次最多 20 条,如果题库超过这个数,用 skip 分页或者按题型分开拉取都行。毕设题库规模一般控制在 100 题以内,按听力、阅读、书写各取一部分,正好能满足评委「覆盖面够不够」的检查。
缓存键设计成questions_听力_四级这种组合,是为了避免不同科目之间的数据串台。退回到本地题库的 catch 分支往往会被忽略,但答辩现场网络不可控,这一行可能是整场演示的救命代码。顺手把 console.error 打出来,真出问题时能在真机调试面板里看到失败原因,而不是莫名空白。
4.2 判分逻辑:单选、多选与判断的统一处理
判分是整个小程序里最容易被写出问题的地方,尤其多选题。单选题直接比下标,判断题也一样;多选题要对答案数组排序后逐项比较。我把三种题型的判分收敛到一个函数里,页面调用时不用关心题型差异。
// utils/quiz.js function judgeAnswer(question, selected) { const answer = question.answer // 多选:selected 和 answer 都是数组 if (question.type === 'multi') { if (!Array.isArray(selected) || selected.length !== answer.length) { return { ok: false } } const sortedSelected = [...selected].sort() const sortedAnswer = [...answer].sort() return { ok: sortedSelected.every((item, index) => item === sortedAnswer[index]) } } // 单选、判断:answer 是数字下标,selected 需要转成数字 return { ok: Number(selected) === Number(answer) } }这里有个血泪经验:小程序里 picker、checkbox 组件返回的 selected 很可能是字符串,比如'0'而不是数字0,直接和数字答案比较会永远判错。所以函数最后一行做了Number(selected)强转。题型的区分靠 question.type 字段,这个字段在导入数据时就要保证规范,单选 single、多选 multi、判断 judge,页面渲染和判分逻辑都围绕它展开。
判分之后紧接着是错题回收,这是 MHK 在线学习小程序区别于普通刷题工具的关键功能。我习惯把它单独抽成一个云数据库操作函数,答错时自动调一次。
// utils/quiz.js async function saveWrong(question, wrongAnswer) { const db = wx.cloud.database() const _ = db.command const existed = await db.collection('wrong_book') .where({ _openid: '{openid}', questionId: question._id }) .get() if (existed.data.length > 0) { // 已有错题记录,错误次数 +1 await db.collection('wrong_book').doc(existed.data[0]._id).update({ data: { wrongTimes: _.inc(1), lastWrongTime: db.serverDate() } }) } else { // 第一次错,插入新记录 await db.collection('wrong_book').add({ data: { questionId: question._id, wrongAnswer, wrongTimes: 1 } }) } }_openid: '{openid}'是云开发的保留写法,查询时自动过滤为当前用户的记录,不需要自己维护用户标识。_.inc(1)是原子自增操作,多次答题并发写入时不会丢数据。这里我特意用了「先查再改」而不是「直接插入」,避免了同一道题在错题本里出现多条记录的情况。数据闭环就靠这个逻辑维持:同一道题反复错,wrongTimes 一直涨,个人中心里能画出一条「薄弱知识点」统计。
4.3 模拟考试:计时器、自动交卷与成绩单
模拟考试模块的卖点是「限时 + 自动交卷 + 成绩单」。计时器用 setInterval 每秒减一秒,时间归零自动交卷。实现不复杂,但要注意页面生命周期——退出考试页时必须把计时器清掉,否则 setInterval 还在后台跑,再次进入页面会叠出两个计时器。
// pages/exam/exam.js const EXAM_CONFIG = { totalMinutes: 10, // 正常考试 10 分钟 autoSubmit: true // 时间到自动交卷 } startExam(questions) { const totalSeconds = EXAM_CONFIG.totalMinutes * 60 this.setData({ remain: totalSeconds, questions }) this.timer = setInterval(() => { const next = this.data.remain - 1 if (next <= 0) { clearInterval(this.timer) this.autoSubmit() // 超时自动交卷 return } this.setData({ remain: next }) }, 1000) } onUnload() { if (this.timer) { clearInterval(this.timer) // 页面退出必须清理计时器 } }一个值得说明的参数设计:总时长放在 EXAM_CONFIG 对象里,而不是写死在 setInterval 逻辑里。这样做的好处是演示录像时可以临时把时长改成 1 分钟,快速跑完「开考→交卷→出成绩」全流程,而不需要改业务代码。平时完整考试 10 分钟,录像时 60 秒,同一套代码,两种演示节奏。
交卷后生成成绩单,写入 exam_record 集合。成绩单里除了总分,我把每道题的 selected、correct、耗时都存进去,这样错题明细能直接关联到 wrong_book,个人中心的统计页也能按天聚合做题量。成绩单展示页建议用 canvas 画一个简单的分数环形图,视觉上比纯数字好看很多,实现成本不高,但演示效果提升明显。
5. 毕设交付物避坑指南:源码与演示里的 5 个翻车点
5.1 真机预览白屏:environment not found 是最常见的黑匣子
现象:模拟器一切正常,扫码后手机打开白屏,Console 报 environment not found。 原因:app.js 里 wx.cloud.init 没有传 env,或者传了拷贝少一位的环境 ID。本地只有一套环境时经常能蒙混过关,真机上多环境并存就暴露了。 解决:去云开发控制台复制环境 ID,确认 app.js 里 env 字段与之一致。云函数内部则用cloud.DYNAMIC_CURRENT_ENV动态获取当前环境,避免硬编码。我排查这类问题从来不看代码,先看环境 ID 对不对,十次里有八次是这里写错。
5.2 tabBar 图标不显示:路径与格式的玄学
现象:编译通过,但底部导航栏的图标是空白或者灰色方块。 原因:iconPath 用了网络图片、图片超过 40KB、格式不是 png、路径大小写和实际文件名不一致。tabBar 图标不支持网络地址,必须放到本地 images 目录下。 解决:把图标裁成 81×81 像素左右的 png,控制在 40KB 以内,路径按 app.json 所在目录相对写。改完配置后重新编译,如果还不显示,检查图片文件名后缀到底是 .png 还是 .PNG。这类问题不报错,纯靠眼睛找,所以我把这归类为玄学问题,遇到先怀疑路径,再怀疑格式。
5.3 错题本莫名清空:权限、缓存与异常吞掉
现象:答错几道题后进错题本有数据,退出小程序再进来就空了。 原因:两个方向。一是错题写进了本地 storage,而开发者工具「清除缓存」把 storage 清了;二是云数据库集合权限设为「仅创建者可读写」,但写入时前端把异常吞掉了,数据根本没提交上去。 解决:错题本坚持走云数据库,写入用 saveWrong 里的先查再改逻辑,并在 catch 里把 error 打出来。集合权限设为「所有用户可读,仅创建者可读写」。用本地 storage 做错题本就等于放弃了跨设备同步,换手机演示时直接露怯。
5.4 答辩现场断网:题库要能做本地兜底
现象:答辩教室 Wi-Fi 不稳定,打开练习页转圈圈,题库死活加载不出来。 原因:题库只放云数据库,没有任何缓存和降级策略。示例代码里 getQuestions 的 catch 分支退回本地 JSON,就是专门防这种场景的。 解决:题库数据本地备份一份,页面加载先读缓存,缓存没有再请求云端,云端失败退回本地。数据可以不是最新的,但演示不能断。这个兜底逻辑在说明书里写明白,反而是加分项,说明你考虑到了真实使用场景的弱网环境。
5.5 演示录像画面模糊、声音听不清:输出前先自查
现象:录像文件播放时文字看不清,声音小,答辩现场投影后更是灾难。 原因:录制分辨率太低,或者直接录的模拟器窗口,真机字体在屏幕上被缩放后糊成一团。 解决:录像前把模拟器窗口放大到合适尺寸,或者直接用真机演示并录屏。录制结束后自己先播放一遍,重点看小字号文字是否清晰、答题的点击反馈是否被录进去。录像要打包进毕设材料,画面质量直接反映你对待交付物的态度,模糊录像会让评委觉得你连提交物都没检查过。
6. 把演示录像录出“答辩感”:3 分钟脚本与三个加分习惯
演示录像不是把操作过程从头到尾录一遍,而是在最短时间内把「数据闭环」讲清楚。我录这类录像的习惯是先写脚本,再动手录,时间控制在 3 分钟左右。超过 5 分钟的录像,评委基本没耐心看完。
| 时间段 | 画面内容 | 旁白要点 |
|---|---|---|
| 0:00 - 0:20 | 打开小程序进入首页 | 一句话介绍:面向 MHK 考生的在线学习小程序 |
| 0:20 - 1:00 | 词汇页搜索「锻炼」 | 说明词表按等级划分,支持拼音搜索 |
| 1:00 - 1:40 | 专项练习答 3 道题 | 展示即时判分、解析弹窗 |
| 1:40 - 2:20 | 模拟考试开考 | 展示计时器,等自动交卷 |
| 2:20 - 3:00 | 进错题本重做刚才的错题 | 展示数据闭环,错题自动回收 |
三个加分习惯,亲测有效。第一,在设置页留一个「演示模式」开关,把考试时长改为 1 分钟、题目顺序打乱,录像节奏会紧凑很多。第二,录像时故意答错一题,然后进错题本把它重做对,这个「错→回收→重做→消除」的完整链路比任何口头讲解都直观。第三,旁白提前写好,录的时候照着念,避免即兴发挥时语速忽快忽慢。声音建议用手机直接录,配乐和特效都不需要,清晰是第一位的。
我帮 A 同学改过一份毕设录像,第一版 9 分钟,全程在点按钮没解说,评委会后反馈是「不知道你想展示什么」。后来按上面的脚本重录到 3 分半,答辩现场只被问了一个问题,还是关于错题本统计逻辑的。这个经历让我养成了一个习惯:任何演示材料,先写脚本再动手,脚本写不顺说明功能还没想透。希望帮到你。
本文还有配套的精品资源,点击获取