ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

微信小程序+PHP公考助学系统:ThinkPHP/Laravel选型与数据模型

微信小程序+PHP公考助学系统:ThinkPHP/Laravel选型与数据模型 做公考培训行业的技术开发最头疼的不是代码本身而是业务需求一直在变。今天要加一个“每日打卡”明天要加一个“错题导出”后天又说“视频课要支持缓存”。我参与过的这个公务员考试课程复习助学系统就是一个很典型的微信小程序 PHP后端组合项目小程序负责学员端的学习体验ThinkPHP和Laravel负责后台接口与管理系统开发覆盖课程点播、刷题练习、复习计划、学习统计整套闭环。这篇文章把项目的设计思路、数据模型、接口实现、小程序端开发细节以及我实际踩过的坑完整复盘出来给准备做教育类小程序或者正在纠结PHP框架选型的团队一个直接能用的参考。1. 项目全貌与技术选型思路1.1 公考助学系统到底解决什么问题公考备考和普通职业培训不太一样学员的复习周期长、科目多、资料分散。我接触到的很多培训机构课程视频放在网盘、每日一练发在微信群、模考卷子用Excel统计学员想看一下自己整体复习进度都做不到。这个助学系统的核心价值就是把“课程、题库、计划、数据”四件事收进一个小程序里让学员在手机上完成从听课到刷题到复盘的全部动作。具体拆分下来系统首先要解决三个痛点。第一是内容聚合课程按行测、申论、面试等方向分门别类视频、讲义、课件统一管理第二是练习闭环学员刷题之后要能看到正确率、错题解析并且错题能被自动收集第三是计划落地系统根据考试倒计时生成复习计划学员每天打卡形成习惯。这三个需求看起来很常规但真正做下来会发现数据模型的设计比页面开发复杂得多尤其是错题和刷题记录之间的关系设计不好后面统计接口会写得非常痛苦。这个系统适合谁的场景去参考如果你在培训机构做内部系统或者接了一个教育类小程序的外包项目又或者想用PHP做一套带后台管理的小程序产品这篇内容都能用上。前端是微信小程序原生开发后端在ThinkPHP和Laravel之间做取舍我会把两种框架的写法都对照着说方便你根据团队情况选择。1.2 为什么我坚持选微信小程序加PHP这套组合先说前端微信小程序几乎是为教育类产品量身定做的。学员不需要去应用商店下载App微信里搜一下或者扫个码就能打开登录直接调微信授权用户openid就是天然的用户标识省掉了短信验证码那一套再加上分享卡片、好友排行这类能力培训机构做老带新裂变非常方便。相比UniApp或者其他跨端方案原生小程序在视频播放、组件交互上更稳定而且不用处理Android、iOS、鸿蒙多端的兼容差异。后端选PHP核心原因只有两个部署成本低开发速度快。公考培训机构的后台管理系统本质上就是课程管理、题库管理、订单管理、学员管理这些CRUD操作PHP配合现成的框架几天就能把后台界面搭出来。相比Java那一套环境配置和编译流程PHP在LNMP环境下一个文件夹就能跑起来对中小团队太友好了。也有人说Node.js做小程序后端很火Python的Flask、Django也可以。但我实际对比下来教育类项目的后端更看重“稳”而不是“炫”PHP的ThinkPHP和Laravel都经过了大量商业项目验证文档和社区资料也最齐全。而且PHP的主机成本低一个普通的云服务器就能扛住几千个学员同时刷题的并发性价比很高。对于这个项目来说技术的先进程度不重要快速上线、稳定运行、能随时改需求才是第一位的。1.3 ThinkPHP还是Laravel我的选型建议与两者的真实差距很多人在标题里看到“Thinkphp-Laravel”会疑惑这俩不是两个框架吗实际上这个项目的特殊之处在于早期版本用ThinkPHP开发后台管理后来因为业务扩展和团队技术栈调整核心API部分在另一个子项目中用了Laravel重构。两种框架在同一个体系里并存的情况很常见尤其是培训机构的技术团队往往哪个顺手用哪个。关键是你要知道它们各自的脾气。我列一个真实的对比不谈那些虚的框架排名只讲开发体验。对比维度ThinkPHPLaravel上手门槛低中文文档友好约定少中高命令多概念多路由定义路由文件加注解都支持路由文件为主写法更规范灵活模型层自带ORM简单直接Eloquent ORM功能强大关联关系写起来爽中间件有但用得少核心概念权限、跨域、日志都好处理Session内置File/Redis驱动配置简单功能全但默认需要关注驱动配置适合场景后台管理系统、中小型API复杂业务、多人协作的大型项目我的建议很直白如果团队里PHP程序员以新手为主项目进度又赶选ThinkPHP它能把你的开发成本压到最低。如果项目需要长期维护、接口规模会持续扩大或者团队愿意花一周时间学习那Laravel的优雅度和扩展性会回馈你这周的投入。千万不要在项目进行中频繁切换框架我见过因为换框架导致接口返工的例子一套接口两种写法维护起来非常痛苦。前台小程序对接的API用Laravel写后台课程管理的管理端用ThinkPHP写两者通过HTTP接口通信这样的分工反而清晰。2. 核心功能拆解与数据模型设计2.1 用户体系与微信登录openid是唯一的锚点微信小程序登录的流程大家应该都熟小程序端调用wx.login拿到code后端拿code去微信的接口换openid和session_key然后后端自己维护一个登录态。很多新手在这里犯的错误是直接用openid当用户表主键。openid虽然是唯一的但它一串很长的字符串做主键会影响索引效率而且一旦微信开放平台绑定关系变动openid可能会变所以用户表建议用自增id做主键openid单独建唯一索引。用户表的设计我建议至少包含这些字段id、openid、unionid、nickname、avatar、phone、grade报考类别、status、created_at、updated_at。grade字段别看简单公考系统里学员要区分考国考还是省考是行测为主还是申论为主后面推荐课程和刷题范围都要用它来过滤。phone字段可以在用户主动填写时补充平时不要强迫授权手机号否则小程序的隐私审核会很难过。登录接口拿到openid之后还要做一件事查一下这个openid在不在表里不在就自动注册存在就更新最近登录时间。这个逻辑放到后端接口里做小程序端不需要关心用户是新用户还是老用户。用户表建好之后所有业务表都可以用user_id去关联后续的刷题记录、课程购买、打卡记录就都有了主心骨。2.2 课程资源的表结构设计分类、章节与视频存储课程模块是助学系统的内容核心。这里要区分“课程”和“章节”两个概念一门课程下面会有多个章节视频不能把所有视频一股脑塞在课程表里。课程表负责存课程的基本信息比如标题、封面图、适用科目、讲师、价格、是否免费试看章节表负责存具体视频比如章节名称、视频地址、视频时长、是否试看、排序。视频存储我强烈建议不要自己用服务器的硬盘存用云点播或者对象存储加CDN来做。公考课程的视频时长普遍在一个小时以上学员同时在线观看对带宽的压力很大自己扛服务器流量成本高还容易卡顿。云点播服务商一般都会提供防盗链功能给视频URL加一个过期时间的签名学员复制链接给别人的时候链接已经失效了。这个防盗链很重要教育类内容的版权问题比想象中严重。在设计课程表的时候推荐用一个status字段控制上下架不要直接删数据。培训机构经常要调整课程内容比如某个老师的申论课要下架重新录制逻辑删除比物理删除安全得多。另外课程表最好加一个sort字段控制展示顺序需求方经常说“把某某老师的课排前面”没有sort字段你就要改代码了。2.3 刷题与错题本题目表、记录表、错题表怎么拆刷题模块是整个系统里数据结构最复杂的部分建议拆成四张表题目表、题目选项表、刷题记录表、错题表。题目表存题目的公共属性题干内容、题型单选、多选、判断、所属科目、难度、解析、正确答案。这里需要注意正确答案不要用文本直接存比如“A”而是存成JSON数组或者用单独一张选项表去关联。因为多选题的答案是一个集合判断题的答案又像布尔值统一用JSON存起来解析的时候再按题型处理代码写起来更统一我也踩过坑一开始用varchar存答案后来多选和判断的答案格式对不上改表结构折腾了很久。刷题记录表用来记录学员每一次答题行为字段大概是user_id、question_id、user_answer、is_correct、answer_time、source_type。source_type用来区分这道题是来自每日练习、专项练习还是模拟考试后面做统计排错就能知道哪个环节的题目正确率最低。刷题记录表会非常大一个活跃学员一天能刷两三百道题所以这张表必须建索引组合索引user_id、question_id和user_id、source_type都加上。错题表的逻辑就简单一些学员答错之后往错题表里插入一条记录前提是这个题已经存在并且没有被标记为“已掌握”。我做的版本里加了mastered字段学员在错题本里点“我学会了”这道错题就不再出现在待复习列表里但历史记录还保留。这样设计既能满足“消灭错题”的成就感又不丢失数据。2.4 复习计划与学习统计让学员看得见自己的进度复习计划听起来像是个小功能但实际上它承担的是留存和活跃的职责。计划表的结构可以这样设计user_id、plan_name、start_date、end_date、total_days、current_day、status。学员报名考试之后后端根据考试日期自动生成倒计时计划每天打开小程序会看到一个“今日任务”。任务和打卡是绑定的。每天的任务内容可以动态生成比如“今日学习行测数量关系视频第3节 完成15道逻辑填空题”生成规则不复杂但要跟课程表和题目表联动。打卡表记录每天的完成情况user_id、plan_id、task_date、is_completed、completed_at。如果连续三天没打卡系统可以给学员推送一条提醒这个在小程序里用订阅消息实现一次授权可以多次发送。学习统计这块我做了一个数据看板接口把学员的总学习天数、累计刷题数、平均正确率、课程完成率一次性返回给小程序端展示。SQL写起来不复杂但要注意性能尤其是刷题记录表的数据量大了以后聚合查询会很慢。我建议统计接口不要实时查表每天凌晨用定时任务把昨天的数据汇总到一张统计表里用户打开小程序页面读的是汇总表响应速度能快很多。3. 后端API设计与核心实现3.1 统一接口规范code、message、data的三段式响应小程序端和后端联调最怕的就是接口返回格式不统一。有的接口返回JSON有的接口直接返回字符串前端解析的时候得写一堆兼容代码。我在这个项目里定了一个规范所有接口统一返回这个结构{ code: 0, message: success, data: {} }code为0表示成功非0表示各种错误类型。这里要注意HTTP状态码和业务码要分开比如用户登录过期HTTP状态码可能还是200但业务码是10001小程序端拿到code之后跳转登录页。这样做的好处是网络层的错误和业务逻辑的错误分开处理排查问题的时候思路清晰。错误码表一定要在项目一开始就维护起来否则后面会乱套。我给几个常见错误码供参考10001登录态失效、10002用户不存在、20001参数缺失、20002题目不存在、30001课程已下架、40001无权访问。错误码不要用得太模糊比如统一返回“400请求失败”其实等于没返回前端根本不知道是登录过期还是参数问题。每个错误码都要配上对应的message前端可以直接把这个message弹给用户看。3.2 路由配置与地址跳转ThinkPHP和Laravel的写法对照这部分的痛点主要来自伪静态配置和路由分组。真实开发中接口地址是一长串比如/api/v1/user/info如果你不做伪静态URL里全是index.php问号参数小程序端倒是能访问但看着难受也不利于后续CDN和日志分析。ThinkPHP的写法以TP8为例路由文件放在route/app.php里use think\facade\Route; Route::group(api/v1, function () { Route::post(login, Auth/login); Route::get(course/list, Course/lists); Route::post(answer/submit, Answer/submit); })-middleware([\app\middleware\ApiAuth::class]);Laravel的写法比如Laravel 11在routes/api.php里Route::prefix(api/v1)-middleware(auth:api)-group(function () { Route::post(/login, [AuthController::class, login]); Route::get(/course/list, [CourseController::class, lists]); Route::post(/answer/submit, [AnswerController::class, submit]); });两种框架的路由都支持分组和中间件差别在语法风格上。真正要提醒的是路由匹配顺序很重要如果你同时定义了/course/{id}和/course/list一定要把静态路由放在动态路由前面否则list会被当成{id}的值去匹配返回一个“课程不存在”的错误。这个坑我遇到不止一次尤其是接口越来越多的时候路由一多就容易出现这种“被吞”的情况。3.3 登录态保持为什么小程序里不能用session一套到底传统Web开发中登录态靠Session加Cookie实现浏览器自动带Cookie后端从Session里取用户信息。但微信小程序的运行环境跟浏览器不一样请求由wx.request发起并不会自动携带Cookie。如果你在小程序里写了Session::set(user_id, 1)最后会发现每个请求都拿不到这个值因为请求根本没有带着Session ID过来。正确的做法是Token机制。用户登录成功之后后端生成一个token字符串返回给小程序端小程序把它存在Storage里后续每个请求都在Header里带上Authorization: Bearer token后端根据token解析出用户身份。token用什么生成可以用JWT也可以直接把这个token存数据库表里我倾向于用JWT因为它天然带过期时间服务端不需要存session分布式部署的时候也友好。这是很多PHP工程师容易忽略的一个点习惯性地把Web开发思维带进小程序结果对接的时候到处踩坑。在ThinkPHP里用token的方式推荐直接操作Redis存储token和对应的用户id。Laravel则可以用auth中间件配合tymon/jwt-auth扩展包成熟稳定。无论用哪种token过期时间建议设置成7天小程序端每次启动时做一次静默登录检测如果token快过期了用refreshToken去换新的保证用户无感续期。3.4 三个核心接口的完整实现从登录到交卷这里我把三个最有代表性的接口写出来包括登录接口、课程列表接口、提交答题记录接口都是我实际跑通过的逻辑。首先是登录接口核心代码在Laravel里的写法public function login(Request $request) { $code $request-input(code); if (!$code) { return $this-fail(参数缺失, 20001); } // 调微信接口换取openid $appid config(weixin.appid); $secret config(weixin.secret); $url https://api.weixin.qq.com/sns/jscode2session?appid{$appid}secret{$secret}js_code{$code}grant_typeauthorization_code; $client new Client(); $response $client-get($url); $result json_decode($response-getBody(), true); if (empty($result[openid])) { return $this-fail(微信登录失败, 10003); } // 查找或创建用户 $user User::where(openid, $result[openid])-first(); if (!$user) { $user User::create([ openid $result[openid], nickname 公考学员 . mt_rand(1000, 9999), ]); } // 生成token $token Auth::login($user); return $this-success([ token $token, user_info $user ]); }这里的微信接口请求我建议用guzzlehttp/guzzle或者在ThinkPHP里用think-http不要直接用file_get_contents超时控制和错误处理都更好。课程列表接口比较简单但要注意分页和筛选条件我直接给一个Laravel版本的示例ThinkPHP思路一模一样public function lists(Request $request) { $categoryId $request-input(category_id, 0); $page $request-input(page, 1); $pageSize $request-input(page_size, 10); $query Course::where(status, 1); if ($categoryId 0) { $query-where(category_id, $categoryId); } $list $query-orderBy(sort, desc) -paginate($pageSize, [id, title, cover, price, desc], page, $page); return $this-success($list); }提交答题记录的接口稍微复杂因为一次可能要提交一组答案。前端会传一个数组question_ids和answers一一对应后端要遍历处理并计算正确率。这里要注意事务一组题里只要有一条写失败整组答案就不能算提交成功用事务包裹起来失败就回滚。4. 微信小程序前端实现4.1 页面架构与底部导航原生tabBar能省一半事小程序的页面架构我建议不要用自定义导航能省事就省事。app.json里直接声明tabBar配置首页、课程、刷题、我的这四个底部导航微信会自动渲染原生导航栏和底部栏稳定性和体验都好。只有那些需要复杂顶部效果的页面才考虑自定义导航栏比如课程详情页要放一个渐变色头部。顶部导航栏高度是很多新手会踩的坑尤其是做自定义导航栏的时候。不同机型的状态栏高度不一样iPhone X以上的刘海屏高度是44px普通安卓机是24px你如果写死一个高度真机上一测就露馅。我用的一个稳妥方案是在进入页面的时候获取系统信息const sysInfo wx.getSystemInfoSync(); const statusBarHeight sysInfo.statusBarHeight; // 胶囊按钮位置 const menuButton wx.getMenuButtonBoundingClientRect(); const navBarHeight (menuButton.top - statusBarHeight) * 2 menuButton.height;这样算出来的导航栏高度适配各种机型。把这个逻辑封装成一个工具函数所有的自定义导航页面统一调用避免每个页面都重新写一遍。4.2 请求封装一次写好全站复用小程序的wx.request是比较底层的API如果每个页面都直接调会出现两个问题一是重复代码太多二是处理登录态过期、错误提示的逻辑分散在各个页面后面想统一改都难。我建议封装一个request.js核心代码如下const request (url, method GET, data {}) { return new Promise((resolve, reject) { const token wx.getStorageSync(token); wx.request({ url: baseUrl url, method: method, data: data, header: { Content-Type: application/json, Authorization: token ? Bearer token : }, success: (res) { if (res.statusCode 200) { if (res.data.code 0) { resolve(res.data.data); } else if (res.data.code 10001) { // 登录态过期跳转登录页 wx.navigateTo({ url: /pages/login/login }); reject(res.data); } else { wx.showToast({ title: res.data.message, icon: none }); reject(res.data); } } else { wx.showToast({ title: 网络异常, icon: none }); reject(res); } }, fail: (err) { wx.showToast({ title: 请求失败, icon: none }); reject(err); } }); }); }; module.exports { request };这里有个关键点登录态过期不能只提示一下还要把当前用户的缓存清掉因为用户可能已经换账号了。我在跳转登录页之前会先执行wx.removeStorageSync(token)和wx.removeStorageSync(user_info)避免脏缓存。4.3 课程视频与缓存策略播放可以下载要留个心眼视频播放是教育类小程序的核心体验。微信小程序自带的video组件很好用支持自定义封面、播放进度记忆、倍速播放直接传入视频地址就行。唯一要注意的是视频地址必须在小程序后台配置到downloadFile合法域名里而且必须是HTTPS否则真机上播放不出来。关于视频缓存和下载这里要区分两种情况。对于在线播放AppData里会自动缓存一部分不需要你额外干预对于“下载后离线观看”的需求微信小程序的wx.downloadFile接口确实能下载文件但下载后的文件只能在小程序内部访问而且有单个文件不超过200MB的限制。公考的课程视频动辄几百MB我建议要么对视频做分片处理要么干脆不做离线下载用“缓存时间”的概念把用户最近观看的几个章节的视频缓存到本地超过缓存上限自动清除旧数据。实际落地的时候我设置了一个缓存管理机制每个用户最多缓存5个章节视频超过后弹窗提示“清理缓存”。清理时用wx.getSavedFileList拿到本地文件列表按时间排序删除最早的。这个机制加上去之后学员的反馈很好既满足了离线看课的需求又不会把小程序的包体撑爆。4.4 刷题交互实现单选多选、答题卡与断点续答刷题页面的交互细节特别多。先说题型区分单选题用radio-group组件多选题用checkbox-group组件判断题本质上也是单选只是选项变成了“正确”和“错误”。组件本身不复杂麻烦的是状态管理用户选完之后要允许修改点了“下一题”之后就不能再改还要显示当前题号、答题进度。答题卡功能我推荐做一个半屏弹窗展示所有题号已答题号变成绿色未答题保持灰色当前题号高亮。这样用户能直观看到还剩多少题没做点击某个题号直接跳转。这个弹窗的数据结构用一个数组维护即可answerStatus数组的索引就是题号值是null或者用户选择的答案。断点续答也很实用。用户在刷题过程中可能随时退出下次进来说不定从哪题继续。我用的方案是每次切换题目的时候都把题目id 当前索引 选择答案 剩余时间存到Storage里页面初始化时读取这份缓存存在就提示“检测到上次未完成练习是否继续”。这个功能在开发时不起眼但学员使用频率很高尤其是备考的人碎片化时间多刷一半去吃饭是常事。5. 开发实战中的常见问题与排查实录5.1 微信开发者工具里请求正常真机上一片红这个问题几乎每个做小程序的人都会遇到表现形式很诡异开发者工具里接口请求正常一扫码到真机上全部请求失败。原因基本都是域名白名单和HTTPS证书的问题。小程序真机环境对网络安全的要求非常严格所有请求的域名必须在小程序后台配置到白名单里而且必须支持HTTPS证书还不能过期。排查思路分三步走。第一步在微信公众平台后台的“开发管理”里找到“服务器域名”把接口域名填进去记住request合法域名和downloadFile合法域名要分开填视频地址走downloadFile域名。第二步确认HTTPS证书是有效的不要用自签名证书真机不认。第三步临时打开开发者工具的“不校验合法域名”开关可以做本地调试但发布版本一定不能依赖这个开关。还有一个容易忽略的点就是域名不能有端口。小程序要求合法域名不能带端口号如果你本地联调用http://localhost:8080只能走开发者工具的不校验开关真机测试一定要用线上域名。我通常的做法是开发环境用内网穿透工具把本地服务映射成HTTPS域名这样真机也能调本地代码。5.2 顶部导航栏高度适配iPhone状态栏高度总在变这个问题在自定义导航栏的时候反复出现。iPhone SE、iPhone 13、iPhone 14 Pro Max的状态栏高度都不一样特别是灵动岛机型状态栏高度和胶囊按钮的位置跟老机型差距很大。如果你的页面写死了导航高度就会出现标题上移或者下移的bug。我的解决方案是写了一个通用的getNavInfo()方法统一返回状态栏高度、导航栏高度、胶囊按钮位置const getNavInfo () { const sysInfo wx.getWindowInfo(); const menuButton wx.getMenuButtonBoundingClientRect(); return { statusBarHeight: sysInfo.statusBarHeight, navBarHeight: (menuButton.top - sysInfo.statusBarHeight) * 2 menuButton.height, menuButton: menuButton }; };注意wx.getSystemInfoSync已经在很多基础库版本里被标记为不推荐了新项目建议用wx.getWindowInfo获取窗口信息。拿到这些数据之后自定义导航栏的整个高度等于状态栏高度加导航栏高度里边的标题文字垂直居中在导航栏区域。5.3 session过期与token失效静默续期怎么做小程序里的token过期处理如果处理不细心就会出现用户用着用着突然被踢出去的情况。我的处理方式包含两层第一层是token有效期设置为7天用户在7天内不需要重新登录第二层是额外发一个refresh_token有效期30天当检测到token过期时小程序端静默调用刷新接口换取新token然后自动重放刚才失败的请求。Laravel的JWT配置里可以设置ttl为10080分钟7天refresh_ttl为43200分钟30天。ThinkPHP里如果你用JWT扩展包也有对应的有效期配置。刷新接口的代码核心逻辑是public function refresh(Request $request) { try { $newToken Auth::refresh(); return $this-success([token $newToken]); } catch (\Exception $e) { // refresh_token也失效强制重新登录 return $this-fail(登录已过期, 10001); } }重点说一下请求重放小程序端在请求封装里遇到10001错误时先调用刷新接口拿到新token之后重新发起原来的请求。这里要防止死循环如果刷新接口本身也返回过期就直接跳登录页不再重试。把这段逻辑写在request.js里整个项目都受益。5.4 小程序审核与支付上线前最容易被卡的两个点辛苦开发完结果卡在审核上这种滋味太难受了。公考助学系统属于教育类目审核最容易出问题的有两个点类目资质和虚拟支付。类目方面如果小程序涉及课程播放和在线学习一般需要选择“教育-在线视频课程”或者“教育-教育信息服务”类目后者通常不需要提供办学许可证但具体要看平台最新规则。资质方面提前准备好营业执照如果涉及学科类培训内容管控会更严格建议上线前先拿一个最小版本去走一遍审核流程用“试错”的方式把资质问题摸清楚。虚拟支付是另一个大坑。微信小程序iOS端对虚拟支付限制很严格课程这类虚拟商品iOS上是不能直接使用微信支付或者苹果IAP支付的否则会审核不通过。我的解决方案是iOS端的课程购买入口可以展示但点击购买时提示“请使用安卓手机购买”或者引导用户跳转到H5页面完成支付。这个方案体验不是最好但合规。我见过很多团队因为这个问题上线后又被下架这个前置工作一定要做好。最后再分享一点我个人的体会。这类助学系统真正难的不是某个技术点而是数据模型设计阶段能不能把业务想透。课程、题目、记录、计划这几张表之间的关系决定了后面所有功能的开发效率。我实际重构过一次数据库才深刻理解“先设计表结构再写代码”这句话的分量。所以如果你正准备动手做类似项目我建议多花三天时间把表设计好把字段类型、索引、关联关系理清楚后面开发会顺很多。另外视频播放和支付这两块合规问题一定要在项目启动前调研清楚不要等到开发完了再改架构那样返工成本太高。
返回列表