☰
微信小程序+ThinkPHP+uni-app在线考试模拟系统开发复盘与实战方案
2026/10/6 5:11:48 网站建设 项目流程

做在线考试系统,我一开始想的是做一个网页版就够了,结果需求方一句话把我拉了回来:"老师们基本都只拿手机看,学生也都有微信,没人愿意为了做个模拟考试去专门装 App。"于是方案就变成了微信小程序,后端用 ThinkPHP 出接口,前端用 uni-app 写一遍,编译成微信小程序跑。这套组合做完以后,我发现它其实非常适合中小型在线考试模拟系统——开发效率高、跨端能力强、部署成本也低。这篇内容就是完整的项目复盘,包含数据库设计、后端接口逻辑、小程序端页面实现,以及打包上线过程中踩过的坑,给正在做类似"微信小程序 + thinkphp + uniapp"项目的朋友一个可直接参考的实现路径。

1. 在线考试系统的整体架构与关键技术选型

1.1 为什么是 uniapp + thinkphp 而不是其他组合

先说说选型逻辑。前端这块,uni-app 最大的价值是一套代码可以编译到微信小程序、H5、App 等多个平台。我做的是考试模拟系统,虽然首期只要求微信小程序,但需求方话里话外流露出"以后可能要做 App 版、要开网页端给学生电脑上做题"的意图,如果前端用原生微信小程序写,后面迁移就是重写一遍。uni-app 用的是 Vue 语法,团队成员只要会 Vue 就能直接上手,招人成本也低。

后端选 ThinkPHP,原因更直接:国内生态成熟,文档和现成轮子多,Composer 扩展丰富,数据库操作简单,不用像 Java 那样配一堆环境。TP6 的架构比 TP5 更干净,路由、中间件、依赖注入都有,做一个中低并发量的考试系统完全够用。你要说用 Spring Boot 行不行?当然行,但对这个体量的项目属于杀鸡用牛刀,开发周期至少翻一倍。

整体信息流是这样一个闭环:

  • 用户打开微信小程序,前端通过wx.login()拿到临时 code,传给 ThinkPHP 后端
  • 后端用 code 向微信接口换取 openid,生成登录态 token 返回给小程序
  • 小程序拿到 token 后,请求试卷列表、试卷题目、提交答案等所有操作都带着 token
  • 交卷时后端自动判分,把分数、正确率、逐题解析写回数据库
  • 小程序端展示成绩单和答题解析

1.2 功能模块拆解与核心流程

在线考试模拟系统看着简单,拆开以后功能其实不少:

  • 用户模块:微信静默登录、头像昵称授权、登录态维护
  • 考试模块:试卷列表、开始考试、逐题作答、答题卡跳转、倒计时、交卷
  • 成绩模块:交卷判分、成绩单展示、逐题解析
  • 管理模块:题库维护、试卷配置、成绩统计(这个一般做后台,不在小程序里)

考试主流程是:进入试卷列表 -> 选择一套试卷 -> 点开始考试 -> 逐题作答 -> 手动交卷或倒计时结束自动交卷 -> 后端判分 -> 展示成绩和解析。

这里有一个关键设计决策必须提前想清楚:题目和答案怎么下发。有两种做法:

  1. 一次性下发整套题的题目和答案,前端本地判分
  2. 只下发题目,交卷时把答案传给后端,后端统一判分

很多人图省事选方案 1,但这是典型的埋雷行为。微信小程序前端代码可以被反编译,就算代码混淆过,接口里的数据也能被抓包,答案直接暴露。我做的是方案 2,题目下发时答案字段一律不返回,判分完全在后端做,前端拿到的分数只做展示。别怕后端判分多那几毫秒,考试系统这点并发量根本不算压力。

2. 数据库设计:题库、试卷与答题记录的完整表结构

数据库是整个考试系统的基础,表设计得好不好,直接决定后面开发是顺手还是别扭。我的核心表一共五张:用户表、题库表、试卷表、试卷题目关联表、答题记录表,另外加一张答题明细表用来存每道题的作答情况。

2.1 用户表与题库表

用户表很简单,微信小程序登录后能拿到的信息就那些,别存多余字段:

字段类型说明
idint自增主键
openidvarchar(64)微信用户唯一标识
nicknamevarchar(64)昵称
avatarvarchar(255)头像地址
tokenvarchar(64)登录态令牌,每次登录刷新
create_timedatetime注册时间

题库表字段一开始我觉得想得很全了,后来真做才发现少了两个很重要的字段,一个是difficulty,一个是category_id。没有难度分级,以后想搞"随机组卷出几道简单题几道难题"就完全没法做;没有分类,题库一多管理起来就是灾难。

字段类型说明
idint自增主键
question_typetinyint1单选,2多选,3判断
contenttext题干
optionstext选项内容,JSON格式存储
answervarchar(255)正确答案,多选存多个选项值
analysistext答案解析
difficultytinyint难度,1-5
category_idint知识点分类
statustinyint1启用,0禁用

关于options为什么用 JSON 文本而不是单独建一张选项表,我是这么考虑的:在线考试系统的题目选项数量相对固定(单选 4 个选项,多选 5 个,判断也是 2 个),用 JSON 存最简单,ThinkPHP 里json_decode一下就是数组,前端拿到直接渲染。如果哪天真要做"选项不定长、选项要支持富文本"的需求,再拆选项表也不迟,但就这个项目而言,JSON 字段的性价比最高。

2.2 试卷表与试卷题目关联表

试卷表存的是试卷的元信息,不直接存题目内容:

字段类型说明
idint自增主键
titlevarchar(100)试卷名称
durationint考试时长(分钟)
total_scoreint试卷总分
single_countint单选题数量
multi_countint多选题数量
judge_countint判断题数量
statustinyint1启用,0禁用

single_count、multi_count、judge_count这三个字段是随机组卷的核心配置,比如一套试卷配置了"单选 10 题、多选 5 题、判断 5 题,总分 100 分",那后端从题库里随机抽对应数量的题目就行,不用手动给每套卷子指定题目,这是和固定试卷模式最大的区别。

试卷题目关联表我一开始觉得可有可无,后面发现它必不可少。随机组卷时,学生开始考试就要固定住题目,不能每次刷新都抽新题,所以需要一张表把"这套试卷这次抽了哪些题"固化下来:

字段类型说明
idint自增主键
exam_idint试卷ID
question_idint题目ID
scoreint本题分值

这张表还有个好处:学生在考试过程中就算退出重进,重新查询这张表就能恢复到原来的题目,不会出现"断线重连题目全变了"这种情况。

2.3 答题记录表与答题明细表

答题记录表记录一次完整的考试过程:

字段类型说明
idint自增主键
user_idint用户ID
exam_idint试卷ID
statustinyint0考试中,1已交卷
scoreint最终得分
correct_countint正确题数
start_timedatetime开始考试时间
submit_timedatetime交卷时间

status字段是我做这个项目时特意加的。用户点"开始考试"时,我先插入一条记录,状态是 0;交卷时更新为 1。这样有两个好处:一是用户中途退出小程序再进来,可以查询到"有一场未完成的考试",提示继续作答;二是后台统计时能区分开"做了没交"和"交了的",数据更清晰。

答题明细表是逐题作答情况:

字段类型说明
idint自增主键
record_idint答题记录ID
question_idint题目ID
user_answervarchar(255)用户提交的答案
is_correcttinyint0错误,1正确

这张表的数据量会随考试次数线性增长,后期可以考虑按月归档,但在模拟考试场景下,数据量不大,不用过度设计。

2.4 判分规则设计

单选题和判断题很简单,答案比对一致就得分。多选题要提前定好规则:全对才得分,还是漏选得一半分?我这个项目用的是"多选全对才得分,少选、多选、错选都不得分",规则最简单,不容易扯皮。如果以后要支持"漏选得半分",只需要在判分逻辑里加一个数组交集判断,表结构不用改动。

另外所有判分计算都在后端做,前端提交过来的答案只是原始字符串数组,后端逐题比对完累加分数再写库。这一点无论如何不要妥协,分数可信度是考试系统的底线。

3. ThinkPHP 后端实现:登录鉴权、随机组卷与自动判分

3.1 项目初始化与路由规范

后端我用的是 ThinkPHP 6.0,创建项目:

composer create-project topthink/think tp_online_exam

目录结构上,我把控制器按模块分好,接口统一走Api命名空间:

app/ ├─ controller/ │ ├─ api/ │ │ ├─ Login.php // 登录相关 │ │ ├─ Exam.php // 试卷与考试 │ │ ├─ Question.php // 题库管理(后台用) │ │ └─ Record.php // 成绩相关

路由用 ThinkPHP 6 的路由定义方式,在route/app.php里统一配置:

use think\facade\Route; Route::post('api/login', 'api.Login/login'); Route::get('api/exam/list', 'api.Exam/examList'); Route::get('api/exam/detail', 'api.Exam/examDetail'); Route::post('api/exam/start', 'api.Exam/startExam'); Route::post('api/exam/submit', 'api.Exam/submitExam'); Route::get('api/record/detail', 'api.Record/detail');

统一前缀api/的好处是后面配 Nginx 反向代理或者加全局中间件都方便。每个接口进入控制器后第一步就是校验 token,我写了一个BaseController,所有需要登录的接口继承它。

3.2 微信登录与 token 机制

小程序端wx.login()拿到的 code 是一次性的,有效时间只有几分钟,后端用 code 向微信接口换 openid:

public function login() { $code = $this->request->param('code'); if (!$code) { return json(['code' => 1, 'msg' => '缺少code参数']); } $appid = config('wechat.appid'); $secret = config('wechat.secret'); $url = "https://api.weixin.qq.com/sns/jscode2session?appid={$appid}&secret={$secret}&js_code={$code}&grant_type=authorization_code"; $result = json_decode(file_get_contents($url), true); if (isset($result['openid'])) { $user = Db::name('user')->where('openid', $result['openid'])->find(); if (!$user) { $userId = Db::name('user')->insertGetId([ 'openid' => $result['openid'], 'create_time' => date('Y-m-d H:i:s') ]); } else { $userId = $user['id']; } // 生成新token $token = md5($result['openid'] . time() . rand(1000, 9999)); Db::name('user')->where('id', $userId)->update(['token' => $token]); return json(['code' => 0, 'data' => ['token' => $token, 'user_id' => $userId]]); } return json(['code' => 1, 'msg' => $result['errmsg'] ?? '登录失败']); }

这里有个容易踩的坑:file_get_contents在本地 PHP 环境如果没开allow_url_fopen会直接失效,报错让你排查半天。我建议直接用 cURL,兼容性更好。另外微信的jscode2session接口有频率限制,不建议每次请求都走这个接口换取 openid——token 机制就是为了避免这个问题。token 生成我用md5(openid + 时间戳 + 随机数),虽然强度不高,但配合 30 天有效期和每次登录刷新,对这个项目足够。别图省事直接把 openid 当 token 给前端,openid 是你系统里最重要的用户标识,一旦泄露,用户的账号体系就裸奔了。

3.3 试卷列表与随机组卷接口

试卷列表接口比较简单,从exam表查询状态为启用的试卷,同时统计一下当前用户是否已经考过这套卷子,前端就可以显示"已考过/未考"的状态。

重点看开始考试这个接口,它要做的事比较多:

public function startExam() { $userId = $this->userId; $examId = $this->request->param('exam_id'); // 1. 检查是否已有未完成的考试 $existing = Db::name('exam_record') ->where('user_id', $userId) ->where('exam_id', $examId) ->where('status', 0) ->find(); if ($existing) { // 返回已有的考试记录,前端可以直接续考 return json(['code' => 0, 'data' => [ 'record_id' => $existing['id'], 'is_new' => 0 ]]); } // 2. 根据试卷配置随机抽题 $exam = Db::name('exam')->where('id', $examId)->find(); $questions = []; $singleIds = Db::name('question') ->where('question_type', 1) ->where('status', 1) ->orderRaw('RAND()') ->limit($exam['single_count']) ->column('id'); // multi和judge同理... // 3. 写入试卷题目关联表 $allIds = array_merge($singleIds, $multiIds, $judgeIds); $recordId = Db::name('exam_record')->insertGetId([ 'user_id' => $userId, 'exam_id' => $examId, 'status' => 0, 'start_time' => date('Y-m-d H:i:s') ]); foreach ($allIds as $qid) { Db::name('exam_question')->insert([ 'exam_id' => $examId, 'question_id' => $qid, 'score' => 5 // 这里根据题型算分值 ]); } return json(['code' => 0, 'data' => ['record_id' => $recordId, 'is_new' => 1]]); }

随机抽题用的ORDER BY RAND(),很多人担心大数据量下性能问题,但对考试系统这种题库规模(几千到几万道题),完全没有压力。如果哪天真到了几十万道题,可以改成先SELECT id FROM question WHERE ...然后array_rand在应用层随机选,但那是后话了。

题目下发接口返回的数据里一定不要带answer字段。我是在查询后用hidden()方法隐藏字段,或者干脆手动拼装返回数组。有一次我图省事直接返回了整条记录,写完发现答案也在里面,连夜改接口,这个教训必须说给大家听。

3.4 交卷接口与自动判分

交卷接口是整个后端最核心的部分,前端把答题记录和用户的答案数组传过来,后端逐题判分,在事务里更新成绩:

public function submitExam() { $userId = $this->userId; $recordId = $this->request->param('record_id'); $answers = $this->request->param('answers/a'); // 格式:[{"question_id":1,"user_answer":"A"}, ...] // 事务处理 Db::startTrans(); try { $record = Db::name('exam_record') ->where('id', $recordId) ->where('user_id', $userId) ->where('status', 0) ->find(); if (!$record) { throw new \Exception('考试记录不存在或已交卷'); } // 检查是否超时(超过考试时长也算交卷) $startTime = strtotime($record['start_time']); $exam = Db::name('exam')->where('id', $record['exam_id'])->find(); if (time() - $startTime > $exam['duration'] * 60) { // 超时自动交卷,前端可能没提示,但后端要做兜底 } $score = 0; $correctCount = 0; foreach ($answers as $answer) { $question = Db::name('question')->find($answer['question_id']); // 判断题:单选用字符串比较,多选要排序后比较 $isCorrect = strtoupper(trim($answer['user_answer'])) == strtoupper(trim($question['answer'])); if ($isCorrect) { $score += 5; // 每道题5分 $correctCount++; } Db::name('answer_record')->insert([ 'record_id' => $recordId, 'question_id' => $answer['question_id'], 'user_answer' => $answer['user_answer'], 'is_correct' => $isCorrect ? 1 : 0 ]); } Db::name('exam_record')->where('id', $recordId)->update([ 'status' => 1, 'score' => $score, 'correct_count' => $correctCount, 'submit_time' => date('Y-m-d H:i:s') ]); Db::commit(); return json(['code' => 0, 'data' => ['score' => $score, 'correct_count' => $correctCount]]); } catch (\Exception $e) { Db::rollback(); return json(['code' => 1, 'msg' => $e->getMessage()]); } }

多选的答案比对要特别注意顺序问题。用户可能选了 AB,数据库答案是 BA,直接字符串相等地判错。我处理的办法是统一转成数组,排序以后再拼成字符串比对:

function checkMultiAnswer($userAnswer, $correctAnswer) { $userArr = explode(',', $userAnswer); $correctArr = explode(',', $correctAnswer); sort($userArr); sort($correctArr); return implode(',', $userArr) === implode(',', $correctArr); }

前端提交多选答案时,也要求把选项拼接成固定格式如 "A,B,C",这样前后端规则一致。

4. uni-app 小程序端实现:考试界面、倒计时与答题卡交互

4.1 项目创建、请求封装与登录流程

前端我用 HBuilderX 创建 uni-app 项目,模板选择"默认模板",不选 TypeScript 版本,因为 TypeScript 版本编译慢一点,而且团队成员对 TS 不熟,需求又简单,没必要上。

manifest.json 里配置微信小程序 AppID,如果还没有,用测试号也行。

请求封装是必要的,因为每个接口都要带 token。我在utils/request.js里把uni.request包了一层 Promise:

const BASE_URL = 'https://你的域名/api' export function request(url, method = 'GET', data = {}) { return new Promise((resolve, reject) => { uni.request({ url: BASE_URL + url, method, data, header: { 'Content-Type': 'application/json', 'Authorization': uni.getStorageSync('token') }, success: (res) => { if (res.statusCode === 200 && res.data.code === 0) { resolve(res.data.data) } else if (res.statusCode === 401) { // token过期,跳转登录 uni.navigateTo({ url: '/pages/login/login' }) } else { uni.showToast({ title: res.data.msg || '请求失败', icon: 'none' }) reject(res) } }, fail: (err) => { uni.showToast({ title: '网络异常', icon: 'none' }) reject(err) } }) }) }

登录逻辑在首页onLoad里执行:

onLoad() { // 先看本地有没有token const token = uni.getStorageSync('token') if (token) { this.getExamList() return } // 没有token就静默登录 uni.login({ provider: 'weixin', success: (loginRes) => { request('/login', 'POST', { code: loginRes.code }) .then((res) => { uni.setStorageSync('token', res.token) this.getExamList() }) } }) }

头像昵称这块要提醒大家:微信官方对wx.getUserProfile的管控一直在变,目前是弹窗授权制,但用户拒绝授权你不能拦着,所以我的策略是不强制授权头像昵称,默认给一个"考生"名称。等用户考完试想看排行榜或者想展示成绩时,再引导授权。这个顺序很重要,先让用户用起来,再谈信息丰富度。

4.2 考试页面的核心交互设计与答题状态管理

考试页是整套小程序最复杂的一个页面,核心功能包括:题目显示、选项选择、上一题/下一题切换、答题卡抽屉、倒计时、交卷。

我做的是一屏一题的布局:顶部固定显示剩余时间和进度条,中间是题干和选项,底部是"上一题/下一题/答题卡"三个按钮。这种布局在手机端体验最好,一屏信息量不超载。

答题状态我用一个对象存储:

data() { return { questions: [], // 题目列表(不含答案) currentIndex: 0, // 当前题目索引 userAnswers: {}, // 用户答案映射表 { questionId: "A" } 或 { questionId: "A,B" } questionStatus: {}, // 题目状态 0未答 1已答 -1当前题 remainTime: 0, // 剩余秒数 timer: null // 定时器 } }

单选题和判断题的交互是点击选项直接选中;多选题是点击选项切换选中状态,然后点"确定"进入下一题。为什么多选不学单选点击就跳下一题?因为多选题要考虑"还有没有要补选的",用户经常先选一个,犹豫一下再补选,点确定最稳妥。

答题卡我做成底部弹出的半屏模态层,题号用网格排列,绿色表示已答,灰色表示未答,红色高亮当前题。点击题号直接跳转到对应题目。

4.3 倒计时实现与切后台处理的完整方案

倒计时用setInterval每秒减 1,这个大家都会,但有几个细节处理不好会出大问题:

第一,页面隐藏时必须暂停计时器。手机来电话、用户切出去回微信,小程序会触发onHide生命周期,如果计时器还在跑,用户回来发现时间被扣了几分钟,体验极差。我的处理是:

onHide() { if (this.timer) { clearInterval(this.timer) this.timer = null } }, onShow() { if (this.recordId && !this.submitted) { // 从本地缓存读取剩余时间,恢复计时 this.remainTime = uni.getStorageSync('remain_' + this.recordId) this.startTimer() } }

第二,小程序的定时器在页面被回收时会失效。用户答题中途切后台太久,小程序进程被微信销毁,等用户再进来时onShow里要重新获取数据。我在onHide里不光暂停计时器,还把剩余时间和答题进度全部存到本地 Storage:

onHide() { uni.setStorageSync('exam_progress_' + this.recordId, { currentIndex: this.currentIndex, userAnswers: this.userAnswers, remainTime: this.remainTime, questions: this.questions }) }

这样用户再进来时,先检查本地有没有未交卷的缓存,有就直接恢复,不用从头开始。

第三,倒计时到 0 要自动交卷。用户可能正在看题忘了时间,倒计时结束就要自动提交。我在setInterval的每次回调里判断:

this.remainTime-- if (this.remainTime <= 0) { clearInterval(this.timer) this.autoSubmit() // 直接提交当前已答题目 }

后端也有超时兜底(交卷接口会检查开始时间是否超过考试时长),前端倒计时和后端超时保护双保险,哪怕前端被绕过,后端也不会让超时答卷生效。

4.4 交卷确认与异常恢复

交卷这个操作不能手滑点到就交了,我设计了两层确认:点"交卷"按钮弹出模态框问"确定交卷吗?还有 X 题未答",点"继续作答"可以返回,点"确定交卷"才真正提交。

提交答案时要注意,如果网络不好请求失败,不能直接把答题数据丢掉。我做了一个失败重试:请求失败后把答案缓存到本地,弹窗提示"交卷失败,点击重试",用户点重试就重新提交。如果用户直接退出小程序,本地缓存还在,下次进来可以继续交卷。

提交成功后,把本地缓存的考试进度清掉,然后跳转成绩页。成绩页从后端拿record_id查询答题明细,展示总分、正确题数、以及每道题的正确答案和解析。

5. 微信小程序打包、真机调试与上线避坑

5.1 包体积超限问题:2MB 限制与分包策略

微信小程序主包大小限制是 2MB,很多人第一次打包都会遇到类似"source size 2612kb exceed max limit 2mb"的报错,尤其是用了 uni-app 以后,uni_modules里组装了一堆组件,体积蹭蹭往上涨。

我的处理分三步:

第一步是分包。把考试页、成绩页、解析页都放到分包里,主包只保留登录页和首页(试卷列表)。pages.json 里这样配:

{ "pages": [ { "path": "pages/index/index", "style": { "navigationBarTitleText": "模拟考试" } }, { "path": "pages/login/login", "style": { "navigationBarTitleText": "登录" } } ], "subPackages": [ { "root": "pagesExam", "pages": [ { "path": "exam/exam", "style": { "navigationStyle": "custom" } }, { "path": "result/result", "style": { "navigationBarTitleText": "成绩单" } } ] } ] }

考试页我特意用了自定义导航栏,因为考试页面需要把倒计时固定在导航栏的位置,省掉系统导航栏能多出几十像素的答题空间。

第二步是清理uni_modules。HBuilderX 的插件导入功能很坑,很多组件传依赖会把整个组件库导进来。我查了一下uni_modules目录,发现里面躺着七八个没用到的插件包,全删掉以后小了三四百 KB。

第三步是静态资源压缩。图片尽量用小程序的外链地址,不要打进包里,除非图片必须本地展示。字体文件只用用到的字重,如果只是为了一个图标字体就引整个字库,纯属浪费。

5.2 自定义导航栏与安全区适配

考试页用了"navigationStyle": "custom"以后,顶部状态栏的高度就要自己处理了。不同手机的屏幕宽度、刘海屏高度、胶囊按钮位置都不同,不能写死。

获取状态栏高度和信息:

// 状态栏高度 const systemInfo = uni.getSystemInfoSync() this.statusBarHeight = systemInfo.statusBarHeight // 胶囊按钮位置(微信小程序专有API) const menuButton = uni.getMenuButtonBoundingClientRect() // 导航栏内容区域的高度 = 胶囊按钮的顶部 - 状态栏高度 + 胶囊按钮高度 + 胶囊按钮底部 - 状态栏高度 this.navBarHeight = (menuButton.top - this.statusBarHeight) * 2 + menuButton.height

这个公式理解起来很绕,我当初也是试了好几台设备才推出来的。简单说就是:状态栏下面到胶囊按钮之间留白,胶囊按钮下面再留白,两段留白几乎相等,所以navBarHeight = (胶囊.top - 状态栏高度) * 2 + 胶囊.height。把计算好的导航栏高度存到 data 里,模板里给顶部容器动态设置height,倒计时和标题就稳稳当当地显示在正确位置了。

真机调试时一定要多找几台不同系统的手机试。我遇到过一台 Android 定制系统,它的胶囊按钮位置和其他手机不一样,按公式算出来顶部多了一条边距,最后是通过uni.getMenuButtonBoundingClientRect()的动态值而不是写死值才解决的。

5.3 开发环境接口调试与抓包

开发阶段最头疼的是:ThinkPHP 跑在电脑上,小程序真机在手机上,怎么让手机访问到电脑上的接口?

本地开发时我用的是局域网 IP 直连。电脑和手机连同一个 Wi-Fi,把BASE_URL临时改成http://192.168.x.x:8000/api,注意 ThinkPHP 的入口在public目录下,PHP 内置服务器要这样起:

cd tp_online_exam/public php -S 0.0.0.0:8000

微信开发者工具里要勾选"不校验合法域名、web-view(业务域名)、TLS 版本以及 HTTPS 证书",不然工具会拦截 HTTP 请求。但真机预览时这个勾选无效,真机默认只允许 HTTPS 和已备案的合法域名。所以最省事的方式是:调试阶段用开发者工具(可以直接勾掉域名校验),要在真机上跑就给手机上手动装"开发版"小程序,并在details -> 本地设置里勾选"不校验合法域名"。正式环境必须配 HTTPS 域名,这是绕不过去的。

接口出错时需要抓包排查。开发者工具自带的 Network 面板能看大部分请求,但还是建议配合抓包工具看细节,比如 header 是否带了 token、返回 JSON 的字段类型对不对。用抓包工具时记得设置手机代理,让手机流量走电脑,这样小程序真机发的请求全都能看到。需要注意的一点是,微信小程序到后端接口在正式环境必须走 HTTPS,如果证书配置有问题,抓包看到的现象往往是"请求已发出但没响应"或者errno: 600001,这时候优先排查 TLS 版本和证书链是否完整。

5.4 审核与上架经验

小程序审核这里,考试类目是重点审核对象。我做模拟考试系统提审时,类目选的是"教育-在线教育",实际审核时平台要求提供相应的资质证明。个人主体小程序做这类内容有很大的资质风险,建议走企业主体。

另外一个是隐私保护指引。微信现在强制要求小程序在后台填写隐私保护指引,模板里必须明确说明收集了哪些用户信息(openid、设备信息、操作日志等),如果没填或者填得不全,用户授权弹窗都弹不出来,小程序审核也会被拒。提前把mp后台的"用户隐私保护指引"配置好,声明收集和使用规则,别等到提交审核才发现这层卡住了。

最后说几句实在话

整个项目做下来,我最深的感受是:在线考试模拟系统,核心功能写代码也就两三天的事,真正难缠的永远是边界情况——用户答到一半手机没电、小程序被系统回收、倒计时结束前端卡死、交卷时断网。所以我优先保证的是"答题进度不丢、分数可信、超时兜底",这三条做到了,项目就立住了。如果你也想做类似系统,我建议先跑通一个最小闭环:登录 + 一套试卷 + 交卷判分 + 成绩展示,然后再去加题库分类、随机组卷、错题本这些功能。再提醒一句,题库的正确答案字段务必只在后端使用,前端接口永远不要返回答案,这是考试系统的底线,也是你作为开发者的职业底线。

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

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

立即咨询