1. 项目到底在做什么:先把景区酒店平台的需求拆明白接到这个项目的时候,甲方给的话只有一句:"我们景区周边的酒店想上线一个预订平台,让游客能按景区找酒店、看价格、下单,后台我们能管房态和订单。"听起来很常规,但真正落地的时候会发现,旅游景区酒店服务平台比普通电商多一些特殊约束:酒店库存是按"日期"来切的,不是按总数切的;游客下单时需要考虑入住离店日期;景区旺季流量可能有明显的波峰;后台运营人员又希望操作非常简单。我当时用的技术组合是Python生态里的两套主力Web框架:Flask负责对外API和核心业务接口,Django负责运营后台和管理端。这个决策不是一开始就有的,是边做边被需求逼出来的。整个平台的核心业务围绕景区、酒店、房型、订单、价格日历、评价这六个关键词展开,大部分工作其实都是在把这些关键词变成真实可用的代码和表结构。这篇文章就把这个python flask django旅游景区酒店服务平台完整拆开讲一遍。从需求梳理、框架选型、表结构设计,到API实现、后台改造、部署上线和常见坑,全部按我真实做过的流程来写。如果你也准备做类似的项目,可以直接对照着参考。1.1 一句话需求背后的三个隐藏问题甲方给的原始需求里面,至少藏着三个必须提前想清楚的问题。第一个是酒店归属。景区周边的酒店通常不是一家,而是几十家独立运营的民宿、酒店、客栈,平台到底是自营统一管理,还是让各家商户自己维护自家酒店?如果不提前定清楚,后台的权限模型和审核流就会做错。我当时选择的是平台统一维护基础数据、商户后台可维护自身房价和房态的模式。第二个问题是库存模型。普通商城商品的库存是"总数递减",但酒店预订的平台库存是"某一天某个房型剩余多少间"。比如一个标准间,某家酒店一共有10间,那么6月1日到6月3日这个区间可售是每天分别判断,而不是整体减1。多数小程序、H5前端的展示方式是按日期选择,所以API返回给前端的数据必须是按日期分组的日历结构,而不是一个简单的剩余数量。第三个问题是订单状态流。旅游景区酒店预订基本都会有"支付、确认、入住、取消、退款、离店"这些环节,并且取消是否免费通常和入住前的时间有关。如果一开始不把这些状态转移画清楚,后面写接口的时候会频繁改表结构。因为甲方没有提出特别复杂的财务结算要求,我最终把订单状态收敛为待支付、已支付/待确认、已确认、已入住、已完成、已取消、退款中、已退款这八个状态,并且用一张状态变更记录表追踪流转轨迹。1.2 功能边界和优先级到底怎么定项目需求看似不多,但如果不做功能优先级,团队很容易陷进细节里。我把整个平台的MVP功能收敛成了三大块。第一块是游客端核心链路:按景区查酒店列表、查酒店详情和房型价格日历、创建订单、在线支付回调、查看订单详情。第二块是运营后台:酒店管理、房型管理、价格库存管理、订单管理、用户管理、基础数据统计。第三块是系统基础能力:统一的登录鉴权、操作日志、数据导出、接口文档。刻意砍掉的有三个:多语言、多景区聚合推荐、实时AR导航之类。这些功能不是不好,而是做平台初期用不上。记住一个原则:功能边界不是"技术能不能实现",而是"业务上能不能持续维护"。旅游平台的数据准确性比界面炫酷要重要得多,与其做十个花哨入口,不如把酒店价格、房态更新的正确性做到位。我实际用Excel画了一张业务流程图,把游客从进入景区页到完成入住的所有操作节点列出来,又画了一张运营后台的操作时序。两份图对照之后,才确定表结构里必须要有景区表、酒店表、房型表、价格日历表、订单表、支付流水表、用户表、评价表这八类核心表。后面所有代码基本就是围绕这八张表转。2. 技术选型:Flask和Django为什么能同时出现在一个项目里很多开发者看到"python flask django旅游景区酒店服务平台"会觉得奇怪:同一门语言的两个Web框架,为什么不二选一。其实在一个项目里同时用Flask和Django是常见的架构选择,尤其在重运营、重API的场景下特别合理。我的思路是:对外接口用Flask,管理后台用Django,两者共享同一个MySQL数据库和Redis缓存。这样选型有几个实际好处。Flask足够轻,接口层可以自由定义路由和返回结构,不需要被Django的中间件和APP规则绑得太死;Django自带Admin、ORM、用户权限体系,后台管理功能开发速度非常快,而且表单验证、分页、筛选这些基础能力开箱即用。两个框架之间通过数据库和HTTP层通信,不会出现模块耦合的问题。2.1 单框架也能做,为什么非要拆开如果只用Django一整套也能做API,DRF(Django REST Framework)写接口并不慢;如果只用Flask也能做后台,但需要手动搭建用户登录、权限分组、操作日志、数据列表筛选,这些功能是后台管理的"天花板工作量",手写很容易漏。反过来,Django对API输出的控制相对重,比如每个接口默认的序列化规则、异常返回格式,需要写很多定制配置;Flask配合蓝图和SQLAlchemy可以非常直接地输出JSON,想怎么组织就怎么组织。在真实项目里,拆开还有一个运维层面的考虑:Flask接口层和Django管理端可以独立重启,互不影响。旅游平台运营人员白天在用后台改房价,前端游客同时在刷接口抢房间,如果后台某个功能临时写了个慢查询导致服务卡住,只要两个服务是分开部署的,至少不会相互拖垮。实际上我当时用同一个Nginx入口做了路径分流:/api/ 开头走Flask服务,/admin/ 开头走Django服务,两者看起来是一个域名,后端是两个独立进程。2.2 Flask和Django之间如何共享数据同时用两个框架,最大的担忧是模型层重复定义。我的做法是统一用SQLAlchemy Core风格维护表结构,Flask项目里用Flask-SQLAlchemy做ORM,Django项目里使用Django ORM连接同一套MySQL数据库。因为数据库的表结构是共同约定,所以不存在代码层面的实体冲突。简单说,在Flask侧定义模型类时尽量字段名、类型、注释都与数据库表保持一致。而在Django侧,我不会把所有表都反向生成Model,只对后台需要操作的几张表做Model映射,其他数据直接通过数据库连接或只读接口展示。这样既减少了重复工作,也让两个框架各取所长。框架职责划分可以用下面这张表来表达:服务角色使用框架主要职责访问入口游客端APIFlask酒店搜索、价格日历、订单创建、支付回调/api/ 前缀运营管理后台Django酒店、房型、价格库存、订单、用户管理/admin/ 前缀数据存储层MySQL八张核心业务表共用数据库缓存和分布式锁Redis接口缓存、库存锁、会话状态两边共用静态文件和入口Nginx反向代理、HTTPS、静态资源域名根路径这套架构的落地效果就是:前端和Flask对接时觉得很干净,运营同事和Django后台交互时觉得功能很全,而我维护代码时也不会因为框架混用而精神分裂。3. 数据库表设计:先把八张核心表的关系画清楚旅游景区酒店服务平台的表结构并不复杂,但有几个设计点特别容易被经验不足的开发者做错。核心原则是:把价格、日历、酒店这三个属性拆清楚,不要把所有内容都塞进一个"酒店信息表"。我实际的表结构大概是这样:景区表和酒店表是一对多关系;酒店表和房型表是一对多关系;房型表与价格日历表是一对多关系,价格日历表保存某房型在具体某天的价格和可售房间数;订单表与房型表、用户表分别关联;支付流水表与订单表是一对多关系;评价表与订单表是一对一关系。下面重点展开每个容易踩坑的字段设计。3.1 Flask侧的核心模型定义用Flask-SQLAlchemy写模型时,可以直观看清楚字段关系。我贴一段核心模型的核心字段,实际开发时还加了索引和约束。from datetime import date from flask_sqlalchemy import SQLAlchemy from werkzeug.security import generate_password_hash db = SQLAlchemy() class ScenicArea(db.Model): __tablename__ = "scenic_area" id = db.Column(db.Integer, primary_key=True) name = db.Column(db.String(64), nullable=False, index=True) city = db.Column(db.String(64), index=True) longitude = db.Column(db.Numeric(10, 7)) latitude = db.Column(db.Numeric(10, 7)) description = db.Column(db.Text) status = db.Column(db.SmallInteger, default=1) # 1上架 0下架 class Hotel(db.Model): __tablename__ = "hotel" id = db.Column(db.Integer, primary_key=True) scenic_id = db.Column(db.Integer, db.ForeignKey("scenic_area.id"), index=True) name = db.Column(db.String(128), nullable=False, index=True) address = db.Column(db.String(255)) level = db.Column(db.String(16)) # 民宿/舒适/高档 phone = db.Column(db.String(32)) default_checkin_time = db.Column(db.String(8), default="14:00") default_checkout_time = db.Column(db.String(8), default="12:00") status = db.Column(db.SmallInteger, default=1) class RoomType(db.Model): __tablename__ = "room_type" id = db.Column(db.Integer, primary_key=True) hotel_id = db.Column(db.Integer, db.ForeignKey("hotel.id"), index=True) name = db.Column(db.String(64), nullable=False) area_sqm = db.Column(db.Numeric(6, 1)) max_guests = db.Column(db.Integer, default=2) bed_type = db.Column(db.String(16)) base_price = db.Column(db.Numeric(10, 2)) # 基准价 breakfast = db.Column(db.Boolean, default=False) status = db.Column(db.SmallInteger, default=1) class PriceCalendar(db.Model): __tablename__ = "price_calendar" id = db.Column(db.Integer, primary_key=True) room_type_id = db.Column(db.Integer, db.ForeignKey("room_type.id"), index=True) biz_date = db.Column(db.Date, nullable=False, index=True) price = db.Column(db.Numeric(10, 2), nullable=False) inventory = db.Column(db.Integer, nullable=False) # 当日可售间数 locked = db.Column(db.Integer, default=0) # 当日已被锁定房间数 class UserAccount(db.Model): __tablename__ = "user_account" id = db.Column(db.Integer, primary_key=True) mobile = db.Column(db.String(20), unique=True, nullable=False, index=True) nickname = db.Column(db.String(64)) password_hash = db.Column(db.String(256)) created_at = db.Column(db.DateTime, server_default=db.func.now()) def set_password(self, password): self.password_hash = generate_password_hash(password) def check_password(self, password): from werkzeug.security import check_password_hash return check_password_hash(self.password_hash, password) class Order(db.Model): __tablename__ = "order" id = db.Column(db.Integer, primary_key=True) order_no = db.Column(db.String(32), unique=True, nullable=False, index=True) user_id = db.Column(db.Integer, db.ForeignKey("user_account.id"), index=True) room_type_id = db.Column(db.Integer, db.ForeignKey("room_type.id"), index=True) checkin_date = db.Column(db.Date, nullable=False) checkout_date = db.Column(db.Date, nullable=False) room_count = db.Column(db.Integer, default=1) total_amount = db.Column(db.Numeric(10, 2), nullable=False) status = db.Column(db.String(16), default="pending_payment", index=True) customer_name = db.Column(db.String(32)) customer_mobile = db.Column(db.String(20)) created_at = db.Column(db.DateTime, server_default=db.func.now())这里有一个很多人容易忽略的细节:checkout_date存的是离店日期,库存占用只计算checkin_date到checkout_date的前一天。比如游客订6月1日入住、6月3日离店,那么真正占用库存的是6月1日和6月2日两晚。所有价格汇总、库存校验都必须按这个区间逐日计算,不能直接用日期差减一之后就直接乘单日价格,还要注意晚数、间数的乘积关系。3.2 库存扣减必须防并发超卖酒店的房型每天都有可售间数,如果多个用户同时下单,最简单的select inventory where room_type_id=? and biz_date=?加update inventory = inventory - 1在高并发下必然超卖。我采用的方案是Redis分布式锁加数据库乐观锁的双重保护。在下单接口里,我先按日期范围把所有涉及的biz_date在Redis里拼成锁key,例如lock:room:12:2024-08-01、lock:room:12:2024-08-02。使用SET lock_key user_token NX EX 5的方式依次加锁,拿不到锁就返回"房间被抢占",拿到锁之后再去查数据库并做扣减。数据库层面对price_calendar表增加一个locked字段,每次扣减使用类似这样的语句:UPDATE price_calendar SET locked = locked + :room_count WHERE room_type_id = :room_type_id AND biz_date BETWEEN :start_date AND :end_date AND inventory - locked >= :room_count如果受影响行数等于区间天数,说明所有日期都扣减成功;如果小于区间天数,说明某些日期已经满了,需要立刻回滚已经扣减的日期。回滚时再用同样的反向UPDATE把locked减回去。这个写法的好处是,不用在应用层加载整个日期的库存快照,数据库行锁就能避免并发覆盖。实际压测时,单房型100并发同时下单,最终成功订单数和可售数完全一致,没有出现超卖。4. Flask API接口实现:游客端主流程的关键代码游客端的H5和小程序是同一套API,所有接口返回统一JSON结构。我定了统一响应格式:成功时data是实际数据,code为0,失败时code为业务错误码,msg是给前端提示的中文信息。这里不做HTTP状态码的过度拆分,统一200返回更利于前端处理。接口层用Flask蓝图按业务拆分,hotel、order、user、payment四个模块各一个蓝图。下面按核心链路拆开讲。4.1 酒店列表和价格日历怎么设计接口酒店列表接口要支持按景区、入住日期、离店日期、人数来过滤。最简单的实现是只查酒店表和景区关联,但实际游客更关心可用房间,所以我在列表查询时会把价格日历表和房型表按日期做子查询:判断该酒店在目标日期区间内有没有至少一个房型可售。伪代码可以这样理解:先根据景区过滤出酒店ID集合,然后查出这些酒店下所有房型在目标日期区间的可售数量,聚合后放到酒店详情里返回。为了避免每次用户搜索都把全量数据查一遍,我用Redis缓存了热点景区的酒店聚合结果,key设计为hotel_list:{scenic_id}:{checkin}:{checkout}:{guests},缓存时间设为120秒。旅游项目的酒店列表并不会秒级变化,120秒的缓存足够应对突发流量。价格日历接口我建议单独做,因为它的请求量大、实时性要求高。接口入参是room_type_id和起始结束月份,返回按日价格和可售间数。这里有一个体验优化:如果某天库存已经为0或被人锁定完,后端不返回价格只返回available=false,前端在日历上置灰。需要特别注意时区问题,表的biz_date是日期类型,Python端统一用date.today(),不要混用datetime.now(),否则到晚上切换日期时会踩到隐式时区的坑。4.2 下单、幂等与支付回调创建订单接口的核心逻辑是:参数校验、库存预占、生成订单号、返回待支付信息。订单号我用时间戳 + 用户ID后四位 + 房型ID + 随机数拼接,保证唯一性。这个订单号不只展示给用户,还是后续幂等控制的唯一键。所谓幂等控制,就是前端因为网络抖动重复点击"提交订单"时,后端不能生成两笔一模一样的单。我的做法是要求前端在请求头里传一个Idempotency-Key,通常用时间戳加UUID生成。订单服务里把这个key和用户ID存到Redis,key存在且订单已创建则直接返回已存在的订单,不再重复扣库存。没有这个设计的时候,游客双击提交按钮很可能产生两笔订单和两次库存扣减,看起来是小概率问题,但在低价房型上经常发生。支付回调的逻辑相对固定:第三方支付平台把回调结果POST到Flask接口,服务端验签后判断订单是否待支付或支付中,然后更新订单状态并写入支付流水。这里很容易忽略回调的重复通知,所以支付流水表对payment_no加了唯一索引,重复回调直接跳过。关键路径我贴一段简化的订单创建方法:def create_order(user_id, room_type_id, checkin, checkout, room_count, idempotency_key): cached = redis.get(f"idem:{idempotency_key}") if cached: return jsonify(code=0, data=json.loads(cached)) room_type = RoomType.query.get(room_type_id) if not room_type or room_type.status != 1: return jsonify(code=40001, msg="房型不存在或已下架") date_list = get_date_range(checkin, checkout) lock_keys = [f"lock:room:{room_type_id}:{d}" for d in date_list] lock_success = acquire_locks(lock_keys, token=user_id, expire=5) if not lock_success: return jsonify(code=40002, msg="房间正在被其他游客抢占,请重新选择") try: affected = PriceCalendar.query.filter( PriceCalendar.biz_date.in_(date_list), PriceCalendar.inventory - PriceCalendar.locked >= room_count, ).update( {"locked": PriceCalendar.locked + room_count}, synchronize_session=False, ) if affected != len(date_list): return jsonify(code=40003, msg="所选日期部分房间已满") total = compute_total_price(room_type_id, date_list, room_count) order = Order(order_no=generate_order_no(), ...) db.session.add(order) db.session.commit() redis.set(f"idem:{idempotency_key}", json.dumps(order.to_dict()), ex=1800) return jsonify(code=0, data={"order_id": order.id, "order_no": order.order_no}) except Exception as exc: db.session.rollback() # 回滚所有锁定的日期 rollback_locked(room_type_id, date_list, room_count) raise exc finally: release_locks(lock_keys, token=user_id)这里面最容易被忽略的并不是代码逻辑,而是数据库事务隔离级别。我项目的MySQL默认是REPEATABLE READ,在高并发下库存更新可能导致间隙锁范围变大,简单做法是不在事务里手动加表锁,依赖行更新条件判断。如果为了稳妥,可以把订单相关的事务隔离级别调整为READ COMMITTED,在Django/Flask连接串里追加?isolation_level=READ COMMITTED即可,MySQL端不需要额外改。4.3 用户鉴权和登录会话Flask接口层我用JWT做无状态鉴权,登录成功后签发access_token,有效期为7天。用户在H5上登录一次之后,后续请求头里带Authorization: Bearer xxx。JWT的secret一定要放到环境变量里,不能写死在代码中。为了管理后台和游客端互不影响,Django后台使用Django自己的Session认证,不混用JWT。我踩过的一个坑是:Flask侧如果直接对Django用户的密码哈希做校验,会出现两边加密方式不一致的问题。解决方案是统一在用户表里用Flask侧的werkzeug.security.generate_password_hash生成密码,Django后台侧只负责展示和管理,不直接改动用户密码字段。需要重置密码时,由Django后台调Flask侧提供的一个内部接口完成,避免两套加密逻辑互相污染。5. Django后台管理系统:怎么把Admin变成可用的运营后台Django自带Admin是后台开发效率的利器,但默认界面和交互偏"程序员风格",运营人员不一定看得懂。我的改造策略是:先快速用Admin完成所有数据的增删改查,再通过自定义admin.py、列表页筛选、导出功能和操作按钮,让后台从一个"数据库管理工具"变成一个"业务流程工具"。5.1 Django Admin的快速搭建与二次改造在Django项目里,注册Model和管理类是常规操作。为了让运营人员更好用,我在几个核心管理类上做了这些调整:列表页默认展示关键的业务字段,比如房型价格、日期、库存状态;订单列表增加按状态和日期的筛选器;所有列表页开启搜索功能;价格日历表提供按酒店和日期批量导入Excel的功能,因为运营人员通常是从线下的房价表复制数据。给一段简化的Django Admin代码,方便你理解怎么把列表页做得更顺手:from django.contrib import admin from .models import Hotel, RoomType, PriceCalendar, Order @admin.register(Hotel) class HotelAdmin(admin.ModelAdmin): list_display = ("id", "name", "scenic", "level", "status") list_filter = ("scenic", "status") search_fields = ("name", "address") @admin.register(PriceCalendar) class PriceCalendarAdmin(admin.ModelAdmin): list_display = ("id", "room_type", "biz_date", "price", "inventory", "locked") list_filter = ("biz_date",) search_fields = ("room_type__hotel__name",) date_hierarchy = "biz_date" actions = ["mark_inventory_zero", "export_selected"] @admin.action(description="将选中日期的库存设为0") def mark_inventory_zero(self, request, queryset): updated = queryset.update(inventory=0) self.message_user(request, f"已更新 {updated} 条记录") @admin.action(description="导出选中数据") def export_selected(self, request, queryset): import csv from django.http import HttpResponse response = HttpResponse(content_type="text/csv") response["Content-Disposition"] = "attachment; filename=price_calendar.csv" writer = csv.writer(response) writer.writerow(["房型", "日期", "价格", "库存", "锁定"]) for row in queryset: writer.writerow([row.room_type, row.biz_date, row.price, row.inventory, row.locked]) return response这里要特别说一个经验:Django Admin默认的list_editable和date_hierarchy会显著提升运营人员的工作效率。比如价格日历表可以开启list_editable = ("price", "inventory", "locked"),运营人员在列表页直接修改多行数据一键保存,而不是点进每一行去改。一开始我担心这种方式太危险,实际设置好权限后,并不会造成误操作,反而被运营同事表扬"比原来的Excel方便多了"。5.2 运营权限模型:不是所有后台用户都能改房价旅游景区平台的后台用户至少有三种身份:超级管理员、酒店运营、财务客服。超级管理员管理全部数据和系统配置;酒店运营只能维护酒店、房型、价格库存和订单确认;财务客服只能查看订单、处理退款和导出流水,不能修改房价。Django自带的auth.Group完全能满足这个需求,不需要自己造轮子。我创建了三个Group,分别赋予不同Model的add、change、delete、view权限。这里踩过的一个坑是:Django的Model权限默认不能精确到字段级,比如不想让财务客服看到成本价,只靠Group做不到。我的处理方式是不在Django后台展示成本价字段,单独在API层做数据脱敏,财务客服导出报表时由接口过滤字段。另一个坑是Admin操作日志,Django自带的LogEntry只记录ModelAdmin操作,不记录直接调queryset.update()的批量操作,所以凡是对价格、库存、订单状态做批量修改时,我都在action里自己写入一条操作日志表,后面出问题才好追溯。后台和Flask之间还有一个需要打通的点:当房价或库存被修改时,前台缓存不能一直保留。我的做法是Django侧在Model的save()和delete()方法里发一个信号,把对应房型的Redis热点缓存key删掉。比如hotel_list:{scenic_id}:{checkin}:{checkout}:{guests}这种key在修改库存时无法精确匹配删除,就采用失效策略:按景区ID删除该景区下所有hotel_list:*前缀key,成本很低但能保证数据一致性。6. 部署上线和日常问题排查:从本地到服务器的完整路径项目做到能跑只是第一步,真正上线考验的是部署方案和排查效率。旅游景区的流量高峰有明显的时间段,比如节假日、周末、寒暑假,系统稳定性比花哨功能重要得多。我最终采用了Nginx + Gunicorn + Flask、Nginx + uWSGI + Django、MySQL、Redis 的经典组合,两个Python服务用Docker容器管理,MySQL和Redis单独部署在云数据库服务上,方便备份和扩容。6.1 两套服务如何用Docker Compose编排为了让环境一致,我写了一个docker-compose.yml,里面只放Flask和Django两个应用容器,Nginx、MySQL、Redis放在宿主机或云服务上。因为Flask的API服务对并发模型比较敏感,我用了Gunicorn的gthreadworker模式,设置workers=4、threads=8,这样既不会因为进程太多占用内存,也能应对在线支付回调带来的瞬时连接数。Django侧用uWSGI,设置processes=4、threads=2。两个服务都通过环境变量读取数据库和Redis连接串,不把敏感信息写进镜像。部署时我习惯先跑迁移,迁移成功后重启Django和Flask容器,再reload Nginx。有一个容易忽略的步骤是Nginx的client_max_body_size。酒店平台会有运营人员上传酒店图片,如果图片是几MB的,默认1MB限制会导致上传失败。我在Nginx配置里对/api/和/admin/都设置成client_max_body_size 20m;,并且在上传时限制图片分辨率和压缩级别,既保证加载速度,也减少存储压力。6.2 上线后高频问题速查表做这个项目过程中,我整理了下面几条最常排查的问题,基本上90%的故障都集中在这几个位置:现象可能原因解决思路游客下单时提示房间被抢占缓存锁未释放 / 数据库锁等待超时检查Redis锁的过期时间;在finally块中确认释放锁后台修改房价后前端不生效Redis缓存未删除检查Django信号是否注册成功;手动清理对应cache key支付回调成功但订单状态不变回调流水唯一索引冲突被吞异常查看支付流水表是否已有记录;检查验签逻辑两个日期跨天出现下单价不对时区或日期字段类型不一致统一使用date类型;禁用在业务代码里传datetimeFlask接口偶发500错误数据库连接池耗尽增加SQLAlchemy连接池pool_size和max_overflow,并设置pool_recycleDjango后台登录后跳转不准CSRF中间件或SITE_ID配置问题检查Django版本对应中间件设置,以及域名配置除了这些常规问题,我再分享一个容易被新人忽略的性能点:价格日历表是全项目数据量最大的表,因为每个酒店每个房型每天都有一行库存记录。一年下来可能几十万行。不要对整张表做复杂的模糊查询,要保证(room_type_id, biz_date)的联合索引唯一。如果运营要在后台按日期范围筛选,直接利用DjangoAdmin的date_hierarchy,它内部会走索引,性能没问题。为了进一步加快聚合查询,我还在Redis里存了最近30天的可用房型ID集合,作为接口的预过滤层,明显减少了MySQL压力。最后说一点个人体会。刚接到这个项目时,我也怀疑过Flask和Django混用是不是过度设计,但最终交付时发现,这套架构恰好匹配了业务两端的不同需求:游客端API需要轻量、灵活、快速迭代,运营后台需要完整、稳定、好维护。两个框架在同一个项目里不是互相打架,而是各管一段,边界清晰反而让代码维护变的简单。做完这个旅游景区酒店服务平台后,我不太会在"只用某一个框架"上纠结,而是更关注业务形态到底需要什么。如果你把表结构、库存模型、状态流想清楚了,用Flask还是Django都只是顺手的事。
1. 项目到底在做什么:先把景区酒店平台的需求拆明白接到这个项目的时候,甲方给的话只有一句:"我们景区周边的酒店想上线一个预订平台,让游客能按景区找酒店、看价格、下单,后台我们能管房态和订单。"听起来很常规,但真正落地的时候会发现,旅游景区酒店服务平台比普通电商多一些特殊约束:酒店库存是按"日期"来切的,不是按总数切的;游客下单时需要考虑入住离店日期;景区旺季流量可能有明显的波峰;后台运营人员又希望操作非常简单。我当时用的技术组合是Python生态里的两套主力Web框架:Flask负责对外API和核心业务接口,Django负责运营后台和管理端。这个决策不是一开始就有的,是边做边被需求逼出来的。整个平台的核心业务围绕景区、酒店、房型、订单、价格日历、评价这六个关键词展开,大部分工作其实都是在把这些关键词变成真实可用的代码和表结构。这篇文章就把这个python flask django旅游景区酒店服务平台完整拆开讲一遍。从需求梳理、框架选型、表结构设计,到API实现、后台改造、部署上线和常见坑,全部按我真实做过的流程来写。如果你也准备做类似的项目,可以直接对照着参考。1.1 一句话需求背后的三个隐藏问题甲方给的原始需求里面,至少藏着三个必须提前想清楚的问题。第一个是酒店归属。景区周边的酒店通常不是一家,而是几十家独立运营的民宿、酒店、客栈,平台到底是自营统一管理,还是让各家商户自己维护自家酒店?如果不提前定清楚,后台的权限模型和审核流就会做错。我当时选择的是平台统一维护基础数据、商户后台可维护自身房价和房态的模式。第二个问题是库存模型。普通商城商品的库存是"总数递减",但酒店预订的平台库存是"某一天某个房型剩余多少间"。比如一个标准间,某家酒店一共有10间,那么6月1日到6月3日这个区间可售是每天分别判断,而不是整体减1。多数小程序、H5前端的展示方式是按日期选择,所以API返回给前端的数据必须是按日期分组的日历结构,而不是一个简单的剩余数量。第三个问题是订单状态流。旅游景区酒店预订基本都会有"支付、确认、入住、取消、退款、离店"这些环节,并且取消是否免费通常和入住前的时间有关。如果一开始不把这些状态转移画清楚,后面写接口的时候会频繁改表结构。因为甲方没有提出特别复杂的财务结算要求,我最终把订单状态收敛为待支付、已支付/待确认、已确认、已入住、已完成、已取消、退款中、已退款这八个状态,并且用一张状态变更记录表追踪流转轨迹。1.2 功能边界和优先级到底怎么定项目需求看似不多,但如果不做功能优先级,团队很容易陷进细节里。我把整个平台的MVP功能收敛成了三大块。第一块是游客端核心链路:按景区查酒店列表、查酒店详情和房型价格日历、创建订单、在线支付回调、查看订单详情。第二块是运营后台:酒店管理、房型管理、价格库存管理、订单管理、用户管理、基础数据统计。第三块是系统基础能力:统一的登录鉴权、操作日志、数据导出、接口文档。刻意砍掉的有三个:多语言、多景区聚合推荐、实时AR导航之类。这些功能不是不好,而是做平台初期用不上。记住一个原则:功能边界不是"技术能不能实现",而是"业务上能不能持续维护"。旅游平台的数据准确性比界面炫酷要重要得多,与其做十个花哨入口,不如把酒店价格、房态更新的正确性做到位。我实际用Excel画了一张业务流程图,把游客从进入景区页到完成入住的所有操作节点列出来,又画了一张运营后台的操作时序。两份图对照之后,才确定表结构里必须要有景区表、酒店表、房型表、价格日历表、订单表、支付流水表、用户表、评价表这八类核心表。后面所有代码基本就是围绕这八张表转。2. 技术选型:Flask和Django为什么能同时出现在一个项目里很多开发者看到"python flask django旅游景区酒店服务平台"会觉得奇怪:同一门语言的两个Web框架,为什么不二选一。其实在一个项目里同时用Flask和Django是常见的架构选择,尤其在重运营、重API的场景下特别合理。我的思路是:对外接口用Flask,管理后台用Django,两者共享同一个MySQL数据库和Redis缓存。这样选型有几个实际好处。Flask足够轻,接口层可以自由定义路由和返回结构,不需要被Django的中间件和APP规则绑得太死;Django自带Admin、ORM、用户权限体系,后台管理功能开发速度非常快,而且表单验证、分页、筛选这些基础能力开箱即用。两个框架之间通过数据库和HTTP层通信,不会出现模块耦合的问题。2.1 单框架也能做,为什么非要拆开如果只用Django一整套也能做API,DRF(Django REST Framework)写接口并不慢;如果只用Flask也能做后台,但需要手动搭建用户登录、权限分组、操作日志、数据列表筛选,这些功能是后台管理的"天花板工作量",手写很容易漏。反过来,Django对API输出的控制相对重,比如每个接口默认的序列化规则、异常返回格式,需要写很多定制配置;Flask配合蓝图和SQLAlchemy可以非常直接地输出JSON,想怎么组织就怎么组织。在真实项目里,拆开还有一个运维层面的考虑:Flask接口层和Django管理端可以独立重启,互不影响。旅游平台运营人员白天在用后台改房价,前端游客同时在刷接口抢房间,如果后台某个功能临时写了个慢查询导致服务卡住,只要两个服务是分开部署的,至少不会相互拖垮。实际上我当时用同一个Nginx入口做了路径分流:/api/ 开头走Flask服务,/admin/ 开头走Django服务,两者看起来是一个域名,后端是两个独立进程。2.2 Flask和Django之间如何共享数据同时用两个框架,最大的担忧是模型层重复定义。我的做法是统一用SQLAlchemy Core风格维护表结构,Flask项目里用Flask-SQLAlchemy做ORM,Django项目里使用Django ORM连接同一套MySQL数据库。因为数据库的表结构是共同约定,所以不存在代码层面的实体冲突。简单说,在Flask侧定义模型类时尽量字段名、类型、注释都与数据库表保持一致。而在Django侧,我不会把所有表都反向生成Model,只对后台需要操作的几张表做Model映射,其他数据直接通过数据库连接或只读接口展示。这样既减少了重复工作,也让两个框架各取所长。框架职责划分可以用下面这张表来表达:服务角色使用框架主要职责访问入口游客端APIFlask酒店搜索、价格日历、订单创建、支付回调/api/ 前缀运营管理后台Django酒店、房型、价格库存、订单、用户管理/admin/ 前缀数据存储层MySQL八张核心业务表共用数据库缓存和分布式锁Redis接口缓存、库存锁、会话状态两边共用静态文件和入口Nginx反向代理、HTTPS、静态资源域名根路径这套架构的落地效果就是:前端和Flask对接时觉得很干净,运营同事和Django后台交互时觉得功能很全,而我维护代码时也不会因为框架混用而精神分裂。3. 数据库表设计:先把八张核心表的关系画清楚旅游景区酒店服务平台的表结构并不复杂,但有几个设计点特别容易被经验不足的开发者做错。核心原则是:把价格、日历、酒店这三个属性拆清楚,不要把所有内容都塞进一个"酒店信息表"。我实际的表结构大概是这样:景区表和酒店表是一对多关系;酒店表和房型表是一对多关系;房型表与价格日历表是一对多关系,价格日历表保存某房型在具体某天的价格和可售房间数;订单表与房型表、用户表分别关联;支付流水表与订单表是一对多关系;评价表与订单表是一对一关系。下面重点展开每个容易踩坑的字段设计。3.1 Flask侧的核心模型定义用Flask-SQLAlchemy写模型时,可以直观看清楚字段关系。我贴一段核心模型的核心字段,实际开发时还加了索引和约束。from datetime import date from flask_sqlalchemy import SQLAlchemy from werkzeug.security import generate_password_hash db = SQLAlchemy() class ScenicArea(db.Model): __tablename__ = "scenic_area" id = db.Column(db.Integer, primary_key=True) name = db.Column(db.String(64), nullable=False, index=True) city = db.Column(db.String(64), index=True) longitude = db.Column(db.Numeric(10, 7)) latitude = db.Column(db.Numeric(10, 7)) description = db.Column(db.Text) status = db.Column(db.SmallInteger, default=1) # 1上架 0下架 class Hotel(db.Model): __tablename__ = "hotel" id = db.Column(db.Integer, primary_key=True) scenic_id = db.Column(db.Integer, db.ForeignKey("scenic_area.id"), index=True) name = db.Column(db.String(128), nullable=False, index=True) address = db.Column(db.String(255)) level = db.Column(db.String(16)) # 民宿/舒适/高档 phone = db.Column(db.String(32)) default_checkin_time = db.Column(db.String(8), default="14:00") default_checkout_time = db.Column(db.String(8), default="12:00") status = db.Column(db.SmallInteger, default=1) class RoomType(db.Model): __tablename__ = "room_type" id = db.Column(db.Integer, primary_key=True) hotel_id = db.Column(db.Integer, db.ForeignKey("hotel.id"), index=True) name = db.Column(db.String(64), nullable=False) area_sqm = db.Column(db.Numeric(6, 1)) max_guests = db.Column(db.Integer, default=2) bed_type = db.Column(db.String(16)) base_price = db.Column(db.Numeric(10, 2)) # 基准价 breakfast = db.Column(db.Boolean, default=False) status = db.Column(db.SmallInteger, default=1) class PriceCalendar(db.Model): __tablename__ = "price_calendar" id = db.Column(db.Integer, primary_key=True) room_type_id = db.Column(db.Integer, db.ForeignKey("room_type.id"), index=True) biz_date = db.Column(db.Date, nullable=False, index=True) price = db.Column(db.Numeric(10, 2), nullable=False) inventory = db.Column(db.Integer, nullable=False) # 当日可售间数 locked = db.Column(db.Integer, default=0) # 当日已被锁定房间数 class UserAccount(db.Model): __tablename__ = "user_account" id = db.Column(db.Integer, primary_key=True) mobile = db.Column(db.String(20), unique=True, nullable=False, index=True) nickname = db.Column(db.String(64)) password_hash = db.Column(db.String(256)) created_at = db.Column(db.DateTime, server_default=db.func.now()) def set_password(self, password): self.password_hash = generate_password_hash(password) def check_password(self, password): from werkzeug.security import check_password_hash return check_password_hash(self.password_hash, password) class Order(db.Model): __tablename__ = "order" id = db.Column(db.Integer, primary_key=True) order_no = db.Column(db.String(32), unique=True, nullable=False, index=True) user_id = db.Column(db.Integer, db.ForeignKey("user_account.id"), index=True) room_type_id = db.Column(db.Integer, db.ForeignKey("room_type.id"), index=True) checkin_date = db.Column(db.Date, nullable=False) checkout_date = db.Column(db.Date, nullable=False) room_count = db.Column(db.Integer, default=1) total_amount = db.Column(db.Numeric(10, 2), nullable=False) status = db.Column(db.String(16), default="pending_payment", index=True) customer_name = db.Column(db.String(32)) customer_mobile = db.Column(db.String(20)) created_at = db.Column(db.DateTime, server_default=db.func.now())这里有一个很多人容易忽略的细节:checkout_date存的是离店日期,库存占用只计算checkin_date到checkout_date的前一天。比如游客订6月1日入住、6月3日离店,那么真正占用库存的是6月1日和6月2日两晚。所有价格汇总、库存校验都必须按这个区间逐日计算,不能直接用日期差减一之后就直接乘单日价格,还要注意晚数、间数的乘积关系。3.2 库存扣减必须防并发超卖酒店的房型每天都有可售间数,如果多个用户同时下单,最简单的select inventory where room_type_id=? and biz_date=?加update inventory = inventory - 1在高并发下必然超卖。我采用的方案是Redis分布式锁加数据库乐观锁的双重保护。在下单接口里,我先按日期范围把所有涉及的biz_date在Redis里拼成锁key,例如lock:room:12:2024-08-01、lock:room:12:2024-08-02。使用SET lock_key user_token NX EX 5的方式依次加锁,拿不到锁就返回"房间被抢占",拿到锁之后再去查数据库并做扣减。数据库层面对price_calendar表增加一个locked字段,每次扣减使用类似这样的语句:UPDATE price_calendar SET locked = locked + :room_count WHERE room_type_id = :room_type_id AND biz_date BETWEEN :start_date AND :end_date AND inventory - locked >= :room_count如果受影响行数等于区间天数,说明所有日期都扣减成功;如果小于区间天数,说明某些日期已经满了,需要立刻回滚已经扣减的日期。回滚时再用同样的反向UPDATE把locked减回去。这个写法的好处是,不用在应用层加载整个日期的库存快照,数据库行锁就能避免并发覆盖。实际压测时,单房型100并发同时下单,最终成功订单数和可售数完全一致,没有出现超卖。4. Flask API接口实现:游客端主流程的关键代码游客端的H5和小程序是同一套API,所有接口返回统一JSON结构。我定了统一响应格式:成功时data是实际数据,code为0,失败时code为业务错误码,msg是给前端提示的中文信息。这里不做HTTP状态码的过度拆分,统一200返回更利于前端处理。接口层用Flask蓝图按业务拆分,hotel、order、user、payment四个模块各一个蓝图。下面按核心链路拆开讲。4.1 酒店列表和价格日历怎么设计接口酒店列表接口要支持按景区、入住日期、离店日期、人数来过滤。最简单的实现是只查酒店表和景区关联,但实际游客更关心可用房间,所以我在列表查询时会把价格日历表和房型表按日期做子查询:判断该酒店在目标日期区间内有没有至少一个房型可售。伪代码可以这样理解:先根据景区过滤出酒店ID集合,然后查出这些酒店下所有房型在目标日期区间的可售数量,聚合后放到酒店详情里返回。为了避免每次用户搜索都把全量数据查一遍,我用Redis缓存了热点景区的酒店聚合结果,key设计为hotel_list:{scenic_id}:{checkin}:{checkout}:{guests},缓存时间设为120秒。旅游项目的酒店列表并不会秒级变化,120秒的缓存足够应对突发流量。价格日历接口我建议单独做,因为它的请求量大、实时性要求高。接口入参是room_type_id和起始结束月份,返回按日价格和可售间数。这里有一个体验优化:如果某天库存已经为0或被人锁定完,后端不返回价格只返回available=false,前端在日历上置灰。需要特别注意时区问题,表的biz_date是日期类型,Python端统一用date.today(),不要混用datetime.now(),否则到晚上切换日期时会踩到隐式时区的坑。4.2 下单、幂等与支付回调创建订单接口的核心逻辑是:参数校验、库存预占、生成订单号、返回待支付信息。订单号我用时间戳 + 用户ID后四位 + 房型ID + 随机数拼接,保证唯一性。这个订单号不只展示给用户,还是后续幂等控制的唯一键。所谓幂等控制,就是前端因为网络抖动重复点击"提交订单"时,后端不能生成两笔一模一样的单。我的做法是要求前端在请求头里传一个Idempotency-Key,通常用时间戳加UUID生成。订单服务里把这个key和用户ID存到Redis,key存在且订单已创建则直接返回已存在的订单,不再重复扣库存。没有这个设计的时候,游客双击提交按钮很可能产生两笔订单和两次库存扣减,看起来是小概率问题,但在低价房型上经常发生。支付回调的逻辑相对固定:第三方支付平台把回调结果POST到Flask接口,服务端验签后判断订单是否待支付或支付中,然后更新订单状态并写入支付流水。这里很容易忽略回调的重复通知,所以支付流水表对payment_no加了唯一索引,重复回调直接跳过。关键路径我贴一段简化的订单创建方法:def create_order(user_id, room_type_id, checkin, checkout, room_count, idempotency_key): cached = redis.get(f"idem:{idempotency_key}") if cached: return jsonify(code=0, data=json.loads(cached)) room_type = RoomType.query.get(room_type_id) if not room_type or room_type.status != 1: return jsonify(code=40001, msg="房型不存在或已下架") date_list = get_date_range(checkin, checkout) lock_keys = [f"lock:room:{room_type_id}:{d}" for d in date_list] lock_success = acquire_locks(lock_keys, token=user_id, expire=5) if not lock_success: return jsonify(code=40002, msg="房间正在被其他游客抢占,请重新选择") try: affected = PriceCalendar.query.filter( PriceCalendar.biz_date.in_(date_list), PriceCalendar.inventory - PriceCalendar.locked >= room_count, ).update( {"locked": PriceCalendar.locked + room_count}, synchronize_session=False, ) if affected != len(date_list): return jsonify(code=40003, msg="所选日期部分房间已满") total = compute_total_price(room_type_id, date_list, room_count) order = Order(order_no=generate_order_no(), ...) db.session.add(order) db.session.commit() redis.set(f"idem:{idempotency_key}", json.dumps(order.to_dict()), ex=1800) return jsonify(code=0, data={"order_id": order.id, "order_no": order.order_no}) except Exception as exc: db.session.rollback() # 回滚所有锁定的日期 rollback_locked(room_type_id, date_list, room_count) raise exc finally: release_locks(lock_keys, token=user_id)这里面最容易被忽略的并不是代码逻辑,而是数据库事务隔离级别。我项目的MySQL默认是REPEATABLE READ,在高并发下库存更新可能导致间隙锁范围变大,简单做法是不在事务里手动加表锁,依赖行更新条件判断。如果为了稳妥,可以把订单相关的事务隔离级别调整为READ COMMITTED,在Django/Flask连接串里追加?isolation_level=READ COMMITTED即可,MySQL端不需要额外改。4.3 用户鉴权和登录会话Flask接口层我用JWT做无状态鉴权,登录成功后签发access_token,有效期为7天。用户在H5上登录一次之后,后续请求头里带Authorization: Bearer xxx。JWT的secret一定要放到环境变量里,不能写死在代码中。为了管理后台和游客端互不影响,Django后台使用Django自己的Session认证,不混用JWT。我踩过的一个坑是:Flask侧如果直接对Django用户的密码哈希做校验,会出现两边加密方式不一致的问题。解决方案是统一在用户表里用Flask侧的werkzeug.security.generate_password_hash生成密码,Django后台侧只负责展示和管理,不直接改动用户密码字段。需要重置密码时,由Django后台调Flask侧提供的一个内部接口完成,避免两套加密逻辑互相污染。5. Django后台管理系统:怎么把Admin变成可用的运营后台Django自带Admin是后台开发效率的利器,但默认界面和交互偏"程序员风格",运营人员不一定看得懂。我的改造策略是:先快速用Admin完成所有数据的增删改查,再通过自定义admin.py、列表页筛选、导出功能和操作按钮,让后台从一个"数据库管理工具"变成一个"业务流程工具"。5.1 Django Admin的快速搭建与二次改造在Django项目里,注册Model和管理类是常规操作。为了让运营人员更好用,我在几个核心管理类上做了这些调整:列表页默认展示关键的业务字段,比如房型价格、日期、库存状态;订单列表增加按状态和日期的筛选器;所有列表页开启搜索功能;价格日历表提供按酒店和日期批量导入Excel的功能,因为运营人员通常是从线下的房价表复制数据。给一段简化的Django Admin代码,方便你理解怎么把列表页做得更顺手:from django.contrib import admin from .models import Hotel, RoomType, PriceCalendar, Order @admin.register(Hotel) class HotelAdmin(admin.ModelAdmin): list_display = ("id", "name", "scenic", "level", "status") list_filter = ("scenic", "status") search_fields = ("name", "address") @admin.register(PriceCalendar) class PriceCalendarAdmin(admin.ModelAdmin): list_display = ("id", "room_type", "biz_date", "price", "inventory", "locked") list_filter = ("biz_date",) search_fields = ("room_type__hotel__name",) date_hierarchy = "biz_date" actions = ["mark_inventory_zero", "export_selected"] @admin.action(description="将选中日期的库存设为0") def mark_inventory_zero(self, request, queryset): updated = queryset.update(inventory=0) self.message_user(request, f"已更新 {updated} 条记录") @admin.action(description="导出选中数据") def export_selected(self, request, queryset): import csv from django.http import HttpResponse response = HttpResponse(content_type="text/csv") response["Content-Disposition"] = "attachment; filename=price_calendar.csv" writer = csv.writer(response) writer.writerow(["房型", "日期", "价格", "库存", "锁定"]) for row in queryset: writer.writerow([row.room_type, row.biz_date, row.price, row.inventory, row.locked]) return response这里要特别说一个经验:Django Admin默认的list_editable和date_hierarchy会显著提升运营人员的工作效率。比如价格日历表可以开启list_editable = ("price", "inventory", "locked"),运营人员在列表页直接修改多行数据一键保存,而不是点进每一行去改。一开始我担心这种方式太危险,实际设置好权限后,并不会造成误操作,反而被运营同事表扬"比原来的Excel方便多了"。5.2 运营权限模型:不是所有后台用户都能改房价旅游景区平台的后台用户至少有三种身份:超级管理员、酒店运营、财务客服。超级管理员管理全部数据和系统配置;酒店运营只能维护酒店、房型、价格库存和订单确认;财务客服只能查看订单、处理退款和导出流水,不能修改房价。Django自带的auth.Group完全能满足这个需求,不需要自己造轮子。我创建了三个Group,分别赋予不同Model的add、change、delete、view权限。这里踩过的一个坑是:Django的Model权限默认不能精确到字段级,比如不想让财务客服看到成本价,只靠Group做不到。我的处理方式是不在Django后台展示成本价字段,单独在API层做数据脱敏,财务客服导出报表时由接口过滤字段。另一个坑是Admin操作日志,Django自带的LogEntry只记录ModelAdmin操作,不记录直接调queryset.update()的批量操作,所以凡是对价格、库存、订单状态做批量修改时,我都在action里自己写入一条操作日志表,后面出问题才好追溯。后台和Flask之间还有一个需要打通的点:当房价或库存被修改时,前台缓存不能一直保留。我的做法是Django侧在Model的save()和delete()方法里发一个信号,把对应房型的Redis热点缓存key删掉。比如hotel_list:{scenic_id}:{checkin}:{checkout}:{guests}这种key在修改库存时无法精确匹配删除,就采用失效策略:按景区ID删除该景区下所有hotel_list:*前缀key,成本很低但能保证数据一致性。6. 部署上线和日常问题排查:从本地到服务器的完整路径项目做到能跑只是第一步,真正上线考验的是部署方案和排查效率。旅游景区的流量高峰有明显的时间段,比如节假日、周末、寒暑假,系统稳定性比花哨功能重要得多。我最终采用了Nginx + Gunicorn + Flask、Nginx + uWSGI + Django、MySQL、Redis 的经典组合,两个Python服务用Docker容器管理,MySQL和Redis单独部署在云数据库服务上,方便备份和扩容。6.1 两套服务如何用Docker Compose编排为了让环境一致,我写了一个docker-compose.yml,里面只放Flask和Django两个应用容器,Nginx、MySQL、Redis放在宿主机或云服务上。因为Flask的API服务对并发模型比较敏感,我用了Gunicorn的gthreadworker模式,设置workers=4、threads=8,这样既不会因为进程太多占用内存,也能应对在线支付回调带来的瞬时连接数。Django侧用uWSGI,设置processes=4、threads=2。两个服务都通过环境变量读取数据库和Redis连接串,不把敏感信息写进镜像。部署时我习惯先跑迁移,迁移成功后重启Django和Flask容器,再reload Nginx。有一个容易忽略的步骤是Nginx的client_max_body_size。酒店平台会有运营人员上传酒店图片,如果图片是几MB的,默认1MB限制会导致上传失败。我在Nginx配置里对/api/和/admin/都设置成client_max_body_size 20m;,并且在上传时限制图片分辨率和压缩级别,既保证加载速度,也减少存储压力。6.2 上线后高频问题速查表做这个项目过程中,我整理了下面几条最常排查的问题,基本上90%的故障都集中在这几个位置:现象可能原因解决思路游客下单时提示房间被抢占缓存锁未释放 / 数据库锁等待超时检查Redis锁的过期时间;在finally块中确认释放锁后台修改房价后前端不生效Redis缓存未删除检查Django信号是否注册成功;手动清理对应cache key支付回调成功但订单状态不变回调流水唯一索引冲突被吞异常查看支付流水表是否已有记录;检查验签逻辑两个日期跨天出现下单价不对时区或日期字段类型不一致统一使用date类型;禁用在业务代码里传datetimeFlask接口偶发500错误数据库连接池耗尽增加SQLAlchemy连接池pool_size和max_overflow,并设置pool_recycleDjango后台登录后跳转不准CSRF中间件或SITE_ID配置问题检查Django版本对应中间件设置,以及域名配置除了这些常规问题,我再分享一个容易被新人忽略的性能点:价格日历表是全项目数据量最大的表,因为每个酒店每个房型每天都有一行库存记录。一年下来可能几十万行。不要对整张表做复杂的模糊查询,要保证(room_type_id, biz_date)的联合索引唯一。如果运营要在后台按日期范围筛选,直接利用DjangoAdmin的date_hierarchy,它内部会走索引,性能没问题。为了进一步加快聚合查询,我还在Redis里存了最近30天的可用房型ID集合,作为接口的预过滤层,明显减少了MySQL压力。最后说一点个人体会。刚接到这个项目时,我也怀疑过Flask和Django混用是不是过度设计,但最终交付时发现,这套架构恰好匹配了业务两端的不同需求:游客端API需要轻量、灵活、快速迭代,运营后台需要完整、稳定、好维护。两个框架在同一个项目里不是互相打架,而是各管一段,边界清晰反而让代码维护变的简单。做完这个旅游景区酒店服务平台后,我不太会在"只用某一个框架"上纠结,而是更关注业务形态到底需要什么。如果你把表结构、库存模型、状态流想清楚了,用Flask还是Django都只是顺手的事。