做酒店管理系统,我最早其实是拿ThinkPHP硬写的单页应用,前端全靠jQuery拼,改一个页面动全身,上线之后被老板催着改需求,差点没把人逼疯。后来换成了php + uniapp的组合,前端用小程序同时兼顾微信端,后台继续用PHP出接口,才终于把开发和维护的节奏理顺。这篇就是把当时踩过的坑、总结的设计思路和可以直接抄的代码思路整理出来,给正在做课程设计、毕业设计,或者想给自家小酒店做一套管理系统的朋友参考。
这个小程序能干什么,一句话说清楚:面向住客的微信小程序负责订房、查房、退房;PHP后端负责管房态、算账、出报表;数据库维护所有房间和订单数据。适合谁看?要么是学生党要做毕设,要么是想低成本给民宿、快捷酒店搭一套轻量系统的开发者。下面前面讲整体拆解和数据库设计,中间给核心接口和前端实现,最后是真正上线时必须面对的坑。
1. 项目整体拆解与落地思路
1.1 先别急着写代码,把需求按场景切开
我做这个项目第一件事不是装环境,而是先把“有哪些人要用”列清楚。酒店管理系统最典型的三类角色是:住客、前台/管理员、老板。住客在微信小程序里看房、下单、查订单;前台在后台确认入住、办理退房、处理打扫状态;老板关心的是每天卖了几间房、收了多少钱,所以要的是统计报表。
按照这个逻辑,系统就把功能拆成了三个主要模块:小程序端、PHP接口端、数据库端。小程序端负责展示和操作入口,接口端负责业务逻辑和数据处理,数据库负责最终落盘。每一层只做自己的事,后续要加功能(比如加一个房间售卖小商城)也不会把代码搅成一锅粥。
给需求分层的时候有一个容易忽略的点:**民宿类的小系统,房间数一般在20间以内,并发量根本不需要考虑分布式,但“状态一致”必须一开始就设计好。**最常见的问题是同一间房被两个小程序用户同时选中,结果都下单成功了,后面只能靠人工取消。这个问题后面会专门讲。
1.2 为什么选PHP + uniapp,而不是Java + Android或纯Web
选PHP当后端,主要原因是快。用户注册、订单增删改查、统计报表,这些都是典型CRUD加一点运算逻辑,PHP一个文件就能出一个接口,开发效率确实高。特别是配合ThinkPHP或者Laravel这种框架,路由、参数校验、ORM模型都现成了,不用自己造轮子。做毕设也好、做小系统也罢,用PHP能把大量时间省在业务实现上,而不是搭地基。
选uniapp则是为了“一套代码,多端运行”。同一套页面,编译出来能跑微信小程序,也能跑H5和安卓App。我实际用下来,90%的代码是共享的,只有平台差异化的小功能需要单独做条件编译。对个人开发者来说,这比同时维护小程序原生的一套加上Vue Web一套要划算得多。
还有一个关键点是生态。微信小程序端的UI组件库,uView Plus、uv-ui这类都是基于uni-app的,现成的表单、日期选择、弹窗都不需要自己写。我最早用原生小程序写过一个日历房态组件,写了三天,换到uniapp用插件市场现成的组件,半天就接完了。做通用型系统,会借力比会造轮子更实际。
1.3 整体数据流和目录结构设计
我的目录结构是这么拆的:
- 后端ThinkPHP项目:
application/api/controller/放对接小程序的控制器,application/api/model/放数据模型,输出统一JSON格式。 - 前端uniapp项目:
pages/index/放小程序首页,pages/room/放房间详情和预订,pages/order/放订单列表和管理,pages/user/放登录和个人中心。 - 数据库脚本单独放一个SQL文件,方便别人部署时直接跑。
接口数据流可以简单理解为:小程序发起请求 → PHP路由接受参数 → 模型查数据库 → 业务层处理 → 返回JSON → 小程序渲染页面。“接口返回格式统一”这件事,从一开始就要定死。我的格式是:
{ "code": 200, "msg": "success", "data": [] }code为200代表成功,4xx是客户端传参有问题,5xx是服务端异常。小程序端收到非200就直接弹msg,避免每个页面都写一套错误处理。
2. 数据库设计与核心表结构
2.1 房间、房型、订单、用户四张核心表怎么设计
只要是一套业务系统,数据库设计就是天花板。表没设计好,后面代码写得再漂亮也会卡在查询上。酒店类系统,核心表就是四张:房型表、房间表、订单表、用户表,再加一张可选的房间清扫记录表。
房型表不直接面向房间,而是面向“出售的产品”,比如大床房、双床房、套房。一张房型对应多个物理房间。字段我就放得比较简单:id、name、price、area、bed_type、max_people、pic、description。价格直接放这张表,好处是前台展示房型价格只用查一次。
房间表才是实实在在的房间资源,每个房间一行。字段是:id、room_type_id、room_no、floor、status。其中status我最开始是用字符串存的,available/occupied/cleaning,后来发现还是用数字更好维护,0空闲、1入住中、2待打扫、3维修。为什么用数字?因为前端要判断状态图标,后端要做统计,数字范围可控,不容易拼错。
订单表信息量最大,需要考虑好。核心字段大致包括:
id、order_sn:订单号,房间下单的时候要生成唯一订单号user_id:下住客IDroom_id:实际入住的物理房间IDroom_type_id:下单时锁定的房型IDcheck_in_date、check_out_date:入住和退房日期nights:入住晚数total_amount:订单总金额status:状态机,用数字。0待支付、1已支付待入住、2已入住、3已退房、4已取消、5已退款
用户表除了常规的手机号、昵称、头像之外,我会额外存一个openid字段,用来关联微信登录。openid是微信小程序里识别用户的唯一凭证,同一个用户在不同的小程序里openid不一样,所以在哪家小程序下单,就用哪个小程序授权得到的openid。
2.2 最隐蔽的坑:房价日历和预订冲突
这套设计里最隐蔽的坑就是“一笔订单跨多个日期”造成的房态判断。很多人第一版设计房价时只想着房间表当前状态,结果入住日期和退房日期的房间库存没法判断。
我的做法是加一张“房态日历表”room_calendar,字段是id、room_id、date、is_booked。当一个订单支付成功后,就把这笔订单覆盖的所有日期在日历表里全部标成已预订。这样用户查询的时候,只要查某一天某个房间的is_booked状态,就能判断能不能订。
有人会问:为什么不直接看订单表里有没有重叠日期?看订单表也可以,但SQL写起来要处理多个日期条件,而且一旦加了取消/退款逻辑,查询条件会复杂很多。用日历表相当于把“房间在某天有没有被占”这个高频问题做了一个缓存,查询性能更好,代码也更直白。
生成日历数据的时候注意一个细节:**订单覆盖的是入住日到退房日的前一天。**比如入住10月1日,退房10月3日,实际占用的应该是1号和2号两天,3号可以继续卖给新的住客。我见过有人在这个地方把退房当天也算作占用,导致明明有空房却订不进去,等退房当天客人还没走,新客人又在门口了,造成线下冲突。
3. 后端接口设计与核心实现
3.1 接口清单:列全了再动手写
动手写代码前,我建议先把接口清单列出来,前后端根据这个来对齐,效率会高很多。我的接口清单大致是这样:
| 模块 | 接口 | 说明 |
|---|---|---|
| 用户 | POST /api/user/login | 微信登录,用code换openid,返回token |
| 用户 | GET /api/user/info | 获取个人资料和订单数量 |
| 房型 | GET /api/room/types | 获取房型列表(小程序首页展示) |
| 房间 | GET /api/room/available | 根据日期筛选可订房间 |
| 订单 | POST /api/order/create | 创建订单(抢房时的核心接口) |
| 订单 | GET /api/order/list | 当前用户的订单列表 |
| 订单 | POST /api/order/cancel | 取消订单,释放房态日历 |
| 订单 | POST /api/order/pay | 模拟支付或真实微信支付 |
| 后台 | POST /api/admin/login | 管理员登录 |
| 后台 | PUT /api/admin/room/status | 修改房间状态 |
接口清单的意义在于让前后端都能看到全貌,不会出现前端等着后端接口,后端以为前端不需要这种尴尬。实际开发中我把这套接口定义在文档里,前端按文档模拟数据就能并行开发。
3.2 登录鉴权:小程序登录不是简单拿个用户名密码
小程序端的登录,我推荐用“微信授权登录 + 后端签发token”的方式,而不是自己写一套用户名密码注册。原因是微信小程序天然有身份体系,用户点一下“微信授权”就能完成身份识别,注册表单的门槛一降低,订单转化率会明显提升。
整个流程是这样的:小程序端调用uni.login()拿到一个临时code,这个code有效期只有几分钟,把它传给后端;后端拿着code加上小程序的AppID和AppSecret,去微信的jscode2session接口换openid和session_key;拿到openid后,去用户表查有没有这个人,没有就自动注册,有就更新登录时间;最后后端用openid签发一个token返回给小程序,后续请求都带上这个token识别身份。
token的生成我用的是简单可复现的方式:
// 生成token并缓存,这里不做复杂JWT,直接用hash function createToken($userId) { $token = md5($userId . time() . uniqid()); // 存到redis或者数据库session表,设置7天过期 return $token; }这里说一句,我没用JWT而是用缓存token,主要考虑到是小型系统,做成token存Redis,后端要在鉴权时查一下,实现简单,踢人也方便。如果追求无状态API,JWT也是好选择,但刷新和吊销机制会复杂一点。
注意AppSecret绝对不能存到小程序前端代码里,因为小程序代码包可以被人扒开看,曾经就有开发者把AppSecret写在前端,被同行拿去刷接口,损失惨重。AppSecret只存在于PHP后端环境变量或配置文件中。
3.3 预订接口:如何防止“多人抢同一间房”
这是整个系统最关键的业务逻辑。正常逻辑是这样:用户选好入住日期和退房日期后,前端调/api/room/available把可订房间列出来,用户看了房型,点了“立即预订”,后端要做的第一件事不是直接写订单,而是“校验房态”。
我的实现思路是加了一个“预占”环节,用数据库事务把创建订单和锁定房间串在一起:
public function createOrder($userId, $roomId, $checkIn, $checkOut) { $res = Db::transaction(function () use ($userId, $roomId, $checkIn, $checkOut) { // 1. 查询该房间在入住日期到退房日期间是否已被占用 $dates = getDateRange($checkIn, $checkOut); $booked = Db::name('room_calendar') ->where('room_id', $roomId) ->where('date', 'in', $dates) ->where('is_booked', 1) ->count(); if ($booked > 0) { throw new \Exception('当前房间已被预订,请重新选择'); } // 2. 锁定这些日期的房态 Db::name('room_calendar') ->where('room_id', $roomId) ->where('date', 'in', $dates) ->update(['is_booked' => 1]); // 3. 生成订单 $orderId = Db::name('order')->insertGetId([ 'order_sn' => generateOrderSn(), 'user_id' => $userId, 'room_id' => $roomId, 'check_in_date' => $checkIn, 'check_out_date' => $checkOut, 'nights' => count($dates) - 1, 'total_amount' => $this->calcAmount($roomId, $dates), 'status' => 1 ]); // 4. 清空之前可能残留的缓存信息 cache("room_price_" . $roomId, null); return $orderId; }); return $res; }可以看到,事务保证了:要么房态锁定和订单创建都成功,要么一个都没发生。如果不加事务,第一步查的时候显示可订,但第二步锁房态时发现被别人占了,就会产生超卖。
还有一个细节:用户创建订单后如果一直不支付,这些日期就被白白占着,影响其他客人下单。所以我会加“待支付订单锁定时限”,比如15分钟。超过时限后通过定时任务把未支付订单取消,并释放对应的房态日历。
3.4 房间价格要不要做动态计算
很多毕设级别的系统都是房型表里一个固定价格。但真实酒店场景没那么简单,节假日要涨价,连住几天可能有折扣,不同渠道价格还不同。我在这套系统里给房型表加了一个price_rules字段,用JSON存规则,比如:
{ "normal_price": 268, "holiday_price": 398, "holiday_dates": ["2025-10-01", "2025-10-02", "2025-10-03"], "long_stay_discount": 0.9, "long_stay_nights": 3 }在算总价的方法里,根据入住日期判断是否属于节假日,如果入住晚数≥3晚就打9折。实际开发中不用把价格规则做太重,能覆盖“节假日调价”和“连住优惠”两种常见运营需求就够了。动态价格是挺常见的加价点,毕设答辩时也能成为一个亮点。
4. 前端小程序页面与交互实现
4.1 uniapp页面架构和小程序端适配
uniapp的项目结构跟Vue项目几乎一样,页面写在pages目录下,路由由pages.json自动生成。我做的是四个底部Tab页面:首页(房间预览)、订单(订单列表)、消息(通知)、我的(个人中心)。另外有房型详情页、订单确认页、支付结果页。
这里有个uniapp和小程序原生开发最大的差异:**uniapp里数据绑定用的是Vue语法,页面不能用小程序原生的setData。**比如显示房间列表,数据放在data里后,用v-for就能渲染:
<view class="room-card" v-for="item in roomList" :key="item.id" @click="goDetail(item)"> <image :src="item.pic" mode="aspectFill"></image> <view class="room-name">{{ item.name }}</view> <view class="room-price">¥{{ item.price }}/晚</view> </view>Vue语法对小程序的差异主要体现在生命周期和平台API上。比如页面加载时获取数据,uniapp是小程序风格的onLoad,不是Vue的created。这个很容易踩坑,我第一次写的时候就习惯性用了mounted,结果数据一直没加载出来。
4.2 房间列表页:按日期过滤可订房型
房间列表页是用户进入小程序后看到的核心页面,也是转化率的关键。我建议不用一上来就把所有房间都展示出来,而是让用户先选择入住日期和退房日期,再根据后台房态日历返回可订房型。这样子可以提前筛掉没房的日期,用户体验大幅提升。
页面逻辑是这样的:顶部两个日期选择器,默认是今天入住、明天退房;用户改变日期后重新请求/api/room/available接口;接口返回的数据是每个房型剩余的可订房间数和价格。代码示意:
async loadRooms() { const res = await uni.request({ url: '/api/room/available', data: { start: this.checkInDate, end: this.checkOutDate } }); this.roomList = res.data.data; }注意一个交互细节:选择退房日期时,至少要大于入住日期一天,否则在后端校验就会报错。前端要在日期选择器的@change事件里加判断,如果退房日期小于等于入住日期,就自动把退房日期改成入住日期加一天,并提示用户。这种小细节对用户体验的影响比功能本身还大。
4.3 订单确认页:金额明细和状态流转展示
用户点了房型详情页里的“立即预订”,进入订单确认页。订单确认页有几个重点:显示房型图片、入住/退房日期、每晚价格、总价;选择入住人手机号(这里可以直接用当前微信登录用户的信息);填写备注,比如“需要大床房无烟房”。都填完了再点提交。
订单创建成功之后进入待支付状态。这时页面上要立即显示一个倒计时,15分钟内未支付自动取消。前端倒计时用setInterval实现,每秒减一,到0时把订单状态改成已取消,页面跳回列表页并提示“订单超时未支付,已自动取消”。
订单状态流转这个事,我在前端专门封装了一个工具函数,根据后端返回的status字段渲染对应的操作按钮:
- status 0:显示“去支付”按钮
- status 1:显示“入住中”,并提示房间号和入住密码
- status 2:显示“退房”按钮
- status 3:显示“已完成”和“评价”按钮
这样后端只存状态码,前端根据状态码展示不同界面。有人喜欢把前端展示文案直接存在后端,其实没必要,文案改动会导致前后端版本要同步发布,不如前端自己维护文案。
4.4 管理后台:一个够用的管理页面
管理后台我建议单独写一个H5页面,不做成小程序。因为管理后台的使用者是酒店前台和老板,他们不可能拿手机小屏去录入房间信息、看报表,更多是PC上操作。uniapp的H5编译能力这时候又能派上用场,同一套代码里,用#ifdef H5条件编译区分平台,页面布局在H5上用更宽的栅格。
管理后台核心页面有哪些?房间管理(创建编辑房间、修改房型价格)、订单管理(查看所有订单、确认入住、办理退房)、统计报表(今日入住率、今日营收、本月营收)、清洁管理(房间打扫状态切换)。这些页面不需要太花哨,数据表格加上操作按钮,功能齐全即可。
做H5管理后台时有个关键点:H5端和小程序端的用户体系不同,管理员的登录一定要单独做。我给管理员单独建了一张表,账号密码登录,生成管理员token,和住客的token做区分。权限上,管理员能看所有订单,住客只能看自己的。这块如果混用一套逻辑,很容易出现用户A看到用户B订单的低级错误。
5. 常见问题与排查技巧实录
5.1 小程序请求后端接口的跨域和合法域名问题
小程序端请求HTTP接口,跟浏览器环境一样有跨域限制吗?简单说:小程序开发工具里可以关闭合法域名校验,但真机预览和线上发布必须配置合法域名。如果后端接口跑在本地,真机就访问不了,这是新手最常见的报错。
我自己碰到的典型场景是:开发阶段用的http://localhost:8080/api/,开发工具里勾选了“不校验合法域名”,一切正常;一上线,小程序后台配置域名的时候发现没有HTTPS证书,只能临时买了一年证书。所以如果打算上线,域名和HTTPS证书要提前准备,阿里云或腾讯云的免费证书够用。
如果只是想本地调试,后端PHP需要处理好跨域头:
header('Access-Control-Allow-Origin: *'); header('Access-Control-Allow-Methods: GET, POST, PUT, DELETE, OPTIONS'); header('Access-Control-Allow-Headers: Content-Type, Authorization, token');还有一点容易忘记:小程序端自定义的请求头如果带了Authorization字段,后端必须把它放在Access-Control-Allow-Headers里,否则OPTIONS预检请求会被拦截。这个问题我排查过半天,最后发现是这一个头没写对。
5.2 时间日期处理的常见坑
PHP里处理跨天入住,最经典的坑就是时区问题。默认情况下PHP的date()函数使用服务器时区,如果服务器时区不是Asia/Shanghai,那么生成订单时间、判断日期都会乱掉。所以PHP文件开头要设置:
date_default_timezone_set('Asia/Shanghai');前端小程序端也要注意。小程序中的new Date()获取的是本机时间,如果用户手机日期被手动改过,那前端传回来的日期可能跟真实日期不符。我做了个保险:前端在选择日期后,不传“当前时间”给后端,而是传经过日期选择器格式化后的日期字符串(YYYY-MM-DD),后端按这个字符串做业务判断,不依赖前端时间。这样做的原因是后端只关心用户选的入住日期,而不是用户下单一刻的手机时间,实际订单支付时间由后端自己记录,就避免了时间伪造的问题。
5.3 PHP版本迁移到7+之后的老坑
不少人的毕设代码是老PHP写的,比如用mysql_query()这种早就移除的函数。如果系统要跑在PHP 7以上,有几个兼容性大坑得注意:
mysql_*系列函数全部移除,必须用mysqli_或PDO。- PHP 7不再支持
mysql_escape_string(),要用mysqli_real_escape_string()或参数绑定。 - 类名和函数名不再区分大小写,但这是一个警告级别的切换,尽量统一小写。
each()循环在PHP 8彻底移除,老代码里如果有,直接改成foreach。- 数组字符串偏移量语法
$str{0}在PHP 8废弃了,改成$str[0]。
如果在写学校项目时一开始就用PDO预处理,这些坑就一个都踩不到。PDO预处理除了防注入,还能让代码在新老PHP版本间平滑迁移,这是我在重构老系统过程中最深的体会。
5.4 uniapp打包成App和小程序遇到的差异化问题
同一个uniapp项目,编译到微信小程序、H5、Android App时会有差异,最常见的几个:
第一,微信小程序端必须使用uni.request,不能用axios。虽然uniapp兼容了部分Web API,但小程序的网络请求底层就是wx.request,uniapp已经做了封装,直接用uni.request最稳妥。
第二,App端使用相机、定位、支付这类原生能力,如果沿用小程序平台的API,会报“xxx is not a function”错误。需要针对App端使用plus对象或者原生插件。比如我的系统里有个扫码办理入住的功能,小程序端用uni.scanCode,App端就得用uni.scanCode也支持,但如果涉及蓝牙打印,就必须走原生插件。
第三,H5和App端的跨域限制不同,H5受浏览器跨域限制,App端不受。这就导致同一个请求,在开发工具里跑通了,真机App也跑通了,但H5发布到另一台服务器时会有跨域报错。处理方案是想清楚前端部署在哪里,接口地址用相对路径还是绝对路径。
5.5 房态并发冲突的兜底方案
前面在接口实现里讲了事务处理,但这里再补充一个兜底方案:就算后端做了事务,在网络极端延迟的情况下,仍可能出现用户点完提交没反应、再次点击产生两个订单的情况。我给前端加了“提交锁”,变成提交按钮时加一个loading状态,在结果返回前禁止再次点击;后端同一用户、同一房间、同一入住日期的重复订单也要加唯一索引或逻辑判重。
逻辑判重的代码就是在创建订单的表里加一个唯一字段,比如order_token,前端提交时生成一个唯一的token(UUID),后端在插入前检查这个token是否已被使用。如果重复提交,直接返回“please don't submit twice”。这个小改动在真实场景中救过我好几次,尤其是用户手机网络差的时候,按钮重复点击导致数据错乱,处理起来的成本远大于那几行判断代码。
6. 实测下来比较顺手的扩展方向
系统基础跑通之后,如果还有余力,我建议往三个方向扩展,性价比都比较高。
第一个是加入扫码入住和智能门锁联动。客人到店后,通过小程序扫描房间里贴的二维码,后端校验订单状态是已支付,就返回一个临时开门密码。做这个功能需要和智能门锁厂商对接API,但是对接方式都是HTTP回调,后端PHP实现不难,却能让整个系统的“科技感”上一个档次。
第二个是接入真实微信支付。我上面用的是模拟支付,真实支付需要申请微信支付商户号,然后在后端调用微信支付统一下单API,拿到支付参数再返回给前端,前端用uni.requestPayment唤起支付。流程不复杂,麻烦的是微信审核的资质材料,企业主体才能申请。
第三个是加一个数据统计的可视化看板。后台管理端用Chart.js或者ECharts,把每个月的入住率、营收趋势、热门房型做成图表。做这个功能有助于老板做经营决策,也让整个系统显得完整。PHP端只需要聚合SQL查一下每日订单数和营收,接口返回数据,前端画图就行。
7. 我个人踩过几次坑之后的体会
整个项目做下来,我的体会是:系统复杂度不在于代码量,而在于状态管理。酒店这个业务,房态、订单状态、支付状态,每一种状态之间都有流转关系,一不留神就会出现“房已入住但状态还是待支付”这种逻辑漏洞。所以在动手写第一个接口之前,花两天时间把状态流转图画清楚,把每个状态之间的转换条件和触发动作列出来,后面编码其实就是填空。
另一个深刻的体会是:别把系统设计得太重。很多毕设同学一开始就想着加权限管理、加消息推送、加日志系统,结果主流程还没走通,就被这些附加功能拖住了。建议第一版只做核心的“找房-下单-支付-入住-退房”闭环,把边界和异常处理做好。跑通以后,再加功能会非常顺,因为地基是稳的。
如果你正在做类似的系统,从这套方案里能拿走的,首先是数据库设计的思路,特别是房态日历表的设计;其次是下单接口的事务处理;再就是前端按状态渲染按钮的模式。这套组合在酒店、民宿、会议室预订这些场景里都能复用。照着做一套,至少能省一个月的试错时间。