简介:这是一套基于ThinkPHP框架开发的教育在线网完整源码,面向想学习PHP Web开发、研究在线教育平台架构的开发者与建站人员。系统集会员中心、PC端与WAP手机自适应页面于一体,并内置在线教学与考试模块,可用于搭建培训类教育站点或作为二次开发的学习范本。压缩包共约2000个文件,整体130.83MB,以gif、jpg、png等图片素材和php业务脚本为主,辅以html模板、js与css前端资源,另含少量sql数据库文件、txt说明及音视频示例,目录结构完整,覆盖前端界面与后端逻辑。资源中附有更新说明,便于了解版本迭代与功能修复情况。目前已有1095人学习下载,读者可从中掌握ThinkPHP的MVC组织方式、会员权限设计、响应式布局与在线考试流程的实现思路,适合具备一定PHP基础、希望深入理解教育系统开发的中级开发者参考借鉴。
1. 一套 ThinkPHP 内核的在线教育系统,到底能跑通哪些业务闭环
最近在帮一个做职业培训的朋友看后台系统,他手里有一套基于 ThinkPHP 内核的教育在线网完整源码,功能列表里写着会员中心、PC 版、WAP 手机自适应、在线教学和考试系统。他问我的第一个问题不是“这代码好不好”,而是“我能不能直接拿它开一个班”。这个问题其实很典型——很多做在线教育 教育 ThinkPHP 项目的从业者,拿到源码后最关心的不是架构多优雅,而是能不能在两周内把课程上架、学员注册、在线考试这条链路跑通。
这套源码的定位很明确:它不是那种只带一个后台管理壳子的半成品,而是把会员体系、课程展示、在线答题、多端适配都揉进了一个 ThinkPHP 应用里。适合谁用?一是中小型培训机构想快速搭一个自有网校,二是接外包的开发者需要一套能改能扩的底子,三是自己录课卖课的个人讲师,想省掉从零写用户系统和考试模块的时间。它解决的核心问题就一个:把“招生—教学—考核”这条线用一套代码串起来,而不是东拼西凑三四个系统。
2. 会员中心与权限体系:从注册到分角色的落地拆解
2.1 会员数据表设计与注册登录流程
拿到源码后我习惯先看数据库结构,因为会员中心是整个系统的地基。这套源码里跟会员相关的核心表通常包括user(主表)、user_group(用户组)、user_level(等级)、user_bind(第三方绑定)这几张。ThinkPHP 的模型层会把它们关联起来,注册时往user写一条记录,同时根据注册渠道往user_group里塞一个默认组 ID。
注册登录的入口一般在application/home/controller/User.php这类文件里。我一般会先跑一遍注册流程,看它密码加密用的是不是password_hash,还是老式的md5加盐。如果是后者,上线前必须换掉,这是血泪经验——早期很多 ThinkPHP 项目栽在这上面。
// 典型的注册逻辑片段(ThinkPHP 5.x 风格) public function register() { if (request()->isPost()) { $data = input('post.'); // 先校验手机号或邮箱是否已存在 $exist = Db::name('user')->where('mobile', $data['mobile'])->find(); if ($exist) { return json(['code' => 0, 'msg' => '该手机号已注册']); } // 密码加密,不要用 md5 $data['password'] = password_hash($data['password'], PASSWORD_DEFAULT); $data['create_time'] = time(); $data['group_id'] = 2; // 默认普通会员组 $uid = Db::name('user')->insertGetId($data); // 注册后自动登录,写入 session session('user_id', $uid); return json(['code' => 1, 'msg' => '注册成功']); } }这段代码里几个参数值得注意:group_id默认值决定了新用户能看到哪些课程,create_time用时间戳方便后续做统计,password_hash的第二个参数用PASSWORD_DEFAULT让 PHP 自己选当前最安全的算法。如果你要改成手机验证码注册,就在insertGetId之前加一层短信校验,把验证码存 Redis 并设过期时间。
2.2 用户组权限与课程访问控制
会员中心不只是登录注册,真正影响业务的是权限。这套源码里用户组通常分管理员、讲师、普通学员、VIP 学员几类。课程访问控制一般写在课程控制器里,判断当前用户的group_id是否在课程允许的组列表内。
我见过不少翻车案例:开发者把权限判断只写在前端,后端接口没校验,结果普通学员直接改 URL 就能看付费课程。正确的做法是在application/home/controller/Course.php的detail方法里,先查课程表拿到allow_group字段,再跟当前 session 里的用户组比对。
public function detail($id) { $course = Db::name('course')->where('id', $id)->find(); if (!$course) { $this->error('课程不存在'); } // 获取当前用户组,未登录给一个游客组 $groupId = session('user_id') ? Db::name('user')->where('id', session('user_id'))->value('group_id') : 0; $allowGroups = explode(',', $course['allow_group']); if (!in_array($groupId, $allowGroups)) { $this->error('您没有权限学习该课程', url('user/vip')); } // 记录学习进度 Db::name('course_log')->insert([ 'user_id' => session('user_id'), 'course_id' => $id, 'create_time' => time() ]); $this->assign('course', $course); return $this->fetch(); }这里的allow_group字段存的是逗号分隔的组 ID,比如2,3,4。course_log表用来记学习轨迹,后面做学习时长统计和考试资格判断都靠它。参数上要注意:如果课程是免费的,allow_group可以留空或者写0,代码里加一个if (empty($course['allow_group']))直接放行。
3. PC 与 WAP 自适应:一套模板怎么同时伺候两端
3.1 自适应布局的模板组织方式
这套源码号称 PC 版和 WAP 手机自适应,实际落地时有两种常见做法:一种是纯 CSS 媒体查询做响应式,一套模板走天下;另一种是 ThinkPHP 的模板主题机制,根据 UA 判断加载view/pc/还是view/wap/目录。我拆过的版本里,多数用的是后者,因为教育类页面在手机上需要重新排布,纯响应式改起来反而更累。
模板主题切换一般写在公共控制器或行为扩展里。ThinkPHP 5.x 可以在application/common.php里加一个钩子,检测request()->isMobile()然后设置config('template.view_path')。
// 在公共初始化文件里判断设备类型 use think\facade\Config; use think\Request; $request = Request::instance(); if ($request->isMobile()) { // 手机端模板目录 Config::set('template.view_path', APP_PATH . 'home/view/wap/'); } else { Config::set('template.view_path', APP_PATH . 'home/view/pc/'); }这段逻辑的关键是isMobile()方法,ThinkPHP 内置了 UA 判断,能识别大部分手机浏览器。但要注意:iPad 和一些大屏手机可能被误判,我一般会再加一个屏幕宽度判断,或者让用户在页面底部手动切换 PC/WAP 版,把选择存 Cookie。
3.2 移动端支付与视频播放的适配坑
WAP 端最容易被忽略的是支付和视频播放。PC 端用支付宝当面付或者微信 Native 扫码,手机端必须换成 H5 支付或者 JSAPI。源码里如果只写了 PC 支付逻辑,手机端下单会直接报错。我一般会在支付控制器里根据设备类型走不同分支。
public function pay($orderId) { $order = Db::name('order')->where('id', $orderId)->find(); if (request()->isMobile()) { // 手机端走 H5 支付,返回跳转链接 $payUrl = $this->wxH5Pay($order); return json(['code' => 1, 'url' => $payUrl]); } else { // PC 端返回二维码 $qrCode = $this->wxNativePay($order); return json(['code' => 1, 'qrcode' => $qrCode]); } }视频播放这块,手机浏览器对video标签的自动播放限制很严,必须加playsinline和webkit-playsinline属性,而且不能默认自动播放,否则 iOS 上直接黑屏。另外 WAP 端视频地址最好走 HLS 切片,MP4 在弱网下卡顿明显。这些细节源码里不一定全有,需要自己补。
4. 在线教学与考试系统:题库、组卷和判分的实现细节
4.1 题库表结构与题目类型支持
考试系统是这套源码里技术含量最高的部分。题库表一般叫exam_question,字段包括id、type(题型)、content(题干)、options(选项,JSON 格式)、answer(正确答案)、analysis(解析)、score(分值)。题型通常支持单选、多选、判断、填空、简答。
我拆的时候会先看options字段怎么存。比较合理的做法是存 JSON,比如{"A":"选项一","B":"选项二","C":"选项三","D":"选项四"},判分时把用户提交的答案跟answer字段比对。多选题的answer存"A,B"这种格式,判分时先拆分再排序比对,避免顺序不同导致误判。
// 判分逻辑片段 public function checkAnswer($questionId, $userAnswer) { $question = Db::name('exam_question')->where('id', $questionId)->find(); $correct = $question['answer']; if ($question['type'] == 2) { // 多选题 $userArr = explode(',', $userAnswer); $correctArr = explode(',', $correct); sort($userArr); sort($correctArr); return $userArr == $correctArr ? $question['score'] : 0; } elseif ($question['type'] == 3) { // 判断题 return $userAnswer == $correct ? $question['score'] : 0; } else { // 单选和填空 return trim($userAnswer) == trim($correct) ? $question['score'] : 0; } }参数上要注意:填空题最好支持多个可接受答案,用|分隔,判分时逐个比对。简答题没法自动判分,一般标记为“待人工阅卷”,在后台给讲师一个批改入口。
4.2 组卷策略与考试防作弊
组卷有两种模式:固定试卷和随机抽题。固定试卷直接从exam_paper表读题目 ID 列表;随机抽题按题型和知识点从题库里order by rand()抽,但数据量大时rand()性能很差,我一般改成先查 ID 范围再随机取。
// 随机抽题,避免 order by rand() public function randomQuestions($type, $num) { $maxId = Db::name('exam_question')->where('type', $type)->max('id'); $minId = Db::name('exam_question')->where('type', $type)->min('id'); $ids = []; while (count($ids) < $num) { $randId = mt_rand($minId, $maxId); $exist = Db::name('exam_question')->where('id', $randId)->where('type', $type)->find(); if ($exist && !in_array($randId, $ids)) { $ids[] = $randId; } } return Db::name('exam_question')->whereIn('id', $ids)->select(); }防作弊方面,源码里一般有切屏检测和限时交卷。切屏检测用 JS 监听visibilitychange事件,切出去超过三次自动交卷。限时交卷在服务端记录开考时间,交卷时比对time() - start_time是否超过考试时长。注意服务端一定要校验,前端计时器可以被篡改。
5. 避坑与常见问题排查:部署和二次开发中容易翻车的地方
5.1 环境配置与伪静态规则
现象:部署后首页能打开,但课程详情页 404。原因:ThinkPHP 需要伪静态规则,Nginx 下没配try_files。解决:在站点配置里加location / { if (!-e $request_filename) { rewrite ^(.*)$ /index.php?s=$1 last; } },Apache 则确认.htaccess文件存在且AllowOverride开启。
5.2 数据库编码与中文乱码
现象:后台添加课程标题,前台显示问号。原因:数据库、表、字段的字符集不是utf8mb4,或者连接配置里charset写成了utf8。解决:建库时指定CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci,ThinkPHP 的database.php里charset改成utf8mb4,同时检查config里有没有params覆盖。
5.3 考试提交后分数为 0
现象:学员答完题提交,系统显示 0 分。原因:判分时用户答案和正确答案的类型不一致,比如一个是字符串"A",一个是数组["A"]。解决:在checkAnswer里统一做类型转换,$userAnswer = is_array($userAnswer) ? implode(',', $userAnswer) : $userAnswer;,再跟answer字段比对。
5.4 手机端上传图片失败
现象:WAP 端上传课程封面,提示“上传失败”。原因:手机浏览器拍照上传的图片格式可能是heic,而源码里白名单只写了jpg,png,gif。解决:在上传配置里加heic和webp,或者前端用 canvas 压缩转成 jpg 再上传。另外检查php.ini的upload_max_filesize和post_max_size是否够用。
5.5 会员到期后仍能看课程
现象:VIP 会员过期了,但还能访问付费课程。原因:权限判断只看了group_id,没查vip_expire_time。解决:在课程详情页的权限校验里加一层过期判断,if ($user['group_id'] == 3 && $user['vip_expire_time'] < time()) { $this->error('会员已过期'); },同时后台加一个定时任务,每天凌晨把过期会员的group_id降回普通组。
6. 二次开发进阶:把考试系统接进微信小程序和数据分析
这套源码的考试系统如果只跑在 PC 和 WAP 上,其实浪费了它的接口化设计。我一般会先把考试相关的控制器改成 API 模式,返回 JSON 而不是渲染模板,这样小程序端可以直接复用同一套判分逻辑。具体做法是在application/api/controller/Exam.php里重写一套方法,调用相同的模型层。
// API 模式下的交卷接口 public function submit() { $data = input('post.'); $paperId = $data['paper_id']; $answers = json_decode($data['answers'], true); // 前端传 JSON 字符串 $totalScore = 0; foreach ($answers as $qid => $ans) { $totalScore += $this->checkAnswer($qid, $ans); } // 写入考试记录 Db::name('exam_record')->insert([ 'user_id' => session('user_id'), 'paper_id' => $paperId, 'score' => $totalScore, 'create_time' => time() ]); return json(['code' => 1, 'score' => $totalScore]); }参数上要注意:小程序端没有 session,得用 token 换用户 ID,我一般会在中间件里统一处理。另外answers字段建议用 JSON 字符串传,避免表单数组在跨端时格式不一致。
数据分析这块,考试记录表exam_record积累多了之后,可以按课程维度统计平均分、及格率、错题率。我习惯写一个定时脚本,每天凌晨跑一次,把统计结果写进exam_stat表,后台直接读这张表出报表,避免每次实时查大表。
-- 统计每份试卷的平均分和及格率 INSERT INTO exam_stat (paper_id, avg_score, pass_rate, stat_date) SELECT paper_id, AVG(score) AS avg_score, SUM(CASE WHEN score >= 60 THEN 1 ELSE 0 END) / COUNT(*) AS pass_rate, CURDATE() FROM exam_record WHERE create_time >= UNIX_TIMESTAMP(CURDATE() - INTERVAL 1 DAY) GROUP BY paper_id;这条 SQL 每天跑一次,把前一天的数据汇总。注意pass_rate算出来是小数,前端展示时乘 100 加百分号。如果考试满分不是 100,及格线要按比例算,别写死 60。
从那以后我每次拿到一套教育类源码,都强制先跑一遍“注册—选课—考试—交卷”这条完整链路,再去看后台的统计和权限。很多问题只有走完全流程才会暴露,光看代码是看不出来的。希望这套拆解能帮到你,少走点我踩过的弯路。
本文还有配套的精品资源,点击获取