简介:本资源是一套面向计算机专业本科生的毕业设计级票务预订系统源码,聚焦民航票价查询、火车票预定及旅社在线预订三大核心场景,适用于课程设计、毕设开发与全栈项目实践。压缩包共21个文件,总大小1.47MB,包含6个SQL脚本(涵盖数据库建表、触发器、随机数据生成等)、3个核心Python源文件(admin.py、dbSQL.py、TourBookingSystem.py)、4个编译后的pyc文件、3张界面截图PNG、1个C++数据生成程序(.cpp)以及PDF/DOCX格式的实验报告与操作说明文档,结构完整、模块职责清晰。已有283人学习下载,读者可直接部署运行,获取从MySQL数据库设计、Python后端逻辑实现、基础前端交互到完整业务流程闭环的全流程参考;配套文档详述系统功能与使用步骤,特别适合初学者理解多类型票务系统架构与跨语言协作开发模式。
1. 为什么一个民航+火车票预定系统,必须用 Python 做后端、MySQL 做数据底座?
你可能已经见过不少“订票系统”Demo:界面花哨、动画流畅,但一查数据库设计就露馅——航班和车次硬编码在字典里,余票靠全局变量减一,用户注册直接写文件。这种实现连并发 10 个请求都扛不住。真正能跑在测试环境、经得起压力验证的民航票价与火车票预定系统,核心不在前端交互多炫,而在于如何用 Python 精确建模运输资源的时空约束,再靠 MySQL 的事务隔离与索引能力守住数据一致性底线。它不是教学玩具,而是典型的“高并发读+低频写+强一致性+多维查询”场景:航班时刻表按天分区、车次座位按车厢/席别/铺位建模、票价需支持淡旺季浮动+折扣叠加+税费拆分、订单状态流转必须原子化。本文面向已会写 Flask/Django 路由、能建 MySQL 表但常卡在“余票扣减不准确”“车次查询慢”“订单超时未释放座位”等实际问题的开发者,从零推演一套可落地、可压测、可扩展的源码骨架——所有代码均基于 Python 3.9+ 和 MySQL 8.0+,不依赖任何商业中间件,全部使用标准库与主流 ORM(SQLAlchemy)。
2. 用 SQLAlchemy 定义民航与铁路双模实体:为什么不能共用一张 ticket 表?
2.1 运输资源的本质差异决定表结构必须分离
民航与铁路虽同属客运,但底层资源模型截然不同:
- 民航:航班号(CA123)唯一标识一次飞行,固定起降时间、机型、舱等分级(经济/公务/头等),座位按“行×列”物理布局,票价按舱等+提前天数动态浮动;
- 铁路:车次号(G101)每日循环开行,停靠站动态组合,座位按“车厢号+座位号+席别(二等/一等/商务)”三维定位,票价按区间里程+基准价+浮动系数计算。
若强行合并为ticket单表,必然导致大量 NULL 字段(如民航无“停靠站”,铁路无“机型”),且查询时无法利用索引——WHERE 条件既要判transport_type='air' AND flight_no='MU5101',又要兼容transport_type='rail' AND train_no='D301' AND from_station='SHA',MySQL 优化器极易放弃索引走全表扫描。
2.2 实体定义:用继承映射实现逻辑复用与物理隔离
# models.py from sqlalchemy import Column, Integer, String, DateTime, DECIMAL, ForeignKey, Boolean, Enum from sqlalchemy.ext.declarative import declarative_base from sqlalchemy.orm import relationship from enum import Enum as PyEnum Base = declarative_base() class TransportType(PyEnum): AIR = "air" RAIL = "rail" class TransportResource(Base): __tablename__ = 'transport_resources' id = Column(Integer, primary_key=True) transport_type = Column(Enum(TransportType), nullable=False) # 公共字段:运营状态、最后更新时间 is_active = Column(Boolean, default=True) updated_at = Column(DateTime, onupdate=func.now()) # 民航专用表:存储航班级元数据 class Flight(Base): __tablename__ = 'flights' id = Column(Integer, primary_key=True) flight_no = Column(String(10), unique=True, index=True) # 如 MU5101 airline_code = Column(String(3)) # 航空公司三字码 departure_airport = Column(String(4)) # IATA 机场码,如 SHA arrival_airport = Column(String(4)) scheduled_departure = Column(DateTime) scheduled_arrival = Column(DateTime) aircraft_type = Column(String(10)) # 如 B737-800 total_seats = Column(Integer) # 总座位数(按舱等汇总) # 关联公共资源 resource_id = Column(Integer, ForeignKey('transport_resources.id')) resource = relationship("TransportResource", backref="flight") # 铁路专用表:存储车次级元数据 class TrainRoute(Base): __tablename__ = 'train_routes' id = Column(Integer, primary_key=True) train_no = Column(String(10), unique=True, index=True) # 如 G101 train_type = Column(String(5)) # G/D/C 字头 departure_station = Column(String(20)) # 上海虹桥 arrival_station = Column(String(20)) departure_time = Column(DateTime) arrival_time = Column(DateTime) total_cars = Column(Integer) # 编组车厢数 # 关联公共资源 resource_id = Column(Integer, ForeignKey('transport_resources.id')) resource = relationship("TransportResource", backref="train_route") # 座位库存表:按运输类型分区存储,避免跨类型锁竞争 class SeatInventory(Base): __tablename__ = 'seat_inventory' id = Column(Integer, primary_key=True) transport_type = Column(Enum(TransportType), nullable=False, index=True) # 民航:关联 flight_id;铁路:关联 train_route_id —— 用同一字段存不同外键,需应用层保证 resource_id = Column(Integer, index=True) # 对应 flights.id 或 train_routes.id date = Column(DateTime, index=True) # 出发日期,用于分区查询 seat_class = Column(String(10)) # economy/business/sleeper/first seat_count = Column(Integer, default=0) # 当日该席别总座位数 available_count = Column(Integer, default=0) # 可售余票 # 复合索引:快速定位某日某车次某席别余票 __table_args__ = ( Index('ix_transport_date_class', 'transport_type', 'date', 'seat_class'), )提示:
resource_id字段虽指向不同表,但通过transport_type字段在应用层做路由判断,比用 JSON 存储或 EAV 模式更利于 MySQL 优化器生成执行计划。实际部署时,建议对seat_inventory表按date字段做 RANGE 分区(如每月一分区),避免单表过大拖慢查询。
2.3 为什么票价计算必须脱离数据库,在 Python 层实现?
MySQL 存储过程虽能写逻辑,但民航票价规则(如“提前 30 天预订享 9 折,7 天内加收 20% 旺季附加费”)和铁路票价公式(“基础票价 × 区间里程系数 × 席别倍率 × 浮动系数”)涉及大量条件分支与浮点运算,且需对接外部汇率、燃油附加费 API。若硬塞进存储过程:
- 调试困难:MySQL 的调试器远不如 Python 的 pdb;
- 版本管理失控:票价策略变更需 DBA 执行 SQL 脚本,无法纳入 Git;
- 扩展性差:新增“学生票”“军人优待”等规则需反复修改存储过程。
正确做法是将票价引擎封装为 Python 类,在 ORM 查询出基础数据后调用:
# pricing_engine.py class AirfareCalculator: def __init__(self, base_price: float, flight: Flight, booking_date: datetime): self.base_price = base_price self.flight = flight self.booking_date = booking_date def calculate(self) -> float: days_before_departure = (self.flight.scheduled_departure - self.booking_date).days price = self.base_price # 提前预订折扣 if days_before_departure >= 30: price *= 0.9 elif days_before_departure >= 7: price *= 0.95 # 旺季附加费(假设 7-9 月为旺季) if self.flight.scheduled_departure.month in [7, 8, 9]: price *= 1.2 # 税费附加(模拟) price += 50.0 # 固定机场建设费 + 燃油附加费 return round(price, 2) # 使用示例 flight = session.query(Flight).filter_by(flight_no="MU5101").first() calculator = AirfareCalculator(base_price=1200.0, flight=flight, booking_date=datetime.now()) final_price = calculator.calculate() # 返回 1260.003. 用乐观锁+数据库事务实现高并发余票扣减:为什么 UPDATE ... SET available_count = available_count - 1 不够?
3.1 直接 UPDATE 的致命缺陷:超卖与幻读
假设 100 个用户同时抢购同一航班的 1 张余票,执行以下 SQL:
UPDATE seat_inventory SET available_count = available_count - 1 WHERE transport_type = 'air' AND resource_id = 123 AND date = '2024-08-15' AND seat_class = 'economy' AND available_count > 0;表面看available_count > 0是安全阀,但问题在于:
- 超卖风险:MySQL 默认 REPEATABLE READ 隔离级别下,多个事务读到的
available_count初始值都是 1,各自执行-1后都写入 0,最终available_count变成 -1; - 幻读干扰:若另一事务在此期间插入新记录(如新增一个席别),当前事务的 WHERE 条件可能匹配到新行,破坏业务逻辑。
3.2 正确方案:SELECT FOR UPDATE + 事务边界控制
# booking_service.py from sqlalchemy.exc import NoResultFound def reserve_seat(session, transport_type: str, resource_id: int, date: datetime, seat_class: str, quantity: int = 1) -> bool: try: # 1. 加锁查询当前余票(FOR UPDATE 锁住该行,阻塞其他事务修改) inventory = session.query(SeatInventory).filter( SeatInventory.transport_type == transport_type, SeatInventory.resource_id == resource_id, SeatInventory.date == date, SeatInventory.seat_class == seat_class ).with_for_update().one() # 若无记录抛 NoResultFound # 2. 应用层校验余票充足(锁已持有,其他事务无法修改此行) if inventory.available_count < quantity: return False # 3. 扣减余票(此时可安全更新) inventory.available_count -= quantity session.flush() # 同步到 DB,但事务未提交 # 4. 创建订单记录(关联此库存行) order = Order( user_id=current_user.id, transport_type=transport_type, resource_id=resource_id, seat_class=seat_class, quantity=quantity, total_price=calculate_price(...), # 调用定价引擎 status="confirmed" ) session.add(order) # 5. 提交事务:原子性保证扣减与订单创建同步生效 session.commit() return True except NoResultFound: session.rollback() return False except Exception as e: session.rollback() raise e注意:
with_for_update()在 MySQL 中对应SELECT ... FOR UPDATE,它会对匹配的行加写锁,后续事务对该行的SELECT FOR UPDATE或UPDATE将被阻塞,直到前一事务提交或回滚。这是解决超卖问题最可靠的方式,比应用层 Redis 计数器更严格(Redis 无法保证与订单表的事务一致性)。
3.3 索引优化:让 SELECT FOR UPDATE 快到毫秒级
上述查询的 WHERE 条件包含四个字段,必须建立复合索引,否则FOR UPDATE会锁住整个表:
-- 在 seat_inventory 表上创建高效索引 CREATE INDEX idx_seat_inv_lookup ON seat_inventory (transport_type, resource_id, date, seat_class);验证索引是否生效:
EXPLAIN SELECT * FROM seat_inventory WHERE transport_type = 'air' AND resource_id = 123 AND date = '2024-08-15' AND seat_class = 'economy' FOR UPDATE;输出中key列应显示idx_seat_inv_lookup,rows应为 1(精准定位单行)。
4. 订单状态机与超时释放:如何防止用户下单不支付导致座位长期锁定?
4.1 状态流转必须用数据库驱动,而非内存标记
常见错误是用 Redis 存储“已锁定座位”,定时任务扫描过期 Key 并释放。问题在于:
- Redis 与 MySQL 数据不一致:订单表已确认,Redis 还显示锁定;
- 网络分区时,释放任务失败,座位永久冻结。
正确做法是将状态存在 MySQL,并用status字段 +locked_until时间戳双控:
# models.py 新增字段 class Order(Base): __tablename__ = 'orders' id = Column(Integer, primary_key=True) user_id = Column(Integer) transport_type = Column(Enum(TransportType)) resource_id = Column(Integer) seat_class = Column(String(10)) quantity = Column(Integer) total_price = Column(DECIMAL(10,2)) status = Column(String(20), default="pending") # pending/confirmed/cancelled created_at = Column(DateTime, default=func.now()) locked_until = Column(DateTime) # 仅 pending 状态有效,超时自动释放 # 状态机方法 def confirm_order(session, order_id: int) -> bool: # 乐观锁更新:只允许从 pending → confirmed result = session.execute( text("UPDATE orders SET status = 'confirmed' WHERE id = :id AND status = 'pending'"), {"id": order_id} ) if result.rowcount == 0: return False # 状态已变,拒绝确认 session.commit() return True def release_expired_locks(session): # 定时任务执行:释放超过 15 分钟未支付的 pending 订单 now = datetime.now() session.execute( text("UPDATE orders SET status = 'cancelled' WHERE status = 'pending' AND locked_until < :now"), {"now": now} ) session.commit()4.2 自动释放的触发时机与可靠性保障
- 触发方式:用 Linux cron 每分钟执行一次
release_expired_locks(),或集成 Celery Beat 定时任务; - 幂等性:
UPDATE ... WHERE status = 'pending'确保重复执行无副作用; - 监控告警:在定时任务中统计每次释放的订单数,若连续 5 分钟释放量突增(如 >100 单/分钟),触发告警——可能意味着支付网关故障,大量用户卡在 pending 状态。
# crontab 示例:每分钟检查一次 * * * * * cd /path/to/project && python -m scripts.release_locks >> /var/log/booking/lock_release.log 2>&1# scripts/release_locks.py from models import SessionLocal from booking_service import release_expired_locks if __name__ == "__main__": session = SessionLocal() try: released_count = release_expired_locks(session) print(f"[{datetime.now()}] Released {released_count} expired locks") finally: session.close()5. 查询性能攻坚:如何让“上海→北京高铁明日余票”接口在 200ms 内返回?
5.1 问题定位:慢查询日志暴露真实瓶颈
开启 MySQL 慢查询日志(slow_query_log = ON,long_query_time = 0.2),捕获典型查询:
SELECT s.seat_class, s.available_count, t.train_no, t.departure_time FROM seat_inventory s JOIN train_routes t ON s.resource_id = t.id WHERE s.transport_type = 'rail' AND t.departure_station = '上海虹桥' AND t.arrival_station = '北京南' AND s.date = '2024-08-16' ORDER BY t.departure_time;EXPLAIN显示type: ALL(全表扫描),原因是train_routes表缺少(departure_station, arrival_station)索引,导致 JOIN 时无法高效过滤。
5.2 索引与查询重写双管齐下
第一步:补全关键索引
-- 在 train_routes 表上添加复合索引 CREATE INDEX idx_train_route_stations ON train_routes (departure_station, arrival_station, departure_time);第二步:改写查询,避免 JOIN 时的笛卡尔积
原查询因seat_inventory与train_routes无直接业务关联字段(resource_id是外键,但train_routes.id与seat_inventory.resource_id一一对应),JOIN 效率低。改为子查询先定位车次,再查库存:
# 优化后的查询逻辑(Python 层) def get_rail_availability(session, from_station: str, to_station: str, date: datetime): # 1. 先查满足起止站的车次 ID 列表(利用新索引) train_ids = session.query(TrainRoute.id).filter( TrainRoute.departure_station == from_station, TrainRoute.arrival_station == to_station ).all() train_id_list = [t[0] for t in train_ids] # 2. 再查这些车次在指定日期的余票(利用 seat_inventory 复合索引) results = session.query( SeatInventory.seat_class, SeatInventory.available_count, TrainRoute.train_no, TrainRoute.departure_time ).join(TrainRoute, SeatInventory.resource_id == TrainRoute.id).filter( SeatInventory.transport_type == 'rail', SeatInventory.date == date, TrainRoute.id.in_(train_id_list) ).order_by(TrainRoute.departure_time).all() return results第三步:缓存高频查询结果
对“固定起止站+未来 7 天”的查询结果,用 Redis 缓存 5 分钟(避免重复计算):
import redis r = redis.Redis(host='localhost', port=6379, db=0) def cached_rail_availability(from_station, to_station, date_str): cache_key = f"rail_avail:{from_station}:{to_station}:{date_str}" cached = r.get(cache_key) if cached: return json.loads(cached) # 执行上述优化查询 results = get_rail_availability(...) r.setex(cache_key, 300, json.dumps(results)) # 缓存 5 分钟 return results5.3 参数调优:让 MySQL 为 OLTP 场景发力
在my.cnf中调整以下参数(基于 16GB 内存服务器):
| 参数 | 推荐值 | 说明 |
|---|---|---|
innodb_buffer_pool_size | 12G | 缓冲池占内存 75%,确保热数据常驻内存 |
innodb_log_file_size | 512M | 提升写吞吐,避免频繁 checkpoint |
innodb_thread_concurrency | 0 | 交给 OS 调度,避免 InnoDB 内部线程争用 |
query_cache_type | 0 | 关闭查询缓存(MySQL 8.0+ 已移除,但旧版需禁用) |
验证效果:压测时SHOW ENGINE INNODB STATUS\G中ROW OPERATIONS部分read views数量稳定,无lock wait timeout报错。
6. 源码结构与部署 checklist:如何让这套系统真正跑起来?
6.1 最小可运行目录结构
booking-system/ ├── requirements.txt # 核心依赖 ├── config.py # 数据库连接、密钥配置 ├── models.py # SQLAlchemy 实体定义(含迁移脚本) ├── pricing_engine.py # 票价计算逻辑 ├── booking_service.py # 订票核心服务(含锁、事务) ├── api/ # Flask/FastAPI 接口层 │ ├── __init__.py │ ├── routes.py # /api/flights, /api/trains, /api/orders │ └── schemas.py # Pydantic 请求/响应模型 ├── scripts/ │ ├── init_db.py # 初始化表结构与基础数据(航空公司、车站) │ └── release_locks.py # 定时释放过期锁 └── tests/ # 单元测试(重点覆盖余票扣减、状态机)6.2 初始化数据库的 5 个必做动作
创建数据库并授权
CREATE DATABASE booking DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; CREATE USER 'booking_app'@'%' IDENTIFIED BY 'StrongPass123!'; GRANT SELECT,INSERT,UPDATE,DELETE ON booking.* TO 'booking_app'@'%'; FLUSH PRIVILEGES;执行建表与索引
运行python scripts/init_db.py,它会调用 SQLAlchemy 的metadata.create_all()并手动执行索引 SQL。导入基础静态数据
- 航空公司代码表(IATA 三字码)
- 全国火车站列表(含拼音、电报码)
- 常见航线/车次模板(供测试用)
配置连接池
在config.py中设置:SQLALCHEMY_ENGINE_OPTIONS = { "pool_pre_ping": True, # 连接前检测有效性 "pool_recycle": 3600, # 连接复用 1 小时后重建 "pool_size": 10, # 初始连接数 "max_overflow": 20 # 最大溢出连接数 }验证事务行为
手动启动两个终端,分别执行:# 终端1:开始事务并锁定一行 mysql -u booking_app -p -e "START TRANSACTION; SELECT * FROM seat_inventory WHERE id=1 FOR UPDATE;" # 终端2:尝试更新同一行 mysql -u booking_app -p -e "UPDATE seat_inventory SET available_count=0 WHERE id=1;"观察终端2 是否阻塞,直至终端1 执行
COMMIT或ROLLBACK—— 这是乐观锁生效的直接证据。
提示:首次部署后,务必用
mysqlcheck -u booking_app -p --optimize booking对所有表执行优化,重建索引统计信息,避免优化器误判。
本文还有配套的精品资源,点击获取