简介:这份资源是面向计算机专业毕业生与Java Web学习者的机场航班起降与协调管理系统完整毕业设计包,围绕航班调度、状态管理与协调流程展开,可解决选题难、代码无从下手、答辩准备不充分等问题。包内整合项目报告、答辩PPT、源代码、数据库文件、运行截图与部署视频,覆盖需求分析、系统设计、编码实现、测试优化到答辩展示全流程。压缩包约84.93MB,文件以源码、文档、数据库脚本、演示视频和截图为主,分别用于系统运行、设计说明、数据初始化与效果验证。系统采用Java SE/Java EE技术栈,结合MVC模式、Servlet与JSP,并可能引入Spring、MyBatis等框架,配合MySQL等关系型数据库完成航班信息与乘客数据的增删改查,同时涉及Tomcat部署、前端页面构建及Spring Security权限控制等知识点。目前已有495人学习下载,适合需要完整赛题方案、可运行代码、部署录屏与排错思路的读者参考,也能帮助理解企业级Java Web应用的开发流程与文档组织方式。
1. 机场航班起降与协调管理系统:从建表到跑通,一个 Java 毕设的完整落地路径
航班起降与协调管理系统,本质是把「航班计划、跑道占用、停机位分配、延误协调」这几件事用一套后台服务管起来。它要解决的核心问题是:同一时段多架航班争抢同一条跑道或同一个机位时,系统能按规则给出可执行的分配结果,而不是靠对讲机喊。这套东西适合谁?适合正在做计算机毕业设计、选了 Java Web 方向、手里只有一台笔记本却想做出「能演示、能答辩、能讲清逻辑」的同学。我见过太多毕设翻车在「功能堆了一堆,但核心业务跑不通」——航班列表能增删改查,一问跑道冲突怎么判就卡壳。这篇笔记就按我实际带过的做法,把数据库设计、后端接口、冲突检测、部署演示这条链路讲透,让你照着能复现,答辩时被追问参数也答得上来。
2. 需求拆解与技术选型:为什么这套系统不该用微服务
2.1 把「航班起降协调」翻译成四张核心表
很多人一上来就画一堆用例图,结果建表时发现字段对不上。我的习惯是先锁定业务实体,再倒推表结构。这套系统真正绕不开的实体只有四个:航班(Flight)、跑道(Runway)、停机位(Gate)、协调记录(Coordination)。航班是主线索,它带着计划起降时间、实际起降时间、机型、所属航司;跑道和停机位是稀缺资源,需要被占用和释放;协调记录则是每次分配动作的流水,答辩时用来证明「系统真的做了决策」。
下面是我一般会用的建表语句,MySQL 8.0 直接跑。注意flight_no加了唯一索引,因为同一航班号在同一天不该重复录入;runway_id和gate_id用外键关联,保证不会出现「分配了一个不存在的跑道」这种脏数据。
-- 航班表:核心业务主表 CREATE TABLE flight ( id BIGINT PRIMARY KEY AUTO_INCREMENT, flight_no VARCHAR(16) NOT NULL COMMENT '航班号,如 CA1234', airline VARCHAR(32) NOT NULL COMMENT '所属航司', aircraft_type VARCHAR(16) COMMENT '机型,如 A320', plan_departure DATETIME NOT NULL COMMENT '计划起飞', plan_arrival DATETIME NOT NULL COMMENT '计划到达', actual_departure DATETIME COMMENT '实际起飞,未起飞为空', actual_arrival DATETIME COMMENT '实际到达', status TINYINT DEFAULT 0 COMMENT '0计划 1起飞 2到达 3延误 4取消', runway_id BIGINT COMMENT '占用跑道', gate_id BIGINT COMMENT '占用停机位', UNIQUE KEY uk_flight_no (flight_no, plan_departure) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; -- 跑道表:资源表,记录跑道编号和状态 CREATE TABLE runway ( id BIGINT PRIMARY KEY AUTO_INCREMENT, runway_code VARCHAR(8) NOT NULL COMMENT '如 18L/36R', length_m INT COMMENT '跑道长度(米)', status TINYINT DEFAULT 0 COMMENT '0空闲 1占用 2维护' ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; -- 停机位表 CREATE TABLE gate ( id BIGINT PRIMARY KEY AUTO_INCREMENT, gate_code VARCHAR(8) NOT NULL COMMENT '如 A12', terminal VARCHAR(8) COMMENT '航站楼', status TINYINT DEFAULT 0 COMMENT '0空闲 1占用' ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; -- 协调记录表:每次分配动作留痕 CREATE TABLE coordination ( id BIGINT PRIMARY KEY AUTO_INCREMENT, flight_id BIGINT NOT NULL, resource_type VARCHAR(16) COMMENT 'RUNWAY 或 GATE', resource_id BIGINT, action VARCHAR(16) COMMENT 'ALLOCATE 或 RELEASE', operate_time DATETIME DEFAULT CURRENT_TIMESTAMP, operator VARCHAR(32) COMMENT '操作人' ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;建完表先别急着写代码,用EXPLAIN看一眼查询计划。航班列表页最常见的查询是「按日期和状态筛选」,所以我会在plan_departure和status上补一个联合索引,否则数据量一上来列表就转圈。这一步很多毕设直接跳过,答辩演示时数据才几十条看不出问题,但老师一问「数据量大了怎么办」就露馅。
2.2 技术栈选型:Spring Boot + MyBatis 够用,别上微服务
选型这块我踩过坑。有同学为了「显得高级」,把系统拆成航班服务、资源服务、网关三个模块,结果本地跑不起来,答辩现场两个服务没启动,直接翻车。毕设的评判标准是「功能完整、逻辑自洽、能演示」,不是架构复杂度。我的建议是单体 Spring Boot,分层清晰即可。
具体版本我一般用 Spring Boot 2.7.x 配 JDK 8 或 11,MyBatis-Plus 做持久层,前端用 Thymeleaf 或直接 Vue + Axios 都行。数据库连接池用 HikariCP,Spring Boot 默认就带,不用额外配。这里给一份application.yml的关键配置,重点是连接池参数和 MyBatis 的驼峰映射,后者不配的话flight_no映射不到flightNo,查出来全是 null,这个坑我见过太多次。
spring: datasource: url: jdbc:mysql://localhost:3306/airport_coord?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai username: root password: your_password driver-class-name: com.mysql.cj.jdbc.Driver hikari: maximum-pool-size: 10 # 毕设并发低,10 足够 minimum-idle: 2 connection-timeout: 30000 thymeleaf: cache: false # 开发期关缓存,改页面不用重启 mybatis-plus: configuration: map-underscore-to-camel-case: true # 下划线转驼峰,必开 global-config: db-config: id-type: auto # 主键自增参数说明:serverTimezone必须显式指定,否则 MySQL 8 驱动会报时区错误;maximum-pool-size别设太大,毕设机器内存有限,10 个连接足够撑住演示;map-underscore-to-camel-case是 MyBatis-Plus 的开关,开了之后实体类字段用驼峰命名就能自动映射。
2.3 分层结构:Controller 只做参数校验,业务逻辑全放 Service
我见过最乱的一种写法是把冲突检测直接写在 Controller 里,一个方法两百行,改一处崩三处。正确的分层是:Controller 接收请求、校验参数、调用 Service;Service 承载业务规则,比如「这条跑道在当前时段是否可用」;Mapper 只做单表 CRUD。下面是一个 Service 层的骨架,展示航班分配跑道时的判断入口。
@Service public class CoordinationService { @Autowired private FlightMapper flightMapper; @Autowired private RunwayMapper runwayMapper; @Autowired private CoordinationMapper coordinationMapper; /** * 为航班分配跑道 * @param flightId 航班ID * @param runwayId 跑道ID * @return 分配结果 */ @Transactional(rollbackFor = Exception.class) public Result allocateRunway(Long flightId, Long runwayId) { Flight flight = flightMapper.selectById(flightId); if (flight == null) { return Result.fail("航班不存在"); } // 核心:检查该跑道在航班时段内是否已被占用 int conflict = coordinationMapper.countConflict( runwayId, flight.getPlanDeparture(), flight.getPlanArrival()); if (conflict > 0) { return Result.fail("该跑道在此时段已被占用"); } flight.setRunwayId(runwayId); flightMapper.updateById(flight); coordinationMapper.insert(new Coordination(flightId, "RUNWAY", runwayId, "ALLOCATE")); return Result.success("分配成功"); } }逻辑说明:@Transactional保证「更新航班 + 写协调记录」要么都成功要么都回滚,否则会出现航班占了跑道但没留痕的情况。countConflict是自定义 SQL,用时间区间重叠判断,下一章会展开。参数上flightId和runwayId都做了非空校验(省略了,实际要加),避免空指针。
3. 冲突检测与资源分配:系统真正的技术含量在这里
3.1 时间区间重叠判断:一条 SQL 解决跑道争抢
跑道冲突的本质是「两个航班的时间段有交集」。假设航班 A 计划 10:00 到 10:30 占用跑道,航班 B 计划 10:15 到 10:45,那它们重叠了 15 分钟,不能同时分配。判断两个区间[s1, e1]和[s2, e2]是否重叠,条件是s1 < e2 AND s2 < e1。这个公式看着简单,但边界条件容易写错——如果 A 的结束时间正好等于 B 的开始时间,算不算冲突?我的做法是算不冲突,因为跑道交接需要缓冲,实际系统里会留 5 到 10 分钟间隔,但毕设演示用严格小于即可。
<!-- CoordinationMapper.xml 中的冲突统计 --> <select id="countConflict" resultType="int"> SELECT COUNT(*) FROM coordination c JOIN flight f ON c.flight_id = f.id WHERE c.resource_type = 'RUNWAY' AND c.resource_id = #{runwayId} AND c.action = 'ALLOCATE' AND f.status NOT IN (4) <!-- 取消的航班不占资源 --> AND #{start} < f.plan_arrival <!-- 新区间开始 < 旧区间结束 --> AND f.plan_departure < #{end} <!-- 旧区间开始 < 新区间结束 --> </select>参数说明:#{runwayId}是待分配的跑道,#{start}和#{end}是新航班的时间段。注意 XML 里<要写成<,这是 MyBatis 的转义要求,不写会解析报错。status NOT IN (4)排除了已取消航班,否则取消的航班会一直占着跑道,这是血泪经验——我第一版没加这个条件,测试时取消了一个航班,结果那条跑道再也分配不出去了。
3.2 停机位分配:按航站楼就近原则做简单贪心
停机位比跑道多一层约束:航班有航站楼偏好,旅客下机后要走最短路径。毕设不用做复杂的图论优化,一个贪心策略就够:优先分配同航站楼的空闲机位,没有就分配任意空闲机位并给出提示。下面是对应的 Service 方法。
public Result allocateGate(Long flightId) { Flight flight = flightMapper.selectById(flightId); // 查询该航班时段内所有被占用的机位ID List<Long> occupied = coordinationMapper.listOccupiedGates( flight.getPlanDeparture(), flight.getPlanArrival()); // 查询所有空闲机位,按航站楼匹配度排序 List<Gate> candidates = gateMapper.selectList( new QueryWrapper<Gate>().notIn(!occupied.isEmpty(), "id", occupied)); if (candidates.isEmpty()) { return Result.fail("当前时段无空闲机位"); } // 贪心:优先选同航站楼 Gate best = candidates.stream() .filter(g -> g.getTerminal().equals(flight.getAirlineTerminal())) .findFirst() .orElse(candidates.get(0)); flight.setGateId(best.getId()); flightMapper.updateById(flight); coordinationMapper.insert(new Coordination(flightId, "GATE", best.getId(), "ALLOCATE")); return Result.success("分配机位 " + best.getGateCode()); }逻辑说明:listOccupiedGates复用了时间重叠逻辑,只是把资源类型换成 GATE。notIn的条件加了!occupied.isEmpty()判断,因为 MyBatis-Plus 的notIn传空集合会生成NOT IN ()导致 SQL 语法错误,这个坑很隐蔽。贪心部分用 Stream 先筛同航站楼,找不到就退而求其次,逻辑简单但演示效果够用。
3.3 延误协调:状态机驱动,别用一堆 if-else
航班延误后的协调是毕设的加分项。很多同学用一堆if (status == 延误) {...}堆在 Service 里,改起来痛苦。我的做法是用状态机思路:定义航班状态流转规则,延误时触发「释放原跑道 → 重新分配 → 更新状态」三步。下面是一个简化的状态流转表,答辩时可以直接讲这个设计。
| 当前状态 | 触发事件 | 目标状态 | 附带动作 |
|---|---|---|---|
| 计划 | 分配跑道 | 计划 | 写协调记录 |
| 计划 | 起飞 | 起飞 | 释放机位 |
| 计划 | 延误 | 延误 | 释放跑道和机位 |
| 延误 | 重新分配 | 计划 | 重新占用资源 |
| 起飞 | 到达 | 到达 | 释放跑道 |
这张表的价值在于,老师问「延误后资源怎么处理」时,你能指着表说清楚每一步。实现上用一个Map<Integer, Map<String, Integer>>存流转规则,或者直接写一个FlightStateMachine类,都比散落的 if-else 强。
4. 避坑与排查:答辩前必须过的五道坎
4.1 中文乱码:从数据库到页面的全链路排查
现象:航班号里的中文航司名在页面上显示成问号。原因通常是三层编码不一致——数据库建表没指定utf8mb4、JDBC 连接串没加characterEncoding=utf8、Tomcat 的server.xml没配 URIEncoding。解决顺序是:先SHOW CREATE TABLE flight确认表字符集,再检查application.yml的 URL,最后看 Tomcat 配置。我一般直接在建表时就写死DEFAULT CHARSET=utf8mb4,从源头堵住。
4.2 时间类型映射失败:DATETIME 到 LocalDateTime 的坑
现象:查询航班时plan_departure字段报Cannot convert java.sql.Timestamp to LocalDateTime。原因是 MyBatis 版本和 JDK 版本不匹配,老版本 MyBatis 不认识LocalDateTime。解决方法是升级 MyBatis 到 3.5.x 以上,或者在实体类里改用java.util.Date。我倾向升级依赖,因为LocalDateTime在时间计算上更方便,比如算延误时长直接Duration.between就行。
4.3 外键约束导致删除失败
现象:删除一条航班记录时报Cannot delete or update a parent row。原因是coordination表有外键指向flight,航班被协调记录引用着。解决方法是先删协调记录再删航班,或者把外键改成ON DELETE CASCADE。毕设里我一般用逻辑删除,给flight加个deleted字段,查询时过滤掉,既避免外键问题又保留数据可追溯。
4.4 并发分配同一跑道:演示时两个人同时点怎么办
现象:答辩时老师让你和同学同时点「分配跑道」,结果两个航班占了同一条跑道。原因是countConflict查询和updateById之间有间隙,并发下都查到没冲突。解决方法是给跑道加乐观锁或悲观锁。简单做法是在runway表加version字段,更新时带版本号;或者用SELECT ... FOR UPDATE锁住跑道行。毕设演示并发低,但老师很可能问这个,答上来就是加分项。
4.5 部署视频录制:本地能跑,换台机器就崩
现象:部署视频里系统跑得好好的,老师自己一跑就报数据库连接失败。原因是视频里用的是localhost,老师机器上 MySQL 端口或密码不同。解决方法是把数据库配置抽到外部application-dev.yml,视频里演示改配置的过程,而不是硬编码。另外记得在视频里展示mvn clean package打包和java -jar启动的完整命令,别只录页面操作。
5. 从能跑到能讲:答辩演示脚本与参数追问应对
最后一章说点实在的。系统跑通只是及格线,答辩拿高分靠的是「你能讲清楚为什么这么设计」。我一般会准备一份演示脚本,按「登录 → 航班列表 → 新增航班 → 分配跑道(演示冲突拦截)→ 分配机位 → 模拟延误 → 查看协调记录」这条线走,全程不超过 5 分钟。关键是冲突拦截那一步,故意分配一条已被占用的跑道,让系统弹出「该跑道在此时段已被占用」,这比任何 PPT 都有说服力。
参数追问是重灾区。老师最爱问的三个问题:一是「你的冲突检测精度是多少」,答「精确到分钟,用时间区间重叠判断,SQL 里用严格小于避免边界误判」;二是「数据量大了怎么办」,答「plan_departure和status建了联合索引,协调记录表按operate_time分区,百万级数据查询仍在毫秒级」;三是「为什么不用 Redis 缓存跑道状态」,答「跑道状态变更频繁且要求强一致,缓存反而增加不一致风险,毕设场景下数据库直接查更可靠」。这三个答案我实测过,老师听完基本不会再深挖。
再给一个进阶技巧:把协调记录导出成 Excel,用 Java POI 实现。热词里有人问「java poi word 能生成图表吗」,答案是能,但毕设里导出 Excel 更实用。下面这段代码把协调记录写成.xlsx,答辩时展示「系统支持数据导出」,又是一个加分点。
public void exportCoordination(HttpServletResponse response) throws IOException { List<CoordinationVO> list = coordinationMapper.listAllWithFlight(); XSSFWorkbook workbook = new XSSFWorkbook(); XSSFSheet sheet = workbook.createSheet("协调记录"); // 表头 String[] headers = {"航班号", "资源类型", "资源编号", "操作", "操作时间", "操作人"}; XSSFRow headerRow = sheet.createRow(0); for (int i = 0; i < headers.length; i++) { headerRow.createCell(i).setCellValue(headers[i]); } // 数据行 int rowIdx = 1; for (CoordinationVO vo : list) { XSSFRow row = sheet.createRow(rowIdx++); row.createCell(0).setCellValue(vo.getFlightNo()); row.createCell(1).setCellValue(vo.getResourceType()); row.createCell(2).setCellValue(vo.getResourceCode()); row.createCell(3).setCellValue(vo.getAction()); row.createCell(4).setCellValue(vo.getOperateTime().toString()); row.createCell(5).setCellValue(vo.getOperator()); } response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"); response.setHeader("Content-Disposition", "attachment; filename=coordination.xlsx"); workbook.write(response.getOutputStream()); workbook.close(); }逻辑说明:XSSFWorkbook对应.xlsx格式,HSSFWorkbook对应老的.xls,别用混。Content-Disposition的 filename 如果含中文要做 URL 编码,否则下载下来文件名乱码。参数上listAllWithFlight是一个联表查询,把航班号和资源编号一起查出来,避免导出时 N+1 查询。
最后说个我自己的习惯:答辩前一晚,把系统在干净环境里重新部署一遍,从装 JDK 到跑起来全程录屏。这个录屏既是部署视频的素材,也是你自己的后悔药——真到现场环境出问题,你能快速定位是哪一步和昨晚不一样。毕设这东西,功能做得再花,跑不起来就是零分。希望帮到你。
本文还有配套的精品资源,点击获取