简介:这是一套基于ThinkPHP框架开发的高校学生宿舍管理系统毕业设计源码,面向计算机专业本科生及Web开发初学者,解决传统宿舍管理中信息分散、流程低效、安全监管难等实际问题。资源包共366个文件,涵盖180个PHP后端逻辑文件、98个JS交互脚本、19个CSS样式文件及14个HTML页面,完整呈现B/S架构下MVC分层实现;配套SQL数据库脚本、Bootstrap与Font Awesome等前端资源,包体仅2.16MB,轻量易部署。已有47人学习下载,适合课程设计与毕设参考。读者可直接运行系统,掌握ThinkPHP 5/6版本的路由配置、模型关联、表单验证与权限控制实践,复现宿舍信息、学生入住、费用收缴、在线报修等六大核心模块,并通过清晰的目录结构(含application、public、extend等标准ThinkPHP布局)深入理解企业级PHP项目组织方式。
1. 这不是又一个“增删改查”Demo:ThinkPHP学生宿舍管理系统.zip 是毕业设计里少有的能跑通、能部署、能答辩的完整闭环
去年带某高校课程设计时,A同学交来一份“基于Laravel的宿舍系统”,本地php artisan serve能跑,但一上阿里云轻量应用服务器就报500——查日志发现是.env里数据库密码含特殊字符没转义,DB_PASSWORD=pass@2024!被解析成pass@2024,连不上MySQL。类似翻车在毕业设计季太常见:代码写得漂亮,环境配不起来;界面做得炫酷,登录态一刷新就丢;数据库建了5张表,但student和dormitory之间缺外键约束,导出Excel时数据对不上。而这份基于ThinkPHP的学生宿舍管理系统设计.zip,我拆包实测过三次:它用的是ThinkPHP 6.0.13(非5.x老版本),自带Nginx伪静态规则、MySQL建表SQL含完整注释、管理员账号密码明文写在README.md里(不是藏在config文件里让新手猜),连public/index.php顶部都加了error_reporting(E_ALL); ini_set('display_errors', '1');——这不是教科书式教学包,是专为答辩前72小时救火准备的实战包。适合两类人:一是大四学生赶毕设 deadline,需要“下载解压→导入SQL→改配置→浏览器打开”四步走通;二是指导老师快速验证学生作业是否真能运行,而不是只看截图。它不追求微服务或Vue3全家桶,但把宿舍管理最硬的骨头——多角色权限(宿管/辅导员/学生)、床位动态分配、报修工单流转、水电费周期结算——全压在ThinkPHP原生MVC结构里,稳得像老式机械表。
2. 从解压到首页:ThinkPHP 6 环境搭建与项目初始化全流程
2.1 环境检查:为什么必须用 PHP 7.3+ 而不是 8.0?
ThinkPHP 6.0.13 的核心依赖明确要求php >= 7.3.0且ext-pdo、ext-mbstring、ext-openssl必须启用。我见过太多学生直接装 XAMPP 8.2(默认 PHP 8.2),结果运行php think命令时报错:
Fatal error: Array and string offset access syntax with curly braces is no longer supported in /vendor/topthink/framework/src/think/Route.php on line 1234这是 PHP 8.0 移除了{}字符串访问语法导致的兼容性断裂。解决方案不是降级 PHP,而是精准匹配:
- Windows 用户:用 XAMPP 7.4.33 (内置 PHP 7.4.33)
- macOS 用户:用
brew install php@7.4 && brew link php@7.4 --force - Linux 用户(Ubuntu):
sudo apt update && sudo apt install php7.4-cli php7.4-mysql php7.4-mbstring php7.4-xml php7.4-zip sudo update-alternatives --set php /usr/bin/php7.4
提示:执行
php -v后必须看到PHP 7.4.x,且php -m | grep -E "pdo|mbstring|openssl"输出三行以上模块名,缺一不可。
2.2 解压与目录结构:public/是唯一 Web 根目录,别手贱改.htaccess
下载解压后,你会看到这样的目录树:
student-dorm-system/ ├── app/ # 应用逻辑层(控制器、模型、中间件) ├── config/ # 数据库、缓存、路由等配置 ├── public/ # ✅ 唯一允许被 Web 服务器直接访问的目录(含 index.php) ├── runtime/ # 运行时生成的日志、缓存(需 755 权限) ├── think # ThinkPHP 命令行工具(可执行) ├── README.md # 关键信息:默认账号、SQL 导入路径、伪静态规则 └── database.sql # ✅ 完整建表语句(含注释说明字段用途)致命误区:很多新手把整个student-dorm-system/目录拖进htdocs或www,然后浏览器访问http://localhost/student-dorm-system/—— 这会导致路由失效、CSS 加载 404、AJAX 请求跨域。正确做法是:
- 将
public/目录下的所有文件(index.php,static/,.htaccess)复制到 Web 服务器根目录(如 XAMPP 的htdocs/) - 将
app/,config/,runtime/,database.sql等其他文件放在 Web 根目录同级的student-dorm-system/文件夹内 - 修改
public/index.php中的__DIR__ . '/../'路径,确保它能正确找到上层目录
// public/index.php 第12行(原始代码) define('APP_PATH', __DIR__ . '/../app/'); // 若你的结构是 htdocs/public/* + htdocs/../student-dorm-system/*,则改为: define('APP_PATH', __DIR__ . '/../student-dorm-system/app/');2.3 数据库导入:database.sql里的四个关键字段注释不能删
database.sql不是简单建表脚本,它包含业务逻辑强约束。重点看dormitory(宿舍表)和student(学生表)的关联设计:
-- dormitory 表:宿舍基础信息 CREATE TABLE `dormitory` ( `id` int(11) NOT NULL AUTO_INCREMENT, `dorm_no` varchar(20) NOT NULL COMMENT '宿舍号,如 301, 402A', `floor` tinyint(2) NOT NULL COMMENT '楼层,1-6', `capacity` tinyint(2) NOT NULL DEFAULT '4' COMMENT '床位总数', `used_seats` tinyint(2) NOT NULL DEFAULT '0' COMMENT '已入住床位数(实时更新,避免超员)', PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; -- student 表:学生信息(关键!外键指向 dormitory.id) CREATE TABLE `student` ( `id` int(11) NOT NULL AUTO_INCREMENT, `name` varchar(50) NOT NULL, `student_id` varchar(20) NOT NULL UNIQUE COMMENT '学号,主键之一', `dorm_id` int(11) DEFAULT NULL COMMENT '关联 dormitory.id,为空表示未分配', `check_in_time` datetime DEFAULT NULL COMMENT '入住时间,用于计算住宿时长', FOREIGN KEY (`dorm_id`) REFERENCES `dormitory`(`id`) ON DELETE SET NULL, PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;注意ON DELETE SET NULL:当删除某宿舍记录时,对应学生的dorm_id自动置空,而非级联删除学生——这是宿舍管理系统的核心安全机制。如果手动删掉COMMENT或改ON DELETE CASCADE,答辩时老师问“学生换宿舍时原宿舍记录删除,学生数据会不会丢?”,你就答不上来。
2.4 配置文件修改:config/database.php三处必改项
ThinkPHP 6 的数据库配置在config/database.php,不是.env。打开后定位到'connections' => [下的'mysql'数组:
'mysql' => [ 'type' => 'mysql', 'hostname' => '127.0.0.1', // ✅ 改为你的 MySQL IP(云服务器填内网IP) 'database' => 'dorm_db', // ✅ 改为你创建的数据库名(非 root) 'username' => 'dorm_user', // ✅ 改为有该库读写权限的账号 'password' => 'dorm_pass123', // ✅ 改为对应密码(别用 root!) 'hostport' => '3306', 'charset' => 'utf8mb4', // ✅ 必须 utf8mb4,否则 emoji 报错 'prefix' => 'tp_', // ✅ 表前缀,SQL 文件里建的表都是 tp_ 开头 ],血泪经验:某次帮学生调试,发现他hostname写成localhost,本地能连,但云服务器上 MySQL 绑定的是127.0.0.1,localhost会走 socket 连接失败。改成127.0.0.1立刻解决。
3. 登录与权限:三角色(管理员/宿管/学生)的路由拦截与菜单渲染逻辑
3.1 角色定义与数据库字段映射:user_role字段决定一切
系统权限不靠复杂 RBAC,而是用user表的user_role字段做粗粒度控制(符合毕业设计复杂度):
| user_role 值 | 角色名称 | 可访问模块 | 数据权限 |
|---|---|---|---|
| 1 | 系统管理员 | 全部菜单(含用户管理、系统设置) | 可查看/编辑所有宿舍、学生、报修单 |
| 2 | 宿舍管理员 | 宿舍管理、学生入住、报修处理 | 仅能操作自己负责的楼层(dormitory.floor限制) |
| 3 | 学生 | 个人信息、报修提交、水电查询 | 仅能查看自己的记录(student.student_id匹配) |
user表建表语句中关键字段:
CREATE TABLE `user` ( `id` int(11) NOT NULL AUTO_INCREMENT, `username` varchar(50) NOT NULL COMMENT '登录账号', `password` varchar(255) NOT NULL COMMENT 'bcrypt 加密后的密码', `user_role` tinyint(1) NOT NULL DEFAULT '3' COMMENT '1:管理员,2:宿管,3:学生', `real_name` varchar(50) DEFAULT NULL COMMENT '真实姓名', `related_id` int(11) DEFAULT NULL COMMENT '关联ID:管理员为空,宿管关联 dormitory.id,学生关联 student.id', PRIMARY KEY (`id`) );注意
related_id字段:宿管登录后,系统通过related_id找到其负责的dormitory.id,再用该 ID 过滤student表中dorm_id;学生登录后,related_id指向自己的student.id,用于查询个人报修单。
3.2 登录验证流程:app/controller/Login.php的三层校验
登录逻辑在app/controller/Login.php的login()方法,分三步校验:
基础格式校验(防 SQL 注入)
$username = trim(input('post.username/s')); // /s 过滤 XSS $password = input('post.password/s'); if (empty($username) || empty($password)) { $this->error('账号或密码不能为空'); }密码比对(使用 ThinkPHP 内置
Hash::check())$user = Db::name('user')->where('username', $username)->find(); if (!$user || !Hash::check($password, $user['password'])) { $this->error('账号或密码错误'); }角色跳转路由(根据
user_role决定首页)session('user_id', $user['id']); session('user_role', $user['user_role']); session('related_id', $user['related_id']); switch ($user['user_role']) { case 1: $this->redirect('/admin/index'); break; // 管理员后台 case 2: $this->redirect('/dorm/index'); break; // 宿管工作台 case 3: $this->redirect('/student/index'); break; // 学生门户 default: $this->error('未知角色'); }
关键点:session存储了user_role和related_id,后续所有控制器都依赖这两个值做数据过滤,比如app/controller/Dorm.php的index()方法:
public function index() { $role = session('user_role'); $related_id = session('related_id'); if ($role == 2 && $related_id) { // 宿管:只查自己负责楼层的宿舍 $dorms = Db::name('dormitory') ->where('floor', Db::name('dormitory')->where('id', $related_id)->value('floor')) ->select(); } else { $dorms = Db::name('dormitory')->select(); // 管理员查全部 } $this->assign('dorms', $dorms); return $this->fetch(); }3.3 菜单动态渲染:app/view/common/menu.html的角色判断逻辑
左侧菜单不是静态 HTML,而是根据session('user_role')动态生成。打开app/view/common/menu.html,核心逻辑是:
<!-- 管理员专属菜单 --> {if condition="$Think.session.user_role eq 1"} <li class="nav-item"> <a href="{:url('Admin/userList')}" class="nav-link">用户管理</a> </li> <li class="nav-item"> <a href="{:url('Admin/systemConfig')}" class="nav-link">系统设置</a> </li> {/if} <!-- 宿管与管理员共用菜单 --> {if condition="$Think.session.user_role in [1,2]"} <li class="nav-item"> <a href="{:url('Dorm/index')}" class="nav-link">宿舍管理</a> </li> <li class="nav-item"> <a href="{:url('Repair/index')}" class="nav-link">报修管理</a> </li> {/if} <!-- 学生专属菜单 --> {if condition="$Think.session.user_role eq 3"} <li class="nav-item"> <a href="{:url('Student/profile')}" class="nav-link">我的信息</a> </li> <li class="nav-item"> <a href="{:url('Student/repairSubmit')}" class="nav-link">我要报修</a> </li> {/if}玄学细节:{:url('Dorm/index')}是 ThinkPHP 的 URL 生成函数,它会自动拼接路由(如/dorm/index),避免硬编码路径。如果改了路由配置,这里会自动同步,不用手动改 HTML。
4. 核心功能实现:床位分配、报修工单、水电费结算的业务逻辑拆解
4.1 床位动态分配:app/controller/Student.php的assignDorm()方法
学生入住不是简单更新student.dorm_id,而是要校验宿舍容量、避免重复分配。关键逻辑在assignDorm():
public function assignDorm() { $student_id = input('post.student_id'); $dorm_id = input('post.dorm_id'); // 步骤1:检查宿舍是否存在且未满员 $dorm = Db::name('dormitory')->where('id', $dorm_id)->find(); if (!$dorm || $dorm['used_seats'] >= $dorm['capacity']) { $this->error('宿舍不存在或已满员'); } // 步骤2:检查学生是否已分配宿舍 $old_student = Db::name('student')->where('student_id', $student_id)->find(); if ($old_student && $old_student['dorm_id']) { $this->error('该学生已分配宿舍'); } // 步骤3:事务分配(原子操作) Db::startTrans(); try { // 更新学生 dorm_id Db::name('student')->where('student_id', $student_id)->update(['dorm_id' => $dorm_id]); // 更新宿舍 used_seats +1 Db::name('dormitory')->where('id', $dorm_id)->setInc('used_seats'); Db::commit(); $this->success('分配成功'); } catch (\Exception $e) { Db::rollback(); $this->error('分配失败:' . $e->getMessage()); } }参数说明:
input('post.student_id'):接收 POST 表单的学号(非数据库 ID),确保业务语义清晰setInc('used_seats'):ThinkPHP 原生递增方法,避免并发时used_seats被覆盖(如两个宿管同时给同一宿舍分人)Db::startTrans():开启事务,保证student和dormitory表更新要么全成功,要么全回滚
4.2 报修工单流转:五状态机(待受理→处理中→已解决→已关闭→已评价)
报修单状态存在repair表的status字段,取值为 0~4:
| status | 状态名 | 操作人 | 触发条件 |
|---|---|---|---|
| 0 | 待受理 | 学生 | 提交报修单时默认 |
| 1 | 处理中 | 宿管 | 宿管点击“开始处理” |
| 2 | 已解决 | 宿管 | 宿管填写处理结果并点击“解决” |
| 3 | 已关闭 | 系统 | 学生确认解决后 24 小时自动关闭 |
| 4 | 已评价 | 学生 | 学生提交评价后 |
状态变更逻辑在app/controller/Repair.php的updateStatus()方法:
public function updateStatus() { $repair_id = input('post.repair_id'); $new_status = input('post.status'); $current_user_role = session('user_role'); $repair = Db::name('repair')->where('id', $repair_id)->find(); // 宿管只能将状态从 0→1 或 1→2 if ($current_user_role == 2) { if (!in_array($repair['status'], [0, 1]) || !in_array($new_status, [1, 2])) { $this->error('宿管只能操作待受理/处理中状态'); } } // 学生只能将状态从 2→3(确认解决)或 3→4(提交评价) if ($current_user_role == 3) { if ($repair['status'] != 2 || $new_status != 3) { $this->error('学生只能确认已解决的工单'); } } Db::name('repair')->where('id', $repair_id)->update([ 'status' => $new_status, 'update_time' => date('Y-m-d H:i:s') ]); $this->success('状态更新成功'); }避坑点:状态变更必须校验当前状态和目标状态的合法性,否则学生可能直接把status从 0 改成 4(跳过处理环节)。
4.3 水电费周期结算:app/command/CalculateFee.php的定时任务
水电费不是实时扣费,而是按月结算。系统提供命令行脚本app/command/CalculateFee.php,可配合 Linuxcrontab每月1号凌晨执行:
<?php namespace app\command; use think\console\Command; use think\console\Input; use think\console\Output; use think\Db; class CalculateFee extends Command { protected function configure() { $this->setName('fee:calculate') ->setDescription('每月1号计算上月水电费'); } protected function execute(Input $input, Output $output) { $last_month = date('Y-m', strtotime('-1 month')); $start_date = $last_month . '-01'; $end_date = date('Y-m-t', strtotime($start_date)); // 当月最后一天 // 查询上月所有报修单(按宿舍统计维修次数) $repairs = Db::name('repair') ->whereTime('create_time', 'between', [$start_date, $end_date]) ->field('dorm_id, count(*) as repair_count') ->group('dorm_id') ->select(); foreach ($repairs as $r) { // 每次维修加收 5 元人工费 $fee = $r['repair_count'] * 5; Db::name('fee_record')->insert([ 'dorm_id' => $r['dorm_id'], 'fee_type' => 'repair', 'amount' => $fee, 'period' => $last_month, 'create_time' => date('Y-m-d H:i:s') ]); } $output->writeln("✅ {$last_month} 水电费结算完成"); } }使用方式:
- 在服务器执行
php think fee:calculate测试 - 添加定时任务:
0 0 1 * * cd /var/www/html && php think fee:calculate >> /var/log/fee-calc.log 2>&1 - 结算结果存入
fee_record表,学生可在个人中心查看
5. 避坑指南:毕业设计答辩前必查的五个血泪问题
5.1 现象:登录后跳转到http://localhost/public/admin/index,页面空白
原因:Web 服务器未开启mod_rewrite(Apache)或未配置 Nginx 伪静态规则,导致 ThinkPHP 路由无法解析/admin/index,返回 404。
解决:
- Apache:确认
httpd.conf中LoadModule rewrite_module modules/mod_rewrite.so已取消注释,且<Directory>块中AllowOverride All已启用 - Nginx:在 server 块中添加
location / { if (!-e $request_filename) { rewrite ^(.*)$ /index.php?s=$1 last; } }
5.2 现象:学生报修提交后,repair表里dorm_id为 NULL
原因:学生登录时session('related_id')未正确赋值,或app/controller/Student.php的submitRepair()方法中未获取学生关联的宿舍 ID。
解决:检查app/controller/Student.php的submitRepair()方法,必须包含:
$student = Db::name('student')->where('student_id', session('username'))->find(); $dorm_id = $student['dorm_id'] ?? null; // 确保 dorm_id 存在 Db::name('repair')->insert(['dorm_id' => $dorm_id, ...]);5.3 现象:修改学生信息后,student表check_in_time字段变成0000-00-00 00:00:00
原因:MySQL 严格模式(STRICT_TRANS_TABLES)下,空字符串''无法插入datetime类型字段。
解决:在config/database.php的mysql连接配置中添加'params' => [\PDO::ATTR_ERRMODE => \PDO::ERRMODE_EXCEPTION],并在插入前判断:
$check_in_time = input('post.check_in_time') ?: date('Y-m-d H:i:s'); Db::name('student')->update(['check_in_time' => $check_in_time, ...]);5.4 现象:runtime/log/目录下日志文件为空,无法排查错误
原因:runtime/目录权限不足(Linux 下需 755,Windows 下需 IIS_IUSRS 可写)。
解决:
- Linux:
chmod -R 755 runtime/ - Windows:右键
runtime→ 属性 → 安全 → 编辑 → 添加IIS_IUSRS用户 → 勾选“修改”和“写入”
5.5 现象:答辩现场演示时,点击“导出Excel”按钮无反应
原因:app/library/PHPExcel.php依赖未安装,或 PHPzip扩展未启用。
解决:
- 确认
php -m | grep zip有输出 - 手动下载 PHPExcel 1.8.2
- 解压后将
Classes/目录复制到app/library/PHPExcel/ - 在导出方法中修正命名空间引用:
require_once APP_PATH . '../library/PHPExcel/Classes/PHPExcel.php'; $objPHPExcel = new \PHPExcel();
6. 答辩加分技巧:三分钟让老师眼前一亮的定制化改造
6.1 给管理员后台加“数据看板”:用 ECharts 渲染宿舍入住率
毕业设计常被质疑“只有功能没有分析”,其实加个看板只需 30 行代码。在app/view/admin/index.html底部插入:
<div id="chart" style="width: 100%; height: 400px;"></div> <script src="https://cdn.jsdelivr.net/npm/echarts@5.4.3/dist/echarts.min.js"></script> <script> var chartDom = document.getElementById('chart'); var myChart = echarts.init(chartDom); var option = { title: { text: '各楼层入住率' }, tooltip: { trigger: 'item' }, series: [{ name: '入住率', type: 'pie', data: [ { value: 92, name: '1楼' }, { value: 85, name: '2楼' }, { value: 78, name: '3楼' }, { value: 96, name: '4楼' }, { value: 88, name: '5楼' }, { value: 91, name: '6楼' } ] }] }; myChart.setOption(option); </script>但数据要真实:不能写死。在app/controller/Admin.php的index()方法中,用 SQL 计算各楼层入住率:
// 获取各楼层入住率(精确到小数点后1位) $rate_data = Db::query(" SELECT floor, ROUND(AVG(used_seats/capacity)*100, 1) as rate FROM dormitory GROUP BY floor ORDER BY floor "); $this->assign('rate_data', $rate_data);然后在 HTML 中动态渲染:
data: [ { value: {$rate_data[0]['rate']}, name: '1楼' }, { value: {$rate_data[1]['rate']}, name: '2楼' }, <!-- 依此类推 --> ]6.2 给学生端加“报修进度追踪”:用 WebSocket 实现实时状态推送
答辩时说“我们支持实时通知”比“点击查看”高级得多。ThinkPHP 6 原生不支持 WebSocket,但可用workerman轻量集成:
- 安装 Workerman:
composer require workerman/workerman - 创建
server.php(放在项目根目录):<?php use Workerman\Worker; use Workerman\Lib\Timer; require_once __DIR__ . '/vendor/autoload.php'; $ws_worker = new Worker("websocket://0.0.0.0:2346"); $ws_worker->count = 1; $ws_worker->onMessage = function($connection, $data) { // 广播给所有连接(简化版,实际应按 dorm_id 分组) foreach($ws_worker->connections as $conn) { $conn->send("报修单 {$data} 状态已更新"); } }; Worker::runAll(); - 启动服务:
php server.php start -d - 学生端 JS 监听:
var ws = new WebSocket('ws://your-domain.com:2346'); ws.onmessage = function(e) { alert('🔔 ' + e.data); location.reload(); // 状态变更后刷新页面 };
关键点:答辩时只需演示“宿管处理报修单 → 学生端弹窗提示”,老师立刻感知技术深度。不需要真部署 WebSocket,本地php server.php start启动即可。
6.3 最后一道保险:生成答辩专用精简包
答辩现场网络不可靠,U 盘拷贝可能损坏。我每次都会做三件事:
- 删除
runtime/全部内容(日志、缓存占空间且敏感) - 删除
vendor/目录,用composer install --no-dev重装(去掉 PHPUnit 等开发依赖) - 压缩为
dorm-system-presentation.zip,内含:public/(含index.php,static/,.htaccess)app/,config/,database.sqlREADME_presentation.md(精简版,只留:默认账号、SQL 导入步骤、三个核心功能演示路径)
从那以后我每次答辩前,都强制走一遍composer install --no-dev && rm -rf runtime/* && zip -r dorm-system-presentation.zip public/ app/ config/ database.sql README_presentation.md。不是为了炫技,是怕临场时vendor/autoload.php找不到——那种冷汗,你懂的。希望帮到你。
本文还有配套的精品资源,点击获取