ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

酒店客房管理系统开发实战:Python+Flask+MySQL全解析

酒店客房管理系统开发实战:Python+Flask+MySQL全解析 1. 从Excel表格到系统化这家小酒店的痛点逼我写了这套系统做酒店客房管理系统之前我一直觉得这活儿不难——无非是登记、开房、退房、算钱。直到一个做民宿老板的朋友拿着一个塞满公式和合并单元格的Excel把我堵在咖啡厅我才意识到排房不是“登记”那么简单。那几天前台三班倒每个班次都要在共享表格里改房态结果经常出现同一间房被两个渠道订出去或者明明标了“已入住”却没人填客人信息。她问我能不能用Python给我做一套简单的系统要有源码要能自己改最好连数据库脚本和文档一起交付不然以后接手的员工根本看不懂。我答应的时候还没想到这套“基于Python的酒店客房管理系统”会从一个课程设计级别的Demo慢慢演化成交付给真实场景使用的项目。本文要聊的就是这套系统的完整拆解包括我怎么选型、怎么设计数据库、怎么把预订到退房的整个状态机撸顺以及那些不跑一遍根本发现不了的坑。适合正在做课设、想接私活、或者单纯想搞明白“酒店管理系统到底怎么实现”的朋友参考。文章里给出的思路和关键代码段都是可复用的源码结构、数据库脚本和文档的整理思路也会一并讲透。系统的定位很明确不碰供应链、不碰员工考勤只把“客房”这一条核心链路做穿。具体包括房间档案管理、客户信息登记、预订锁房、入住办理、换房、退房结算、入住率统计这几个模块。技术栈锁定Python Flask MySQL数据库设计上耗尽心思做了状态字段和订单流水双轨制最终交付物包含完整Python源码、可直接导入的SQL脚本、以及一份前台操作指南加上开发文档。下面就从需求开始讲。2. 技术选型的取舍Python Flask MySQL是怎么凑到一起的2.1 为什么放弃Django选了Flask很多人在社区问酒店管理系统用Django不是现成的有admin后台吗确实Django自带Admin、模型、迁移、认证一套组合拳打下来似乎更快。但我最后选了Flask原因是这个系统本质上是一个高度定制的中小型业务系统不是内容型网站。Django的ORM在使用上确实顺手但它的App结构和中间件机制对于一个小团队来说有点重尤其当我想在路线上写一些简单的状态机判断时Flask的“微框架”属性让我能更自由地控制请求的生命周期。另一个现实因素是从部署角度考虑Flask应用用一个app.py就能跑起来配合waitress或gunicorn都很轻不像Django要配一堆WSGI中间件。对于酒店前台那种只有一两台Windows收银机的环境Flask明显更友好。还要考虑一个关键点这套系统的使用者是非技术背景的前台人员。Flask Jinja2模板能让我把整个页面逻辑集中在服务端渲染里不需要单独起一个前端Node服务。反观Django即使不写前端也默认带了不少用不上的能力数据库迁移逻辑在项目初期频繁改表时也会拖慢节奏。所以如果你的项目偏重业务逻辑的快速迭代、需要贴合前台操作习惯的后台界面Flask可以用而且能让你少踩很多“框架替你做了决定”的坑。2.2 SQLite还是MySQL以及连接池的必要性这是踩过的很实在的一个问题。开发阶段我用了SQLite因为单文件、免安装跑起来零负担。但项目需要交付给真实酒店跑就不得不换成MySQL。为什么第一SQLite不支持并发写前台三台收银机同时操作时SQLite的数据库锁会让排房和结算出现明显的卡顿甚至“database is locked”报错。第二SQLite在数据量上去后备份和权限管理很弱。MySQL有独立的账户体系binlog日志方便恢复更重要的是它支持SELECT ... FOR UPDATE这样的行级锁这在处理“同一间房并发预订”时几乎是救命特性。既然用了MySQL连接池就必须提上日程。说实话很多Python项目根本不配连接池每次请求都pymysql.connect一次短连接在高并发下很容易把数据库的线程数打满。我用的方案是SQLAlchemy自带的核心连接池配置了pool_size5、max_overflow10并在Flask应用启动时初始化引擎。这里有个细节连接池不是越大越好。对于酒店前台这种并发峰值也就几十个人的场景连接池设个5到10就够了省得数据库被一堆空闲连接拖垮。如果你不想引SQLAlchemy太重用DBUtils.PooledDB配合PyMySQL也能达到类似效果但我在项目里是直接用SQLAlchemy Core做连接管理和ORM模型共用一套引擎少一层中间件。2.3 ORM与原生SQL的边界哪些查询必须手写关于ORM和原生SQL的争论在这里的结论建模查询用ORM统计报表用原生SQL。为什么ORM在对象关系映射上确实省心特别像房间表、订单表、客户表这种典型的CRUDSQLAlchemy的模型写出来非常直观。但有几种查询必须手写否则要么性能崩要么逻辑绕到你怀疑人生。第一个是复杂的日期交叉判断例如查某个时间段内的可用房间用ORM表式join会写得很痛苦但原生SQL用NOT EXISTS子查询就能轻松解决。第二个是入住率聚合后台要按日统计出租率、按房型统计收入这些窗口函数如ROW_NUMBER、SUM OVER在ORM里要拼半天直接用text()片段嵌进SQL更清爽。我在系统中维护了一个orders表统计当日入住率时先查出当天所有在住的订单记录然后按房型分组再关联房间总数算百分比。这类带窗口函数的语句用ORM写不仅可读性差还会多出不少隐式查询导致前台页面转圈半天。项目中凡涉及统计报表的地方我都用了db.session.execute(text(...))原生SQL确保数据库一次算完返回结果。这个边界划清楚后维护成本低很多。3. 数据库设计把房间、订单、账单变成可检索的表格3.1 房间表与状态机从“空闲/入住/脏房”扩展到六态酒店房态不能拍脑袋设计成“空闲、占用、脏”三个字段就完事。真实前台面临的房态至少包括空闲可预订、预占已被预订但客人未到、已入住在住、已退房房间已退还没清洁、清洁中保洁正在打扫、维修中排查维修问题。其中“已退房”和“清洁中”必须分开因为退房后不一定立刻有人打扫而前台的排房操作在“清洁中”结束时才能释放房间。我建了一张rooms表包含room_id、room_number、room_type、floor、status、price_per_night、capacity等字段其中status用TINYINT存枚举值0空闲、1预占、2入住、3已退、4清洁中、5维修。这张表最核心的设计不是字段多而是状态流有方向性。举例预占的房间不能直接变空闲必须经过“取消预订”或者“办理入住”已入住的房间不能直接变空闲必须经过“退房”变成已退房再由清洁完成变成空闲。为了把这个规则落到数据库层我除了在应用层写状态机校验还在rooms表加了一个status_updated_at字段记录状态变更时间方便做超时提醒。比如预占状态超过24小时客人没到前台列表里就要高亮显示。3.2 订单主表子表预订、入住、换房、加床的归一化处理订单结构是这套系统的灵魂。我采用“订单主表orders 订单明细表order_items”的做法。主表存的是客户主键、总金额、状态待入住/在住/已退/已取消、check_in_date、check_out_date、created_at。明细表存的是每晚的房费、加床费、减免费用和备注。为什么要拆分因为换房场景下客户可能在一个订单里住了A房两天又换了B房一晚费用明细不同但订单整体还是一个连续履约过程。如果把换房信息全堆在一个表里退房结算时很难分清哪天多少钱。这里有个更关键的设计订单状态使用“step”字段而不是用一系列时间字段去推算。例如预订阶段的订单step1入住登记后step2退房后step3取消step0。这样在列表筛选时直接where step in (1,2)就能找到所有“需要今天接待”的客人不需要去比较入住时间。换房操作实际上是“把原房间的剩余晚数拆出来建一个新订单明细”同时把原明细停止日期修改为换房日。所有明细都外键关联order_id删除主订单时级联删除保证数据不会出现脏孤儿记录。3.3 金额字段为什么要用Decimal列而不是Float列这个坑特别典型而且很多课设代码里都是错的。数据库里凡是涉及钱的字段一律用DECIMAL(10,2)不能图省事用FLOAT。为什么因为浮点数在二进制里无法精确表示0.1。比如119.9乘以3在Python的float计算里得到359.69999999999994直接入库再Reselect账单就会对不上。我第一版就踩了这坑晚上对账时发现差了一分钱查了半天才想起来是浮点精度问题。除了金额类型订单的计费日期字段我统一用DATETIME不用DATE因为前台退房往往发生在12:00左右延时退房会额外收费需要存精确时间点。而像统计“某天入住率”时只需要比较DATETIME的日期部分即可。这里有个小技巧数据库连接字符串里加上use_timezoneFalse统一用服务器本地时间不要引入UTC偏移否则凌晨退房的订单日期可能错位。对于单店应用保存本地时间就够了别给自己添乱。4. 核心功能落地登录、排房、退房结算的代码实现4.1 用Flask-Login还是JWT我最后选了JWT的完整理由酒店管理系统的登录模块看着简单但涉及角色权限。系统里有两类角色店长能看报表、调房价、操作所有房间、前台只能办理入住和退房。一开始我想省事用Flask-Loginsession在服务端存状态非常经典。但我发现一个问题前台的浏览器是那种老Windows上的Chrome动不动就清Cookiesession一丢前台就要重新登录。另外当我需要给之后的小程序端预留接口时session方案天生不适合移动端。所以最终用了JWT。实现方式很简单登录接口验证账密后生成一个带角色信息的JWT放在cookie里请求进来时通过Flask的before_request钩子做解析再把用户身份存进g对象。JWT的好处是服务端不用存会话状态重启应用也不掉线而且payload里带角色标识权限判断就是一个装饰器的事儿。坏处是如果要强制退出不容易但酒店内部系统无所谓token过期时间设成6小时前台上班登录一次就行。这边附一个关键装饰器代码from functools import wraps from flask import request, jsonify, g import jwt def login_required(roleNone): def decorator(f): wraps(f) def wrapper(*args, **kwargs): token request.cookies.get(token) if not token: return jsonify({code: 401, msg: 未登录}), 401 try: payload jwt.decode(token, app.config[SECRET_KEY], algorithms[HS256]) g.user_id payload[uid] g.user_role payload[role] except jwt.ExpiredSignatureError: return jsonify({code: 401, msg: 登录已过期}), 401 except jwt.InvalidTokenError: return jsonify({code: 401, msg: 无效凭证}), 401 if role and g.user_role ! role: return jsonify({code: 403, msg: 权限不足}), 403 return f(*args, **kwargs) return wrapper return decorator实际使用时在房态操作的路由上加login_required(roleadmin)就能限制只有店长能调价前台角色则被拦截。这套逻辑简单清晰没引入额外扩展包调试也方便。4.2 预订和入住的房间锁定事务与行锁的正确姿势“同一间房同时被两个人抢着预订”是并发问题的典型场景。方案是在事务里用SELECT ... FOR UPDATE锁定rooms表的行然后检查状态如果状态不是空闲立刻回滚。关键是必须把“检查状态”和“更新状态”放在同一个事务里中间不能有任何让出事务的IO操作。很多课设代码是分开的两个request先查一次房态再点提交中间隔了几分钟那肯定有并发漏洞。我当时的实现是在MySQL事务里执行类似这样的SQLSELECT status FROM rooms WHERE room_id ? FOR UPDATE; -- 应用层判断status 0空闲后继续 UPDATE rooms SET status 1, pre_customer_id ?, pre_check_in_date ? WHERE room_id ?; INSERT INTO orders ...; COMMIT;用SQLAlchemy时注意session.begin()之后的查询会默认自动使用当前事务但只有执行了FOR UPDATE子句才能真正锁行。SQLAlchemy里直接支持db.session.execute(text(...))所以我把这个核心操作写成了原生SQL避免ORM把锁语义吞掉。顺带提一句innodb的行锁是作用在索引上的所以必须保证WHERE条件room_id是主键或唯一索引否则会退化成表锁一锁就锁一排。入住办理的逻辑和预订类似但多了一步“把预订状态改成在住”。这里要注意如果客人是先预订后退房比如线上渠道再现场入住订单记录可能已经存在status1的状态那么办理入住时的状态流应该是锁房间行 - 校验订单状态必须为1 - 更新房间状态为2 - 更新订单状态为2。这个状态机务必写在应用层的统一接口里不要让前端业务逻辑绕过接口直接改状态。4.3 退房结算夜间房价、延时费与折扣的算法流程退房结算涉及最复杂的计算逻辑。一个订单的实际应付金额并不简单等于房晚单价乘以晚数。举例客人预订2晚实际住了2晚但第二天早上10点退房这算正常如果14点退房就要看酒店规则——有些酒店收取半天房费有些则收取全天。还有各种折扣比如连住优惠、协议客户优惠。我在结算模块里把流程拆成四步计算实际入住晚数根据orders表的check_in_dateDATETIME和实际退房时刻实际退房时间单独存一个actual_check_out字段按酒店规则计算。规则是当天中午12点前退房不额外算晚数12点后加收半天18点后加收全天。这些规则全部做成常量配置方便店长在后台调整。遍历order_items明细把每晚房费累加到总价。添加额外的消费项minibar加床等这些不是房费单独立表或者存json字段。应用折扣会员95折、协议客户9折这些折扣作用在房价总额上不影响minibar。这四步放在一个事务里最后写orders表的bill_amount字段和actual_check_out时间然后把房间状态从“在住”更新为“已退房”。钱的计算全程使用Python的decimal.Decimal不用浮点。核心一个函数如下from decimal import Decimal, ROUND_HALF_UP def calc_bill(order_items, extra_consumes, discount_rateDecimal(1.00)): total_room sum(item.amount for item in order_items) # item.amount是Decimal total_extra sum(item.amount for item in extra_consumes) total (total_room * discount_rate total_extra).quantize(Decimal(0.01), roundingROUND_HALF_UP) return total这里注意Decimal在MySQL里取出后SQLAlchemy会转为Decimal类型不需要额外转换。可是如果你用PyMySQL原生的fetchall拿到的是字符串容易误操作建议还是保持SQLAlchemy的类型转换。5. 踩坑实录并发订同一间房、跨天日期计算与账单精度5.1 CASE 1同一秒两个人订了同一间大床房这问题是在测试环境用两个浏览器隐身窗口同时操作暴露出来的。一个窗口开在A房点击“预订”另一个窗口也开在A房点击“预订”两个请求几乎同时到服务端。最初的代码是这样写的先查rooms表状态如果是空闲就生成订单再更新房间状态。结果两个请求都查到了状态是空闲都生成了订单虽然最后房间状态还是被置为预占但产生了两个预订单都对应同一间房。如果客人实际到店其中一个订单就成“幽灵订单”。修复过程就是前面说的行锁。但还有一个细节即使加了FOR UPDATE也有可能因为事务隔离级别的问题出现幻读——两个事务同时执行FOR UPDATE第一个拿到锁后更新状态并提交第二个等待后恢复执行应该能看到最新状态。MySQL默认的REPEATABLE READ级别下FOR UPDATE本身就是“当前读”会读到最新已提交数据所以没问题。关键是千万不要把状态校验放在锁外面。后来我在订单生成接口还加了唯一约束rooms表预留一个pre_order_id字段预定后把订单id写进去在MySQL层面再设一个UNIQUE索引双保险。5.2 CASE 2“住了一晚”到底是23:00-次日12:00还是24小时这个坑极具迷惑性。前台的订单如果按标准计算客人23:00入住次日12:00退房住了一晚但如果客人23:00入住次日晚20:00退房按实际时间差就是21小时可是酒店规则是按“天数”收费这应该算两天吗我最初用datetime.timedelta换算小时数然后除以24算出晚数结果发现这家民宿的老板压根不认这个算法。真实规则是无论几点入住只要在次日12:00前退房都算一晚12:00-18:00退房算一晚加半天18:00以后退房算两晚。所以要建立一个独立的计费函数不能简单用时间差除以每天24小时。import datetime def calc_nights(check_in, check_out, rule_timedatetime.time(12, 0)): # 确保退房日期大于入住日期 days (datetime.datetime.combine(check_out.date(), rule_time) - check_in).days if days 0: return 1 # 不足一天按一天算 hours_over check_out - datetime.datetime.combine(check_out.date(), rule_time) if hours_over datetime.timedelta(hours6): return days Decimal(0.5) else: return days Decimal(1.0)这个算法就是先把“基准时间”定在每天中午12点然后看离店时刻超出了多久。为了确保前端输入合理我在页面用的是日期选择器加“到店时间”“离店时间”后端单独解析不让前台自由填写时间。这个教训告诉我们酒店业务规则永远要按行业习惯建模不要按数学直觉建模。5.3 CASE 3Float算房费出现的0.01元差额有一次对账时发现总营业额比订单明细总额少了0.01元。排查到最后发现是从订单明细到总金额的汇总过程用了Python原生的float相加。例如订单三笔明细价格分别为99.90元、99.90元、99.80元在float里的存储误差累加后round到2位小数就变成了299.60元但实际应为299.60元当时出现的是299.59。单笔差一分几十笔订单汇总后差值就被放大。解决方法只有一条所有涉及钱的变量从数据库读出来后立即转为Decimal计算完成后入库还是Decimal。我在SQLAlchemy模型里把Numeric(10,2)字段映射为Decimal查询结果天然是Decimal所以唯一要防的是在Python业务代码里不要使用float()去转换。另外在页面展示时使用flask模板的format_price过滤器内部调Decimal的quantize绝对不会出现浮点误差。这也是一个非常容易忽略但又极其重要的坑。6. 交付一个能直接跑的项目源码、SQL脚本、文档三件套6.1 项目目录结构与核心文件的职责边界拿到标题“源码数据库文档”的交付要求就要把项目组织得像样不然接手的开发或者老板根本不知道从哪看起。我的最终目录是这样的hotel_manager/ ├── app.py # Flask应用入口蓝图注册、初始化 ├── config.py # 配置文件数据库地址、密钥、规则常量 ├── models/ # SQLAlchemy模型 │ ├── __init__.py │ ├── room.py │ ├── order.py │ └── user.py ├── blueprints/ │ ├── auth.py # 登录/登出 │ ├── room.py # 房态管理 │ ├── order.py # 预订/入住/退房 │ └── stats.py # 统计报表 ├── templates/ # Jinja2模板 │ ├── base.html │ ├── dashboard.html │ ├── room_list.html │ └── order_checkout.html ├── static/ # 静态资源 js/css ├── db/ │ ├── init.sql # 初始化表结构 │ ├── seed.sql # 演示数据 │ └── update_001.sql # 迭代版本增量脚本 ├── docs/ │ ├── 需求说明书.md │ ├── 数据库设计.md │ └── 部署文档.md └── requirements.txt核心逻辑尽量塞在models和blueprints里app.py只做初始化避免一个文件写到2000行。models/下每个表一个文件order.py里包含订单状态机常量和结算函数这样代码结构清晰找问题也快。deliver时把requirements.txt固定好版本像Flask2.3.0、SQLAlchemy1.4.46、PyMySQL1.0.2这能省去后续大量的环境兼容问题。6.2 数据库初始化脚本的多版本管理与演示数据数据库脚本不是一份就够。我在db目录下放了init.sql建表seed.sql填充少量演示数据并且坚持用增量脚本记录每次表结构变更。比如第一次上线后发现orders表缺少actual_check_out字段就写一个update_001.sqlALTER TABLE orders ADD COLUMN actual_check_out DATETIME NULL COMMENT 实际退房时间; ALTER TABLE orders ADD INDEX idx_status_checkout (status, actual_check_out);这样好处是后续你去维护老客户的项目时只要按版本顺序跑增量脚本就能把数据库升到最新。千万别在已有项目的init.sql里直接改字段那样新环境跑没问题老环境补不上。seed.sql也很重要给一套能直接演示的数据5种房型、20个房间、几条不同状态的订单。演示数据要刻意包含“在住”“已退”“预占”三种状态方便打开页面立刻看到效果。注意演示数据里的密码不能是明文我用Flask自带generate_password_hash生成哈希值存到user表里避免文档里出现真实密码。6.3 文档怎么写才能让接手的同学不骂人很多源码包里的文档就是README写两句话太糊弄。我坚持写三份需求说明书、数据库设计文档、部署文档。需求说明书重点第1-2页写出系统边界哪些功能不做哪些功能做前台操作流程是什么。业务规则要写清比如退房延时规则、折扣计算规则。数据库设计文档必须画清楚实体关系用的是Markdown表格加文字说明列出每张表的字段名、类型、是否允许空值、含义、枚举值。特别要把状态枚举表列出来例如room.status0空闲、1预占等。部署文档要写两种环境开发环境直接python app.py跑和生产环境用waitress启动Windows服务然后写清楚如何初始化数据库、修改config.py里的连接串。我见过很多源码包写“部署时请修改数据库配置”然后没有下文。我的习惯是直接在部署文档里贴出完整的config示例把cursorclassdict参数都提前配好。做到这份上交付才算真的完成。7. 上线后还能怎么扩展从半自动化到真正无人值守系统上线稳定跑了一个季度后朋友又提出了几个新需求第一要能在小程序上查剩余房量第二前台的大屏上要滚动显示当天入住率第三需要把订单通过API推给OTA平台。这就意味着原来纯服务端渲染的Flask模板工程要开始接REST接口了。我的做法是保留现有页面不动额外增加一个/api前缀的蓝图把关键查询和操作封装成JSON接口。接口的鉴权同样走JWT但和后台cookie登录分开小程序端用Bearer token头部携带。这一步改动没有颠覆架构因为模型的业务方法都是复用的。另外我引入了简单的缓存层把当天的房态统计结果缓存到内存里设置30秒过期避免小程序端高频轮询把MySQL打疼。如果你的项目未来可能往“多渠道售卖”方向走这里有一个重要建议把房间的“库存”概念从订单里剥离出来。也就是说orders表是客户履约记录rooms_stock表用于渠道路由每个渠道有自己的可售库存前台手动排房占用的库存和OTA渠道锁定的库存分开管理。我当时没有这么做因为初期只是单店直售但如果你预计要接携程、美团这步迟早要做。坦白说酒店客房管理系统功能不难难点全在业务语义的边界和数据一致性上。如果你想拿这套系统练手或者应付验收建议从抄我的设计开始但一定要把“状态机”和“事务边界”想清楚。这两个东西想透了就算你最后写出来的代码一行都不像我这个项目也算真正吃透了。
返回列表