资讯中心

用Python构建小型酒店管理系统:Flask+SQLAlchemy实战全攻略

📅 2026/9/24 18:51:25
用Python构建小型酒店管理系统:Flask+SQLAlchemy实战全攻略
如果你在搜索引擎里搜“酒店管理系统”大概率会先看到一堆商业SaaS产品的报价页面。我这次没碰那些现成系统而是花了两周多时间用Python从零写了一套能实际跑起来的小型酒店管理系统。做这件事的直接原因很简单我认识一位经营民宿的朋友前台每天还在用Excel手工记订单经常出现房间重复预订、退房算错钱这类低级问题。这套系统的目标就很明确把订房、排房、退房结算、房态统计这些高频操作收拢到一个界面里减少人工出错。文章适合有Python入门基础、想通过一个完整项目把知识点串起来的开发者也适合需要一个轻量后台的小型经营业主参考。整个项目从业务调研、数据库设计、功能编码到部署测试每个环节都有值得展开的地方这篇我按实际开发的顺序来拆。1. 选型阶段在想什么谁说酒店管理系统一定得上Java很多开发者的第一反应是“酒店管理系统不是Java的经典课设吗”这个印象不无道理高校课程设计里它确实常和Java Swing、SSH框架绑定。但放到真实的小型业务场景里技术选型应该从问题本身倒推而不是从“哪个技术看起来更高级”去选。1.1 这套系统要解决的真实业务痛点先说需求。一个小型酒店或民宿的日常运转核心就三件事房态要清楚、订单要准确、账要算明白。房态清楚意味着前台任何时候要知道哪间房空着、哪间房被预订、哪间房入住中、哪间房在维修。订单准确意味着同一个日期段不能出现两拨客人订同一个房间。账算明白则是退房的时候系统要根据入住时长、房型价格、可能的加床和超时费用自动计算出最终金额而不是靠人拿计算器按。这些需求罗列出来之后你会发现它的业务逻辑并不复杂难点在于状态多、边界条件多、并发操作会互相干扰。比如两个客人同时在前台订同一间房系统要能拦住其中一个。这种量级的项目Python完全扛得住而且还扛得比Java更省力。1.2 Python选型的理由我选Python的核心理由是开发效率。这个项目从建表到核心功能跑通我大概用了四天如果换成Java要写大量样板代码光是实体类、DAO、Service的层次结构就要折腾不少时间。Python的动态特性让原型迭代特别快今天想加一个“钟点房”规则改一处函数就能验证效果。第二个理由是生态成熟。数据库操作用SQLAlchemyWeb层用Flask写测试用pytest密码哈希用werkzeug自带的工具这些库稳定可靠几乎不需要自己造轮子。尤其是SQLAlchemy它把数据库表映射成Python对象之后业务代码写起来非常直观我下面章节会具体展示。第三个理由可能被很多人忽略Python在数据处理和可视化上的能力。酒店管理系统做到后面一定会有统计需求比如本月入住率、各房型营收占比、每日订单量趋势。这些数据用Python的pandas处理再用ECharts或Matplotlib展示比在Java里拼报表方便太多。我这次就在系统里集成了一个简单的入住率统计页面后续想扩展成完整的BI看板也有基础。1.3 技术栈定案Flask SQLite 服务端渲染最终技术栈我选了Flask SQLAlchemy SQLite前端用Jinja2模板加一点点原生JavaScript。为什么不是DjangoDjango确实大而全自带Admin后台、ORM、迁移工具但对于这种规模的项目有点重而且Flask的灵活度更高每个模块都是我按业务需求搭的代码结构清楚教学演示效果更好。为什么不是前后端分离很多课程设计强行上Vue REST API但前后端分离意味着要处理跨域、Token认证、接口文档、前端构建工具链这些复杂度对于一个小型内部系统是纯负担。我用Flask直接渲染HTML模板一个请求返回一个完整页面表单提交后重定向开发起来不用切换上下文后续如果要迁到前后端分离再把现有视图函数改成返回JSON也容易。SQLite作为数据库开发阶段非常舒服它就是一个文件不需要安装数据库服务。系统上线后如果并发量上来了SQLAlchemy的模型层不用动只改数据库连接串就能切到MySQL这是我把抽象层做出来的价值。2. 数据模型先行四张核心表决定系统上限动工写代码之前我花了大半天时间设计数据库表。这一步特别重要因为绝大多数业务Bug的根源不在代码逻辑而在数据模型没设计好。比如房间状态放错表、订单缺少唯一约束、时间字段没想清楚存日期还是时间戳后面都要付出成倍的代价去改。2.1 从业务流转提炼出的核心实体酒店的业务主线是“房间被预订客人入住客人退房”。围绕这条主线我抽象出了四个核心实体系统用户、房间、客户、订单。系统用户就是登录系统的前台员工和管理员不牵扯复杂的权限体系只需要角色区分和密码保护。房间是酒店的基本资源要记录房号、类型、门市价、可住人数、状态。客户是预订或入住的客人记录姓名、手机号、证件号这些必备信息。订单是业务流转的核心它把客户、房间、时间、价格关联在一起。这四张表的关系很清晰一张订单属于一个客户关联一个房间状态由系统用户操作变更。客户和房间都是多张订单可以共用的所以订单表通过外键引用客户和房间这个关系不需要额外的关联表。2.2 建表语句与字段设计理由我用SQLAlchemy的模型类来定义表结构这里直接写出核心代码并解释每个关键字段的用意。# models.py from datetime import datetime from werkzeug.security import generate_password_hash, check_password_hash from extensions import db class User(db.Model): __tablename__ users id db.Column(db.Integer, primary_keyTrue) username db.Column(db.String(50), uniqueTrue, nullableFalse, indexTrue) password_hash db.Column(db.String(128), nullableFalse) role db.Column(db.String(20), defaultfrontdesk) # admin / frontdesk created_at db.Column(db.DateTime, defaultdatetime.now) def set_password(self, raw_password): self.password_hash generate_password_hash(raw_password) def check_password(self, raw_password): return check_password_hash(self.password_hash, raw_password)用户名必须加唯一约束这是登录系统的底线。密码绝对不能明文存储我用werkzeug的哈希算法把密码转成不可逆的哈希串即使数据库文件泄露也无法直接拿到原始密码。角色字段先只区分管理员和前台后续如果要细分权限再扩展。class Room(db.Model): __tablename__ rooms id db.Column(db.Integer, primary_keyTrue) room_no db.Column(db.String(10), uniqueTrue, nullableFalse, indexTrue) room_type db.Column(db.String(30), nullableFalse) # 标准间/大床房/套房 price db.Column(db.Numeric(10, 2), nullableFalse) # 门市价 max_guests db.Column(db.Integer, default2) status db.Column(db.String(20), defaultavailable) # available/booked/occupied/maintenance房间表的status字段非常关键它就是前台的房态看板数据来源。这里有个设计上的取舍房间的“已预订”和“已入住”到底应该直接改rooms.status还是应该查询订单表动态计算出来我最终采用了“状态冗余”的方式即同时维护rooms.status和订单状态。原因是房态看板需要极快地展示所有房间当前状态如果每次页面刷新都要对订单表做日期范围查询再逐个判断数据量大了会明显变慢而维护房间状态字段退房时在事务里同时更新订单和房间状态简单可靠。维护状态一致性的代价就是下面章节要讲的并发和事务问题。class Customer(db.Model): __tablename__ customers id db.Column(db.Integer, primary_keyTrue) name db.Column(db.String(50), nullableFalse) phone db.Column(db.String(20), nullableFalse) id_card db.Column(db.String(30), indexTrue, nullableFalse) created_at db.Column(db.DateTime, defaultdatetime.now)客户表的id_card加普通索引是为了后续按证件号快速查历史订单。证件号在业务上通常要求唯一但真实场景里存在没带证件、用其他类型证件登记的情况所以我没加唯一约束只在应用层做逻辑校验。class Order(db.Model): __tablename__ orders id db.Column(db.Integer, primary_keyTrue) order_no db.Column(db.String(32), uniqueTrue, nullableFalse, indexTrue) customer_id db.Column(db.Integer, db.ForeignKey(customers.id), nullableFalse) room_id db.Column(db.Integer, db.ForeignKey(rooms.id), nullableFalse) check_in_date db.Column(db.Date, nullableFalse) check_out_date db.Column(db.Date, nullableFalse) actual_check_in_at db.Column(db.DateTime) # 实际上办理入住的时间 actual_check_out_at db.Column(db.DateTime) # 实际退房时间 status db.Column(db.String(20), defaultreserved) # reserved/checked_in/closed/cancelled total_price db.Column(db.Numeric(10, 2), nullableFalse) created_at db.Column(db.DateTime, defaultdatetime.now)订单表是整个系统的核心每个字段都有讲究。order_no用UUID加时间戳生成保证并发下不会重复。check_in_date和check_out_date存的是日期而不是时间戳因为酒店预订粒度是“天”不是“时刻”。actual_check_in_at和actual_check_out_at则存精确时间用于计算钟点房超时费和退房时间差。2.3 状态枚举与流转约束数据表建好后还要定义状态之间的合法流转。房间的状态流转是available空闲→ book被预订→ occupied已入住→ available退房available也可以直接进入occupied当天空房直接入住maintenance是独立状态可以看作手动标记。订单的状态流转是reserved已预订→ checked_in已入住→ closed已结算关闭reserved也可以直接cancelled。这些流转我在Service层用统一的函数处理而不是在每个视图里到处改状态这样规则只维护一份不会出现“这个入口改了状态但忘了改另一个状态”的问题。我在代码里用常量类来管理这些状态值而不是散落字符串。class RoomStatus: AVAILABLE available BOOKED booked OCCUPIED occupied MAINTENANCE maintenance class OrderStatus: RESERVED reserved CHECKED_IN checked_in CLOSED closed CANCELLED cancelled这样做的好处是调用时写RoomStatus.BOOKED而不是直接写booked一旦需要改名或增加状态只有一处要改。而且配合IDE的自动补全能减少拼写错误带来的隐蔽Bug。3. 核心功能编码登录、房态、预订与退房的实现思路数据模型定了开发效率就上来了。我按业务路径把功能分成四个模块登录认证、房态查询、预订、退房结算。每个模块都在视图层和Service层之间做了清晰划分视图层只负责拿参数、校验格式、渲染页面具体业务规则全部下沉到Service层。3.1 登录认证密码不能明文存登录这块我用了Flask-Login来管理会话它帮你处理了session里的用户状态、登录态保持、登出这些重复劳动。密码验证就用模型层写好的check_password方法。auth_bp.route(/login, methods[GET, POST]) def login(): form LoginForm() if request.method POST and form.validate_on_submit(): user User.query.filter_by(usernameform.username.data).first() if user and user.check_password(form.password.data): login_user(user) return redirect(url_for(dashboard.index)) flash(用户名或密码错误, danger) return render_template(login.html, formform)登录页面有几个交互细节值得说。第一登录失败提示要统一不要透露出“用户不存在”还是“密码错误”否则容易被枚举出有效用户名。第二登录成功后应立即跳转到工作台显示屏上不要停留在登录页。第三Flask-Login需要配合SECRET_KEY配置这个Key在部署时必须换成随机长字符串不要用默认值。3.2 房态查询与展示工作台首页就是房态总览。它的核心是一个按房间分组的状态矩阵经典做法是把所有房间按楼层横排、按状态色块展示。我实现了两个展示维度一个是“当前实时状态”的网格另一个是“指定日期范围的可用房查询”。实时房态查询很简单就是查询rooms表按状态分组。rooms Room.query.order_by(Room.room_no).all()用房间状态字段直接打标。可用房查询则要复杂一些它需要查订单表找出在目标日期段内已经占用或预订的房间再从全量房间中排除掉。这个查询就是上面提到的日期重叠判断我会在第四章单独讲。3.3 预订与退房的Service层逻辑预订流程是系统里最重要的环节。它不是一个简单的INSERT而是一系列原子操作检查房间在目标日期段是否可订计算总价插入订单记录更新房间状态而且这些操作必须放在同一事务里任何一个环节失败都要整体回滚。def create_reservation(room_id, customer_data, check_in, check_out): room Room.query.get(room_id) if not room: raise BusinessError(房间不存在) # 日期合法性 if check_in check_out: raise BusinessError(入住日期必须早于退房日期) # 房间在目标日期段是否可订 if not is_room_available(room_id, check_in, check_out): raise BusinessError(该房间在目标日期段已被预订或入住) # 计算总价这里先按整晚计价舒适版再引入钟点房 nights (check_out - check_in).days total_price room.price * nights # 创建客户如果手机号已存在则复用 customer Customer.query.filter_by(phonecustomer_data[phone]).first() if not customer: customer Customer(**customer_data) db.session.add(customer) db.session.flush() order Order( order_nogenerate_order_no(), customer_idcustomer.id, room_idroom.id, check_in_datecheck_in, check_out_datecheck_out, total_pricetotal_price ) db.session.add(order) room.status RoomStatus.BOOKED db.session.commit() return order这套逻辑里有个细节新增客户后要调db.session.flush()它的作用是把customer数据先写入数据库并拿到自增id但还没有真正提交事务这样order在引用customer_id时不会拿到None。这类“会话内先写后读”的操作新手经常栽在这里。退房结算函数和预订对称它要做的是把订单状态置为closed把房间状态改为available计算实际应付金额如果超时还要加费写入相关记录。这里同样要在一个事务里完成绝不能让“订单关闭了但房间还是occupied”这种不一致状态出现。4. 开发中最容易翻车的三个细节日期冲突、并发、计价这个项目写完后我复盘了一遍排在前面的坑几乎都集中在三个地方日期重叠判断、并发环境下的数据竞争、房价计算规则。这三个问题如果只靠“写个方法跑通一次”根本发现不了必须靠完整的测试用例和并发模拟去逼出来。4.1 日期重叠判断的边界条件判断一个房间在某个日期段是否空闲核心SQL逻辑看起来简单但边界条件极容易写错。正确的重叠判断条件是两个窗口存在交集新订单的入住日小于已有订单的退房日并且新订单的退房日大于已有订单的入住日同时把已取消的订单排除掉。def is_room_available(room_id, check_in, check_out): overlapping Order.query.filter( Order.room_id room_id, Order.status.in_([OrderStatus.RESERVED, OrderStatus.CHECKED_IN]), Order.check_in_date check_out, Order.check_out_date check_in ).first() return overlapping is None我第一次写的时候用的是check_in_date check_out结果出现了相邻日期被误判为重叠的BugA订单是1号到2号新订单是2号到3号正常情况1号住一晚、2号让新房客进来这是合法的但判断把退房日和新入住日重叠也算成了冲突。正确逻辑里只要新订单入住日不小于已有订单退房日就没有冲突。这里还有一个容易漏的点房间状态字段与订单重叠判断之间的一致性。假如房间状态是available但order表里还躺着一条未关闭的旧订单查询时就会漏判。所以我要求所有流程必须走Service层函数不能绕过检查直接改数据库否则状态字段和订单表迟早会不一致。4.2 并发预订下的事务保护我一直以为SQLite单文件数据库不会有并发问题直到用两个浏览器窗口同时提交同一个房间的预订才发现糟糕的情况两个请求都通过了is_room_available检查都插入了订单最终把同一间房卖给了两拨客人。这就是经典的并发竞态条件。原因在于检查房间可用性和插入订单是两步操作两个请求可能同时通过检查。解决思路不是把检查条件写得更严格而是要让“检查占用”成为一个原子操作。在关系型数据库里最有效的做法是使用行级锁或事务隔离。SQLAlchemy提供了with_for_update()来实现SELECT ... FOR UPDATE在MySQL和PostgreSQL中它会给查询到的行加锁另一个事务在锁释放前必须等待。但SQLite对并发写入的隔离级别更弱所以我换了一个更稳妥的思路把状态更新操作设计成条件更新通过“比较并交换”让数据库层面的约束去兜底。result Room.query.filter( Room.id room_id, Room.status.in_([RoomStatus.AVAILABLE, RoomStatus.BOOKED]) ).update({status: RoomStatus.BOOKED}, synchronize_sessionFalse) if result 0: raise BusinessError(房间状态已变化请刷新后重试)这条语句的含义是只有房间当前状态是可用或已预订时才把它更新为已预订update影响行数为0就说明状态已经不是预期值说明被别的请求抢先占用直接拒绝。配合数据库在事务里update行会加锁的机制两个并发请求最终只有一个能成功更新另一个会拿到0。这类问题的通用解法我总结成一句话不要“先查再改”而要“条件更新检查影响行数”。把判断放进写操作里让数据库的锁机制来保护。4.3 房价计算规则的落地房价看起来就是“单价乘以晚数”但真实场景比这个复杂。酒店房费存在几种常见情况全日房按晚计价、半天房/钟点房按小时计价、超时退房要加收费用、跨天入住的日期怎么算。我的计价函数设计成面向规则的def calc_order_price(room_price, check_in_date, check_out_date, actual_check_in_atNone, actual_check_out_atNone): price Decimal(0.00) # 整晚基础费用 nights (check_out_date - check_in_date).days if nights 0: price Decimal(room_price) * nights # 实际入住时间晚于预订日产生钟点房占用 if actual_check_in_at and actual_check_out_at: actual_hours (actual_check_out_at - actual_check_in_at).total_seconds() / 3600 planned_hours nights * 24 if actual_hours planned_hours: extra_hours actual_hours - planned_hours # 超时费按门市价的1/8每小时计相当于3小时算半天 price Decimal(room_price) * Decimal(str(extra_hours / 8)) return price这个函数里我故意把“超时费”设计成按百分比而不是写死一个数字金额是为了让后续灵活调价。实际项目中这些规则应该尽量做成可配置项不要硬编码在代码里否则销售策略一变就得改代码重新部署。测试这块我建议把计价逻辑抽成纯函数这样不需要数据库和页面环境就能用pytest做单元测试。我后面专门写了十几个测试用例覆盖这种边界场景比如预订日期和实际入住日期不一致、退房超时跨天、免费取消截止时间等。5. 界面不要丑到没人用房态看板与数据可视化功能逻辑再正确界面如果难用前台宁可回到Excel去。这套系统的界面我实打实调了好几天重点放在房态看板和统计报表上。5.1 为什么选服务端渲染而非前后端分离我在第一章提到用服务端渲染这里补充界面层面的理由。这种内部管理系统不需要像C端产品那样频繁重渲染服务端渲染可以让每个页面都是完整的HTML文档刷新按钮就能拿到最新房态心智负担小。而且Flask可以直接在Jinja2模板里写循环、条件判断、格式化时间非常顺手。比如房态看板的核心是遍历所有房间并按状态输出色块{% for room in rooms %} div classroom-card {{ room.status }} span classroom-no{{ room.room_no }}/span span classroom-type{{ room.room_type }}/span /div {% endfor %}再配一条状态到CSS颜色的映射available是绿色、booked是黄色、occupied是红色、maintenance是灰色。前台人员扫一眼就能看清整个楼层还有几间空房这是我做这个界面时最直观的收获。5.2 房态看板的交互设计与统计图表光有静态色块还不够我看市面上的商业系统都会支持“点选房间查看订单详情”。我也做了一个房间卡片点击弹窗展示这个房间未来七天的预订情况。弹窗的数据靠一个JSON接口返回dashboard_bp.get(/api/room/int:room_id/upcoming) def room_upcoming(room_id): today date.today() end_date today timedelta(days7) orders Order.query.filter( Order.room_id room_id, Order.status.in_([OrderStatus.RESERVED, OrderStatus.CHECKED_IN]), Order.check_in_date end_date, Order.check_out_date today ).all() data [{check_in: o.check_in_date.isoformat(), check_out: o.check_out_date.isoformat(), customer: o.customer.name} for o in orders] return jsonify(data)前端拿到数据后用少量JavaScript生成一个时间轴列表。这个功能让前台不用反复去订单列表里翻直接点房间就能看到什么时候空出来实际使用中反馈很好算是我这版比较出彩的交互。统计报表这块我做了两个页面月度入住率趋势和房型营收排行。入住率按“当月已售间夜数除以当月可售间夜数”计算SQL查询直接按天聚合订单数据。营收排行按房型分组、对订单金额求和。图表展示我用ECharts的CDN引入两个折线图和柱状图画面变化一下前台的日报周报就不用自己拉了。5.3 表单校验与错误提示界面层另一个被低估的是表单校验。我并没有只依赖前端JavaScript校验而是用Flask-WTF在服务端做了一层完整校验。原因是前台人员可能会用不同浏览器或者发起接口请求跳过页面服务端校验是最后一道防线绝不能省。错误提示我用Flash消息统一渲染在页面顶部颜色区分成功和失败。表单输入错误时会把已经填的内容回显不让用户重填一遍。这些交互细节看着小但真实使用中直接决定前台愿不愿意用这个系统。6. 测试、环境配置与上线的个人复盘功能全部写完只是第一步真正让我放心的是测试和环境部署。这部分经验同样值得记录尤其是没有人告诉你“项目做完之后会卡在哪”。6.1 用pytest守住核心逻辑我在项目里引入pytest给Service层的核心函数写了十多个测试用例覆盖这几大类登录密码校验、日期重叠判断的所有边界、预订成功与预订冲突、退房结算的超时加费、取消订单对房间状态的释放。举一个日期测试用例它把各种边界都摆出来import pytest from datetime import date pytest.mark.parametrize(existing_in, existing_out, new_in, new_out, expected, [ (date(2025, 5, 1), date(2025, 5, 3), date(2025, 5, 3), date(2025, 5, 5), True), (date(2025, 5, 1), date(2025, 5, 3), date(2025, 5, 2), date(2025, 5, 4), False), (date(2025, 5, 1), date(2025, 5, 3), date(2025, 5, 1), date(2025, 5, 3), False), (date(2025, 5, 1), date(2025, 5, 3), date(2025, 5, 5), date(2025, 5, 6), True), ]) def test_room_availability(existing_in, existing_out, new_in, new_out, expected): assert is_date_range_available(existing_in, existing_out, new_in, new_out) is expected这类测试的价值在于当以后有人改了Service层代码跑一遍测试就能知道有没有破坏原有业务规则我从一开始就维护测试用例系统出Bug的概率明显下降。6.2 从零配置Python环境到跑起来简明版如果你是按这篇文章自己动手实践环境配置我建议直接按这个顺序来先安装Python 3.10以上的版本安装时勾选“Add Python to PATH”避免后面命令行找不到命令。然后创建虚拟环境在项目目录执行python -m venv venv再把Flask、SQLAlchemy这些依赖装进去。用VS Code的话CtrlShiftP调出命令面板搜索“Python: Select Interpreter”选中虚拟环境的Python就能正常调试了。我用一个requirements.txt锁定依赖版本flask3.0.0 flask-sqlalchemy3.1.1 flask-login0.6.3 flask-wtf1.2.1 pytest8.0.0固定版本非常重要我遇到过直接pip install flask装到新版导致SQLAlchemy API不兼容的问题锁定版本可以完全避免。6.3 部署上线与后续迭代建议部署阶段我把SQLite换成了MySQL因为线上环境是Linux服务器SQLite单文件的并发写能力确实扛不住多前台同时操作。操作方式不复杂在SQLAlchemy配置里把SQLite连接串换成MySQL连接串然后执行db.create_all()重新建表再写一个脚本把SQLite里的历史数据迁移过去。Web服务我用gunicorn做WSGI容器前面再挂一个nginx做反向代理。Python项目部署最常踩的坑是静态文件和模板路径问题Flask的静态文件如果启用了CDN或nginx接管页面里的CSS和JS要用url_for(static, filename...)来生成路径不能写死。上线后我复盘了几条可迭代方向。数据备份要自动化我写了一个mysqldump定时任务每天凌晨备份权限拆分可以更细比如店长能看营收、前台只能管房态和订单房态看板可以加“未来30天预订预报表”帮经营者在淡旺季做定价参考。小型系统的优势就在这里每个新需求都可以快速在Python里迭代上线不用经历大型项目的漫长发布流程。这套系统做完后最深的感受是技术栈本身不复杂真正的复杂度都在业务规则和边界处理上。如果你也想用Python练一个完整的实战项目我建议别做那些网上抄来抄去的管理系统模板而是找一个真实的业务场景把数据模型、核心流程、并发事务、测试部署整条链路跑通收获远比看十个教程都大。

看完文章,想为自己的企业也做一次专业网站诊断?

尧图顾问免费为您评估现有网站,并给出建站/改版建议与报价方案。

免费获取方案