简介:面向计算机、软件工程专业学生及课程设计/毕业设计写作者的机票预订系统详细设计报告,聚焦详细设计阶段,帮助读者解决系统分析、架构分层、数据库建模、模块划分与文档规范等撰写难题。压缩包内仅1个PDF文件,大小约2.14MB,可直接查阅、打印或作为电子模板使用。报告目录覆盖题目与问题定义、系统设计概述、可行性研究、需求分析、系统原理与主要技术、详细设计等章节,并展开逻辑模型、业务流程图、软件结构、用户/航班/订单数据表、功能模块、安全性设计及测试计划,内容偏重Web端B/S架构下的项目蓝图与设计文档表达。已有2824人学习下载,适合用作课程设计参考、毕设模板、实验报告、项目立项及答辩材料,也能辅助快速搭建机票预订系统的设计框架。
1. 机票预订系统详细设计报告到底要交付到什么粒度
在软件工程课程设计和毕设选题里,"机票预订系统"常年排在最常见的题目当中,真正卡住人的往往不是写代码,而是把那份软件工程 机票预订系统 详细设计 报告.pdf写到合格线以上。常见做法是把概要设计里的模块框图复制一遍,再补几句"负责查询航班"就交上去,评阅人一眼就能看出问题:没有算法、没有数据结构、没有接口签名、没有出错分支,那是目录不是详细设计。
详细设计的判定标准很朴素:拿到这份文档的人,能不能照着把代码写出来。所以必须出现的是模块划分依据、库表字段与约束、关键算法的输入输出与复杂度、接口函数签名、状态迁移和错误码。下面按实际做这类系统的顺序,把模块结构、数据库、航班搜索算法、订单状态机和 PDF 成稿逐段说清,代码以 Python 与标准 SQL 为主,报告里可以直接改写成伪码或流程图。适合正在做软件工程课程设计、毕业设计,或者第一次接手机票预订这类票务系统的后端同学。
2. 从用例到模块:机票预订系统的分层与接口契约
详细设计的第一页通常不是类图,而是模块边界。边界没锁死,后面的表结构、接口和流程图都会互相打架,写到最后只能靠"其他"模块兜底。
2.1 用用例-模块映射表锁死模块边界
把每一条用例落到唯一一个主责模块上,是详细设计里最省事也最有效的约束。下面这张表可以直接放进报告,模块名后面写代码时保持一致,评审时也能看出职责没有重叠。
| 用例 | 归属模块 | 输入 | 输出 | 关键约束 |
|---|---|---|---|---|
| 查询航班 | flight_search | 出发地、目的地、日期、舱位 | 可选航班列表(含中转) | 单次响应 800ms 内,最多 3 段中转 |
| 选择舱位占座 | inventory | 航班号、日期、舱位、人数 | 锁定凭证 lock_id | 锁定 15 分钟,到期自动释放 |
| 提交订单 | order | lock_id、乘客信息、联系人 | 订单号 | 幂等,超时回滚库存 |
| 支付 | payment | 订单号、金额、支付渠道 | 支付流水号 | 支付窗口 15 分钟 |
| 出票 | ticketing | 支付成功事件 | 票号 | 与航司接口对账,失败可重推 |
| 退改签 | order | 订单号、目标航班、乘客 | 差额与手续费 | 按退改规则阶梯计价 |
判断粒度是否合理,看两点:一个用例是否只有一个主责模块,跨模块调用是否全部走接口。如果某个模块既改订单表又直接读库存表,说明边界划错了,后面并发一上来必然出问题。
2.2 分层包结构与依赖方向
模块定下来之后,用包结构把它固化。这里的核心是依赖单向,下层不认识上层,领域对象不依赖任何框架。
flight_booking/ ├── api/ # 参数校验与响应组装,不写业务分支 │ ├── flight_api.py │ └── order_api.py ├── service/ # 业务编排,事务边界在这里 │ ├── search_service.py │ ├── inventory_service.py │ └── order_service.py ├── repository/ # 只放 SQL 与结果映射,不做业务判断 │ ├── flight_repo.py │ └── order_repo.py ├── domain/ # 实体与值对象,可被任意层引用 │ ├── flight.py │ └── order.py └── infra/ # 缓存、消息队列、支付网关适配 ├── cache.py └── pay_gateway.py依赖方向是 api → service → repository → domain,infra 横向被 service 依赖。写报告时把这段结构画成包图即可,重点是标注"repository 不允许被 api 层直接调用"这类禁令,评阅人看的就是这种约束。
2.3 接口先写签名,再写实现
详细设计里的接口清单如果只有中文描述,等于没写。用类型注解把签名固定下来,参数含义和默认值一起交代清楚,报告里可以原样贴成接口表。
from dataclasses import dataclass from datetime import date, datetime from typing import Protocol, List, Optional @dataclass(frozen=True) class FlightQuery: dep_city: str # 出发城市三字码,如 PEK arr_city: str # 到达城市三字码,如 SHA dep_date: date # 出发日期,按当地时区 cabin: str = "Y" # 舱位:Y 经济舱 / C 公务舱 / F 头等舱 max_stop: int = 1 # 最大中转次数,0 表示只看直飞 page_size: int = 20 # 分页大小,上限 100 @dataclass(frozen=True) class FlightOption: segments: List[str] # 航段列表,直飞长度为 1 total_price: float # 含税总价,单位元 depart_at: datetime arrive_at: datetime remain_seats: int # 该舱位剩余可售座位 class SearchService(Protocol): def search(self, q: FlightQuery) -> List[FlightOption]: ... def lock_seat(self, flight_no: str, d: date, cabin: str, n: int) -> Optional[str]: ...几个参数值得在报告里单独解释:max_stop是后续是否调用中转路径算法的开关,取 0 时只走直飞 SQL;page_size必须设上限,否则一次把全部航班拉进内存排序,接口会被拖垮;cabin用单字母码而不是中文,是为了和航司报文、库存表主键保持一致,避免多一层映射。
2.4 流程图写到能数出分支的粒度
最常见的问题是流程图画成"开始 → 查询 → 显示 → 结束"四个框。详细设计的流程图应该能让人数出至少五条分支。以查询下单主链为例,至少要覆盖:参数校验失败返回错误码、直飞命中直接返回、直飞为空进入中转分支、占座时库存不足、锁定超时被释放后订单回滚。
用绘图工具画完之后导出矢量图(SVG 或 EMF)再嵌进文档,比截 PNG 更保险,缩放和打印都不会发虚。流程里每个判断节点旁边标注对应模块和接口名,这样图和 2.1 的映射表能互相印证,不会出现流程里出现了表里没有的模块。
3. 机票预订系统数据库详细设计:表结构、索引与座位并发
库表是详细设计里最容易被敷衍的部分,也是唯一能直接转成建表语句的部分。写的时候按"字段名、类型、约束、含义"四列给全,别只写中文名。
3.1 六张核心表与字段取舍
票务系统的表不用多,但要够用。下面六张表能支撑查询、占座、下单、支付、出票、退改全流程。
| 表名 | 作用 | 关键字段 | 约束 |
|---|---|---|---|
| flight | 航班计划 | flight_no、dep_city、arr_city、dep_time、arr_time、aircraft | 主键 flight_no + dep_date |
| cabin_inventory | 舱位库存 | flight_no、dep_date、cabin、total_seats、sold_seats、version | 唯一键 flight_no+dep_date+cabin |
| order | 订单主表 | order_no、user_id、amount、status、request_id、expire_at | 唯一键 request_id |
| passenger | 订单乘客 | order_no、name、id_card、ticket_no | 外键 order_no |
| payment | 支付流水 | pay_no、order_no、channel、amount、status | 唯一键 pay_no |
| segment | 中转航段 | order_no、seq、flight_no、dep_date | 唯一键 order_no+seq |
字段取舍上有两条经验:库存不要用"一条座位一行"的建模,几百万行座位记录会让查询和锁都变重,"总量 + 已售 + 版本号"三列足够;订单和乘客拆表,是因为一个订单经常带多个乘客,票号是乘客级别的,混在一张表里退改签会很难写。
3.2 建表 SQL 与索引选择
CREATE TABLE cabin_inventory ( flight_no VARCHAR(10) NOT NULL, dep_date DATE NOT NULL, cabin CHAR(1) NOT NULL, total_seats INT NOT NULL DEFAULT 0, sold_seats INT NOT NULL DEFAULT 0, version INT NOT NULL DEFAULT 0, -- 乐观锁版本号 PRIMARY KEY (flight_no, dep_date, cabin) ) ENGINE=InnoDB; CREATE TABLE `order` ( order_no VARCHAR(32) NOT NULL, user_id BIGINT NOT NULL, amount DECIMAL(10,2) NOT NULL, status TINYINT NOT NULL DEFAULT 0, -- 0 待支付 1 已支付 2 已出票 3 已取消 request_id VARCHAR(64) NOT NULL, -- 幂等键,由客户端生成 expire_at DATETIME NOT NULL, created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (order_no), UNIQUE KEY uk_request (request_id), KEY idx_user_status (user_id, status) ) ENGINE=InnoDB;idx_user_status是给"我的订单"列表用的,用户维度加状态过滤是最频繁的组合,单独建索引比在 status 上建单列索引更实用。uk_request是幂等的基础,重复提交会被数据库直接拦掉,不用在应用层写分布式锁。注意amount用 DECIMAL 而不是 FLOAT,金额比较和累加不会出现精度误差。
3.3 座位库存扣减:乐观锁还是悲观锁
库存扣减是整个系统唯一真正需要认真对待并发的地方。选型结论通常是:库存行冲突概率低时用条件更新(乐观),冲突高时用SELECT ... FOR UPDATE(悲观)。课程设计规模下,条件更新足够,逻辑也更清楚。
-- 条件更新:把判断和扣减合并成一条语句,避免先查后改的竞态 UPDATE cabin_inventory SET sold_seats = sold_seats + :n, version = version + 1 WHERE flight_no = :flight_no AND dep_date = :dep_date AND cabin = :cabin AND total_seats - sold_seats >= :n; -- 余量足够才更新def lock_seat(self, flight_no, d, cabin, n): # rowcount 为 0 表示余量不足或航班不存在,统一按库存不足处理 rows = self.db.execute(SQL_DEDUCT, {...}) if rows.rowcount == 0: return None lock_id = f"LK{int(time.time() * 1000)}{random.randint(100, 999)}" self.cache.setex(f"lock:{lock_id}", 900, json.dumps({ # 900 秒 = 15 分钟 "flight_no": flight_no, "dep_date": str(d), "cabin": cabin, "n": n })) return lock_id这里的关键点是"判断余量"和"扣减"在同一条 SQL 里完成,WHERE里的余量条件保证了不会超卖;返回的rowcount是唯一可靠的判断依据,不要再用一条 SELECT 去确认。缓存里的锁凭证设 15 分钟过期,配合一个定时任务扫描过期未支付的订单做回补,把sold_seats减回去。参数上,锁定时间不要超过支付窗口,否则用户付完款座位已经释放,出票会失败。
3.4 幂等键与索引:避免重复下单和慢查询
幂等键由客户端生成,服务端只做唯一约束校验。重复请求进来时捕获唯一键冲突,直接查已有订单返回,而不是重新走一遍占座流程。
try: order_no = self.order_repo.insert(request_id=req_id, ...) except DuplicateKeyError: order_no = self.order_repo.find_by_request_id(req_id) # 幂等返回航班查询的索引要按实际 SQL 走。查询条件基本是"出发城市 + 到达城市 + 日期",因此flight表建联合索引idx_route_date (dep_city, arr_city, dep_date),把日期放在最后,因为前两列的区分度更高。一个常见的误用是把dep_time也塞进联合索引,实际查询按天过滤即可,时间范围交给 SQL 的区间条件,多一列反而增大索引体积。
4. 航班搜索与中转路径算法:Floyd、Dijkstra 怎么落地
直飞查询是简单过滤,真正能体现详细设计深度的是中转方案。中转本质上是图上的最短路径问题,标题里出现"类似 Floyd 的算法"时,多半就是要在这里做文章。
4.1 直飞检索的 SQL 与排序权重
SELECT f.flight_no, f.dep_time, f.arr_time, c.total_seats - c.sold_seats AS remain, p.price FROM flight f JOIN cabin_inventory c ON c.flight_no = f.flight_no AND c.dep_date = f.dep_date AND c.cabin = :cabin JOIN price p ON p.flight_no = f.flight_no AND p.cabin = :cabin WHERE f.dep_city = :dep AND f.arr_city = :arr AND f.dep_date = :d AND c.total_seats - c.sold_seats >= :n ORDER BY p.price ASC, f.dep_time ASC LIMIT :limit;排序权重建议做成配置而不是写死在 SQL 里:价格优先、耗时优先、直飞优先三种策略对应不同的ORDER BY组合。如果后续要加"综合排序",把它拆成price * w1 + duration * w2的表达式,权重写进配置文件,报告里也方便画成参数表。
4.2 把中转航线建模成带权有向图
中转查询的第一步是把航线网络抽象成图。城市是顶点,直飞航段是边,权重按你的优化目标定义。
| 图元素 | 对应实体 | 说明 |
|---|---|---|
| 顶点 V | 城市三字码 | 如 PEK、CAN、URC |
| 边 E | 直飞航段 | 有向,PEK→CAN 与 CAN→PEK 是两条边 |
| 边权 w | 价格 / 飞行时长 / 综合分 | 决定最短路径的含义 |
| 约束 | 衔接时间、中转次数 | MIN_CONNECT 最短衔接时间,MAX_STOP 最大中转次数 |
| 路径 | 一张联程票 | 航段序列,按时间递增排序 |
建模时有两个容易忽略的点:一是边权要能随日期变化,同一航段不同日期的价格不一样,所以图是"按出发日期切片"的,不能建一张静态图缓存到底;二是中转必须满足最短衔接时间(国内航班常见 90 分钟),否则算出来的路径在实际中根本接不上。
4.3 Floyd 与 Dijkstra 的实现与选型
Floyd 求全源最短路,三重循环,代码短,适合城市数量有限的场景;Dijkstra 求单源最短路,配合优先队列,适合只查某一个出发城市的场景。
INF = float("inf") def floyd(cities, w): """w[i][j] 为 i 到 j 的直飞代价,无边填 INF;返回全源最短路矩阵""" n = len(cities) dist = [row[:] for row in w] for k in range(n): # k 必须是最外层,代表允许经过的中转点 for i in range(n): if dist[i][k] == INF: continue # 剪枝:不可达就不用继续内层循环 for j in range(n): if dist[i][k] + dist[k][j] < dist[i][j]: dist[i][j] = dist[i][k] + dist[k][j] return dist import heapq def dijkstra(graph, src): """graph[u] = [(v, cost), ...];返回 src 到各点的最小代价与前驱""" dist = {src: 0} prev = {} pq = [(0, src)] while pq: d, u = heapq.heappop(pq) if d > dist.get(u, INF): continue # 过期条目,跳过 for v, cost in graph.get(u, []): nd = d + cost if nd < dist.get(v, INF): dist[v], prev[v] = nd, u heapq.heappush(pq, (nd, v)) return dist, prev两者的差别要在报告里写清楚:Floyd 时间复杂度 O(n³),n 是城市数量,几百个城市就是几千万次运算,但一次算完可以回答任意城市对;Dijkstra 用二叉堆是 O((V+E)logV),只回答一个源点,边权非负。课程设计的城市规模通常在几十到一百之间,如果系统要提供"任意两地中转方案"的预计算,Floyd 更省心;如果只是用户查一次算一次,Dijkstra 每次只跑相关子图,响应更稳。边权含负值的情况在票价场景下不存在,不需要考虑 Bellman-Ford。
4.4 剪枝参数:最短衔接时间与最大中转次数
不做剪枝的最短路在中转场景下会算出实际不可售的路径,比如凌晨落地、早上起飞、中间只有 40 分钟。下面这组参数建议在报告里单列一张表,并且和实现里的常量一一对应。
| 参数 | 建议值 | 作用 |
|---|---|---|
| MIN_CONNECT | 90 分钟 | 国内中转最短衔接时间,低于此值直接剪掉 |
| MAX_STOP | 2 | 最大中转次数,超过 3 段的路径不返回 |
| MAX_DETOUR | 1.8 倍 | 总耗时不超过直飞的 1.8 倍,避免绕远 |
| TOP_K | 20 | 每个城市对最多保留 20 条候选路径再排序 |
剪枝放在 Dijkstra 的松弛阶段做最自然:在nd < dist[v]判断之后,再加一层"衔接时间是否满足、段数是否超限、耗时是否超倍"的检查,不满足就不入堆。Floyd 版本则是先建图时就把不满足衔接时间的边权重设为 INF,把约束前移到建图阶段,代码更干净。注意中转次数统计的是边数减一,写代码时容易和边数混淆,测试时用一条两段中转的用例专门验证。
5. 订单状态机、时序与异常分支的详细设计
订单状态写不清,后面所有异常处理都会变成 if-else 堆叠。详细设计里把它做成一张迁移表,代码和测试用例都能从表里推出来。
5.1 订单状态迁移表
| 当前状态 | 触发事件 | 目标状态 | 动作 |
|---|---|---|---|
| 待支付(0) | 支付成功回调 | 已支付(1) | 写支付流水,通知出票 |
| 待支付(0) | 超时未支付 | 已取消(3) | 回补库存,释放锁定 |
| 待支付(0) | 用户取消 | 已取消(3) | 回补库存 |
| 已支付(1) | 出票成功 | 已出票(2) | 写票号,发通知 |
| 已支付(1) | 出票失败 | 已支付(1) | 记录重试次数,进入重推队列 |
| 已出票(2) | 退票申请 | 已退款(4) | 计算手续费,调用退款 |
状态用整数存储,注释里写明含义,别用中文枚举存库。迁移表之外的状态组合一律视为非法,更新时用条件更新兜底,见 5.4。
5.2 三段式下单时序与超时补偿
下单不要在一个事务里做完所有事,分成三段更符合实际:先占座,再创建订单,最后等支付回调确认。
def create_order(self, req_id, flight_info, passengers): cached = self.cache.get(f"req:{req_id}") # 1. 幂等前置检查 if cached: return json.loads(cached) lock_id = self.inventory.lock_seat(**flight_info) # 2. 占座,失败直接返回 if not lock_id: raise BizError(4001, "库存不足") order = self.order_repo.create( # 3. 落单,写库即幂等 request_id=req_id, status=0, expire_at=now() + timedelta(minutes=15), passengers=passengers) self.cache.setex(f"req:{req_id}", 3600, json.dumps(order)) self.mq.publish("order.created", order["order_no"], delay=900) # 4. 延迟消息兜底 return order延迟消息是这套流程里最关键的一环:即使缓存回调和定时任务都失效,15 分钟后延迟消息一定会到,消费端检查订单状态,仍是待支付就回补库存并把状态改成已取消。参数上,expire_at和锁定时间、队列延迟时间三者必须一致,改一个就要同时改另外两个,报告里用一张对照表写清楚。
5.3 错误码与异常分支设计
| 错误码 | 含义 | 处理方式 | 是否可重试 |
|---|---|---|---|
| 4001 | 库存不足 | 提示换航班 | 否 |
| 4002 | 锁定已过期 | 重新占座 | 是 |
| 4003 | 订单状态不允许该操作 | 返回当前状态 | 否 |
| 5001 | 支付网关超时 | 查询订单状态,勿盲目重发 | 是(查询优先) |
| 5002 | 出票接口失败 | 进入重试队列,最多 5 次 | 是 |
| 5003 | 数据库唯一键冲突 | 按幂等键返回已有订单 | 是 |
错误码要区分"业务拒绝"和"系统故障"两类,前者直接返回给用户,后者必须记日志并进重试队列。支付超时最忌讳直接重发扣款请求,正确做法是先查询支付渠道的订单状态,确认未支付再重新发起。
5.4 幂等键与重试参数在报告里的写法
所有状态更新都写成条件更新,把允许的前置状态放进 WHERE,让数据库来保证迁移合法。
UPDATE `order` SET status = 1, updated_at = NOW() WHERE order_no = :order_no AND status = 0; -- 只允许从待支付迁到已支付重试参数集中成配置:出票接口最多 5 次、间隔 30 秒指数退避、失败后进人工对账队列。这些数字不用精确,但要在文档里给出理由,比如"航司接口平均恢复时间在 2 分钟内,30 秒起步、5 次重试可覆盖约 8 分钟窗口"。评阅人关心的不是数值本身,而是你有没有想过失败之后会发生什么。
6. 详细设计报告 PDF 成稿:一致性自检与中文排版
内容写完只是半程,PDF 里图表编号错位、函数名和代码对不上、中文字体变成方框,都会让整份详细设计掉档。这一节讲几个能立刻用上的处理方式。
6.1 图表编号与交叉引用
图和表用"图 3-1 库存扣减流程"这样的章节内编号,正文里引用编号而不是"如下图"。这样中途插入一张图时只需局部重编,不会全文乱套。函数名、表名、字段名在正文和代码里必须完全一致,最省事的做法是把接口签名和建表 SQL 作为唯一来源,正文只引用不重写。
6.2 Markdown 导出中文 PDF 的可用命令
如果正文用 Markdown 写,导出时最容易踩的坑是中文缺字。用 pandoc 走 XeLaTeX 并指定中文字体,基本能一次成功。
# 导出中文 PDF:必须用 xelatex,并显式指定 CJK 字体 pandoc design.md \ -o 机票预订系统详细设计报告.pdf \ --pdf-engine=xelatex \ -V CJKmainfont="Noto Serif CJK SC" \ -V geometry:margin=2.5cm \ -V fontsize=11pt \ --toc --toc-depth=3 \ --number-sections几个参数的含义:--pdf-engine=xelatex是中文不出现方框的前提,pdflatex 处理 CJK 要额外配置;CJKmainfont换成系统里已安装的字体名,Linux 上可以先fc-list :lang=zh查一遍;--number-sections让章节号和正文的"图 3-1"能对上;--toc-depth=3保证目录细到三级标题。如果不用 pandoc,Word 里另存为 PDF 时记得在选项里勾选"创建书签时使用标题",否则目录点不动。
6.3 交付前的自检清单
| 检查项 | 合格标准 |
|---|---|
| 模块边界 | 每个用例只有一个主责模块,跨模块调用都有接口 |
| 接口签名 | 参数名、类型、默认值齐全,与代码一致 |
| 库表 | 有主键、唯一键、索引说明,字段类型明确 |
| 并发 | 库存扣减有明确方案,超卖如何避免写得出来 |
| 算法 | 有输入输出、复杂度、剪枝参数,能照着实现 |
| 异常 | 错误码表覆盖业务拒绝与系统故障两类 |
| 排版 | 图表编号连续,目录层级正确,中文无方框 |
导出命令建议固化成脚本,文档改一次就跑一次,比手动另存为可靠得多;脚本里加上字体检查那一行,换机器时能立刻发现问题出在字体而不是内容。
本文还有配套的精品资源,点击获取