简介这是一套基于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-7section是第几节1-5week是周次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 classform-group label手机号/label input typetext namephone value{$info.phone|default} classform-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」这个步骤。看起来是小事但新手很容易在这里翻车。希望帮到你。本文还有配套的精品资源点击获取