资讯详情

Django与MySQL实战:航空订票系统从建模到部署全解析

📅 2026/9/14 3:54:02 | 华诺云谱 👁 阅读
Django与MySQL实战:航空订票系统从建模到部署全解析
简介基于Django框架的航空订票管理系统完整项目定位为毕业设计、期末大作业及课程设计场景的参考成品面向需要快速上手Web开发的Python初学者。系统采用Python、Django与MySQL数据库实现涵盖用户登录、航班查询、在线订票、订单管理等核心功能模块代码内附详细注释便于二次开发与答辩讲解。压缩包共2000个文件前端以1629个JS、252个HTML、53个CSS和33个HTM为主负责页面交互、界面样式与静态资源组织后端包含5个Python核心文件和4个XML配置文件另有4个TXT、1个Markdown说明文档及19个JSON数据文件整体仅5.89MB结构清晰、部署简单。项目经严格调试可正常运行界面美观、操作便捷曾被导师评为高分作品。目前已有65人学习下载既适合直接作为毕业设计或期末大作业交付也适合在Django路由、视图、ORM、模板等机制上做进一步学习与扩展。1. 一个能跑起来的航空订票系统Django 与 MySQL 是怎么分工的航空订票管理系统是 Django 学习者绕不开的一个完整项目。它不像博客只有增删改查也不像商城要处理支付和商品规格业务主线非常明确航班查询、座位预订、订单取消。这条主线恰好把 ORM 关联查询、事务并发控制、后台维护全部串起来。用 Django 和 MySQL 的成熟方案是Django 负责业务和页面渲染MySQL 负责持久化内置 admin 充当管理后台。模型建对之后查询、页面、表单都能靠通用机制生成这是「超级简单」的关键。但简单不等于没有门槛。新手卡住的位置集中在三处mysqlclient 装不上、时区导致订票日期差一天、取消订单后库存没有回补。下文按建模、业务、部署、调优的顺序把从零到可部署的路径拆开讲透。2. 数据模型先行用 Django ORM 把航班、乘客、订单三张主表立起来2.1 先建模而不是先写视图的原因拿到「航空订票管理系统」这个需求很多人的第一反应是去写视图。这个顺序是反的。Django 的开发节奏是 startapp → model → migrate → admin → view模型决定了数据库里有什么表、表之间怎么关联、表单校验有哪些约束视图只是把模型暴露出来的数据组织成页面。模型一旦定错后面所有视图、模板、admin 配置都要跟着返工所以建模是整个项目的压舱石。最小数据模型是三张主表Flight 航班表、Passenger 乘客表、Order 订单表。航班和乘客是基础数据订单是中间表外键同时指向航班和乘客。这里有一个设计取舍是否把座位拆成独立的 Seat 表。独立座位表能支撑选座和舱位管理但会让下单逻辑复杂一个量级如果需求只是「卖完为止」在 Flight 上维护一个已售座位数字段是更简单的方案。项目定位既然是简单管理系统计数方案更合理。先执行python manage.py startapp ticket创建应用把 ticket 加进 INSTALLED_APPS再写模型后续所有迁移都以这个应用为边界。2.2 航班、乘客、订单三张表的模型定义# ticket/models.py from django.db import models from django.contrib.auth.models import User class Flight(models.Model): flight_no models.CharField(航班号, max_length10, uniqueTrue) origin models.CharField(出发城市, max_length50) destination models.CharField(到达城市, max_length50) depart_time models.DateTimeField(起飞时间) arrive_time models.DateTimeField(到达时间) price models.DecimalField(票价, max_digits10, decimal_places2) total_seats models.PositiveIntegerField(总座位数, default180) booked_seats models.PositiveIntegerField(已售座位数, default0) class Meta: db_table ticket_flight indexes [ models.Index(fields[origin, destination, depart_time], nameidx_route_time), ] def remaining(self): return self.total_seats - self.booked_seats def __str__(self): return f{self.flight_no} {self.origin}-{self.destination} class Passenger(models.Model): name models.CharField(姓名, max_length50) id_card models.CharField(身份证号, max_length18, uniqueTrue) phone models.CharField(手机号, max_length11) class Meta: db_table ticket_passenger def __str__(self): return self.name class Order(models.Model): order_no models.CharField(订单号, max_length32, uniqueTrue) user models.ForeignKey(User, on_deletemodels.CASCADE, verbose_name下单用户) flight models.ForeignKey(Flight, on_deletemodels.CASCADE, verbose_name航班) passenger models.ForeignKey(Passenger, on_deletemodels.PROTECT, verbose_name乘机人) status models.SmallIntegerField(状态, choices( (1, 已支付), (0, 待支付), (-1, 已取消), ), default0) created_at models.DateTimeField(下单时间, auto_now_addTrue) class Meta: db_table ticket_order def __str__(self): return self.order_no代码说明Flight 用 booked_seats 记录已售座位数remaining() 是计算属性不落库避免每下一次单要同步两个数字。Order 的 status 用 SmallIntegerField 加 choices比字符串状态省空间、比较更快。Passenger 的 id_card 加 unique 约束防止同一身份证在「无实名限制」的情况下重复建乘客。三个类都在 Meta 里显式指定 db_table避免 Django 自动生成的表名与后续 SQL 运维脚本对不上。外键约束上Order → User 用 on_deletemodels.CASCADE用户注销时订单跟着删Order → Passenger 用 on_deletemodels.PROTECT乘机人名下有订单时不允许删除乘客。Django 2.0 之后 on_delete 是必填参数新手最容易漏的就是这里迁移时会直接报 TypeError。db_table、indexes、choices 这三个 Meta 配置在订票场景里都是刚需分别对应表名可读性、查询性能和状态可维护性。2.3 settings 连接 MySQL 与执行迁移命令模型写好之后先配数据库再跑迁移。默认配置连的是 SQLite直接 migrate 只会生成本地文件不会连到 MySQL这一步是「mysql 安装配置」和 Django 接轨的起点。# airline/settings.py DATABASES { default: { ENGINE: django.db.backends.mysql, NAME: airline, USER: root, PASSWORD: your_password, HOST: 127.0.0.1, PORT: 3306, OPTIONS: { charset: utf8mb4, init_command: SET sql_modeSTRICT_TRANS_TABLES, }, } }配置要点NAME 是 MySQL 里提前建好的库名字符集用 utf8mb4 而不是 utf8因为 MySQL 的 utf8 存不了 emoji 和生僻字STRICT_TRANS_TABLES 让写入超长字段时报错而不是静默截断调试时能少很多脏数据。HOST 在本地开发填 127.0.0.1部署到宝塔后改成服务器内网地址或保持默认即可。# 先装驱动再建库最后迁移 pip install mysqlclient mysql -u root -p -e CREATE DATABASE airline CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; python manage.py makemigrations ticket python manage.py migrate命令逻辑makemigrations 读取 models.py 生成迁移文件migrate 把迁移应用到数据库两者缺一不可。如果提示 No changes detected说明应用没注册进 INSTALLED_APPS这是新手最高频的报错。以下表格对应迁移后落库的三张核心表表名关键字段约束用途ticket_flightflight_no / origin / destination / depart_timeflight_no 唯一航班基础与余票ticket_passengername / id_card / phoneid_card 唯一乘机人信息ticket_orderorder_no / flight_id / passenger_id / statusorder_no 唯一订票业务记录提示本地用 SQLite 开发过再切 MySQL 的项目必须重新 migrate。SQLite 对类型检查宽松MySQL 严格得多原来能存进去的脏数据可能直接报 1366 或 1406 错误。3. 核心业务闭环航班检索、下单扣座、取消订单与 Admin 接管后台3.1 航班检索日期范围过滤与组合条件查询订票系统第一个功能是查航班用户按出发城市、到达城市、出发日期三个条件筛航班。多条件组合用链式 filter 即可真正的坑在日期字段。depart_time 是 DateTimeField如果写成depart_time__datedayMySQL 会对该列做函数运算索引直接失效。正确做法是算出当天的起止时间再走范围查询。# ticket/views.py from datetime import datetime, timedelta from django.shortcuts import render from .models import Flight def flight_search(request): origin request.GET.get(origin, ).strip() destination request.GET.get(destination, ).strip() day request.GET.get(date, ).strip() flights Flight.objects.all() if origin: flights flights.filter(originorigin) if destination: flights flights.filter(destinationdestination) if day: try: start datetime.strptime(day, %Y-%m-%d) end start timedelta(days1) flights flights.filter(depart_time__gtestart, depart_time__ltend) except ValueError: pass context { flights: flights.order_by(depart_time), city_list: Flight.objects.values_list(origin, flatTrue).distinct(), } return render(request, ticket/search.html, context)逻辑说明每个条件先用 if 判断参数是否为空避免只填一个条件时把其他 filter 也带上。日期过滤用__gte和__lt组成左闭右开区间覆盖当天的 00:00:00 到 23:59:59能用到 depart_time 上的索引。city_list 用 values_list 加 distinct 取出去重城市列表给页面下拉框用比单独建字典表省事。这里没有用 Q 对象因为条件之间都是 AND 关系链式 filter 更直观。Q 对象适合「出发城市等于北京 OR 到达城市等于北京」这种跨字段 OR 场景后期如果要做「无座航班也展示但标售罄」的扩展再用 Q 对象改写不迟。另一个常见方向是 Django 前后端分离用 DRF 输出 JSON 给 Vue 或小程序消费如果项目只是教学演示或内部使用模板渲染路线成本更低。3.2 下单扣库存事务与 select_for_update 防止超卖下单是系统核心。两个用户同时抢最后一张票如果各自读余票再写回两个请求都读到余票为 1各加一结果卖出两张。防超卖的标准做法是用select_for_update()加行锁再用事务把「查余票 → 扣库存 → 建订单」包成原子操作。# ticket/views.py import time from django.db import transaction from django.shortcuts import render, redirect, get_object_or_404 from .models import Flight, Order, Passenger transaction.atomic def create_order(request, flight_id): # 行锁必须发生在事务内部否则不生效 flight Flight.objects.select_for_update().get(pkflight_id) if flight.remaining() 0: return render(request, ticket/error.html, {msg: 余票不足}) passenger Passenger.objects.create( namerequest.POST[name], id_cardrequest.POST[id_card], phonerequest.POST[phone], ) flight.booked_seats 1 flight.save(update_fields[booked_seats]) Order.objects.create( order_notime.strftime(%Y%m%d%H%M%S) str(flight_id), userrequest.user, flightflight, passengerpassenger, status1, ) return redirect(order_detail, order_idflight_id)参数说明select_for_update()依赖事务所以函数必须有transaction.atomic装饰器事务提交后锁才释放。save(update_fields[booked_seats])只更新该字段减少 MySQL 写入量和锁持有时间。order_no 用时间戳加航班 ID 拼装演示项目够用同一秒内大量并发时可能撞号生产环境要换成 UUID 或独立发号器。transaction.atomic的附带收益是异常自动回滚假如 Passenger 创建成功但 Order 创建失败booked_seats 会回到原值不会出现库存扣了订单没建成的中间态。3.3 取消订单Django 执行查询删除对象的正确姿势取消订单是下单的反向操作。Django 执行查询删除对象直接调用delete()方法但订票业务的取消不能只删 Order 记录还要把航班库存加回去这又是一个需要事务保护的组合操作。# ticket/views.py from django.db import transaction from django.shortcuts import render, redirect from .models import Order, Flight transaction.atomic def cancel_order(request, order_id): order Order.objects.select_related(flight).get(pkorder_id) flight Flight.objects.select_for_update().get(pkorder.flight_id) if order.status -1: return render(request, ticket/error.html, {msg: 订单已取消}) order.status -1 order.save(update_fields[status]) if flight.booked_seats 0: flight.booked_seats - 1 flight.save(update_fields[booked_seats]) return redirect(order_list)实现要点这里没有调用order.delete()而是把 status 置为 -1属于逻辑删除。逻辑删除比物理删除安全后续对账、统计退票率时数据还在真正需要物理删除的场景Order.objects.filter(idorder_id).delete()同样可行但必须把库存回补语句放在同一事务里。给订单和航班都加锁防止取消订单与新的下单并发时库存计算错乱。删除方式的选择可以直接决定数据审计能不能做订票这类涉及资金和票务的系统默认逻辑删除是比较稳妥的。删除方案执行方式适用场景物理删除order.delete()测试数据、错误数据清理逻辑删除status -1业务订单、需要审计追溯3.4 Django Admin 界面美化与管理端列表配置Django 内置 admin 是这套订票系统最省力的后台模型注册后增删改查全部自动生成。但默认列表只显示__str__返回值必须通过 ModelAdmin 配置才能看到业务字段这一步就是最常见的 Django admin 界面美化入口。# ticket/admin.py from django.contrib import admin from .models import Flight, Passenger, Order admin.register(Flight) class FlightAdmin(admin.ModelAdmin): list_display (flight_no, origin, destination, depart_time, price, remaining) list_filter (origin, destination) search_fields (flight_no, origin) ordering (depart_time,) date_hierarchy depart_time admin.register(Order) class OrderAdmin(admin.ModelAdmin): list_display (order_no, user, flight, passenger, status, created_at) list_filter (status,) search_fields (order_no, user__username) list_editable (status,) list_per_page 20参数说明list_display 配满业务字段后列表页才有信息量remaining 是模型方法也能直接展示list_filter 在右侧生成筛选栏status 和城市这类枚举字段最适合放这里search_fields 生成搜索框跨表搜索用user__username双下划线语法list_editable 让状态可以在列表页直接下拉修改避免每次进详情页。原生界面嫌丑可以换 django-simpleui 这类主题但建议先跑通功能再美化皮肤不解决业务问题。4. 项目落地mysqlclient 安装、宝塔部署 Django 与静态资源4.1 本地环境python 安装、django 版本与 mysqlclient 依赖标题里写了「项目源码文档说明」意味着拿到代码的人要在自己的机器上把它跑起来。环境安装是第一个坎python 建议直接装 3.10 或 3.12django 用 4.2 LTS 版本mysqlclient 在 Windows 上经常编译失败在 Linux 上要先装系统依赖这一套组合踩坑率最高。# Windows 下直接安装官方安装包或预编译 whl pip install django4.2 pip install mysqlclient # Ubuntu/Debian 下先装编译依赖再装驱动 sudo apt-get update sudo apt-get install python3-dev default-libmysqlclient-dev build-essential pip install django4.2 mysqlclientmysqlclient 安装失败的典型报错是error: command gcc failed或者找不到 mysql_config根因是系统缺少 MySQL 客户端开发库Ubuntu 装上 default-libmysqlclient-dev 后一般能解决。另一个方案是改用纯 Python 的 PyMySQL在 manage.py 里做兼容处理但 PyMySQL 的连接性能和高并发表现不如 mysqlclient生产环境我一般还是坚持用 mysqlclient。4.2 宝塔部署 DjangouWSGI 与 Nginx 的典型配置把订票系统部署到服务器常见做法是宝塔面板 uWSGI Nginx。Django 自带的 runserver 只能用于开发并发能力很差生产环境必须交给 WSGI 服务器。先在项目根目录写 uwsgi.ini# uwsgi.ini [uwsgi] chdir /www/wwwroot/airline module airline.wsgi:application master true processes 2 threads 2 socket 127.0.0.1:8001 chmod-socket 664 vacuum true die-on-term true参数说明module 指向项目目录下的 airline/wsgi.py这是 Django 自动生成的 WSGI 入口宝塔部署时路径要和站点目录完全一致。socket 用本机端口而不是 http 端口因为前面还有 Nginx 做反向代理。processes 和 threads 先按 CPU 核数乘以 2 估算业务量上来后再调不要一上来就开 8 个进程。Nginx 站点配置至少要配两个 locationserver { listen 80; server_name your_domain.com; location /static/ { alias /www/wwwroot/airline/static/; } location / { include uwsgi_params; uwsgi_pass 127.0.0.1:8001; } }启动顺序是先启动 uwsgi再 reload nginx。uwsgi 用uwsgi --ini uwsgi.ini启动宝塔的 Python 项目管理器本质也是帮你起 uwsgi 进程。部署后首页能打开但 CSS 全丢问题几乎都出在静态文件见下一节。配置项值作用socket127.0.0.1:8001uwsgi 与 Nginx 通信端口processes2worker 进程数threads2每进程线程数vacuumtrue退出时清理 socket 文件4.3 时区、静态文件与媒体文件的三个高频坑第一个坑是时区。settings.py 默认TIME_ZONE UTC航班起飞时间按北京时间展示会差 8 小时订票日期直接错一天。国内项目统一改为下面三行LANGUAGE_CODE zh-hans TIME_ZONE Asia/Shanghai USE_TZ True容易混淆的点USE_TZ 保持 True 时Django 在数据库里存 UTC 时间渲染时按 Asia/Shanghai 转成本地时间。如果对时间精度要求不高可以 USE_TZ False 让 MySQL 直接存本地时间但所有 datetime 比较都要保证时区一致出问题更难排查。订票系统涉及航班时刻强烈建议 USE_TZ True只在展示层做转换。第二个坑是静态文件。开发模式下 runserver 自动处理 /static/部署后 Django 不再托管静态文件必须执行 collectstatic 汇总再交给 Nginx。python manage.py collectstatic --noinput第三个坑是媒体文件。系统如果支持上传头像或行程单要配置 MEDIA_ROOT 和 MEDIA_URL并在 Nginx 里加 /media/ 的 location否则部署环境上传的文件会丢失或报权限错误。这三处配置每一项都对应一次线上事故部署前逐项核对。5. 查询变慢以后MySQL 创建索引、慢查询定位与数据回滚验证5.1 MySQL 创建索引与组合索引的最左前缀数据量上去后航班查询是第一个性能瓶颈。前面模型里已经在 origin、destination、depart_time 上声明了组合索引如果数据库是手工建的或者要给线上已有表补索引直接用 SQL 执行CREATE INDEX idx_flight_route ON ticket_flight(origin, destination, depart_time);组合索引的字段顺序有讲究MySQL 最左前缀原则要求查询条件必须从最左字段开始匹配。只填出发城市和日期时索引只用到 origin三个条件全填才能走完整索引。反过来如果「只按日期查全部航班」是高频场景还要单独给 depart_time 建单列索引因为组合索引在跳过了最左字段的查询里无法生效。这类业务逻辑放在 Django 事务层处理不需要动用 MySQL 存储过程存储过程会让排查问题和版本管理都变麻烦。5.2 慢查询日志与 EXPLAIN 定位具体 SQL页面响应变慢时先开 MySQL 慢查询日志看有没有可疑 SQL而不是急着加 Redis。SET GLOBAL slow_query_log ON; SET GLOBAL long_query_time 1; SHOW VARIABLES LIKE slow_query_log%;long_query_time 1 表示超过 1 秒的查询会被记录。拿日志里的 SQL 配合 EXPLAIN 看执行计划EXPLAIN SELECT * FROM ticket_flight WHERE origin 北京 AND destination 上海 AND depart_time 2024-06-01 00:00:00 AND depart_time 2024-06-02 00:00:00;EXPLAIN 输出里 type 字段是 ALL 说明全表扫描、索引没生效出现 ref 或 range 说明索引被正确使用。rows 字段估算扫描行数越小越好。订票查询变慢的根因九成不是 SQL 语法错误而是日期字段上写了DATE(depart_time) 2024-06-01这种函数包裹让索引失效改回范围查询立刻恢复。5.3 批量造数据与事务回滚验证技巧最后给一个调优前必须会的操作批量造测试数据。手工点几百次下单验证性能不现实用 Django shell 一次生成即可。# 项目根目录执行 python manage.py shell from ticket.models import Flight from datetime import datetime, timedelta base datetime(2024, 6, 1, 8, 0) cities [北京, 上海, 广州, 成都, 杭州, 西安] flights [] for i in range(10000): flights.append(Flight( flight_nofCA{i:04d}, origincities[i % 6], destinationcities[(i 3) % 6], depart_timebase timedelta(hoursi % 48), arrive_timebase timedelta(hoursi % 48 3), price500 (i % 10) * 100, total_seats180, booked_seats0, )) Flight.objects.bulk_create(flights, batch_size1000) print(Flight.objects.count())bulk_create 是批量写库的标准接口循环 create 一万次会建立一万次数据库连接bulk_create 只发少量 SQL 完成速度差两个数量级。同样的思路可以验证并发开两个 shell 同时调用下单函数确认 select_for_update 生效后不会超卖。验证完记得清理按 ticket_order、ticket_passenger、ticket_flight 的顺序删数据或直接对三张表 TRUNCATE 让自增 ID 归零避免测试数据污染后续统计。本文还有配套的精品资源点击获取
📝

华诺云谱内容团队

资深建站顾问 · 行业研究员

10年+企业数字化服务经验,专注智能建站、SEO优化与品牌营销,持续输出建站技巧、行业洞察与营销干货,已帮助5000+企业实现数字化增长。

你可能需要的服务

订阅华诺云谱资讯周报

每周一封,精选建站技巧、SEO与营销干货,直达邮箱。已有 8,000+ 企业主订阅,助你少走弯路。