毕业设计选了航班管理系统这个题目?说实话,这个选题在SpringBoot毕设里算"标准款",既没有惊艳到让评委眼前一亮,也没有冷门到让人无从下手。但这恰恰是它的优势——业务链路完整、需求边界清晰、技术点能撑得住答辩追问,上限和下限都掌握在你自己手里。我做完这个"基于SpringBoot的航空客运服务平台"之后最大的感受是:题目越常规,越要往深处挖,把并发、事务、权限这些环节做出真东西来。这篇就围绕我的完整开发过程,从选题逻辑、技术选型、数据库设计到核心代码和踩坑记录,给准备做同类项目的同学一份可以照着走的参考。
1. 选题为什么选它:一个"问不倒"的毕设题目长什么样
毕业设计答辩有个残酷的现实:评委问你的问题,大多不是围绕"你用什么技术",而是围绕"你为什么用这个技术""业务上这个逻辑怎么兜底"展开。一个题目能不能抗住追问,比它听起来是否高大上重要得多。航班管理系统正好属于那种业务链条完整、每个环节都有真实问题可以深挖的题目。
1.1 核心价值:一条完整业务闭环带来的天然优势
航班管理系统从用户端看是一条清晰的交易链路:注册登录、航班查询、下单购票、订单查看。从管理端看又是一条完整的数据维护链路:航班信息管理、舱位价格配置、订单处理、数据统计。
这看起来简单,但真正动手设计的时候你会发现,它天然包含了几个非常值得展开的技术点:航班余票的并发扣减要防超卖、订单状态要防重复提交、航班信息变更后已有订单如何处理、多角色权限如何控制。这些问题随便抓一个出来,都能在答辩时讲上三五分钟,而且每个问题都有实际业务场景做支撑,评委一听就知道你不是背的八股文。
1.2 系统角色与功能边界:别一上来就想做"大而全"
我在开始编码之前花了整整两天梳理需求,最后把功能边界定成下面这样,供你参考:
用户端(旅客角色):
- 注册与登录:基于JWT的无状态认证,支持密码加密存储
- 航班查询:按出发城市、到达城市、出发日期组合检索,支持按时间/价格排序
- 在线购票:选择航班、填写乘客信息、确认下单、模拟支付出票
- 订单管理:查看订单状态、取消未出票订单
管理端(管理员角色):
- 航班管理:航班的增删改查、上下架(启停售)
- 航线管理:维护起降城市对与航班周期
- 订单管理:按条件检索全部订单,处理异常订单
- 数据概览:按日/按航线统计订单量与基础营收数据
这里有个建议:退改签、会员积分、舱位等级矩阵这类功能,除非你的开题报告里明确写了,否则放到"后续扩展"里提一嘴就够了,不要在V1.0版本里强行做。毕业设计的核心是"把一个闭环做扎实",不是"把功能列得多",功能太多反而每个都浅,答辩时容易被问到细节就露馅。
1.3 非功能需求:决定系统质量的那20%
功能性需求能让系统跑起来,但真正体现工作量的是非功能需求。我在这套系统里重点做了三个方向:
第一是数据一致性。购票时余票扣减必须和订单生成放到同一个事务里,而且要用行级锁防止并发超卖,这是整个系统里技术含量最高的地方,也是我答辩时被追问最多的点。
第二是接口安全性。管理端接口必须做权限校验,普通用户不能访问管理员接口。我通过拦截器统一校验JWT中的角色字段,再配合自定义注解做细粒度控制。
第三是可维护性。数据访问层统一用MyBatis Plus,业务逻辑全部下沉到Service层,Controller只做参数接收和响应封装,保证代码结构是"看起来能让别人接手"的水平。
2. 技术选型:SpringBoot之外还要配什么
技术选型是毕设里最容易"无脑选热门"但最值得讲清楚的部分。我最终的技术栈是SpringBoot、MyBatis Plus、MySQL、Redis、JWT和Vue(Element UI),下面逐一说选择理由。
2.1 SpringBoot本身解决了什么问题
SpringBoot对Spring的贡献是"零配置启动"——它通过自动配置和起步依赖,把过去Spring项目里繁琐的XML配置和组件整合全部简化成了引入一个依赖加几行配置。我做这个系统用的是当前稳定的Spring Boot 2.7.x版本,最直接的好处是内嵌了Tomcat,本地开发不需要单独装容器,打成JAR包扔到服务器上就能跑。
更重要的是SpringBoot生态对后续业务扩展是友好的。比如后期我想加个消息通知功能,直接引入Spring Boot的Mail或WebSocket starter就能无缝集成。这种扩展性对于毕设来说意味着:就算你答辩时说要加功能,也不是"推倒重来",而是在现有骨架上加模块。
2.2 辅助技术组件的选型逻辑
我整理一下这套系统里其它技术的选型图谱:
| 技术组件 | 用途 | 为什么选它而非备选项 |
|---|---|---|
| MyBatis Plus | ORM数据持久层 | 内置分页插件、条件构造器,代码量比原生MyBatis少很多,比JPA的实体关系更好理解 |
| MySQL | 主数据库 | 数据量级在毕设范围内远未达到瓶颈,运维简单,资料多,出问题容易查 |
| Redis | 分布式缓存 | 缓存热点航班查询结果,同时可辅助实现接口幂等。备选方案是Caffeine本地缓存,但Redis能讲出"缓存一致性"的深度 |
| JWT | 用户认证 | 无状态,服务器不需要维护会话,前后端分离时天然适用。备选项是Session+Redis,但JWT能讲清楚"为什么不用Session" |
| Vue 2 + Element UI | 管理端前端 | 前后端分离展示工程化能力,Element UI表格和表单组件能快速出页面 |
| Hutool | 工具类库 | 省去写日期处理、随机数、Bean拷贝等重复代码,主打一个效率 |
这里特别说下Redis。有个容易被忽略的点:我除了拿它做缓存,还利用Redis的SETNX命令做了一个防重复提单的幂等键。用户在提交订单时,前端会生成一个唯一的requestId,后端收到后先尝试在Redis里写入这个key,写不进去说明是重复请求,直接拦截。这个设计答辩时讲出来很有亮点,而且实现成本很低。
2.3 关于前后端分离和非分离的取舍
我见过不少同学毕设用的是SprinBoot + Thymeleaf模板渲染,让Java直接渲染页面。这种做法省去了跨域、联调这些麻烦,但我最后还是选了前后端分离,理由是:现实中企业项目基本都走前后端分离,毕设期间提前把跨域处理、AJAX交互、接口联调这套流程走一遍,面试时能拿出来说的项目经验会扎实很多。代价就是前端需要单独维护一套Vue工程,开发周期多花大概三到五天,个人觉得值。
3. 数据库设计:三张核心表如何撑起整个系统
数据库设计是整个系统里最不能赶工的部分。我一开始直接用Navicat手工建表,建到订单表的时候发现字段怎么安排都有冗余感,后来老老实实画了ER图重新拆了一遍。最终核心表是五张:用户表、航班表、航线表、订单表、乘客表。这里重点讲三张核心表的设计思路。
3.1 航班信息表:冗余字段的取舍很关键
航班表是查询的主表,也是余票库存的载体。我的建表语句核心部分如下:
CREATE TABLE `flight` ( `id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '主键ID', `flight_no` VARCHAR(20) NOT NULL COMMENT '航班号', `route_id` BIGINT NOT NULL COMMENT '航线ID', `departure_city` VARCHAR(30) NOT NULL COMMENT '出发城市', `arrival_city` VARCHAR(30) NOT NULL COMMENT '到达城市', `departure_time` DATETIME NOT NULL COMMENT '计划起飞时间', `arrival_time` DATETIME NOT NULL COMMENT '计划到达时间', `aircraft_type` VARCHAR(50) DEFAULT NULL COMMENT '机型', `total_seats` INT NOT NULL COMMENT '总座位数', `remaining_seats` INT NOT NULL COMMENT '剩余座位数', `price` DECIMAL(10,2) NOT NULL COMMENT '经济舱价格', `status` TINYINT NOT NULL DEFAULT '1' COMMENT '状态:1可售 0停售', `create_time` DATETIME DEFAULT CURRENT_TIMESTAMP, `update_time` DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (`id`), KEY `idx_departure_city` (`departure_city`), KEY `idx_departure_date` (`departure_time`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='航班信息表';这里有个设计决定想重点说下:我把出发城市、到达城市直接冗余到了航班表里,同时保留了route_id指向航线表。为什么这么做?因为用户查询航班时是按照起降城市来筛选的,如果只存route_id,每次查询都得多一次关联。冗余城市字段用空间换了查询性能,这是很典型的报表/交易类系统设计习惯。代价是如果城市名称变更,需要同步更新航班表,但这种变更在业务上极少发生,可以接受。
另一个关键点是remaining_seats直接放在航班表上而不是单独一张库存表。这样做在复杂度上是"够用且低耦合"的,配合SQL层的行锁可以很好地解决超卖问题。如果你把库存拆到单独表,表面上更规范,但在下单时要同时维护两张表的数据一致性,毕设级别的系统没必要冒这个险。
3.2 用户表与订单表:状态机设计决定业务边界
用户表没什么特殊的,重点是密码用BCrypt加密存储,加上username唯一索引和role字段区分管理员与普通用户。订单表就比较讲究了,它的核心是状态设计和唯一性约束:
CREATE TABLE `orders` ( `id` BIGINT NOT NULL AUTO_INCREMENT, `order_no` VARCHAR(32) NOT NULL COMMENT '订单编号', `user_id` BIGINT NOT NULL COMMENT '下单用户ID', `flight_id` BIGINT NOT NULL COMMENT '航班ID', `flight_no` VARCHAR(20) NOT NULL COMMENT '航班号冗余', `passenger_id` BIGINT NOT NULL COMMENT '乘机人ID', `seat_count` INT NOT NULL DEFAULT '1' COMMENT '购票数量', `total_price` DECIMAL(10,2) NOT NULL COMMENT '订单总价', `status` TINYINT NOT NULL DEFAULT '0' COMMENT '0待支付 1已支付 2已出票 3已取消 4已退票', `remark` VARCHAR(255) DEFAULT NULL, `create_time` DATETIME DEFAULT CURRENT_TIMESTAMP, `update_time` DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (`id`), UNIQUE KEY `uk_order_no` (`order_no`), KEY `idx_user_id` (`user_id`), KEY `idx_flight_id` (`flight_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='订单表';订单编号我采用的生成规则是:日期(yyyyMMdd)+ 用户ID后四位 + 随机流水号,例如2025061212345678。用日期前缀的好处是排查问题时能一眼看出订单发生的时间,而且带唯一索引后天然防止重复下单。这里不建议直接用数据库自增ID当作订单号对外展示,一是暴露系统订单总量,二是并发下ID可猜测性太高,这属于安全习惯的训练。
订单状态我用TINYINT数字枚举:0待支付、1已支付、2已出票、3已取消、4已退票。状态流转必须是单向的:待支付可以变为已支付或已取消,已支付可以变为已出票或已退票,已出票之后只能变为已退票。这种状态机约束不能只靠前端隐藏按钮,必须在Service层写状态校验逻辑,比如"只有待支付订单才能取消",否则脏状态会产生连锁问题。我在代码里统一封装了一个OrderStatusEnum枚举类,Service里所有状态流转都通过枚举判断,这个习惯强烈建议养成。
3.3 乘客表和航线表:两张辅助表怎么简化逻辑
乘客表用于记录常用乘机人信息,姓名、证件号、手机号,属于订单的依赖数据。航线表则维护城市对信息,比如"北京-上海"是一条航线,某条航线每天执行几个班次对应到多张航班表记录。真正的查询主表还是航班表,航线表在管理端维护航班时做辅助。把这两张表单独拆出来,其实主要是为了让管理端的操作逻辑更清晰,避免所有城市信息都塞在航班表里导致难以维护。
4. 核心业务落地:从航班查询到购票出票的完整链路
数据库定稿后就开始写业务代码。整个系统的核心模块可以分成四块:航班查询、购票流程、管理端航班维护、登录认证和权限控制。下面按模块讲实现思路和关键代码。
4.1 航班查询:多条件组合筛选与排序
航班查询是系统里的高频接口,用户的典型操作是选定出发城市、到达城市和日期,再从结果里按价格或时间排序。如果直接用SQL拼接,条件一多代码就开始发散。MyBatis Plus的条件构造器在这里非常顺手:
@Override public PageResult<FlightVO> queryFlights(FlightQueryDTO dto) { LambdaQueryWrapper<Flight> wrapper = new LambdaQueryWrapper<>(); // 支持出发城市为空时查询全部 if (StrUtil.isNotBlank(dto.getDepartureCity())) { wrapper.eq(Flight::getDepartureCity, dto.getDepartureCity()); } if (StrUtil.isNotBlank(dto.getArrivalCity())) { wrapper.eq(Flight::getArrivalCity, dto.getArrivalCity()); } // 日期范围:从当天00:00:00到23:59:59 if (dto.getDepartureDate() != null) { wrapper.between(Flight::getDepartureTime, DateUtil.beginOfDay(dto.getDepartureDate()), DateUtil.endOfDay(dto.getDepartureDate())); } wrapper.eq(Flight::getStatus, 1); // 只查可售航班 // 排序规则:默认起飞时间升序,可切换为价格升序 if ("price".equals(dto.getSort())) { wrapper.orderByAsc(Flight::getPrice); } else { wrapper.orderByAsc(Flight::getDepartureTime); } Page<Flight> page = new Page<>(dto.getPageNum(), dto.getPageSize()); flightMapper.selectPage(page, wrapper); return PageResult.of(page); }这套代码的核心便利点是LambdaQueryWrapper不会把列名硬编码成字符串,字段经过编译期校验,重构时不容易出错。日期范围用DateUtil转换起始和结束时间点,可以避免用户在凌晨时段查询时数据少算一天的问题。这里还有个体验细节:如果出发城市和到达城市相同,直接在Service层拦截返回空页,不做数据库查询,省一次无意义的IO。
4.2 购票流程:事务边界与库存扣减
购票是整套系统里最需要严谨的环节。它的业务链条是:校验航班存在且可售、校验余票足够、锁定航班的行记录、再次校验余票、扣减余票、创建订单(待支付)、模拟支付成功后更新订单状态为已支付并出票。如果任意一步失败,整个操作回滚。
这里最关键的技术决策是用悲观锁。我直接在SQL层面使用SELECT ... FOR UPDATE锁定航班行,保证同一时刻只有一个事务能修改该航班的余票:
@Transactional(rollbackFor = Exception.class) public OrderVO createOrder(OrderCreateDTO dto) { // 1. 前置校验 Flight flight = flightMapper.selectById(dto.getFlightId()); if (flight == null || flight.getStatus() != 1) { throw new BizException("航班不存在或已停售"); } // 2. 锁定航班行 Flight lockedFlight = flightMapper.selectByIdForUpdate(dto.getFlightId()); if (lockedFlight.getRemainingSeats() < dto.getSeatCount()) { throw new BizException("余票不足"); } // 3. 扣减余票并生成订单 flightMapper.decrementSeats(dto.getFlightId(), dto.getSeatCount()); Order order = buildOrder(lockedFlight, dto); orderMapper.insert(order); // 4. 模拟支付(实际项目这里是调用支付网关) boolean paySuccess = mockPay(order.getOrderNo()); if (!paySuccess) { throw new BizException("支付失败,订单已回滚"); } return OrderVO.from(order); }对应的Mapper方法:
@Select("SELECT * FROM flight WHERE id = #{id} FOR UPDATE") Flight selectByIdForUpdate(Long id); @Update("UPDATE flight SET remaining_seats = remaining_seats - #{count} " + "WHERE id = #{id} AND remaining_seats >= #{count}") int decrementSeats(Long id, Integer count);很多人会问:为什么不用乐观锁(version字段)?乐观锁在竞态不激烈时性能更好,而且UPDATE带条件判断天然防超卖。但在商品抢购场景下,乐观锁会频繁重试或直接放弃更新,用户体验差;悲观锁虽然串行化,但在航班购票这种写多读少的场景里更可控。毕设答辩时把这两种方案的取舍讲清楚,本身就是加分的亮点。
@Transactional(rollbackFor = Exception.class)这个注解也很重要。如果不显式指定rollbackFor,Spring默认只在RuntimeException时回滚,遇到检查异常不会回滚,很容易出现"支付失败但订单还在"的诡异状态。这是事务边界最容易踩的坑,后面我会单独展开。
4.3 管理端:航班维护与订单处理
管理端航班维护就是标准的CRUD,但有几个细节要注意。新建航班时要校验航班号不能重复,同一航线同一时段不能出现两个班次冲突。停售航班时还需要检查是否存有待支付订单,如果有,应该提示管理员先把订单处理掉,否则用户支付成功后突然发现航班停飞,体验会很糟糕。
订单处理模块主要是按条件查询和状态修改。这里推荐一个做法:管理端的查询尽可能使用单独写SQL的Mapper方式,不要复用用户端的查询逻辑。管理端往往需要多表关联,比如查订单时关联出用户昵称、航班座舱信息,直接用@Select写联表SQL比用Wrapper硬拼清晰得多。系统整理功能的统计概览,我用了SELECT聚合加日期分组,返回给前端渲染趋势图,这部分代码量不大但视觉效果好,尤其适合在答辩PPT里展示。
4.4 登录认证与权限控制:JWT加拦截器的组合方案
前后端分离项目里Session不太好使,我选择的方案是JWT。用户登录成功后,后端生成一个包含userId和role的Token返回给前端,前端后续所有请求在HTTP头里带上Authorization: Bearer token。后端通过拦截器统一解析校验。
@Component public class AuthInterceptor implements HandlerInterceptor { @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { // 放行登录接口和静态资源 if (request.getRequestURI().contains("/api/auth/login") || request.getRequestURI().contains("/api/auth/register")) { return true; } String token = request.getHeader("Authorization"); if (token == null || !token.startsWith("Bearer ")) { throw new BizException("未登录或Token缺失"); } // 验证并解析Token Claims claims = JwtUtil.parseToken(token.substring(7)); Long userId = claims.get("userId", Long.class); Integer role = claims.get("role", Integer.class); // 保存到请求上下文,供Controller直接获取当前用户 UserContext.set(new LoginUser(userId, role)); if (request.getRequestURI().contains("/api/admin/") && role != 1) { throw new BizException("无管理员权限"); } return true; } @Override public void afterCompletion(...) { UserContext.clear(); } }JWT方案带来的好处是服务端无状态,水平扩展时不需要共享会话信息,这在分布式场景里的价值能直接说出来。缺点是Token有效期管理比Session复杂,比如用户被禁用后Token依然有效,直到过期。我在系统里给Token设了24小时过期时间,原因是毕设答辩演示时不想频繁登出登录这个环节浪费时间。
另外,权限校验光写在拦截器里还是不够细,我额外给管理端的一些写操作加了自定义注解@RequireAdmin,在Controller方法上声明后由AOP切面统一拦截。这个设计可能比拦截器更优雅一点,给读者留个进阶方向。
5. 联调与部署阶段踩过的坑
写代码的过程还算顺利,真正让我头皮发麻的是联调和测试阶段。这些坑单靠看文档是避不开的,写出来希望对你有帮助。
5.1 时间字段莫名差了8个小时
第一次联调时,我在前端页面上看到航班起降时间全都比数据库里存的少了8小时。排查到末尾发现是JSON序列化的时区问题。Jackson默认把LocalDateTime序列化时使用的时间源是UTC,而MySQL驱动里存的又是东八区时间,两边一折算就偏了。
解决办法是在application.yml里配置:
spring: jackson: time-zone: Asia/Shanghai date-format: yyyy-MM-dd HH:mm:ss同时数据库连接串上加上serverTimezone=Asia/Shanghai。这应该是所有SpringBoot项目的标配配置,但几乎每个新手都会踩一遍。建议你在建项目的第一步就把这两个配置配上,后面能省很多排查时间。
5.2 第一轮并发压测:超卖问题当场暴露
我用JMeter模拟100个并发用户抢同一航班的最后10张余票,结果发现订单表里生成了12条记录,库存变成了负数。当时第一反应是"加锁",但后来又琢磨出真正的根因不止一个。
第一个问题是扣减SQL没有带余票充足的条件。如果只写SET remaining_seats = remaining_seats - #{count},那就算有锁,多线程串行执行时也可能出现负数扣减。所以我改成了UPDATE ... WHERE id = #{id} AND remaining_seats >= #{count},让数据库在更新时做第二层校验。
第二个问题是有些操作没有加事务。比如说先查余票、判断充足、再扣减,这三步不在同一个事务里,中间时刻可能有别的请求插入修改。所以套路必须统一:先锁行(FOR UPDATE)、再判断、再修改,整个过程必须在一个事务内完成。改完之后再压测,数据就完全对得上了。
5.3 try-catch 把事务回滚给吞了
这个坑是测试退票流程时发现的。退票操作要先校验订单状态、再修改状态、最后恢复航班余票,整个方法加了@Transactional。但我为了给前端返回友好错误提示,在Service方法内部用try-catch包住了业务逻辑,然后catch里抛出BizException。结果事务就是不回滚,数据停留在半修改状态。
原因很好解释:Spring的事务代理是通过方法抛出的异常来触发回滚的。如果你在方法内部就把异常捕获并吞掉了,事务管理器根本感知不到状态异常,自然就不会回滚。
正确做法是Service方法内不做try-catch,让业务异常层层往上抛,由全局异常处理器统一捕获并转换成前端提示消息。只有一种情况允许方法内catch,就是你明确知道这个异常不需要回滚事务,并且已经做了补偿处理。这个边界要分清楚。
5.4 字段命名:前端要驼峰,后端返回下划线
还有一个不大不小但很磨人的问题。数据库表字段是下划线风格(比如departure_city),MyBatis Plus默认映射到后端实体是驼峰(departureCity),这没问题。但接口返回给前端时,如果你手动封装了VO并且字段名是departure_city,前端用row.departureCity取值就会一直是undefined。
我最后统一了规范:数据库表字段用下划线,Java实体用驼峰,VO和前端交互统一用驼峰,JSON序列化时靠Jackson的驼峰命名自动转换。这里你只需要记住一点——所有前后端交互的字段,选择一种命名风格写到底,不要一会儿下划线一会儿驼峰,联调时切换来切换去极易出bug。
6. 答辩准备与后续可扩展的方向
代码写完、测试通过,剩下的就是答辩这一关。答辩的好坏和代码写得好坏不完全对等,你需要把系统里最值得讲的技术亮点提前打磨成一段流畅的叙述。
6.1 评委最可能追问的几个点
我梳理了答辩现场被问得最多的"夺命题",逐一准备好答案,你也能照着准备一份:
| 评委常问 | 参考回答思路 |
|---|---|
| 为什么用悲观锁而不用乐观锁? | 购票属于高冲突写场景,悲观锁能保证事务串行化,避免乐观锁频繁重试导致体验差;同时说明了悲观锁在极端高并发下的吞吐瓶颈以及可替换为Redis预扣库存的演进空间 |
| 事务回滚的触发条件是什么? | 默认RuntimeException回滚,检查异常不回滚;我们通过rollbackFor=Exception.class显式指定,同时说明了try-catch吞异常导致回滚失效这个反例 |
| JWT和Session各有什么优缺点? | JWT无状态、适合水平扩展,但无法主动失效;Session服务端可控,但需要维护会话存储,跨域和分布式会比较麻烦 |
| 航班查询缓存怎么保持一致性? | Redis缓存航班信息后,管理端更新航班时删除对应缓存,查询时缓存未命中再回源数据库并重建缓存,通过"先删缓存再更新DB"避免脏数据 |
| 订单状态机为什么不用字符串? | TINYINT枚举占空间小且索引效率高,状态流转通过枚举类保证合法路径,非法状态变更在代码层就被拦截 |
这些问题的答案没有标准抄法,关键是你要真正理解自己代码里的取舍逻辑。真诚地说"这个方案在当时的数据规模下够用,如果并发量再高,我会换成XXX方案",比硬吹强得多。
6.2 这个系统再往前走两步会变成什么样
如果你答辩完还有余力,或者想把系统作为找工作的项目亮点,有三个方向的升级建议:
一是把模拟支付替换成真实支付网关对接,比如支付宝沙箱环境。这会让系统从"演示品"变成"可上线系统",含金量直接提升一个级别。二是引入消息队列(比如RabbitMQ)处理订票后的异步通知,让"购票成功短信通知"这类辅助流程和主流程解耦,可讲述性和团队合作痕迹都会更强。三是基于Redis的库存预热和令牌桶限流方案,把秒杀场景的抗压能力做上去,非常适合在面试时展示高并发设计能力。
我个人在实际开发中的体会是:航班管理系统看起来是个普通项目,但如果每一步都追问"为什么这么设计",它就能变成一门小型的架构课。数据库的冗余和反范式要能讲清理由、并发控制要能讲清冲突场景、事务边界要能讲清回滚机制——这些经验放到任何一个Shop项目或者Booking项目上都通用。最后再分享一个小技巧:开发过程中把你自己踩过的坑和对应解法同步记录在项目文档里,答辩前翻一遍,很多题你根本不用临时想,直接就有真实素材可讲。