☰
微信答题小程序云开发实战:四个云函数搞定登录抽题判分排行
2026/10/20 13:13:14 网站建设 项目流程

简介:面向微信小程序开发者与云开发初学者,这套基于微信原生小程序和云开发构建的答题小程序项目,提供了从页面交互、用户登录到题库管理、成绩排名的完整实现,适合作为课设、毕设或商业答题活动的快速起步模板。资源包共75个文件,约6.06MB,以png界面截图、wxml/wxss页面结构、json配置和js逻辑文件为主,清晰呈现前端布局、云函数调用及数据存储方式。目前已有222人学习,内容包含首页、答题、详情、历史、排行、结果等核心页面,并附后台题目管理截图、样式指南及README说明,便于对照梳理云开发在小程序中的实际落地路径。开发者可借助该项目快速掌握小程序端调用云函数的完整链路,理解题库数据组织、成绩计算与排行展示等典型业务逻辑,为二次开发节省大量搭建时间。

1. 微信答题小程序:把后端塞进云开发,四个云函数跑通全套答题流程

做答题小程序,最容易被卡住的往往不是前端页面,而是后端:用户身份要跟 openid 绑定,题库得有人维护,交卷要判分,排行榜还得扛住并发写入。这些逻辑如果都放到传统服务器上,光环境搭建、数据库设计、接口鉴权就能消耗掉大半工期。而这个项目把后端整个放进了微信云开发,用四个云函数撑起登录、抽题、判分、排行四个核心环节,数据库和鉴权全部走微信生态自带能力,前端只负责渲染和交互。适合想做企业内部答题活动、知识竞赛、培训考核但又不想长期养后端服务的人,也适合想快速吃透云开发完整玩法的小程序开发者。

2. 架构与技术选型:原生小程序加云开发,四个集合如何分工

2.1 为什么选原生小程序而不是跨端框架

很多团队一上来就会纠结用 uni-app 还是 Taro。我的看法是,答题类小程序的核心传播场景在微信生态内,分享卡片、开放数据域、云开发 SDK 这些能力只有原生小程序能给到最完整的接入体验。跨端框架确实能省一套代码,但换来的是编译链路变长、原生调试变绕、云开发调用偶尔要包一层兼容层,对一个页面不超过五个的小程序来说,收益很低。

原生小程序的 WXML 语法虽然有各种被人吐槽的地方,但配上微信开发者工具里小程序端和云函数端的联动调试,排查问题的效率其实很高。尤其这个项目里云函数和小程序端在同一个工具窗口里管理,右键上传云函数、直接看云端日志,这个体验是跨端框架给不了的。

2.2 云开发三件套在答题链路里的分工

云开发给到的核心能力是云函数、云数据库、云存储三件套。在这个项目里:

  • 云函数承载所有业务逻辑:登录时获取 openid、抽题时服务端取样、交卷时判分并更新排行榜。
  • 云数据库存放结构化数据:用户表、题库表、答题记录表、排行榜表。
  • 云存储放题目图片或用户头像这类静态资源,答题场景用得不多,但题库里如果有图片选项,就会用到。

数据库是整个架构的关键,四个集合的设计决定了后续代码怎么写。

集合名用途关键字段访问方式
users用户基础信息_openid、nickname、bestScore云函数读写,前端不直连
questions题库category、title、options、answerIndex、score、status云函数读取,前端不直连
results答题记录_openid、score、details、createdAt云函数写入,前端仅查看自己记录
rank排行榜_openid、nickname、bestScore、updatedAt云函数读写,前端通过云函数读取

所有集合在小程序端一律禁止直连,读写全部经云函数。这是这套架构里最重要的一个决定:云函数运行在服务端,天然拥有管理权限,不受集合安全规则限制,同时可以在返回前端之前把敏感字段过滤掉。下面看一道题的存储结构。

{ "_id": "q_1001", "category": "安全知识", "title": "灭火器压力表指针在哪个区域表示压力正常?", "options": ["红色", "黄色", "绿色", "蓝色"], "answerIndex": 2, "score": 10, "status": "published" }

注意这里的answerIndex是正确选项下标。题库文档把答案和题目放在同一条记录里,这本身没问题,关键是不能让前端直接拿到这条记录。所以前端通过云函数获取题目时,云函数返回的数据里只有title、options、score,answerIndex在服务端就被剥离了。

2.3 目录结构:云开发项目的标准双端布局

云开发项目在微信开发者工具里天然分成两个目录,cloudfunctions放所有云函数,miniprogram放小程序页面。下面是这个项目的目录结构。

cloud-answer/ ├── cloudfunctions/ │ ├── login/ # 登录,返回 openid │ ├── getQuestions/ # 随机抽题,不返回答案 │ ├── submitAnswer/ # 判分、写记录、更新排行 │ └── getRank/ # 返回排行榜 Top 50 └── miniprogram/ ├── app.js # 云开发初始化 ├── app.json ├── pages/ │ ├── index/ # 首页:选分类、开始答题 │ ├── quiz/ # 答题页:展示题目、接收选项 │ ├── result/ # 结果页:得分、错题解析 │ └── rank/ # 排行榜 └── utils/ └── format.js # 时间与分数格式化

每个云函数目录是独立部署的,意味着改getQuestions只需要单独上传这个函数,不影响其他函数。页面和云函数的对应关系也很清晰:index页调getQuestions拿题,quiz页收集答案后调submitAnswer,rank页调getRank,整个链路没有多余的中间层。

四个云函数的入参出参在设计初期就应该定下来,避免后面页面写完了再去改接口。

云函数入参出参
login无openid、isNewUser
getQuestionscategory、size题目数组(不含答案)
submitAnswerquizId、answersscore、totalScore、resultId
getRank无排行数组

3. 核心模块拆解:一次答题从登录到排行的完整链路

3.1 登录云函数:openid 的获取与用户注册

小程序的登录不应该是前端把用户名密码传上来,而是直接利用微信的身份体系。云函数里通过cloud.getWXContext()拿到当前用户的 openid,这个值由微信服务端注入,前端伪造不了。

// cloudfunctions/login/index.js const cloud = require('wx-server-sdk') cloud.init({ env: cloud.DYNAMIC_CURRENT_ENV }) exports.main = async () => { const { OPENID } = cloud.getWXContext() const db = cloud.database() const users = db.collection('users') const { total } = await users.where({ _openid: OPENID }).count() if (total === 0) { await users.add({ data: { _openid: OPENID, nickname: '答题者', bestScore: 0, createdAt: db.serverDate() } }) } return { openid: OPENID, isNewUser: total === 0 } }

这里用count()判断用户是否已注册,避免每次进入小程序都重复插一条记录。db.serverDate()是数据库服务端时间,避免用户本地时钟不准导致的时间混乱。前端在app.js的onLaunch里调用这个云函数,拿到 openid 之后存到全局变量,后续所有请求都不需要再传用户标识,云函数自己就能从上下文里取。

3.2 题库下发:聚合 sample 做服务端随机抽题

答题应用最忌讳把整个题库拉到前端再随机,一是浪费流量,二是把题量暴露给了用户。这个项目的getQuestions云函数直接在服务端完成过滤和抽样。

// cloudfunctions/getQuestions/index.js const cloud = require('wx-server-sdk') cloud.init({ env: cloud.DYNAMIC_CURRENT_ENV }) exports.main = async (event) => { const { category, size = 10 } = event const db = cloud.database() const match = { status: 'published' } if (category && category !== 'all') match.category = category const { list } = await db.collection('questions') .aggregate() .match(match) .sample({ size }) .end() return list.map(q => ({ _id: q._id, title: q.title, options: q.options, score: q.score })) }

聚合操作里的sample({ size })对应数据库底层的随机抽样,题目数量大时性能比「先全量查出来再前端 shuffle」好一个量级。match阶段先按分类和上下架状态过滤,sample再从中随机取指定数量。返回字段里刻意没有answerIndex,前端拿到的题目天然不含答案,就算有人抓包也猜不到正确选项。

参数category来自首页的选择,比如「安全知识」「企业文化」「产品常识」,传all表示全题库随机;size是一次答题的题目数,默认 10,可以在前端按需调整。

3.3 交卷判分:一次云函数完成写入与排行更新

交卷是逻辑最重的一步:拿到用户提交的答案数组,逐题比对题库里的正确答案,算出得分,写入答题记录,再更新排行榜里的历史最高分。

// cloudfunctions/submitAnswer/index.js const cloud = require('wx-server-sdk') cloud.init({ env: cloud.DYNAMIC_CURRENT_ENV }) exports.main = async (event) => { const { OPENID } = cloud.getWXContext() const { quizId, answers } = event const db = cloud.database() const _ = db.command const ids = answers.map(a => a.questionId) const { data: questions } = await db.collection('questions') .where({ _id: _.in(ids) }) .get() let score = 0 const details = questions.map(q => { const picked = answers.find(a => a.questionId === q._id).selected const correct = picked === q.answerIndex if (correct) score += q.score || 1 return { questionId: q._id, correct, answerIndex: q.answerIndex, picked } }) const res = await db.collection('results').add({ data: { _openid: OPENID, quizId, score, details, createdAt: db.serverDate() } }) const { total } = await db.collection('rank').where({ _openid: OPENID }).count() if (total === 0) { await db.collection('rank').add({ data: { _openid: OPENID, nickname: event.nickname || '答题者', bestScore: score, updatedAt: db.serverDate() } }) } else { await db.collection('rank').where({ _openid: OPENID }).update({ data: { bestScore: _.max(score), updatedAt: db.serverDate() } }) } return { score, totalScore: questions.length, resultId: res._id } }

判分逻辑并不复杂,重点在于排行榜更新用了_.max(score)这个原子操作。db.command.max会把字段更新成字段当前值和传入值里的较大者,这一步避免了并发交卷时「先读旧分再写新分」导致的高分被低分覆盖问题。即使两个交卷请求同时到达,数据库端也会按原子语义处理,谁的分高最终留谁的。

answers入参是一个数组,每个元素包含questionId和selected两个字段,selected是用户选中的选项下标。返回的details里带了每道题的正确答案和用户选项,前端结果页可以直接拿来展示错题解析,不需要再查一次题库。

3.4 排行榜:聚合排序与 Top N

排行榜的读取用聚合排序就能完成。按最高分倒序,同分时按更新时间正序,这样先达到该分数的人排前面。

// cloudfunctions/getRank/index.js const cloud = require('wx-server-sdk') cloud.init({ env: cloud.DYNAMIC_CURRENT_ENV }) exports.main = async () => { const db = cloud.database() const { list } = await db.collection('rank') .aggregate() .sort({ bestScore: -1, updatedAt: 1 }) .limit(50) .end() return list }

排行榜展示 Top 50,排序在服务端完成,前端拿到的直接就是有序数组。需要展示「我的排名」时,前端拿到这个数组后根据自己的 openid 去定位即可。聚合查询跑在数据库引擎里,不消耗小程序端流量,也避免了前端逐条读取再排序的低效写法。

4. 避坑指南:题库权限、云函数超时与排行榜并发的那些坑

4.1 云函数超时:一次把整个题库拉出来翻车了

现象:题库录入到一千道之后,getQuestions偶尔报Function Timeout,答题高峰期尤其明显。

原因:初版代码用的是「先把where({ status: 'published' })查出来的所有题放到数组里,再用Math.random()打乱取前十条」的方案。云函数默认超时时间是 3 秒,题库超过一定量之后,全量读取加上排序耗时把时间窗占满了,叠加冷启动直接超时。

解决:改成aggregate().sample({ size })服务端抽样,数据库只需要返回指定数量的随机文档,扫描量从全库变为抽样。同时给questions集合的status和category字段加上索引,让match阶段的过滤走索引,避免全表扫描。从那以后我接入任何云函数前都会先确认数据量级和查询是否走索引,而不是等超时了再排查。

4.2 数据库权限陷阱:前端直读题库等于把答案挂网上

现象:开发阶段为了方便调试,把questions集合的权限调成了「所有人可读」。结果真机测试时发现,懂点技术的人用调试器直接在小程序端执行db.collection('questions').get(),能拿到包括answerIndex在内的全部题库数据。

原因:集合权限一旦设置成所有人可读,小程序端直连数据库的操作就不受任何逻辑限制了。云开发数据库的四种预设权限里,「所有人可读」意味着任何拿到小程序的人都能读取这个集合的全部记录。

解决:所有集合一律不对前端开放直连权限,维持默认的「仅创建者可读写」,前端只通过云函数访问数据。云函数端天然拥有管理权限,不受集合安全规则限制。同时云函数返回给前端的数据在代码里明确过滤过字段,即使请求链路被截获,拿到的也不含答案。这里的关键习惯是:数据敏感度判断不能依赖前端代码,必须在服务端返回层就做收敛。

4.3 排行榜并发更新:读改写让最高分被覆盖

现象:内部测试时多人同时交卷,某个用户的成绩从 90 分提升到 95 分,但排行榜上显示的还是 90。

原因:初版更新排行用的逻辑是「先where({ _openid }).get()查出旧的 bestScore,比较后算出新值再 update」。两个请求同时进来时,后一个请求读到的是更新前的 90,算完新值写回去,把已经写入的 95 覆盖掉了。这是典型的「读-改-写」竞态。

解决:改成_.max(score)原子更新,把比较逻辑交给数据库引擎而不是云函数运行时。db.command.max会在服务端原子地比较旧值和新值并写入较大者,从根上消除并发覆盖。这类问题在自建服务器里要用事务或乐观锁才能解决,云开发一条原子操作就够,前提是你要知道有db.command这套操作符。

4.4 本地缓存黑匣子:题库更新后用户还在答旧题

现象:运营在后台把一道错题答案修正了,但老用户打开小程序还是看到旧答案,换台手机又能看到新内容。

原因:答题页把题目数组缓存到了wx.setStorageSync,缓存 key 只跟分类相关。题库更新后缓存没有失效机制,用户看到的是上一次进入时留下的旧数据。

解决:最省事的方案是不缓存题目本身,答题页每次进入都从云函数拉取最新数据。如果一定要做缓存,在questions集合里维护一个版本号字段,缓存 key 拼上版本号,版本变化时旧缓存自然读不到。我的做法是:题目数据量只有几百条的情况下完全不做本地缓存,把存储留给答题进度和用户设置,换来的是「永远答新题」的确定性。

5. 部署与运行:从 AppID 创建到真机跑通

5.1 环境准备:AppID 与云开发环境

把项目跑起来之前,先确认你手上有一个真实的小程序 AppID,而不是微信开发者工具里的测试号。测试号不支持云开发,这是最容易被忽略的前置条件。个人主体注册的小程序就能开通云开发,不要求企业资质,注册完拿到 AppID 即可。

用微信开发者工具导入项目根目录,在「详情」里把 AppID 替换成自己的。然后点击工具栏的「云开发」按钮,按引导开通并创建一个云开发环境。创建成功后记下环境 ID,格式类似cloud1-xxxxxxxx,这个值稍微后会填进app.js。云开发自带免费额度,这个量级的答题小程序在额度内完全可以跑起来,费用这块不用太担心。

5.2 初始化数据库与集合权限

在云开发控制台左侧找到「数据库」,手动创建users、questions、results、rank四个集合。集合权限维持默认的「仅创建者可读写」即可,因为按照前面的架构设计,前端完全不直连数据库,所有读写都发生在云函数端,而云函数不受集合权限限制。

题库初始化可以直接在控制台操作:打开questions集合,点「导入」,选择 JSON 文件。文件内容是一个数组,每个元素对应一条题目记录。

[ { "category": "安全知识", "title": "灭火器压力表指针在哪个区域表示压力正常?", "options": ["红色", "黄色", "绿色", "蓝色"], "answerIndex": 2, "score": 10, "status": "published" } ]

导入时注意_id不需要你提供,云开发会自动生成。status统一填published,前端抽题只认这个状态值,后续如果要做题库上下架,把状态改成draft就能让题目下架。

5.3 上传云函数与真机调试

在开发者工具左侧文件树里,对着cloudfunctions下的每个函数目录右键,选择「上传并部署:云端安装依赖」。这一步会把本地代码连同package.json里声明的依赖一起打包上传。四个函数要逐个上传,不能只传一个。

然后打开miniprogram/app.js,检查wx.cloud.init里的env是否填成了你自己的环境 ID。

// miniprogram/app.js App({ onLaunch() { wx.cloud.init({ env: 'cloud1-xxxxxxxx', traceUser: true }) } })

traceUser开启后可以在云开发控制台看到用户访问链路,排查问题时比较有用。编译并在模拟器里点一遍登录、答题、交卷、看榜,确认没有报错后,点击「预览」生成二维码,用真机扫码调试。

真机调试阶段最常见的三个报错:

  • Cloud API isn't enabled:AppID 没开通云开发,或者环境还没创建成功。
  • errCode: -501000云函数不存在:云函数没有上传成功,或者调用的函数名和上传的目录名拼写不一致。
  • collection not exists:四个集合没有全部创建,或者集合名写错。

遇到这三类问题,回到控制台逐个确认环境和集合状态,基本能定位。云函数冷启动偶尔会让第一次调用变慢,这不是代码 bug,多调两次或者等几秒再试即可。

6. 二次开发:题库批量导入、倒计时与错题本改造思路

6.1 题库批量导入

控制台手动导入适合几十上百道题,题库量到千级之后,更推荐写一个一次性云函数或本地 Node 脚本连接云数据库批量写入。常见做法是把题目整理成 CSV,脚本里逐行转成文档结构,分批写入并打上status: 'published'。批量导入最容易踩的坑是重复导入导致题目翻倍,导入前先用category加title做一次去重查询,或者干脆导入前清空集合。

6.2 给答题页加倒计时

quiz页加载题目时同时启动一个倒计时:

// miniprogram/pages/quiz/quiz.js let timer = null Page({ onLoad(options) { this.setData({ remain: options.duration || 60 }) timer = setInterval(() => { const next = this.data.remain - 1 if (next <= 0) { clearInterval(timer) this.submit() } else { this.setData({ remain: next }) } }, 1000) }, onUnload() { if (timer) clearInterval(timer) } })

注意页面卸载时必须clearInterval,否则用户中途退出答题页,定时器还在跑,会在后台触发一次多余的submitAnswer调用。

6.3 错题本复用成绩数据

submitAnswer返回的details里已经包含每道题的正确状态、正确答案和用户选项,结果页可以直接用来展示解析,不需要再查题库。要做错题本的话,把details里correct为false的题目单独存一个集合或塞进本地缓存,下次从首页入口进入错题复习模式即可。判分逻辑在云函数里是统一的,前端只负责展示,改造成本不高。

这套项目的核心收益在于架构简单:前端不碰库、云函数不掺页面逻辑、集合权限按最严标准设置。从那以后我每接到一个云开发小程序需求,都会先画一遍「哪些数据前端能直连、哪些必须走云函数」的权限表再动手写代码。希望这个项目的拆解思路能帮到你,少走几个我走过的弯路。

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

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询