ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

基于ThinkPHP的教学管理系统实战:排课选课成绩录入与权限控制

基于ThinkPHP的教学管理系统实战:排课选课成绩录入与权限控制

简介:这是一套基于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建表脚本与phpt测试用例,结构完整便于按模块查阅。资源围绕教师、学生、课程、成绩等实体展开,涉及数据库表设计、路由配置、控制器与模型交互、视图模板引擎、RBAC权限管理、RESTful API接口、异常处理与日志记录、SQL注入与XSS防护等知识点,适合对照源码梳理Web开发流程。目前已有1581人学习下载,可作为教学管理类项目的入门实践与二次开发基础。

1. 从一张排课表说起:这套教学管理系统到底要解决什么

每年开学前两周,教务办公室里最常听到的一句话是「这个班的课又和那个老师的时间撞了」。排课、调课、统计课时、导出成绩,这些事单看都不复杂,可一旦落到几百个学生、几十位老师身上,靠 Excel 来回传就会变成一场灾难——版本对不上、数据被覆盖、谁改了哪一行没人知道。这套基于 ThinkPHP 开发的教学管理系统,要解决的就是这类「数据分散、流程靠人盯」的问题。它把学生、教师、课程、成绩、排课这几条主线收进一个后台,用统一的权限和数据结构管起来,适合正在做课程设计的学生,也适合需要一套能直接改的中小型教务系统的开发者。选 ThinkPHP 而不是从零手写框架,理由很实在:MVC 分层清晰、ORM 和验证器开箱即用,能把精力放在业务逻辑上,而不是重复造轮子。

2. 需求拆解与技术选型:为什么是 ThinkPHP 而不是别的

2.1 先把角色和用例理清楚

教学管理系统的第一件事不是写代码,而是把「谁用、用来干什么」列清楚。这个项目里通常有三类角色,权限边界必须一开始就定死,否则后期加权限会非常痛苦。

角色核心用例数据权限范围
管理员用户管理、课程管理、排课、数据统计全部数据
教师查看自己的课表、录入成绩、查看选课名单仅本人授课数据
学生选课、查课表、查成绩仅本人数据

我一般会建议把「管理员」和「教务人员」再拆一层,因为实际场景里录成绩和管账号往往不是同一个人。但课程设计阶段,三类角色足够把权限模型讲清楚。用例理清之后,数据库表基本就浮出来了:用户表、角色表、课程表、选课表、成绩表、课表表,六张核心表能撑起整个系统。

2.2 技术选型:ThinkPHP 的取舍

选 ThinkPHP 的核心原因是它对中小型项目足够友好。相比手写原生 PHP,它省掉了路由分发、数据库连接、模板渲染这些重复劳动;相比一些重型框架,它的学习曲线又足够平缓,一个熟悉 PHP 基础的人两三天就能上手写业务。

具体到版本,通常的做法是选 ThinkPHP 6.x,配合 Composer 管理依赖,数据库用 MySQL 5.7 或 8.0,前端用原生 HTML + Bootstrap 或者简单的 Vue 页面即可。这里有个血泪经验:不要一上来就上前后端分离,课程设计的时间预算撑不住。先用服务端渲染把功能跑通,把业务逻辑验证对了,再考虑要不要拆前端。

# 用 Composer 创建 ThinkPHP 6 项目 composer create-project topthink/think tp-edu # 进入项目目录,配置数据库连接 cd tp-edu # 编辑 .env 文件,填入数据库信息

.env里的数据库配置是第一个容易翻车的地方,很多人直接改config/database.php,结果换环境时忘了改回来。用.env隔离环境配置是更稳妥的做法。

# .env 数据库配置示例 [DATABASE] TYPE = mysql HOSTNAME = 127.0.0.1 DATABASE = edu_system USERNAME = root PASSWORD = your_password HOSTPORT = 3306 CHARSET = utf8mb4 DEBUG = true

CHARSET一定要用utf8mb4,否则学生姓名里的生僻字会变成问号,这个坑我在不止一个项目里见过。DEBUG在开发阶段开true方便看报错,上线前记得关掉。

2.3 目录结构怎么规划

ThinkPHP 默认是单应用模式,但教学管理系统模块多,建议用多应用模式,把 admin、teacher、student 拆成三个应用,各自独立又共享模型层。

# 多应用模式目录结构 app/ ├── admin/ # 管理员端 │ ├── controller/ │ ├── model/ │ └── view/ ├── teacher/ # 教师端 ├── student/ # 学生端 ├── common/ # 公共模型和工具 │ ├── model/ │ └── validate/ └── middleware/ # 权限中间件

把模型放在common/model下,三个应用共用,避免同一个User模型写三遍。权限校验用中间件统一处理,不要在每个控制器里重复写if ($role != 'admin'),那样后期改权限规则会改到崩溃。

3. 核心模块实现:从登录鉴权到成绩录入

3.1 登录与权限中间件

登录本身不难,难的是登录之后每个请求怎么判断「这个人能不能干这件事」。我的做法是登录成功后把用户 ID 和角色写入 Session,然后用一个中间件在请求进入控制器前统一校验。

<?php // app/middleware/AuthCheck.php namespace app\middleware; use think\facade\Session; use think\Response; class AuthCheck { // 定义各角色允许访问的应用 protected $roleMap = [ 'admin' => 'admin', 'teacher' => 'teacher', 'student' => 'student', ]; public function handle($request, \Closure $next) { $userId = Session::get('user_id'); $role = Session::get('role'); // 未登录直接跳登录页 if (!$userId) { return redirect('/login'); } // 当前访问的应用名 $appName = app('http')->getName(); // 角色与访问应用不匹配则拒绝 if (isset($this->roleMap[$role]) && $this->roleMap[$role] !== $appName) { return Response::create('无权限访问', 'html', 403); } return $next($request); } }

这段中间件的关键在$roleMap,它把「角色」和「能进哪个应用」绑定在一起。管理员只能进 admin 应用,学生只能进 student 应用,跨应用访问直接返回 403。这样做的好处是权限规则集中在一处,加角色或改规则只动这个数组。注册中间件时在app/middleware.php里全局绑定,或者在每个应用的middleware.php里单独绑定,后者更灵活。

3.2 课程与选课模块

选课是教学系统里并发最容易出问题的地方。一门课限选 50 人,如果 100 个人同时点「选课」,不做控制就会出现超选。ThinkPHP 里可以用数据库事务加行锁来解决。

<?php // app/common/model/Course.php 中的选课方法 public function selectCourse($courseId, $studentId) { // 开启事务 $this->startTrans(); try { // 加锁查询课程,防止并发超选 $course = $this->lock(true)->find($courseId); if (!$course) { throw new \Exception('课程不存在'); } // 已选人数达到上限 if ($course->selected >= $course->capacity) { throw new \Exception('该课程已选满'); } // 检查是否重复选课 $exists = \app\common\model\CourseSelect::where([ 'course_id' => $courseId, 'student_id' => $studentId, ])->find(); if ($exists) { throw new \Exception('你已选过这门课'); } // 写入选课记录并更新已选人数 \app\common\model\CourseSelect::create([ 'course_id' => $courseId, 'student_id' => $studentId, 'create_time' => time(), ]); $course->selected = $course->selected + 1; $course->save(); $this->commit(); return ['code' => 0, 'msg' => '选课成功']; } catch (\Exception $e) { $this->rollback(); return ['code' => 1, 'msg' => $e->getMessage()]; } }

lock(true)是关键,它会在查询时加排他锁,保证同一时刻只有一个请求能读到课程余量。事务保证「写选课记录」和「更新人数」要么都成功要么都回滚。这里有个细节:selected字段建议用整型而不是每次count()统计,前者性能好得多,代价是要保证计数准确,所以必须放在同一个事务里。

3.3 成绩录入与统计

成绩模块的难点不在录入,而在「谁能录、录完能不能改、改了留不留痕」。我的做法是成绩表加一个status字段,区分「草稿」和「已提交」,提交后教师不能直接改,要走管理员审核。

-- 成绩表结构 CREATE TABLE `score` ( `id` int NOT NULL AUTO_INCREMENT, `course_id` int NOT NULL COMMENT '课程ID', `student_id` int NOT NULL COMMENT '学生ID', `teacher_id` int NOT NULL COMMENT '录入教师ID', `score` decimal(5,2) DEFAULT NULL COMMENT '成绩', `status` tinyint DEFAULT 0 COMMENT '0草稿 1已提交', `update_time` int DEFAULT NULL, PRIMARY KEY (`id`), UNIQUE KEY `uk_course_student` (`course_id`,`student_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

uk_course_student这个唯一索引很重要,它从数据库层面杜绝了同一个学生同一门课出现两条成绩记录。很多系统只在代码里判断,一旦并发或代码有漏洞就会出脏数据,加个唯一索引是最后一道防线。统计功能用 ThinkPHP 的聚合查询就能搞定,比如算某门课的平均分、及格率:

// 统计某课程成绩 $stats = \app\common\model\Score::where('course_id', $courseId) ->where('status', 1) ->field('AVG(score) as avg_score, MAX(score) as max_score, MIN(score) as min_score, COUNT(*) as total') ->find(); // 及格率单独算 $passCount = \app\common\model\Score::where('course_id', $courseId) ->where('status', 1) ->where('score', '>=', 60) ->count(); $passRate = $stats['total'] > 0 ? round($passCount / $stats['total'] * 100, 2) : 0;

把统计逻辑放在模型里而不是控制器里,是为了复用——教师端要看,管理员端也要看,写两遍迟早不一致。

4. 避坑与常见问题:这几个地方最容易翻车

4.1 中文乱码:从数据库到页面全链路排查

现象是学生姓名显示成问号或方块。原因通常出在三个环节之一:数据库字符集不是utf8mb4、连接配置里charset写错、页面没声明 UTF-8。解决顺序是从下往上查:先确认建库时用了utf8mb4,再检查.env里的CHARSET,最后看模板的<meta charset="utf-8">。三处都对了基本就不会乱码。

4.2 权限越权:学生能访问教师接口

现象是学生登录后手动改 URL 就能进教师页面。原因是只在菜单里做了隐藏,没在服务端校验。解决办法就是前面说的中间件,所有权限判断必须在服务端做,前端隐藏只是体验优化,不是安全措施。我一般会在中间件里再加一层「操作级」校验,比如教师只能改自己授的课的成绩,这个判断要落到具体业务方法里。

4.3 选课超选:并发下人数超过上限

现象是限选 50 人的课最后选了 53 人。原因是「查余量」和「写记录」之间有时间差,两个请求同时读到 49 就都放行了。解决办法是用事务加行锁,把这两步包在一个原子操作里,就是 3.2 节那段代码。如果并发量特别大,还可以用 Redis 预扣减,但课程设计阶段事务加锁足够了。

4.4 成绩重复录入:同一学生同一课两条记录

现象是统计平均分时数值偏高。原因是录入时没做唯一性校验,或者校验了但并发下失效。解决办法是数据库加唯一索引uk_course_student,代码里再用saveOrUpdate逻辑。数据库约束是兜底,代码校验是体验,两个都要有。

4.5 部署后 500:开发环境正常线上报错

现象是本地跑得好好的,传到服务器就白屏。原因通常是.env没配、目录权限不对、或者 PHP 扩展缺失(比如pdo_mysql)。排查方法是先看服务器错误日志,再确认runtime目录可写,最后用php -m检查扩展。上线前把DEBUG关掉,否则报错信息会暴露数据库账号。

5. 验收与答辩:怎么证明这套系统真的能用

验收不是把功能点念一遍,而是要让评审相信「这套系统在真实场景下不会崩」。我一般会准备三样东西:一份功能清单、一组测试数据、一段演示脚本。

功能清单按角色列,每个角色三到五个核心用例,逐条演示。测试数据要提前造好,别现场录,尤其是成绩和选课这种依赖数据的模块,空库演示会非常尴尬。演示脚本写清楚每一步点什么、预期看到什么,避免现场手忙脚乱。

验收项验收标准验证方式
登录鉴权三种角色只能进各自应用用学生账号访问 admin 路径,应返回 403
选课并发限选人数不被突破用脚本模拟 100 并发选 50 人的课,最终人数为 50
成绩唯一同一学生同一课只有一条成绩重复提交,数据库应只有一条记录
统计准确平均分、及格率与手工计算一致造 10 条已知成绩,核对统计结果
中文显示姓名、课程名无乱码录入含生僻字的姓名,页面正常显示

答辩时最容易被问的是「并发怎么处理的」「权限怎么保证的」「数据一致性怎么做的」,这三个问题对应 3.1、3.2、3.3 三节,提前把代码和思路理清楚。如果评审让你现场改一个需求,比如「加一个按班级筛选成绩的功能」,别慌,先想清楚改哪张表、哪个模型、哪个控制器,说清楚思路比写出来更重要。

从那以后我每次交项目前,都会强制自己走一遍「换一台干净机器部署」的流程,因为本地环境和服务器环境的差异,永远是答辩现场最大的黑匣子。把部署文档写清楚,把.env.example留好,比多写两个花哨功能有用得多。希望这套结构能帮到你,照着搭一遍,课程设计也好,实际交付也好,都能拿得出手。

本文还有配套的精品资源,点击获取

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进