boxmoe_header_banner_img

Hello! 欢迎来到闪光阁!站长微信:Y16S888 备注来意

加载中

文章导读

从零到一:我开发了一款教资刷题微信小程序


avatar
顺哥哥 2026年6月13日 50
# 🎯 教资刷题小程序 — 从零到一的开发实践
> **一款覆盖小学、中学、幼儿园全学段的教师资格证考试备考助手,支持刷题练习、模拟考试、错题本、学习统计等完整功能链。**
## 📖 缘起:为什么做这个?
去年年底,身边几个朋友在准备教师资格证考试,微信群里天天吐槽:
– 「知识点太多,刷题没方向」
– 「找个好用的刷题工具真难」
– 「市面上的 App 广告满天飞,功能臃肿还收费」
我一看市面上的教资刷题产品——要么**广告轰炸**,要么**功能堆砌**,要么**上来就让付费**。恰好那段时间我在研究微信小程序开发,一拍脑袋:
> **为什么不自己做一个呢?**
于是就诞生了这个项目——一款**轻量、专注、清爽**的教资刷题小程序。
## 🛠️ 技术选型
选型原则很简单:**够用、轻量、好部署、一个人 hold 得住**。
| 层级 | 技术 | 选型理由 |
|:—|:—|:—|
| 🖥️ 前端 | **微信小程序原生**(WXML / WXSS / JS) | 原生开发,API 调用最直接,无需额外构建 |
| 🔧 后端 | **PHP**(零框架) | 简单路由,随写随用,部署成本极低 |
| 🗄️ 数据库 | **SQLite** | 零配置,单文件,无需运维数据库服务 |
| 🔐 认证 | **微信 OAuth + 自定义 JWT** | 天然微信生态集成,无感登录 |
| 📦 题库 | **JSON 文件** | 结构简单,扩展方便,改一个文件即可 |
| 🚀 部署 | **Nginx + 宝塔面板** | 运维友好,伪静态配置即可 |
没有用 Vue / React,没有用 Laravel / ThinkPHP,没有用 MySQL / Redis——**越简单的东西越容易维护,越不容易出问题**。
## 🏗️ 项目架构一览
“`
teaching-cert-quiz/
├── 📱 miniprogram/                    # 微信小程序前端
│   ├── pages/                        # 7 个功能页面
│   │   ├── index/      🏠 首页       # 学段/科目选择 + 今日统计
│   │   ├── quiz/       ✍️ 刷题       # 在线练习 + 每日限制 + 激励广告
│   │   ├── exam/       📝 模拟考试   # 设置→答题→结果 完整流程
│   │   ├── result/     📊 结果报告   # 分数 + 等级 + 错题回顾
│   │   ├── wrongbook/  📕 错题本     # 按学段/科目/章节筛选
│   │   ├── stats/      📈 统计       # 打卡日历 + 周趋势 + 章节分析
│   │   └── mine/       👤 我的       # 微信登录 + 个人数据
│   └── utils/
│       ├── api.js                    # API 请求封装(统一鉴权 / 错误处理)
│       └── util.js                   # 工具函数(日期格式化 / 洗牌 / 节流等)
└── ⚙️ php-server/                    # PHP 后端
    ├── index.php                     # 路由入口(20+ API 端点)
    ├── includes/
    │   ├── db.php                    # SQLite 封装(自动建表 / 种子数据)
    │   └── auth.php                  # JWT 生成 / 验证(HMAC-SHA256)
    ├── handlers/
    │   ├── user.php                  # 用户登录 / 打卡 / 个人统计
    │   ├── questions.php             # 题库查询 / 答题校验 / 章节统计
    │   ├── progress.php              # 学习进度 / 错题管理 / 日历数据
    │   └── config.php                # 广告配置 / 系统设置
    └── data/
        ├── subject_*.json            # 6 套科目题库文件
        └── app.db                    # SQLite 数据库(初次访问自动生成)
“`
整个项目结构一目了然——前端 7 个页面,后端 4 个处理器,**没有任何多余的抽象层**。
## 🎨 核心功能拆解
### 🏠 首页 — 学段与科目选择
首页顶部是品牌区,中间是三张学段卡片(小学 / 中学 / 幼儿园),点击展开对应的科目入口。
设计上的巧思:
– **科目一共通**:「综合素质」是所有学段的必考科目
– **科目二区分**:根据学段动态展示——`小学→教育教学知识与能力`、`中学→教育知识与能力`、`幼儿园→保教知识与能力`
– **今日统计卡片**:展示当日做题数、正确率、连续打卡天数,形成**每日正反馈**
首页的交互代码很简洁,核心就是 `selectedGrade` 状态切换:
“`javascript
// 选中/取消学段
onSelectGrade(e) {
    const { grade } = e.currentTarget.dataset;
    if (this.data.selectedGrade === grade) {
        // 点击已选中的学段 → 收起
        this.setData({ selectedGrade: ”, subjects: [] });
        return;
    }
    // 展开学段,显示科目
    this.setData({ selectedGrade: grade, subjects: [‘subject1’, ‘subject2’] });
}
“`
交互逻辑:点一下展开,再点一下收起,**符合直觉**。
### ✍️ 刷题练习 — 核心体验
这是整个小程序**使用频率最高**的页面。刷题交互流程:
“`
看题 → 选选项 → 立即反馈(对/错) → 看解析 → 下一题 → 全部完成 → 结果页
“`
**值得一提的设计细节:**
#### 🔹 乐观更新
用户点击选项后**立即高亮**,不等待 API 返回。200ms 的等待时间虽然不长,但对用户体验的伤害是巨大的。
“`javascript
// 立即高亮选中的选项
this.setData({ selectedAnswer: key });
// 然后才去调 API 校验…
const res = await api.post(‘/questions/check’, { questionId, answer: key });
“`
#### 🔹 状态持久化
前后切换题目时,已作答状态完全恢复——选了哪项、对了还是错了、解析展示与否,全部记住。这依赖于 `userAnswers` 和 `answerResults` 两个 Map 结构:
“`javascript
const userAnswers = Object.assign({}, this.data.userAnswers, {
    [questionId]: key
});
const answerResults = Object.assign({}, this.data.answerResults, {
    [questionId]: { correct: correctAnswer === key, correctAnswer }
});
“`
#### 🔹 每日限制 + 广告解锁
默认每天 10 题免费练习。用完弹出弹窗,观看激励视频即可解锁**全天无限刷题**。广告逻辑做了**乐观解锁**——即使用户看广告后的服务端通知失败,前端也会直接解锁:
“`javascript
api.adUnlock().then(() => {
    // 服务端解锁成功
    this.setData({ remaining: 999, adUnlocked: true });
}).catch(() => {
    // 即使接口失败也给用户解锁 —— 用户体验优先
    this.setData({ remaining: 999, adUnlocked: true });
});
“`
> 宁可少一次广告收入,也不要让用户觉得「看了广告还没解锁」。
#### 🔹 容错设计
即使答题校验接口挂了,前端也能让用户继续做题:
“`javascript
catch (err) {
    // 即使接口失败也让用户可以继续下一题
    this.setData({
        answered: true,
        answerResults: { [questionId]: { correct: false, localError: true } }
    });
}
“`
### 📝 模拟考试 — 完整的限时挑战
模拟考试分为三个阶段,三个模式:
“`
🟢 SETUP(设置) → 🟡 TAKING(答题) → 🔴 RESULT(结果)
“`
**设置阶段**:选择学段 → 科目 → 题量(10/20/50 题)→ 计时开关
**答题阶段**:
– 顶部有进度计时器,倒计时最后 60 秒变红警告
– 题号导航网格,已答/未答/标记三种状态一目了然
– 支持标记题目,回头检查
– 答题完成确认交卷
**结果阶段**:
– 圆形进度分数展示(`conic-gradient` 实现)
– 正确 / 错误 / 未答 / 用时四维统计
– 逐题回顾列表,点击可看详情
– 再来一次 / 查看解析
计时逻辑很合理——每道题 30 秒,最少 5 分钟,最长 60 分钟:
“`javascript
const timeLimit = Math.max(300, Math.min(3600, count * 30));
“`
### 📕 错题本 — 针对性复习
错题本支持**三级筛选**:学段 → 科目 → 章节。
每条错题展示:题目内容、正确答案、**错误次数**、**上次答错时间**。错误次数高的题目会格外醒目——这是你需要重点复习的地方。
**数据加载的优化**:错题列表只返回 ID 和统计信息,详情通过 `Promise.allSettled` 批量并发获取:
“`javascript
const detailPromises = wrongItems.map(item => {
    return fetchQuestionDetail(item.id, selectedGrade, selectedSubject, token);
});
Promise.allSettled(detailPromises).then(results => {
    const detailList = results.map(r => r.status === ‘fulfilled’ ? r.value : null).filter(Boolean);
    // 合并到列表…
});
“`
这样首屏加载飞快,详情并发拉取,体验流畅。
### 📊 学习统计 — 数据驱动备考
统计页面集成了**五个维度的数据展示**:
| 模块 | 展示内容 | 价值 |
|:—|:—|:—|
| 📋 用户总览 | 总答题数 / 正确率 / 连续打卡 | 宏观了解学习进度 |
| 📅 打卡日历 | 月度热力图 | 直观看到哪天学了、哪天偷懒了 |
| 📈 周趋势 | 近 7 天每日做题量柱状图 | 短期学习节奏把控 |
| 📚 章节统计 | 各章节正确率 | **精准定位薄弱环节** |
| 🏆 考试记录 | 历次模拟考试成绩 | 进步轨迹一目了然 |
其中章节统计是最有价值的功能——它能回答「我到底哪里不行」这个灵魂拷问:
“`javascript
// 逐章节计算正确率
foreach ($chapters as $ch) {
    $chQuestions = array_filter($data[‘questions’], function($q) use ($ch) {
        return ($q[‘chapter’] ?? ”) === $ch[‘id’];
    });
    // 查询该章节所有题目的答题记录…
    $result[] = [
        ‘name’ => $ch[‘name’],
        ‘totalCount’ => $totalCount,
        ‘accuracy’ => $answeredCount > 0 ? round($correctCount / $answeredCount * 100) : 0
    ];
}
“`
## 🔧 后端设计要点
### 无状态 JWT 认证
用 HMAC-SHA256 签名 JWT,30 天过期。PHP 实现不到 50 行,两个核心函数搞定:
“`php
function generateToken($openid) {
    $header = base64url_encode(json_encode([‘typ’ => ‘JWT’, ‘alg’ => ‘HS256’]));
    $payload = base64url_encode(json_encode([
        ‘openid’ => $openid,
        ‘exp’ => time() + 30 * 24 * 3600
    ]));
    $signature = base64url_encode(
        hash_hmac(‘sha256’, “$header.$payload”, JWT_SECRET, true)
    );
    return “$header.$payload.$signature”;
}
“`
> 相比 session 方案,JWT 无需服务端存储,水平扩展零成本。
### 轻量级路由
没有用框架,一个 `index.php` + 关联数组搞定 20 多个 API 端点:
“`php
$routes = [
    ‘/api/questions/list’   => [‘handler’ => ‘questions’, ‘action’ => ‘listQuestions’],
    ‘/api/user/login’       => [‘handler’ => ‘user’,     ‘action’ => ‘login’],
    ‘/api/progress/exam’    => [‘handler’ => ‘progress’, ‘action’ => ‘examSubmit’],
    // …
];
“`
性能和可读性都非常好,而且**零学习成本**。
### SQLite + WAL 模式
SQLite 的 WAL(Write-Ahead Logging)模式允许多个并发读取和一个写入同时进行,对刷题场景绰绰有余:
“`php
$db->exec(‘PRAGMA journal_mode=WAL’);
$db->exec(‘PRAGMA foreign_keys=ON’);
“`
而且整个数据库就是一个文件,备份迁移:**拷走就行**。
### 题库格式设计
每道题的 JSON 结构:
“`json
{
    “id”: “s1_p_101”,
    “type”: “single”,
    “chapter”: “laws”,
    “difficulty”: “medium”,
    “content”: “题目内容”,
    “options”: [“A. 选项一”, “B. 选项二”, “C. 选项三”, “D. 选项四”],
    “answer”: “A”,
    “explanation”: “解析内容”
}
“`
扩展题库只需要在 JSON 文件里加题 → 重启服务器 → 生效。如果以后要做管理后台,直接改写这个 JSON 即可,**数据与逻辑完全解耦**。
## 💰 广告变现设计
小程序接入了微信流量主,广告场景布局:
| 场景 | 广告类型 | 触发时机 |
|:—|:—|:—|
| 每日限制解锁 | **激励视频** | 免费题数用完后,观看视频解锁全天无限刷 |
| 答题结果页 | **插屏广告** | 刷题完成/模拟考试交卷后展示 |
广告配置通过**服务端 API 动态获取**,不硬编码在代码里:
“`javascript
// 从后台获取广告位 ID(可随时更换,无需发版)
api.getAdUnit().then(res => {
    const adUnitId = res.data.unlock_quiz;
    that.playAd(adUnitId);
});
“`
这意味着**广告位 ID 可以在后台修改,无需提交小程序审核**,非常灵活。
### 广告实例管理
激励视频广告有个容易踩的坑:**重复创建实例会导致内存泄漏和广告不触发**。我的处理方式:
“`javascript
// 复用实例,避免重复创建
if (!that._videoAd) {
    that._videoAd = wx.createRewardedVideoAd({ adUnitId: adUnitId });
}
// 页面卸载时清理
onUnload() {
    if (this._videoAd) {
        this._videoAd.offLoad();
        this._videoAd.offError();
        this._videoAd.offClose();
        this._videoAd = null;
    }
}
“`
## 🧠 写代码过程中的一些思考
### 1️⃣ 容错意识:后端挂了 ≠ 前端崩了
整个小程序贯彻一个原则:**尽力而为,优雅降级**。
– ✅ 题库加载失败 → 使用本地模拟数据
– ✅ 答题校验接口失败 → 乐观更新,本地计算
– ✅ 用户信息获取失败 → 读取本地缓存
– ✅ Token 过期 → 自动跳转登录页
– ✅ 广告加载失败 → 静默关闭弹窗,不影响做题
### 2️⃣ 统一接口规范
所有 API 返回 `{code, msg, data}` 的三段式结构。错误码同步到 HTTP 状态码:
“`php
if ($code === 401) http_response_code(401);
if ($code === 404) http_response_code(404);
if ($code === 500) http_response_code(500);
“`
这样微信的 `wx.request` 回调能通过 `statusCode` 准确识别错误类型,前端代码更干净。
### 3️⃣ 细节决定体验
一些看似不起眼的细节,加起来就是好的产品体验:
– 📳 **触觉反馈**:选答案时 `wx.vibrateShort`——从物理上确认操作
– ⏳ **延迟展示**:插屏广告延迟 1.5 秒——等结果页渲染完再弹
– 📦 **数据传递**:错题用 `globalData` 而非 URL 参数——防止 URL 超长截断
– 🔄 **状态保持**:离开页面再回来,做题进度、滚动位置全部恢复
– 🎯 **错误引导**:401 时弹 Modal 说明「登录已过期」,而不是直接白屏
### 4️⃣ 兼容性处理
微信 API 在不同版本、不同设备上差异很大。举个典型的例子——广告 API:
“`javascript
// wx.createInterstitialAd 在低版本微信上不存在
if (wx.createInterstitialAd) {
    this._interstitialAd = wx.createInterstitialAd({ adUnitId: ‘…’ });
} else {
    // 降级:不展示插屏广告
    console.log(‘当前微信版本不支持插屏广告’);
}
“`
还有 iPhone 刘海屏适配:
“`javascript
const model = sysInfo.model;
if (model.indexOf(‘iPhone X’) >= 0 || model.indexOf(‘iPhone 11’) >= 0 || …) {
    that.globalData.isIPhoneX = true;
}
“`
## 📊 项目数据
| 指标 | 数据 |
|:—|:—|
| 📦 题库数量 | **6 套**(小学/中学/幼儿园 × 2 科目) |
| 🖥️ 功能页面 | **7 个** |
| 🔌 API 端点 | **20 个** |
| 🗄️ 数据库表 | **6 张** |
| 📜 前端代码 | **约 3000 行** |
| ⚙️ 后端代码 | **约 1500 行** |
| ⏱️ 开发周期 | **约 2 周**(断断续续) |
## 🔮 复盘与展望
### ✅ 做对了的
– **技术选型务实**:微信小程序 + PHP + SQLite 的轻量组合非常适合个人开发者
– **刷题场景契合**:小程序「即用即走」的特性与刷题场景天然匹配
– **广告变现平衡**:每日限制 + 激励视频的设计既保证了用户体验,也带来了收入
– **架构简单**:没有过度设计,一个人完全 hold 得住
### ❌ 可以改进的
– **题库管理**:目前是静态 JSON,后续可以改造成后台可维护的动态题库
– **筛选维度**:缺少随机抽题、按难度筛选、按知识点筛选等高级功能
– **社交属性**:没有做打卡排行榜、好友 PK,学习动力少了一个来源
– **题型单一**:目前只支持单选题,可以扩展多选题、判断题、材料分析题
– **后端语言**:PHP 毕竟不是强类型,代码量大了以后维护成本会增加
### 🗓️ 接下来的计划
– [ ] 📚 增加更多科目:学科知识与教学能力(科目三)
– [ ] 🔀 扩展题型:多选题、判断题、材料分析题
– [ ] 🖥️ 搭建管理后台:在线题库编辑、数据可视化
– [ ] 🤖 AI 智能推荐:根据错题分布和薄弱环节,智能推荐练习题目
– [ ] 👥 学习社区:打卡排行榜、学习小组、题目讨论
## 💬 写在最后
这个项目前后断断续续写了大概两周。从需求分析、技术选型、编码实现到部署上线,整个过程最大的感受是:
> **小程序的生态确实成熟了。**
微信提供的登录、支付、广告、云开发等能力,让一个人也能完成以前需要一个团队才能做的事。而 PHP + SQLite 这个「老古董」组合,在轻量级场景下依然能打,**简单、可靠、够用**。
如果你也在准备教资考试,或者想做一个自己的小程序项目,欢迎交流!✉️
> **项目地址**:暂时不公开
>
> **小程序名称**:教资刷题(微信搜索即可体验)
>
> **技术栈**:微信小程序原生 + PHP + SQLite + JWT

*如果你觉得这篇文章有帮助,欢迎分享给正在准备教资的朋友 🙌*


评论(0)

查看评论列表

暂无评论


发表评论