ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

机票预订系统详细设计说明书:状态机、库存与接口的落地实践

2026/9/18 15:21:41 拓冰建站 浏览量
机票预订系统详细设计说明书:状态机、库存与接口的落地实践 简介机票预订系统详细设计说明书是一份面向软件开发学习者、课程设计及毕业设计人员的完整技术文档围绕JavaJSPMySQL技术栈对系统做了细化设计。文档覆盖查询预订系统和退订系统两大模块从程序结构、功能、性能、输入输出、算法、流程逻辑到接口、存储分配均有说明尤其细化了机票预订、取票通知、航班查询、打印机票等功能场景。资源仅含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_status或state_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压测时的观察指标有两个第一个是接口返回的成功和失败次数第二个是数据库最终剩余座位数。比如初始库存 1002000 个并发请求每个请求扣 1 个座位最终看到的成功数应该是恰好 100失败数 1900数据库余票为 0且全程没有任何时刻出现负值。如果你的压测结果显示成功数超过 100说明扣减 SQL 的WHERE条件没写对或者事务隔离级别出了问题。另一个值得验证的是支付超时后的库存释放设置极短的支付时限例如 30 秒发起订单后不支付等定时任务将订单置为已取消并释放库存然后查询该航班余票是否恢复。这个场景在详细设计说明书中描述得再多也不如实测一次来得让人放心。验证完后把压测结果和实际状态流转日志贴进交付说明里比写十页“本系统保证不会超卖”更有说服力。本文还有配套的精品资源点击获取