SSM框架下航空机票预订系统开发:从建表到并发扣减实践 📅 发布时间:2026/9/14 12:14:18 👁 浏览次数: 简介一套基于SSM框架Spring、SpringMVC、MyBatis的航空机票预订系统毕业设计资源面向计算机相关专业学生及需要快速搭建同类项目的开发者。系统覆盖管理员、会员、航班、订单、公告、留言等核心管理模块具备完整的增删改查与权限控制逻辑适合用于毕业设计、课程设计或项目实战参考。压缩包内共1817个文件包含Java源码、JSP页面、XML配置、SQL数据库脚本以及jar依赖包等另有PPT和文档资料整体约58.7MB。资源包结构清晰从源码到数据库再到演示文档一应俱全可帮助读者理解SSM整合流程、业务分层设计与数据表关系也可直接导入开发工具进行二次学习与扩展。目前已有36人学习对于需要完整毕业设计案例的读者而言是一份实用且可直接借鉴的参考资料。1. 基于SSM的航空机票预订系统源码包背后真正要解决的问题这个标题在毕设源码交易里出现频率不低但大多数下载者真正缺的不是源码而是“把源码跑起来并讲清楚”的方法。SSMSpringSpringMVCMyBatis组合解决的是分层职责Spring管对象和事务SpringMVC管路由MyBatis管SQL。航空机票预订系统的业务虽然不大却包含了用户、航班、订单三张核心表以及余票扣减、多条件查询、订单状态流转这类典型问题。我会按自己的落地顺序讲先定表结构再跑通预订主流程然后处理并发扣减最后说数据库脚本和答辩演示怎么配合源码。这篇内容适合正在做毕设、以及想用一套经典SSM骨架改造成其他业务场景的读者。2. SSM分层下航空机票预订系统的数据模型与建表细节2.1 用户、航班、订单三个业务域如何拆分航空机票预订系统不要把表设计成一张大表。常见做法是拆成三个业务域用户域负责账号与登录态航班域负责航班信息和余票订单域负责预订行为和支付状态。用户域需要用户ID、用户名、密码、手机号航班域需要航班号、出发城市、到达城市、起飞时间、价格、座位总数、余票数订单域需要订单号、用户ID、航班ID、订购数量、下单时间、状态。状态字段通常用int或tinyint表示0待支付、1已支付、2已出票、3已取消、4已退款。用int的优势是前端和接口都容易判断调试时直接看数字。状态码含义说明0待支付下单成功但未支付超时后自动取消1已支付用户支付成功等待出票2已出票票号生成行程可查3已取消用户主动取消或超时取消4已退款已支付订单取消后退款这种拆分的好处是每个模块的增删改查都能独立测试。比如航班管理页面只更新航班表结算功能才写订单表。实际开发中我一般会在订单表里冗余一份航班快照字段把航班号、出发到达城市、时间、价格都复制进订单表。原因是航班信息后续可能改航季订单作为历史凭证不能跟着变。这点在数据库课程设计或毕设答辩里能体现出你对数据一致性的理解。2.2 三张核心表的DDL与索引设计下面是最小可运行的建表语句适合SSM项目初始化。我故意把订单号用业务号而不是自增ID因为机票订单需要短、可读、能作为对外编号。CREATE TABLE user ( id int(11) NOT NULL AUTO_INCREMENT, username varchar(50) NOT NULL, password varchar(64) NOT NULL, phone varchar(20) DEFAULT NULL, create_time datetime DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), UNIQUE KEY uk_username (username) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; CREATE TABLE flight ( id int(11) NOT NULL AUTO_INCREMENT, flight_no varchar(20) NOT NULL, departure varchar(50) NOT NULL, arrival varchar(50) NOT NULL, departure_time datetime NOT NULL, arrival_time datetime NOT NULL, price decimal(10,2) NOT NULL, total_seats int(11) NOT NULL, remaining_seats int(11) NOT NULL, PRIMARY KEY (id), KEY idx_departure_arrival_time (departure,arrival,departure_time) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; CREATE TABLE orders ( id int(11) NOT NULL AUTO_INCREMENT, order_no varchar(32) NOT NULL, user_id int(11) NOT NULL, flight_id int(11) NOT NULL, seat_count int(11) NOT NULL DEFAULT 1, order_status tinyint(4) NOT NULL DEFAULT 0, total_amount decimal(10,2) NOT NULL, create_time datetime DEFAULT CURRENT_TIMESTAMP, pay_time datetime DEFAULT NULL, PRIMARY KEY (id), UNIQUE KEY uk_order_no (order_no), KEY idx_user_status (user_id,order_status) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;建表逻辑说明用户表用户名唯一避免两个相同账号航班表把出发、到达、起飞时间做成联合索引因为机票查询页最常见的搜索条件是“从某城市到某城市按时间排序”订单表用user_id和order_status联合索引支撑“我的订单”列表按状态过滤。这里需要注意MySQL 5.7以下版本对datetime字段用DEFAULT CURRENT_TIMESTAMP支持不完整如果项目用的是5.6把create_time改成在应用层插入即可。2.3 余票字段与数据库约束的取舍余票数remaining_seats是一个可被修改的累计值。有人把它设计成不落库、每次由总座位数减已售数算出来这在机票系统里不现实因为写入和查询并发高实时计算会加重数据库压力。常见做法是落库并配合行锁或条件更新保证不超卖。表设计阶段应确定余票值允许为零但不能为负这个约束可以在MyBatis更新SQL里通过remaining_seats 0条件实现也可以在数据库层加CHECK (remaining_seats 0)。由于MySQL 5.7部分版本对CHECK约束不生效更好的做法是在更新语句里带条件我在第四章会写具体写法。字段类型上金额用decimal(10,2)不要用float时间是datetime不要用字符串。字符串时间在排序和区间查询上很容易出错比如“2025-05-01 08:00”和“2025-05-01 8:00”在字典序上不同但数据库datetime会做正常比较。这里也顺便解释一个数据库面试常问的点为什么InnoDB要选B树索引因为航班查询的高频是等值匹配加范围排序B树叶子节点有序且带双向链表范围扫描不需要回表。这个点可以在论文里扩展。3. 用SpringSpringMVCMyBatis把机票预订主流程跑通3.1 Maven依赖与项目骨架选择拿到源码包后第一步不是打开Tomcat而是先看pom.xml。SSM项目的依赖很容易版本冲突常见的坑是Spring与JDK版本不匹配Spring 4.x配上JDK 8没问题但如果你用JDK 11直接跑CGLIB代理会报“Unsupported class file major version”。我一般会固定用Spring 5.1.x JDK 8Tomcat 8.5这组组合经过大量项目验证稳定且和多数教材对得上。pom.xml关键依赖如下dependency groupIdorg.springframework/groupId artifactIdspring-context/artifactId version5.1.20.RELEASE/version /dependency dependency groupIdorg.springframework/groupId artifactIdspring-jdbc/artifactId version5.1.20.RELEASE/version /dependency dependency groupIdorg.springframework/groupId artifactIdspring-webmvc/artifactId version5.1.20.RELEASE/version /dependency dependency groupIdorg.mybatis/groupId artifactIdmybatis/artifactId version3.5.6/version /dependency dependency groupIdorg.mybatis/groupId artifactIdmybatis-spring/artifactId version2.0.6/version /dependency dependency groupIdmysql/groupId artifactIdmysql-connector-java/artifactId version5.1.49/version /dependency我给的是单模块SSM项目的最小依赖。注意mybatis-spring版本必须和MyBatis主版本匹配老项目里经常出现mybatis 3.2配mybatis-spring 1.2的情况虽然能跑但很多动态代理的坑。数据源用Druid还是HikariCPSSM项目里Druid更常见因为它自带监控页面答辩时能展示SQL执行情况。数据源配置里有一个经常被忽略的参数maxWait如果不设置数据库连接池占满后线程会无限等待前端表现为页面卡死。这个参数我一般设5000毫秒。3.2 机票多条件查询的MyBatis动态SQL机票查询是预订入口。用户可能只输入出发城市也可能加上到达城市和日期因此SQL必须动态拼接。MyBatis的where标签可以自动去掉第一个多余的AND这是手写JDBC时代最容易出错的地方。下面给出FlightMapper.xml中的核心查询select idsearchFlights resultTypecom.example.entity.Flight SELECT id, flight_no, departure, arrival, departure_time, arrival_time, price, total_seats, remaining_seats FROM flight where if testdeparture ! null and departure ! AND departure LIKE CONCAT(%, #{departure}, %) /if if testarrival ! null and arrival ! AND arrival LIKE CONCAT(%, #{arrival}, %) /if if testdepartureTime ! null AND DATE(departure_time) DATE(#{departureTime}) /if /where ORDER BY departure_time ASC /select这段SQL里LIKE拼接用了CONCAT而不是直接在模板里写%${departure}%因为$符号会做字符串替换可能引发SQL注入。很多网上源码包为了省事直接用${}在答辩时通常会被老师追问。动态SQL的判断条件要注意空字符串用户清空输入框时前端可能提交成空串所以每个if都要做null和双重判断。DATE()函数会导致departure_time上的索引失效如果数据量大更好的方案是传日期区间departure_time #{start} AND departure_time #{end}。这个优化点值得写进论文。3.3 订单事务边界放在Service层的关键参数预订动作分两步扣减余票、插入订单。两步必须在一个事务里否则可能出现“订单创建成功但余票没扣”的数据不一致。SSM事务声明式配置放在ServiceController只负责参数封装和视图转发。事务管理器配置如下bean idtxManager classorg.springframework.jdbc.datasource.DataSourceTransactionManager property namedataSource refdataSource/ /bean tx:advice idtxAdvice transaction-managertxManager tx:attributes tx:method namecreate* propagationREQUIRED rollback-forException/ tx:method nameupdate* propagationREQUIRED/ tx:method namequery* propagationSUPPORTS read-onlytrue/ /tx:attributes /tx:advice重点是rollback-forException如果不写Spring默认只在RuntimeException上回滚像IOException这种受检异常抛出来时事务不会回滚余票就扣错了。create和update方法强制加入事务query*方法用SUPPORTS保证读操作在无事务环境中执行减少锁占用。这里也回答一个数据库面试题事务传播行为REQUIRED和REQUIRES_NEW有什么区别REQUIRED是外层有事务就复用REQUIRES_NEW是挂起当前事务开启新事务。订单系统里没有特殊情况不建议用REQUIRES_NEW因为两个事务各自提交回滚不同步。传播行为行为说明机票系统的建议REQUIRED有事务则加入当前事务预订主流程使用REQUIRES_NEW挂起当前事务开启新事务支付回调日志可考虑核心数据不要用NESTED保存点回滚子故障可单独回滚适合附属信息写入但要注意MySQL锁持有时间4. 机票余票扣减与订单一致性并发场景的四个必调点4.1 用条件更新代替先查后扣很多SSM教程里的订票写的是先select余票然后在Java里判断ifremaining 0再update。这个做法单机测试没问题一压测就超卖。原因是在select和update之间另一个线程可能已经扣了余票你手里的remaining是过期数据。常见做法是直接把判断放进update语句用数据库的行锁保证只有一个线程修改同一行。UPDATE flight SET remaining_seats remaining_seats - 1 WHERE id #{flightId} AND remaining_seats 0这条SQL执行后返回影响的记录数MyBatis里update方法返回int。如果int等于0说明这一趟航班已经没有余票Service层应当抛出“余票不足”异常并回滚事务。注意这里没有用乐观锁的version字段因为票量本身就是天然的版本号条件remaining_seats 0就是校验。如果一次预订多张票SQL改成remaining_seats - #{seatCount} 0即remaining_seats #{seatCount}。这个写法的边界在哪它依赖InnoDB行锁所以查询条件必须命中索引。比如这里WHERE id#{flightId}走主键索引锁的是这一行如果你用flight_no去更新需要确保flight_no有唯一索引。如果字段没有索引InnoDB会走全表扫描并锁上所有扫描过的行并发性能直接降级成表锁。4.2 订单状态机与超时未支付处理订单状态是典型状态机。0待支付、1已支付、2已出票、3已取消、4已退款。更新状态必须带前置条件UPDATE orders SET order_status 1, pay_time NOW() WHERE order_no #{orderNo} AND user_id #{userId} AND order_status 0这样防止用户重复点击支付按钮第二次点击返回0行就不会把已支付订单再改一遍。超时未支付通常有两种处理最常见的是支付回调触发检查另一个是后台定时任务扫描。定时任务用Spring的Scheduled比较轻量扫描的条件是order_status0 and create_time NOW() - INTERVAL 15 MINUTE找到后先把订单置为已取消再恢复航班余票。这里要注意恢复余票和取消订单也需要在一个事务里否则余票数量会漂移。更稳的是在订单表加一个version字段取消时用乐观锁更新成功才恢复余票。如果项目规模大可以引入延迟队列但在SSM毕业设计中用定时任务更直观也容易在答辩时说明白。4.3 重复提交、连接池与连接泄漏重复提交是机票系统的高发问题。同一个用户快速点两次“确认预订”可能产生两个订单。前端按钮置灰是一种做法但接口层也要防重。常见做法是在创建订单前查一下同一用户、同一航班、同一日期且status0的订单是否存在存在就直接返回已有订单号。这个查询要利用我们在第二章设计的联合索引idx_user_status不要全表扫。连接池方面Druid监控里能直接看到active count和wait count。我一般会设置initialSize5, minIdle5, maxActive50。还需要打开testWhileIdletrue否则数据库重启一段时间后连接池里的旧连接会失效报“Connection is not available, request timed out”。这属于SSM项目最常见的运行期故障和源码关系不大但直接影响演示效果。还有一个容易忽略的点是事务方法执行时间不能太长否则连接池会被长时间占用的线程拖垮。排查时可以用Druid的WebStatFilter看SQL执行耗时优先优化慢查询。5. 源码包配套文档与答辩演示数据库脚本、PPT和验证脚本的组织方式5.1 数据库初始化脚本如何组织才能一次跑通拿到源码包后数据库脚本一般放在sql目录。我习惯分成schema.sql和data.sql。schema.sql建库建表data.sql插入管理员账号和测试航班数据。data.sql里航班数据要注意两点起飞时间要写未来的日期否则机票列表为空价格要覆盖几个价格区间方便演示筛选。插入管理员密码时如果是MD5加密SQL里直接写密文并且在论文里说明MD5不可逆。5.2 用PPT讲清架构与演示路径PPT不用多15页就够。第一页放系统架构图标注SpringMVC、Service、MyBatis三层。重点画清楚一次预订请求的调用链浏览器→Controller→Service→Mapper→MySQL。答辩时老师问“你的系统怎么保证不超卖”直接把第四章的UPDATE语句截图放出来比背概念有用。每张PPT只放一张图和少量关键词不要复制大段代码。5.3 一个可验证的并发扣减小脚本演示时很难用手点出并发我一般用bash的curl并发请求模拟。前提是系统已有测试用户和航班数据下面脚本对同一个航班ID发20个请求每个请求预订1张票航班初始余票设5跑完后余票应为0订单表应有5条成功记录。for i in $(seq 1 20); do curl -s -X POST http://localhost:8080/air/order/create \ -H Content-Type: application/x-www-form-urlencoded \ -d flightId1userId1001seatCount1 done wait mysql -uroot -p123456 air -e SELECT remaining_seats FROM flight WHERE id1;脚本逻辑说明curl放在后台并发执行wait等待全部请求结束最后用mysql客户端查余票。如果余票为0且成功订单数等于5说明条件更新生效如果余票为负或者超过5个订单说明代码里用了先查后扣的写法。这个小脚本可以直接放进论文的“系统测试”章节测试截图比文字描述更有说服力。注意实际项目里userId应从session获取这里用固定用户只是压测方便。本文还有配套的精品资源点击获取