☰
Flask与Django混合架构实践:构建景区酒店预订平台
2026/10/20 15:51:50 网站建设 项目流程

做了小半年的旅游景区酒店服务平台,总算从能跑变成了能用。这个项目从需求梳理到上线踩了不少坑,其中一个比较有代表性的决策,就是技术栈同时用到了 Python 生态里的 Flask 和 Django。很多人一听到"Flask + Django"就本能地问:这俩不是一个重量级的框架吗,为什么放在一起?这正是我想在这篇文章里聊透的事。

这个平台的核心业务很简单:游客登录后可以浏览景区介绍、查看周边酒店列表、按日期和房型搜索空房、提交订单并在线支付,运营方在后台管理景区内容、酒店房源、价格日历和订单状态。项目跑起来之后,后台管理人员、酒店方的操作体验和游客端的响应速度都达到了预期。如果你也在做类似的信息管理类平台,或者正纠结 Flask 和 Django 到底该怎么选、能不能混用,这篇文章应该能给你一些参考。

2. 项目整体设计与技术选型思路

1.1 为什么一把梭用了两个框架

先说结论:这个项目的主体业务用的是 Django,Flask 承担了部分独立服务接口。这不是为了炫技,而是被实际需求逼出来的。

项目里有一个比较棘手的功能模块:酒店房价和余量的实时同步。合作酒店的系统会定期推送房价库存数据,格式五花八门,有 JSON 的、有 XML 的,甚至还有 CSV。同步服务需要做数据清洗、格式转换、异常重试,而且它和主站的业务逻辑耦合度很低,几乎就是一条独立的数据管道。用 Django 直接做也不是不行,但会牵动主项目的 models、settings、迁移脚本,稍微改点东西就要跑一遍测试,风险太大。

Flask 在这里的优势就很明显了——它是一个"即插即用"的微型框架,起一个独立进程当数据接收端和清洗服务,和主站的 Django 通过 HTTP 接口通信。数据管道坏了不影响主站,主站发版也不用带着同步服务一起重启。所以这个项目的实际架构是:Django 负责核心业务,Flask 负责辅助服务,两者各司其职。

1.2 系统架构与模块划分

整个平台按职责拆成了三块:

  • Django 主站:用户认证、景区内容、酒店房型管理、订单流转、支付回调、后台管理。
  • Flask 辅助服务:酒店房价库存同步、节假日调价通知、数据清洗任务。
  • 前端页面:服务端渲染为主,配合少量 Ajax 局部刷新。

Django 这一侧用到了它的全套能力:自带 User 模型做扩展、ORM 做数据库操作、Admin 后台给运营人员用、模板引擎渲染页面。Flask 那一侧不连主数据库,只做数据处理,处理完的结果通过内部接口回传给 Django。

模块划分的思路是"主站封闭、边界清晰"。同步服务的所有数据都先落在独立表里,清洗完成后再通过服务层接口写入主库,不允许 Flask 服务直接操作 Django 的 model。这样两个项目虽然共用一个 MySQL 实例,但逻辑上完全隔离,出了问题也好排查。

1.3 业务场景梳理

需求方给的原始诉求其实很朴素:游客能查到景区附近的酒店,能下单,酒店方能收到订单通知。

但实际梳理下来,业务场景远比这复杂。景区有淡旺季,房价要跟着日期浮动;酒店的房间分为大床房、双床房、套房等类型,每种房型每天的可售数量不一样;订单可能取消、可能修改日期,钱要原路退回;平台方还得在后台手动调整某个日期的底价,应对临时活动。

这些场景对数据模型的设计要求很高。价格日历、库存日历、订单状态流转、取消策略、支付对账,每一个都是单独的复杂度。选 Django 当主框架的一个关键原因也在这里——它对这种多模型关联、事务操作、后台管理的场景支持非常成熟,开发效率明显比 Flask 高。

2. 核心细节解析与实操要点

2.1 数据模型设计与关键字段考量

数据模型是整个项目的基石,这里踩过的坑最有代表性。最初的设计只做了酒店表、房型表、订单表,没有考虑价格日历和库存日历的概念,结果上线第一天就发现问题:不同日期预订同样的房型,价格完全一样,库存也没有按天区分,根本没法用。

后来重构成了日历模型,核心表结构如下:

  • 景区表:名称、介绍、地址、图片、开放时间、评分。
  • 酒店表:所属景区、名称、地址、星级、设施列表、封面图。
  • 房型表:所属酒店、名称、面积、可住人数、床型、基础价格。
  • 房价日历表:房型、日期、价格、状态(可售/关房/满房)。
  • 库存日历表:房型、日期、剩余房间数。
  • 订单表:用户、酒店、房型、入住日期、离店日期、订单金额、状态。
  • 支付记录表:订单、支付流水号、支付金额、渠道、回调状态。

这些表里最值得注意的坑是价格和金额的字段类型。价格一律用 DecimalField 而不是 FloatField,浮点数做金额计算会出现 0.1 + 0.2 != 0.3 的问题,对账的时候会炸。库存字段虽然看起来是整数,但更新时要用乐观锁,后面会细说。

2.2 预订流程与状态机设计

预订流程是整个平台最核心的业务链路,涉及事务一致性。游客选定房型和日期后,系统要做几件事:锁定库存、生成订单、发送支付请求、等待回调。这一串操作里任何一步失败,都不能让用户数据处于中间状态。

订单状态机设计为:待支付 -> 已支付 -> 入住完成,以及待支付 -> 已取消,已支付 -> 已退款。这里有个值得分享的细节:待支付订单并不是永久有效的,超过了 15 分钟未支付要自动释放库存。这个功能用 Django 的定时任务实现,每隔 5 分钟扫描一次超时订单,批量修改状态并回补库存。

事务处理用 Django 的 transaction.atomic 块包裹,库存扣减使用乐观锁:UPDATE ... SET stock = stock - 1 WHERE id = ... AND stock > 0,确保并发下单时不会超卖。这个在后面的常见问题里我会展开讲。

2.3 Django Admin 的高效运用

Django Admin 是选择 Django 的另一个决定性因素。运营方需要的酒店管理、房型管理、价格调整、订单查询功能,几乎不用额外开发,配置好 ModelAdmin 就能用。

价格调整这块印象最深。酒店方经常要批量修改某一段日期的房价,比如国庆节前几天突然要求涨价。我们通过自定义 Admin 动作实现了"选择房型后,在指定日期范围内批量上调/下调价格",运营人员几分钟就能完成过去需要写 SQL 才能搞定的操作。

Admin 还有一个隐藏价值:它天然提供了数据录入和修改的完整审计日志,运营方排查数据问题时有据可查。这一点很多团队容易忽略,真到出问题要对账的时候才发现有个操作日志有多重要。

2.4 Flask 子服务的定位与接口约定

Flask 在项目里的定位是"旁路服务",核心职责是接收酒店方推送的房价库存数据。酒店方用的对接协议不一样,有的是 POST JSON,有的是上传 CSV 文件,我们的 Flask 服务就要把这些不同格式统一清洗成标准数据,再通过内部接口写入主库。

接口约定这里有一个比较重要的经验:Flask 和 Django 之间的内部接口虽然只在服务器内部调用,但依然要做签名校验。原因是这类服务很有可能因为部署配置问题意外暴露到公网,如果没有签名,任何人都可以往你的库存表里灌脏数据。

签名方案用的是最简单的"密钥 + 时间戳 + 请求体哈希"拼接后做 HMAC 校验,Flask 侧校验通过后,再调用 Django 的服务层接口写入数据。这套方案不复杂,但能挡住绝大多数误调用和恶意请求。

3. 实操过程与核心环节实现

3.1 环境准备与项目骨架搭建

开发环境用的是 Python 3.10,Django 4.2 LTS 版本,Flask 2.3。数据库选 MySQL 8.0,用 Docker 起实例,本地开发和线上环境一致,避免"在我机器上是好的"这种尴尬。

项目结构按"两个独立应用 + 共享代码目录"的方式组织:

project/ ├── django_main/ # Django 主站 │ ├── apps/ │ │ ├── accounts/ # 用户模块 │ │ ├── scenes/ # 景区模块 │ │ ├── hotels/ # 酒店房型模块 │ │ └── orders/ # 订单模块 │ └── config/ # Django 配置 ├── flask_service/ # Flask 同步服务 │ ├── app.py # Flask 入口 │ └── handlers/ # 数据清洗处理器 └── common/ # 两边共享的工具函数

虚拟环境到了这个阶段建议直接用 pipenv 或 poetry 管理,单纯用 requirements.txt 在多项目协作时经常出现依赖版本打架的情况。Python 虚拟环境是基本功,不再多说,但把 Django 和 Flask 的依赖分开放在两个虚拟环境里是必须的,因为它们对某些第三方库的版本要求并不完全一样。

3.2 核心业务代码实现

Django 侧的模型代码是整个平台的核心。以房价日历表为例,模型定义大致是这样:

from django.db import models class PriceCalendar(models.Model): room_type = models.ForeignKey("hotels.RoomType", on_delete=models.CASCADE, related_name="prices") date = models.DateField() price = models.DecimalField(max_digits=10, decimal_places=2) status = models.SmallIntegerField(choices=((1, "可售"), (2, "关房"), (3, "满房")), default=1) class Meta: db_table = "price_calendar" unique_together = ("room_type", "date") indexes = [models.Index(fields=["room_type", "date"])]

联合唯一约束在这里特别重要,它保证了同一个房型同一天只能有一条价格记录,从数据库层面杜绝了重复数据。

库存扣减的乐观锁实现:

from django.db import transaction from django.db.models import F @transaction.atomic def deduct_stock(room_type_id, date, count): result = InventoryCalendar.objects.filter( room_type_id=room_type_id, date=date, stock__gte=count ).update(stock=F("stock") - count) if result == 0: raise ValueError("库存不足") return True

使用F("stock") - count而不是先查后改,是因为查询和更新之间的时间窗口里很可能有其他请求也在改这行记录。F 表达式让更新操作在数据库层面原子完成,配合stock__gte=count的过滤条件,能够可靠避免超卖。

视图层用 Django 的类视图和数据库查询的 select_related/prefetch_related 优化关联查询。酒店详情页需要展示房型、价格日历、酒店图片等多组关联数据,直接用 ORM 默认取值会产生大量 SQL 查询,页面响应时间会从几十毫秒变成几秒。用预取优化后快很多,这个技巧在后面也会详细说。

3.3 Flask 同步服务实现

Flask 服务最核心的代码是数据清洗和统一入库。酒店方推送的数据格式不确定,所以用了 Flask 的蓝图注册版本路由,每种格式对应一个处理函数:

from flask import Flask, request, jsonify import hmac, hashlib app = Flask(__name__) @app.post("/api/sync/hotel_price") def sync_hotel_price(): # 1. 验签 sign = request.headers.get("X-Signature") if not verify_sign(request.get_data(), sign): return jsonify({"code": 401, "msg": "invalid signature"}), 401 # 2. 解析数据,这里假设收到的是 JSON payload = request.get_json() # 3. 清洗:校验字段、转换日期、剔除非法数据 cleaned = clean_price_data(payload) # 4. 调用 Django 内部接口写入主库 result = call_django_api("/internal/sync/prices", cleaned) return jsonify({"code": 0, "data": result})

这里面有一个隐藏得很深的坑:Flask 的request.get_json()在请求体是空字符串时会抛异常,而且乱数据还可能出现编码问题。我们在清洗函数里加了全面的数据校验,任何一条数据不合法就跳过并记录日志,不能让一条脏数据摧毁整个批次。

3.4 前后端联调与数据流转

前后端联调阶段最耗时的是预订流程的数据流转。游客在前端选择日期后,前端要请求房价日历接口获取可选房型和价格;提交订单时要把房型、日期、游客信息打包发给 Django 接口; Django 生成订单后返回订单号和支付参数;前端再调用支付接口,最后通过支付回调确认支付结果。

这个链路数据方向多、状态变化频繁,联调时经常出现订单状态停留在"待支付"的问题。后来加了一个联调用的调试面板,把整个请求链路的关键日志和状态变化输出到页面上,问题定位快了不少。把支付回调的日志全部落库,也是一个排障利器,回调失败还是参数错误,一眼就能看出来。

4. 常见问题与排查技巧实录

4.1 并发预订导致库存超卖

这个问题是预订系统里最经典的坑,我们也是在压测阶段发现的。压测脚本模拟 50 个用户同时预订同一房型同一日期,结果库存从 10 变成了负数。原因就是最初的库存扣减代码是"先查询 stock,再判断 if stock > 0,然后执行 update",查询和更新之间存在时间差,并发请求全都读到了同一个剩余库存。

修复方案就是刚才讲的乐观锁。用update语句的stock__gte=count条件确保数据库层面只有足够库存时才扣减,返回值等于 0 表示扣减失败,直接提示用户库存不足。修复后连续压测多次,库存数据始终正确。

4.2 ORM 查询导致的 N+1 问题

N+1 问题是 Django 新手最容易踩的坑。酒店列表页要展示每家酒店及其关联的景区名称、最新价格,如果直接用 ORM 查询酒店列表,然后在模板里通过关系字段取景区名称,每取一条酒店就会额外发一条 SQL 查询,列表有 50 家酒店就会产生 1 + 50 次查询。

解决方案是查询时主动用select_related拉取外键关联对象。对于多对多关系或反向关联,用prefetch_related。最关键的是在开发环境安装一个查 SQL 计数的工具,把页面请求产生的 SQL 数量和耗时直接打印到终端,任何 N+1 问题一眼就能发现。

4.3 图片上传与静态资源处理

景区图片和酒店图片是运营内容的刚需,但这个环节特别容易在部署后出问题。开发环境下用 Django 的 MEDIA_ROOT 存文件没问题,但线上如果还用同样的方案,应用服务器重启或者多实例部署时,用户上传的图片就可能丢失。

我们最后的处理是:本地图片目录只做临时存储,图片上传后立即转存到对象存储服务,数据库里存的是对象的文件路径 URL。模板里展示图片时直接拼完整 URL,不经过 Django,这样静态资源请求不占用应用服务器带宽,页面加载速度也快很多。

4.4 接口鉴权与数据安全

游客端的酒店查询接口、房价日历接口理论上不需要登录也能访问,但订单相关接口必须校验登录状态。最开始用的是 Django 自带 session 认证,前端通过 Cookie 携带 sessionid,联调一直没问题。后来发现手机端浏览器对第三方 Cookie 的限制越来越严格,session 方案不稳定,就重构了 Token 认证方案。

自定义了一个简单的 Token 模型,登录成功后生成随机 Token 存到 Redis,设置过期时间,前端请求时放在 Authorization 请求头里,后端写一个自定义认证类解析 Token 并获取用户。这个方案对前后端分离的场景友好得多,也方便后续接入小程序。

4.5 定时任务与订单超时处理

订单超时自动取消的功能看起来简单,但因为涉及库存回补,必须保证幂等。定时任务扫描到超时订单后,先判断订单当前状态是否还是"待支付",是的话才执行取消和库存回补。如果任务执行到一半挂了,重启后再次扫描不会重复回补库存,因为这个订单的状态已经变成"已取消"了。

这个幂等设计是整个定时任务里最重要的一环。没有这个保障,一个订单超时后如果触发两次回补,酒店方的可售库存就会虚高,实际入住时才发现安排不下,那就成事故了。

5. 踩坑后的几点真实体会

项目收尾之后,回头去看最初"Flask 和 Django 一起用"的决定,我觉得是值得的。Django 把人从重复造轮子的工作中解放出来,Admin 后台、认证系统、ORM 这些能力确实稳,但代价是框架本身比较重,不适合做高吞吐的旁路数据处理;Flask 刚好补上了这个短板。两个框架放在一起,如果有清晰的模块边界,完全是一种务实的组合。

个人印象最深的一个教训是:上线前一定要做并发测试,尤其是涉及金额、库存这类核心资源的操作。很多问题看起来在单用户测试时完美运行,并发一上来就原形毕露。乐观锁、事务、幂等性这些概念,读文档时总觉得懂了,只有真正被线上事故教育过,才知道这些设计不是理论,而是保命的东西。

再分享一个小技巧:给所有外部调用和内部模块之间的接口都加上请求 ID,日志里带着请求 ID 打,排查问题的时候顺着一个 ID 能把整条调用链串起来。项目刚上线那阵子一天要查好几次生产问题,全靠这个设计节省了大量时间。

这个平台后续要扩展的方向还挺多,比如接入民宿房源、增加景区门票预订、做酒店价格的智能推荐,技术架构上现有的 Django 主站 + Flask 旁路服务的骨架还能继续支撑。如果你也在做类似的项目,欢迎根据你的场景去调整这个方案,核心思路是一样的:主业务选成熟的框架,旁路服务保持轻量独立,数据安全和幂等性从一开始就要想清楚。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询