简介:这是一份面向微信小程序开发者与云开发初学者的完整答题类项目实战资源,适用于在线考试、知识竞赛、培训测评等轻量级互动场景。项目采用微信原生小程序前端 + 腾讯云开发(TCB)全栈架构,免服务器运维,聚焦业务逻辑,降低全栈开发门槛。压缩包共75个文件,含10个JS逻辑文件(实现答题流程与交互)、10个WXSS样式文件(含ColorUI等UI组件)、6个WXML页面结构、35个PNG及4个JPG图片资源(覆盖首页、题目页、排行榜、后台管理界面等),以及JSON配置、README说明和.gitignore等工程文件,整体大小6.06MB,结构清晰、模块分明。已有222人学习下载,资源提供可直接运行的小程序源码、完整的前后端协同方案、云数据库设计思路、云函数答题判分逻辑及典型异常处理示例,助开发者快速掌握云开发在互动类小程序中的落地实践。
1. 一个不用买服务器、不配 Nginx、连域名备案都省了的答题小程序,上线只花了 37 分钟
上周帮某高校实验室快速搭一个期中知识自测工具,需求就三句话:学生微信里点开就能做题、交卷自动批改并返回得分、老师后台能看谁做了/错在哪。我打开 IDE,新建项目,选「云开发模板」,敲完wx.cloud.init(),把题库 JSON 往云数据库一扔,再写个 20 行的云函数做判分逻辑——37 分钟后,二维码发到班级群里,第一个学生扫码提交,后台实时弹出他的姓名、用时、正确率和错题详情。没有服务器采购流程,没填过 ICP 备案表,没折腾过 HTTPS 证书续期,更没在凌晨三点被502 Bad Gateway报警电话吵醒。这就是微信原生小程序 + 云开发的真实交付节奏:它不解决高并发秒杀或 PB 级图像处理,但它把「验证想法 → 收集反馈 → 迭代功能」这个闭环压缩到了以小时为单位。适合教研组、培训团队、社区运营者,以及所有被运维成本拖慢产品节奏的中小场景开发者。
2. 架构拆解:为什么是「原生小程序 + 云开发」,而不是 Taro 或 uni-app?
2.1 技术栈选择背后的硬约束:轻量、可控、零运维
这个项目的技术选型不是凭喜好,而是被三个现实条件框死的:
- 交付周期紧:需求方明确要求“下周一开始用”,没时间走跨端框架的兼容性调试;
- 数据敏感度低但读写频次稳:题库固定(<500 题),每日活跃用户预估 200–800 人,峰值并发 <50,云开发的免费额度(每月 10GB 云存储、50 万次云函数调用)完全覆盖;
- 无历史系统对接需求:不需要和旧 Java 后台打通,也不需要复用已有 Vue 组件库,纯绿field 新建。
所以放弃 Taro/uni-app 的根本原因不是它们不好,而是——多一层抽象,就多一层黑匣子。比如 uni-app 的uniCloud虽然也叫“云开发”,但它的云函数部署、日志查看、数据库权限配置,全要跳转到 DCloud 控制台;而微信原生云开发的所有操作,都在微信开发者工具里完成:数据库可视化编辑、云函数一键上传、日志实时滚动、甚至云调用(如发送订阅消息)的 token 自动管理,全部内嵌。对一个单人开发、无 DevOps 支持的场景,这省下的不是代码量,是决策成本。
提示:如果你的项目需要对接企业微信 API、调用第三方支付回调、或必须用 MySQL 做复杂关联查询,云开发不是最优解。但本项目只需 CRUD 题目、统计、简单计算,云开发就是精准匹配。
2.2 整体架构图:三层收敛,边界清晰
整个系统只有三个物理层,且每层职责单一、无交叉依赖:
| 层级 | 组成 | 关键能力 | 开发者操作入口 |
|---|---|---|---|
| 前端层(小程序) | WXML/WXSS/JS +wx.cloudSDK | 发起云函数调用、直接读写云数据库、上传/下载云文件 | 微信开发者工具「编辑器」面板 |
| 云服务层(云开发) | 云数据库(MongoDB 兼容)、云存储(对象存储)、云函数(Node.js 16) | 数据增删改查、文件托管、业务逻辑执行、定时触发 | 微信开发者工具「云开发」面板 或 云开发控制台 |
| 管理层(微信侧) | 登录态(wx.login)、用户信息(wx.getUserProfile)、订阅消息(wx.requestSubscribeMessage) | 用户身份识别、消息触达、权限管控 | 小程序管理后台「开发管理」与「订阅消息」模块 |
注意:没有传统意义上的「后端服务层」。云函数替代了 Express/Koa,云数据库替代了 MySQL+Sequelize,云存储替代了 Nginx 静态资源服务。所有网络请求都走微信官方通道(https://api.weixin.qq.com/tcb/...),天然免跨域、免鉴权中间件、免 SSL 配置。
2.3 目录结构解析:从app.js到cloudfunctions/的真实路径
项目根目录结构如下(已剔除无关文件,仅保留核心):
miniprogram/ ├── app.js // 小程序全局逻辑:初始化云开发、监听登录态 ├── app.json // 页面路由、窗口样式 ├── project.config.json // 项目配置(含云开发环境 ID) ├── pages/ │ ├── index/ // 首页:题目列表 & 开始按钮 │ ├── quiz/ // 答题页:单题渲染、答案提交 │ └── result/ // 结果页:得分、错题解析、重新开始 └── utils/ └── db.js // 封装云数据库常用操作(如 getQuestionById) cloudfunctions/ ├── initDB/ // 初始化题库(仅首次运行) ├── submitAnswer/ // 核心判分云函数(接收答案,返回得分+解析) └── getRanking/ // 获取排行榜(聚合查询)关键点:
project.config.json中必须配置"cloudfunctionRoot": "cloudfunctions/"和"envVersion": "release",否则云函数无法被识别;- 所有云函数目录名(如
submitAnswer)会自动成为其 HTTP 触发路径的一部分(https://xxx.tcloudbase.com/submitAnswer),命名需符合小写字母+下划线规范; utils/db.js是高频复用模块,封装了const db = wx.cloud.database()及常用集合操作,避免每个页面重复初始化。
3. 核心模块拆解:从一道单选题的提交,到后台得分计算的完整链路
3.1 前端:答题页(pages/quiz/quiz.js)如何构造提交数据
用户在答题页点击「提交」时,前端收集的是结构化答案数据,而非原始 DOM 值。关键代码如下:
// pages/quiz/quiz.js Page({ data: { questionId: '', // 当前题目 ID(来自上一页传参) options: [], // 选项数组,如 [{id: 'A', text: '苹果'}, {id: 'B', text: '香蕉'}] userAnswer: '' // 用户选择的选项 ID,如 'A' }, // 提交按钮点击事件 onSubmit() { const { questionId, userAnswer } = this.data; // 1. 基础校验:必须选了答案 if (!userAnswer) { wx.showToast({ title: '请先选择答案', icon: 'none' }); return; } // 2. 构造提交 payload(严格按云函数入参定义) const payload = { questionId, // 字符串,必填 answer: userAnswer, // 字符串,必填(对应题库中 correctAnswer 字段) timestamp: Date.now() // 数字,用于后续统计耗时 }; // 3. 调用云函数 submitAnswer wx.cloud.callFunction({ name: 'submitAnswer', data: payload, success: res => { // res.result 是云函数返回值,结构由 submitAnswer 决定 if (res.result.code === 0) { wx.navigateTo({ url: `/pages/result/result?score=${res.result.score}&detail=${encodeURIComponent(JSON.stringify(res.result.detail))}` }); } else { wx.showToast({ title: res.result.msg || '提交失败', icon: 'none' }); } }, fail: err => { console.error('云函数调用失败', err); wx.showToast({ title: '网络错误,请重试', icon: 'none' }); } }); } });参数说明与设计意图:
questionId用字符串而非数字:云数据库_id默认是字符串,避免类型转换歧义;answer字段名与题库文档中correctAnswer字段名保持一致,降低云函数内映射成本;timestamp不存入数据库,仅用于计算用户答题耗时(Date.now() - startTimestamp),体现前端主动埋点意识;success回调中对res.result.code做判断,而非直接信任res.result.score,这是防御性编程习惯——云函数可能因数据异常返回{code: 1, msg: '题目不存在'}。
3.2 云函数:submitAnswer/index.js如何完成判分与记录
云函数是整个业务逻辑的核心,它运行在微信云环境,拥有独立的 Node.js 上下文和数据库访问权限。以下是精简后的核心实现:
// cloudfunctions/submitAnswer/index.js const cloud = require('wx-server-sdk'); cloud.init({ env: cloud.DYNAMIC_CURRENT_ENV // 使用当前环境,无需硬编码环境 ID }); const db = cloud.database(); const _ = db.command; exports.main = async (event, context) => { const { questionId, answer, timestamp } = event; try { // 1. 查询题目详情(带正确答案) const questionRes = await db.collection('questions').doc(questionId).get(); if (!questionRes.data || questionRes.data.length === 0) { return { code: 1, msg: '题目不存在' }; } const question = questionRes.data[0]; // 2. 判分逻辑:严格字符串相等(避免 'A ' 和 'A' 误判) const isCorrect = question.correctAnswer.trim() === answer.trim(); // 3. 写入答题记录(云数据库集合:answers) const record = { _id: cloud.database().autoGenerateID(), // 自动生成唯一 ID questionId, userId: context.OPENID, // 微信自动注入,无需前端传 answer, isCorrect, timestamp, createdAt: new Date() // 服务端时间,防客户端时间篡改 }; await db.collection('answers').add({ data: record }); // 4. 返回结果(前端 result 页面直接消费) return { code: 0, score: isCorrect ? 10 : 0, detail: { questionText: question.text, userAnswer: answer, correctAnswer: question.correctAnswer, explanation: question.explanation || '暂无解析' } }; } catch (err) { console.error('submitAnswer 执行异常', err); return { code: 2, msg: '系统繁忙,请稍后重试' }; } };关键设计点解析:
context.OPENID是微信云开发自动注入的用户唯一标识,绝对可信,无需前端传、不可伪造,这是实现「免登录态管理」的基础;db.collection('answers').add()使用add而非set,因为每条答题记录都是新文档,无更新需求;createdAt: new Date()使用服务端时间,避免用户手机时间错误导致统计失真;try/catch包裹全部逻辑,并在catch中返回统一错误码,前端可据此做友好提示(如code:2显示「系统繁忙」而非堆栈);question.correctAnswer.trim() === answer.trim()的.trim()是血泪经验:用户可能误触空格,前端输入框未做 trim,云函数必须兜底。
3.3 数据库设计:questions与answers两个集合的字段语义
云数据库使用 MongoDB 兼容语法,集合(Collection)即传统关系型数据库的「表」。本项目仅需两个集合:
集合questions(题库)
| 字段名 | 类型 | 示例 | 说明 |
|---|---|---|---|
_id | String | q_001 | 主键,建议用业务 ID(如 q_001)而非自动生成 ID,便于前端关联 |
text | String | 以下哪种水果是红色的? | 题干文本 |
options | Array | [{"id":"A","text":"苹果"},{"id":"B","text":"香蕉"}] | 选项数组,每个选项含 id(供前端绑定)和 text |
correctAnswer | String | A | 正确答案 ID,与 options.id 对应 |
explanation | String | 苹果成熟时果皮呈红色 | 解析文本,可为空 |
集合answers(答题记录)
| 字段名 | 类型 | 示例 | 说明 |
|---|---|---|---|
_id | String | ans_abc123 | 自增 ID,无业务含义 |
questionId | String | q_001 | 关联 questions._id |
userId | String | oABC123xyz... | 用户 OPENID,微信自动注入 |
answer | String | A | 用户所选答案 |
isCorrect | Boolean | true | 判分结果,布尔值便于聚合查询 |
timestamp | Number | 1715678901234 | 客户端提交时间戳(毫秒) |
createdAt | Date | ISODate("2024-05-15T08:23:45.123Z") | 服务端写入时间 |
注意:云数据库不支持 JOIN,所以
answers中冗余存储questionId而非question.text,查询时用db.collection('questions').doc(questionId).get()单独拉取,这是 NoSQL 的典型范式。
4. 运行与部署:从本地调试到线上可用的六步实操
4.1 前置准备:开通云开发环境与获取环境 ID
这是整个流程的起点,也是新手最容易卡住的一步。务必按顺序操作:
- 登录 微信公众平台 → 进入「开发管理」→「开发设置」→ 记下「AppID」;
- 打开微信开发者工具 → 新建项目 → 填入 AppID,勾选「使用云开发」;
- 工具会自动弹出「开通云开发」向导 → 选择「基础版」(免费,足够本项目)→ 点击「开通」;
- 开通成功后,在开发者工具右上角「云开发」面板中,找到「环境 ID」(格式如
my-env-12345),复制备用; - 在项目根目录
project.config.json中,将"envVersion": "release"改为"envVersion": "release"(确保发布到正式环境),并在cloudfunctionRoot下确认路径正确; - 关键检查:在开发者工具「云开发」面板 → 「数据库」标签页,手动创建
questions和answers两个集合,否则云函数首次写入会报错。
提示:环境 ID 是云资源的唯一命名空间,一旦选定不能修改。测试期可用
test-env-xxx,上线前再切到prod-env-xxx,通过project.config.json切换,无需改代码。
4.2 本地调试:如何在不发版的情况下验证云函数逻辑
云开发支持「本地调试」,这是比「真机预览」更高效的开发方式:
- 在开发者工具中,右键点击
cloudfunctions/submitAnswer/目录 → 「上传并部署:云端安装依赖」; - 部署成功后,右键该目录 → 「本地调试」;
- 工具会启动本地 Node.js 进程,并在控制台输出
Listening on http://localhost:8080; - 此时,前端调用
wx.cloud.callFunction({name: 'submitAnswer', ...})会自动路由到本地进程(需开启「调试基础库」); - 在
submitAnswer/index.js中打console.log(),所有日志实时显示在「云开发」面板的「日志」页签; - 修改代码后,保存即热更新,无需重启。
为什么这比真机调试快?
真机调试需每次上传代码 → 提审 → 审核通过 → 线上生效,耗时 2 小时起;本地调试改一行代码,3 秒内看到效果。尤其对判分逻辑这种纯计算型函数,本地调试覆盖率可达 100%。
4.3 首次数据初始化:用initDB云函数批量导入题库
题库不会手动一条条录入,而是通过云函数一次性导入。initDB/index.js实现如下:
// cloudfunctions/initDB/index.js const cloud = require('wx-server-sdk'); cloud.init({ env: cloud.DYNAMIC_CURRENT_ENV }); const db = cloud.database(); // 模拟题库数据(实际项目中可从 Excel 导出 JSON) const questionsData = [ { _id: 'q_001', text: '以下哪种水果是红色的?', options: [ { id: 'A', text: '苹果' }, { id: 'B', text: '香蕉' }, { id: 'C', text: '橙子' } ], correctAnswer: 'A', explanation: '苹果成熟时果皮呈红色' } // ... 更多题目 ]; exports.main = async (event, context) => { try { // 清空旧数据(谨慎!生产环境请注释掉) await db.collection('questions').remove({}); // 批量插入 const result = await db.collection('questions').add({ data: questionsData }); return { code: 0, msg: `成功插入 ${result.stats.updated} 条题目` }; } catch (err) { console.error(err); return { code: 1, msg: '初始化失败' }; } };执行步骤:
- 在开发者工具「云开发」面板 → 「云函数」→ 找到
initDB→ 点击「测试」; - 在弹窗中输入
{}(空 JSON),点击「确定」; - 查看返回结果,若
code:0,则去「数据库」→questions集合中确认数据已存在; - 重要:此函数仅首次运行,生产环境部署前需删除
await db.collection('questions').remove({})这行,避免每次上线清空题库。
5. 避坑指南:那些让我重启三次开发者工具的典型问题
5.1 现象:云函数调用返回Error: errCode: -404011 cloud function not found
原因:云函数目录名与wx.cloud.callFunction({name: 'xxx'})中的name不一致。常见错误包括:
- 目录名是
submit-answer(含短横线),但代码里写name: 'submitAnswer'; - 目录名是
SubmitAnswer(大驼峰),但云开发只认小写字母+下划线; - 上传时未勾选「云端安装依赖」,导致函数未真正部署。
解决:在「云开发」面板 → 「云函数」列表中,确认函数名与代码中name完全一致(全小写、下划线分隔);右键函数目录 → 「上传并部署」。
5.2 现象:前端wx.cloud.database()报错Cannot read property 'database' of undefined
原因:wx.cloud.init()未执行或执行时机错误。常见于:
- 在
app.js的onLaunch中调用init,但onLaunch可能被多次触发,而init不能重复调用; - 在某个页面的
onLoad中才调用init,但该页面未经过app.js全局初始化。
解决:在app.js的App({})外层,立即执行初始化:
// app.js 顶部 wx.cloud.init({ env: 'my-env-12345', // 替换为你的环境 ID traceUser: true });确保所有页面都能直接使用wx.cloud.database()。
5.3 现象:云数据库查询返回空数组[],但控制台确认数据存在
原因:云数据库默认开启「安全规则」,未授权的客户端无法读取。新环境默认规则是:
{ "permission": "readWrite" }但这只是「集合级」权限,还需在「数据库」面板 → 点击集合名 → 「权限设置」→ 将「读」权限设为「所有用户」。
解决:进入questions集合 → 「权限设置」→ 「读」选择「所有用户」→ 保存。同理设置answers集合。
5.4 现象:wx.requestSubscribeMessage调用失败,提示subscribe template id invalid
原因:订阅消息模板 ID 未在小程序管理后台申请,或申请后未在「云开发」→ 「订阅消息」中绑定。
解决:
- 登录小程序管理后台 → 「订阅消息」→ 「添加模板」→ 搜索「答题结果通知」类模板 → 提交审核;
- 审核通过后,复制模板 ID(如
AT0001); - 在开发者工具「云开发」面板 → 「订阅消息」→ 「添加模板」→ 粘贴模板 ID → 保存;
- 前端调用时,
tmplIds数组必须包含此 ID。
5.5 现象:真机预览时云函数返回Error: errCode: -404012 access denied
原因:云函数执行环境版本与基础库版本不匹配。云开发 Node.js 运行环境默认是16.x,但部分旧版基础库(如 2.20.0 以下)不兼容。
解决:在开发者工具右上角 → 「详情」→ 「项目设置」→ 将「调试基础库」设为2.27.0或更高;同时在app.json中设置"libVersion": "2.27.0"。
6. 二次开发实战:从单题答题到支持章节练习与错题本的三步改造
6.1 改造一:题库结构升级——支持「章节」与「难度」标签
原始题库是扁平列表,无法按教学进度组织。升级方案是在questions集合中增加两个字段:
{ "_id": "q_001", "text": "以下哪种水果是红色的?", "chapter": "植物学基础", // 新增:字符串,表示所属章节 "difficulty": 1, // 新增:数字,1=简单,2=中等,3=困难 "options": [...], "correctAnswer": "A" }前端适配:在首页index.wxml中,用wx:for渲染章节列表,点击章节跳转到该章节题目页:
<!-- pages/index/index.wxml --> <view wx:for="{{chapters}}" wx:key="chapter"> <navigator url="/pages/chapterQuiz/chapterQuiz?chapter={{item.chapter}}"> {{item.chapter}}({{item.count}}题) </navigator> </view>云函数增强:修改submitAnswer,在写入answers时,同步写入chapter字段,便于后续按章节统计:
// submitAnswer/index.js 中写入记录部分 const record = { // ...原有字段 chapter: question.chapter, // 新增 difficulty: question.difficulty // 新增 }; await db.collection('answers').add({ data: record });6.2 改造二:错题本功能——基于answers集合的聚合查询
错题本本质是查询answers中isCorrect: false的记录,并关联题目详情。云数据库聚合管道(Aggregation Pipeline)可一步完成:
// cloudfunctions/getWrongAnswers/index.js const cloud = require('wx-server-sdk'); cloud.init({ env: cloud.DYNAMIC_CURRENT_ENV }); const db = cloud.database(); const $ = db.command.aggregate; exports.main = async (event, context) => { const openid = context.OPENID; try { // 聚合:1. 筛选用户错题;2. 关联 questions 集合获取题干 const res = await db.collection('answers').aggregate() .match({ userId: openid, isCorrect: false }) .lookup({ from: 'questions', localField: 'questionId', foreignField: '_id', as: 'questionInfo' }) .project({ _id: 1, questionId: 1, answer: 1, 'questionInfo.text': 1, 'questionInfo.options': 1, 'questionInfo.explanation': 1 }) .end(); return { code: 0, data: res.list.map(item => ({ ...item, question: item.questionInfo[0] // lookup 返回数组,取第一个 })) }; } catch (err) { console.error(err); return { code: 1, msg: '获取错题失败' }; } };前端调用:在错题本页面wrongList.js中:
wx.cloud.callFunction({ name: 'getWrongAnswers', success: res => { if (res.result.code === 0) { this.setData({ wrongList: res.result.data }); } } });6.3 改造三:防作弊加固——限制同一题目 24 小时内只能答一次
这是真实业务需求:避免学生反复刷题记答案。方案是在answers集合中增加复合索引,并在云函数中加查询拦截:
- 数据库层面:在
answers集合创建复合索引{userId: 1, questionId: 1, createdAt: 1}(升序),支持按用户+题目+时间范围查询; - 云函数逻辑增强:在
submitAnswer/index.js开头加入防重逻辑:
// submitAnswer/index.js 开头新增 const twentyFourHoursAgo = Date.now() - 24 * 60 * 60 * 1000; const recentAnswer = await db.collection('answers') .where({ userId: context.OPENID, questionId, createdAt: _.gte(new Date(twentyFourHoursAgo)) }) .limit(1) .get(); if (recentAnswer.data.length > 0) { return { code: 3, msg: '该题目 24 小时内已作答,请明日再试' }; }验证方法:
- 用同一微信号连续提交两次同一题,第二次应返回
code:3; - 修改手机时间为 25 小时后,再次提交,应成功;
- 换另一个微信号提交,不受影响。
从那以后我每次加业务规则,都强制走一遍「最小闭环验证」:改一行代码 → 本地调试 → 真机跑通 → 截图发给需求方确认。不是为了炫技,而是因为微信生态的每一次发布都带着审核等待,少一次返工,就多一天真实用户反馈。希望帮到你。
本文还有配套的精品资源,点击获取