简介:Muu云课堂公众号小程序带插件源码包,适用于在线教育与知识付费场景,面向希望基于公众号搭建网校系统的开发者和运营者,用以快速实现课程展示、购买与播放等核心功能。资源共2000个文件,以脚本、服务端接口、页面结构与样式文件为主,其中前端脚本文件承担交互逻辑,服务端脚本提供接口和后台能力,小程序页面配置辅助界面构建,同时包含图片、数据表及相关配置文件,压缩包整体约46.2MB。包内附带插件和清晰的目录组织,便于按模块理解公众号端、管理后台和扩展功能,也能直接参考接口设计、页面布局与数据结构进行二次开发。已有122人学习下载,适合具备小程序前端或后端开发基础、希望获得整套可运行实现的中高级读者。
1. Muu云课堂V2的1.9.2版本到底讲了什么——公众号、小程序与插件的三角关系
做在线教育系统的人,大部分都踩过同一道坎:公众号里卖课是一套逻辑,小程序里卖课是另一套逻辑,后台管理又是第三套。三套东西数据不通、会员对不上、订单要手动同步,光维护就够喝一壶。Muu云课堂V2就是奔着这个痛点去的,1.9.2作为V2系列里的一个带插件版本,意味着它不只是修修补补,而是把公众号端、小程序端和后台管理端收拢到同一套代码体系里,同时把“插件”做成了真正的扩展机制,而不是往核心代码里堆功能。
这篇文章不讲Muu云课堂是什么背景、谁开发的那些虚的,只讲你拿到这套系统之后,怎么部署、怎么接公众号、怎么写插件、上线时备案和回调这些事怎么办。适合正在选型在线教育系统的技术负责人,也适合接了二开需求、需要快速摸清这套代码结构的PHP工程师。
2. Muu云课堂V2的架构与部署:ThinkPHP下公众号与小程序端如何共用一套后台
2.1 从V1到V2:为何重写为模块化路由与API分离
Muu云课堂V1沿用的是常见的单应用CMS思路,控制器直接混着页面输出,公众号H5和小程序各写一套入口。V2最明显的调整是模块化路由和前后端分离:前端请求走后端API接口,业务逻辑按模块组织,插件通过注册机制挂载,而不是改核心控制器。这么做的直接收益是公众号H5和小程序端可以共用同一套API和同一套后台数据,会员、课程、订单、支付回调都是同一份逻辑,不至于出现“公众号端付了款、小程序端查不到订单”这种数据分裂问题。
1.9.2这个版本号属于V2的中期迭代,插件机制已经相对稳定。对二开的人来说,理解它的价值在于:你不需要为了一个功能改动去动系统核心文件,照着插件规范写一个独立包,后台开关一开就生效。这也是我建议团队优先研究插件机制而不是直接改核心的原因,后续升级能少很多冲突。
2.2 目录结构与运行环境准备
拿到代码包后,第一件事不是急着配域名,而是先看目录结构,明确三个入口分别指向哪里。一个常见的V2目录布局大致如下:
project-root/ ├── app/ │ ├── api/ # API模块,小程序和H5共用 │ ├── admin/ # 管理后台模块 │ ├── plugin/ # 插件目录,每个插件一个子目录 │ └── common/ # 公共函数与常量 ├── public/ │ ├── index.php # 入口文件 │ └── uploads/ # 上传资源目录,需可写权限 ├── config/ ├── route/ └── extend/部署前先对照环境要求,避免装到一半才发现PHP版本不满足。我一般建议按表里的配置来准备,特别是PHP的fileinfo扩展,少装这个会导致上传功能直接报错。
| 环境项 | 推荐配置 | 说明 |
|---|---|---|
| PHP | 7.4 - 8.0 | 低于7.4会有语法兼容问题,高于8.0部分老插件报错 |
| MySQL | 5.7 + | 8.0可用,但注意数据库连接驱动要选mysql |
| Web服务器 | Nginx + PHP-FPM | Apache也可,注意伪静态规则不同 |
| 必装扩展 | fileinfo、pdo_mysql、curl | fileinfo缺失最常见,表现为图片上传500 |
| 小程序端 | uni-app 项目 | 需HBuilderX或CLI方式运行 |
这里尤其提示一下:运行环境不是越新越好。Muu云课堂V2这套代码带着商业系统的演进痕迹,对PHP 8.1以上版本的部分语法糖支持并不完整,别一上来就用PHP 8.2跑生产环境,很可能会踩到extend目录里某些老库的兼容问题。
2.3 本地部署最小步骤:从克隆到管理后台可登录
部署步骤说复杂也复杂,说简单其实就那么几步。我把最小可行路径写出来,你按顺序执行,走到后台登录页就算成功了第一步。
git clone <你的代码仓库地址> muu-cloud-classroom cd muu-cloud-classroom composer install --no-dev # 创建数据库并导入初始SQL mysql -uroot -p -e "CREATE DATABASE muu_cloud DEFAULT CHARACTER SET utf8mb4;" mysql -uroot -p muu_cloud < install.sql # 配置环境文件 cp .env.example .env vim .env # 设置数据库连接、APP_DEBUG装完依赖、导完数据、改完环境配置之后,还要处理两个容易忽略的细节。第一个是public目录的web根目录指向,Nginx的root要指到public/,否则访问的就是框架起始目录,路由全乱。第二个是runtime目录的写入权限,PHP-FPM进程用户的写权限要到位,不然页面会报“runtime目录不可写”的错误,这一步在Linux服务器上几乎必踩。
配好之后的启动命令不是启动一个常驻进程,而是启动PHP内置服务器用于本地调试:
php think run -p 8080 # 浏览器访问 http://localhost:8080/admin # 默认账号密码见安装文档,首次登录后务必修改之所以用php think run而不是直接配虚拟主机,是因为本地调试阶段这样最快,改完代码刷新即生效。生产环境再切换到Nginx + PHP-FPM,路由规则用ThinkPHP标准伪静态。
2.4 公众号端、小程序端、管理后台三个入口的分工
理解三个入口的分工,是后续所有配置的认知基础。公众号端走的是HTTP请求直接返回H5页面或跳转授权链接,小程序端走的是uniapp编译后的微信小程序,请求后端API时在Header里带token。管理后台则是独立的Admin模块,控制课程上架、订单处理和插件启停。
三者共用数据库表,但业务缓存是分开的。比如小程序端的首页课程列表会缓存在redis里,公众号端H5的页面缓存可能是文件缓存,两边同时改课程价格后,可能出现一端更新了另一端还是旧数据。所以部署完先确认缓存驱动配置,建议统一用Redis,并且后台有“刷新缓存”按钮时养成点击习惯。这也是为什么后台管理里总是有一个“清缓存”的入口,它不是摆设,是应对这种双端架构差异的兜底方案。
3. 公众号H5与小程序端的接入实战:从JS-SDK签名到定位与标题设置
3.1 公众号H5在微信内的登录态与JS-SDK签名流程
Muu云课堂V2的公众号端不需要自己实现微信登录,后端API已经封装好了OAuth授权流程。你做的只是在公众号后台配置网页授权域名,然后前端引导用户跳转到授权链接,拿到code后换取openid。这里最容易踩的坑是:微信网页授权回调域名不能带端口,且必须是ICP备案过的域名,否则回调时微信会直接报错“redirect_uri参数错误”。
真正需要手写的是JS-SDK签名。凡是涉及获取地理位置、分享朋友圈、扫一扫这些微信原生能力,都要先做签名。V2后台的“公众号配置”页面通常会让你填AppID和AppSecret,后台会缓存access_token并生成签名所需要的数据。但有些版本不会自动生成签名,需要接口里自己算,代码逻辑一般是这样的:
public function jsSdkConfig() { $appId = config('site.mp_appid'); $jsapiTicket = $this->getJsapiTicket(); // 缓存7200秒 $timestamp = time(); $nonceStr = md5(uniqid(mt_rand(), true)); $currentUrl = 'http://' . $_SERVER['HTTP_HOST'] . $_SERVER['REQUEST_URI']; $string = "jsapi_ticket={$jsapiTicket}&noncestr={$nonceStr}×tamp={$timestamp}&url={$currentUrl}"; $signature = sha1($string); return compact('appId', 'timestamp', 'nonceStr', 'signature'); }这段代码的核心是拼接字符串的字段顺序:jsapi_ticket、noncestr、timestamp、url,一个都不能少且顺序不能乱,最后用SHA1加密。这里的$currentUrl必须是当前页面完整的URL,去掉#号后面的部分,否则签名校验失败。我看到很多人的问题是把后端接口地址当作签名URL传进去了,微信比对的是实际页面的地址,不是API地址。
3.2 用uniapp开发H5嵌入公众号:定位与路由的坑
如果你用uniapp打包H5嵌入Muu云课堂的公众号端,两个问题和定位相关,一个是路由模式,一个是定位API调用。uniapp H5模式下路由默认是hash模式,公众号网页授权回调时URL会带上code参数,如果回调地址是https://yourdomain.com/#/pages/course/detail?id=1,那么code在#号后面的路径解析里,微信回调带参那一套容易拿不到code。解决办法是后端在授权回调页处理完code换取openid之后,再重定向回前端路由,用session或token维持登录态。
获取定位是另一个高频需求。公众号网页端获取地理位置有两个层次:使用微信JS-SDK的wx.getLocation接口获取的是WGS84坐标,小程序端的wx.getLocation返回的则是GCJ02坐标,两者混用会把地图标偏移几百米。我在Muu云课堂的H5端推荐这样处理:
// 判断是否在微信浏览器内 const ua = navigator.userAgent.toLowerCase(); const isWechat = /micromessenger/.test(ua); if (isWechat) { wx.ready(function () { wx.getLocation({ type: 'gcj02', success: function (res) { // 将坐标传给后台,用于课程推荐或附近面授班 uni.request({ url: '/api/location/update', data: { latitude: res.latitude, longitude: res.longitude }, header: { token: uni.getStorageSync('token') } }); } }); }); } else { navigator.geolocation.getCurrentPosition(function (pos) { // 浏览器环境走HTML5定位兜底 console.log(pos.coords.latitude, pos.coords.longitude); }); }注意这里判断了微信环境和普通浏览器环境。微信内必须走JS-SDK,因为普通浏览器定位在微信里会被拦截;微信外则可以用HTML5原生的getCurrentPosition。定位拿到的坐标建议直接传给后台接口,由后台决定是存储还是反解析成城市,不要在前端做经纬度反查,因为腾讯地图JavaScript API的配额限制和跨域问题会让前端代码臃肿不少。
3.3 小程序端的API对接与动态标题设置
小程序端与公众号端的核心差异在于请求方式。公众号H5可以依赖Cookie和Session,小程序则必须在请求头显式携带token。Muu云课堂V2的API模块对小程序端做了单独鉴权逻辑,登录返回的token通常有效期为7天,过期后调用接口返回401。前端需要在拦截器里统一处理token刷新或重新登录,不能只对一个接口做处理。
小程序里还有一个高频需求:动态设置页面标题。不同课程、不同讲师页面要显示不同标题,这直接影响分享卡片的效果。用uniapp开发时,不能用document.title,而是要用小程序的wx.setNavigationBarTitle,在uni-app里封装了一层,代码如下:
onLoad(options) { // 从路由参数读取课程名称 const courseTitle = options.title ? decodeURIComponent(options.title) : '云课堂'; // 同时设置导航栏标题和分享标题 uni.setNavigationBarTitle({ title: courseTitle }); uni.setNavigationBarColor({ frontColor: '#ffffff', backgroundColor: '#3f7cff' }); // 分享给好友时的标题与缩略图 uni.showShareMenu({ withShareTicket: true }); }这段代码放在页面级组件的onLoad里,route跳转时把标题作为query参数传进来。设置导航栏标题后必须同步设置分享菜单里的标题,否则用户点分享时看到的还是默认的“Muu云课堂”而不是课程名,两边不一致会直接影响转化率。另一个细节是setNavigationBarColor要在标题设置之后调用,先设置颜色再设置标题的话,部分安卓机型会有闪烁。
4. “带插件”是怎么实现的:插件机制、最小插件与参数约定
4.1 插件不是模块:V2插件机制的目录与注册表设计
很多人把ThinkPHP生态里的“模块”和“插件”混为一谈。模块是系统级的业务边界,比如admin、api、index;插件则是在不影响核心代码的前提下扩展某个具体业务的包。Muu云课堂V2的插件机制参考了常见TP插件的设计思路:每个插件是一个独立目录,包含自己的控制器、模型、配置文件和钩子注册入口。系统启动时扫描app/plugin/目录下的所有插件,读取它们的plugin.ini清单文件来决定是否加载。
这种设计的核心优势是隔离性。写插件时你可以把数据库表名、业务缓存、模板文件都包在插件自己的目录里,不污染主表结构。2.x版本里系统对插件的启停控制存储在一个配置表里,后台“插件管理”页面展示的列表就是扫描目录后与启用状态表join出来的结果。
4.2 编写一个最小插件:从控制器到钩子输出
手写一个最小插件比你想的要简单。我以“课程报名后给学员发送公众号模板消息”为例,拆解插件代码的组织方式。
app/plugin/ └── enrollment-notify/ ├── plugin.ini # 插件清单:名称、版本、作者、启用的钩子 ├── EnrollNotify.php # 插件主类,实现钩子响应 └── config.php # 插件私有配置项plugin.ini的内容决定了系统怎么识别这个插件:
[plugin] name = enrollment-notify title = 报名通知助手 version = 1.0.0 hook = after_enroll_successEnrollNotify.php是插件主逻辑:
<?php namespace app\plugin\enrollment_notify; use think\facade\Log; class EnrollNotify { public function handle($params) { $orderId = $params['order_id'] ?? 0; $courseId = $params['course_id'] ?? 0; if (!$orderId || !$courseId) { return false; } // 拉取订单信息,发送模板消息 $openid = db('order')->where('id', $orderId)->value('openid'); $res = $this->sendTemplateMessage($openid, $courseId); if (!$res) { Log::error('报名通知发送失败', ['order_id' => $orderId]); } return $res; } private function sendTemplateMessage($openid, $courseId) { // 组装模板消息参数 // 调用公众号API发送 } }主类里的handle方法就是钩子触发时系统回调的入口,$params数组由系统在事件发生时收集并传入。这样做的优点是显而易见的:核心代码不需要知道插件的内部实现,它只负责在特定时间点调用handle。如果你的插件要同时响应多个钩子,可以在插件类里定义多个方法,或者在清单文件里声明多个hook,系统按顺序依次调用。
4.3 插件的启用、卸载与数据库变更
插件不是放进目录就生效的,还要在后台完成启用操作。启用过程常见做法是后台读取插件目录生成启用记录,同时执行插件自带的安装SQL,创建它需要的独立数据表。卸载则是反向操作:先执行卸载SQL,可能删除表,然后移除启用记录。
这里有一个非常重要的提示:卸载插件时不要自动去删插件目录里的代码文件,只做数据库清理和启用状态移除。原因是开发者可能改过插件代码,你删了目录后如果再想装回来,改动的代码就丢了。我自己处理二开项目的惯例是:卸载只停用,不删除文件;确认不再使用时再手动删目录。这个习惯能避免很多“插件不见了但数据还在”的恢复事故。
数据库变更的规范也值得说两句。插件自带的建表语句应该使用系统统一的前缀,比如enroll_order_log要用成muu_enroll_order_log,这需要在插件安装逻辑里对SQL做前缀替换。有的插件作者偷懒写死表名,安装到第二个站点时就会和别的插件撞表。碰到这种插件,接手的团队第一件事就要检查它的SQL语句使用了什么前缀策略。
5. 上线的最后一公里:备案信息、回调白名单与常见失败点
5.1 小程序备案备注信息与服务器配置的对应关系
小程序从2023年起需要完成备案才能上线,后台“小程序备案备注信息”这一栏经常把人卡住。刚看你以为随便写点业务描述就行,实际上微信审核会把备注内容和你的服务类目、页面内容做比对。你的小程序如果主要是课程展示和购买,备注信息就围绕“在线课程展示、购买、学习记录查询”来写,不要提“社交”“直播”这类扩展类目。
备案信息和服务器配置是一一对应的关系。服务器域名必须是HTTPS,且需要在小程序后台“开发管理-服务器域名”里配置request合法域名、uploadFile合法域名和downloadFile合法域名。很多人的接口在开发者工具里能通、真机上不通,大概率是忘了加downloadFile合法域名——课程封面图、视频预览都算downloadFile,少数几个图片可以走base64,但视频和PDF课件必须走合法域名下载。
5.2 三个高频失败场景:token验证、签名无效、上传超时
公众号后台配置服务器URL时,微信会发一条token验证请求。这套代码里如果你把site_url配成了带http://的形式,验证大概率失败,因为微信要求URL和Token完全匹配,且Token值包含字母数字以外字符时校验也可能出现意外。遇到验证失败,先看后台日志,日志里记录了微信发来的timestamp和nonce,对比一下本地计算出来的signature是否一致。
JS-SDK签名无效是第二个高频场景。现象是前端wx.config里报了invalid signature,多数原因是URL获取不完整。有的代码用了window.location.href,但微信内置浏览器里这个值会多出一些utm参数或者被URL编码过,后端拿到的URL和前端实际打开的URL不一致就导致签名失败。解决办法是后端接口里把当前完整URL原样传回来做对比,两边差一个字符都算不一样。
上传超时排在第三。课程封面、视频文件传到/uploads目录下,如果Nginx配置了client_max_body_size默认1M,超过几MB的图片直接413。视频上传则要检查PHP的upload_max_filesize和post_max_size,这两个值一个管单文件大小,一个管整个请求体大小,只改前者、后者不跟着改,大文件还是会失败。
5.3 验证清单:用一份检查单快速定位问题
不用等用户报故障,上线前照下面这份清单逐项过,能挡掉大多数常见问题。建议把这份检查清单直接整理成团队内部wiki,每次上线新环境都过一遍:
- [ ] 公众号后台:网页授权域名是否带https且无端口 - [ ] 公众号后台:JS接口安全域名是否已添加 - [ ] 小程序后台:request/downloadFile合法域名是否已配置 - [ ] 小程序后台:不可信域名是否全部为HTTPS - [ ] 服务器:PHP fileinfo扩展是否存在 - [ ] 服务器:runtime与uploads目录是否可写 - [ ] 数据库:字符集是否为utf8mb4 - [ ] 缓存:Redis是否连通且后台可刷新成功 - [ ] 支付:回调URL是否在商户平台配置且可公网访问最后一条支付回调往往被忽略。调试支付时如果回调地址填的是http://localhost或者内网穿透地址,微信支付回调会直接失败,因为微信服务器无法访问你的内网。开发环境用内网穿透工具调试没问题,但上线前必须把回调地址改成正式域名的/api/pay/notify。判断是否配置正确,直接看支付成功后的订单状态变化,1分钟内未变成已支付就要立刻查日志里的回调记录,别等用户来投诉。
本文还有配套的精品资源,点击获取