☰
微信小程序+双框架PHP:公考助学系统设计与实现全解析
2026/9/26 6:16:18 网站建设 项目流程

开篇:为什么我用“双框架”做了一套公考助学小程序

去年帮一位准备考公的朋友做了一个刷题小程序,需求其实很朴素:把行测和申论的视频课、题库、错题本、学习打卡整合到一个微信小程序里,让他在地铁上、午休时也能随时刷两道题、看一小节视频。项目做完后整理源码时发现,很多学弟学妹都在做类似的毕业设计或课程项目——“公务员考试课程复习助学系统”,但大多数都卡在前后端数据交互、框架选择、小程序授权登录这几个环节上。

这个项目的完整名字是“公务员考试课程复习助学系统的微信小程序的设计与实现”,后端采用了ThinkPHP和Laravel两套框架配合使用:Laravel负责小程序API主服务,ThinkPHP负责后台管理端的数据接口。你可能会问,为什么一个项目要搞两个框架?说白了,Laravel在API开发、队列、缓存、模型关系上体验更顺滑,适合作为C端服务的核心;而ThinkPHP部署简单、上手门槛低,后台管理界面用TP来做能省不少折腾时间。两个框架通过同一个MySQL数据库协作,各自负责不同域名的入口。

这套系统核心解决的痛点有三个:一是公考内容分散,视频课在A平台、题库在B平台、笔记在C平台,切换成本高;二是缺少针对考公场景的“学练测评”闭环,光看不练记不住;三是很多在校生或在职备考者没有大块时间,移动端的轻量化学习体验几乎是刚需。

适合谁来参考?如果你正准备做毕业设计选题、想接私活做类似的教育类小程序、或者单纯想练习“微信小程序 + PHP框架”的全栈实战,这篇文章都能给你一条完整可落地的主线。我会把从需求拆分、数据库设计、接口实现到小程序端联调的全过程都写出来,包括踩过的坑和可以“抄作业”的代码片段。

1. 项目需求拆解与整体技术选型

1.1 从用户角色出发拆功能模块

任何教育类系统,第一步不是写代码,而是把“谁在用、用来干什么”想清楚。这套公考助学系统我按角色拆成了三类:普通学员、课程管理员、系统管理员。

学员端的核心功能包括:微信一键登录、浏览课程列表与详情、在线播放视频课程、章节练习与模拟考试、错题自动归集、学习进度记录、每日打卡、个人中心。管理端的功能则更重:课程分类管理、视频资源上传与转码、题库批量导入、试卷组卷、学员数据统计、内容发布审核。

一开始我也想做大而全的“在线直播 + 社区论坛 + 智能推荐”,但后来砍掉了。原因很简单:直播对服务器带宽和实时通信要求高,毕设或课程项目很难撑住;社区论坛会大幅增加内容审核成本;而智能推荐在冷启动阶段基本没数据可用。与其做一堆用不上的功能,不如把“课程 + 题库 + 错题 + 进度”这条主链路做扎实。

1.2 为什么后端选Laravel + ThinkPHP 双框架

说实话,“一个系统一个框架”才是最常规的选项,但实际项目里我碰到了具体问题:小程序端需要的接口以JSON返回为主,Laravel的Eloquent ORM写联表查询、资源控制器非常顺手,加上中间件做登录鉴权很优雅;而管理后台需要渲染大量服务端页面,ThinkPHP的模板引擎和快速CRUD开发风格更贴近传统PHP习惯。

你完全可以用单一框架完成所有功能,但双框架的落地思路是这样的:小程序端所有请求走api.example.com(Laravel),后台管理端走admin.example.com(ThinkPHP),两个站点共用同一个MySQL库。这样做的额外收益是,如果以后想把管理端拆分给另一个团队维护,前后端边界天然是清晰的。

1.3 微信小程序端技术准备清单

小程序端我用的原生微信小程序框架,没有引入uni-app或Taro。原因很实际:这类项目要求的是把微信生态的能力吃透,原生框架在调试、真机预览、微信API调用上最直接,也不存在跨端编译的中间层损耗。

技术清单大致如下:

  • 开发工具:微信开发者工具稳定版
  • 前端语言:WXML + WXSS + JavaScript(ES6语法)
  • 状态管理:小程序原生globalData + 简单的eventBus
  • 网络请求:wx.request 封装 Promise + 拦截器
  • 视频播放:微信小程序 video 组件 + 服务端MP4直链
  • 本地存储:wx.setStorageSync 做答题记录与学习进度缓存

2. 数据库设计与核心表结构

2.1 表和字段设计的原则

教育类系统的核心数据无外乎:用户、课程、章节、视频、题目、试卷、做题记录、错题。表设计过程中我坚持了三件事:一是不做过度冗余,凡是能通过join查出来的数据不另存字段;二是时间字段统一用int时间戳,方便排序和格式化;三是逻辑删除统一用deleted_at字段,避免误删数据后无法恢复。

数据库字符集选utf8mb4,排序规则utf8mb4_unicode_ci。原因很简单:如果课程标题里出现生僻字或者学员昵称里有emoji,utf8mb3就会直接报错,utf8mb4才能完整支持。

2.2 核心表结构解读

学员表student

字段类型说明
idint unsigned主键
openidvarchar(64)微信openid,唯一索引
nicknamevarchar(50)昵称
avatar_urlvarchar(255)头像地址
study_daysint累计学习天数
sign_streakint连续签到天数
created_atint注册时间

课程表course

字段类型说明
idint unsigned主键
category_idint分类ID
titlevarchar(100)课程标题
cover_urlvarchar(255)封面图
introtext课程简介
total_chaptersint总章节数
is_freetinyint是否免费课程
pricedecimal(10,2)价格
sort_orderint排序权重

做题记录表answer_record是整个系统里数据量增长最快的表,设计时重点考虑索引。

CREATE TABLE `answer_record` ( `id` int unsigned NOT NULL AUTO_INCREMENT, `student_id` int NOT NULL, `question_id` int NOT NULL, `paper_id` int DEFAULT 0, `student_answer` varchar(10) NOT NULL, `is_correct` tinyint NOT NULL DEFAULT 0, `created_at` int NOT NULL, PRIMARY KEY (`id`), KEY `idx_student_question` (`student_id`, `question_id`), KEY `idx_created` (`created_at`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

为什么student_answer用varchar而不是更小的char?因为题型可能包含多项选择题,答案需要存类似“ABC”这样的组合,用定长char万一以后出现判断题答案“对/错”这种中文字符就会出问题,varchar的适配性更好。

2.3 错题本如何设计与实现

错题本不是单独一张“错题表”存储所有错题,而是在每次做题时实时判断结果,如果错误则往wrong_question表写一条记录。表结构如下:

  • id
  • student_id
  • question_id
  • wrong_count(累计错误次数)
  • last_wrong_time(最后一次错误时间)
  • created_at

这样做的好处是:学员可以在错题本里按“错误次数”排序,优先巩固那些反复错的题目;同时当学员重新答对某道题达到一定次数后,可以手动移除或由系统自动弱化。实际项目里我没有做自动移除,因为考公场景下错题重做对了不代表真正掌握了,保持记录让学员自己决定是否移除更合理。

3. Laravel后端API接口设计与实现

3.1 目录结构与接口规划

Laravel端我按模块划分了控制器目录:AuthController、CourseController、QuestionController、ExamController、ProgressController、SignController。路由统一走api.php文件,所有接口遵循接口域名/api/v1/资源/动作的规则。

接口返回格式统一封装成下面的JSON结构:

{ "code": 0, "message": "success", "data": {} }

前端小程序在每个请求的拦截器里先判断code,不为0时弹出message,这样后端只要按约定返回,前端所有的错误处理逻辑就是一套。这套封装是实际开发里最值得先写好的代码,没有之一。

接口清单大致如下:

  • POST /api/v1/auth/login —— 微信登录换取token
  • GET /api/v1/course/list —— 课程列表(分页)
  • GET /api/v1/course/detail —— 课程详情
  • GET /api/v1/chapter/video —— 获取视频播放地址
  • GET /api/v1/question/list —— 题目列表
  • POST /api/v1/answer/submit —— 提交答题结果
  • GET /api/v1/wrong/list —— 错题本列表
  • POST /api/v1/sign/do —— 每日签到

3.2 微信登录与会话管理

微信小程序登录的核心流程是先调用wx.login()拿到临时code,然后传给后端,后端拿着code去微信接口服务换openid和session_key。这个流程很多新手会犯错:直接把code当作登录凭证传给后端,后端就信任了,不校验、不换取,这是非常大的安全隐患。

Laravel端的实现核心代码:

public function login(Request $request) { $code = $request->input('code'); $appid = env('WECHAT_APPID'); $secret = env('WECHAT_SECRET'); $url = "https://api.weixin.qq.com/sns/jscode2session?appid={$appid}&secret={$secret}&js_code={$code}&grant_type=authorization_code"; $response = Http::get($url); $result = $response->json(); if (!isset($result['openid'])) { return $this->error('登录失败,请重试'); } // 查找或创建用户 $student = Student::firstOrCreate( ['openid' => $result['openid']], ['nickname' => '公考学员', 'avatar_url' => ''] ); // 生成自定义token $token = md5($result['openid'] . time() . uniqid()); cache(['token:' . $token => $student->id], 86400 * 7); return $this->success([ 'token' => $token, 'student' => $student ]); }

这里我用了Laravel自带的Cache存储token映射关系,没有引入JWT。原因很直接:小程序端的token只服务一个系统,不需要JWT跨域分发那么多特性,cache天然支持过期时间,七天登录态过期后用起来也方便。

3.3 视频播放地址的权限处理

公考课程的视频资源是核心资产,不能简单把MP4直链暴露给所有前端。我的处理思路是:播放地址不直接返回给小程序,而是一次性带签名的临时URL。签名过期时间设为30分钟,学员打开课程详情再点播放,签名足够支撑一节课的加载缓冲;就算地址被人拿着分享出去,30分钟后也自动失效。

public function videoUrl(Request $request) { $chapterId = $request->input('chapter_id'); $chapter = Chapter::find($chapterId); if (!$chapter || $chapter->is_free == 0) { // 非免费章节需校验是否购买 $hasPurchased = Order::where('student_id', $request->user_id) ->where('course_id', $chapter->course_id) ->where('status', 1) ->exists(); if (!$hasPurchased) { return $this->error('请先购买课程'); } } // 生成带签名的临时视频地址 $expire = time() + 1800; $sign = md5($chapter->video_path . $expire . env('VIDEO_SECRET')); $url = 'https://cdn.example.com/' . $chapter->video_path . '?sign=' . $sign . '&expire=' . $expire; return $this->success(['video_url' => $url]); }

3.4 ThinkPHP管理后台的关键实现

管理后台我用ThinkPHP 6.0实现,专职处理三个核心场景:题库批量导入、课程资源上传、每日学习数据报表。

题库批量导入是使用频率最高的功能。管理后台提供Excel模板下载,管理员按模板填写题目、选项、正确答案、解析,然后上传到服务器,ThinkPHP端用phpoffice/phpspreadsheet库读取并批量写入数据库。批量导入的PHP代码核心逻辑:

public function importQuestions() { $file = $this->request->file('file'); $path = $file->getRealPath(); $spreadsheet = IOFactory::load($path); $sheet = $spreadsheet->getActiveSheet(); $rows = $sheet->toArray(); $insertData = []; foreach ($rows as $index => $row) { if ($index < 2) continue; // 跳过表头 if (!$row[1]) continue; // 题目为空则跳过 $insertData[] = [ 'category_id' => $this->request->post('category_id'), 'type' => $row[0], // 题型:单选题/多选题/判断题 'question' => $row[1], 'option_a' => $row[2], 'option_b' => $row[3], 'option_c' => $row[4], 'option_d' => $row[5], 'answer' => strtoupper($row[6]), 'analysis' => $row[7], 'created_at' => time(), ]; // 每500条批量插入一次,避免数据量过大内存溢出 if (count($insertData) >= 500) { Db::name('question')->insertAll($insertData); $insertData = []; } } if (!empty($insertData)) { Db::name('question')->insertAll($insertData); } return json(['code' => 0, 'msg' => '导入成功,共导入' . ($index - 1) . '条题目']); }

这个导入逻辑我用了几种题型混合的Excel文件做测试,五千条题目大约三秒内导入完成,比在后台一个个手输效率提升量级。

4. 微信小程序端核心功能开发

4.1 request请求封装与拦截器设计

小程序端所有网络请求必须统一封装。不然每个页面各写各的wx.request,不出二十个页面代码就全面失控。我的封装思路是:

const request = (url, method = 'GET', data = {}) => { return new Promise((resolve, reject) => { wx.request({ url: BASE_URL + url, method: method, data: data, header: { 'Content-Type': 'application/json', 'Authorization': wx.getStorageSync('token') || '' }, success: (res) => { if (res.data.code === 0) { resolve(res.data.data); } else if (res.data.code === 401) { // token过期,重新登录 wx.removeStorageSync('token'); wx.navigateTo({ url: '/pages/login/login' }); reject(res.data); } else { wx.showToast({ title: res.data.message, icon: 'none' }); reject(res.data); } }, fail: (err) => { wx.showToast({ title: '网络异常,请检查网络', icon: 'none' }); reject(err); } }); }); }; module.exports = { request };

一个值得注意的细节:当code === 401时不能只弹错误提示,必须清掉本地token并跳转登录页。这是很常见的坑——token过期后不清缓存,用户会一直卡在“请求失败”的状态里。

4.2 课程列表与详情页面

课程列表页面用scroll-view+ 触底加载实现分页。每次请求传page和page_size,后端返回total后判断是否还有下一页。触底加载最怕的场景是重复触发——用户快速滑动到底部,请求还没返回又触发了下一次,这里加一个isLoading锁变量即可。

课程详情页设计了三段式结构:顶部封面轮播与基本信息、中部课程目录树、底部购买栏。课程目录树用wx:for嵌套两层渲染,外层是章节,内层是小节(视频)。嵌套列表在微信小程序里有一个需要注意的问题:内层wx:for的wx:key不能用默认的index,要绑定小节ID,否则某个小节状态变化时整个列表都会重新渲染,体验会非常卡。

4.3 题库练习模式与计时逻辑

刷题模块我做了两种模式:练习模式和考试模式。练习模式每做一题实时判断对错并显示解析;考试模式则一次性加载所有题目,全部作答完统一提交。

做题的计时逻辑用setInterval每秒更新剩余时间,但这里有个必须处理的隐患:小程序页面切到后台时setInterval会自动暂停,等用户切回来时间可能已经“冻结”了。稳妥做法是在onHide时记录当前时间戳,在onShow时用Date.now()计算差值来校准计时器。

onHide() { this.timer && clearInterval(this.timer); this.hideTimestamp = Date.now(); }, onShow() { if (this.hideTimestamp) { const gap = Math.floor((Date.now() - this.hideTimestamp) / 1000); this.setData({ remainTime: this.data.remainTime - gap }); } this.startTimer(); }

4.4 答题卡与本地缓存机制

考试模式下学员需要有一个可视化答题卡,点击题号可以跳转到对应题目。答题卡数据直接存在小程序本地缓存里,key按试卷ID区分。每次切换题目或点击选项后,立即把当前的答题状态写入缓存。这样做的目的很直接:真机环境中学员来一个电话、或者不小心锁屏,回来之后所有作答状态不会丢失。

缓存的粒度需要注意——不要整张试卷一次性存,而是只存增量。例如选择了一道题,就把这道题的题号和选项号存进去,下次读取时合并更新。wx.setStorageSync的同步API在小数据量场景下性能毫无压力,完全没必要用异步版增加代码复杂度。

5. 视频播放与下载限制的实现细节

5.1 小程序video组件与视频源选型

视频播放是公考课程系统的核心体验。微信小程序官方video组件支持的网络源格式主要为MP4/HLS,实测下来MP4的兼容性和清晰度稳定性比HLS好,尤其是在弱网环境下拖动进度条,MP4起播速度明显更快。

视频存储在云存储或CDN上,我最终选择了阿里云OSS + CDN加速。OSS权限设成私有读,所有视频地址必须通过后端签名URL访问,CDN上做Referer防盗链。双保险之下,即使签名URL泄漏,外站也无法直接盗播。

5.2 视频下载的防与放

行业里对视频下载的态度分两派:一派是把下载功能彻底关闭,只允许在线播放;另一派是允许下载到小程序本地缓存。针对公考课程这个品类,我采用的策略是“允许缓存、禁止导出”:

  • 小程序端video组件开启enable-cache属性,播放过的视频会存在微信本地缓存中,下次打开秒开
  • 前端不提供“保存到相册/下载到本地”按钮,下载中途也无法获取真实MP4链接
  • 浏览器端或非微信环境无法播放签名URL,因为签名URL带了微信小程序的referer校验参数

这个方案在用户便利性和版权保护之间取了平衡,实测学员在地铁等弱网场景下依旧能流畅复习已学过的章节。

5.3 播放入口限免逻辑

免费试看策略也很关键。系统允许将每个课程的前两节设为免费试看,小程序端未购买用户点击第三节课时弹出购买引导。后端的判定逻辑是在videoUrl方法前判断order表是否存在已支付记录,若课程为收费课程且无购买记录则直接拦截并返回错误码,小程序端捕获后引导用户到购买页。

6. 核心业务场景的完整实现链路

6.1 从微信授权到学习数据同步

完整链路如下:用户打开小程序 -> 首次访问显示登录页 -> 点击登录触发wx.login-> 拿到code传给Laravel后端 -> 后端换取openid -> 查询或创建学员记录返回token -> 小程序存储token并跳转首页。首页展示学习天数、打卡状态、进度概览。

初始化登录时有个体验细节:不要在用户刚打开小程序时就强制弹窗授权手机号,只需要静默登录拿到openid,等用户真正使用到需要手机号的场景(比如购买课程)再弹窗。这样可以大幅提升小程序的用户留存率,因为少一次授权弹窗就少一批流失。

6.2 模拟考试自动判分的实现

行测模拟考试的判分逻辑需要区分题型。单选题直接比对answer字段;多选题需要把学员答案字符串拆解成字符数组后与标准答案数组比对,顺序不同也算正确;判断题则根据选项文本内容判断。

public function judgeAnswer($question, $studentAnswer) { $correct = $question['answer']; if ($question['type'] == 'multi') { $studentArr = str_split($studentAnswer); $correctArr = str_split($correct); sort($studentArr); sort($correctArr); return $studentArr === $correctArr; } return strtoupper($studentAnswer) === strtoupper($correct); }

多选排序后比对是必需步骤。行测里的多选通常要求全对才得分,少选多选都算错,这套逻辑直接卡死就行。

6.3 学习进度报表的实现方法

学员首页的“学习进度”展示本周每天的学习时长柱状图,数据源来自study_log表。学员在小程序端每次播放视频或答题时,后端都会通过中间件记录一条学习日志,包含日期、学习时长、学习内容ID。前端按周聚合查询:

public function weeklyReport(Request $request) { $start = strtotime(date('Y-m-d', strtotime('-6 days'))); $end = time(); $logs = StudyLog::where('student_id', $request->user_id) ->whereBetween('created_at', [$start, $end]) ->selectRaw('FROM_UNIXTIME(created_at, "%Y-%m-%d") as day, SUM(duration) as total_duration') ->groupBy('day') ->get(); return $this->success($logs); }

后端返回七天的数据后,前端用canvas绘制柱状图。小程序原生的canvas在老版本上有性能问题,实测后改用了一个轻量级的ec-canvas组件——这是ECharts官方提供的微信小程序适配方案,配置项和Web端完全一致,画一张简单的柱状图没有任何压力。

7. 部署上线与常见问题排查

7.1 服务器环境配置清单

生产环境我用了两台轻量应用服务器:一台跑Laravel API + ThinkPHP管理后台(Nginx + PHP 8.1),一台作为MySQL数据库服务器(初期数据量小,其实可以合并,但分开更利于后续扩展)。

Nginx中两个站点的伪静态配置是常见的坑,ThinkPHP和Laravel都要求所有请求转发到入口文件:

# Laravel站点配置 server { listen 80; server_name api.example.com; root /var/www/laravel/public; index index.php; location / { try_files $uri $uri/ /index.php?$query_string; } location ~ \.php$ { include fastcgi_params; fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; fastcgi_pass unix:/run/php/php8.1-fpm.sock; } }

7.2 线上环境踩过的三个坑

坑一:微信小程序必须配置合法域名。开发工具中可以勾选“不校验合法域名”,但真机预览和发布时必须把所有API域名、CDN域名配置到小程序后台的request合法域名和downloadFile合法域名里。这个配置一般要等一两个小时生效,不要改完马上拿真机测试就说“怎么还是不行”。

坑二:HTTPS证书过期导致视频播放白屏。OSS的CDN域名证书、API域名证书都有有效期,证书到期后的典型表现是“小程序在开发工具里一切正常,但真机上视频黑屏/图片不显示”。排查方式是直接在手机浏览器里打开资源URL看是否有证书错误。

坑三:PHP-FPM进程数配置过低,高峰期接口全部超时。我的CPU是2核,刚开始按默认配置运行,10个人同时刷题就卡死。后来把pm.max_children提高到20、pm.start_servers设为5,才稳定下来。这个数值需要根据云服务器的内存来估算,每个PHP-FPM子进程大约占用30-50MB内存,2GB内存的机器建议不超过30个。

7.3 常见问题速查表

问题现象可能原因解决方案
真机请求API 502 Bad GatewayPHP-FPM进程崩溃或未运行检查php-fpm服务状态,看error.log
视频加载失败、一直转圈域名未配置或签名过期检查小程序后台downloadFile合法域名,检查签名URL时间戳
登录后token失效快Cache驱动未配置Laravel.env中CACHE_DRIVER改成redis或database
上传Excel导入题库中文乱码编码不是UTF-8Excel另存为CSV UTF-8格式,或代码中做编码转换
小程序白屏无报错基础库版本过低在开发者工具中切到新版基础库,或真机上点右上角更新

8. 性能优化与后续扩展方向

8.1 数据库索引与缓存优化

当答题记录表超过十万行后,最直接的优化动作是加索引覆盖常用查询。比如“查询某个学员最近做错的100道题”,就会优先命中idx_student_question(student_id, question_id)和idx_created(created_at)的组合索引。业务中如果发现慢查询告警,先用EXPLAIN看执行计划,别急着改业务代码,很多时候一条索引就能解决问题。

课程详情、首页轮播图这类高频读低频写的数据,用Laravel的Cache门面做十分钟缓存,接口响应时间从上百毫秒降到个位数毫秒,体验提升明显。缓存失效策略很简单——管理员在后台修改课程后同步删除对应缓存key即可。

8.2 按“日活用户”评估服务器容量

很多同学做项目上线之后最焦虑的就是服务器扛不扛得住,其实有一个粗略的估算公式可以参考:假设日活1000人,平均每人每天发起30次API请求,一天的请求总量是3万次,分摊到高峰期约两小时(假设占全天60%流量),每秒约25个请求。单台2核4G的服务器用Nginx + PHP-FPM跑这个量级完全没问题。

8.3 后续可以扩展的三个方向

如果这个项目要继续做深,我的建议优先级是:

第一,接入腾讯云点播或者阿里云视频点播服务,把视频转码、加密、倍速播放这些能力交给专业服务,省去自己维护视频文件的麻烦。第二,增加每日一题推送功能。微信小程序的订阅消息允许给用户推送一次服务通知,可以用来做“每日一题”的定时提醒,这个功能对学习类产品的日活提升非常明显。第三,把练习记录同步维度从“课程”升级到“知识点”,给每道题打上知识点标签,后期就能输出“薄弱知识点分布图”,这是付费转化最强的功能点。

我在实际开发这套系统时最大的体会是:教育类小程序的难点从来不在单个功能的复杂度,而在把“课程—刷题—错题—进度—反馈”这条闭环串起来的完整性。很多项目死于功能点到为止——视频能播但没法记录看到哪里,能刷题但错题不沉淀,打卡后没有数据反馈。只要你把这条主链路每一环都走通,哪怕UI朴素一点,项目的完成度和答辩说服力都会远超那些只做了“前端展示”的同类作品。最后再分享一个实在的小建议:开发过程中保持接口返回格式统一,坚持每个接口写完立刻用apifox或postman自测一遍再往下走,这个习惯能帮你把联调阶段踩坑的时间省掉一大半。

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

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

立即咨询