简介面向酒店集团及多门店运营场景这套多酒店版管理系统支持无限创建分店覆盖入住管理、预订管理、换房、商品销售等核心业务并提供语音提示、房态实时、图表数据、预警提示、短信营销、会员管理、报表中心、财务管理、设备管理等模块配套APP、小程序、H5可完成酒店预订、点餐、叫服务等店内操作另有内部员工App支撑日常运营。资源压缩包含2582个文件约80.13MB以1428个PHP业务文件为主搭配HTML、JS、CSS/SCSS等前端页面样式、MP3语音播报素材及JPG/GIF图片资源并含SQL数据库脚本、JSON/XML配置及依赖组件便于完整搭建与二次开发。已有393人学习下载适合需要开发多端酒店管理系统、研究前后端架构或有快速部署需求的企业开发者、PHP工程师及产品经理。从预览文件可见内置Bootstrap前端框架、Windows启动脚本及PHP运行库组件可快速在本地环境启动项目按分店、房态、订单、会员等维度拆解前后端交互与业务逻辑适合二次开发与功能扩展。1. 多酒店预订系统APP、H5、小程序三端到底在做什么很多团队第一次接到“多酒店预订系统”时第一反应是把单店的预订网站套壳成 APP再做一个小程序。结果做到一半就发现真正麻烦的不是壳而是多酒店这个前提每家酒店的房型不一样、房价不一样、退改规则不一样甚至支付方式也分区域。APP、H5、小程序只是三个展示窗口背后如果还是单店那套“一个酒店一张房价表”的做法会很快在数据层翻车。这套系统的核心价值在于用同一套业务底座支撑三种前端形态同时让每家酒店能独立管理自己的房态和房价预订订单又能汇到总平台统一处理。适合的场景是连锁酒店集团、区域多店民宿品牌以及做酒店代运营的平台方。本文会从数据模型、三端 API 设计、预订核心链路到部署排错把一套可复现的方案拆开讲清楚。2. 先定数据模型多酒店共享一套系统还是每家一套这个选择题直接决定后面所有开发的难度。常见的错误是一开始图省事给每家酒店单独建一套数据库平台查询时做跨库聚合。短期能用等到要做报表、统计、会员积分、跨店结算时跨库 join 写到你怀疑人生。我更推荐的方式是一套数据库所有业务表带 hotel_id用字段隔离酒店数据用代码保证不串店。2.1 酒店与房型统一建模的最小集合先把“酒店”和“房型”的关系理清。多酒店系统里房型表不能直接挂酒店 ID 就完事因为房型还涉及渠道同样的房型在自家 APP 和小程序里叫“高级大床房”在第三方渠道里叫“精品大床房”价格还不一样。所以房型要在“酒店 房型”的组合上再挂一层“渠道映射”。CREATE TABLE hotel ( id BIGINT PRIMARY KEY, name VARCHAR(128) NOT NULL, region_code VARCHAR(32) NOT NULL, status TINYINT NOT NULL DEFAULT 1, created_at DATETIME NOT NULL ) COMMENT 酒店主表; CREATE TABLE room_type ( id BIGINT PRIMARY KEY, hotel_id BIGINT NOT NULL, name VARCHAR(128) NOT NULL, area INT NULL, max_guests TINYINT NOT NULL DEFAULT 2, bed_type VARCHAR(32) NULL, status TINYINT NOT NULL DEFAULT 1, INDEX idx_hotel (hotel_id) ) COMMENT 房型表物理上冗余 hotel_id;房型表冗余 hotel_id而不是通过关联查出来是为了让所有后续查询都只要一层过滤避免一次预订查询关联酒店表再关联房型表。区域编码 region_code 建议直接存行政区划代码别用自定义编号后面做按城市搜索、按商圈聚合时会省很多事。渠道映射表单独设计CREATE TABLE room_channel_mapping ( id BIGINT PRIMARY KEY, hotel_id BIGINT NOT NULL, room_type_id BIGINT NOT NULL, channel VARCHAR(16) NOT NULL COMMENT APP/H5/WECHAT_MP/OTA, channel_room_name VARCHAR(128) NOT NULL, is_sync_price TINYINT DEFAULT 1, UNIQUE KEY uk_hotel_room_channel (hotel_id, room_type_id, channel) ) COMMENT 房型与渠道名称映射;这里的关键参数是 is_sync_price。如果一家酒店在 OTA 渠道的价格不想同步到自家三端就把这个字段置为 0报价逻辑会自动跳过该渠道的价格推送。我在实际项目中是把渠道名统一成小写枚举APP、H5、小程序各算一个渠道OTA 每家有独立渠道值方便后续分别算佣金和结算。2.2 房价与库存日历表才是预订系统的地基单店系统很多人用“房价表存一个今天的价、一个明天的价”预订时按日期加减。多酒店系统这么做会乱因为每家酒店的调价周期不同有的周末价有的节假日价有的连住优惠用“基础房价 规则”的方式天然和日历冲突。正确做法是落地一张房价日历表库存也落一张日历表日期作为主键维度。CREATE TABLE room_price_calendar ( id BIGINT PRIMARY KEY, hotel_id BIGINT NOT NULL, room_type_id BIGINT NOT NULL, channel VARCHAR(16) NOT NULL, biz_date DATE NOT NULL, price DECIMAL(10,2) NOT NULL, currency VARCHAR(8) NOT NULL DEFAULT CNY, created_at DATETIME NOT NULL, updated_at DATETIME NOT NULL, UNIQUE KEY uk_calendar (hotel_id, room_type_id, channel, biz_date) ) COMMENT 房价日历表每日期一价;价格字段我用 DECIMAL(10,2)不用 FLOAT。酒店价格的精度要求高浮点算总价时容易出现 0.1 0.2 不等于 0.3 的问题虽然最后展示会四舍五入但订单金额一旦错一分钱对账环节非常难受。CREATE TABLE room_stock_calendar ( id BIGINT PRIMARY KEY, hotel_id BIGINT NOT NULL, room_type_id BIGINT NOT NULL, biz_date DATE NOT NULL, total_rooms INT NOT NULL DEFAULT 0, sold_rooms INT NOT NULL DEFAULT 0, sold_updated_at DATETIME NOT NULL, UNIQUE KEY uk_stock (hotel_id, room_type_id, biz_date) ) COMMENT 库存日历表total 固定sold 递增;库存控制里最常见的坑是库存直接存一个剩余数每次下单减一。这样一旦出现并发下单两条请求同时读到剩余 1同时减成 0就超卖了。正确做法是总房量 total_rooms 保持不变sold_rooms 在锁住行的情况下递增判断条件始终是 sold_rooms total_rooms。这样即使并发来数据库行锁会排队不会超卖。这个细节后面避坑章节还会再展开。2.3 订单主表与订单明细先想清退款和改期再建表订单表的设计决定了后续退款、改期、换房好不好做。很多系统只建一张订单表所有的原价、优惠、实付、退款都堆在上面结果一个订单改一次期原来的订单行被改得面目全非。我的习惯是拆成订单主表和订单明细表明细表记录每一次的日期、房型、价格主表只维护当前状态。CREATE TABLE booking_order ( id BIGINT PRIMARY KEY, order_no VARCHAR(32) NOT NULL UNIQUE, hotel_id BIGINT NOT NULL, channel VARCHAR(16) NOT NULL, member_id BIGINT NULL, status TINYINT NOT NULL DEFAULT 0 COMMENT 0待支付 1已支付 2入住中 3已完成 4已取消, total_amount DECIMAL(10,2) NOT NULL, paid_amount DECIMAL(10,2) NOT NULL, refund_amount DECIMAL(10,2) NOT NULL DEFAULT 0, guest_name VARCHAR(64) NOT NULL, guest_phone VARCHAR(32) NOT NULL, created_at DATETIME NOT NULL, updated_at DATETIME NOT NULL, INDEX idx_hotel_date (hotel_id, created_at) ) COMMENT 订单主表; CREATE TABLE booking_order_item ( id BIGINT PRIMARY KEY, order_id BIGINT NOT NULL, room_type_id BIGINT NOT NULL, biz_date DATE NOT NULL, nights TINYINT NOT NULL, price DECIMAL(10,2) NOT NULL, amount DECIMAL(10,2) NOT NULL, INDEX idx_order (order_id) ) COMMENT 订单明细表一个订单多天;订单明细表按 bed_date 拆行还是按一段日期区间存我建议拆行。虽然会多占数据量但查某一天全平台的入住率、占用数时直接按日期聚合明细表即可不需要对区间做日期展开。一次订三晚就三条明细数据量完全扛得住。多酒店系统的报表需求往往很重明细拆得越细后面写统计越轻松。3. 让 APP、H5、小程序共用一套 API分层设计与登录打通三端共用一个后端接口层是控制成本的关键。如果三个前端各配一套接口等于同时维护三套权限、三套登录、三套数据校验项目基本废了。正确做法是 API 层只认渠道参数不认前端框架APP、H5、小程序传同一个 channel 枚举值业务逻辑只按 channel 分支处理差异点。3.1 三端的技术差异与 API 收敛技术选型上APP 我用原生加 WebView 混合的方式H5 是纯移动端网页小程序是原生语法。框架不同不要紧接口层只要保证两点统一返回 JSON 结构、统一鉴权方式。推荐的返回结构是所有接口固定包一层{ code: 0, message: success, data: {} }code 非 0 时data 可以为空message 直接对应用户提示语。这样前端不需要为每个接口写错误解析。真正的差异点只集中在三处登录方式、唤起支付的方式、消息推送的通道。这三处单独设接口其他业务接口全部复用。渠道参数 channel 必须显式传不能靠后端推断。因为 H5 在微信内置浏览器里打开时User-Agent 可能带上微信标识但用户也可能用普通浏览器打开同一个 H5。我在项目里曾吃过这个亏后端试图自动识别渠道结果用户复制链接到浏览器打开后系统误判为微信渠道调不起微信支付订单卡住。显式传 channel 虽然多写一次参数但能杜绝这类玄学问题。3.2 登录与 sessionAPP 扫码登录、H5 短信登录、小程序 wx.login三端登录方式天然不同但登录后的会话体系必须统一。统一方案是后端签发一个 token三端各自拿到 token 后存起来请求时放到 Authorization header。token 用 Redis 存设置滑动过期时间用户在 APP 上登录一次后H5 或者小程序需要重新登录这是正常现象不需要强求单点。APP 端我一般支持密码登录和短信验证码登录额外做一个“扫码登录”能力用户在电脑后台点登录弹出二维码APP 扫码确认后后端把 token 通过 WebSocket 推给电脑端。小程序端则走 wx.login 获取 code后端用 code 换 openid。// 小程序端登录示例 wx.login({ success: async (res) { const code res.code; const resp await fetch(/api/v1/auth/wx_login, { method: POST, headers: { Content-Type: application/json }, body: JSON.stringify({ code, channel: WECHAT_MP }) }); const data await resp.json(); if (data.code 0) { wx.setStorageSync(token, data.data.token); } } });后端收到 code 后调用对应平台的接口换取 openid再查会员表绑定关系。首次登录会自动创建会员账号并把 openid 绑定到该手机号下。这里参数 channel 必须等于 WECHAT_MP因为同一个 code 只能换一次 openid换完再换会报错前端要做好“登录失败重新触发 wx.login”的重试逻辑。H5 端我推荐只做短信验证码登录不要做密码登录。移动端用户在 H5 里输密码的体验很差而且密码找回流程会让开发量膨胀。短信登录的业务要点是后端生成验证码存 Redis5 分钟有效同一手机号 60 秒只能发一次接口做好频控。3.3 用一份接口文档约束三端三端并行开发时接口不一致的坑比想象中多。APP 端传 hotel_id 用数字H5 端传字符串小程序端干脆漏传——这些问题在联调时才暴露相当浪费时间。我的习惯是分三步避免第一步接口文档用 API 描述文件管理工具负责生成。字段类型、是否必填、枚举值都在里面定义清楚。第二步每个接口在服务端做入参校验hotel_id 必须是正整数channel 必须是枚举值不合法直接返回特定错误码。第三步写一个简单的在线接口调试页面三端开发都能看到实时请求日志谁传错参数一目了然。这里要注意的是接口返回的时间字段统一用时间戳还是格式化字符串必须提前定死。我推荐统一用格式化字符串带时区。APP 端如果用了原生日期组件直接显示字符串即可H5 和小程序端格式化也更方便。如果某一端坚持用时间戳时间戳的时区换算又是一个容易踩坑的地方。4. 预订核心链路查房态、锁库存、下单、支付与确认预订链路是系统的命门。用户从三端任何一端进来看到房价、提交订单、完成支付整个过程涉及报价引擎、库存锁定、支付回调、订单状态推进。这个链条任何一环出问题都会直接变成客诉要么用户付了钱酒店说没房要么房间锁了用户没支付成功。4.1 查房态与报价渠道价、会员价、门市价三套价多酒店系统里价格至少有三种门市价酒店挂牌价、渠道价APP、H5、小程序各自的价格、会员价按会员等级打折或直减。报价时优先级从高到低会员价 渠道价 门市价。查询接口设计时一次请求往往要覆盖多天多房型我在项目里用的是按酒店维度拉取 N 天内所有房型的日历价再在内存里组装响应。这样做比前端循环逐日查询效率高很多尤其 H5 端弱网环境下少一次请求体验差别很大。SELECT rp.biz_date, rp.room_type_id, rp.price FROM room_price_calendar rp JOIN room_type rt ON rp.room_type_id rt.id WHERE rp.hotel_id ? AND rp.channel ? AND rp.biz_date BETWEEN ? AND ? AND rt.status 1 ORDER BY rp.biz_date;节日价是另一个常见需求。我一般直接在房价日历表里预生成好而不是在查询时写规则。比如国庆期间价格上浮 30%最好是通过后台批量工具把价格生成好写进日历表。查询时不要做实时计算否则规则越堆越多报价逻辑变成黑匣子排查问题非常痛苦。4.2 锁库存用数据库事务还是 Redis锁库存的争议很多。纯数据库方案简单可靠但高并发时压力大Redis 方案性能好但丢数据时库存会出错。我的做法是以数据库为主Redis 只做查询层的缓存锁库存永远走数据库事务。锁库存的 SQL 核心是原子递增UPDATE room_stock_calendar SET sold_rooms sold_rooms 1, sold_updated_at NOW() WHERE hotel_id ? AND room_type_id ? AND biz_date ? AND sold_rooms total_rooms;如果影响行数为 0说明库存不足直接返回“无房”。这一条 SQL 同时完成了判断和更新不需要先 SELECT 再 UPDATE从机制上避免了并发下的竞态。这里要说明的是查询房价和锁库存之间会有时间差用户看到的房价序号可能到提交时已经没了这是正常情况后端返回明确提示“房间已被预订”即可前端补做推荐附近房型的引导。订单提交接口里下单和锁库存必须在同一事务里。我的做法是接口先开启事务执行库存扣减再插入订单主表和明细表全部成功后提交如果订单插入失败则事务回滚库存扣减也跟着回滚。这样不会出现“库存扣了订单没生成”的状态。4.3 下单、支付与确认订单状态机的推进订单状态的推进最好画成一条直线不要设计太多并行路径。我的状态机是待支付 - 已支付 - 入住中 - 已完成另外从待支付可以走到已取消。已支付状态可以走到已取消退房退款但需要走退款流程。整个状态机里最怕的是支付回调到达顺序和本地订单状态修改顺序不一致。def confirm_payment(order_no, paid_amount): order get_order(order_no) if order.status ORDER_STATUS_PAID: log_warning(重复回调, order_no) return OK if order.status ORDER_STATUS_CANCELLED: log_error(已取消订单收到支付回调, order_no) return FAIL # 更新订单状态 记录流水 update_order_status(order_no, ORDER_STATUS_PAID) insert_payment_log(order_no, paid_amount)支付回调接口必须做好两件事一是幂等重复回调不能重复入账二是金额校验回调金额一定要和订单应付金额比对不一致直接告警。支付平台回调里都会带金额但本地订单总价可能因为优惠计算产生偏差校验不过的订单要进入人工处理队列而不是直接自动确认。支付成功后的“预订确认”动作我放在回调处理里同步做。确认包括给用户发短信和站内消息、给酒店管理员推送一条新的入住订单提醒、更新运营数据。注意不要放在支付成功页面前端跳转里做用户关闭页面导致前端没跳转订单状态就不更新了必须以后端回调为准。5. 部署与联调避坑多酒店场景最容易翻车的五个位置这一章是从我的血泪经验里提炼出来的。前面几章的方案都跑通了但真正上线时出问题的往往是这些看起来不起眼的位置。每条我都按“现象 - 原因 - 解决”的方式梳理你做完遇到同类问题可以直接对照。5.1 超卖并发一上来库存就变负数现象活动日开始后同一房型同一日期出现订单数大于总房量的情况前台和线上同时在卖房线上订单确认了客人到店却没房。原因前台 PMS 系统和线上预订系统用的是两套库存线上扣完了线下不知道或者线上用的是“先查剩余再更新”的非原子操作并发时互相覆盖。解决线上系统锁库存用前面那一条原子 UPDATE与 PMS 的库存同步改成接口实时同步同步失败的酒店在预订接口直接熔断宁可让用户订不了也不能收了钱再没房。另外每天对全量酒店做一次“在售房量 当日线上线下总售卖数”的核对数字对不上立刻告警。5.2 时区与日期边界凌晨下单的日期算错现象凌晨 1 点用户提交订单预订的入住日期被算成前一天或者后一天。原因后端服务器时区用了 UTC数据库连接串没加时区参数而前端传的是中国时区的日期字符串。解决所有日期统一规定传字符串格式 YYYY-MM-DD后端全部按 Asia/Shanghai 处理数据库连接参数里强制设置时区日期计算统一用日期函数不要自己拼字符串偏移。我在项目里加了一个规约任何接口返回值里日期字段不带时间时间字段才带日期。防止 H5 端把“2025-06-01 00:00:00”直接展示成 6 月 1 号零点让用户误以为是当天凌晨。5.3 支付回调乱序或丢失现象订单显示已支付但用户收到银行扣款短信订单状态还是待支付或者反过来订单已确认但支付平台对账发现金额不一致。原因支付回调网络抖动导致延迟用户在前端已经点了返回轮询接口没查到支付结果另外回调处理没有做幂等重复回调造成重复入账。解决回调状态的处理只认最终结果不认顺序。将回调写成幂等接口重复处理直接返回成功。用户在支付完成页不依赖前端跳转改为轮询订单状态接口轮询超时 30 秒调用支付平台主动查单接口。每天凌晨做一次和支付平台的对账把系统内已支付订单和平台账单进行比对差异单自动标记人工处理。5.4 三端 token 不一致导致登录态混乱现象用户在小程序下单后跳转 H5 活动页发现未登录要重新输验证码或者在 APP 上已经登录打开 H5 却要求登录。原因三端各自存 tokentoken 没有打通后端没有做同一会员多端的 session 绑定。解决会员表不做三端区分openid、手机号、APP 账号都绑定到同一 member_id。token 颁发时增加 member_id 关联任意一端登录后后端都能通过 member_id 查到该会员在其他端的登录状态。如果需要三端自动登录互通可以在受控环境下做 token 互换接口但要限制换 token 的接口频次防止被刷。5.5 报表统计口径不一致现象订单列表页显示今日订单 100 单报表中心显示 98 单财务对账显示 102 单。原因有的地方按订单创建时间统计有的按入住日期统计还有的按支付成功时间统计统计口径没统一。解决报表模块单独建一张统计中间表每天凌晨定时任务按统一口径计算后写入。当日实时看板允许实时查但必须标注“实时数据”历史数据一律以统计中间表为准。我在项目里干脆把统计口径做进接口枚举里创建时间、入住日期、支付时间三个参数值调用方必须显式传不传默认拒绝从源头避免“各算各的”。6. 进阶用一张二维表验证订单状态机三端共用一套状态逻辑最怕后端改了一个状态某端还保持着旧判断。我现在有个习惯把订单状态机画成一张二维表横向是当前状态纵向是触发动作相交格写目标状态。这张表既是设计文档也是测试用例而且可以在 CI 里直接跑断言。比如“取消订单”这个动作待支付状态点取消直接置为已取消已支付状态点取消要先走退款流程退款成功才能置为已取消入住中状态不允许取消只能走提前退房已完成状态不可取消。把这些规则写成一张状态转换表比在代码里到处 if else 清晰得多也方便新加入的开发者快速理解。# 状态转换表驱动避免散落的 if else STATE_TRANSITIONS { (PENDING, CANCEL): CANCELLED, (PENDING, PAY): PAID, (PAID, CANCEL): REFUNDING, (PAID, CHECK_IN): STAYING, (STAYING, CHECK_OUT): FINISHED, (REFUNDING, REFUND_SUCCESS): CANCELLED, } def transition(order, action): key (order.status, action) if key not in STATE_TRANSITIONS: raise ValueError(f非法状态转换: {order.status} - {action}) return STATE_TRANSITIONS[key]这张表的额外好处是接口测试只需要覆盖表里的每条路径不需要把所有异常路径都写一遍。每个非法转换会直接抛出异常返回业务错误码提示“当前状态不可执行该操作”。我在做某个模拟项目时把这张表贴在了团队文档首页后端实现、前端判断、测试用例都对着同一份表状态逻辑的沟通成本降了很多。自动化验证上我习惯用接口测试脚本跑一遍关键链路预订 - 支付 - 入住 - 退房再单独跑一遍取消和退款。脚本直接调支付平台的沙箱环境做到每天定时执行保证三端发版后状态链路没有被改坏。这个系统做下来最大的感受是多酒店的复杂度不在“多”字上而在“统一”上。三端统一接口、库存统一口径、状态统一推进把这几条管住了多酒店预订系统开发就成功了大半。我见过太多的项目是在联调阶段才去补这些统一约定的结果改一处崩三处返工周期比写新代码还长。希望这篇文章能让你一次就把这些基础打牢把精力留给真正的业务优化。本文还有配套的精品资源点击获取