简介这份资源是基于ThinkPHP框架开发的学生宿舍管理系统完整源码包面向高校计算机相关专业的毕业设计、课程设计学生以及需要PHP实战项目练手的开发者。系统采用B/S架构以PHP为后端语言、MySQL存储数据涵盖宿舍信息、学生信息、住宿分配、费用、维修与安全管理等模块并遵循MVC设计模式实现业务逻辑、数据访问与视图层的分离便于二次开发与维护。压缩包共366个文件约2.16MB其中180个php文件承载核心业务逻辑98个js与19个css、14个html文件构成前端交互与页面样式另含sql建表脚本、tpl模板及字体图标等静态资源目录结构清晰。目前已有47人学习下载。读者可据此获得一套可直接部署运行的宿舍管理方案理解ThinkPHP的控制器与模型组织方式、数据表设计思路以及分页、验证、安全加密等常用功能的实现路径为毕业设计选题与答辩提供完整参考。1. 基于ThinkPHP的学生宿舍管理系统从“能跑起来”到“敢交出去”的完整落地路径很多同学做毕设或课程设计拿到“基于ThinkPHP的学生宿舍管理系统”这个题目时第一反应是去搜一套现成源码下载解压改个数据库配置浏览器里能打开登录页就觉得大功告成。但真正到了答辩现场老师问一句“你这个分配算法怎么处理的”“晚归记录怎么和门禁数据对上”立刻卡壳。问题不在于ThinkPHP本身难而在于大多数人只做了“让它跑起来”这一步没做“让它经得起追问”这一步。这篇笔记就是把我自己从零搭这套系统、以及后来帮别人排查各种翻车现场的经验按可复现的顺序讲清楚。适合正在做宿舍管理系统毕设的本科生、需要快速交付一个内部管理工具的开发者以及想用ThinkPHP练手但不想只写CRUD的进阶学习者。核心思路是先想清楚数据模型和权限边界再动手写代码最后用真实场景压测一遍。2. 宿舍管理系统的数据模型与ThinkPHP选型为什么不是随便建几张表2.1 宿舍管理场景里绕不开的四类实体关系宿舍管理系统看起来简单无非是学生、宿舍、楼栋、管理员。但真正动手建表时你会发现实体之间的关系比想象中纠缠。一个学生只能住一个床位一个床位属于一个宿舍一个宿舍属于一个楼栋一个楼栋有多个管理员。这还没完学生可以申请调宿调宿会产生历史记录晚归记录要关联学生和具体时间报修单要关联宿舍和维修状态。如果一开始不把这些关系理清楚后面写查询时就会陷入无穷无尽的联表。我一般会先把核心实体画成三张主表加若干关联表。学生表存学号、姓名、性别、班级、联系方式、当前床位ID。宿舍表存宿舍号、楼栋ID、床位数、已住人数、性别限制。楼栋表存楼栋名、管理员ID、楼层数。然后单独建一张住宿记录表记录学生ID、床位ID、入住时间、退宿时间、状态。这样当前住宿关系通过状态字段筛选历史记录也保留下来了。调宿申请单独一张表关联原床位和新床位审批通过后更新住宿记录表并写一条调宿日志。提示不要在学生表里直接存宿舍号字符串后期调宿你会后悔。用床位ID做外键宿舍号通过联表查出来。2.2 ThinkPHP版本选择与项目初始化命令现在新项目我一般直接上ThinkPHP 6或8配合Composer管理依赖。ThinkPHP 5虽然资料多但PHP版本兼容性和安全更新已经跟不上新项目没必要再用。初始化命令如下composer create-project topthink/think dorm-system cd dorm-system composer require topthink/think-migration composer require topthink/think-validate第一行创建项目骨架第二行进入目录第三行安装迁移工具用来管理数据库表结构第四行安装验证器。迁移工具的好处是你不需要手动去数据库里建表所有表结构用PHP代码描述换一台机器执行迁移命令就能还原。验证器用来处理表单提交时的参数校验比如学号格式、手机号格式、床位是否已被占用。参数说明topthink/think-migration安装后在项目根目录会生成database/migrations文件夹每个迁移文件对应一次表结构变更。topthink/think-validate安装后在app/validate目录下创建验证器类。这两个包是后续所有功能的基础不要跳过。2.3 用迁移文件建表一次写对到处运行以学生表和宿舍表为例迁移文件里这样写?php use think\migration\Migrator; use think\migration\db\Column; class CreateStudentTable extends Migrator { public function change() { $table $this-table(student, [engine InnoDB, comment 学生信息表]); $table-addColumn(student_no, string, [limit 20, comment 学号]) -addColumn(name, string, [limit 50, comment 姓名]) -addColumn(gender, integer, [limit 1, default 1, comment 1男2女]) -addColumn(class_name, string, [limit 100, default , comment 班级]) -addColumn(phone, string, [limit 20, default , comment 手机号]) -addColumn(bed_id, integer, [default 0, comment 当前床位ID]) -addColumn(status, integer, [limit 1, default 1, comment 1在读2毕业3退学]) -addTimestamps(create_time, update_time) -addIndex([student_no], [unique true]) -create(); } }这段代码定义了一张学生表学号唯一索引防止重复录入床位ID默认0表示未分配状态字段区分在读和毕业。addTimestamps自动生成创建时间和更新时间字段ThinkPHP的模型会自动维护这两个字段不需要手动赋值。执行php think migrate:run后表就建好了。宿舍表类似但要多一个字段记录当前入住人数方便判断是否满员。楼栋表关联管理员ID。住宿记录表用student_id和bed_id做联合索引查询某个学生当前住哪个床位时直接走索引速度很快。3. 权限控制与核心业务逻辑别让宿管阿姨看到校长才能看的页面3.1 用中间件做角色隔离而不是在每个控制器里写if宿舍管理系统通常有三类角色超级管理员、楼栋管理员、学生。超级管理员能看所有楼栋数据楼栋管理员只能看自己管的楼栋学生只能看自己的住宿信息和提交申请。如果每个控制器方法里都写if ($role admin)代码会变得又臭又长而且容易漏判。我一般用ThinkPHP的中间件做统一拦截。先定义一个AuthMiddleware在app/middleware.php里注册全局中间件然后在中间件里根据当前登录用户的角色和请求的控制器/方法判断是否有权限。具体做法是维护一张权限映射表比如admin/*允许超级管理员building/*允许超级管理员和楼栋管理员student/*允许所有登录用户。中间件里解析当前请求的控制器和方法匹配映射表不匹配就返回403。?php namespace app\middleware; use think\facade\Session; use think\Response; class AuthMiddleware { protected $rules [ admin [super], building [super, building_admin], student [super, building_admin, student], ]; public function handle($request, \Closure $next) { $controller strtolower($request-controller()); $role Session::get(user.role); if (!$role) { return redirect(/login); } $allowed $this-rules[$controller] ?? []; if (!in_array($role, $allowed)) { return Response::create(无权限访问, html, 403); } return $next($request); } }这段中间件的逻辑是先取当前请求的控制器名转小写再取Session里的用户角色。如果没登录直接跳登录页。然后查权限映射表如果当前角色不在允许列表里就返回403。参数说明$this-rules数组的键是控制器名值是该控制器允许的角色列表。新增控制器时只需要在这里加一行不用改任何业务代码。3.2 床位分配的核心算法先到先得还是按班级集中床位分配是宿舍管理系统里最容易被老师追问的点。最简单的做法是先到先得学生登录后自己选空床位。但实际宿舍管理里学校通常希望同班级的学生集中住在一起方便管理。所以分配逻辑要分两步先按班级分组再在班级内部按提交申请的时间排序依次分配同楼栋同楼层的空床位。具体实现时我一般写一个BedAllocator类方法接收班级名称和楼栋ID返回分配结果。内部逻辑是查询该班级所有未分配床位的学生按申请时间升序排列查询该楼栋下所有空床位按楼层和宿舍号排序然后依次配对。如果该楼栋床位不够返回失败并提示剩余可选的楼栋。public function allocate($className, $buildingId) { $students Db::name(student) -where(class_name, $className) -where(bed_id, 0) -where(status, 1) -order(create_time asc) -select(); $beds Db::name(bed) -where(building_id, $buildingId) -where(status, 0) -order(floor asc, room_no asc, bed_no asc) -select(); if (count($beds) count($students)) { return [code 0, msg 床位不足剩余空床 . count($beds)]; } Db::startTrans(); try { foreach ($students as $index $student) { $bed $beds[$index]; Db::name(student)-where(id, $student[id])-update([bed_id $bed[id]]); Db::name(bed)-where(id, $bed[id])-update([status 1, student_id $student[id]]); Db::name(stay_record)-insert([ student_id $student[id], bed_id $bed[id], check_in date(Y-m-d H:i:s), status 1, ]); } Db::commit(); return [code 1, msg 分配成功]; } catch (\Exception $e) { Db::rollback(); return [code 0, msg 分配失败 . $e-getMessage()]; } }这段代码先查未分配学生和空床位按规则排序后一一配对。关键点在于用了数据库事务任何一步失败就回滚不会出现学生分配了但床位状态没更新的脏数据。参数说明$className是班级名称$buildingId是楼栋ID。返回数组里code为1表示成功0表示失败并带原因。实际部署时这个方法应该放在服务层控制器只负责接收参数和返回JSON。3.3 晚归记录与报修工单的状态流转晚归记录通常由宿管手动录入或者对接门禁系统自动导入。手动录入时表单需要选学生、选日期、填晚归时间、备注原因。保存时除了写晚归表还要更新学生的信用分字段。报修工单则是一个典型的状态机待处理、处理中、已完成、已取消。学生提交后是待处理维修人员接单后变处理中修完变已完成。每次状态变更都要写一条操作日志记录操作人和时间。状态流转用ThinkPHP的模型事件来触发日志写入而不是在每个控制器里手动写。在RepairOrder模型里定义onUpdate事件检测status字段是否变化变化了就插入一条日志。这样业务代码只需要更新状态日志自动生成。4. 避坑与排查那些让我熬夜到凌晨三点的翻车现场4.1 现象登录后跳转回登录页Session像失忆了一样原因ThinkPHP的Session默认存在文件里如果项目目录权限不对或者runtime目录被清空Session文件写不进去。另一个常见原因是中间件里用了redirect但没排除登录页本身导致死循环。解决先检查runtime/session目录是否存在且可写chmod -R 755 runtime。然后在中间件里加白名单登录页和验证码接口不拦截。如果还是不行把Session驱动改成Redis或数据库在config/session.php里改type为redis配置好连接信息。4.2 现象床位分配后学生列表里有人显示两个床位原因分配方法里更新了学生表的bed_id但没把旧的住宿记录状态改成已退宿。查询当前住宿时如果按学生ID查所有状态为1的记录就会查出两条。解决在分配逻辑里先查该学生是否有状态为1的住宿记录有的话先更新为状态2已退宿再插入新记录。或者查询当前住宿时用order(id desc)-find()只取最新一条。我一般两个都做数据干净查询也快。4.3 现象迁移文件执行报错“Table already exists”原因之前手动建过表或者迁移执行到一半失败了但迁移记录表里已经写入了版本号。ThinkPHP的迁移工具靠migrations表记录哪些迁移已执行如果表存在但记录没写就会冲突。解决先手动删掉冲突的表或者去migrations表里删掉对应的版本记录再重新执行php think migrate:run。更稳妥的做法是每次改表结构都新建一个迁移文件不要改旧的迁移文件因为旧的可能已经在别的环境执行过了。4.4 现象中文搜索宿舍号查不出结果原因数据库连接字符集不是utf8mb4或者ThinkPHP的数据库配置里charset写成了utf8。utf8在MySQL里最多存3字节遇到某些生僻字或emoji会截断导致查询匹配不上。解决在config/database.php里把charset改成utf8mb4同时检查MySQL建库语句CREATE DATABASE dorm DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci。已经建好的表用ALTER TABLE student CONVERT TO CHARACTER SET utf8mb4转换。4.5 现象导出Excel时内存溢出页面白屏原因一次性查了所有学生数据用PHPExcel或PhpSpreadsheet逐行写入数据量一大内存就爆了。宿舍管理系统虽然数据量不大但几千条记录加上关联查询很容易超过PHP默认的128M内存限制。解决用分批查询每次查500条写完后清空对象缓存。或者直接用ThinkPHP的chunk方法。导出格式如果不需要复杂样式用CSV比Excel省内存得多。在php.ini里把memory_limit临时调到512M也能应急但治标不治本。5. 进阶技巧用命令行脚本做数据初始化和批量导入5.1 写一个自定义命令一键生成测试数据开发阶段最烦的是手动录数据。我一般写一个ThinkPHP自定义命令放在app/command/InitData.php用php think init:data执行。命令里用循环生成楼栋、宿舍、床位、学生并随机分配一部分床位。这样每次重置数据库后一条命令就能恢复测试环境。?php namespace app\command; use think\console\Command; use think\console\Input; use think\console\Output; use think\facade\Db; class InitData extends Command { protected function configure() { $this-setName(init:data)-setDescription(生成测试数据); } protected function execute(Input $input, Output $output) { Db::name(building)-insert([name 1号楼, floor 6, admin_id 1]); $buildingId Db::name(building)-getLastInsID(); for ($floor 1; $floor 6; $floor) { for ($room 1; $room 20; $room) { $roomNo $floor . str_pad($room, 2, 0, STR_PAD_LEFT); Db::name(room)-insert([ building_id $buildingId, room_no $roomNo, floor $floor, capacity 4, ]); $roomId Db::name(room)-getLastInsID(); for ($bed 1; $bed 4; $bed) { Db::name(bed)-insert([ room_id $roomId, building_id $buildingId, bed_no $bed, status 0, ]); } } } $output-writeln(测试数据生成完毕); } }这段命令先生成一个楼栋然后循环生成6层每层20间宿舍每间4个床位。getLastInsID()拿到刚插入记录的ID用于关联下一层数据。执行后数据库里就有了480个床位足够测试分配逻辑。参数说明楼栋名和楼层数可以改成从命令行参数读取但测试数据没必要那么灵活写死更快。5.2 用事务和批量插入优化导入速度如果是从Excel导入真实学生数据不要一条一条insert。我一般把Excel解析成数组后用Db::name(student)-insertAll($list)批量插入每500条一批。插入前先用array_chunk切分。同时整个导入过程包在事务里失败就全部回滚避免导入一半留下脏数据。验证导入结果时不要只看“成功导入N条”要抽查几条数据确认班级、学号、性别字段没有错位。Excel列顺序和数据库字段顺序不一致是常见翻车点我习惯在解析时用表头映射而不是按列索引取值。5.3 最后说一个我自己的习惯每次改完核心逻辑比如床位分配或权限中间件我一定会用php think init:data重置数据然后手动走一遍完整流程登录、分配、调宿、报修、导出。这个习惯帮我拦住了至少三次“改A坏B”的事故。宿舍管理系统不难难的是细节之间的联动不出错。希望帮到你。本文还有配套的精品资源点击获取