云开发这个词,最近都快被各种考试和竞赛卷出花了。我这次接到的任务就是一个“云开发考试用”的在线考试项目,内部代号就叫 qwe123,要求基于 Serverless 云开发实现题库管理、组卷发卷、限时答题、自动判分、成绩统计这些核心模块。说实话,刚拿到题目的时候我也没底,毕竟平时写前端多,后端基建都是能用现成模板就不自己碰。但一套撸下来,我发现云开发真正的门槛根本不在于写业务代码,而在于把环境、权限、数据模型这三块底子打好。这篇文章就按照我从零到一搭这套考试系统的实操顺序复盘一遍。
如果你正准备云开发相关的上机考试或认证,或者想给班级/公司快速做一个轻量的考试答题工具,这篇可以直接当操作参考。我会把每一处选型和关键步骤的“为什么这么做”也一起讲明白,尽量少让你走弯路。
1. 先算清楚:考试系统为什么要用云开发
1.1 云开发到底解决了什么问题
传统考试系统的后端开发,典型链路是至少买一台云服务器或容器,装 Nginx、PHP/Java/Node 运行环境,再配 MySQL/Redis,然后处理鉴权、跨域、对象存储上传等一堆脏活。流程非常成熟,但也很重。
问题是,考试场景往往有一个明显的时间约束:一个开发考核给你三五天,甚至一次上机考试只有几小时,你不可能把时间全砸在建表、搭环境、调部署上。云开发的价值就在这里,它直接把服务器、数据库、存储、身份鉴权打包成了云服务,你只需要关心业务逻辑。比如身份鉴权,考生打开小程序自动获取 openid,不需要自己写注册登录和 token 签发。
我当时给这套考试系统定的技术路线非常简单:云函数承担所有业务逻辑,云数据库负责存题库和成绩,云存储用于放批量导入的 Excel 题目附件和考生上传的作答文件。整个项目没有买一台虚拟机,没有配一次反向代理,最后照样稳定跑完了模拟考试。
1.2 功能需求拆解成一张业务表
考试类应用听起来简单,实际做起来功能点不少。如果你直接开始写代码,很容易漏掉某个边界。我习惯先把需求列成一张大表,每个功能点对应到云开发的具体资源和实现方式。
| 功能模块 | 场景描述 | 云开发承载方式 |
|---|---|---|
| 考生身份 | 小程序打开后自动识别用户 | 云函数获取 openid,写入 users 集合 |
| 题库管理 | 维护题目、分类、正确答案、分值 | questions 集合,管理端操作 |
| 组卷发卷 | 从题库随机抽题生成一份试卷 | createPaper 云函数 |
| 限时答题 | 记录开始时间,提交时校验超时 | papers 集合与日期字段 |
| 自动判分 | 比对答案、返回分数 | submitPaper 云函数在服务端完成 |
| 成绩统计 | 查看我的历史成绩、排行 | scores/papers 集合查询聚合 |
| 批量导题 | 管理员上传 Excel 一次导入 | 云存储 + importQuestions 云函数 |
拆完之后能看得很清楚:所有操作最终都落在“集合 + 云函数”这两个抽象上。没有表关联、没有 ORM、没有复杂事务,至少在考试系统这个量级,云开发单薄得刚好够用。
1.3 环境命名的小教训:qwe123 真的只配当练习名
项目代号 qwe123 是题目里给的,我也真就用它创建了云开发环境。环境 ID 创建之后是改不了的,后面我把代码传到 Git 仓库之后才发现,里面到处散落着 qwe123 这个字符串,想隐藏代码里的真实环境名已经来不及了。因为只是练习项目问题不大,但如果这是上线项目,相当于把你的环境标识公开了,容易被人拿去试探接口。
我的建议是练习环境随便命名没问题,越容易记越好;正式环境一定要用类似 exam-prod-a1b2c3 这种带明确含义和随机后缀的 ID,并配合环境变量或配置文件来引用,不要把环境 ID 写死在代码的各个角落里。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 环境初始化与项目脚手架搭建
2.1 开通云开发环境:控制台和 CLI 两条路
这个考试项目我用了微信小程序作为前端容器,因为微信生态下云开发的接入成本最低,很多能力开箱即用。第一步是在微信开发者工具中点击“云开发”按钮,按提示开通。
开通时会让你输入环境 ID,很多教程会跳过这个细节,但它非常关键。环境 ID 是后续所有代码里都要用到的标识,我建议你直接用英文小写加数字组合。比如 qwe123,虽然不够正式,但作为练习环境输入简单,不容易打错。
如果你更习惯命令行,云开发也提供了官方 CLI:
bash复制npm i -g @cloudbase/cli
tcb login
tcb env:create qwe123
tcb env:list
一般学校或考试机房里没要求 CLI,用开发者工具可视化创建就行。但我会在本地装好 CLI,因为后面上传云函数、查看日志时比 UI 操作要快不少。
2.2 小程序前端初始化与云能力启用
创建完环境后,在项目根目录的 app.js 里加入初始化代码:
js复制App({
onLaunch() {
if (!wx.cloud) {
console.error('当前基础库版本过低,请使用 2.2.3 以上版本')
return
}
wx.cloud.init({
env: 'qwe123',
traceUser: true
})
}
})
这段代码的作用是告诉小程序:所有云开发调用都发往 qwe123 这个环境。traceUser 开启后,云开发后台能看到每个用户的操作行为,排查问题时非常有用。
很多新手会漏掉 env 参数,结果在开发者工具里测试没问题,真机预览时数据却对不上——因为开发者工具默认指向了工具自己的测试环境。这是考试环境最容易翻车的点之一,务必确认 env 字段和你在控制台开通的环境 ID 完全一致。
2.3 Taro init 到底有没有“云开发版本”
在准备这个项目时,我也搜过 Taro 脚手架和云开发结合的问题。看热词想搜“tarojs init 有没有云开发版本”的同学,大概率是遇到了一个认知误区:Taro 是一个编译到多端的开发框架,云开发是后端服务,两者并不是二选一的关系,也就没有所谓的“云开发版本 Taro 脚手架”。
Taro 项目里接入云开发,实际上分两步走。第一步还是正常执行 taro init exam-app,选择你熟悉的 React 或 Vue 模板;第二步是安装云开发对应的 SDK,并针对不同运行端做初始化。在小程序端可以这样判断:
js复制if (process.env.TARO_ENV === 'weapp') {
wx.cloud.init({ env: 'qwe123', traceUser: true })
}
如果你要编译成 H5,则用官方 Web SDK:
bash复制npm i @cloudbase/js-sdk
js复制import cloudbase from '@cloudbase/js-sdk'
const app = cloudbase.init({ env: 'qwe123' })
所以“tarojs init 有没有云开发版本”这个问题的答案就是:没有,也不需要有。脚手架能给你的只是项目结构,云开发接入是代码层面的活儿。
2.4 考试项目目录结构
我在项目里同时放了前端页面和云函数目录,后端逻辑统一放在 cloudfunctions 下,一个云函数一个文件夹,方便独立上传和调试:
text复制exam-app/
├── src/
│ ├── pages/
│ │ ├── exam/ // 考试页
│ │ ├── result/ // 成绩页
│ │ └── admin/ // 管理端导题页
│ └── utils/
│ └── cloud.js // 云环境初始化与公共方法
└── cloudfunctions/
├── login/
├── createPaper/
├── submitPaper/
└── importQuestions/
到现在这个阶段,项目基本能跑起来了。接下来要进入正题,把这些页面和云函数串成真正的考试闭环。
3. 核心链路:题库、组卷、答题判分的实现
3.1 数据库集合设计与权限规则
云开发数据库是文档型 NoSQL,和 MySQL 那套思维方式完全不同。考试系统我设计了四个集合,结构如下。
| 集合名 | 主要字段 | 说明 |
|---|---|---|
| users | openid, name, studentNo | 考生信息,注册时填写 |
| questions | type, category, title, options, answer, score | 题库,答案字段用于判分 |
| exams | title, startTime, endTime, duration | 考试场次配置 |
| papers | examId, openid, questionIds, answers, score, status | 每名考生的一张答卷记录 |
数据库权限是这套系统里最容易埋雷的地方。如果直接在控制台把 questions 设为“所有用户可读”,那考生在手机上就能看到接口返回的 answer 字段,整个考试就废了。我的处理是:所有集合默认不开放客户端直接读写,题目读取、答卷提交全部通过云函数完成。云函数端默认拥有管理员的数据库权限,不受前端安全规则限制,也不需要在客户端暴露表结构。
另外需要注意,云开发数据库的文档在创建时会自动带上 _openid 字段,表示这个文档的创建者。非微信端或云函数里写入的文档没有这个字段也不受影响,你可以在云函数里手动维护 openid 字段。
3.2 组卷抽题:用聚合解决随机取样
组卷的需求是:从题库里随机抽取指定数量的题目,生成一张唯一的试卷。数据库查询有个天然限制:get 一次返回的记录条数有限,如果题库量大了,直接把全表拉到本地再随机 index 的做法不现实。
我的做法是用聚合的 sample 操作在数据库端完成随机取样:
javascript复制const cloud = require('wx-server-sdk')
cloud.init({ env: cloud.DYNAMIC_CURRENT_ENV })
const db = cloud.database()
const _ = db.command
exports.main = async (event) => {
const { examId, category, count } = event
const $ = db.command.aggregate
const res = await db.collection('questions')
.aggregate()
.match({ category, status: 'enabled' })
.sample({ size: count })
.end()
const questions = res.list.map(q => ({
_id: q._id,
type: q.type,
title: q.title,
options: q.options,
score: q.score
}))
// 创建一份答卷,先不保存答案
const paperResult = await db.collection('papers').add({
data: {
examId,
openid: cloud.getWXContext().OPENID,
questionIds: res.list.map(q => q._id),
answers: {},
score: -1,
status: 'ongoing',
startTime: Date.now()
}
})
return { paperId: paperResult._id, questions }
}
这个云函数有几个细节:
第一,sample 是在数据库端完成的随机抽样,不会把题库全量传输到本地,就算题库几千道题也能撑住。第二,返回给前端的数据里我把 answer 字段主动剔除掉了。第三,题目虽然随机抽了,但完整答案存留在服务端数据库,前端自始至终拿不到正确答案,这是后面防作弊的关键。
3.3 交卷判分:为什么不能在前端做
有人会觉得,判断答案不就是 if(option === answer) 吗?直接在前端写完判分再把结果提交给数据库不就行了。这个想法非常危险。
前端代码理论上是可以被逆向和改写的,哪怕正常考生不使用工具,你把判分逻辑放在前端也意味着规则暴露给了所有会看控制台的人。考试系统里,判分必须在云函数里做,这样至少能保证分数结果不是从浏览器里伪造出来的。
提交答卷的云函数我这样写:
javascript复制exports.main = async (event) => {
const { OPENID } = cloud.getWXContext()
const { paperId, answers } = event
const paperRes = await db.collection('papers').doc(paperId).get()
const paper = paperRes.data
if (!paper) {
return { code: -1, msg: '答卷不存在' }
}
if (paper.openid !== OPENID) {
return { code: -1, msg: '无权提交他人答卷' }
}
if (paper.status === 'submitted') {
return { code: -1, msg: '不能重复提交' }
}
const questionRes = await db.collection('questions')
.where({ _id: _.in(paper.questionIds) })
.get()
let totalScore = 0
let correctCount = 0
const detail = {}
questionRes.data.forEach(q => {
const userAnswer = answers[q._id] || ''
const isCorrect = String(userAnswer) === String(q.answer)
if (isCorrect) {
totalScore += q.score
correctCount++
}
detail[q._id] = {
userAnswer,
correct: isCorrect
}
})
await db.collection('papers').doc(paperId).update({
data: {
answers: detail,
score: totalScore,
correctCount,
status: 'submitted',
submitTime: Date.now()
}
})
return { code: 0, score: totalScore, correctCount }
}
这里还对超时提交做了隐式控制,因为试卷创建时的 startTime 已经记录,如果前端有倒计时,后端在正式场景下还要再增加一道校验,即当前时间是否超过 exam 的结束时间。永远别相信前端传过来的时间戳,你自己在云函数里 Date.now() 才算数。
3.4 考生身份:从 openid 到用户信息
考试系统毕竟是面向“人”的,总要知道谁参加了考试。云开发在小程序端会自动给每个用户一个 openid,同一个用户在你的小程序里永远不变,这才是天然的账号体系。
登录云函数逻辑很简单:
javascript复制exports.main = async () => {
const { OPENID } = cloud.getWXContext()
const userRes = await db.collection('users').where({ openid: OPENID }).get()
if (userRes.data.length > 0) {
return { code: 0, user: userRes.data[0] }
}
return { code: 1, msg: '未注册' }
}
第一次登录时,让考生填写姓名和学号,存在 users 表里;后续再考其他场次,不需要重复提交身份信息。这里注意,云函数里获取用户身份要用 cloud.getWXContext(),而不是信任前端传过来的 openid。前端组件的任何值都能伪造,云函数上下文体里拿到的 openid 才是小程序运行时环境中帮我们确认好的身份证据。
4. 管理端、批量导题与扩大应用边界
4.1 用云存储实现 Excel 批量导入
考试系统的出题人通常是老师或行政同事,你不可能要求他们一条一条在小程序页面里手动录入题目。实际可行的方式是先做一个 Excel 模板,让管理员填好,再上传到云存储。
具体实现上,管理页把文件上传后拿到一个 fileID,再调导入云函数:
javascript复制const cloud = require('wx-server-sdk')
const xlsx = require('node-xlsx')
cloud.init({ env: cloud.DYNAMIC_CURRENT_ENV })
exports.main = async (event) => {
const { fileID } = event
const fileRes = await cloud.downloadFile({ fileID })
const sheets = xlsx.parse(fileRes.fileContent)
const rows = sheets[0].data
// 第一行是表头,从第二行开始解析
for (let i = 1; i < rows.length; i++) {
const [category, type, title, optionA, optionB, optionC, optionD, answer, score] = rows[i]
await db.collection('questions').add({
data: {
category,
type,
title,
options: [optionA, optionB, optionC, optionD],
answer,
score,
status: 'enabled',
createTime: Date.now()
}
})
}
return { code: 0, total: rows.length - 1 }
}
这个函数看起来简短,但从方案到落地有两个坑。第一,云函数环境里要用 npm 安装 node-xlsx 依赖,而且上传云函数时必须选择“云端安装依赖”,否则找不到模块。第二,一次导几百道题没有问题,如果上万道题就可能碰到云函数耗时上限,我的建议是分批导入,控制每次不超过几百条。
4.2 部署上线前要过三道手续
很多项目卡在本地能跑、上线报错这一步。云开发项目部署时,我建议按这个顺序排查。
第一步,在开发者工具中对每个云函数右键选择“上传并部署:云端安装依赖”。只上传不装依赖会导致运行时出现 module not found。第二步,检查云函数超时时间。考试判分和导题这种耗时操作,如果只保留默认的 3 秒,很容易在数据量大时超时,建议在云函数配置里把超时调到 20 秒左右,配合前端 loading 提示。第三步,如果你项目跑在 H5 而不是小程序里,一定别忘了在 CloudBase 控制台的“安全配置”里添加 Web 安全域名,不加的话浏览器端会被拦截请求。
4.3 本地调试与真机预览要区分开
我在开发那几天基本是早上写逻辑、下午真机预览调试。比较坑的是本地调试和真机环境的数据库环境可能指向不同的环境,导致本地怎么测都正常,手机上一查数据就发现是空的。
我的习惯是,在 utils/cloud.js 里把所有环境相关配置收敛到一个文件,明确标注本地调试环境 ID 和正式环境 ID。上线前全局搜索一遍环境 ID 字符串,确认没有混用。如果团队多人协作,这个文件最好也提交到仓库但保持只读,不要每个人都改成本地环境再提交,否则冲突会非常严重。
4.4 云开发不是万能药:传统后端与云点播的边界
前面提到的热词里,有人搜“thinkphp6 开发阿里云点播”,这个方向其实和云开发考试系统要解决的问题不太一样,但也值得放在一起思考边界。如果你的考试系统包含大段视频题目、直播监考、实时多人互动,这时候云函数并不擅长——它适合短周期请求,不适合长连接。
有现成 ThinkPHP6 后端团队的话,更合理的架构是各司其职:ThinkPHP6 负责对接阿里云点播的 SDK,上传视频、获取播放凭证、管理转码任务;云开发负责承载小程序端考试业务和题库数据。云开发解决的是快速交付和免运维的问题,传统后端解决的是复杂生态集成问题,两者完全可以同时存在。考 cloud 的题目未必要求你抛弃旧技术,而是考察你知不知道在正确的位置放正确的工具。
5. 复盘:这轮考试项目里踩过的坑
5.1 环境 ID 和权限配置翻车盘点
整个项目里让我花时间最多的问题,反而不是代码 bug,而是环境与权限。
第一个坑是环境 ID 写错。本地调试环境用的是 qwe123-dev,正式环境是 qwe123-prod,有一版代码里只改了部分页面,结果考试成绩存到了测试环境。这个问题的排查方式很笨,我只能一条一条看云函数日志里的环境编号。后来吸取教训,前端只通过一个全局配置变量引用环境 ID,上线时不要去全文替换。
第二个坑是数据库权限误设。考试需要前端直接读题库吗?不需要。但我在第一次构建时图省事,把 questions 集合设成了“所有用户可读”。虽然云函数判分时没问题,可一旦有人用控制台把所有题库文档拉下来,答案就泄露了。正确的处理应该是把集合权限全部收口到云函数,客户端不要有直读数据库的入口。
5.2 云数据库查询的条数限制
NoSQL 数据库看着简单,但写查询时很容易遇到条数限制。不同端和不同版本的云数据库单次 get 返回条数限制不一样,具体数值在官方文档里有说明,实践中我记住一个总原则:任何端都不应该一次性把整个集合拉下来,查询条件能缩就缩,数量能分页就分页。
在阅卷统计时,需要计算所有人的平均分。最简单的方案是先 count 一下总人数,再由云函数分批查询所有成绩,累加后除以总数。不要图省事直接 get 全表,因为一旦记录数超过单次返回上限,你的平均分就会算偏低,而且这种错误很难肉眼发现。
5.3 云函数超时与冷启动的优化
考试刚开始时使用量集中,很容易触发云函数冷启动。现象是前端点击“开始考试”按钮后要等 2 秒甚至更久才有响应,这在真实考场上会让人很焦虑。
我采取的优化方式有三层:第一,把常用的浏览题库、查询考试列表这类只读操作改成客户端直接读数据库,前提是安全规则允许;只有组卷、判分这些敏感操作用云函数。第二,在非考试时段用定时触发器提前调用一次核心云函数,让实例预热的方案在微信云开发里不太可控,但也可以在逻辑里减少无关数据库操作来缩短运行时。第三,前端做过渡状态,点击后立即显示 loading 并按钮置灰,防止有人反复点击导致多个云函数并发执行,同一个考生生成了多份答卷。
5.4 考前自检清单
以下清单是我在正式模拟考前跑一遍的检查列表,强烈建议你复制一份:
| 检查项 | 操作要点 | 状态 |
|---|---|---|
| 环境 ID | 全局配置与线上环境一致 | 已确认 |
| 集合权限 | questions 不开放给客户端直读 | 已确认 |
| 云函数依赖 | 所有函数已上传且云端安装依赖 | 已确认 |
| 判分不可伪造 | 答案比对在云函数内完成 | 已确认 |
| 重复提交拦截 | papers 表状态位已校验 | 已确认 |
| 超时与会话 | 考试时间校验基于服务端时间 | 已确认 |
| 真机预览 | 换了 3 台测试机,微信基础库正常 | 已确认 |
5.5 手动模拟一次全流程考试
最后,我在正式考试使用前做了一次全链路演练:注册一个新微信号模拟考生,导入 20 道题,创建一场考试,用这 20 道题完整做了一遍,故意错 5 道,然后验证成绩是否正确、能否重复交卷、管理端是否能看到统计数据。这个过程只花了不到十分钟,却把数据库权限、openid 映射、判分逻辑都验证了一遍。
真正到了考场上,我才发现考前花这十分钟有多值。因为现场紧张状态下你不会再有精力去看代码逻辑,任何一个小配置错了,都可能直接把人打懵。
云开发这套系统做完以后,我最大的体会是:它把所有后端复杂度都收走了,但把“数据边界设计”和“权限边界设计”这两件事的重要性放大了。以前写考试系统要考虑服务器并发、数据库连接池、缓存策略;现在这些都不用想了,但如果集合里漏了一条安全规则,或者答题答案被客户端代算,那整场考试的公信力就没了。用云开发考试,拼的已经不只是写业务代码的速度,更是你有没有建立起一套清晰的云端数据安全直觉。
