简介这套开箱即用的酒店管理系统面向酒店运营者、开发人员及信息化管理者将后台管理、官方网站与微信小程序三大版块整合为一体覆盖实时房间动态、信息推送、订单管理、房间管理、订餐管理、小程序下单与订餐、微信在线支付及退款等完整业务闭环可快速部署落地减少重复开发。资源包共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) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;用数字状态而不是字符串是因为三端判断一个状态要跨语言数字的传输和比较成本最低。房型和菜品两个维度也照这个思路拆表这里不展开但记住一条铁律任何一张表里的状态字段都要形成状态机。比如房间状态机是“可用 → 已入住 → 脏房 → 可用”中间不能跳过。脏房和维修房不允许直接变已入住前端房态图的按钮也只能按状态机点亮否则房态数据迟早乱掉。订单表的结构需要兼顾订单管理和订餐管理。主订单放入住信息订餐作为子表挂订单 ID这样“订餐管理”只需要查主订单加餐品列表不用在多个表之间疯狂 join。状态字段同样用数字后面第 3 章会专门展开。2.3 后端工程拆分一套 API 如何同时服务三个端后端工程不要按“后台接口 / 网站接口 / 小程序接口”拆模块要按业务域拆。我的分包习惯是 controller 包放三个端公用的 REST 入口然后按模块分room、order、food、message、user。三端的差异只在鉴权方式和页面路由数据结构和接口地址完全一致。比如房间状态接口GET /api/room/list?status1后台管理端和小程序端都调这个只是后台拿到全量房型状态小程序只返回可用房间。这样改一处逻辑三个端同时生效。消息推送的接口也走统一设计。后端用 Spring 的ServerEndpoint暴露 WebSocket 端点连接里带 userId前端建立长连接后后台改房态时调一次HotelWsEndpoint.sendToAll(...)所有连接都能收到。虽然“实时信息推送”听着高级但落地时其实就两个环节谁产生变更、谁消费变更。后台改状态是生产者小程序和网站是消费者订单管理和订餐管理的消息同理。把生产者封装成 MessageService所有业务模块都调它而不是直接在业务代码里sendText后面排查消息丢失时会省很多事。2.4 权限边界后台、网站、小程序各自能做什么许多主流酒店管理系统翻车都在权限上。后台管理端必须拆角色管理员能改房价、能看营收报表前台员工只能办入住、退房、点确认厨师只能看到订餐单。网站端面向游客只做客房展示和价格查询预订跳小程序微信小程序面向住客能下单、查房态、收推送但不能看其他客房数据。三个端共用同一套用户体系后台和小程序注册的是同一张用户表角色字段区分前台 / 管理员 / 住客。权限控制放在后端做不写在页面里。很多人只在菜单上隐藏按钮结果一个访客拿 POST 请求照样下单后端再做校验就晚了。3. 核心功能最小实现实时房态、信息推送、订单与订餐怎么做3.1 房间状态控制数据库状态机与前端房态图在房间表基础上后台管理端最常用的是房态图横轴房型、纵轴楼层每个格子显示房号和状态颜色。实现上前端调用GET /api/room/list拿到房间数组然后根据status字段映射颜色可用映射绿色已入住红色脏房黄色维修灰色。小程序端只展示可用房因此加一个参数?status1过滤。房态变更的接口要遵循状态机。比如入住动作的请求只有四种合法流转可用 → 已入住、脏房 → 可用、维修 → 可用、可用 → 脏房。其他组合直接返回状态码 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 ConcurrentMapString, 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 17Maven 3.8数据库用 MySQL 8.0因为 utf8mb4 支持好JSON 字段也方便。管理后台和网站用 Node 18小程序用 HBuilderX 配合微信开发者工具。我习惯把 MySQL 装在本地 Docker 里避免本机装了太多服务互相抢端口。docker run -d --name hotel-mysql \ -p 3306:3306 \ -e MYSQL_ROOT_PASSWORDroot123 \ -e MYSQL_DATABASEhotel \ mysql:8.0 --default-time-zone8:00这里的--default-time-zone8:00是给时区坑打预防针后面 5.4 会专门讲。装完之后把三个前端的 Node 依赖都装好后端导入表结构然后开始本地联调。4.2 部署顺序先接口、再后台、最后小程序联调顺序一定是从后往前先确认后端接口能跑再让后台管理端和网站端读取接口最后才是小程序。因为小程序端出问题时接口错误和前端错误最难分清。后端打包启动mvn clean package -DskipTests java -jar target/hotel-server.jar --spring.profiles.activeprod启动后先测健康接口curl http://localhost:8080/api/health。返回{status:UP}再往后走。接着启动后台管理端把 API 地址指到本地登录进去改一个房间状态看房态图有没有变化。网站端同理。最后打开微信开发者工具导入小程序工程在“详情 - 本地设置”里勾选“不校验合法域名”。这一步只用于本地调试真机预览和上线前记得取消勾选并在小程序后台配置合法域名。4.3 三端联调必须确认的配置项三端联调时错误集中在配置不一致。我通常列一张配置表格防止改了后台忘了小程序。配置项本地生产备注API 基础地址http://192.168.x.x:8080/apihttps://api.example.com/api小程序强制 HTTPSWebSocket 地址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 同时从小程序选同一间房两张订单都出现在后台付款时发现一间房被重复锁住。原因下单逻辑分两步先生成订单再更新房间状态中间没有锁两个请求同时读到房间 status1各自认为房间可用。解决下单接口要在一个事务里执行更新房间的 SQL 带上status1条件影响行数等于 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 连接的参数没加serverTimezoneAsia/Shanghai代码里用了new Date()和前端传的字符串直接比较前端传的是“2025-06-01 10:30”后端按 UTC 解析时间直接差 8 小时。解决MySQL 连接参数显式配置serverTimezoneAsia/ShanghaiuseUnicodetruecharacterEncodingutf8。代码里时间字段统一用LocalDateTime前端传时间时全部用yyyy-MM-dd HH:mm:ss不要传时间戳。订餐的餐段判断写成一个独立方法用后端当前时间取餐段前端只做展示。5.5 WSS 线上连接失败Nginx 代理没配置升级头现象本地用ws://可以连上部署到服务器用wss://小程序一连接就报Error: connection timeout。原因请求先到 NginxNginx 默认没有把 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 上的乐观锁有没有生效。做了这么多年酒店管理系统我最大的教训就是“别信本地点两下通过就算功能完成”。三端系统只要有一端没验证完整的消息循环和并发场景开业第一天就会被客人教做人。所以我现在的习惯是每次改完房态或订单逻辑强制自己模拟一遍“住客小程序下单、前台后台确认、厨师订餐出餐、客人收到推送”的完整闭环再顺手看看推送记录表里的消息有没有丢。希望帮到你。本文还有配套的精品资源点击获取
