ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

微信答题小程序源码拆解:从数据库到JavaScript逻辑

微信答题小程序源码拆解:从数据库到JavaScript逻辑 简介这是一款基于JavaScript开发的微信小程序答题考试刷题系统前后端源码与数据库齐备依托云开发部署无需自购服务器和域名备案。它面向企业招聘笔试、知识竞赛、培训认证及毕业设计等场景支持候选人扫码答题、实时出分、主办方快速筛选考生也可在限定时间段内发布活动赛事。资源共包含6313个文件以JavaScript/TypeScript脚本、JSON配置和Markdown文档为主体另含wxml页面结构、wxss样式、png图片等小程序必备资源依赖文件完整项目结构清晰可支撑完整项目运行与二次开发。压缩包仅10.05MB轻量便携目前已有1402人学习浏览。借助无限制题库、错题训练、随机题序、答案解析与错题本重练等功能开发者可快速搭建职业刷题平台并依据业务需要扩展考试预约、内部考核等模块。1. 拿到答题小程序源码只是开始看懂 JavaScript 逻辑和数据库才能改造成自己的刷题工具很多开发者手上都有一份这样的压缩包微信小程序答题考试刷题源码前端是 JavaScript后面跟一份数据库脚本。这东西能做什么往小里说是把几百道题搬进微信让用户随时刷往大里说可以接上账号体系、考试计时、错题记录和成绩分析变成企业考核或在线教育的入口。适合三类人想给学生做练习的老师要给团队做考核的管理者还有接外包单子的前端开发者。先泼一盆冷水源码能跑和能上线是两码事题库结构、判分逻辑、审核资质每一项都可能翻车。下面这份拆解是按这类源码的常见结构完整走一遍。2. 先看源码结构和数据流JavaScript 逻辑层与页面层怎么分工解开压缩包后第一件事不是急着改 UI而是把目录结构过一遍。这类答题小程序的源码绝大多数遵循微信小程序的官方工程规范页面放在 miniprogram/pages 下公共逻辑抽到 utils数据库脚本独立放一份。你只有先分清页面、逻辑和数据三块后面改题、加功能才不会乱。2.1 源码包的核心目录拆解找到题库文件、请求封装和页面入口按常见做法一个基于原生 JavaScript 的答题小程序源码包目录大概是这样的miniprogram/ pages/ index/ // 首页练习模式、考试模式入口通常带一个按钮组 exam/ // 答题页题目渲染、选项点击、计时器、交卷 result/ // 成绩页得分、正确率、用时、错题列表 record/ // 记录页历史答题记录按日期分组 utils/ api.js // wx.request 封装统一处理 baseURL、超时和错误码 question.js // 题库加载、随机抽题、选项乱序 storage.js // 做题进度、错题本、设置项的本地缓存封装 app.js // 全局数据openid、用户信息、全局配置 app.json // 页面注册、tabBar、window 配置 database/ schema.sql // 建表语句题目表、答题记录表、错题表 seed.sql // 初始题库数据常见 100500 道示例题这里有个容易踩的坑很多人拿到包后直接搜题库两个字以为题目会是一个 JSON 或 JS 文件结果在 database 目录里找到了 SQL。两种来源都存在但如果题目在数据库里小程序的纯前端是不能直连 MySQL 的中间必须有一层接口。你需要的不是改 SQL 本身而是确认源码里有没有配套的后端服务或者 api.js 是否指向一个可用的服务地址。接下来看数据流。一张答题卡从打开到交卷经历的路径是用户点击答题页 → 页面 bindtap 事件触发 → 逻辑层调用 question.js 读取题目 → 数据交给 setData 渲染到视图层 → 用户选中选项后再次通过事件回传 → 逻辑层判分并把结果写入 storage 或提交接口。这套链路里最关键的一句话是小程序里视图层和逻辑层是分离的任何时候要改界面都必须调用 setData不能像写传统网页那样直接操作 DOM。2.2 答题页的 JavaScript 生命周期onLoad 拉题、onUnload 善后答题页是整个源码的心脏。我拆过不少同类型的包答题页的 js 文件通常在一个 300 到 800 行的范围内核心逻辑集中在几个生命周期函数和自定义方法里。先说生命周期小程序页面有四个常用的onLoad 在页面创建时执行一次适合拉取考试配置和恢复未完成的进度onShow 在页面每次显示时触发适合刷新倒计时onHide 在页面被切走时触发适合暂停计时onUnload 在页面销毁时触发适合提交答题记录和清理定时器。一个典型的答题页框架看起来是这样Page({ data: { questions: [], // 当前考试的题目数组 currentIndex: 0, // 当前正在答的题号 selectedMap: {}, // 已选答案的映射{ 题号: 答案 } remainTime: 0, // 剩余秒数 examConfig: {} // 考试模式、时长、总分等配置 }, onLoad(options) { // options.examId 来自页面跳转路径例如 /pages/exam/exam?examId1001 const examId options.examId || default; this.initExam(examId); this.restoreProgress(); // 从缓存恢复上次未交卷的进度 }, onUnload() { if (this.timer) { clearInterval(this.timer); // 离开页面必须清定时器否则内存泄漏 } this.saveProgress(); // 未交卷时保存进度下次进来继续 } });这段代码要说明三点。第一setData 是异步的但它写入 data 对象是同步的所以当你需要立即读取当前值做判断时直接读 this.data 是拿不到最新渲染值的要读局部变量或在回调里处理。第二remainTime 和 currentIndex 这些频繁变化的数据能用局部变量就用局部变量最后一次性 setData能明显减少渲染卡顿这在考试倒计时场景里特别重要。第三onUnload 里保存进度是个好习惯但要注意别在 onUnload 里发 wx.request因为页面都销毁了回调绑定的 this 上下文可能已经失效常见做法是用 storage 写入等下次 onLoad 再统一提交。答题小程序里还有一个频繁出现的逻辑选项乱序。考试类应用为了防止作弊通常要求同一道题不同人看到的选项顺序不一样。这个功能一般在 question.js 里实现做法是复制一份选项数组用洗牌算法打乱然后把新顺序存起来判分时不能再用原始答案下标而要用一个选项文本 → 是否正确的映射。源码里如果没有这层处理建议自己加上这属于考试类小程序的基本要求。3. 把题库数据库跑起来表结构设计、初始化导入与接口接入标题里带了数据库这是很多人忽略的重点。答题小程序的核心资产是题库而题库的存放方式直接决定了后续的维护成本。我见过两种极端一种把所有题目塞进一个 JSON 文件前端一次性加载题目一变就得重新发版另一种上云数据库表结构设计得极其复杂一个小功能查询四五张表。合理方案取中间题库表结构保持简单但字段设计要经得起增长。3.1 题目表、答题记录表、错题表的字段设计从一份 SQL 看设计思路下面是一份我在类似源码里常见的题库表结构兼容 MySQL 与 SQLite跑得通也够用-- 题目表一道题一行选项用 JSON 或分隔符存储 CREATE TABLE question ( id INTEGER PRIMARY KEY AUTOINCREMENT, category VARCHAR(50) NOT NULL DEFAULT default, -- 章节/科目分类 type VARCHAR(10) NOT NULL DEFAULT single, -- single / multi stem TEXT NOT NULL, -- 题干 options TEXT NOT NULL, -- 选项JSON 数组 answer VARCHAR(10) NOT NULL, -- 正确答案多选用逗号分隔 analysis TEXT, -- 答案解析 difficulty TINYINT DEFAULT 2, -- 1 简单 2 中等 3 困难 sort_order INT DEFAULT 0 -- 手动排序权重 ); -- 答题记录表一次交卷一行记录整体结果 CREATE TABLE exam_record ( id INTEGER PRIMARY KEY AUTOINCREMENT, user_key VARCHAR(64) NOT NULL, -- 用户标识openid 或本地生成的 uuid exam_id VARCHAR(32), -- 考试/练习场次标识 total_num INT NOT NULL, -- 总题数 correct_num INT NOT NULL, -- 正确题数 duration INT, -- 用时单位秒 score DECIMAL(5,2), -- 换算后的得分保留两位小数 create_time DATETIME DEFAULT CURRENT_TIMESTAMP ); -- 错题表答错的题进错题本方便后续重练 CREATE TABLE wrong_book ( id INTEGER PRIMARY KEY AUTOINCREMENT, user_key VARCHAR(64) NOT NULL, question_id INT NOT NULL, wrong_answer VARCHAR(10), -- 用户当时选的错误答案 create_time DATETIME DEFAULT CURRENT_TIMESTAMP, UNIQUE(user_key, question_id) -- 同一道错题只记一次 );核心字段的用途可以浓缩成一张表字段类型说明categoryVARCHAR(50)章节/科目分类用于按分类组卷typeVARCHAR(10)题型标记single 或 multioptionsTEXTJSON 数组包含 key 和 textanswerVARCHAR(10)正确答案多选题用逗号分隔difficultyTINYINT难度档位随机抽题时加权用scoreDECIMAL(5,2)得分字段前端展示时用 toFixed(2) 避免浮点误差这份表结构有几个设计考量。第一options 用 JSON 数组存而不是拆成 option_a、option_b 四列原因是有的题是判断题只有两个选项有的可能有六个选项拆列会让查询逻辑变得僵硬JSON 数组在查询层解析一下就行开发者工具里直接 JSON.parse 不存在兼容问题。第二answer 字段对多选题用逗号分隔存储比如 A,C判分时只要把两边排序后比对就不需要为多选单独建关联表省掉大量 join。第三difficulty 和 sort_order 是给随机抽题用的sort_order 让你可以手动控制题目顺序比如把真题放前面把模拟题放后面。关于自增主键这里要补一句如果你后续要把题库迁到微信云开发的 doc 型数据库就不能用自增 id 了得改用字符串 id。云开发数据库的 _id 是自动生成的随机字符串所以如果源码里已经有按数字 id 查询的地方迁移时要一起改。这个后面第 6 章会再提。3.2 初始化题库导入 SQL、验证行数与接口联调拿到 database 目录下的 schema.sql 和 seed.sql 后第一步是把它们导进本地数据库。常见做法是先用 SQLite 或 MySQL 把结构跑通再决定是否上云。导入命令很简单# MySQL 导入假设本地装了 mysql且已创建 quiz_app 库 mysql -uroot -p quiz_app schema.sql mysql -uroot -p quiz_app seed.sql # SQLite 导入不需要服务直接生成一个 .db 文件 sqlite3 quiz.db schema.sql sqlite3 quiz.db seed.sql导入完成后必须做的验证不是看命令行有没有报错而是查行数和抽查字段SELECT COUNT(*) FROM question; -- 期望值与 seed 里的说明一致 SELECT COUNT(*) FROM question WHERE typemulti; -- 多选题数量确认不是 0 SELECT id, stem, options, answer FROM question LIMIT 3; -- 肉眼检查 JSON 是否完整这里有个血泪经验SQL 脚本经常在 options 字段的引号上出错比如 SQL 里用了普通字符串存 JSON而 JSON 本身含有双引号导入时如果没用转义或没有用参数化可能会中途截断。现象是导入不报错但查出来的 options 只有半个{。解决方法是导入后抽查而不是信命令行顺手用WHERE options NOT LIKE {%筛一遍能发现大多数损坏行。题库导入只是第一步小程序要能读到题还得过接口或缓存这一层。如果源码里没有后端服务常见的替代方案是把题库导出成 JSON 片段放到小程序的 utils/question.js 里或者放到云开发的数据库中。如果源码里有 api.js那么你要关注的是 baseURL 这个变量把它改成你实际服务的地址并且确认接口返回的字段名与前端解析的字段名一致——这又是一处高频翻车点后端返回option_list前端读的是options一查一个空数组。后续做题库管理本质上就是对 question 表做增删改查接口层把这四个动作暴露清楚就够了。4. 拆解答题核心流程的 JavaScript 实现渲染、判分、计时与进度恢复这一章是源码的技术内核。前面建立起来的目录和数据流最终都要落到几个关键函数上。我会按答题过程的先后顺序把最核心的几段 JavaScript 逻辑拆开讲解每一段都说明为什么这么写。4.1 选项渲染与判分逻辑单选、多选题用一套代码兼容答题页最基础的需求是展示题干和选项用户点击选项后立刻反馈对错或者先选好再统一交卷。两种交互模式源码里都有但更常见的是边答边判的练习模式和交卷统判的考试模式。判分函数是两者的公共底座。// utils/score.js —— 判分核心支持单选与多选 function isAnswerCorrect(question, selectedStr) { const type question.type || single; let correctStr question.answer; // 判分前先做一次数据类型判断后端可能返回数组统一转成字符串 if (Array.isArray(correctStr)) { correctStr correctStr.join(,); } if (typeof correctStr ! string) { correctStr String(correctStr); } if (type single) { // 单选直接比对字符 return selectedStr correctStr; } // 多选先把 A,C 拆成数组排序后重新拼接再比对 const sort (str) str.split(,).map(s s.trim()).filter(Boolean).sort().join(,); return sort(selectedStr) sort(correctStr); } module.exports { isAnswerCorrect };这个函数看起来简单但有几个边界值得说。第一多选判分必须排序因为用户选AC和选CA是同一个答案不排序就会出现误判。第二过滤空字符串很重要用户如果选完又取消selectedStr 可能是空串直接 split(,) 会得到一个[]排序拼接后变成空字符串刚好和正确答案比对不上行为正确但容易在调试时造成困惑。第三选项 key 统一用字母而不是数字下标展示给用户更直观存储也更稳定。接下来是渲染端。小程序里单选对应 radio-group多选用 checkbox-group这个不用多说。关键是每个选项怎么携带自己的值。我见过不少新手在 wxml 里写死>// 选项乱序后仍然能正确判分的关键key 跟着选项走 function shuffleOptions(question) { const opts JSON.parse(question.options); // 例如 [{key:A, text:北京}, {key:B, text:上海}] for (let i opts.length - 1; i 0; i--) { const j Math.floor(Math.random() * (i 1)); [opts[i], opts[j]] [opts[j], opts[i]]; } return opts; }这里的要点是洗牌只打乱数组顺序每个选项的 key 保持不变。这样用户选到 key 为 C 的选项即便它显示在第一个位置提交的答案仍然是 C判分逻辑不需要关心显示顺序。如果你拿到源码里没有这个机制强烈建议自己补上——这是考试类小程序区别于玩具 demo 的分水岭。4.2 计时器与进度恢复setInterval 的清理和 storage 的持久化考试模式一定要有倒计时。源码里最常见的实现是 setInterval 每秒递减 remainTime但这里坑很多。第一个坑setInterval 在页面切到后台时并不会停如果用户按了 Home 键再回来时间已经跑掉一大截用户投诉我什么都没干就扣了五分钟。第二个坑开发者工具里一切正常真机上息屏十分钟回来定时器可能被系统回收页面显示的时间还停在十分钟前。// 考试倒计时基于时间戳差值计算而不是简单减 1 startTimer(duration) { const endTime Date.now() duration * 1000; this.endTime endTime; this.timer setInterval(() { const remain Math.max(0, Math.ceil((this.endTime - Date.now()) / 1000)); if (remain 0) { this.autoSubmit(); // 时间到自动交卷 return; } // 只在剩余秒数变化时更新视图减少无谓的 setData if (remain ! this.data.remainTime) { this.setData({ remainTime: remain }); } }, 500); // 500ms 检查一次兼顾及时性与性能 }这个实现的关键是不依赖每秒减一的自身累加而是用结束时间戳和当前时间戳的差值计算剩余时间。这样即使 setInterval 被系统延迟或暂停了若干秒恢复后差值依然准确不存在累积误差。另一个细节是 500ms 的间隔而不是 1000ms因为 setInterval 在低端安卓机上可能丢帧1000ms 的间隔一旦丢一次用户会看到从 60 直接跳到 58500ms 的检查频率让跳变的可能性大幅降低。即使跳了也在视觉上更接近真实。进度恢复是另一个必须写对的功能。用户答到第 20 题误触退出回来应该还在第 20 题且已选答案还在。这个功能的常见实现是把 { currentIndex, selectedMap, remainTime } 序列化到 wx.setStorageSynconLoad 时再读出来// 保存与恢复答题进度 saveProgress() { const snapshot { currentIndex: this.data.currentIndex, selectedMap: this.data.selectedMap, remainTime: this.data.remainTime, questions: this.data.questions // 题目本身也存避免恢复时重新请求 }; wx.setStorageSync(exam_progress, snapshot); } restoreProgress() { const saved wx.getStorageSync(exam_progress); if (saved saved.questions saved.questions.length 0) { this.setData({ questions: saved.questions, currentIndex: saved.currentIndex, selectedMap: saved.selectedMap, remainTime: saved.remainTime }); } }这里有一个值得注意的设计决策把 questions 整个存进 storage 而不是恢复时重新请求。原因是考试中途网络可能不稳定重新请求有可能拿到一份洗牌后不同的题目顺序用户会发现自己刚答过的题变位置了。直接把题目数组快照存下来恢复的就是用户离开时那一份。代价是 storage 占用一份 50 题的考试快照大约 10 到 30 KB完全在可接受范围内。关于恢复进度还有一个细节交卷成功之后必须清掉这份快照否则下一次打开考试会恢复上一次的题。常见做法是在 autoSubmit 成功后调用 wx.removeStorageSync(exam_progress)并且要在提交成功的回调里清不能在点击交卷时就清否则提交失败就两头都丢了。这个顺序是血泪经验我见过不止一次因为提前清除快照导致用户成绩丢失的翻车现场。5. 上线前避坑审核类目、真机兼容与数据同步的 5 个高频问题这一章统一回答源码能跑为什么一上线就出事。我会按现象、原因、解决的顺序写每一条都是我在接类似项目时踩过或帮别人排过的真实问题。有些问题看起来像玄学其实是适配没做全。5.1 开发者工具正常、真机上选项错位或按钮被遮挡现象模拟器里页面完美iPhone 真机上交卷按钮被底部小黑条挡住部分机型选项文字换行后对齐错乱。原因答题页的底部操作栏用了固定定位但没有适配 iPhone 的 safe-area 区域选项文字用了固定行高字一旦变长或字体缩放就撑破容器。另外如果源码用了自定义导航栏顶部导航栏高度不是写死 64px 就行的iPhone 刘海屏和安卓机的状态栏高度差很大写死的话按钮会顶到状态栏里。解决底部按钮容器加上padding-bottom: env(safe-area-inset-bottom)选项文字行高改用 min-height 配合 flex 布局避免固定高度。自定义导航的高度用wx.getWindowInfo()动态计算状态栏高度再叠加不要写死。还有一个隐藏点微信开发者工具默认字体是微软雅黑真机是苹方或思源同一段文字在真机上的宽度能差 5% 到 10%所以选项容器不要设计成刚好容纳四个字的宽度留足余量。5.2 提交审核被拒服务类目与考试字样的资质问题现象小程序提交审核反馈你提供的服务涉及在线教育/考试相关类目需要提供相应资质审核不通过。原因答题刷题小程序从功能上是教育学习工具但个人主体小程序不能选择教育-在线视频这类需要 ICP 备案和办学资质的类目而源码里首页大字写着模拟考试真题冲刺容易触发系统类目检测。解决两条路。第一把产品文案从考试往练习自学测评靠首页按钮叫章节练习和随机练习结果页叫本次表现而不是考试成绩从而落到教育-教育信息服务类目个人主体可尝试。第二如果是企业主体且有相关资质直接选教育-在线教育或教育-职业培训并上传资质。无论如何不要在功能里出现替考泄题包过这类违规词触发就是永久封禁级别的处罚。5.3 交卷瞬间丢数据请求超时与重复点击现象用户答完 50 题点了交卷没反应再点一次页面白屏或分数显示为 0答题记录没入库。原因交卷是高频冲突点。第一次点击发起的 wx.request 还没返回第二次点击又发了一次后端同时收到两条相同记录可能因为主键或唯一索引直接报错而前端只处理了成功回调报错路径走了 fail没有提示也没有本地兜底。解决前端加一个 submitting 锁进入提交函数后立刻置 true请求回来前所有再次点击直接 return同时把提交内容在成功后写入本地一条已交卷标记后端加幂等判断用 user_key exam_id create_time 做唯一键。更稳的做法是交卷数据先写本地缓存再异步上传后台上传失败下次进入记录页时自动重试考试类应用尤其值得做这层。5.4 题库 JSON 过大导致加载白屏现象题库从 200 题增加到 2000 题后进入考试页白屏 3 秒以上部分低端安卓机直接闪退。原因源码里很可能一次性 request 拿到全量题库然后 setData 塞进渲染层。小程序 setData 的数据量过大是性能杀手2000 条题目字符串可能超过 500 KB同时渲染层要生成几千个节点直接卡死。解决改分页加载。一次只拉 20 题答完一题或答完一组后再拉下一批进度恢复时也只恢复当前页的 20 题而不是全量题库。另一个常见做法是进入考试前先把题库在空闲时下载到本地 storage考试时只读本地答完再上报这样网络抖动也不影响考试过程。5.5 数据库时间字段时区错乱记录时间和用户实际时间差 8 小时现象数据库里答题记录的 create_time 是凌晨 4 点用户明明是下午 4 点做的题或历史记录页按日期分组分组错位。原因数据库服务器时区是 UTC小程序端通过 wx.request 提交时带的是服务器时间字符串没有做时区转换。MySQL 的 DATETIME 不带时区信息写入的是 UTC 时间读取时又当成本地时间展示于是差了 8 小时。解决统一约定用 UTC 存储、前端展示时转本地。后端接口返回时间戳秒或毫秒前端new Date(ts * 1000)后使用getFullYear/getMonth/getDate取本地时间如果后端返回的是格式化字符串就要求接口改成 ISO8601 带时区的格式或者在建立连接时设置会话时区为08:00。这条不算源码 bug但几乎每个上线的答题小程序都会遇到。6. 把源码改造成自己的刷题工具两个进阶改动和上线前的验证清单前面几章把源码从结构到核心逻辑都过了一遍这一章讲怎么让它从别人的源码变成你的产品。最值得做的两个改动一是把固定题库改成按日期动态组卷的每日一练二是把本地题库迁到微信云开发数据库免掉自建后端。每日一练的思路不复杂用日期字符串作为随机种子对题库做一次固定顺序洗牌取前 20 题。关键在固定顺序——同一天内用户每次进入看到的题应该一样早上做一半、晚上继续做体验才连贯。做法是用 dateString 作为种子调用伪随机函数而不是每次 Math.random()。种子相同洗牌结果就相同既保证每天题目不同又保证当天内稳定。这改动大约 20 行代码但留存价值提升很明显。第二个改动是把题库迁到云开发数据库。好处是不需要自建服务器小程序端直接 wx.cloud.database() 读写。迁移时注意两个坑云数据库 _id 是字符串原来 SQL 里 WHERE id 100 要改成 doc(xxx)云数据库一次 get 最多返回 20 条要分页或用 limit/skip。上线前验证清单我每次交付都会照走一遍真机双端各跑完整答题流程中途切后台再回来断网时答题交卷不崩溃恢复网络能补交两个微信号登录同一手机确认记录不串号检查时间字段时区把题库临时加到 2000 题测加载耗时。说一个我自己的教训有次交付前只测了批量导入题库没测错题本去重上线次日用户反馈一道错题出现三条。原因是 UNIQUE 约束没生效插入走了 INSERT OR REPLACE旧记录 id 变了导致唯一键失效。后来改成先查询再插入才解决。这种问题不跑真实用户场景很难提前发现。希望这份拆解能帮你少走弯路真正交付给用户使用。本文还有配套的精品资源点击获取
返回列表