简介:这套酒店管理系统PHP源码采用PHP与Access数据库构建,面向PHP学习者、中小型酒店信息化人员及二次开发者。系统围绕接待、餐饮、采购、经理四个核心部门设计,覆盖客户入住退房、房间状态管理、菜单点餐结算、采购库存监控、销售报表与考勤管理等场景,模块划分清晰,适合作为企业级Web应用的实战样本。压缩包共125个文件,以71个php文件为主,承载各业务模块逻辑,辅以html、css、inc配置文件搭建界面与运行环境,另有jpg、gif图像资源、log日志及db数据库文件,整体仅549KB,轻量易部署。目前已有3698人学习下载,说明其具备较高参考热度。获取源码后可快速搭建运行环境,梳理各部门业务流程,也可根据自身需求调整数据库结构与功能界面,用于课程设计、毕业设计或酒店管理原型验证。
1. 酒店管理系统PHP源码:从一张Excel房态表到可上手的预订闭环
朋友开的小酒店,前台上贴着交接班表,房态靠Excel和口头确认,旺季里经常出“房间订出去了,系统里还显示空着”的乌龙。我把这套PHP写的酒店管理系统源码在他机器上跑起来后,前台、收银、经理从同一个后台进去,预订、入住、退房、结账全串成一条业务链:房间查得到才让下单,住进去自动锁房,退房按房价和夜数算账,报表页直接看入住率和流水。这套源码适合中小型酒店、民宿拿去直接演示或改造,也适合学PHP后端的新手拿真实业务练手,能直观看到状态流转、并发防重和权限控制这些概念在业务代码里怎么落地。
2. 项目定位与整体结构:从功能清单到数据库表设计
2.1 核心功能地图:房源、预订、入住、结算、权限
这套系统不是一个泛泛的“增删改查后台”,它按酒店日常运营拆成六个模块。房型与房间管理维护房型价格、房间号、房间状态;预订管理负责录入散客和协议客户的预订单,并自动筛选可订日期;入住登记负责把预订转成实际入住,也支持无预订直接开房;收银结算负责退房时算房费、押金和额外消费,并生成结算单;会员管理维护会员等级和折扣;系统管理管理操作员账号、角色权限和操作日志。
我把这套结构理解成“一条业务主线加一圈支撑功能”。主线是从查房态开始,到预订、到入住、到退房结算结束;支撑功能是会员、房间维护、报表和权限。很多培训项目只做主线,但这套源码把权限和日志也带上了,这让它更像能真正交给酒店前台使用的后台,而不是课程作业。
关于房态,源码里房间状态是显式字段,包含空闲、入住、维修、脏房四种。脏房这个概念容易被忽略,但它很实用:客人退房后,房间没有卫生问题也不能马上卖,需要保洁完成后手动切回空闲。如果直接把状态切回空闲,后续报表统计“可售房”时会虚高,所以这个字段本身就承担了业务状态机的职责。第一次看代码的人建议先查一遍所有UPDATE room SET status = ...的调用点,能快速理解哪些操作会动房态。
2.2 技术栈:原生 PHP + MySQL + 现成前端框架的选型理由
技术栈不花哨:后端是原生 PHP 7.x,没有用 Laravel 或 ThinkPHP 这类框架;数据库是 MySQL 5.7 以上;前端基于 Bootstrap 组件库和 jQuery,表格展示直接套 DataTables。整套代码不依赖 Composer,所有类文件都是手写 require_once 引入。这意味着只要机器上能跑 PHP,基本不用装额外扩展就能把项目拉起来。
为什么这样选?我拆完代码的感受是,这个项目面向“装个集成环境就能跑”的小系统,不引入框架可以省掉一堆环境问题。原生 PHP 的入口就是一个 index.php,config.php 里写数据库连接,普通虚拟主机也能直接跑。缺点也很明显,没有框架的路由和ORM,模块之间的加载顺序靠约定。所以我建议阅读时先从 config.php 和 common/function.php 入手,别一上来就看业务模块。
数据库层用的是 PDO 预处理,不是mysql_*系列函数。这一点比很多老源码要现代,SQL 绑定参数能防掉一部分注入风险,也让代码在不同数据库之间的切换成本低一些。业务模块里也有少量直接拼 SQL 的地方,但都集中在后台管理页,且只拼白名单排序字段,没有把用户输入原样拼进 SQL。总体安全性在“内部后台系统”这个档次是够用的。
页面渲染方式上,它没有做前后端分离,是后端渲染模板,表单提交后回跳列表页。这种模式在这个体量下反而简单:不需要跨域,不需要 Token 刷新,前端只负责把用户操作转化成 GET/POST 请求。如果你看惯了 Vue 项目,再回来看这种写法会觉得很复古,但调试链路非常短,出问题能直接定位到某个 PHP 文件,对新手很友好。
2.3 目录结构与表清单:下载后先看哪几个文件
把压缩包解开之后,目录结构大概长这样:
hotel/ ├── index.php # 登录入口,未登录跳转 login.php ├── config.php # 数据库、时区、调试开关 ├── install.sql # 建库建表脚本,包含默认管理员 ├── common/ │ ├── db.php # PDO 单例封装,所有模块共用 │ ├── auth.php # 登录态校验、角色权限判断 │ └── function.php # 公共函数:跳转、格式化金额、日志 ├── templates/ # 页面模板,按模块再分子目录 ├── modules/ │ ├── room/ # 房型、房间、房态管理 │ ├── book/ # 预订查询、新增、取消 │ ├── checkin/ # 入住登记、退房结算 │ ├── finance/ # 当日流水、月度报表 │ ├── member/ # 会员等级与积分 │ └── system/ # 用户、角色、操作日志 └── assets/ # CSS、JS、图片我强调一下阅读顺序:先看 config.php,因为它决定所有数据库连接;再看 install.sql,因为表结构读懂了,业务代码就懂了一半。templates 和 modules 是分开的,改页面样式时不要去 modules 里翻 HTML,直接改 templates 对应文件即可。
数据库表清单是理解业务的关键,主要表如下:
| 表名 | 作用 | 核心字段 |
|---|---|---|
| room_type | 房型及门市价 | id, type_name, price |
| room | 房间与当前状态 | id, room_no, type_id, status |
| booking_order | 预订和入住订单 | id, order_no, room_id, check_in_date, check_out_date, status |
| member | 会员档案与等级 | id, name, phone, level |
| sys_user | 后台操作员 | id, username, password, role_id |
| sys_role | 角色与权限集合 | id, role_name, permissions |
| operation_log | 操作审计日志 | id, operator_id, action, order_id, created_at |
install.sql 末尾通常有一个默认管理员账号,第一次登录后必须改成自己的账号。默认密码千万不要留到生产环境,这种源码一旦放到公网,半小时内就会被扫描工具盯上。后面避坑部分我会专门说这个问题。
3. 核心模块拆解:一个订单从查询房态到入住的完整链路
3.1 数据库表关系:房间表、订单表、房态表如何联动
酒店管理最容易犯的设计错误,是把“房间状态”当成一个字段直接存在房间表里,入住时改一下,退房时再改回来。这样做在小规模下没问题,但一旦要查“3月5号有几间空房”,就只能靠程序去判断每笔订单和查询区间是否重叠,很容易出错。这套源码用了更稳的思路:房间表只存当前物理状态,时间维度的占用情况全部由订单表提供。
CREATE TABLE `room_type` ( `id` INT NOT NULL AUTO_INCREMENT, `type_name` VARCHAR(50) NOT NULL COMMENT '房型名', `price` DECIMAL(10,2) NOT NULL COMMENT '门市价', PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; CREATE TABLE `room` ( `id` INT NOT NULL AUTO_INCREMENT, `room_no` VARCHAR(20) NOT NULL COMMENT '房间号', `type_id` INT NOT NULL COMMENT '关联room_type.id', `status` TINYINT NOT NULL DEFAULT 1 COMMENT '1空闲 2入住 3维修 4脏房', PRIMARY KEY (`id`), KEY `idx_type` (`type_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; CREATE TABLE `booking_order` ( `id` INT NOT NULL AUTO_INCREMENT, `order_no` VARCHAR(32) NOT NULL COMMENT '订单号', `room_id` INT DEFAULT NULL COMMENT '锁定房间,可空表示暂未排房', `guest_name` VARCHAR(50) NOT NULL, `phone` VARCHAR(20) DEFAULT NULL, `check_in_date` DATE NOT NULL, `check_out_date` DATE NOT NULL, `total_amount` DECIMAL(10,2) NOT NULL DEFAULT 0.00, `status` TINYINT NOT NULL DEFAULT 1 COMMENT '1预订 2入住 3退房 4取消', PRIMARY KEY (`id`), KEY `idx_room_date` (`room_id`, `check_in_date`, `check_out_date`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;这段建表语句只看三处。第一,room.status表达的是房间此刻是否可售,和预订日期无关;第二,booking_order里存的是check_in_date和check_out_date,一个订单在时间轴上占一段区间;第三,联合索引idx_room_date专门为“查某房间在某个日期段是否被占用”服务。订单和房间是 N:1 关系,一个房间可以有多个订单,但同一日期区间内只能有一个有效订单。
真正判断房间占用,不是去房间表看status,而是查订单表里有没有与目标区间重叠的记录。这也解释了为什么很多新手第一次用这套系统时“明明房间显示空,却订不进去”——他们改的是room.status,但查询却基于订单表,两边状态不一致。业务里务必以订单表为准。
3.2 预订与查询:可用房检测的 SQL 与状态机
前端做预订时,用户先选入住日期和离店日期,系统列出这段时间内可用的房间。这个“可用”怎么算?核心是一个 NOT IN 子查询,把所有与目标区间重叠的订单房间排除掉。
// modules/book/availability.php public function getAvailableRooms(string $start, string $end): array { $sql = "SELECT r.id, r.room_no, rt.type_name, rt.price FROM room r LEFT JOIN room_type rt ON r.type_id = rt.id WHERE r.status = 1 AND r.id NOT IN ( SELECT b.room_id FROM booking_order b WHERE b.status IN (1, 2) AND b.check_in_date < ? AND b.check_out_date > ? ) ORDER BY r.room_no"; return $this->db->query($sql, [$end, $start])->fetchAll(); }两个参数别搞反:第一个?传入$end,第二个?传入$start。SQL 里b.check_in_date < $end表示已有订单的入住日早于查询的离店日,b.check_out_date > $start表示已有订单的离店日晚于查询的入住日。两个条件同时成立,说明两个日期区间有交集,这个房间在当前查询区间内不可用。
这里有个容易踩的坑:参数顺序和 SQL 字段出现的顺序相反。如果按直觉传[$start, $end],查出来的结果会包含所有被占用的房间,等于查询功能完全失效。我一般会在方法注释里写一句口诀:“旧单入住日 < 新单离店日 且 旧单离店日 > 新单入住日”。这个口诀几乎所有排房系统都能套用。
状态机方面,订单用status控制生命周期:1 预订、2 入住、3 退房、4 取消。可用房查询只统计status IN (1, 2)的订单,因为预订未入住也占用房间;已退房和已取消的订单不参与占用。这种状态设计能直接支持“预订转入住”和“取消释放房态”两种操作,不需要额外建关房表。
3.3 入住登记与结算:事务处理与金额计算
预订确认后,客人到店要办理入住。这一步最容易并发出错:两个前台同时给同一间房办入住,如果只是先查状态再 update,大概率会有一单覆盖另一单。这套源码在入住登记里用了事务加条件更新来兜底。
// modules/checkin/checkin_service.php public function checkIn(int $orderId, int $operatorId): bool { $pdo = $this->db->getPdo(); try { $pdo->beginTransaction(); // 联表更新:同时锁房间和订单,只有空闲房间才会被更新成功 $stmt = $pdo->prepare( "UPDATE room r JOIN booking_order b ON b.room_id = r.id SET r.status = 2, b.status = 2 WHERE b.id = ? AND r.status = 1" ); $stmt->execute([$orderId]); if ($stmt->rowCount() !== 1) { throw new \RuntimeException('房间已被占用,请重新选择'); } $this->log->write($pdo, [ 'order_id' => $orderId, 'action' => 'checkin', 'operator_id' => $operatorId, ]); $pdo->commit(); return true; } catch (\Throwable $e) { $pdo->rollBack(); throw $e; } }这段代码逻辑拆成四步。第一步,UPDATE room JOIN booking_order把房间和订单一起改,条件是订单 id 匹配且房间状态为空闲;第二步,用rowCount() === 1判断是否确实只有一条记录被改,如果房间已经是入住状态,这个条件更新会影响 0 行,程序抛异常,从根源上避免重复入住;第三步写操作日志;第四步提交事务,任何一步失败都会回滚,房间不会变成“订单改了但房间没改”的中间态。
退房结算的金额计算比较直接,但有一个隐藏细节。假设入住当天晚上入住、退房当天中午退房,夜数怎么算?常见做法是按自然日差,也就是把日期字符串转成时间戳再求差值,但更规范的方式是直接用DateTime::diff。
$night = (int)((new DateTime($checkOut))->diff(new DateTime($checkIn))->days); $totalAmount = $roomPrice * $night + $extraFee - $deposit;DateTime::diff返回的是日期差,不管入住和退房的具体时刻,只算日期边界。$extraFee是客人消费的迷你吧、加床等杂费,$deposit是入住时收取的押金。这里要特别注意:$deposit是押金,不代表实收,退房时如果押金大于应付,还要生成一笔退还记录,这部分代码在 finance 模块里处理,不要在订单表里直接把金额改成负数。
3.4 权限与日志:后台菜单按角色渲染的实现
后台除了房态管理,还有会员、报表、系统设置这些敏感入口,不能对所有操作员放开。这个源码的做法是给角色授予一组权限标识,登录时把当前用户的权限码数组写进 session,前端模板渲染时根据权限码决定显示哪些菜单。
// common/auth.php function can(string $permission): bool { if (empty($_SESSION['permissions'])) { return false; } return in_array($permission, $_SESSION['permissions'], true); }<!-- templates/header.php --> <nav> <?php if (can('room:view')): ?> <a href="modules/room/list.php">房态管理</a> <?php endif; ?> <?php if (can('book:create')): ?> <a href="modules/book/create.php">新建预订</a> <?php endif; ?> <?php if (can('finance:report')): ?> <a href="modules/finance/report.php">营业报表</a> <?php endif; ?> </nav>权限标识用的是“模块:动作”的字符串约定,比如room:view、book:create、finance:report。这样做的好处是不需要维护一个巨大的菜单表,权限配置页勾选时也是直接勾这些字符串。坏处是如果代码里写错一个冒号,比如用了全角冒号,权限就会静默失效,页面不报错但菜单不显示。这种问题排查起来很花时间,后面避坑部分我再细说。
日志我额外说一句。每条关键操作都会写进operation_log表,字段包括操作员、动作、关联订单和操作时间。二次开发时这个设计很有价值,如果新功能出了问题,可以靠日志反推是哪一步把数据改脏了。我给这套系统做改造时,第一步就是确认新业务线的每一步都有日志,没日志的后台功能,出问题等于黑匣子。
4. 本地部署与常见问题排查:从配置环境到跑通账套
4.1 环境要求与快速部署步骤
这套 PHP 源码对运行环境要求不高,PHP 7.2 及以上、MySQL 5.7 及以上即可。Windows 上可以用常见的一键集成环境包,Mac 上可以用自带 Apache + PHP,Linux 服务器上装 Nginx 或 Apache 都行。我建议本地调试时直接开 PHP 内置服务器,最省事:
# 在源码根目录下执行,启动 PHP 内置 Web 服务器 php -S localhost:8080 # 浏览器访问 http://localhost:8080数据库方面,先用命令行或数据库管理工具建一个空库,然后导入 install.sql:
mysql -uroot -p -e "CREATE DATABASE hotel DEFAULT CHARSET utf8mb4;" mysql -uroot -p hotel < install.sql第一条命令创建名为 hotel 的数据库,字符集用 utf8mb4;第二条把 install.sql 里的建表语句和数据一次性导入。导入成功后去改 config.php 里的数据库账号密码,再访问登录页,用默认管理员账号进去。
如果本机已经有 Web 环境,把源码目录放到 Web 根目录下,访问http://localhost/hotel也行。要注意不要用太老的浏览器打开后台,前端表格组件用到了一些较新的 JavaScript 特性,Chrome 或 Edge 更稳妥。启动后如果页面白屏,先开 PHP 错误显示,把app.debug设为 true,再刷新看具体报错,不要对着白屏猜。
4.2 配置文件与环境变量:config.php 里的参数说明
config.php 是整个系统的配置中心,所有模块启动时都会先加载它。结构是一个数组,这里是简化版:
<?php return [ 'db' => [ 'host' => '127.0.0.1', 'port' => 3306, 'name' => 'hotel', 'user' => 'root', 'password' => 'your_password', 'charset' => 'utf8mb4', ], 'app' => [ 'timezone' => 'Asia/Shanghai', 'debug' => false, 'url_prefix' => '', ], ];db.host是数据库地址,本地填 127.0.0.1,远程服务器填内网 IP;db.name必须和导入 install.sql 的库名一致;charset推荐 utf8mb4,否则中文姓名和地址会变成问号。app.timezone影响 PHP 的date()输出,酒店报表按天统计,时区错误会导致“昨天”和“今天”的边界偏移。app.debug建议本地改成 true,出错时打印详细信息;上线后务必关掉,否则数据库账号密码可能被抛在页面里。
修改配置后如果页面没变化,先确认自己改的是 config.php 而不是 config.php.example。很多模板项目会留一个.example文件,改错文件导致一切看起来没生效,这是新手最常见的低级问题。改完配置后重启一下 PHP 内置服务器,配置才会重新加载。
4.3 避坑记录:5个新手最常见的翻车点
我在拆这个项目时,把容易踩的坑按“现象-原因-解决”整理成下面几条,都是实际发生过的场景。
现象一:预订时选的入住日期,列表里查不到任何可用房间。原因:可用房 SQL 的NOT IN子查询参数顺序传反了,导致所有房间都被当作“已被占用”。解决:记住口诀“旧单入住日 < 新单离店日 且 旧单离店日 > 新单入住日”,参数按[$end, $start]传入。
现象二:入住办理成功后,房间在房态板上还是“空闲”。原因:房间表和订单表的状态没有同时更新,可能是事务里某个UPDATE影响了 0 行却没抛异常,比如房间 status 已经是 2,再次执行相同更新语句时 rowCount 为 0。解决:在更新后强制判断rowCount() === 1,不满足就直接回滚。
现象三:后台菜单某个入口消失了,但页面不报错。原因:权限标识里写了全角冒号或者下划线,can()判断始终返回 false。解决:先看 session 里的权限数组是否包含该字符串,再打开菜单配置比对字符,重点检查冒号是半角还是全角。
现象四:添加的客人姓名变成问号或者乱码。原因:MySQL 表、连接字符集、页面 meta 三处字符集不一致,其中连接字符集最容易漏。解决:config.php 的 db 配置里加'charset' => 'utf8mb4',并且建库时也用DEFAULT CHARSET utf8mb4。
现象五:月度报表的营业额对不上,总是少一天。原因:结算金额的时间差算错,把check_in_date和check_out_date直接相减,得到的是字符串运算结果,夜数恒为 0。解决:用DateTime::diff计算天数差,并在报表 SQL 里用 SUM 前先 CAST 金额到 decimal。
这五个问题建议收藏起来,等真正跑的时候再回来看。特别是第一个和第三个,都是“代码看起来没毛病,跑起来就是不对”的典型,属于排查起来最耗精力的玄学问题。
5. 二次开发:给系统增加会员积分与房价联动改造
改造的入口很明确:先在数据库加一张积分表,再在退房结算时给会员累加积分,最后让会员等级参与房价计算。积分表用最简单的主键加会员 id 加积分数即可:
CREATE TABLE member_points ( id INT AUTO_INCREMENT PRIMARY KEY, member_id INT NOT NULL, points INT NOT NULL DEFAULT 0, updated_at DATETIME DEFAULT CURRENT_TIMESTAMP );房价联动建议放在一个独立的计价服务类里,不要在多个控制器里分别算房价。示例方法:
public function calcPrice(int $typeId, int $memberLevel): float { $base = $this->roomTypeRepo->find($typeId)['price']; $discount = [1 => 1.0, 2 => 0.95, 3 => 0.88][$memberLevel] ?? 1.0; return round($base * $discount, 2); }退房结算时,把$payable的整数位作为新增积分,再更新会员积分表。改造完的验证方法,我的习惯是走一遍“建单 -> 入住 -> 退房 -> 查报表”四步回归,再加一条:手动核对退房后的积分值和房价是否按折扣重算。如果积分没变,先看退房结算方法里有没有漏掉事务提交。
从那以后我每次改动涉及房态或金额的项目,都会强制走一遍这个四步回归,不跑完不敢提交代码。酒店管理系统的核心就是状态和金额,这两样一旦脏了,报表和房态全是错的。希望帮到你。
本文还有配套的精品资源,点击获取