简介:面向酒店管理软件开发者的PHP酒店管理系统源码(多酒店+数据库),集后台管理、APP、H5与小程序预订于一体,覆盖多酒店独立配置、实时客房预订、自动化入住退房、订单状态跟踪、会员积分与优惠券、财务自动计费及入住率、收入等统计报表,适合连锁酒店统一运营,也适合PHP学习者研究完整业务闭环。资源共2000个文件,核心功能以1428个PHP脚本实现,另含HTML、CSS、JS、SCSS等前端页面资源,图片与音频素材,数据库SQL初始化文件,以及环境配置与部署辅助脚本,压缩包整体78.76MB,目录结构清晰,便于按后台逻辑、多端应用和数据库分层检索。资源已有3609人学习下载。开发者可同时获得后台管理系统、APP与小程序的前后端代码,直观理解移动端预订、支付同步与消息推送等交互流程,也可参考配置文件快速搭建实验环境,或在此基础上二次开发,沉淀从预订到结算的酒店管理实战经验。对于需要快速落地多酒店业务的团队,还能从中掌握从数据库设计到多端联通的现成实现。
1. 多酒店管理为什么比单店系统难一个量级:先看清要解什么问题
一些做酒店外包的开发者看到“PHP酒店管理系统源码(多酒店)+数据库”时,第一反应往往是“单店系统加个酒店ID字段不就完了”。真正动手才发现,多酒店要同时解决总部对多店的房态价格管控、APP/H5/小程序三端预订链路、以及订单状态同步这三件事。任何一环没想透,后面都要返工。
这套方向解决的是酒店集团、连锁品牌或代运营团队的管理需求:运营人员在一套后台切换不同酒店,客人在任意一端完成查房、下单、支付。对PHP工程师来说,这也是一个典型的“业务复杂度不在代码,而在数据结构和状态机”的项目:数据库表和接口层立住了,三端预订只是同一套后端的三种皮肤。
适合它的人有两类:想在酒店行业做外包或自研的PHP开发者,想找一个能复用的多酒店预订骨架;酒店运营方,想掌握源码做二次开发而不是年年交SaaS订阅费。下面按我自己的落地习惯讲多酒店架构、数据库、接口层、三端接入和常见坑。
2. 多酒店数据库设计:一张表怎么撑起整个集团的房态与价格
2.1 核心表结构:酒店、房型、房间、价格日历
单店系统多半把价格直接放在房型表里,房间表挂房型ID,因为一家店的“今天”只有一个概念。到了多酒店场景,这套设计会立刻出问题:不同门店的同名房型价格不一样,同一个房型在节假日、平日、连住优惠时价格也不一样,房间还需要区分所属酒店。所以我把核心表按“酒店 -> 房型 -> 房间 -> 价格日历”四层拆开,而不是把整块业务压到两三张表里。
四张表各自负责一件事:hotel表管门店;room_type表管房型的固定属性,比如床型、面积、可住人数;room表管物理房间,比如1203房在几楼;price_calendar表管“每个房型在具体某天卖多少钱、还剩几间”,这是多酒店系统的核心命脉。房价不放房型表,是因为日期维度一旦拆出来,查询接口直接按日期取价,运营也能在后台精确调整某一天的价格和库存,不需要在代码里写“周末加价”这类规则。
下面是一套经历过订单量和并发校验的基础建表SQL,用的都是InnoDB和utf8mb4,字段注释直接写在表里。
-- 酒店表 CREATE TABLE hotel ( id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY, name VARCHAR(100) NOT NULL COMMENT '酒店名称', city VARCHAR(50) NOT NULL COMMENT '城市', address VARCHAR(200) NOT NULL COMMENT '详细地址', status TINYINT NOT NULL DEFAULT 1 COMMENT '1正常 0停用' ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='酒店信息表'; -- 房型表:房型的固定属性 CREATE TABLE room_type ( id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY, hotel_id INT UNSIGNED NOT NULL COMMENT '所属酒店', name VARCHAR(50) NOT NULL COMMENT '房型名,如高级大床房', area INT UNSIGNED COMMENT '面积,单位平米', bed_type VARCHAR(20) COMMENT '床型', max_guests TINYINT NOT NULL DEFAULT 2 COMMENT '可住人数', base_price DECIMAL(10,2) NOT NULL COMMENT '门市基准价,不参与预订计算', KEY idx_hotel (hotel_id) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='房型表'; -- 房间表:物理房间 CREATE TABLE room ( id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY, hotel_id INT UNSIGNED NOT NULL COMMENT '所属酒店', room_type_id INT UNSIGNED NOT NULL COMMENT '所属房型', room_no VARCHAR(20) NOT NULL COMMENT '房间号,如1201', floor_no TINYINT UNSIGNED COMMENT '楼层', status TINYINT NOT NULL DEFAULT 1 COMMENT '1可售 0维修', KEY idx_hotel_type (hotel_id, room_type_id) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='房间表'; -- 房价日历表:每个房型在具体日期的价格和可售数量 CREATE TABLE price_calendar ( id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY, hotel_id INT UNSIGNED NOT NULL COMMENT '所属酒店', room_type_id INT UNSIGNED NOT NULL COMMENT '所属房型', biz_date DATE NOT NULL COMMENT '营业日期', price DECIMAL(10,2) NOT NULL COMMENT '当日每间夜售价', inventory INT NOT NULL COMMENT '当日可售间数', UNIQUE KEY uk_hotel_type_date (hotel_id, room_type_id, biz_date) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='房价日历表';这套表最容易被忽略的是price_calendar的联合唯一键uk_hotel_type_date。它保证同一个酒店同一个房型在同一天只有一条价格和库存,避免后台重复导入导致价格被覆盖成两行。hotel_id几乎出现在所有业务表上,这是后面做数据权限和报表分组的基础。
字段说明里,base_price只是门市展示价,真正参与预订计算的一律以price_calendar.price为准。inventory表示当天可售间数,不是剩余房间数,扣减后通过它就能判断超卖,不需要再去room表逐间查询。这里强调一个习惯:只要涉及“今天能不能卖”,一律查biz_date,不要让业务代码用date('Y-m-d')去拼逻辑,否则后面时区出问题会非常难排。
2.2 多酒店的权限边界:数据隔离还是混用一张表
多酒店系统面对的运营角色往往不止一种:集团管理员看所有门店,门店店长只看自己店。代码层最怕做成“每个酒店一套数据库”,看着干净,实际升级表结构、汇总报表、跨店调拨房态时都会累到崩溃。常见做法是共享库加hotel_id软隔离,所有核心表都带门店字段,管理后台根据登录人的角色动态生成可见的酒店ID列表。
| 隔离方案 | 优点 | 缺点 | 适用场景 | | 独立库/独立表 | 数据完全隔离,出错影响面小 | 表结构升级要同步多套库,报表要跨库汇总 | 酒店间完全独立、几乎无协作 | | 共享库 + hotel_id | 一套代码一台库,维护成本最低 | 数据权限必须做严 | 连锁酒店、代运营、OTA多店 |
我一般选共享库加hotel_id。实现时有一个细节:users表不要只放一个hotel_id,用户可能在这家店注册,再去另一家店下单。正确的做法是users表只放用户基本信息,再建一张user_hotel表记录用户绑定了哪些门店的会员身份。查询订单时通过orders.hotel_id关联,避免把用户死死绑在注册门店上。
权限过滤的SQL也统一约定:所有列表查询都强制带上“当前登录人可见的hotel_id集合”,不让单个接口自己判断角色。判断逻辑收口到一个公共函数里,功能迭代时新接口必须走同一个入口,防止某天新写了个查询漏掉过滤条件,把A店的订单展示给了B店的运营。
2.3 初始化数据:一建脚本跑出两家酒店和完整的预订骨架
表结构定好之后,至少要能快速初始化出可演示的数据。我通常给这类项目留一个init_data.sql,里面放示例酒店、房型、房间和最近N天的价格日历。下面这段是常用的初始化片段,订单表也一并建好,预订流程会直接用到。
-- 订单表:预订流程的核心 CREATE TABLE orders ( id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY, order_no VARCHAR(32) NOT NULL COMMENT '订单号,展示给用户和客服', hotel_id INT UNSIGNED NOT NULL COMMENT '所属酒店', room_type_id INT UNSIGNED NOT NULL COMMENT '房型', room_id INT UNSIGNED NULL COMMENT '入住前可空,到店排房', user_id INT UNSIGNED NOT NULL COMMENT '下单用户', check_in DATE NOT NULL COMMENT '入住日期', check_out DATE NOT NULL COMMENT '离店日期', nights TINYINT NOT NULL COMMENT '间夜数', total_amount DECIMAL(10,2) NOT NULL COMMENT '实付总价', status TINYINT NOT NULL DEFAULT 0 COMMENT '0待支付 1已支付 2已入住 3已离店 4已取消 5已退款', channel VARCHAR(10) NOT NULL COMMENT 'app/h5/mini', created_at DATETIME NOT NULL, paid_at DATETIME NULL, KEY idx_hotel_status (hotel_id, status), UNIQUE KEY uk_order_no (order_no) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='订单表'; INSERT INTO hotel (id, name, city, address) VALUES (1, 'A市会展店', 'A市', '经开会展路1号'), (2, 'A市高新店', 'A市', '高科创新路2号'); INSERT INTO room_type (hotel_id, name, area, bed_type, max_guests, base_price) VALUES (1, '高级大床房', 28, '1.8米大床', 2, 399.00), (1, '家庭双床房', 35, '1.2米双床', 3, 499.00), (2, '豪华大床房', 32, '1.8米大床', 2, 599.00), (2, '商务双床房', 30, '1.2米双床', 2, 549.00); INSERT INTO room (hotel_id, room_type_id, room_no, floor_no, status) VALUES (1, 1, '1201', 12, 1), (1, 1, '1202', 12, 1), (1, 2, '1203', 12, 1), (2, 3, '801', 8, 1), (2, 3, '802', 8, 1);orders表带上了channel字段,从设计上就把三端预订考虑进去。price_calendar不在这段初始化里写死,是因为它通常由后台的“生成房价”功能按基础价批量生成,或者由渠道管理系统推送过来。初始化脚本的作用是让新环境一晚上跑通:有酒店、有房型、有房间、有完整订单表,再去接接口就顺多了。
到这里,数据库骨架足够支撑预订和支付流程。下一步是接口层。
3. PHP接口层与预订主流程:从登录到支付回调的完整闭环
3.1 接口框架选型:原生PHP、CI还是Laravel
做酒店这类业务,接口层选什么框架不是最重要的,因为核心复杂度在状态和并发上。常见做法是:门店规模不大、团队人手少用轻框架,把路由和数据库封装交给框架,业务自己写;如果后面接的渠道多、报表复杂,再上重型框架也不迟。用原生PHP也不是不行,但路由、参数校验、ORM都要自己搭,第一版开发周期会拉长一倍。
我见过的酒店项目里,用轻量框架的情况最多,因为它部署简单,虚拟主机也能跑。核心要求只有一个:控制器只做参数接收和响应输出,业务逻辑全部放Service层。订单创建、支付回调、库存扣减这些关键动作不允许散落在各个控制器里。
3.2 预订五步接口:锁定房态到生成订单
用户在三端下单时,后端接口大致按五个步骤走:第一步根据check_in和check_out查可用房型;第二步按日期从price_calendar取价计算总价;第三步在事务中插入订单同时扣减库存;第四步生成支付参数返回给前端拉起支付;第五步处理支付平台异步回调确认订单。前三步是并发风险最集中的地方,代码写错就会出现超卖。
下面这段createOrder是预订链路里最需要抄作业的部分,它把“查库存、算总价、扣库存、生成订单”放在一个事务里,并用SELECT ... FOR UPDATE锁住价格日历行。
// 创建订单:锁定房态并生成待支付订单 function createOrder($hotelId, $roomTypeId, $checkIn, $checkOut, $userId, $channel) { $pdo = getConnection(); $pdo->beginTransaction(); try { // 1. 锁定价格日历行,同一行在事务结束前不被其他请求修改 $stmt = $pdo->prepare( "SELECT biz_date, price, inventory FROM price_calendar WHERE hotel_id = ? AND room_type_id = ? AND biz_date >= ? AND biz_date < ? FOR UPDATE" ); $stmt->execute([$hotelId, $roomTypeId, $checkIn, $checkOut]); $rows = $stmt->fetchAll(); $total = 0; foreach ($rows as $row) { if ($row['inventory'] <= 0) { throw new Exception('当前日期无房'); } $total += $row['price']; } // 2. 原子扣减每日可售间数 $stmt = $pdo->prepare( "UPDATE price_calendar SET inventory = inventory - 1 WHERE hotel_id = ? AND room_type_id = ? AND biz_date >= ? AND biz_date < ?" ); $stmt->execute([$hotelId, $roomTypeId, $checkIn, $checkOut]); // 3. 生成订单 $orderNo = date('YmdHis') . random_int(1000, 9999); $nights = (strtotime($checkOut) - strtotime($checkIn)) / 86400; $stmt = $pdo->prepare( "INSERT INTO orders (order_no, hotel_id, room_type_id, user_id, check_in, check_out, nights, total_amount, status, channel, created_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, 0, ?, NOW())" ); $stmt->execute([ $orderNo, $hotelId, $roomTypeId, $userId, $checkIn, $checkOut, $nights, $total, $channel ]); $pdo->commit(); return $orderNo; } catch (Exception $e) { $pdo->rollBack(); throw $e; } }这里的核心不是“先查后扣”,而是FOR UPDATE把查询行锁住。两个请求同时下单时,第二个事务必须等第一个事务结束才能读取价格日历行,于是inventory扣减也就串行化了。如果去掉FOR UPDATE,两个请求都可能读到库存为1,然后各自扣减,数据库最终变为0,但订单却生成两笔,这就是典型的并发超卖。
参数方面有几个细节:check_in、check_out必须是Y-m-d格式,check_out指离店日,不占库存,所以SQL里用biz_date < check_out而不是<=;nights用strtotime差值算,日期跨月也准。total_amount在演示里只算房费,真实环境还要在循环里叠加早餐费、服务费和税费,建议把费用明细单独存一张fee表,避免订单表字段越加越多。
3.3 支付回调与房态回滚:状态机是生死线
订单不是创建完就结束了,从“待支付”到“已支付”再到“已入住”,每一步都必须有明确的状态迁移。如果让回调、取消、退款各自直接改status,很快会出现“用户支付成功又被取消”的现象。给订单定义状态机:0待支付、1已支付、2已入住、3已离店、4已取消、5已退款,并且规定只有0能变1、0能变4、1能变5,其他状态禁止跳变。实现上用一个收口的函数处理变更。
支付回调是所有渠道订单的入口。支付平台的异步通知到达后,必须先验签,验签通过再查订单、比对金额、改状态。下面这段是简化的回调处理逻辑,重点是查订单时也带FOR UPDATE,避免与后台定时取消任务并发改同一个订单。
// 支付平台异步通知处理,返回true表示已处理,支付平台不再重试 function handlePaymentCallback(array $params): bool { // 1. 验签失败直接返回false,让支付平台稍后重试 if (!verifySign($params, $config['pay_key'])) { return false; } $orderNo = $params['order_no'] ?? ''; $payStatus = $params['pay_status'] ?? ''; $payAmount = $params['amount'] ?? 0; $pdo = getConnection(); $stmt = $pdo->prepare("SELECT * FROM orders WHERE order_no = ? FOR UPDATE"); $stmt->execute([$orderNo]); $order = $stmt->fetch(); if (!$order || $order['status'] != 0) { return true; // 订单不存在或已处理,按成功返回 } if ($payStatus === 'SUCCESS' && (float)$payAmount == (float)$order['total_amount']) { $pdo->prepare("UPDATE orders SET status = 1, paid_at = NOW() WHERE order_no = ?") ->execute([$orderNo]); } return true; }回调处理里有两个容易翻车的点:金额比较必须转成float或者用string比较,不能用int强转,否则399.00和399会因精度问题对不上;验签用的原始字符串必须完整保留,不能把参数重新拼接一遍再验,支付平台对字段顺序有要求,字段顺序一变签名就失败。
提示:回调接口返回的内容只能输出固定字符串表示处理结果,不要输出订单详情。支付平台会把非约定输出当成异常反复重试,日志会被刷爆。
4. APP、H5、小程序三端同源:一套API如何支撑三种预订入口
4.1 三个端共同依赖的登录承接口
三端的登录方式不一样:APP用手机号加密码或验证码登录,H5可以复用账号体系,小程序则依赖平台自身的一次性登录凭证来换取用户标识。但登录成功之后,三端拿到的都应该是同一个access_token,这样后续查房、下单、支付接口才能完全复用。后端不要为每个端单独写一套预订接口,那样三端需求一变,要改三处。
APP的登录接口走手机号验证码或者密码,H5走账号密码,小程序需要专门留一个code换token的接口。三个接口最终都调用同一个分配token的服务,返回结构保持一致:token、过期时间、用户昵称和默认酒店ID。默认酒店ID来自用户最近一次下单的门店,用于打开APP时直接展示附近酒店列表。
4.2 token校验中间件:一处实现,三端通用
我习惯用自签token而不是直接依赖框架的session,因为APP和小程序都不方便处理服务端session。token里只放user_id和过期时间,用服务端密钥做HMAC签名,接口层写一个公共校验函数,所有业务接口进来先调它。下面是一个简化可跑的示例,生产环境建议直接用现成JWT库,避免自己处理base64编码的边界字符。
// 生成token function issueToken(int $userId): string { $header = base64_encode(json_encode(['alg' => 'HS256', 'typ' => 'JWT'])); $payload = base64_encode(json_encode([ 'uid' => $userId, 'exp' => time() + 7 * 86400, // 7天免登录 ])); $sign = hash_hmac('sha256', "$header.$payload", SECRET_KEY); return "$header.$payload.$sign"; } // 校验token,返回用户ID,失败返回null function verifyToken(string $token): ?int { [$header, $payload, $sign] = explode('.', $token); if (hash_hmac('sha256', "$header.$payload", SECRET_KEY) !== $sign) { return null; // 签名被篡改 } $data = json_decode(base64_decode($payload), true); if ($data['exp'] < time()) { return null; // 已过期 } return $data['uid']; }这段代码的关键参数是SECRET_KEY和7天有效期。SECRET_KEY要放到配置文件中,不能写死在代码里,一旦泄露等于所有token都可被伪造。7天有效期对酒店预订场景比较合适,客人订房周期一般不超过一周;如果做长租公寓,可以调成30天并加上“记住我”选项。
过期处理也要想清楚:token过期后,APP端会退出登录;小程序端则要静默重新登录,不能让用户在每个页面都重新授权。实际项目里可以在校验函数返回“token过期”这个特殊状态,前端收到后调用刷新接口续期。
4.3 小程序登录与H5 URL参数:同一条预订页的两种打开方式
H5的预订页可以在URL直接带hotel_id和room_type_id,从短信或公众号菜单点进来就落到具体房型页。小程序则不能直接把参数写死在启动页里,必须先进入小程序,再调接口获得当前用户的身份和默认酒店信息。后端接口要同时接受来自H5的query参数和来自小程序的登录态换取token,这是三端同源里面最容易写乱的地方。
小程序首次登录的业务逻辑是:前端拿到code后传给后端,后端拿code去平台接口换取session信息,再通过openid查找或创建用户,最后返回token。简化代码如下:
// 小程序登录:用临时code换取用户token public function miniLogin(string $code): array { // 调用平台接口,用code换取session_key和openid $session = miniProgramCode2Session($code); $openid = $session['openid']; $user = findUserByOpenid($openid); if (!$user) { $user = registerByOpenid($openid); // 首次使用自动注册 } return ['token' => issueToken($user['id'])]; }参数上要注意三点:code是临时凭证,有效期一般只有几分钟,后端拿到后必须立刻使用,不能存起来;openid是用户在小程序端的唯一标识,同一个用户在不同小程序下的openid不一样,如果后面要打通APP和H5的会员体系,绑定手机号是唯一可靠的办法;注册时不要只存openid,还要允许用户后续补手机号,否则运营完全无法触达。
三端接口的差异收敛到一张表里会很清楚:
| 端 | 登录方式 | 身份凭证 | 预订页参数来源 | | APP | 手机号+密码/验证码 | access_token | 登录后默认酒店 | | H5 | 账号密码/短信验证码 | access_token或Cookie | URL query参数 | | 小程序 | 临时code换取 | access_token | 接口返回默认酒店 |
到这一步,后端已经具备支持三端预订的能力。接下来是真正让人头疼的部分——多酒店系统的坑,全踩一遍才能长记性。
5. 多酒店系统最常见的5个坑:踩过的人才懂
5.1 房价跑错:“今天”不是同一个“今天”
现象:运营在后台看某房型今天卖399,客人从APP下单显示399,但从H5进来变成389,价格对不上。原因:后台录入价格日历用的是浏览器本地时区,PHP进程默认时区又没设置,数据库连接时区也可能偏了。三个时区不一致时,某个订单跨到“昨天”或“明天”,查到的就是错误日期的价格。解决:在项目配置里把PHP默认时区统一为东八区,数据库连接建立后执行SET time_zone = '+08:00',所有日期一律以字符串Y-m-d传递和存储,不依赖服务器时间换算。排查时让运营截一张后台价格日历图,再直接查数据库里这一天price_calendar的原始记录,马上能看出是哪一层跑偏。
5.2 超卖:并发下单时房态锁没起到作用
现象:某房型当天只剩2间,同一秒涌进10个订单,结果4单成功,直接超卖。原因:代码先查库存,再在另一个事务里插入订单,这两个动作之间没有锁。查到的库存是2,4个线程都认为自己拿到了2,各自下单成功。解决:订单创建必须按第3章的写法,在事务中用SELECT ... FOR UPDATE锁住价格日历行,或者直接用UPDATE price_calendar SET inventory = inventory - 1 WHERE inventory > 0,让数据库保证扣减条件。并发压力大的时候再加一层Redis预扣,但数据库这层兜底不能省。排查时看MySQL锁等待日志,事务时间超过500ms就说明锁的粒度太大或索引没走对。
5.3 订单状态错乱:回调、取消、退款三方打架
现象:用户刚支付成功,手机就收到“订单已取消”的短信;退款完成后订单状态又变回已支付。原因:支付回调、超时取消任务、用户申请退款各自直接UPDATE status,后执行的覆盖先执行的。解决:所有状态变更收口到一个函数,更新语句必须带当前状态条件,比如UPDATE orders SET status = 1 WHERE order_no = ? AND status = 0,这样就算两个任务同时执行,也只有一个会生效。排查时给订单表加一个status_log表记录每次变更的“原状态、新状态、操作人、时间”,出现问题按时间线回放,几分钟就定位到是谁改了状态。
5.4 小程序登录态失效:Session在页面跳转时丢失
现象:小程序里刚登录成功,跳转到房型详情页又提示未登录,用户得重新授权。原因:不少PHP开发者习惯把session当全局身份,但小程序的网络请求默认不携带Cookie,服务端每次请求都认为是一个新会话,登录态自然保不住。解决:小程序端所有请求把token放在请求头Authorization里,后端用第4章的统一校验函数解析,不读session,不依赖Cookie。排查时抓一个请求的请求头看看有没有Authorization字段,没有就往前端找问题,有但失效就查token签发和过期时间。
5.5 接口字段版本化:前后端各改一半的惨痛教训
现象:APP升级到新版本后,接口返回字段从status改成order_status,H5和小程序还是老版本,结果H5预订页显示不出订单状态,样式全乱。原因:后端接口没有版本概念,字段说改就改,前端三端发版节奏还不一致。解决:所有API路径统一加/v1前缀,业务字段变更时新增/v2接口,旧客户端继续走/v1,稳定运行一个版本周期后再下线旧接口。排查时看前端请求的URL路径,确定它打的是哪个版本;如果路由里没有版本号,赶紧补上。
6. 一晚上跑通的部署验证法:用可重复的脚本验证整个预订链路
数据库建好、接口写完,不能只靠浏览器点点点来验证。这套多酒店系统涉及三端,前后端角色多,我最常用的做法是准备一个curl脚本,模拟“登录 -> 查房 -> 下单 -> 支付回调 -> 查库存”的完整链路,每次部署完跑一遍,输出PASS或FAIL,出现问题直接看是哪一步断了。
#!/bin/bash # 多酒店预订链路自检脚本,按顺序执行并输出每一步结果 BASE="http://127.0.0.1:8080/api/v1" # 1. 登录拿token LOGIN=$(curl -s -X POST "$BASE/auth/login" \ -H 'Content-Type: application/json' \ -d '{"mobile":"13800000000","password":"123456"}') TOKEN=$(echo "$LOGIN" | sed -n 's/.*"token":"\([^"]*\)".*/\1/p') [ -z "$TOKEN" ] && echo "FAIL 登录失败" && exit 1 echo "PASS 登录成功" # 2. 查A市会展店高级大床房未来两天的库存 QUERY=$(curl -s -X GET "$BASE/room/search?hotel_id=1&room_type_id=1&check_in=2026-01-20&check_out=2026-01-22" \ -H "Authorization: Bearer $TOKEN") echo "$QUERY" | grep -q '"inventory"' || echo "WARN 返回中没有库存字段" echo "$QUERY" # 3. 下单并保存订单号 ORDER=$(curl -s -X POST "$BASE/order/create" \ -H 'Content-Type: application/json' \ -H "Authorization: Bearer $TOKEN" \ -d '{"hotel_id":1,"room_type_id":1,"check_in":"2026-01-20","check_out":"2026-01-22","channel":"mini"}') ORDER_NO=$(echo "$ORDER" | sed -n 's/.*"order_no":"\([^"]*\)".*/\1/p') [ -z "$ORDER_NO" ] && echo "FAIL 下单失败" && exit 1 echo "PASS 下单成功 $ORDER_NO" # 4. 模拟支付回调 CALLBACK=$(curl -s -X POST "$BASE/pay/callback" \ -H 'Content-Type: application/json' \ -d "{\"order_no\":\"$ORDER_NO\",\"pay_status\":\"SUCCESS\",\"amount\":798.00}") echo "$CALLBACK" | grep -q 'ok' && echo "PASS 支付回调生效" || echo "FAIL 支付回调未生效" # 5. 复查库存是否扣减 STOCK=$(curl -s -X GET "$BASE/room/stock?hotel_id=1&room_type_id=1&date=2026-01-20" \ -H "Authorization: Bearer $TOKEN") echo "$STOCK"脚本里最关键的是第四步,模拟支付回调时amount必须跟下单时的总价一致。脚本里下的是2026-01-20到2026-01-22两晚,单晚399,所以amount写798,如果价格日历改过,这个数字要同步改。更稳妥的做法是从第二步的查询结果里自动提取每晚价格再求和,避免脚本跑一次改一次。
这套脚本要在真正没人的测试库上跑,不要打在正式环境。每次部署后跑一遍,确认“登录没问题、下单没问题、回调没问题、库存扣了”这四件事都通过,再放上线。在我自己的维护习惯里,这套自检脚本就是后悔药——很多翻车现场,一跑脚本就能定位到是接口、数据库还是参数问题,不用黑匣子式猜。希望帮到你。
本文还有配套的精品资源,点击获取