ARTICLE DETAIL

资讯详情

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

基于Flask的连锁商务酒店管理系统:业务架构与工程实现

基于Flask的连锁商务酒店管理系统:业务架构与工程实现 1. 先想清楚连锁酒店管理系统到底要管什么做酒店管理系统这个活儿真正考验人的不是CRUD而是业务边界的理解。项目标题写的是“Python基于flask的连锁商务酒店管理系统”我第一反应不是Flask怎么用而是“连锁”这两个字背后的复杂度。单体酒店只需要管好一间门店的房态和订单连锁商务酒店面对的是多门店数据隔离、跨店预订、房态同步、集团报表这一整套麻烦事。把这些问题拆解清楚之后再回到技术选型你会发现Python和Flask的组合反而非常合适Flask足够轻适合把业务逻辑一层层叠起来Python开发效率高适合这种业务逻辑密集、管理后台居多、交互复杂度有限的系统。网站上能搜到很多酒店管理源码但大多数只是“房间列表 登记退房”。你拿去给连锁商务酒店用第一天就会被店长和值班经理骂回来。为什么因为商务连锁酒店的利益重心不在单店操作而在总部管控同一品牌下十几家甚至上百家门店每个门店有独立前台和店长但房价策略、会员体系、预订渠道、经营报表是总部统一的。你要做的不是一张排房表而是一套能处理“统一标准、分店运营”的业务系统。1.1 连锁场景和单体酒店的本质区别先说个最简单的例子一个白金会员在A酒店办理入住时问前台能不能顺便帮忙订B酒店明天的大床房这种跨店预订请求单体酒店的管理系统根本不存在对应功能前台只能手动记在纸上然后电话通知B店。但如果系统支持连锁管理前台在同一个界面里就能查B店的房态、下单、扣减B店余房——这才是连锁系统存在的意义。另一个典型场景是集团日报。总部运营每天早上要看到所有门店昨晚的出租率、平均房价、RevPAR变化还要对比上周同期。单体系统只能一家店一家店地看而连锁系统要能在一个页面上出全集团的横向对比表。所以我在设计这个系统时把“多门店数据聚合”作为和“前台操作”等同级的一等公民需求而不是事后补丁。连锁商务酒店的第三个特点是“商务客源占比高”。这部分客人的预订习惯很固定提前1到3天通过电话或APP订房入住时间集中在晚上6点到10点且很少现场砍价。这意味着系统的预订核心是“锁房”而不是“卖房”库存控制比价格浮动的优先级更高。这也是后面我在数据库层引入锁机制的根本原因。1.2 Flask在这里到底承担什么角色很多初学者以为Flask是一个能“撑起整套系统”的大框架其实不是。Flask本质上是WSGI之上的一个HTTP路由和视图渲染层它负责接收浏览器请求、匹配URL到Python函数、调用业务逻辑、返回HTML或JSON。真正的酒店业务逻辑——房态计算、订单状态流转、价格汇总——全部要自己用Python写Flask不帮你做。这个特性对酒店管理系统来说是优点不是缺点。酒店管理的业务规则非常“私有化”每个品牌的计费规则、会员折扣、协议房配额都不同市面上没有哪个ORM或第三方库能统一覆盖。用Flask这种轻框架业务层可以完全自定义不被框架绑架。而且Python的动态特性让你可以随时调整数据模型和业务方法这在需求频繁变动的酒店项目里非常实用。这套系统的整体调用链路是前端模板Jinja2发起请求 → Flask路由接收 → 调用service层业务方法 → SQLAlchemy读写数据库 → 返回结果渲染页面。Flask在中间扮演的是“调度员”不是“业务员”。理解这一点你就不会把大量业务逻辑塞进视图函数里后面扩展权限、报表、对接第三方渠道时才不会翻车。2. 工程架构Flask项目不能一锅炖2.1 目录结构与蓝图划分我刚做第一个Flask项目时把所有路由写在一个app.py里到后来文件三千行改一个功能要反复搜索“哪个函数叫这个名字”非常痛苦。这个连锁酒店系统涉及前台操作、后台管理、报表统计、权限认证四大块如果用单文件开发维护成本直接失控。我的做法是Flask官方推荐的蓝图Blueprint模式按业务模块横向切分hotel_system/ ├── run.py # 应用启动入口 ├── config.py # 环境配置数据库、Redis、密钥 ├── extensions.py # db、login_manager等扩展实例 ├── models/ # SQLAlchemy模型 │ ├── __init__.py │ ├── hotel.py # 酒店、房型、房间 │ ├── reservation.py # 预订、订单、账务 │ └── user.py # 员工、角色、权限 ├── views/ # 蓝图视图 │ ├── __init__.py │ ├── auth.py # 登录认证 │ ├── frontdesk.py # 前台操作 │ ├── admin.py # 集团管理 │ └── report.py # 报表统计 ├── services/ # 业务逻辑层 │ ├── inventory.py # 房态库存逻辑 │ ├── pricing.py # 房价计算 │ └── billing.py # 账务结算 ├── templates/ # Jinja2模板 ├── static/ # 静态资源 └── requirements.txt按业务切分而不是按“Model/View/Controller”这种纯技术分层切是因为酒店系统的业务模块之间耦合点很明确前台要调房态报表要调订单权限要控制两者。按业务域聚合改动一个业务流程时影响范围基本落在一个目录里。2.2 数据库选型与连接配置酒店管理系统对事务一致性要求很高尤其是预订和退房环节绝不能出现“款收了但房没释放”的情况。所以数据库首选MySQL InnoDB而不是SQLite或PostgreSQL之外的什么偏门方案。开发阶段用SQLite跑通逻辑是没问题的但上线前一定要迁移到MySQL因为SQLite对行级锁和高并发写入的支持太弱。连接配置上有一个常被忽略的点连接池大小。连锁酒店前台并发量并不高一台门店终端大概每5秒才有一个操作请求但报表页面会在早高峰集中拉取数据。如果把连接池设太大MySQL默认的max_connections只有151很容易把数据库拖垮。我的配置是# config.py class Config: SQLALCHEMY_DATABASE_URI mysqlpymysql://hotel_user:password127.0.0.1:3306/hotel_db?charsetutf8mb4 SQLALCHEMY_ENGINE_OPTIONS { pool_size: 10, max_overflow: 5, pool_recycle: 300, pool_pre_ping: True, }pool_size设为10意思是常驻连接10个溢出最多再加5个总共15个。对门店规模在50家以内的连锁品牌这个配置足够支撑早高峰。pool_recycle设为300秒是为了避免MySQL的wait_timeout把闲置连接断开这问题我在项目上线后踩到过后面会专门讲。2.3 用工厂模式初始化应用应用入口不要直接实例化Flask而是用一个create_app工厂函数。这样做的最大好处是测试时能换配置不同环境开发、测试、生产不共享状态也不会出现“测试库数据污染生产库”的手滑操作。# run.py from flask import Flask from extensions import db, login_manager from config import Config def create_app(config_classConfig): app Flask(__name__) app.config.from_object(config_class) db.init_app(app) login_manager.init_app(app) from views.auth import auth_bp from views.frontdesk import frontdesk_bp from views.admin import admin_bp from views.report import report_bp app.register_blueprint(auth_bp, url_prefix/auth) app.register_blueprint(frontdesk_bp, url_prefix/frontdesk) app.register_blueprint(admin_bp, url_prefix/admin) app.register_blueprint(report_bp, url_prefix/report) with app.app_context(): db.create_all() return app app create_app() if __name__ __main__: app.run(host0.0.0.0, port5000, debugFalse)注意工厂函数里注册蓝图的顺序把auth放最前面因为其他业务蓝图里的视图函数可能会引用登录用户信息。虽然Flask不强制要求注册顺序但按依赖关系排序会让代码阅读者更容易理解模块边界。3. 数据模型设计把账算清楚后面才不乱3.1 核心表结构设计数据模型是酒店管理系统的地基地基打歪了后面所有功能都是空中楼阁。我这套系统的核心表有五张酒店表、房型表、房间表、预订订单表、账务流水表。额外还有员工表和角色表用于权限控制。# models/hotel.py class Hotel(db.Model): __tablename__ hotel id db.Column(db.Integer, primary_keyTrue) name db.Column(db.String(64), nullableFalse, uniqueTrue) brand db.Column(db.String(32)) # 所属品牌 address db.Column(db.String(128)) city db.Column(db.String(32)) status db.Column(db.SmallInteger, default1) # 1营业 0停业 class RoomType(db.Model): __tablename__ room_type id db.Column(db.Integer, primary_keyTrue) hotel_id db.Column(db.Integer, db.ForeignKey(hotel.id)) name db.Column(db.String(32)) # 大床房、标准间、商务套房 base_price db.Column(db.Numeric(10, 2)) # 门市价 member_price db.Column(db.Numeric(10, 2)) # 会员价 total_rooms db.Column(db.Integer) # 该房型总数 max_guests db.Column(db.Integer) # 最多入住人数 class Room(db.Model): __tablename__ room id db.Column(db.Integer, primary_keyTrue) hotel_id db.Column(db.Integer, db.ForeignKey(hotel.id)) room_type_id db.Column(db.Integer, db.ForeignKey(room_type.id)) room_no db.Column(db.String(16)) # 物理房间号 例如 1208 status db.Column(db.String(16), defaultvacant) # vacant空闲 occupied占用 cleaning清理 repair维修设计时我坚持了两个原则一是所有业务表都带hotel_id这是连锁系统数据隔离的基石二是房间表的status字段只表示“当前物理状态”不代表“可售状态”。可售状态要结合订单和房态库存动态计算不能直接查房间表否则会出现“一个房间被重复预订”的尴尬。3.2 订单状态机与房价计算订单表是整个系统的核心我把订单状态设计成一个严格的状态机不允许随意跳转pending待确认→ confirmed已确认→ checked_in已入住→ checked_out已退房/已结账 ↓ ↓ cancelled已取消 no_show未到店每个状态之间的跳转都对应一个业务操作pending到confirmed是前台确认预订confirmed到checked_in是客人办理入住checked_in到checked_out是退房结账。取消操作只允许在pending和confirmed状态执行入住后想取消只能按“提前退房”处理要收取相应费用。订单模型大概是这样的# models/reservation.py class Reservation(db.Model): __tablename__ reservation id db.Column(db.Integer, primary_keyTrue) order_no db.Column(db.String(32), uniqueTrue) # 订单号 hotel_id db.Column(db.Integer, db.ForeignKey(hotel.id)) guest_name db.Column(db.String(32)) guest_phone db.Column(db.String(20)) room_type_id db.Column(db.Integer, db.ForeignKey(room_type.id)) room_count db.Column(db.Integer, default1) check_in_date db.Column(db.Date) check_out_date db.Column(db.Date) nights db.Column(db.Integer) # 预订晚数 status db.Column(db.String(16), defaultpending) total_amount db.Column(db.Numeric(10, 2)) # 订单总额 discount_amount db.Column(db.Numeric(10, 2), default0) created_at db.Column(db.DateTime, defaultdatetime.utcnow)房价计算不要在视图函数里写死而是放到services/pricing.py里统一管理。因为商务酒店的计价规则非常灵活门市价、会员折扣、协议公司价、连住优惠、早到店加收、延迟退房加收这些规则叠加起来很复杂如果散布在多个视图里改一个规则全局都要排查。核心计算逻辑我用一个简单的函数实现# services/pricing.py def calc_total_amount(room_type, member_level, check_in_date, check_out_date, room_count, is_company_protocolFalse): nights (check_out_date - check_in_date).days if nights 0: raise ValueError(离店日期必须晚于入住日期) price room_type.base_price if is_company_protocol: price room_type.protocol_price # 协议价 elif member_level gold: price room_type.member_price * 0.9 # 金卡再享9折 amount price * nights * room_count return round(amount, 2), nights这里有个实操细节nights的计算直接用date相减得到天数非常直观。但如果check_in_date和check_out_date是datetime类型相减会带上时分秒会出现“明明是两晚却算出1.99晚”的怪问题。所以我在模型里强制用Date类型从源头掐断这个隐患。3.3 字段设计里几个踩过的坑金额字段必须用Numeric(10,2)不要用Float。浮点数的二进制存储特性会导致0.10.2不等于0.3的问题而酒店账单里每一分钱都涉及客人权益和财务审计会计不会接受“因为浮点数误差所以少收了1分”的说法。用Decimal可以精确控制小数位RoundingMode设置为ROUND_HALF_UP。状态字段不要用裸数字。我见过有源码用0、1、2表示订单状态最后没人知道0是“已取消”还是“已退款”。我的做法是在模型层定义常量类比如OrderStatus.PENDING pending视图层引用这个类而不是直接写字符串既便于IDE自动补全也避免手滑写错值。房间号不要用数据库自增id代替。业务上客人订的是“1208房”这个真实房号如果数据库id和房号不一致前台打电话给客房部报房号时就会出现“系统显示1208但保洁去了1210”的事故。我的做法是房间表同时保留自增id做外键关联room_no字段单独存物理房号页面展示一律用room_no。4. 核心业务实现预订、入住、退房全流程4.1 实时房态与锁房机制前面说过可售状态不能光看Room.status而是要看“某房型在某天还剩多少间可卖”。我专门设计了一张房态库存表按“酒店房型日期”记录占用数# models/inventory.py class RoomInventory(db.Model): __tablename__ room_inventory id db.Column(db.Integer, primary_keyTrue) hotel_id db.Column(db.Integer, db.ForeignKey(hotel.id)) room_type_id db.Column(db.Integer, db.ForeignKey(room_type.id)) biz_date db.Column(db.Date) # 营业日期 total db.Column(db.Integer) # 该房型总数 booked db.Column(db.Integer, default0) # 已订出间数这张表在系统启动时自动初始化为未来60天每天的每个房型生成一条记录。这样余房数的计算就是一条简单SQLtotal - booked。酒店管理系统的高并发写入集中在“预订”这一个动作上如果两条请求同时预订同一间房的最后剩余房量必须有锁保证只能成功一个。我的方案是MySQL的悲观行锁# services/inventory.py from sqlalchemy import func from extensions import db def deduct_inventory(hotel_id, room_type_id, biz_date, count): with db.session.begin(): inv RoomInventory.query.filter_by( hotel_idhotel_id, room_type_idroom_type_id, biz_datebiz_date ).with_for_update().first() if not inv: raise Exception(库存记录不存在请检查初始化任务) remaining inv.total - inv.booked if remaining count: raise Exception(f余房不足{biz_date} 仅剩 {remaining} 间) inv.booked count return inv.bookedwith_for_update是InnoDB的行级排他锁先锁住这行再检查余房、扣减、提交事务整个过程串行化。这里有一个坑filter_by的查询条件必须命中索引否则InnoDB会升级为锁全表导致所有预订互相阻塞。我对(hotel_id, room_type_id, biz_date)建了联合唯一索引既保证了数据唯一性又让行锁精准落在目标记录上。4.2 订单创建与支付流程一个完整的预订流程是这样前台或官网客人选择入住日期、退房日期、房型、房间数系统先调库存表校验余房再调价格计算函数得到总价最后创建订单并扣减库存。整个过程必须在同一个事务里完成不能出现“订单建好了但库存扣减失败”或“库存扣了但订单没建成功”的情况。# services/reservation_service.py def create_reservation(data): room_type RoomType.query.get(data[room_type_id]) amount, nights calc_total_amount( room_type, data[member_level], data[check_in_date], data[check_out_date], data[room_count] ) order_no generate_order_no() # 规则门店号日期序列号 reservation Reservation( order_noorder_no, hotel_iddata[hotel_id], guest_namedata[guest_name], guest_phonedata[guest_phone], room_type_idroom_type.id, room_countdata[room_count], check_in_datedata[check_in_date], check_out_datedata[check_out_date], nightsnights, total_amountamount, statuspending ) db.session.add(reservation) # 逐日扣减库存 for i in range(nights): biz_date data[check_in_date] timedelta(daysi) deduct_inventory(data[hotel_id], room_type.id, biz_date, data[room_count]) db.session.commit() return reservation特别注意预订跨N晚要循环扣减N天的库存而不是只扣入住当天的。很多新手在这里只扣了第一天的库存导致第二天该房型显示“有房”实际上房间早被这个订单占住了。订单号生成规则我建议用“门店号日期四位流水号”比如“SH001-20240318-0007”。这个规则的好处是集团客服接到投诉电话时听到订单号就能立刻判断是哪个门店的订单不需要再查数据库。4.3 入住、退房与账务结算入住操作的核心动作是把预订状态从confirmed改为checked_in把具体的房间标记为occupied。这里需要注意预订时只锁了房型库存没有锁具体房间号所以入住时系统要先找一个该房型下的空闲房间。找房原则是“优先分配低楼层、靠近电梯”还是“优先分配高楼层”各酒店策略不同我在系统里做成可配置的分配策略。退房结账是整个系统里最容易出乱子的环节。客人在入住期间的额外消费——早餐、MiniBar、洗衣费、损坏赔偿——都要记录到账务流水表。退房时统一汇总生成账单# services/billing.py def checkout(reservation_id, extra_chargesNone, late_checkout_hours0): with db.session.begin(): reservation Reservation.query.filter_by(idreservation_id).with_for_update().first() if reservation.status ! checked_in: raise Exception(当前状态不可退房) total_extra 0 for item in (extra_charges or []): record AccountRecord( order_idreservation.id, item_nameitem[name], amountitem[amount], record_typeextra ) db.session.add(record) total_extra item[amount] # 延迟退房加收超过14:00按半天门市价超过18:00按全天门市价 if late_checkout_hours 2: late_fee reservation.total_amount / reservation.nights record AccountRecord(...) total_extra late_fee reservation.status checked_out # 释放房间状态 for room in Room.query.filter_by(hotel_idreservation.hotel_id, room_type_idreservation.room_type_id, statusoccupied).all(): room.status vacant退房顺序有一个原则必须遵守先算账、再改订单状态、最后释放房间。如果先把房间释放了另一个前台同事恰好在这个间隙给新客人分配了这个房间后面对账就会发现“同一间房同一晚卖出去了两次”。先把账算完锁定订单状态再释放房间顺序就安全了。5. 连锁管理多门店的数据隔离与汇总5.1 多门店数据隔离方案连锁系统的第一难题是数据隔离集团管理员能看所有门店数据店长只能看自己门店前台只能操作自己门店的日常功能。如果隔离做不好要么A店长能查到B店的经营数据要么集团管理员反而看不到明细两种情况都是运营事故。我的方案是“hotel_id贯穿始终 service层强制过滤”。每个员工的会话里保存当前所属hotel_id所有业务查询方法都接受一个current_hotel_id参数在ORM查询语句里强制加上filter_by(hotel_idcurrent_hotel_id)。这个过滤必须写在service层不能寄希望于视图层每个查询都记得写条件。# services/base_service.py class BaseService: def __init__(self, current_hotel_idNone, current_userNone): self.hotel_id current_hotel_id self.user current_user self.is_group_admin self.user and self.user.role group_admin def _scoped_query(self, model): if self.is_group_admin: return model.query return model.query.filter_by(hotel_idself.hotel_id)_group_scoped_query这个方法会在所有查询入口调用集团管理员不加过滤条件门店用户自动加上hotel_id限制。这样即使某个前端页面忘了传参数数据也不会越权泄露。5.2 跨店查询与集团报表报表模块是连锁酒店管理系统的价值核心。管理层每天最关心三个指标出租率OCC、平均房价ADR、每间可售房收入RevPAR。计算公式分别是OCC 已售房晚数 / 可供房晚数ADR 客房总收入 / 已售房晚数RevPAR 客房总收入 / 可供房晚数 OCC × ADR我建了一张日报汇总表每天凌晨用定时任务跑前一天的数据按酒店日期房型维度汇总而不是在报表页面实时扫描订单明细。因为订单表数据量大实时聚合会拖垮数据库日报表一张表只有几百行查询任何时间范围的趋势都很快。# services/report_service.py def build_daily_report(biz_date): # 按酒店聚合已售房晚和收入 rows db.session.query( Reservation.hotel_id, func.count(Reservation.id).label(sold_room_nights), func.sum(Reservation.total_amount).label(room_revenue) ).filter( Reservation.check_in_date biz_date, Reservation.check_out_date biz_date, Reservation.status.in_([checked_in, checked_out]) ).group_by(Reservation.hotel_id).all() for row in rows: hotel Hotel.query.get(row.hotel_id) available_rooms sum(rt.total_rooms for rt in RoomType.query.filter_by(hotel_idhotel.id)) occ row.sold_room_nights / available_rooms adr row.room_revenue / row.sold_room_nights if row.sold_room_nights else 0 revpar row.room_revenue / available_rooms # 写入 daily_report 表这里统计“sold_room_nights”时用了“入住日期目标日期且离店日期目标日期”这个区间判断条件能正确统计跨夜订单。踩过的坑是如果用check_in_date biz_date作为判断条件一个3月18日入住、3月20日离店的订单在3月19日的报表里就会被漏统计。5.3 权限模型设计权限模型我用的是经典RBAC用户 → 角色 → 权限。三个内置角色frontdesk前台只能操作本店预订、入住、退房不能看报表和改房价manager店长本店全部操作加上房价调整和门店日报group_admin集团管理员跨店查询、集团报表、开新店、调配资源Flask-Login负责登录状态管理自定义装饰器做角色校验# utils/decorators.py from functools import wraps from flask import abort from flask_login import current_user def role_required(*roles): def decorator(fn): wraps(fn) def wrapper(*args, **kwargs): if current_user.role not in roles: abort(403) return fn(*args, **kwargs) return wrapper return decorator # views/admin.py app_bp.route(/hotel/new, methods[POST]) login_required role_required(group_admin) def create_hotel(): ...这个方案对内部管理系统够用且清晰。要注意的是装饰器只能做粗粒度控制界面入口可见性细粒度的数据行过滤还是要靠5.1里的service层scope机制。两个机制配合起来权限体系才算完整。6. 部署上线与问题排查实录6.1 用GunicornNginx部署Flask开发环境用flask run跑内置服务器没问题但生产环境绝对不能这么干。Flask内置服务器是单进程、开发级的既扛不住并发也没有安全加固。我生产环境的方案是Gunicorn Nginx反向代理。启动命令gunicorn -w 4 -b 127.0.0.1:8000 run:create_app()-w 4表示启动4个worker进程每个进程独立处理请求。worker数不是越多越好经验值是2到4倍CPU核心数再多反而会因为GIL竞争和内存占用造成性能下降。我的一台4核服务器用4个worker实测QPS在500左右对酒店前台系统完全够用。Nginx配置一个简单的反向代理加静态资源处理server { listen 80; server_name hotel.example.com; location / { proxy_pass http://127.0.0.1:8000; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; } location /static/ { alias /opt/hotel_system/static/; } }静态文件交给Nginx直出不要让Flask处理能省掉大量无意义CPU开销。6.2 数据库并发与性能优化上线初期最常遇到的性能瓶颈是房态查询。前台每次刷新页面都要查询未来60天所有房型的余房如果每次查数据库页面响应会明显变慢。我的优化方案是把房态库存缓存到Redis# services/inventory.py import json from extensions import redis_client def get_inventory_cache(hotel_id, room_type_id, start_date, end_date): cache_key finv:{hotel_id}:{room_type_id}:{start_date}:{end_date} cached redis_client.get(cache_key) if cached: return json.loads(cached) rows RoomInventory.query.filter_by( hotel_idhotel_id, room_type_idroom_type_id, ).filter(RoomInventory.biz_date.between(start_date, end_date)).all() result {str(inv.biz_date): inv.total - inv.booked for inv in rows} redis_client.setex(cache_key, 30, json.dumps(result)) # 30秒过期 return result缓存过期时间设30秒是因为预订操作会修改booked数量如果缓存时间太长前台看到的房态就不准了。30秒的过期时间在“数据准确性”和“查询性能”之间取了个平衡。预订成功后我会主动删掉对应日期的缓存键强制下次重新查库。6.3 高频问题速查表项目做完后我整理了一张排查表这里挑几个典型问题分享现象根因解决方法MySQL报“server has gone away”连接空闲超时被断开设置pool_recycle300配合pool_pre_pingTrue并发预订最后两间房超卖查余房和扣库存之间无锁用with_for_update行级锁并确保查询命中索引跨夜订单报表统计漏数据判断条件用了check_in_datebiz_date改用入住日期目标日期且离店日期目标日期蓝图页面404蓝图url_prefix没注册或拼写错误检查register_blueprint的url_prefix优先用“/模块名”这样的显式前缀前台页面显示房价多一位小数JSON序列化Decimal变成字符串配置Jinja2过滤器或统一转成两位小数字符串后传入模板缓存里查到过期房态Redis键没在订单创建后清理在下单事务提交后主动delete对应缓存键还有一个很容易忽略的坑订单状态字段是字符串类型数据库排序时“pending”和“confirmed”会按字母序排列导致列表页面默认排序不是你想要的业务顺序。解决方法是加一个sort_order整数列或者查询时显式用case when指定排序规则。另一个真实教训是关于时区的。系统上线后出现了“客人凌晨1点在官网预订订单日期比本地日期早一天”的bug。原因是Python默认的datetime.utcnow返回UTC时间而酒店业务必须用北京时间。后来我在所有涉及日期的操作上统一使用系统配置的时区把库存初始化、订单日期、日报生成全部改为业务时区问题彻底解决。数据库索引方面reservation表我建了hotel_id check_in_date的联合索引能覆盖90%的查询场景。日报生成时每天跑一次全量扫描没问题但如果哪天临时要补历史报表建议先在备库导出数据到本地处理再推送到正式库避免高峰期长时间锁表影响前台操作。最后再说一个我自己开发完这套系统后的体会酒店管理系统的核心价值不在首页有多漂亮而在“数据永远是对得上账的”。你做的每一个订房、每一条账务流水最后都会被财务拿去核对任何一处因为浮点数、时间差、并发顺序导致的差异都会让整个系统可信度崩塌。所以我在开发过程中养成了一个习惯写完一个功能先自己用边界数据反复试比如连住一个月跨月、跨年、同一天入住同一天离店、预订后取消再预订把这些极端情况都跑通了再交付使用。这套方法看似笨拙却能省下后面大量的返工时间。
返回列表