这标题一看就是典型的毕设选题或者课程综合实训项目,我在社群里被问过太多次了。前两年带过几个同学做类似的航空类信息系统,有做机场货运的,有做值机柜台的,还有做行李分拣的,但最经典、最适合拿来练手并写进简历的,还是航班进出港管理这块。原因很简单:业务足够复杂但不至于失控,既有状态流转,又有资源冲突,还有时间维度上的各种坑,数据量也能撑得起分页、筛选这些常规功能,拿SpringBoot+Vue来做再合适不过。
这篇文章我不打算按教科书的方式罗列功能点,而是把我做这类系统时真实遇到的需求边界、表结构设计、并发冲突处理和前端交互细节都摊开讲一遍。如果你正准备动手写这个项目,或者已经写了一半卡在某个地方,这篇文章应该能帮你少踩几个坑。
1. 航班进出港系统到底在管什么:先圈死需求边界
很多人在设计这类系统时容易犯同一个毛病:把系统做得大而全,仿佛要把整个机场的信息化承包下来。航班进出港管理的核心其实只有一件事——让每一架航班在正确的时刻出现在正确的位置,并让旅客和行李跟着它走。
1.1 进出港流程的业务节点
进出港业务可以从两个视角去拆解。
从离港视角看,一架航班的生命周期是:前一日或当日生成航班计划,起飞前数小时开始值机,起飞前一段时间开始登机,然后关门、推出、起飞。从进港视角看则是:航班降落、滑行入位、开舱门下客、行李卸运、旅客提取行李离场。
所以一套完整的进出港管理系统,至少要覆盖航班计划管理、动态信息实时更新、登机口分配、状态变更记录这几个核心模块。这里最见功力的细节是“状态”二字——同一架航班在调度员、值机员、旅客三个角色的眼中,状态是不同步的。系统要做的是保证后台状态一致的前提下,把合适的视图推给合适的人。
1.2 功能清单的选择题
拿我实际带过的项目来说,最终敲定的功能清单是这样的:
- 航班计划管理:航班号、航司、起降机场、计划起降时间、机型、座位数
- 动态管理:实际起降时间、登机口变更、延误原因、状态流转
- 登机口分配与冲突检测:同一时刻同一登机口不能同时分配给两个航班
- 航班状态看板:按时间轴展示当前时刻的进出港航班
- 基础数据维护:航司、机场、登机口、机位
- 用户与角色权限:调度员、值机员、普通查询用户
我没有加的功能包括:机票销售、行李托运全流程追踪、机组排班、机务维修工单。这些统统属于“看着相关但实际上是另一个系统”的范畴。如果你是在做毕设,加这些只会让答辩时把自己绕晕;如果是在做企业项目,这些也需要单独的需求评审,不该混在一起。
1.3 角色权限的粒度设计
权限这块我建议用最简单直接的RBAC模型:用户—角色—权限的三级结构,不需要引入Shiro或者Spring Security那么重的框架,自己写一个拦截器配合Redis存token就够用了。
但有一个细节值得注意:调度员和值机员的权限边界必须清楚。调度员可以修改航班动态、调整登机口,值机员只能查看和自己相关的航班信息,不能动任何资源分配。我当时用一个@RequirePermission注解加一个前置拦截器就解决了,代码量不到五十行,但这种设计思路在答辩时很加分,因为它体现的是业务理解和安全意识,不是技术炫技。
2. 技术栈选型的逻辑:为什么是SpringBoot+Vue而不是微服务
这个问题十个评委有九个会问。你如果回答“因为学校要求”或者“大家都用这个”,那就白做这个项目了。技术选型必须体现你的判断力。
2.1 单体后端足够的原因
航班进出港管理系统的数据量级,在一个中型机场一天也就一千架次上下,峰值吞吐远达不到需要微服务的程度。更重要的是,系统里有大量强一致性的需求——安排登机口、更新航班状态、分配机位,这些操作如果被拆分到多个服务里,分布式事务会把你拖进泥潭。
单体SpringBoot应用的好处恰恰在于:一个业务操作可以直接在一个事务里完成,不用考虑网络调用失败、消息丢失、最终一致性这些头疼的问题。我常和同学说:单体架构不是落伍,而是这个业务场景下复杂度最低、稳定性最高的方案。
2.2 前后端分离与选型理由
前端用Vue3,理由不只是“Vue容易上手”,而是这个项目天然需要数据驱动视图。调度工作台上,航班状态一变,表格的“当前状态”列要即时刷新;登机口看板上一架航班滑入,占用状态要立刻变化。Vue的响应式系统对这类场景的支持是最顺手的,写起来不像用原生JS那样到处手动操作DOM。
组件库我推荐Element Plus,没有别的原因,就是它的表格、表单、日期选择器这些组件足够成熟,样式不会出错。大屏看板如果要做,可以上ECharts,航班进出港的高峰时段分布图、登机口利用率环形图都是现成的图表类型。
2.3 数据存储与缓存的取舍
数据库就是MySQL 8.x,配合MyBatis-Plus。缓存用Redis,但不意味着什么东西都往Redis里塞。实际项目里我重点缓存两类数据:一是基础字典(机场、航司),这些数据几乎不变,访问频率却极高;二是当前时段航班状态看板的查询结果,给调度台前端加一个几十秒的缓存,避免每次刷新都打到数据库上。
有一类数据我不建议缓存——登机口分配结果。因为这类数据涉及并发写入,缓存一旦和数据库不一致,会出现两架航班同时占用一个登机口的极端情况。系统的安全底线宁可慢一点,也不能靠缓存来提速度。
3. 数据库设计:航班表拆分、登机口表与UTC时间的三个坑
数据库是这类系统的根基。很多同学一上来就建一张超级大表,把航班计划、动态信息、分配结果全塞进去。这个方案你写到登机口冲突检测时就会发现根本没法查,索引都不知道怎么建。
3.1 航班计划与动态信息为什么要拆两张表
计划是“原定”,动态是“实际”,这两者的更新频率和生命周期完全不同。航班计划一天最多改几次,而动态信息在航班临起降前可能几分钟就变更一次。
我把它们拆成两张表:flight_plan存航班号、航司、起降机场、计划起降时间、机型等相对静态的信息;flight_dynamic存实际起降时间、延误时长、延误原因、登机口分配、当前状态。两张表通过航班ID关联,动态表每次状态变更时向后追加变更记录,而不是原地更新。
这样设计的好处有三:第一,历史可追溯,一出问题能查出“谁在什么时间把航班改成了什么状态”;第二,查询效率高,调度看板主要查动态表,基础数据不参与条件过滤;第三,为后续做报表统计留了数据基础,比如统计某航司的平均延误时长,直接按航司ID聚合动态表就行。
3.2 核心表结构与字段说明
这里给一张我最终定下来的核心表清单,字段只列最关键的:
| 表名 | 关键字段 | 说明 |
|---|---|---|
| flight_plan | id, flight_no, airline_id, dep_airport_id, arr_airport_id, plan_dep_time, plan_arr_time, aircraft_type | 航班计划主表 |
| flight_dynamic | id, flight_plan_id, actual_dep_time, actual_arr_time, gate_id, flight_status, delay_reason | 动态信息表,flight_status存状态枚举值 |
| gate_info | id, gate_code, terminal, is_available | 登机口基础表 |
| flight_status_log | id, flight_plan_id, from_status, to_status, operator, op_time | 状态流转日志 |
| sys_user / sys_role / sys_user_role | —— | 用户、角色、关联表 |
一个容易被忽略的点:gate_id应该放在动态表而不是计划表。因为登机口是动态分配的,计划阶段还没有确定值。如果把登机口放在计划表,你每次重新分配都要去UPDATE计划记录,逻辑上就别扭了。
3.3 时间字段的三个易错点
这个项目的时间处理是重灾区,我总结三个最常见的坑。
第一个坑是用timestamp还是datetime。我推荐统一用datetime,并且在Java代码里全部使用LocalDateTime,不搞时区换算。因为航班的计划时间本质上是机场本地时刻,不涉及全球跨时区协调。用timestamp反而容易踩MySQL系统时区转换的坑。
第二个坑是跨天航班。搜索某天的航班列表时,光查flight_date = '2024-06-15'是不够的,一架凌晨起飞的航班往往计划起飞时间是前一天的深夜,落地时间在当天凌晨。正确写法应该按时间区间查询:plan_dep_time >= '2024-06-15 00:00:00' AND plan_dep_time < '2024-06-16 00:00:00',但还要兜底查一下凌晨还在飞的前日航班。我当时的方案是额外加一个schedule_date业务日期字段,配合时间区间双条件过滤。
第三个坑是MySQL连接参数的时区问题。serverTimezone=Asia/Shanghai必须显式配置,否则驱动版本一升级,时间读写可能差8个小时。这个坑在部署环节我还会再讲一次。
4. 后端核心逻辑:状态机、冲突检测与并发安全
后端代码我拆三块来讲,这三块也是你答辩时最容易被问住的硬核内容。
4.1 航班动态的状态机设计
航班的生命周期不是一个简单的线性流程,它有线性的主线,也有分叉和回退。我设计的状态机如下:
正常离港链路:计划 -> 值机中 -> 登机中 -> 已起飞
正常进港链路:飞行中 -> 已落地 -> 已到港
异常分支:计划/值机中 -> 延误 -> 值机中,或者计划 -> 取消
这个状态机我用一个枚举类加一个流转校验方法实现,每个状态定义允许的后续状态集合:
public enum FlightStatus { SCHEDULED("计划", Arrays.asList("CHECK_IN", "DELAYED", "CANCELLED")), CHECK_IN("值机中", Arrays.asList("BOARDING", "DELAYED")), BOARDING("登机中", Arrays.asList("DEPARTED", "DELAYED")), DEPARTED("已起飞", Arrays.asList("ARRIVED")), ARRIVED("已落地", Arrays.asList("GATE_AVAILABLE")), GATE_AVAILABLE("已到港", Collections.emptyList()), DELAYED("延误", Arrays.asList("CHECK_IN", "BOARDING", "CANCELLED")), CANCELLED("取消", Collections.emptyList()); public boolean canTransitTo(FlightStatus target) { return allowedTransitions.contains(target.name()); } }每次状态变更都走同一个Service方法,先校验合法性,再更新数据库,再写状态日志。这样做的价值是:业务流程的规则集中在一个地方,不会被到处散落的update status写乱。
4.2 登机口冲突检测的实现思路
这是全系统最核心的算法,也是面试官最爱深挖的点。登机口冲突的本质是时间窗口重叠:两架航班在同一个登机口的时间段不能有交集。
每架航班占用登机口的时间段不是航班本身的起降时间,而是前后各加一段缓冲时间:进港航班从预计降落前15分钟开始占位,到实际接到旅客后30分钟释放;出港航班从值机开始前30分钟占位,到起飞后15分钟释放。这里的缓冲时间是业务经验值,你完全可以在系统里配置化。
冲突检测的核心查询如下,我用的是MySQL原生查询加排他锁:
@Transactional public GateAssignResult assignGate(GateAssignRequest request) { // 1. 锁定登机口记录,防止并发分配 GateInfo gate = gateMapper.selectByIdForUpdate(request.getGateId()); // 2. 查询时间窗口内是否已有冲突航班 List<FlightDynamic> conflicts = dynamicMapper.findConflictsByGate( request.getGateId(), request.getWindowStart(), request.getWindowEnd() ); if (!conflicts.isEmpty()) { return GateAssignResult.conflict(conflicts); } // 3. 无冲突则更新航班动态的登机口字段 dynamicMapper.updateGateId(request.getFlightPlanId(), request.getGateId()); return GateAssignResult.success(); }对应的SQL大概是:
SELECT * FROM flight_dynamic WHERE gate_id = #{gateId} AND flight_status IN ('SCHEDULED', 'CHECK_IN', 'BOARDING', 'DELAYED') AND ( (plan_dep_time BETWEEN #{windowStart} AND #{windowEnd}) OR (actual_arr_time BETWEEN #{windowStart} AND #{windowEnd}) OR (#{windowStart} BETWEEN plan_dep_time AND actual_arr_time) )这个查询的关键是判断区间重叠,而不是简单比较两个时间点的大小。区间的重叠条件是A.start <= B.end AND B.start <= A.end,把边界条件在SQL里都照顾到。
4.3 并发分配同一登机口的竞态处理
登机口分配天然是并发场景。两个调度员可能同时盯着同一个空闲登机口,都想把航班安排进去。如果没有并发控制,两个事务同时查出“无冲突”,然后同时更新,最后航班就会冲突。
我的处理方案分两级。第一级是数据库层面的悲观锁,分配操作开始时先SELECT ... FOR UPDATE锁定登机口记录,让后续分配排队。第二级是Redis分布式锁,锁的粒度是gate:assign:{gateId},加一个几秒的过期时间防止死锁。两级锁都加上就万无一失了。
这里有一个经验教训:不要用“先查再插”的方式来防冲突,因为查和插之间永远有间隙。要么搞唯一约束,要么搞锁,靠代码逻辑的时序是防不住的。
5. Vue3前端:从调度看板到工作台交互的要点
好多同学折腾完后端,前端口子松了就随便写写。但其实这类系统前端的工作量一点不比后端小,尤其是调度工作台,交互逻辑非常核心。
5.1 数据刷新策略:轮询、推送还是手动刷新
航班状态数据要不要实时推送?我的答案是:用轮询就够,不要第一时间上WebSocket。原因很简单:航班状态变更的频率并不高,几分钟一次就算频繁了,用WebSocket纯属杀鸡用牛刀,还会带来心跳维护、断线重连这些额外复杂度。
我的方案是:调度看板用setInterval每30秒自动拉一次当天的进出港航班列表接口,同时给一个手动刷新按钮。如果你想让体验更好一点,可以加一个“最后更新时间”的提示,这样用户看到数据是30秒前的也不会觉得奇怪。等你的系统真的需要秒级推送了,那时候再引入WebSocket也不迟。
5.2 多条件组合筛选的处理方法
航班列表最常用的筛选项有:航班号模糊搜索、航司下拉选择、日期范围、状态多选、进出港类型单选。后端查询时用MyBatis-Plus的LambdaQueryWrapper把条件叠加上去就可以了,重点是要给flight_dynamic表的plan_dep_time建索引,否则日期范围一拉大,全表扫描的耗时就会很难看。
前端侧的组合筛选我建议直接同步到URL参数上,比如/schedule?date=2024-06-15&airline=MU&status=DEPARTED。这样做的好处是:调度员可以把某个常用视图存成书签,下次打开还是同样的筛选条件,非常实用的功能。
5.3 调度操作链路的交互细节
指派登机口是整个系统操作频率最高的功能。我的交互设计是这样的:选中一行航班,点“分配登机口”弹窗,弹窗里列出当前空闲的登机口列表,且每个登机口都有时间线,展示未来两个小时内有没有其他航班占用。
这个时间线展示是我最满意的一个细节。单纯给一个下拉框让你选登机口,调度员根本不知道哪个口空着,选的时候还要来回看别的页面。时间线一上,哪个口在什么时间段被占用,一眼就能看出来。
还有一个细节是二次确认。分配登机口这种操作影响面很大,改错了旅客就得从一个登机口跑到另一个登机口。前端在提交前一定要弹确认框,最好把“目标登机口”和“冲突航班号”都列出来让用户核对一遍再提交。
6. 联调与部署实测:我踩过的那些坑
项目写完后,联调加部署会暴露出一堆平时写代码看不见的问题。我把自己踩过的坑整理成一条排错线路,你按这个顺序自查能省很多时间。
6.1 本地联调最容易翻车的三个点
第一个是跨域问题。前端跑在5173端口,后端跑在8080端口,浏览器的同源策略直接拦截请求。最简单的解法不是在后端写CorsFilter,而是在前端Vite配置里加代理转发,把/api开头的请求代理到后端地址:
// vite.config.js export default defineConfig({ server: { proxy: { '/api': { target: 'http://localhost:8080', changeOrigin: true } } } })这样前端请求/api/flights等于请求http://localhost:8080/api/flights,浏览器视角永远是同源的,就不会有跨域问题。
第二个是Redis没启动。很多人把Redis的依赖加进项目,但本地没启动Redis服务,结果系统一启动就报连接异常。这类问题排查的时候先看启动日志最前面的提示,别一上来就翻业务代码。
第三个是初始化数据的顺序问题。我先建了用户表,又建了角色表,然后插入关联数据时发现之前的ID对不上。解决方法是把测试数据做成schema.sql加data.sql,每次重建数据库都以这两个文件为准,脚本化初始化。
6.2 部署时的数据库时区与字符集配置
部署到服务器上,MySQL的时区问题必须显式指定。application.yml里的JDBC连接串要写成下面这样:
spring: datasource: url: jdbc:mysql://localhost:3306/flight_mgmt?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai同时MySQL服务器自身的my.cnf里也要配default-time-zone = '+08:00'。两处都配上,才能保证程序写入的时间值、MySQL存储的时间值、页面展示的时间值完全一致。
字符集这里同样容易踩坑。建库时要明确指定utf8mb4,不要用默认的latin1。否则一旦有中文延误原因写进去,你看到的不是正常的汉字,而是一堆问号。
6.3 并发场景的复现与验证
最后讲一个能体现系统质量的验证方案。当时写完登机口分配功能后,我没有只测单用户操作,而是写了一个并发脚本模拟两个用户同时抢同一个登机口。用Postman或者JMeter开两个线程同时发请求,然后看数据库里最终状态。
结果第一次测试真的复现了冲突——两个请求都分配成功了。原因是我当时只在Service方法上加了@Transactional,但没加锁,两个事务同时读到了“无冲突”的结果。后来加上selectByIdForUpdate和Redis分布式锁双重保险,再跑并发测试,第二个请求就被阻塞或者返回冲突提示了。
这个排查链路我在答辩时会重点展示。它体现了三个能力:能设计并发场景、能复现问题、能从数据层面验证修复效果。这三个能力比你说“我加了锁所以安全”要有说服力得多。
7. 这套系统的增强方向与我的整体感受
做到这里,一套可用的航班进出港管理系统已经成型。后面如果你想让它更完善,有两条路值得走。
一条是引入消息队列做事件广播。航班一旦延误或者更换登机口,可以通过MQ把事件发给消息中心,消息中心再推送短信或公众号给订阅了该航班的旅客。这样一来系统就从“内部管理工具”变成了“服务终端的一部分”,价值完全不同。
另一条是数据可视化。把每天的航班量、准点率、平均延误时长、登机口使用率等指标汇总到一张大屏上,用ECharts做趋势图和热力图,整个系统的调度管理水平就变得一目了然。
最后说说我的切身体会。这个项目前后端加起来大概五千行代码,量不算大,但难点全藏在业务规则里。登机口的冲突逻辑、航班状态机的流转、时间跨天的判断,每一个都是“看起来简单、写起来要想半天”的地方。我做过三版这个项目,第一版照搬了教务系统的思路,失败;第二版想用微服务硬撑场面,过度设计;第三版老老实实把业务流程吃透,反而顺了。这种项目最考验的不是框架熟练度,而是你能不能把一个领域的业务抽象成清晰的数据结构和状态流转。想清楚这一点,比多背几个API有用得多。