简介面向Java初学者与课程设计人群的民航订票管理系统完整项目包覆盖航班信息查询、客户订票退票、航班信息与航线管理、航班延误处理、已订票客户及会员信息管理等核心业务模块配合Eclipse/IntelliJ IDEA、SQL Server、Java Swing和JDBC技术栈可用于毕业设计、课程实训或自学参考。压缩包共118个文件约2.29MB包含30个Java源文件、75个编译后的class文件、5个XML配置、2个SQL数据库脚本以及项目配置文件、说明文档等其中SQL脚本可直接初始化数据库Java源码与class文件便于对照学习或二次开发。目前已有1816人学习下载。通过源码、配置及设计文档读者可快速理解Swing界面与JDBC数据访问的分层实现思路掌握订票流程的状态管理、异常处理与多表查询技巧也可直接参考其项目结构改造为其他业务管理系统。总体而言这是一份结构清晰、易上手的中小型Java项目学习资料。1. “民航订票管理系统设计文档.zip”一个解压包里藏着完整的课程设计交付物这类压缩包在毕业设计和实训答辩里出现频率很高。它不是一套能直接上线的商用系统而是一份结构完整的教学交付物前半部分是需求分析、数据库设计、接口定义这些文档后半部分是能跑起来的订票核心代码。它的价值恰恰在于“设计先行”——先把数据模型和业务规则定清楚代码只是把这些规则翻译成实现。适合两类人一是要做课程设计或毕业设计的学生想找一个结构干净、能在答辩时讲明白的参照二是刚转行做业务系统开发的工程师想看看传统MIS类系统里数据建模、事务处理和状态机是怎么组织的。拿到这个包正确的打开顺序不是先解压找源码而是先把设计文档读完再让你的代码跟着文档走。2. 设计文档拆解先看数据模型再看业务流程别急着找代码“文档”这三个字才是这个压缩包的核心资产。很多同学拿到压缩包第一反应是解压后直接打开代码工程被一堆类名绕晕后半小时就放弃了。我的习惯是先把文档按类型归好类用文档建立全局视图再回去看代码就很轻松。2.1 文档套件里找这五类东西需求、数据字典、用例图、接口清单、部署说明常见的一套“设计文档.zip”里文档部分通常可以归成五类它们的用途各不相同文档模块涉及内容写代码时怎么用需求分析系统角色、功能用例、非功能需求确认要做哪些模块划清边界数据库设计ER图、表结构、数据字典建表SQL的第一手依据系统设计用例图、类图、时序图理解模块划分和调用关系接口清单每个功能的入参、出参、业务规则Service层和Controller层的实现参照部署说明数据库版本、JDK版本、启动步骤搭环境、跑演示的救命文档我的经验是先看需求分析搞清楚系统有哪几个角色。民航订票系统一般有三个角色管理员维护航班和舱位注册用户订票退票游客只能查航班。角色确定后功能模块就出来了航班管理、舱位管理、订单管理、乘客管理、用户管理。再看数据库设计里的数据字典字段级别的东西能让你判断这套文档是不是完整——数据字典写得越细后面写代码越省力。如果一份压缩包里只有代码没有需求文档和数据字典它的参考价值会大打折扣。反过来说只要数据字典足够详细哪怕代码写得粗糙你也能靠文档把整个设计的逻辑讲清楚答辩时反而更占优势。2.2 数据库设计是这个系统的心脏核心表结构与字段选型民航订票和普通商品订单最大的区别是“座位”和“舱位”。不要只建一张航班表加一个余票数字字段那是火车票思路放在航班场景里会漏掉“经济舱没了但公务舱还有”的真实业务。常见的设计是四张核心表配合一张子表表名核心字段设计要点t_user 用户表id, username, password_hash, phone, id_card密码存哈希不存明文t_flight 航班表id, flight_no, departure_city, arrival_city, departure_time, arrival_time, base_price静态排班与实时余票分离t_cabin 舱位表id, flight_id, cabin_code, price_rate, total_seats, remaining_seats余票放在舱位维度t_order 订单表id, user_id, flight_id, cabin_code, ticket_count, total_price, status, idempotent_key状态字段必须有明确枚举定义t_order_passenger 乘客表id, order_id, passenger_name, id_card订单与乘客是一对多这个设计的核心决策是“余票放舱位不放航班”。一个航班有经济舱、公务舱、头等舱每种舱位的票价系数和余票数量都不一样。订票时锁的是某个舱位的remaining_seats而不是整个航班的可用座位数。舱位表通过flight_id关联航班再把price_rate作为相对基准价的折扣系数算票价时用“基准价乘折扣系数”而不是在订单表里硬编码价格。字段选型上有几个细节值得注意。金额一律用DECIMAL(10,2)不要用FLOAT或DOUBLE否则累计金额会出现精度误差。时间字段用DATETIME不要为了省事存字符串因为排序和区间查询都会变麻烦。状态字段用TINYINT存数字枚举同时必须在数据字典里写明每个数字代表什么比如0待支付、1已出票、2已退票、3已改签。答辩时老师大概率会问“订单状态有哪些”数据字典里写清楚就是白送分。2.3 业务规则如何落到设计文档舱位、余票与价格策略数据表定下来后文档阶段真正体现功力的是业务规则描述。很多翻车案例都是因为文档只贴了ER图规则全靠读者在代码里猜。民航订票有几条规则必须白纸黑字写清楚。价格计算规则成交价航班基准价×舱位折扣系数。Y舱经济舱全价系数1.0折扣舱位可能0.5到0.9C舱公务舱1.5F舱头等舱2.0。这个规则不写清楚数据库里的price_rate字段就失去了存在意义代码里就会出现一堆魔法数字。余票扣减规则下单成功立即扣减对应舱位的remaining_seats退票成功后返还。这里要做一个设计决策要不要引入“座位锁定”状态真实民航系统里用户选座后不一定立即支付需要锁座并超时释放。课程设计一般简化成“下单即扣减、退票即返还”但简化方案必须在文档里明确写出来否则开发过程中很容易把两种方案混在一起越改越乱。订单状态机待支付→已出票→已退票或已改签。改签的本质是“先退原票、再订新票”在系统里表达为两张订单原订单状态改为已退票新订单走正常订票流程。把改签拆成两个动作代码逻辑会清晰很多状态机也不会出现复杂分支。我在文档阶段会把状态流转画成表格每个状态的进入条件和退出动作都标注清楚这块做完写代码时基本不需要停下来问业务逻辑。3. 从文档到可运行代码民航订票核心模块的最小实现文档读完数据库建好接下来就是把设计翻译成代码。这一章用Spring Boot加MyBatis-Plus加MySQL的组合把建表、航班查询、订票三个核心环节的最小实现跑通。技术栈可以换成你熟悉的任意组合但事务、锁和幂等这三个设计要点是通用的。3.1 建库建表SQL把数据字典变成第一份能跑的文件先把数据字典转成建表SQL。下面这份SQL省略了用户表的部分索引保留了最核心的四张表。CREATE DATABASE IF NOT EXISTS airticket DEFAULT CHARSET utf8mb4; USE airticket; -- 航班表静态排班信息不存余票 CREATE TABLE t_flight ( id BIGINT NOT NULL AUTO_INCREMENT COMMENT 航班主键, flight_no VARCHAR(10) NOT NULL COMMENT 航班号例如 CA1234, departure_city VARCHAR(64) NOT NULL COMMENT 出发城市, arrival_city VARCHAR(64) NOT NULL COMMENT 到达城市, departure_time DATETIME NOT NULL COMMENT 起飞时间, arrival_time DATETIME NOT NULL COMMENT 到达时间, plane_type VARCHAR(32) DEFAULT NULL COMMENT 机型, base_price DECIMAL(10,2) NOT NULL COMMENT 经济舱全价基准价, PRIMARY KEY (id), KEY idx_departure (departure_city, departure_time) ) ENGINEInnoDB COMMENT航班表; -- 舱位表余票放到舱位维度支持不同舱位不同折扣 CREATE TABLE t_cabin ( id BIGINT NOT NULL AUTO_INCREMENT, flight_id BIGINT NOT NULL COMMENT 关联航班ID, cabin_code VARCHAR(8) NOT NULL COMMENT 舱位代码Y/C/F, cabin_name VARCHAR(16) NOT NULL COMMENT 经济舱/公务舱/头等舱, price_rate DECIMAL(4,2) NOT NULL COMMENT 价格系数相对基准价, total_seats INT NOT NULL COMMENT 总座位数, remaining_seats INT NOT NULL COMMENT 剩余座位数, PRIMARY KEY (id), UNIQUE KEY uk_flight_cabin (flight_id, cabin_code) ) ENGINEInnoDB COMMENT舱位余票表; -- 订单表幂等键防止重复下单 CREATE TABLE t_order ( id BIGINT NOT NULL AUTO_INCREMENT, user_id BIGINT NOT NULL COMMENT 下单用户ID, flight_id BIGINT NOT NULL COMMENT 航班ID, cabin_code VARCHAR(8) NOT NULL COMMENT 舱位代码, ticket_count INT NOT NULL COMMENT 票数, total_price DECIMAL(10,2) NOT NULL COMMENT 订单总价, status TINYINT NOT NULL DEFAULT 0 COMMENT 0待支付 1已出票 2已退票 3已改签, idempotent_key VARCHAR(128) NOT NULL COMMENT 幂等键用户航班日期, create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), UNIQUE KEY uk_idempotent (idempotent_key) ) ENGINEInnoDB COMMENT订单表; -- 乘客表一个订单可以对应多个乘机人 CREATE TABLE t_order_passenger ( id BIGINT NOT NULL AUTO_INCREMENT, order_id BIGINT NOT NULL COMMENT 订单ID, passenger_name VARCHAR(32) NOT NULL COMMENT 乘机人姓名, id_card VARCHAR(18) NOT NULL COMMENT 身份证号, PRIMARY KEY (id), KEY idx_order (order_id) ) ENGINEInnoDB COMMENT订单乘客表;这段SQL里有两个参数值得说明。t_cabin表的联合国唯一索引uk_flight_cabin保证了同一个航班下不会出现两条相同舱位代码的记录这是数据完整性的第一道防线。t_order表的uk_idempotent是幂等设计的载体它要求idempotent_key全局唯一重复插入会被MySQL直接拒绝这比在Java代码里先查再插要可靠得多——并发场景下查和插之间永远有缝隙。如果你在Windows 10上是用zip压缩包方式装的MySQL 8.0导入这份SQL前记得先确认服务已经启动导入时直接执行source命令或复制到Navicat里运行都可以。MySQL 8.0的默认字符集已经是utf8mb4建库语句里的DEFAULT CHARSET utf8mb4是为了兼容旧版本习惯顺手写上有益无害。3.2 航班查询与价格计算Service层的实现逻辑航班查询是最高频的接口也是演示时第一个被点开的页面。实现逻辑不算复杂但要注意查询条件里“日期范围”的处理。用户输入的是一个日期航班表里存的是DATETIME直接用等于匹配会查不到数据正确做法是把查询范围卡在当天的零点到次日的零点之间。public ListFlightVO queryFlights(String departureCity, String arrivalCity, LocalDate date) { // 把用户选择的日期转换成当天起止时间否则 DATETIME 字段匹配不上 LocalDateTime start date.atStartOfDay(); LocalDateTime end date.plusDays(1).atStartOfDay(); LambdaQueryWrapperFlight wrapper new LambdaQueryWrapper(); wrapper.eq(Flight::getDepartureCity, departureCity) .eq(Flight::getArrivalCity, arrivalCity) .between(Flight::getDepartureTime, start, end) .orderByAsc(Flight::getDepartureTime); ListFlight flights flightMapper.selectList(wrapper); // 一个航班对应多个舱位把舱位信息挂到航班VO上一起返回 ListFlightVO result new ArrayList(); for (Flight flight : flights) { ListCabin cabins cabinMapper.selectList( new LambdaQueryWrapperCabin() .eq(Cabin::getFlightId, flight.getId()) .orderByAsc(Cabin::getPriceRate)); result.add(FlightVO.of(flight, cabins)); } return result; }这里的核心处理是between条件把用户选择的日期卡成[start, end)区间避免当天0点之后的航班被误伤。为什么不在SQL里用DATE(departure_time)来匹配因为对departure_time字段套DATE函数会让索引失效航班多了以后查询会明显变慢。我在生产环境里踩过这个坑所以习惯在代码层算好时间边界再传参。舱位信息单独查询而不是用JOIN一次带出是因为航班和舱位是一对多关系JOIN会把航班信息在每个舱位行上重复一遍组装VO时反而要处理去重。先查航班、再按航班ID查舱位两次简单查询的代价在数据量不大时完全可接受代码却清晰很多。价格计算放在VO组装时做totalPrice basePrice乘priceRate再乘ticketCount不在SQL里算因为SQL里算完还要用BigDecimal接收不如Java侧清晰。3.3 订票接口事务、锁与幂等性的最小实现订票是整个系统最容易出问题的接口也是答辩时老师最爱追问的地方。三个关键词幂等、锁、事务。幂等解决重复提交锁解决并发超卖事务保证扣余票和建订单要么都成功要么都失败。Transactional(rollbackFor Exception.class) public Long createOrder(CreateOrderCmd cmd) { // 幂等键同一用户同一航班同一天只允许下一单 String idempotentKey cmd.getUserId() : cmd.getFlightId() : LocalDate.now(); Order existOrder orderMapper.selectOne( new LambdaQueryWrapperOrder().eq(Order::getIdempotentKey, idempotentKey)); if (existOrder ! null) { throw new BizException(请勿重复提交订单可去订单列表查看); } // 行级锁锁定舱位余票阻止并发下两个请求同时读到同一个剩余数 Cabin cabin cabinMapper.selectForUpdate(cmd.getFlightId(), cmd.getCabinCode()); if (cabin null || cabin.getRemainingSeats() cmd.getTicketCount()) { throw new BizException(余票不足); } // 扣减余票并创建订单两步在同一事务内 cabin.setRemainingSeats(cabin.getRemainingSeats() - cmd.getTicketCount()); cabinMapper.updateById(cabin); Order order new Order(); order.setUserId(cmd.getUserId()); order.setFlightId(cmd.getFlightId()); order.setCabinCode(cmd.getCabinCode()); order.setTicketCount(cmd.getTicketCount()); order.setTotalPrice(calcTotalPrice(cabin, cmd.getTicketCount())); order.setStatus(OrderStatus.WAIT_PAY.getCode()); order.setIdempotentKey(idempotentKey); orderMapper.insert(order); return order.getId(); }selectForUpdate是自定义SQL对应语句是SELECT * FROM t_cabin WHERE flight_id ? AND cabin_code ? FOR UPDATE。FOR UPDATE是MySQL InnoDB的行级锁事务提交前其他事务对这条舱位记录的更新操作都会阻塞。这是解决超卖的最后一层防线光靠Java代码里的if判断余票够不够是不够的——两个请求同时查到的余票都是2同时通过检查同时扣减最后余票变成-1。加了行锁后第二个请求必须等第一个请求的事务提交才能读到该行读到的remaining_seats已经是扣减后的值。Transactional注解上的rollbackFor Exception.class参数很多人会漏掉。Spring默认只在运行时异常时回滚如果抛出的是受检异常事务不会回滚余票扣了但订单没建成的情况就会出现。业务方法里自定义的BizException通常继承RuntimeException所以显式声明rollbackFor保险这也是API幂等性设计里容易被忽略的细节。幂等键的设计用的是“用户航班日期”拼接字符串配合t_order表上的唯一索引。这里有个细节要注意唯一索引比先查再插更可靠。代码里的selectOne查一下只能挡正常重复请求两个并发请求同时走到selectOne时都查不到记录然后都去insert唯一索引会拦住后插入的那条数据库报DuplicateKeyException事务回滚余票恢复。所以降级方案是捕获DuplicateKeyException后查一下已有订单直接返回把这个分支补上就万无一失。4. 避坑指南解压、建模与事务这三个环节的翻车记录这个压缩包从拿到手到跑起来踩坑点集中在三个环节压缩包本身能不能顺利解开、数据库建模有没有留足扩展空间、并发事务有没有处理干净。下面五条都是实际遇到过的案例按“现象→原因→解决”拆开写。4.1 解压报“could not find eocd”文件不完整或格式异常现象解压到一半工具弹出错误提示“invalid zip archive: could not find eocd”整个压缩包无法打开。原因EOCDEnd of Central Directory是zip格式的中央目录结尾标记所有zip文件都必须以它收尾。报这个错基本是两种情况文件下载不完整传输过程中被截断或者压缩包本身是由某些在线压缩工具生成的非标准格式中央目录记录位置不对。解决先看压缩包文件大小和来源页面标注的大小比对差太多就直接删掉重新下载。如果大小一致还是报错换7-Zip打开试试它对损坏zip的容错比系统自带解压器好一些。7-Zip能打开的话马上执行“文件→修复压缩包”把修复结果保存到新文件。这类问题在Windows和macOS上都会遇到和操作系统无关纯粹是包本身的问题。4.2 ZIP伪加密文档提示要密码其实是标记位被改动现象压缩包能正常解出文件列表但真正解压时提示“需要密码”而你确认来源渠道里从没提过加密或者文件解出来之后打开Word提示内容已损坏。原因这是ZIP伪加密。zip文件格式里有一个General Purpose Bit Flag字段第0位是加密标记。有些压缩工具或传输过程会把这一位置为1但实际文件内容并没有被加密算法处理过。解压器看到加密位就要求输密码形成“看起来有密码、其实没密码”的假象。解决用ZipCenOp这类小工具扫描压缩包并执行修复把伪加密标记位清除修复后重新解压即可。这类工具的用法一般是命令行指定jar包路径和文件名修复过程几秒钟。要注意伪加密修复针对的是“加密标记被改动”的情况如果文件真的被强密码加密过这个方法无效。区分方法是看解压器提示输入密码时文件列表里有没有正常显示文件名——伪加密通常会先展示文件列表再要密码。4.3 订单表没留乘客维度多人同行只能写一个名字现象系统演示到“为多人订票”时发现只能传一个乘客名字一张订单无法表达三个乘机人页面上的乘客列表写死了一个输入框。原因建表时图省事把passenger_name和id_card直接做成了t_order的两个字段认为一个订单对应一个乘客就够了。这个设计在单人订票时会一直正常直到产品提出“一个订单可以同时为家人订票”才暴露。解决拆出t_order_passenger子表让订单和乘客变成一对多关系。改造的代价不只是建一张新表还要把原订单表里的passenger_name、id_card字段迁走改保存订单的Service代码前端订票页面从单个乘客输入改成动态列表。好的做法是建模阶段就想清楚“订单”和“乘机人”是两个独立实体不要因为它们看起来“总是一起出现”就合并到一张表。对称的坑还有“联系人和乘客不是同一个人”的场景和订单绑一起同样会返工。4.4 余票扣减和订单创建不在一个事务并发下超卖现象用两个浏览器同时登录不同账号订同一航班的最后一张余票两个请求都提示订票成功但舱位表中余票变成了-1。原因业务方法没加事务或者加了事务但扣余票和插订单之间隔着其他耗时操作。两个请求先后读到余票为1都通过了“余票是否充足”的检查第一个请求扣成0还没提交第二个请求已经把0扣成了-1。解决把扣减余票和插入订单放进同一个事务方法用Transactional保证原子性并发控制靠SELECT FOR UPDATE行锁让后到的请求等前一个事务提交后再读余票。另一个补救手段是扣减余票时用条件更新SQL写成UPDATE t_cabin SET remaining_seats remaining_seats - 1 WHERE id ? AND remaining_seats 0影响行数为0说明扣减失败直接抛异常。这个写法即使没有行锁也能挡住超卖两条防线都加上才算稳妥。4.5 文档说五种状态代码里只有三种评审时最尴尬现象文档里的订单状态枚举写了5个状态代码里的枚举类和数据库字段注释只有3个答辩或项目评审时被老师发现这个不一致整个设计的可信度都受影响。原因开发过程中为了省事把“已取消”和“已关闭”合并成了一个状态或者加了“待出行”这个新状态但忘了同步文档。这类不一致是“设计文档.zip”项目最常见的病因为文档和代码由不同的人维护或者同一个人不同时间维护没有人专门做对照检查。解决把数据字典当成代码评审的检查单。每次改状态、加字段、调长度第一件事是打开文档同步修改。演示前专门花十分钟做一次逐字段对照用文档里的表结构去核对实体类、建表SQL和状态枚举发现不一致当场决定“以文档为准”还是“以代码为准”并立刻补齐另一侧。宁可文档先写错再被改对也不要让文档和代码各说各话。5. 验证与进阶让这套订票系统在演示时不出丑系统跑通只是起点演示和答辩才是检验设计成色的地方。这一章讲两个实用技巧验收时怎么核对设计与实现以及怎么用策略模式给退改签机制升级。5.1 验收核对把数据字典当检查单用我的习惯是打印一份数据字典然后逐表核对实体字段。重点盯三类字段状态字段有没有遗漏枚举值金额字段精度是不是DECIMAL(10,2)关联字段的类型是不是和主表主键一致。曾经遇到过t_order_passenger.order_id是INT而t_order.id是BIGINT的情况平时单表查询没问题一旦联表查询就报类型不匹配。这类问题不需要运行项目就能发现纯靠眼睛扫一遍字段类型对照表。5.2 进阶改装用策略模式处理退改签规则退改签是民航系统里规则最杂的模块经济舱、折扣舱、公务舱的退款比例完全不同。把规则堆在一个方法里的写法前期很快规则多了以后每次改动都提心吊胆。常见做法是定义RefundStrategy接口每种舱位实现一个计算退款的策略类需要新规则时只增加实现类不碰旧代码。这也是“设计模式”在这类系统里最自然的落点比硬套23种设计模式有意义得多。改完记得把策略类配置写进文档这一步经常被忽略。最后说一句我的血泪教训第一次做这类系统时我代码全写完了才发现文档里的状态枚举和代码对不上答辩当场被问了一个订单当前处于什么状态都答不利索。后来养成了先核对数据字典再动手、写完代码再同步文档的习惯再没在这个问题上翻过车。希望帮到你。本文还有配套的精品资源点击获取