机票预订系统详细设计说明书:状态机、库存与接口的落地实践
2026/9/18 15:21:39 网站建设 项目流程

简介:机票预订系统详细设计说明书是一份面向软件开发学习者、课程设计及毕业设计人员的完整技术文档,围绕Java+JSP+MySQL技术栈,对系统做了细化设计。文档覆盖查询预订系统和退订系统两大模块,从程序结构、功能、性能、输入输出、算法、流程逻辑到接口、存储分配均有说明,尤其细化了机票预订、取票通知、航班查询、打印机票等功能场景。资源仅含1个PDF文件,压缩包大小约997KB,便于直接阅读与打印。文档结构清晰,包含引言、程序系统结构、查询预订系统设计说明、退订系统设计说明等章节,并配有IPO图及模块间关系图,适合在系统开发或文档撰写时对照参考。目前已有1378人浏览学习,对于需要完成机票预订系统设计说明书或了解传统Java Web项目详细设计规范的读者,具有较好的参考价值。

1. 机票预订系统先把详细设计说明书当代码来写

相当多团队把详细设计说明书当成上线前的文档任务,评审会开完就归档,没人再翻。但机票预订系统恰恰是那种“文档写不细,上线必出事”的业务:一个订单涉及航班、舱位、乘客、支付、出票、退改签,任何一环的状态没定义清楚,就会出现重复出票、库存超卖、退款对不上账。反直觉的结论是:详细设计说明书的价值不在“写得好”,而在“写得死”——把状态机、库存扣减策略、接口契约这些本应该在代码里反复争论的东西,提前用文字钉死。这篇博文适合后端、架构和测试同事,目标是让你拿到任何一份机票预订系统详细设计说明书,都能快速判断它是否可落地,并且自己也能写出同样水平的设计文档。全程不讲虚的,只讲模块怎么切、状态怎么转、库存怎么扣、接口怎么定。

2. 机票预订系统详细设计说明书里的模块划分与分层架构

2.1 按业务域拆模块,而不是按技术层拆

拿到一份详细设计说明书,我一般先看目录结构。最常见的错误是把系统拆成“控制层、服务层、数据层”这种技术分层,结果每个服务层里塞满了跨业务的耦合代码。机票预订系统的正确拆法,是围绕业务域划分:订单域、航班域、支付域、库存域、乘客域。每个域内部再谈自己的服务与数据访问,域之间只通过接口通信。这个划分原则,直接决定了后续数据库表的归属和团队协作的边界。

com.airline.booking ├── order // 订单域 │ ├── controller │ ├── service │ ├── repository │ └── model ├── flight // 航班域 │ ├── controller │ ├── service │ ├── repository │ └── model ├── inventory // 库存域,独立成域而不是塞进 flight │ ├── service │ └── repository ├── payment // 支付域 │ ├── service │ └── repository └── passenger // 乘客域 ├── service └── repository

这个包结构里值得注意的一个决策是:库存单独拆域。很多初版设计说明书会把可用座位数直接放在航班表里,扣减逻辑写在订单服务中。但机票库存与航班基础信息的变化频率完全不同——航班信息一天改不了几次,库存则是每秒都可能被并发扣减。混在一起,缓存策略和锁粒度都很难定。独立成域后,库存服务的接口可以只暴露“查询可用数”和“尝试扣减”两个方法,内部实现怎么加锁、怎么回滚,对订单域透明。

详细设计说明书在这一章应该回答的问题包括:每个域对外提供哪些服务、依赖哪些其他域、数据是否独立存储。如果文档里出现“订单服务直接修改航班表的余票字段”这种描述,基本可以判定设计没过脑子。

2.2 分层架构在说明书里怎么表达

模块划分定的是“有哪些房间”,分层架构定的是“每个房间里怎么摆放”。对机票预订系统,我推荐四层:接入层、应用层、领域层、基础设施层。接入层管参数校验和会话,应用层管用例编排(创建订单、发起支付),领域层管业务规则(订单状态流转、库存扣减策略),基础设施层管数据库、消息队列、缓存的实际访问。

层次职责关键类示例依赖方向
接入层参数校验、鉴权、协议转换OrderController向下调应用层
应用层用例编排、事务边界CreateOrderService向下调领域层
领域层业务规则、状态机OrderEntity, InventoryService只依赖自身
基础设施层DB、MQ、Cache 访问OrderRepository, FlightMapper被上层依赖

写详细设计说明书时,我建议用一张表把每个层次的职责和关键类列清楚,比画那种层层嵌套的架构图更实用。因为评审人真正关心的是:一个创建订单的请求,从 Controller 进来后经过哪些类、每个类做什么、事务在哪一层开启、失败时靠什么回滚。这些问题的答案,在表里可以直接检索到。

3. 订单与航班状态机:详细设计说明书要钉死的核心逻辑

3.1 订单状态机:没有状态表就没有可靠的售后流程

机票订单最怕的就是状态定义含糊。“已支付”之后能不能直接变“已出票”?“出票失败”和“已取消”是什么关系?如果这些自由状态没有约束,退改签功能基本做不了。详细设计说明书里应该有一张订单状态表,列出每个状态、允许的流入流出、触发动作,以及状态变更时的系统行为。以下是我在多个项目中验证过的状态定义,可以直接参考。

状态定义: - CREATED:订单已创建,未支付 - PENDING_PAYMENT:等待支付(创建后进入) - PAID:支付成功,等待出票 - TICKETING:出票中(调用航司接口) - ISSUED:出票成功 - TICKET_FAILED:出票失败(可重试) - CANCELLED:已取消(支付前可取消) - REFUNDING:退款中 - REFUNDED:已退款

这个状态集合的特点是:把“出票中”单独列为状态,而不是让“已支付”直接跳到“已出票”。因为机票出票通常依赖外部航司接口,耗时可能从几百毫秒到几十秒。如果没有中间状态,超时重试时订单状态会乱套。

3.1.1 状态机在代码里的落地形式

状态机的实现,我推荐用一个流转表驱动而不是散落的 if-else。以下是一个简化的例子:

class OrderState: TRANSITIONS = { "CREATED": ["PENDING_PAYMENT", "CANCELLED"], "PENDING_PAYMENT": ["PAID", "CANCELLED"], "PAID": ["TICKETING", "REFUNDING"], "TICKETING": ["ISSUED", "TICKET_FAILED", "REFUNDING"], "TICKET_FAILED": ["TICKETING", "CANCELLED", "REFUNDING"], "ISSUED": ["REFUNDING"], "REFUNDING": ["REFUNDED"], "REFUNDED": [], "CANCELLED": [], } @classmethod def can_transition(cls, current: str, target: str) -> bool: return target in cls.TRANSITIONS.get(current, []) @classmethod def transition(cls, current: str, target: str): if not cls.can_transition(current, target): raise ValueError(f"非法状态迁移: {current} -> {target}") return target

代码逻辑不复杂,但有一个细节值得展开:状态合法性的校验应该放在领域层,而不是数据库层。很多设计说明书会在数据库里加 CHECK 约束,但那个约束只能防住“完全不可能的值”,防不住“逻辑上不该发生的流转”,例如把“已出票”直接改成“已取消”。领域层校验的意义,是让违反状态机的操作在进入持久化之前就被拦截,同时抛出带语义的错误信息,方便前端展示和日志排查。这里的参数说明是:不要把状态字段命名成status,建议用order_statusstate_code,因为status太通用,在联表查询时容易产生歧义。

3.2 航班状态与订单状态的联动

机票预订系统里还有一套容易忽略的状态机:航班状态。航班可能处于计划、开放预订、关闭预订、延误、取消等状态。订单状态流转必须感知航班状态。比如航班取消时,所有已支付未出票的订单该自动触发退款流程,已经出票的订单要进入非自愿退票流程。详细设计说明书里要画清楚这两套状态机的联动关系。

航班状态对订单的影响系统自动动作
计划中无影响
开放预订允许创建订单
关闭预订禁止新建订单创建订单接口拒绝
已取消已支付订单需处理触发退款/非自愿改签
已延误不禁止订单,但需通知推送通知至乘客

这个表的重点不在“有哪些状态”,而在“状态变化时谁负责触发动作”。我见过的问题是把这些动作散落在各种定时任务里,结果是航班取消半小时后,订单还在“已支付”状态挂着,用户打电话来投诉才知道航班没了。更好的做法是让航班状态事件作为一条消息发到消息队列,订单服务订阅后统一处理。

4. 数据库设计与库存控制:详细设计说明书画死表结构

4.1 订单主表与子表怎么设计

机票订单和普通电商订单最大的区别在于:一个订单包含多个乘客,每个乘客对应一个航段,还可能包含往返两个航段。如果把乘客信息直接塞进订单表,查询和修改都会很痛苦。推荐的模型是订单主表、乘客表、航段表分离。以下是最小可用的建表语句:

CREATE TABLE booking_order ( id BIGINT PRIMARY KEY AUTO_INCREMENT, order_no VARCHAR(32) NOT NULL UNIQUE COMMENT '业务订单号', user_id BIGINT NOT NULL COMMENT '下单用户', order_status VARCHAR(20) NOT NULL COMMENT '订单状态,见状态机定义', total_amount DECIMAL(10,2) NOT NULL COMMENT '订单总金额', create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, update_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, KEY idx_user_id (user_id) ) COMMENT '订单主表'; CREATE TABLE order_passenger ( id BIGINT PRIMARY KEY AUTO_INCREMENT, order_id BIGINT NOT NULL COMMENT '订单ID', passenger_name VARCHAR(50) NOT NULL, id_type VARCHAR(10) NOT NULL COMMENT '证件类型', id_no VARCHAR(32) NOT NULL COMMENT '证件号码', KEY idx_order_id (order_id) ) COMMENT '订单乘客表'; CREATE TABLE order_segment ( id BIGINT PRIMARY KEY AUTO_INCREMENT, order_id BIGINT NOT NULL, flight_no VARCHAR(10) NOT NULL COMMENT '航班号', segment_no VARCHAR(10) NOT NULL COMMENT '航段编号,如去程/返程', depart_airport VARCHAR(10) NOT NULL COMMENT '出发机场三字码', arrive_airport VARCHAR(10) NOT NULL COMMENT '到达机场三字码', depart_time DATETIME NOT NULL, arrive_time DATETIME NOT NULL, KEY idx_order_id (order_id) ) COMMENT '订单航段表';

这三个表的设计有一个关键点:乘客表冗余了乘客的证件信息,而不是通过乘客 ID 关联统一乘客表。因为机票订单是法律凭证,下单那一刻的信息必须被固定下来,之后用户改名、证件号变更不应该影响历史订单。这个冗余是有意的,详细设计说明书里要专门注明字段的冗余原因,否则后续维护的人会“好心”把它改成外键关联。

订单号我建议单独设置业务订单号order_no,而不用数据库自增主键直接暴露给前端。自增 ID 很容易被遍历爬取,而且基于订单号的查询在分库分表时会作为分片键。订单号生成规则不必太复杂,日期+随机数+序列即可,但设计说明书里要写明唯一性保障方案。

4.2 机票库存扣减:别用先查后扣

机票库存是典型的“写多读多、一致性要求高”的数据。最危险的设计是先查余票数,判断大于零,再 UPDATE 减一。两个并发请求同时查到余票 1,然后各减一,最后余票变成 -1,超卖发生。详细设计说明书里必须把库存扣减方案写清楚。最稳妥、性能也可接受的做法是用条件更新的方式:

-- 原子扣减,只在余票足够时成功 UPDATE flight_inventory SET remaining_seats = remaining_seats - 1 WHERE flight_id = ? AND cabin_class = ? AND remaining_seats > 0; -- 检查受影响行数,等于1则扣减成功,等于0则库存不足

这段 SQL 的关键是WHERE中的remaining_seats > 0条件。数据库的行锁保证同一时刻只有一个事务能修改这一行,另一个事务会等待锁释放后发现自己条件不满足,从而更新行数为 0。应用层通过受影响行数判断是否扣减成功,不需要额外加分布式锁。

注意这里仍然有可以深挖的地方:如果一次订单要锁定多个座位,SQL 需要改为AND remaining_seats >= ?。但这样还是有一个窗口:两个订单同时请求 3 个座位和 1 个座位,剩余 2 个,两个请求都判断时会怎样?条件更新依然能解决——第一个更新把余票变成 -1 或 0,第二个更新条件不满足。前提是扣减必须加>= 需要数而不是> 0

4.3 库存扣减与服务端异常的一致性

很多详细设计说明书忽略了库存和订单的一致性。推荐的做法是:订单创建后先把订单状态置为 CREATED,再扣减库存,扣减成功才把订单变为待支付。如果扣减失败,订单直接取消。如果用户支付超时,则需要一个定时任务扫描超过支付时限的订单,释放库存。这里最容易踩坑的是事务边界。我一个常见的错误示范是:在同一个事务里先扣库存再创建订单,一旦创建订单失败,库存回滚;但如果是跨服务调用,分布式事务的引入会让系统复杂度陡增。我的建议是初期用“本地消息表+定时补偿”的最终一致性方案,而不是强一致分布式事务。详细设计说明书里写清“哪个步骤失败后由谁补偿”,比写清“用什么中间件”更有价值。

5. 接口定义与异常返回:详细设计说明书的可执行部分

5.1 创建订单接口的请求与响应定义

详细设计说明书如果只写了模块和数据库,是没有灵魂的。真正可执行的部分是接口定义。每个接口要明确写出请求参数、响应参数、异常场景、幂等策略。以创建订单接口为例:

// POST /api/v1/orders { "flightNo": "CA1831", "departDate": "2025-06-20", "cabinClass": "Y", "passengers": [ {"name": "张三", "idType": "0", "idNo": "110101199001011234"} ], "contactPhone": "13800138000", "idempotentKey": "uuid-xxxx-1234" }

响应体:

{ "code": 0, "message": "success", "data": { "orderNo": "20250615001", "orderStatus": "PENDING_PAYMENT", "expiresAt": "2025-06-15T12:30:00", "totalAmount": 890.00 } }

这个接口定义里有几个参数值得重点说。idempotentKey是幂等键,由客户端生成并随请求传入。因为机票创建订单可能因网络超时而重试,如果没有幂等键,一次下单操作被重复提交就可能生成两笔订单,占两段库存。服务端收到幂等键后去重,同一键只处理一次,后续重复请求直接返回第一次的结果。expiresAt是支付截止时间,它不只用于提示用户,更用于服务端定时释放库存的判定标准。

5.2 异常码设计要覆盖三类场景

机票预订系统的异常码不能像内部开发接口那样只定义 0 和 -1。我习惯把异常分为三类:参数错误、业务规则拒绝、系统异常。以下是异常码表的一部分:

错误码含义处理建议
10001参数校验失败前端修正后重试
20001航班不存在或已取消重新选择航班
20002库存不足提示用户换舱位或邻近日
20003订单状态不允许当前操作刷新订单状态
20004重复提交(幂等冲突)提供已创建订单信息
30001支付网关超时提示稍后查单

异常码设计的原则是:前端看到码后不需要后端介入就能给用户一个合理的反馈。像 20003,前端可以主动刷新订单状态,重新拉取最新状态后让用户再次发起操作。写接口定义时,每个接口都要列出它可能抛出的错误码,不能只写“失败返回 -1 和错误信息”。

5.3 支付回调接口的验签与处理

支付回调是机票预订系统中最容易出安全事故的接口。详细设计说明书里要明确:回调接口不校验用户身份,而是校验网关签名。签名验证失败直接丢弃请求且不返回成功。处理逻辑是:验证签名 -> 查订单 -> 判断订单当前状态是否允许变更为已支付 -> 更新状态 -> 返回给网关“SUCCESS”通知停止重推。这里每一步都要写清楚,尤其是判断订单状态这一步,否则会出现支付成功但订单已被取消的情况,钱收了订单没了。说明书中应该给出处理策略:核对金额一致时,订单自动变为 PAID 并进入出票流程。

6. 用并发压测验证详细设计说明书里的库存不超卖

详细设计说明书写得再完整,不验证就是一张纸。最后一章给你一个可以在本地快速验证库存扣减逻辑的压测方案。工具用 JMeter 或者简单的 Go/Java 并发脚本都可以,我推荐最直接的方式:写一段多线程代码同时调用库存扣减接口,观察最终剩余座位数是否为负数。

# 并发请求模拟,使用 hey 工具 hey -n 2000 -c 100 -m POST \ -H "Content-Type: application/json" \ -d '{"flightId":"CA1831","cabinClass":"Y","seats":1}' \ http://localhost:8080/api/v1/inventory/deduct

压测时的观察指标有两个:第一个是接口返回的成功和失败次数,第二个是数据库最终剩余座位数。比如初始库存 100,2000 个并发请求每个请求扣 1 个座位,最终看到的成功数应该是恰好 100,失败数 1900,数据库余票为 0,且全程没有任何时刻出现负值。如果你的压测结果显示成功数超过 100,说明扣减 SQL 的WHERE条件没写对,或者事务隔离级别出了问题。

另一个值得验证的是支付超时后的库存释放:设置极短的支付时限(例如 30 秒),发起订单后不支付,等定时任务将订单置为已取消并释放库存,然后查询该航班余票是否恢复。这个场景在详细设计说明书中描述得再多,也不如实测一次来得让人放心。验证完后,把压测结果和实际状态流转日志贴进交付说明里,比写十页“本系统保证不会超卖”更有说服力。

本文还有配套的精品资源,点击获取

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

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

立即咨询