简介:这是一套基于ThinkPHP框架搭建的简单教学管理系统源码,面向PHP初学者、Web开发入门者及需要课程设计参考的学生,帮助理解MVC架构下的教务管理实现。压缩包共2000个文件,约11.04MB,以1105个php文件为核心业务代码,384个html与197个js构成前端页面与交互,另有192个png、187个gif等图片资源,以及60个md说明文档、54个css样式、49个xml与48个yml配置、41个json数据文件,并包含少量sql、tpl模板与测试相关文件,结构完整便于按模块查阅。系统覆盖教师、学生、课程、成绩等实体设计,涉及路由配置、控制器与模型交互、模板引擎、RBAC权限管理、RESTful API接口、异常处理与日志记录、SQL注入及XSS防护等知识点。目前已有1581人学习下载,适合通过阅读源码掌握ThinkPHP项目组织方式,并在此基础上进行二次开发与功能扩展。
1. 从一份教务排课表说起:这个教学管理系统到底能干什么
每学期开学前两周,教务老师最头疼的就是排课。课程、教师、班级、教室四个维度互相拉扯,用 Excel 手工排,改一处动全身。我拿到这份基于 ThinkPHP 开发的教学管理系统源码时,第一反应就是看它怎么处理这个场景。它把学生、教师、课程、班级、成绩、排课这几块业务串成了一条完整链路,后台用 ThinkPHP 的 MVC 分层组织,前台是常规的服务端渲染页面。适合谁用?一是想拿一个真实业务练手 ThinkPHP 的开发者,二是学校或培训机构里需要一套轻量教务工具的技术人员。它不追求大而全,胜在结构清晰、业务闭环完整,拿来二次开发的门槛很低。
2. 技术选型与目录结构:ThinkPHP 的 MVC 是怎么落地的
2.1 为什么是 ThinkPHP 而不是别的框架
教学管理系统这类项目有个特点:业务逻辑不复杂,但表单多、CRUD 密集、权限层级分明。用 ThinkPHP 来做,最直接的好处是它的模型层和验证器能省掉大量重复代码。比如学生信息的增删改查,用 ThinkPHP 的模型关联和自动验证,一个控制器方法加一个模型定义就能跑通,不用像裸写 PHP 那样每个字段都手动过滤。
从项目结构看,它走的是 ThinkPHP 标准的单应用模式(不是多应用),入口文件在public/index.php,应用目录application/下按模块划分控制器和模型。这种结构对新手友好,因为路由规则简单,模块/控制器/方法三段式访问,调试的时候直接改 URL 就能定位到代码。
常见做法是把学生、教师、课程分别放在不同的控制器里,比如StudentController、TeacherController、CourseController,每个控制器负责自己那块业务的增删改查和列表展示。模型层则对应StudentModel、TeacherModel等,通过belongsTo和hasMany建立关联。比如查一个学生的成绩,通过StudentModel关联ScoreModel,再关联到CourseModel,一条链就把数据取出来了。
2.2 目录结构逐个拆开看
拿到源码后,我习惯先扫一遍目录,确认哪些是框架自带的,哪些是业务代码。这份项目的目录大致是这样:
application/ ├── admin/ # 后台模块 │ ├── controller/ # 控制器 │ │ ├── IndexController.php │ │ ├── StudentController.php │ │ ├── TeacherController.php │ │ ├── CourseController.php │ │ └── ScoreController.php │ ├── model/ # 模型 │ │ ├── StudentModel.php │ │ ├── TeacherModel.php │ │ ├── CourseModel.php │ │ └── ScoreModel.php │ └── view/ # 模板 │ ├── student/ │ ├── teacher/ │ └── course/ ├── index/ # 前台模块(学生/教师登录后看到的页面) │ ├── controller/ │ ├── model/ │ └── view/ ├── common/ # 公共模块 │ ├── controller/ │ └── model/ config/ # 配置文件 public/ # 入口文件和静态资源 route/ # 路由定义admin模块是后台管理端,教务人员在这里维护基础数据。index模块是前台,学生和教师登录后查看自己的课表、成绩。common放一些公共的基类控制器和工具方法,比如权限验证、分页封装。
这里有个细节值得注意:ThinkPHP 的模块划分不是强制按角色分的,但这份项目按admin和index拆开,说明它在设计时就把管理端和用户端做了隔离。好处是权限控制更清晰,后台的路由可以统一加中间件做登录校验,前台则走另一套逻辑。
2.3 数据库表设计的关键字段
教学管理系统的核心表就那么几张,但字段设计直接影响后续查询效率。我看了下它的表结构,几个关键点:
学生表student里除了基本的姓名、学号、性别,还有一个class_id字段关联班级表。教师表teacher有title(职称)和department(所属院系)。课程表course有credit(学分)、hours(学时)、teacher_id关联授课教师。成绩表score是典型的中间表,存student_id、course_id、score三个核心字段,外加学期标识。
排课表schedule的设计稍微复杂一点,它需要同时存class_id、course_id、teacher_id、classroom_id、weekday、section(第几节)。这几个字段组合起来才能唯一确定一节课的时间和地点。
提示:如果你要二次开发,建议先在
schedule表上加一个联合唯一索引,防止同一时间同一教室被排两门课。这个坑我在测试的时候踩过,排课冲突不会报错,但数据会乱。
3. 核心模块拆解:从登录鉴权到成绩录入的完整链路
3.1 登录鉴权:Session 还是 Token
这份项目用的是 ThinkPHP 自带的 Session 机制做登录态保持。后台管理员登录后,用户信息写入 Session,后续请求通过一个基类控制器BaseController的_initialize()方法做校验。如果 Session 里没有用户信息,直接跳转到登录页。
// application/admin/controller/BaseController.php namespace app\admin\controller; use think\Controller; use think\Session; class BaseController extends Controller { protected function _initialize() { parent::_initialize(); // 检查 Session 中是否存在管理员信息 $adminInfo = Session::get('admin_info'); if (empty($adminInfo)) { // 未登录则跳转到登录页 $this->redirect('admin/index/login'); exit; } // 将管理员信息分配到模板,方便视图层使用 $this->assign('admin_info', $adminInfo); } }这段代码的逻辑很直白:所有后台控制器继承BaseController,初始化时自动检查登录态。Session::get('admin_info')取不到就跳登录页。参数方面,admin_info是一个数组,存了管理员的id、username、role等字段。
这种做法的优点是简单,不用额外配 JWT 或 OAuth。缺点是 Session 依赖服务器存储,如果要做分布式部署,得把 Session 换成 Redis 或数据库驱动。ThinkPHP 的配置文件config/session.php里可以改type参数,改成redis就行。
3.2 学生管理:一个典型的 CRUD 控制器
学生管理模块是理解整个项目代码风格的入口。它的控制器方法基本遵循「列表 → 添加 → 编辑 → 删除」四件套。
// application/admin/controller/StudentController.php namespace app\admin\controller; use app\admin\model\StudentModel; use think\Request; class StudentController extends BaseController { // 学生列表,支持按姓名和学号搜索 public function index() { $keyword = input('keyword', ''); $where = []; if ($keyword) { // 模糊搜索姓名或学号 $where['name|student_no'] = ['like', '%' . $keyword . '%']; } $list = StudentModel::where($where) ->order('id', 'desc') ->paginate(10); // 每页10条 $this->assign('list', $list); $this->assign('keyword', $keyword); return $this->fetch(); } // 添加学生 public function add() { if (request()->isPost()) { $data = input('post.'); // 验证必填字段 $validate = validate('Student'); if (!$validate->check($data)) { return json(['code' => 0, 'msg' => $validate->getError()]); } $result = StudentModel::create($data); if ($result) { return json(['code' => 1, 'msg' => '添加成功']); } return json(['code' => 0, 'msg' => '添加失败']); } // 获取班级列表供下拉选择 $classList = \app\admin\model\ClassModel::all(); $this->assign('classList', $classList); return $this->fetch(); } }index()方法里用input('keyword', '')取搜索关键词,默认空字符串。$where['name|student_no']是 ThinkPHP 的查询语法,表示name或student_no满足条件即可,like做模糊匹配。paginate(10)自动生成分页,模板里用{$list->render()}输出分页链接。
add()方法分两步:POST 请求时处理表单提交,GET 请求时渲染添加页面。验证器validate('Student')对应application/admin/validate/Student.php文件,里面定义规则,比如name必填、student_no唯一。这种把验证逻辑抽到独立文件的做法,比在控制器里写一堆if要清爽得多。
3.3 成绩录入:批量提交与数据校验
成绩录入是教学管理系统里交互最重的模块。一个班几十个学生,不可能一条条提交。这份项目用的是表格批量提交,前端把每行数据组装成数组,后端循环写入。
// application/admin/controller/ScoreController.php public function batchAdd() { if (request()->isPost()) { $data = input('post.'); $courseId = $data['course_id']; $scores = $data['scores']; // 格式:[学生ID => 分数] $insertData = []; foreach ($scores as $studentId => $score) { // 跳过未填写的分数 if ($score === '') { continue; } // 校验分数范围 if (!is_numeric($score) || $score < 0 || $score > 100) { return json(['code' => 0, 'msg' => '分数必须在0-100之间']); } $insertData[] = [ 'student_id' => $studentId, 'course_id' => $courseId, 'score' => $score, 'create_time'=> time() ]; } if (empty($insertData)) { return json(['code' => 0, 'msg' => '没有需要保存的成绩']); } // 批量插入 $model = new \app\admin\model\ScoreModel(); $result = $model->saveAll($insertData); if ($result) { return json(['code' => 1, 'msg' => '保存成功']); } return json(['code' => 0, 'msg' => '保存失败']); } }$scores是从前端传过来的关联数组,键是学生 ID,值是分数。循环里先跳过空值,再做数值范围校验。saveAll()是 ThinkPHP 模型提供的批量插入方法,比在循环里逐条create()效率高。注意这里没有做重复提交的判断,如果同一门课重复录入,会插入多条记录。二次开发时建议加一个unique索引或者先删后插。
注意:批量插入时如果数据量很大(比如超过 1000 条),
saveAll()可能会因为 SQL 语句过长而失败。常见做法是分批处理,每 500 条调一次saveAll()。
3.4 排课模块:冲突检测的简单实现
排课是教学管理系统里逻辑最绕的一块。这份项目的排课逻辑不算复杂,但基本的冲突检测是有的。
// application/admin/controller/ScheduleController.php public function add() { if (request()->isPost()) { $data = input('post.'); // 检测同一教室同一时间是否已有课程 $conflict = \app\admin\model\ScheduleModel::where([ 'classroom_id' => $data['classroom_id'], 'weekday' => $data['weekday'], 'section' => $data['section'], 'week' => $data['week'] ])->find(); if ($conflict) { return json(['code' => 0, 'msg' => '该教室在此时间段已有课程安排']); } // 检测同一教师同一时间是否已有课程 $teacherConflict = \app\admin\model\ScheduleModel::where([ 'teacher_id' => $data['teacher_id'], 'weekday' => $data['weekday'], 'section' => $data['section'], 'week' => $data['week'] ])->find(); if ($teacherConflict) { return json(['code' => 0, 'msg' => '该教师在此时间段已有课程安排']); } $result = \app\admin\model\ScheduleModel::create($data); if ($result) { return json(['code' => 1, 'msg' => '排课成功']); } return json(['code' => 0, 'msg' => '排课失败']); } }这段代码做了两层检测:先查教室冲突,再查教师冲突。查询条件是classroom_id + weekday + section + week四个字段组合。如果find()返回非空,说明有冲突,直接返回错误信息。
参数说明:weekday是星期几(1-7),section是第几节(1-5),week是周次(1-20)。这三个字段加上教室或教师 ID,就能唯一确定一个时间槽。
这个实现有个局限:它只检测了教室和教师冲突,没有检测班级冲突。也就是说,同一个班在同一时间可能被排了两门课。二次开发时可以把班级也加进检测逻辑,或者干脆做一个统一的冲突检测方法,把教室、教师、班级三个维度都覆盖。
4. 避坑与常见问题:跑起来之前先看这几条
4.1 数据库连接报错:配置文件没改
现象:访问首页直接报「数据库连接失败」,或者页面空白但日志里有 SQL 错误。
原因:源码里的config/database.php默认用的是开发环境的数据库账号密码,跟你本地的 MySQL 不一致。
解决:打开config/database.php,把hostname、database、username、password改成你本地的配置。如果用的是 MySQL 8.0,还要注意charset设成utf8mb4,否则中文会乱码。
4.2 后台登录后跳回登录页:Session 目录没权限
现象:输入正确的账号密码,点击登录后页面闪一下又回到登录页,没有任何报错。
原因:ThinkPHP 的 Session 默认存在runtime/session/目录下,如果这个目录没有写权限,Session 写不进去,登录态就保持不住。
解决:给runtime/目录加写权限。Linux 下执行chmod -R 777 runtime/,Windows 下检查目录是否被只读属性锁定。另外确认config/session.php里的path配置指向的是可写目录。
4.3 排课冲突检测失效:字段类型不匹配
现象:明明已经排了课,再排同一时间同一教室,系统还是提示成功。
原因:weekday和section字段在数据库里是int类型,但前端传过来的是字符串。ThinkPHP 的查询构造器在某些版本里不会自动做类型转换,导致where条件匹配不上。
解决:在控制器里用intval()强制转换,或者把数据库字段改成varchar并在查询时统一格式。我一般会在input('post.')之后加一层数据过滤,把所有整型字段转一遍。
4.4 成绩批量保存超时:SQL 语句过长
现象:一个班 50 个学生,批量保存成绩时页面卡住,最后报 500 错误。
原因:saveAll()会把所有数据拼成一条INSERT INTO ... VALUES (...), (...), ...的 SQL。数据量大的时候,SQL 语句长度超过 MySQL 的max_allowed_packet限制。
解决:分批插入,每 100 条调一次saveAll()。或者改用insertAll()并手动控制批次大小。另外可以在config/database.php里把params的PDO::ATTR_EMULATE_PREPARES设为false,让 MySQL 用真正的预处理语句。
4.5 模板样式丢失:静态资源路径不对
现象:页面能打开,但 CSS 和 JS 全部 404,页面排版全乱。
原因:源码里的静态资源路径写的是绝对路径,比如/static/css/style.css,但你的部署环境可能把项目放在了子目录下。
解决:把模板里的静态资源路径改成__STATIC__或__PUBLIC__这种 ThinkPHP 的模板常量,它会自动解析成正确的 URL。或者直接在config/template.php里配置tpl_replace_string,做路径替换。
5. 二次开发上手:从改一个字段到加一个模块
5.1 先跑通再改:最小验证流程
拿到源码后,别急着改代码。先按这个顺序跑一遍:
第一步,建数据库,导入 SQL 文件。源码里一般会带一个.sql文件,用 Navicat 或命令行导入。导入后检查表结构是否完整,特别是student、teacher、course、score、schedule这几张核心表。
第二步,改config/database.php的数据库配置。确认hostname、database、username、password四项跟本地一致。
第三步,配虚拟主机或直接用 PHP 内置服务器。在项目根目录执行php -S localhost:8000 -t public/,然后浏览器访问http://localhost:8000。如果用的是 Apache 或 Nginx,把网站根目录指向public/目录。
第四步,登录后台。默认管理员账号一般在 SQL 文件里有注释,或者看admin表的第一条记录。登录后先别改数据,点一遍各个菜单,确认列表页、添加页、编辑页都能正常打开。
5.2 加一个字段:以学生表加「手机号」为例
假设要给学生的信息里加一个手机号字段,需要改三个地方:
数据库层面,给student表加一列:ALTER TABLE student ADD COLUMN phone VARCHAR(20) DEFAULT '' COMMENT '手机号';
模型层面,StudentModel.php里不需要额外改,ThinkPHP 的模型会自动映射表字段。但如果要加验证规则,得改application/admin/validate/Student.php:
// application/admin/validate/Student.php namespace app\admin\validate; use think\Validate; class Student extends Validate { protected $rule = [ 'name' => 'require|max:20', 'student_no' => 'require|unique:student', 'phone' => 'regex:/^1[3-9]\d{9}$/', // 手机号格式校验 ]; protected $message = [ 'name.require' => '姓名不能为空', 'student_no.unique' => '学号已存在', 'phone.regex' => '手机号格式不正确', ]; }视图层面,在application/admin/view/student/add.html和edit.html里加一个输入框:
<div class="form-group"> <label>手机号</label> <input type="text" name="phone" value="{$info.phone|default=''}" class="form-control" placeholder="请输入手机号"> </div>控制器不用改,因为input('post.')会自动接收所有表单字段,StudentModel::create($data)会自动写入新字段。这就是 ThinkPHP 模型层的好处,字段增减对控制器透明。
5.3 加一个模块:以「教室管理」为例
教室管理在原项目里可能没有独立模块,但排课表里用到了classroom_id。如果要加一个完整的教室管理模块,按这个步骤来:
先建表:CREATE TABLE classroom (id INT PRIMARY KEY AUTO_INCREMENT, name VARCHAR(50), capacity INT, location VARCHAR(100));
然后建控制器application/admin/controller/ClassroomController.php,继承BaseController,写index()、add()、edit()、delete()四个方法。代码结构跟StudentController一模一样,把模型换成ClassroomModel就行。
接着建模型application/admin/model/ClassroomModel.php,继承think\Model,定义$table属性指向classroom表。
最后建视图目录application/admin/view/classroom/,放index.html、add.html、edit.html。模板可以直接从student目录复制过来,改一下字段名和标题。
加完之后,在后台菜单里加一个链接指向/admin/classroom/index。菜单一般在application/admin/view/public/menu.html或者数据库的menu表里配置。
5.4 改造建议:几个值得动手的方向
第一个方向是权限细化。原项目的权限控制只到「登录/未登录」这一层,没有区分管理员和普通教师。可以加一个role字段,在BaseController里根据角色判断能访问哪些控制器。
第二个方向是接口化。如果要做前后端分离,可以把控制器里的return $this->fetch()改成return json($data),前端用 Vue 或 React 重写。ThinkPHP 做纯接口完全没问题,把视图层去掉就行。
第三个方向是排课算法优化。原项目的冲突检测是逐条查询,排一门课要查两次数据库。如果课程量大了,可以一次性把该时间段的所有排课记录查出来,在内存里做冲突判断,减少数据库压力。
| 改造方向 | 改动范围 | 难度 | 适合场景 |
|---|---|---|---|
| 权限细化 | BaseController + 用户表 | 低 | 多角色使用 |
| 接口化 | 控制器 + 前端重写 | 中 | 前后端分离 |
| 排课优化 | ScheduleController | 中 | 课程量大 |
| 成绩统计 | 新增统计控制器 | 低 | 教务报表 |
5.5 一个我踩过的坑
第一次跑这个项目的时候,我图省事,直接把源码解压到www根目录,然后访问http://localhost。结果首页能打开,但所有链接都跳到了http://localhost/admin/index/index,而实际文件在http://localhost/project/public/admin/index/index。折腾了半小时才反应过来,ThinkPHP 的入口文件在public/目录下,网站根目录必须指向public/,不能指向项目根目录。
从那以后我每次部署 ThinkPHP 项目,都强制走一遍「确认网站根目录指向 public」这个步骤。看起来是小事,但新手很容易在这里翻车。希望帮到你。
本文还有配套的精品资源,点击获取