☰
酒店管理系统三端联调实战:房态同步、订餐推送与并发订单避坑指南
2026/9/26 5:20:36 网站建设 项目流程

简介:这套开箱即用的酒店管理系统面向酒店运营者、开发人员及信息化管理者,将后台管理、官方网站与微信小程序三大版块整合为一体,覆盖实时房间动态、信息推送、订单管理、房间管理、订餐管理、小程序下单与订餐、微信在线支付及退款等完整业务闭环,可快速部署落地,减少重复开发。资源包共1477个文件,容量51.88MB,其中515个js、193个wxss、186个wxml构成小程序前端与页面交互,191个json、95个ts、51个css支撑后台管理端开发,配合jpg、png等图片素材以及sql、sequelizerc等配置文件,目录结构清晰,便于按模块定位与二次扩展。目前已有59人学习与下载。借助这套系统,读者可以拿到一套多端联动的酒店业务参考实现,既能学习微信支付、退款及前后端消息联动的工程组织方式,也能基于现有模块继续完善房态展示、餐饮预订、通知推送等场景。整体设计采用模块化方式,可灵活配置功能,适合作为毕业设计、实训项目或中小型酒店数字化改造的起点。

1. 开箱即用的酒店管理系统:三个端到底要解决什么才不算白做

“开箱即用的酒店管理系统”这句话听着像免死金牌,但真正把后台、网站、微信小程序三端联调过的人都会认同:房态能不能实时同步、订单能不能回写、订餐推送能不能到客人手机,这三件事串起来,才叫可用。后台管理核心解决实时房间动态和订单,网站承担展示和预订入口,微信小程序面向住客,解决实时信息推送和订餐管理。常见的酒店管理系统毕业设计,或者小酒店自用,都会卡在同一个地方:三个端各自能跑,但数据对不上。这篇就讲一条三端协同的落地路线:架构选型如何避免各自为政,核心表结构和最小推送怎么设计,部署联调要调哪些参数,以及上线前会遇到的坑。适合想做一套能长期跑的智慧酒店管理系统的开发者照做。

2. 三端架构选型:为什么是Vue3、Spring Boot、uni-app,而不是各写一套

2.1 技术选型对比:管理后台/网站/小程序怎么配

做管理系统第一原则是别给自己造三份轮子。vue3后台管理系统在社区里已经很成熟,配合 Element Plus 做表格、表单、权限路由都有现成方案,这是后台管理端的首选。网站端如果是给酒店做门面和预订入口,Vue3 单页动态路由即可;如果希望房型和活动能被搜索引擎收录,考虑 Nuxt3 做 SSR。小程序端我建议直接用 uni-app,它用 Vue3 语法写一套,可以同时编译到微信小程序、支付宝小程序和 H5。别小看这个,很多酒店店主今天说只要微信端,隔几个月又要上抖音小程序,uni-app 至少能让你少改一半页面。

端推荐理由备选
后台管理Vue3 + Element Plus表单、权限、表格组件现成,招聘好找人React + Antd
网站端Vue3 单页无 SEO 压力时最简单;有 SEO 需求换 Nuxt3Nuxt3
微信小程序uni-app + Vue3一套代码多端可用,维护成本最低原生小程序
后端 APISpring Boot单体内聚,JPA/MyBatis 都能接Django Channels(熟 Python 可选)

这里我刻意没推荐微服务。两三年的小体量系统,单体后端完全够用,拆微服务只会让你多维护一套注册中心,跟“开箱即用”背道而驰。后端我用 Spring Boot,网络请求统一 JSON,三个端共用一套 Swagger 接口文档。如果团队更熟 Python,用 Django 加 Channels 实现 WebSocket 也可以,后端的启动命令会换成 uvicorn,但表结构和推送消息体的设计思路是一样的。

2.2 数据库设计:五张基础表支撑实时房态和订单

标题里所有的功能,落到数据库其实就五张表:房间、房型、订单、订餐、消息推送记录。房间表记录“实时房间动态”的状态位,订单表记录整个入住流程,订餐表挂订单 ID 做子单,消息推送表记录推给谁、成没成功。先看房间表的结构,这是三端一致性的起点,我给一个最常用的字段版本:

CREATE TABLE `room` ( `id` int NOT NULL AUTO_INCREMENT, `room_no` varchar(10) NOT NULL COMMENT '房号', `type_id` int NOT NULL COMMENT '关联房型表', `status` tinyint NOT NULL DEFAULT '1' COMMENT '1可用 2已入住 3脏房 4维修', `current_order_id` int DEFAULT NULL COMMENT '进行中的订单id', `update_time` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

用数字状态而不是字符串,是因为三端判断一个状态要跨语言,数字的传输和比较成本最低。房型和菜品两个维度也照这个思路拆表,这里不展开,但记住一条铁律:任何一张表里的状态字段,都要形成状态机。比如房间状态机是“可用 → 已入住 → 脏房 → 可用”,中间不能跳过。脏房和维修房不允许直接变已入住,前端房态图的按钮也只能按状态机点亮,否则房态数据迟早乱掉。

订单表的结构需要兼顾订单管理和订餐管理。主订单放入住信息,订餐作为子表挂订单 ID,这样“订餐管理”只需要查主订单加餐品列表,不用在多个表之间疯狂 join。状态字段同样用数字,后面第 3 章会专门展开。

2.3 后端工程拆分:一套 API 如何同时服务三个端

后端工程不要按“后台接口 / 网站接口 / 小程序接口”拆模块,要按业务域拆。我的分包习惯是 controller 包放三个端公用的 REST 入口,然后按模块分:room、order、food、message、user。三端的差异只在鉴权方式和页面路由,数据结构和接口地址完全一致。比如房间状态接口GET /api/room/list?status=1,后台管理端和小程序端都调这个,只是后台拿到全量房型状态,小程序只返回可用房间。这样改一处逻辑,三个端同时生效。

消息推送的接口也走统一设计。后端用 Spring 的@ServerEndpoint暴露 WebSocket 端点,连接里带 userId,前端建立长连接后,后台改房态时调一次HotelWsEndpoint.sendToAll(...),所有连接都能收到。虽然“实时信息推送”听着高级,但落地时其实就两个环节:谁产生变更、谁消费变更。后台改状态是生产者,小程序和网站是消费者;订单管理和订餐管理的消息同理。把生产者封装成 MessageService,所有业务模块都调它,而不是直接在业务代码里sendText,后面排查消息丢失时会省很多事。

2.4 权限边界:后台、网站、小程序各自能做什么

许多主流酒店管理系统翻车都在权限上。后台管理端必须拆角色:管理员能改房价、能看营收报表;前台员工只能办入住、退房、点确认;厨师只能看到订餐单。网站端面向游客,只做客房展示和价格查询,预订跳小程序;微信小程序面向住客,能下单、查房态、收推送,但不能看其他客房数据。三个端共用同一套用户体系,后台和小程序注册的是同一张用户表,角色字段区分前台 / 管理员 / 住客。权限控制放在后端做,不写在页面里。很多人只在菜单上隐藏按钮,结果一个访客拿 POST 请求照样下单,后端再做校验就晚了。

3. 核心功能最小实现:实时房态、信息推送、订单与订餐怎么做

3.1 房间状态控制:数据库状态机与前端房态图

在房间表基础上,后台管理端最常用的是房态图:横轴房型、纵轴楼层,每个格子显示房号和状态颜色。实现上,前端调用GET /api/room/list拿到房间数组,然后根据status字段映射颜色:可用映射绿色,已入住红色,脏房黄色,维修灰色。小程序端只展示可用房,因此加一个参数?status=1过滤。

房态变更的接口要遵循状态机。比如入住动作的请求只有四种合法流转:可用 → 已入住、脏房 → 可用、维修 → 可用、可用 → 脏房。其他组合直接返回状态码 400。状态机在代码里写会显得多,但三端都会按状态机点按钮,反而省了联调成本。

@PutMapping("/api/room/status") public Result changeRoomStatus(@RequestBody RoomStatusChangeDTO dto) { // 校验状态机:dto.fromStatus -> dto.toStatus if (!RoomStateMachine.canTransit(dto.getFromStatus(), dto.getToStatus())) { return Result.fail("非法状态流转"); } int rows = roomMapper.updateStatusWithCheck(dto.getRoomId(), dto.getFromStatus(), dto.getToStatus()); if (rows > 0) { HotelWsEndpoint.sendToAll(buildRoomChangeMessage(dto)); return Result.ok(); } return Result.fail("房间状态已变更,请刷新重试"); }

这里updateStatusWithCheck的 SQL 必须带where status = ?条件,防止两个人同时操作同一个房间导致覆盖。这个写法是乐观锁的一种形式,后面第 5 章并发下单部分还会再讲。前端收到返回失败时,不要静默吞掉,要在房态图把目标房间拉成最新状态并提示“房间状态已变化”。

3.2 实时信息推送:用 WebSocket 把变化推到网站和小程序

WebSocket 端点使用@ServerEndpoint("/ws/hotel/{userId}"),登录之后把 userId 传进来。最小实现的核心代码如下:打开连接后放入 Map,关闭时移除。这里我故意用ConcurrentHashMap而不是静态集合,是为了按 userId 精准推送,避免把一个门店的房态广播给所有门店。

@ServerEndpoint("/ws/hotel/{userId}") @Component public class HotelWsEndpoint { private static ConcurrentMap<String, Session> sessionMap = new ConcurrentHashMap<>(); @OnOpen public void onOpen(Session session, @PathParam("userId") String userId) { sessionMap.put(userId, session); } @OnClose public void onClose(@PathParam("userId") String userId) { sessionMap.remove(userId); } @OnError public void onError(Throwable error) { // 小程序断线太常见,别让错误中断循环 error.printStackTrace(); } public static void sendToUser(String userId, String message) { Session session = sessionMap.get(userId); if (session != null && session.isOpen()) { session.getAsyncRemote().sendText(message); } } }

注意,如果项目部署了多个后端副本,sessionMap就是各副本本地数据,无法跨实例推送。单体部署没问题,多副本要换成 Redis 发布订阅,把消息先发到 Redis,再由每个实例推给各自持有的连接。起步阶段不用碰这个,知道边界就行。

前端是小程序端的 uni-app 写法。微信小程序的 WebSocket 不是常驻的,退到后台会被系统杀掉,所以不能在onLoad里只创建一次连接就完事,需要配合onShow重连,后面避坑章会具体说。

const connectHotelSocket = (userId) => { const socketTask = uni.connectSocket({ url: `wss://api.example.com/ws/hotel/${userId}`, success: () => {} }); socketTask.onMessage((res) => { const data = JSON.parse(res.data); if (data.type === 'room_status_change') { // 收到房态变更,立即刷新当前页面的房间列表 fetchRoomList(); } else if (data.type === 'order_push') { uni.showToast({ title: data.message, icon: 'none' }); } }); return socketTask; };

推送消息体建议统一成 JSON 结构,至少包含msgId、type、roomId、status、updateTime。msgId 用于前端去重和消息回执,没有它,断线重连后收到的历史消息会把房态图打乱。小程序里域名必须是后台配置过的合法域名,否则开发工具里会报协议错误。API 前缀拆到环境变量里,本地联调指向局域网 IP,线上换成服务器的 WSS 地址,别在代码里写死。

3.3 订单与订餐管理:最小表结构、乐观锁与状态流转

订单主表和订餐子表的结构前面章节给过基础表,这里重点讲状态。订单状态用数字存:0 待支付、1 已确认、2 已入住、3 已退房、4 已取消。订餐子表挂在订单 ID 下面,状态在父订单里一起控制,不单独建状态。这样订餐管理模块只需查主订单加上餐品列表,不用在多个表之间疯狂 join。

生成订单是个经典并发场景:两个客人同时选中同一间房,小程序端两个请求都通过了,后台落单时就会打架。我的解决办法是给订单表加version乐观锁字段,生成订单时读当前 version,更新时带上。

UPDATE hotel_order SET status = 1, version = version + 1 WHERE id = #{orderId} AND version = #{oldVersion} AND status = 0;

这条 SQL 影响行数为 0 时,说明有人改过这条订单,当前操作直接报错。配合房间表的where status = ?更新,虽然写起来啰嗦,但能保证一间房同时只能被一个订单占用。订餐管理也是一样的套路:送餐状态变更时带 version,厨师和前台同时点“出餐完成”时不会出现状态反复跳。

订餐管理还有个时间段概念。早餐、午餐、晚餐不是简单的前端选三段,后端要按当前时间自动判断,防止客人 23 点在预订午餐。我在后端用LocalDateTime判断餐段,前端的时钟只做展示,不参与业务判定。餐段判断的边界值建议用枚举或者配置表,别硬编码在代码里,不然 10 点半到底算早餐还是午餐,每次改逻辑都要重新发布。接下来第 5 章会把餐段时间错乱的坑展开。

4. 从本地到三端联调:部署顺序和配置清单

4.1 开发环境准备与版本选择

开始前先统一版本,避免环境问题。后端如果选 Spring Boot,建议 JDK 17,Maven 3.8+;数据库用 MySQL 8.0+,因为 utf8mb4 支持好,JSON 字段也方便。管理后台和网站用 Node 18+,小程序用 HBuilderX 配合微信开发者工具。我习惯把 MySQL 装在本地 Docker 里,避免本机装了太多服务互相抢端口。

docker run -d --name hotel-mysql \ -p 3306:3306 \ -e MYSQL_ROOT_PASSWORD=root123 \ -e MYSQL_DATABASE=hotel \ mysql:8.0 --default-time-zone=+8:00

这里的--default-time-zone=+8:00是给时区坑打预防针,后面 5.4 会专门讲。装完之后,把三个前端的 Node 依赖都装好,后端导入表结构,然后开始本地联调。

4.2 部署顺序:先接口、再后台、最后小程序

联调顺序一定是从后往前:先确认后端接口能跑,再让后台管理端和网站端读取接口,最后才是小程序。因为小程序端出问题时,接口错误和前端错误最难分清。后端打包启动:

mvn clean package -DskipTests java -jar target/hotel-server.jar --spring.profiles.active=prod

启动后先测健康接口:curl http://localhost:8080/api/health。返回{"status":"UP"}再往后走。接着启动后台管理端,把 API 地址指到本地,登录进去改一个房间状态,看房态图有没有变化。网站端同理。最后打开微信开发者工具,导入小程序工程,在“详情 -> 本地设置”里勾选“不校验合法域名”。这一步只用于本地调试,真机预览和上线前记得取消勾选,并在小程序后台配置合法域名。

4.3 三端联调必须确认的配置项

三端联调时,错误集中在配置不一致。我通常列一张配置表格,防止改了后台忘了小程序。

配置项本地生产备注
API 基础地址http://192.168.x.x:8080/apihttps://api.example.com/api小程序强制 HTTPS
WebSocket 地址ws://192.168.x.x:8080/ws/hotel/wss://api.example.com/ws/hotel/生产必须 WSS
图片域名localhoststatic.example.com小程序需要授权
小程序 AppID测试号正式 AppID换 AppID 后要重新编译

前端的配置文件建议用环境变量。比如在 uni-app 中,config/env.js里导出一个对象:

const ENV = { API_BASE: location.protocol === 'https:' ? 'https://api.example.com/api' : 'http://localhost:8080/api', WS_BASE: location.protocol === 'https:' ? 'wss://api.example.com/ws/hotel/' : 'ws://localhost:8080/ws/hotel/', APP_ID: 'wx1234567890abcdef' }; export default ENV;

然后在请求封装中统一引入:

const request = (url, data) => { return new Promise((resolve, reject) => { uni.request({ url: `${ENV.API_BASE}${url}`, data: data, header: { 'Authorization': uni.getStorageSync('token') }, success: (res) => resolve(res.data), fail: (err) => reject(err) }); }); };

封装的好处:三端后续如果要把 token 换成 refresh token,或统一错误弹窗,只要改一处。记住“微信小程序 请求封装”不要为了省事在每个页面裸调uni.request,不然后面联调时你会痛苦到怀疑人生。

4.4 本地 HTTP 调试与线上 WSS 联调的差别

本地联调很顺利,上了服务器突然连不上,这是最常见的三端联调事故。浏览器和小程序端本地测试时可以使用ws://,但生产环境必须用wss://,否则小程序在真机上直接拒绝连接。如果 API 域名和 WebSocket 域名不是同一个,还要在 Nginx 里单独配置/ws/的反代路径。

server { listen 443 ssl; server_name api.example.com; location /api/ { proxy_pass http://127.0.0.1:8080; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; } location /ws/ { proxy_pass http://127.0.0.1:8080/ws/; proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection "upgrade"; proxy_read_timeout 300s; } }

这个 Nginx 块里最关键的是Upgrade和Connection两个头。没有它们,Nginx 会把 WebSocket 当成普通 HTTP 请求发给后端,后端返回握手失败,小程序端报connection timeout。本地联调没有 Nginx,所以这个坑只在线上出现,后面避坑章会再强调一次。

5. 三端联调避坑指南:这些坑不处理上线就翻车

5.1 小程序长时间挂断后收不到推送

现象:客人把小程序切到后台半小时,再回来,房态图还是退出前的样子,房间明明已经被前台改成了已入住。

原因:微信小程序的 WebSocket 在应用切后台超过几秒或者网络切换时,会被系统主动断开。前端只创建了一次连接,没有监听onClose事件,也没有做断线重连。

解决:在onShow里检查连接状态,断开就重建;onClose里启动重连定时器,退避延迟从 2 秒起逐步增长,重连成功后再清掉。

let socketTask = null; let retry = 0; const connectSocket = (userId) => { socketTask = uni.connectSocket({ url: `wss://api.example.com/ws/hotel/${userId}`, success: () => { retry = 0; } }); socketTask.onClose(() => { if (retry < 10) { retry++; const delay = Math.min(2 ** retry, 30) * 1000; setTimeout(() => connectSocket(userId), delay); } }); return socketTask; };

这里的退避算法用的是指数退避,2 秒、4 秒、8 秒、16 秒,最高 30 秒。重连超过 10 次就停,等下一次onShow再触发。重点不是重连本身,而是重连成功后要把未读消息补拉一遍,否则断线期间的房态变化就丢了。

5.2 后台改房态小程序没刷新

现象:后台把房间从脏房改成可用,小程序上还是黄块,怎么下拉刷新都不变。

原因:不是后端没推送,是前端把房间列表缓存在全局数据里,收到推送后只更新房态图的一个格子,但页面重新进入时又从缓存读老数据。或者后端推送时用广播发给了所有连接,前端的消息解析没有匹配到当前门店。

解决:推送消息里带roomId和status,前端收到后更新本地缓存并触发对应列表接口重新拉一次。后端推送时,按门店或 userId 过滤目标连接,不要把每个门店的房态广播给所有连接。过滤条件我一般放在@ServerEndpoint的 userId 里,userId 前缀带门店 ID,后端解析前缀做定向发送。

5.3 同一间房被并发下单:乐观锁来兜底

现象:客人 A 和客人 B 同时从小程序选同一间房,两张订单都出现在后台,付款时发现一间房被重复锁住。

原因:下单逻辑分两步,先生成订单再更新房间状态,中间没有锁;两个请求同时读到房间 status=1,各自认为房间可用。

解决:下单接口要在一个事务里执行,更新房间的 SQL 带上status=1条件,影响行数等于 0 就回滚,订单提示“房间已被预订”。订单表再加 version 字段做乐观锁,防止取消后又更新到旧版本。

@Transactional public Order createOrder(CreateOrderDTO dto) { int updated = roomMapper.updateStatusWithCheck(dto.getRoomId(), 1, 2); if (updated == 0) { throw new BusinessException("房间状态已变化,请重新选择"); } orderMapper.insert(dto.toOrder()); return orderMapper.selectLast(); }

这里的updateStatusWithCheck对应的 SQL 就是update room set status = 2 where id = ? and status = 1,影响行数必须等于 1。如果大于 1,说明数据本身有问题,要检查房间表是否有重复数据。

5.4 订餐餐段时间错乱:时区和时间类型

现象:大堂的 iPad 显示午餐订餐时间到 11 点截止,前台在 11 点 05 分提交的午餐订单,后端却默认成了早餐。

原因:数据库和服务器的时区不一致,MySQL 连接的参数没加serverTimezone=Asia/Shanghai;代码里用了new Date()和前端传的字符串直接比较,前端传的是“2025-06-01 10:30”,后端按 UTC 解析,时间直接差 8 小时。

解决:MySQL 连接参数显式配置serverTimezone=Asia/Shanghai&useUnicode=true&characterEncoding=utf8。代码里时间字段统一用LocalDateTime,前端传时间时全部用yyyy-MM-dd HH:mm:ss,不要传时间戳。订餐的餐段判断写成一个独立方法,用后端当前时间取餐段,前端只做展示。

5.5 WSS 线上连接失败:Nginx 代理没配置升级头

现象:本地用ws://可以连上,部署到服务器用wss://,小程序一连接就报Error: connection timeout。

原因:请求先到 Nginx,Nginx 默认没有把 HTTP 升级到 WebSocket 的多协议头,后端进程收到握手请求但缺少升级头,直接拒绝。

解决:在 Nginx server 块里加一个专门的/ws/location,配置Upgrade和Connection头,并调长proxy_read_timeout。代码在 4.4 里已经给出,这里补充一个细节:proxy_pass的路径要写http://127.0.0.1:8080/ws/,末尾的/不能丢,丢了会多一层路径拼接。改完 Nginx 记得nginx -t再 reload。

6. 进阶:推送可靠性与并发下单的验证技巧,以及我的验收习惯

推送可靠性不能只靠 WebSocket 一条路走到黑。我的做法是给每条推送加msgId,前端收到后调POST /api/message/ack回执给后端;后端超过 30 秒没收到回执,就转存到未读消息表,在小程序下次拉起时通过GET /api/message/unread拉取。这样断线期间错过的一切消息,重连后都能补回来。

比如房态推送的 JSON 消息,我通常约定为:

{ "msgId": "uuid-room-123", "type": "room_status_change", "roomId": 12, "status": 2, "updateTime": "2025-06-01 10:00:00" }

订餐管理也是一样的套路:推送菜品变化后,前端必须在收到后调用 ack 接口;如果超过一分钟没有 ack,后端就把订单状态同步到未读消息表,避免厨师出餐了客人还不知道。这个流程看似多写了一张表,实际上排查订单问题时,翻推送记录表就能定位到底是前端没收到,还是后端没发出。

并发下单这块,除了乐观锁,我还有一个习惯:小程序端在用户点击提交时,把房间的当前status一起带到后端;后端校验status == 1后再走事务更新。上线前我会用一个脚本并发创建 20 个订单到同一间房,检查成功订单数必须等于 1。这个脚本不是负载测试,就是专门验证那条关键 SQL 上的乐观锁有没有生效。

做了这么多年酒店管理系统,我最大的教训就是“别信本地点两下通过就算功能完成”。三端系统只要有一端没验证完整的消息循环和并发场景,开业第一天就会被客人教做人。所以我现在的习惯是:每次改完房态或订单逻辑,强制自己模拟一遍“住客小程序下单、前台后台确认、厨师订餐出餐、客人收到推送”的完整闭环,再顺手看看推送记录表里的消息有没有丢。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询