酒店客房预订管理系统开发实战:uniapp小程序与PHP/Python后端设计

发布时间:2026/10/11 18:24:46
酒店客房预订管理系统开发实战:uniapp小程序与PHP/Python后端设计
做酒店客房预订这类管理系统很多人的第一反应是“不就是几个列表页面加一张订单表吗”。等你真的把需求聊完、代码写起来才会发现日期边界、超卖控制、支付回调、角色权限这些细节每一个都能让你调试到半夜。这篇文章要聊的就是一个实际跑过的项目前端用 uniapp 编译成微信小程序后端在 PHP 和 Python 两套方案里选了一条做成一套小型的酒店客房预订管理系统。我会把整体设计、数据库表、核心接口、并发防超卖、微信支付流程、管理排房和上线避坑整个链路讲清楚适合正在做毕业设计、想给自家客栈做信息化、或者打算从零搭一套前后端分离项目的人参考。这类的需求在小型酒店里非常典型前台既要接电话又要回微信旺季的时候经常出现同一间房被口头订两次或者客人到店了才发现房间还没打扫。系统能解决的问题很明确——把“房态”这件事从手写登记本里搬出来让用户能在线看房、下单、支付让管理者能一目了然看到哪晚还有房、订单走到哪一步、这周营收是多少。下面我按实际开发的顺序把整个项目拆开讲。1. 项目概述与需求拆解1.1 小型酒店管理中的真实痛点先说业务场景。某旅游城市景区周边有一家 20 来间客房的小型民宿旺季订单主要来自电话和微信群前台每天要做的事包括接电话记房客信息、翻开手写登记本查房态、用 Excel 做简单的排房表、到了晚上再手动对账。这套流程在房间少的时候勉强能用一旦满房或者有客人改期问题马上暴露出来。第一个痛点是超卖。两家客人同时电话咨询同一间房前台口头答应了其中一家转身忘了登记第二家下单时又给卖了出去。到了入住当天两拨人同时到店场面相当尴尬。第二个痛点是房态信息不同步。纸质登记本上划掉又重新写很容易漏掉改期记录交接班的时候新人看不明白上一班留下的标记只能一个个打电话去问客人。第三个痛点是客户资产沉淀不下来。订单都散落在微信聊天记录里客人下次再来还是陌生人没办法做会员维护也没法统计回头客比例。所以我做这个系统的第一个判断是核心不是把界面做得多花哨而是把“房态”和“订单状态”这两个模型梳理清楚。只要这两件事准确后面所有功能都是水到渠成。1.2 为什么选 uniapp 微信小程序 PHP/Python技术选型上这个项目有两处让很多人纠结的地方前端为什么要用 uniapp 而不是直接写原生小程序后端为什么要在 PHP 和 Python 里二选一而不是用 Node 或 Java。前端用 uniapp 的理由很实际。它是基于 Vue 语法的跨端框架写一套代码可以编译到微信小程序、H5、安卓 App 和 iOS App。对一个小型酒店系统来说今天的形态是微信小程序明天客人可能希望有个网页版的预订入口或者管家希望用手机 H5 查看后台报表。如果一开始就用原生小程序写死后面想扩端基本等于重写。而 uniapp 的语法学习成本低会 Vue 就能上手插件市场里日历、支付、图表这些组件也比较齐全可以少写很多重复代码。后端选择 PHP 或 Python核心考量是“小团队、低成本、够用就好”。小型酒店的业务复杂度远没有到需要微服务的程度核心就是两个领域对象房间和订单。PHP 配合 Nginx 部署非常省心不用常驻进程随便一个虚拟主机就能跑Python 则胜在开发效率高Flask 或 FastAPI 写起接口来很顺手各种微信生态的 SDK 也都有现成封装。我在实际项目里是把接口层设计成语言无关的两边都留了实现方案下面的代码示例也会分别给出。你不要太纠结于选型把业务模型吃透换成哪种语言都是那几张表、那几条 SQL。1.3 功能边界与角色权限项目定下两个角色普通用户和管理员。用户身份共用一张用户表用一个 role 字段区分。普通用户登录后看到的是房型列表、日历房态、订单中心管理员登录后多出管理端入口能维护房型、处理订单、看排房总览和营收统计。功能模块用户端管理端说明房型浏览支持支持用户看价格和图片管理员可编辑日历房态支持支持用户判断哪天有房管理员排房下单预订支持不支持普通用户核心操作微信支付支持不支持用户端发起小程序支付订单管理查看/取消确认入住/退房角色权限需要严格区分营收统计不支持支持按日和按月汇总这里要强调一下管理端功能千万不要和用户端混在一个页面里展示。小型项目经常图省事把管理入口塞在个人中心里上线审核时很容易因为这导致“功能与类目不符”被驳回来。我建议所有管理页面用独立目录管理后端接口也统一做角色校验普通用户即使猜到了接口地址也无法调用。2. 技术选型、工程结构与数据库设计2.1 前端工程搭建HBuilderX uniapp前端工程我用 HBuilderX 创建选默认模板就行不需要勾选太多插件。创建完之后第一步是打开manifest.json在“微信小程序配置”里填上自己的 appid。如果只是本地调试可以用测试号但涉及微信登录和支付最终还是要注册一个小程序账号拿正式 appid。目录结构上我习惯把页面和逻辑分开pages目录放页面用户端和管理端再分子目录utils目录放请求封装、日期工具、常量定义api目录集中管理所有接口调用static目录放图片等静态资源页面的通信都走统一封装的 request 方法。我封装了一个带 token、错误码处理、401 跳转的请求模块代码不长但非常关键// utils/request.js const BASE_URL https://api.example.com/api; // 换成你的实际域名 export function request(path, options {}) { return new Promise((resolve, reject) { uni.request({ url: BASE_URL path, method: options.method || GET, data: options.data || {}, header: { Content-Type: application/json, Authorization: uni.getStorageSync(token) || }, success: (res) { if (res.data.code 0) { resolve(res.data.data); } else if (res.statusCode 401) { uni.removeStorageSync(token); uni.navigateTo({ url: /pages/login/login }); reject(res.data); } else { uni.showToast({ title: res.data.msg || 请求失败, icon: none }); reject(res.data); } }, fail: (err) { uni.showToast({ title: 网络异常, icon: none }); reject(err); } }); }); }页面里调用就很简单比如获取房型列表const list await request(/roomTypes, { method: GET });2.2 后端接口约定与鉴权方案后端我坚持一个约定所有接口统一返回{ code, msg, data }三层结构。code 为 0 表示成功非 0 表示业务错误msg 是给前端提示的信息data 是实际数据。这个约定虽然简单但能让前端逻辑非常清爽不用每种接口各写一套错误处理。鉴权方案用的是 token不是 session。小程序的请求天然需要自定义 headertoken 无状态、可扩展将来如果接入 H5 端或者 App 端同一个 token 机制可以直接复用。具体流程是小程序调用wx.login拿到 code后端拿 code 去微信接口换 openid查用户表如果不存在就自动注册然后生成一个 token 返回给前端。前端把 token 存在 storage 里后续每个请求都带上。Python FastAPI 版本的中间件思路大致是这样# main.py (Python FastAPI 示例) from fastapi import FastAPI, Depends, Header app FastAPI() def check_token(authorization: str Header(...)): # 从数据库或缓存中查 token返回 user_id 和 role if not authorization or not token_valid(authorization): raise HTTPException(status_code401, detail未登录或登录过期) return get_user_by_token(authorization) app.get(/api/roomTypes) def room_types(userDepends(check_token)): return {code: 0, data: []}PHP 版本我喜欢用原生 PDO 自定义中间件不额外引框架因为项目体量小框架带来的路由和类加载机制反而是负担。但如果你接手维护用 ThinkPHP 或 Laravel 也完全没问题核心 SQL 是一样的。2.3 数据库设计6张核心表定乾坤数据库设计是这类系统的重中之重。我用了 6 张表其中 2 张是辅助表千万别小看这些表结构订单状态和房态计算全依赖它们。表名核心字段作用usersid, openid, nickname, role, phone, create_time用户和角色room_typesid, name, price, deposit, cover, description, max_people, area房型定义roomsid, type_id, room_no, floor具体物理房间ordersid, order_no, user_id, room_type_id, room_id, check_in_date, check_out_date, nights, total_price, status, contact_name, contact_phone, pay_time, create_time订单主表room_pricesid, room_type_id, date, price特殊日期价格调整order_logsid, order_id, action, remark, create_time状态变更日志几个细节我要单独说。一是日期字段。check_in_date和check_out_date必须用 date 类型而不是字符串。很多人图省事用 varchar 存2026-07-01结果跨年的时候排序和比较就全乱了。二是订单状态字段我用数字常量0 待支付、1 已支付/待入住、2 已入住、3 已完成、4 已取消、5 已退款。把“已取消”和“已退款”区分开是为了统计营收时能准确排除掉退款订单不至于把退了钱的也算进营业额。三是索引。订单表要建联合索引(room_type_id, check_in_date, check_out_date)房态查询都是按这个条件扫的order_no要建唯一索引防止并发生成重复订单号。2.4 订单状态机的设计细节写接口之前一定要先把状态流转图画出来。这个系统里订单的状态流转主要是下面这几条路径待支付 - 已支付用户完成微信支付回调更新状态待支付 - 已取消用户主动取消或者定时任务超时自动取消已支付 - 已入住管理员确认客人办理入住已支付 - 已退款用户申请取消管理员同意退款已入住 - 已完成客人退房订单结束当前状态可执行动作目标状态备注0 待支付支付成功1 已支付微信回调触发0 待支付主动取消4 已取消用户操作0 待支付超时未支付4 已取消定时任务1 已支付确认入住2 已入住管理员操作1 已支付申请取消5 已退款需扣费规则2 已入住退房3 已完成管理员操作这个状态机一旦确定后端每个接口都严格按照它来写绝对不允许出现“随便改状态”的黑洞。我在开发时踩过一个坑测试阶段为了方便写了一个“一键把订单改成已完成”的临时接口后来忘删导致一批还没入住的订单被误标成完成统计报表直接错了。所以状态变更必须收敛到几个固定的服务方法里前台页面只能调这些方法不要每个页面自己写 SQL 改状态。3. 核心功能实现从房态日历到支付闭环3.1 房态日历日期重叠判定与剩余房间数房态日历是整个系统里最容易出错的地方。先说约定check_in_date是入住日check_out_date是离店日。比如客人 6 月 1 日入住、6 月 3 日离店那么他占用的是 6 月 1 日和 6 月 2 日这两晚6 月 3 日房间必须空出来给下一位客人。用代码表达就是判断某个日期 d 是否被订单占用条件是check_in_date d AND check_out_date d。很多人会把第二个条件写成了结果离店日那天也算被占用白白少卖一晚。计算某房型某天剩余房间数的 SQL核心就一句SELECT COUNT(*) FROM orders WHERE room_type_id :typeId AND status IN (0, 1, 2) AND check_in_date :date AND check_out_date :date然后拿这个房型的总房间数减去占用数就是当晚可售间数。这里有一个需要想清楚的决策待支付状态的订单要不要算作占用我的做法是算并且配合 5 分钟超时自动取消。原因很简单如果待支付不算占用用户下单后一直不付款房间明明被“口头占了”但系统显示有房其他人下单后到店就会冲突如果算占用又必须保证超时能释放否则恶意下单会让房态全红。PHP 里查询一个月每天的剩余房间数我写了一个循环// PHP PDO 示例 function getAvailableRoomsByMonth(PDO $pdo, int $roomTypeId, string $yearMonth): array { $roomCountStmt $pdo-prepare(SELECT COUNT(*) FROM rooms WHERE type_id ?); $roomCountStmt-execute([$roomTypeId]); $totalRooms (int)$roomCountStmt-fetchColumn(); $occupancyStmt $pdo-prepare( SELECT COUNT(*) FROM orders WHERE room_type_id ? AND status IN (0,1,2) AND check_in_date ? AND check_out_date ? ); $days []; $start $yearMonth . -01; $daysInMonth cal_days_in_month(CAL_GREGORIAN, (int)substr($yearMonth, 5, 2), (int)substr($yearMonth, 0, 4)); for ($d 1; $d $daysInMonth; $d) { $date $yearMonth . - . str_pad($d, 2, 0, STR_PAD_LEFT); $occupancyStmt-execute([$roomTypeId, $date, $date]); $occupied (int)$occupancyStmt-fetchColumn(); $days[$date] $totalRooms - $occupied; } return $days; }3.2 下单流程与并发防超卖下单接口是并发压力最大的地方。两个用户同时看中了同一晚同一间房如果谁先创建订单谁就锁住这需要事务保证。下单事务里应该做四件事查可用房间数、锁定房型记录防止并发、插入订单、返回订单号。小型项目不需要引入 Redis 分布式锁数据库的行锁和条件更新就已经够用。我推荐用SELECT ... FOR UPDATE的方式给房型记录加锁让同一个房型的下单请求串行化try { $pdo-beginTransaction(); // 锁定房型记录防止同一房型下单并发 $stmt $pdo-prepare(SELECT id FROM room_types WHERE id ? FOR UPDATE); $stmt-execute([$roomTypeId]); // 检查从入住到离店的每个晚上是否都有房 $nightDates getNightDates($checkInDate, $checkOutDate); // 生成日期序列 foreach ($nightDates as $night) { $check $pdo-prepare( SELECT COUNT(*) FROM orders WHERE room_type_id ? AND status IN (0,1,2) AND check_in_date ? AND check_out_date ? ); $check-execute([$roomTypeId, $night, $night]); $occupied (int)$check-fetchColumn(); if ($occupied $totalRooms) { throw new Exception(该日期段房源不足); } } // 生成订单号并插入订单 $orderNo date(YmdHis) . rand(1000, 9999); $insert $pdo-prepare( INSERT INTO orders (order_no, user_id, room_type_id, room_id, check_in_date, check_out_date, nights, total_price, status, contact_name, contact_phone, create_time) VALUES (?, ?, ?, ?, ?, ?, ?, ?, 0, ?, ?, NOW()) ); // 绑定参数并执行... $pdo-commit(); } catch (Exception $e) { $pdo-rollBack(); throw $e; }注意我只能按房型粒度查总余量如果想锁定具体房间号就要进一步查可用的具体 room_id比如rooms表中关联当前日期没有被订单占用的空闲房间。这个在排房管理时会用到用户下单时可以默认随机分配一间同房型下的可用房。这里我要专门提一个细节判断余量必须遍历用户选择的每一个晚上而不是只看入住日一晚。我之前犯过这个错用户下单住两晚我只检查了第一晚有房结果第二晚刚好只剩一间被前面客人占着导致订单生成了但没有连续房间可住。有连续住宿需求时一定要把日期序列拆开逐一判断宁可多循环几次也不能靠感觉。3.3 微信支付对接的关键步骤微信支付是这类小程序绕不开的一环。整体流程是前端wx.login拿 code 换 openid 并登录 - 后端生成预支付单 - 前端wx.requestPayment拉起支付 - 微信服务器回调后端接口更新订单状态。后端统一下单时有几个参数很容易填错金额单位是分不是元out_trade_no必须全局唯一直接用订单号notify_url必须是公网 HTTPS 地址且不能带参数。我用 PHP 写统一下单的骨架// PHP 微信支付 V2 风格统一下单V3 需要 APIv3 签名 $params [ appid $appid, mch_id $mchId, out_trade_no $orderNo, total_fee intval($totalPrice * 100), // 分 body $roomTypeName . 房费, notify_url https://你的域名/pay/notify.php, trade_type JSAPI, openid $openid, ]; // 按商户key生成sign发起请求后拿到prepay_id // 再生成给小程序wx.requestPayment用的paySign支付回调是线上最容易出问题的环节。回调地址必须公网可达很多本地开发时好好的一上线回调就丢失就是因为服务器没开放端口或者域名没备案。收到回调后第一件事是验签第二件事是查订单号第三件事是判断订单当前状态。如果订单已经标记为已支付直接返回成功不要重复处理否则可能造成重复入账或重复释放库存。前端拉起支付的代码const payRes await request(/pay/create, { method: POST, data: { orderId: orderId } }); uni.requestPayment({ provider: wxpay, ...payRes.payParams, // timeStamp、nonceStr、package、signType、paySign success: () { uni.redirectTo({ url: /pages/order/detail?orderId orderId }); }, fail: (err) { uni.showToast({ title: 支付未完成可在订单列表继续支付, icon: none }); } });3.4 定时任务自动取消超时未支付订单因为我的设计方案里“待支付订单也占房”所以必须有一个机制把超时未支付的订单清理掉。在 Linux 服务器上我写了一个 PHP 定时脚本用 crontab 每 5 分钟执行一次// cron_cancel_timeout_orders.php $stmt $pdo-prepare( UPDATE orders SET status 4 WHERE status 0 AND create_time DATE_SUB(NOW(), INTERVAL 5 MINUTE) ); $stmt-execute();如果你用的是 Python 后端也可以直接在应用里内置一个 APScheduler 调度器启动时注册同一个任务。定时脚本要打印日志排查问题时能快速确认跑了没有、改了多少单。这个任务虽然简单但它是“房态正确性”的最后一环漏了它用户占着房不付款后面的真实客人就订不了房。4. 管理端实现排房、订单处理与统计4.1 同一套代码里的权限控制管理端我建议直接在小程序内实现不单独做网页后台这样可以省一套部署。但入口要藏好不要每个用户都能看到“管理后台”这四个字。登录后根据用户 role 字段判断是否展示管理入口路由层面也要拦。后端接口的权限校验是必须的。比如管理端查询营收的接口如果只有判断是否登录、不判断是否为管理员普通用户改一下请求参数就可能调用到。每个涉及管理的接口都要先检查角色# Python FastAPI 示例 def require_admin(userDepends(check_token)): if user[role] ! 1: raise HTTPException(status_code403, detail无管理员权限) return user app.get(/api/admin/statistics) def statistics(adminDepends(require_admin)): return {code: 0, data: getStatistics()}4.2 排房日历总览排房页面是酒店前台每天都要看的东西。我的设计是按月展示一个宫格横向是日期纵向是房型格子里显示剩余间数和具体已占用的房号。后端接口按月返回数据时不能用“每天查一次”的笨办法那样接口响应时间会很慢。更好的方式是一条 SQL 把整个月的占用情况查出来再在内存里组装SELECT room_type_id, check_in_date, COUNT(*) AS cnt FROM orders WHERE status IN (0, 1, 2) AND check_in_date :monthEnd AND check_out_date :monthStart GROUP BY room_type_id, check_in_date这里我特意把条件写宽松一点check_in_date monthEnd且check_out_date monthStart这样跨月订单也能被正确统计出来。前端拿到数据后在宫格里把“剩余 0 间”标成灰色把“剩余充足”标成绿色一眼就能看出哪晚满房。4.3 订单处理与退房操作管理端的主要操作有三个确认入住、办理退房、处理取消退款。确认入住时把订单状态从 1 改成 2同时可以在rooms表里把对应的房间标记为“在住”。退房时把状态从 2 改成 3房间自动释放。这两个操作都很直接但要注意操作日志。我建议每次状态变更都往order_logs表里插一条记录内容包括操作人、动作、备注和时间。前台如果哪天和客人发生纠纷查日志就能还原整个过程。取消退款相对麻烦。已支付订单如果允许用户在入住前取消需要调用微信支付的退款接口。退款接口需要商户证书金额单位同样是分而且退款金额不能超过原订单金额。我在后台做了一个提示退款到账通常不是实时的要给用户展示“退款处理中”的状态等退款结果回调后再把订单状态从 1 改成 5。4.4 营收统计营收统计按两个维度做按日和按房型。按日汇总的 SQL 很简单SELECT DATE_FORMAT(create_time, %Y-%m-%d) AS stat_date, SUM(total_price) AS total_revenue FROM orders WHERE status 3 GROUP BY DATE_FORMAT(create_time, %Y-%m-%d) ORDER BY stat_date DESC;统计口径要注意只统计最终完成的订单status3退款订单 status5 不计入待支付和已取消的都不算。还有一个容易忽略的点跨月订单的收入归到哪一天我采用的约定是归到支付成功的当天也就是pay_time。严格来说这算现金流口径不算入住确认口径但只要口径统一说明清楚就不会有问题。出租率这个指标小型酒店也很关心。计算方式是一段时间内实际售出的间夜数除以房间总数乘以天数。比如酒店有 20 间房7 月 31 天满房间夜数是 620如果实际卖了 465 个间夜出租率就是 75%。这个数据能直接反映酒店的运营状况比单纯的收入数字更有指导意义。5. 部署上线与避坑记录5.1 小程序发布需要准备的四件事上线一个微信小程序前有四件事必须提前准备少一件都可能被卡住。第一是 HTTPS 域名。小程序正式环境不允许请求 HTTP 接口域名必须备案并配置 SSL 证书。开发调试时可以在微信开发者工具里勾选“不校验合法域名”但上线前一定要把request合法域名加到小程序后台的白名单里。第二是后端部署。PHP 项目我通常直接放到 Nginx 站点目录下配置一个重写规则把所有/api/请求路由到入口文件。Python 项目则用 gunicorn 或 uvicorn 启动再用 supervisor 守护进程。不管选哪种都要在服务器防火墙里放行443端口。第三是小程序类目和隐私保护指引。酒店预订属于“旅游服务”下的酒店类目可能需要提供相关资质涉及收集手机号、头像等个人信息小程序后台需要填写用户隐私保护指引并在代码中调用uni.getUserProfile、获取手机号等接口前有明确说明。第四是支付商户号。小程序支付需要申请微信支付商户号并且要和小程序账号做绑定。开发期可以用沙箱或测试密钥但正式上线一定要确认商户号状态正常、结算银行卡信息无误。很多项目倒在最后一步界面全做好了支付流程也能走通结果商户号审核还没下来只能干等。5.2 常见问题排查表把我在开发过程里遇到的高频问题整理成一个表给后来的人省点时间。问题现象常见原因排查方法真机预览请求不到接口小程序后台未配置 request 合法域名在“开发管理-服务器域名”中添加开发期可临时勾选不校验微信登录一直失败code 重复使用或过期每次登录都重新调用wx.login不要缓存 code支付金额不对元转分时用 float 强转用intval($price * 100)避免浮点精度误差支付回调收不到notify_url 带参数或非 HTTPS回调地址不能带 query必须公网 HTTPS 可达支付重复回调回调未做幂等判断先判断订单状态已支付直接返回成功日历房态多算一天日期边界条件写错检查check_out_date d不是满房还能下单并发未加事务和锁下单接口包在事务里用SELECT ... FOR UPDATE锁行订单排序错乱用字符串存时间字段保证用 date/datetime 类型排序按创建时间字段服务器时间差 8 小时时区未设置PHP 设置date_default_timezone_set(Asia/Shanghai)MySQL 连接设置time_zone管理接口被普通用户调用接口只校验登录未校验角色所有管理接口增加管理员角色判断5.3 几个开发心得这类小型系统最容易失控的不是功能数量而是状态和边界。我在项目里反复吃到的教训是写代码前先把状态机画出来把每个状态能做什么、不能做什么固定住后面所有页面和接口都只认这套规则不搞特殊通道。另一个心得是不要轻易换掉通用的日期处理方式。小程序端有一些现成的日历组件但很多第三方组件的日期格式化逻辑和订单系统的“入住日算占用、离店日不算占用”约定不一致容易出现显示上有房、下单时却提示没房的情况。我最终是自己写了一个轻量的日历宫格不复杂但逻辑完全可控。排房页面尤其如此宁可代码多一些也别让组件黑盒干扰业务逻辑。接口层面还有一个建议普通用户接口和管理接口尽量分开路由比如/api/user/*和/api/admin/*鉴权中间件可以设置不同的拦截规则。这样哪怕将来某一天你要把管理端拆成独立的 H5 项目后端也不用大改。最后说点实际的体会做完这个项目我最深的感受是房态和订单的状态边界比任何花哨功能都重要。本地测试的时候我用一个跨月订单把日历搞“花”了检查了很久才发现是在计算占用日期时把等于离店日那天也算了进去导致多占了一晚。后来的经验是先把边界条件定清楚入住日算占用离店日不算占用check_out_date减check_in_date就是间夜数。这个约定一旦统一后面排房、统计、对账都会顺很多。另外一个建议是支付流程一定要在写界面之前先串通。因为支付牵涉商户号、HTTPS 域名、回调地址、证书验签这些环境问题越早暴露越好改。界面做得再漂亮支付环节卡住整个业务闭环就打不通。如果你也在做类似的小型酒店预订系统希望这篇记录能帮你少踩几个我踩过的坑。