资讯详情

资讯详情

Java课设经典项目:飞机订票系统全栈实现指南

简介本资源是一套完整的软件工程课程设计实践项目——基于Java开发的民航飞机订票系统源码及配套数据库面向高校计算机/软件工程专业学生与初学者解决课程设计中系统分析、模块设计、编码实现与数据库集成等核心实践需求。压缩包共120个文件含30个核心Java源文件、75个编译后Class字节码如BookTicket、FlightInformationDao、VIPRegister等、2个SQL建表与初始化脚本、30个Java类对应的功能模块清晰可溯辅以XML配置、MD说明文档及IDEA/Eclipse工程配置文件整体2.29MB结构规范开箱即用。已有890人学习下载读者可直接导入Eclipse或IDEA运行完整掌握Swing界面开发、JDBC数据库交互、航班查询/订退票/会员管理/延误处理等典型业务逻辑实现同时获得可扩展的分层架构参考与真实课程设计交付物范例。1. 为什么一个“飞机订票系统”源码能撑起软件工程课程设计的半壁江山不是所有Java课设都叫“飞机订票系统”但几乎所有认真带过课设的老师都会在开题阶段把这六个字写进任务书第一行。它不像“学生成绩管理系统”那样泛滥到失去辨识度也不像“智能停车场”那样依赖硬件仿真——它天然具备业务闭环、分层清晰、数据强关联、边界可收敛四大特质用户注册/登录 → 查询航班 → 选座下单 → 支付确认 → 订单管理 → 后台航班调度每一步都能映射到软件工程核心知识模块UML建模用例图/类图/时序图、MVC分层Controller-Service-Dao、数据库范式乘客-航班-订单-座位四表关联、事务控制出票失败必须回滚、并发安全同一座位被多人抢购。我带过7届本科生课设92%的优秀作业都从这个系统起步——不是因为它简单而是因为它能把“软件工程”从课本名词变成你亲手调试过的日志、改过三次的ER图、和凌晨三点死锁时抓耳挠腮的真实体验。适合刚学完Java基础MySQLServlet/JSP的同学也适合作为Spring Boot入门前的“最后一道手工焊点”。2. 从零跑通用最简结构还原民航订票系统的骨架2.1 数据库建模先画清“人-机-票-座”四张表的血缘关系民航订票系统的核心不是代码是数据关系。很多同学一上来就写LoginServlet结果做到订单查询时发现查不到用户历史订单因为没建外键改不了航班状态因为没设航班与座位的级联更新。我们按GB/T 39788‑2021《系统与软件工程 性能测试方法》中“业务实体优先”原则先定四张主表表名主键关键外键业务约束passenger乘客id自增—id_card唯一索引防重复注册flight航班flight_no如CA1234—departure_timearrival_time非空seat座位id自增flight_no→flight.flight_noseat_no如12Aflight_no联合唯一order_info订单order_idUUIDpassenger_id→passenger.idseat_id→seat.idstatus枚举已支付,已取消,待支付提示别用int做订单ID生产环境必须用UUID或雪花算法ID否则高并发下ID冲突会导致订单覆盖。课设阶段可用UUID.randomUUID().toString().replace(-,)生成16位字符串。建表SQL示例MySQL 5.7-- 乘客表 CREATE TABLE passenger ( id INT PRIMARY KEY AUTO_INCREMENT, username VARCHAR(20) NOT NULL UNIQUE, password VARCHAR(64) NOT NULL, -- 存BCrypt加密后的密文 id_card CHAR(18) NOT NULL UNIQUE, phone VARCHAR(11), created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ); -- 航班表简化版实际需加航空公司、机型等 CREATE TABLE flight ( flight_no VARCHAR(10) PRIMARY KEY, departure_city VARCHAR(20) NOT NULL, arrival_city VARCHAR(20) NOT NULL, departure_time DATETIME NOT NULL, arrival_time DATETIME NOT NULL, price DECIMAL(8,2) NOT NULL, status ENUM(正常,延误,取消) DEFAULT 正常 ); -- 座位表关键座位属于某航班且不可跨航班复用 CREATE TABLE seat ( id INT PRIMARY KEY AUTO_INCREMENT, flight_no VARCHAR(10) NOT NULL, seat_no VARCHAR(10) NOT NULL, -- 如 12A, 经济舱-05C class_type ENUM(头等舱,商务舱,经济舱) DEFAULT 经济舱, price_multiplier DECIMAL(3,2) DEFAULT 1.0, -- 相对于航班基准价的倍率 is_booked BOOLEAN DEFAULT FALSE, FOREIGN KEY (flight_no) REFERENCES flight(flight_no) ON DELETE CASCADE ); -- 订单表核心业务聚合点 CREATE TABLE order_info ( order_id VARCHAR(32) PRIMARY KEY, passenger_id INT NOT NULL, seat_id INT NOT NULL, order_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP, status ENUM(待支付,已支付,已取消,已退票) DEFAULT 待支付, pay_amount DECIMAL(8,2) NOT NULL, FOREIGN KEY (passenger_id) REFERENCES passenger(id) ON DELETE CASCADE, FOREIGN KEY (seat_id) REFERENCES seat(id) ON DELETE RESTRICT );为什么这样设计seat表不直接存flight_noseat_no作为联合主键而用自增id是为了让order_info.seat_id能简洁引用——避免订单表出现复合外键MySQL对复合外键支持弱且JDBC映射麻烦order_info中pay_amount单独存而非实时计算flight.price * seat.price_multiplier是因为价格可能变动订单必须锁定成交价seat.is_booked字段看似冗余可通过order_info反查但查询“某航班剩余座位数”时直接COUNT WHERE is_bookedFALSE比LEFT JOIN order_info ON ... WHERE order_id IS NULL快3个数量级——这是课设里最容易被忽略的性能坑。2.2 Java分层实现用原生JDBCServlet搭出可调试的MVC骨架别急着上Spring Boot。课设阶段用原生JDBCServlet才能看清每一层的职责边界。我要求学生手写以下5个核心类DBUtil.java封装Connection获取与释放务必用try-with-resourcesPassengerDao.java只负责passenger表的CRUD不包含任何业务逻辑FlightService.java提供getAvailableFlights()等业务方法内部调用多个DaoOrderServlet.java接收HTTP请求校验参数调用Service返回JSON或跳转JSPOrderConfirm.jsp纯展示层只用EL表达式取值禁止写Java代码块关键代码片段订单创建逻辑// OrderService.java public boolean createOrder(int passengerId, int seatId, BigDecimal payAmount) { String sql INSERT INTO order_info(order_id, passenger_id, seat_id, pay_amount) VALUES (?, ?, ?, ?); try (Connection conn DBUtil.getConnection(); PreparedStatement ps conn.prepareStatement(sql)) { // 生成订单ID课设阶段用时间戳随机数足够 String orderId System.currentTimeMillis() new Random().nextInt(1000); ps.setString(1, orderId); ps.setInt(2, passengerId); ps.setInt(3, seatId); ps.setBigDecimal(4, payAmount); // 关键开启事务否则座位锁定失效 conn.setAutoCommit(false); int rows ps.executeUpdate(); // 更新座位状态必须在同一事务内 updateSeatStatus(seatId, true, conn); // set is_booked TRUE conn.commit(); return rows 1; } catch (SQLException e) { rollbackQuietly(conn); // 自定义回滚方法 throw new RuntimeException(订单创建失败, e); } } private void updateSeatStatus(int seatId, boolean booked, Connection conn) throws SQLException { String sql UPDATE seat SET is_booked ? WHERE id ?; try (PreparedStatement ps conn.prepareStatement(sql)) { ps.setBoolean(1, booked); ps.setInt(2, seatId); ps.executeUpdate(); } }参数说明与踩坑预警conn.setAutoCommit(false)必须在PreparedStatement创建之前调用否则事务无效updateSeatStatus()必须传入conn对象确保和插入订单使用同一连接——这是JDBC事务生效的前提orderId生成方式在课设中够用但绝对不能用于生产环境时间戳随机数有碰撞风险且无全局唯一性rollbackQuietly()要捕获SQLException并吞掉避免二次异常但需记录日志——课设阶段可用System.err.println(事务回滚 e.getMessage())。2.3 前端交互用原生HTMLAJAX实现“查航班→选座→下单”三步链路很多同学把前端写成静态页面结果老师问“怎么体现‘实时座位图’”——答案不在CSS动画而在AJAX轮询服务端状态同步。我们用最简方案searchFlight.jsp输入出发地/目的地提交后AJAX调用/servlet/FlightSearchServlet返回JSON格式航班列表含flight_no,departure_time,price点击某航班 → 加载seatSelect.jsp?flightNoCA1234该页面用AJAX请求/servlet/SeatListServlet?flightNoCA1234SeatListServlet返回JSON座位数组[{seatNo:1A,class:头等舱,isBooked:false},{seatNo:1B,isBooked:true},...]前端用div classseat>// 加载座位时触发 function loadSeats(flightNo) { fetch(/servlet/SeatListServlet?flightNo${flightNo}) .then(res res.json()) .then(data { const container document.getElementById(seat-grid); container.innerHTML ; data.forEach(seat { const div document.createElement(div); div.className seat ${seat.isBooked ? booked : available}; div.textContent seat.seatNo; if (!seat.isBooked) { div.onclick () selectSeat(seat.id, seat.seatNo, seat.price); } container.appendChild(div); }); }); } // 选座后提交订单注意此处省略价格计算实际需从seat.id查price function selectSeat(seatId, seatNo, price) { const passengerId % session.getAttribute(passengerId) %; // JSP中取session fetch(/servlet/OrderCreateServlet, { method: POST, headers: {Content-Type: application/x-www-form-urlencoded}, body: passengerId${passengerId}seatId${seatId}payAmount${price} }) .then(res res.json()) .then(data { if (data.success) { alert(订单创建成功订单号${data.orderId}); location.href orderSuccess.jsp?orderId data.orderId; } else { alert(下单失败 data.message); } }); }为什么不用框架jQuery等库会掩盖HTTP请求本质而课设考察的是“能否理解前后端数据契约”fetchAPI足够轻量且现代浏览器兼容性好Chrome 42/Firefox 39>String lockSql SELECT id FROM seat WHERE id ? AND is_booked FALSE FOR UPDATE; try (PreparedStatement ps conn.prepareStatement(lockSql)) { ps.setInt(1, seatId); ResultSet rs ps.executeQuery(); // 此处会阻塞其他线程直到本事务结束 if (!rs.next()) { throw new RuntimeException(座位已被占用); } }确保conn.setAutoCommit(false)在lockSql执行前设置测试时用Chrome开两个隐身窗口同时点击同一座位——应有一个窗口报“座位已被占用”。3.3 现象订单支付成功后order_info.status仍是“待支付”原因OrderCreateServlet中调用OrderService.createOrder()后未处理返回值或Service层抛出异常但Servlet未捕获导致HTTP响应返回200但数据库未更新解决Servlet必须try-catchService异常并返回明确JSONtry { boolean success orderService.createOrder(passengerId, seatId, payAmount); response.getWriter().write({\success\: success }); } catch (Exception e) { response.setStatus(500); response.getWriter().write({\success\:false,\message\:\ e.getMessage() \}); }前端AJAX必须检查res.status非仅res.ok500错误需提示用户重试在数据库中手动UPDATE order_info SET status已支付 WHERE order_idxxx验证排除SQL语法错误。3.4 现象JSP页面中文乱码显示“???”原因Tomcat默认用ISO-8859-1解码GET参数而页面声明了UTF-8解决在web.xml中配置过滤器课设必备filter filter-nameCharacterEncodingFilter/filter-name filter-classorg.apache.catalina.filters.SetCharacterEncodingFilter/filter-class init-param param-nameencoding/param-name param-valueUTF-8/param-value /init-param /filter filter-mapping filter-nameCharacterEncodingFilter/filter-name url-pattern/*/url-pattern /filter-mapping若用POST提交还需在Servlet开头加request.setCharacterEncoding(UTF-8)检查MySQL连接URL是否含useUnicodetruecharacterEncodingUTF-8如jdbc:mysql://localhost:3306/airline?useUnicodetruecharacterEncodingUTF-8。3.5 现象部署到Tomcat后404但IDE内运行正常原因项目结构不符合Servlet规范或web.xml中servlet-mapping路径与实际URL不匹配解决确认src/main/webapp/WEB-INF/web.xml存在且servlet-class全限定名正确如com.airline.servlet.OrderCreateServlet检查url-pattern是否带/前缀如/servlet/OrderCreateServlet而非servlet/OrderCreateServlet在Tomcatlogs/catalina.out中搜索SEVERE关键字定位类加载失败原因常见缺少mysql-connector-java.jar在WEB-INF/lib下用curl -v http://localhost:8080/your-app-name/servlet/FlightSearchServlet直连测试排除前端JS干扰。4. 数据库进阶用MyBatis-Plus快速生成CRUD但绝不放弃手写SQL的能力当课设进入中期手写DAO会成为效率瓶颈。此时引入MyBatis-Plus非必须但强烈推荐目标不是替代JDBC而是解放重复劳动让你专注业务逻辑。关键原则MyBatis-Plus只生成基础CRUD复杂查询如“查询某用户所有未完成订单及对应航班信息”必须手写XML SQL。4.1 用MyBatis-Plus Generator一键生成Entity/Dao/Mapper下载 mybatis-plus-generator 3.5.3配置generatorConfig.javaFastAutoGenerator.create(jdbc:mysql://localhost:3306/airline?useUnicodetruecharacterEncodingUTF-8, root, password) .globalConfig(builder - builder .author(student) // 作者名 .outputDir(src/main/java) // 输出目录 .disableOpenDir() // 生成后不自动打开文件夹 ) .packageConfig(builder - builder .parent(com.airline) // 包名前缀 .moduleName(system) // 模块名 .entity(entity) .mapper(mapper) .xml(mapper.xml) ) .strategyConfig(builder - builder .addInclude(passenger, flight, seat, order_info) // 只生成这四张表 .entityBuilder() .enableLombok() // 用Lombok简化getter/setter .logicDeleteColumnName(deleted) // 若有软删除字段 .mapperBuilder() .enableMapperAnnotation() // 生成Mapper注解 .serviceBuilder() .formatServiceFileName(%sService) // 接口名格式 ) .execute();运行后生成Passenger.javaLombok注解含TableName(passenger)PassengerMapper.java继承BaseMapperPassenger自带selectById等方法PassengerMapper.xml空文件留作手写SQL为什么保留XML因为MyBatis-Plus的QueryWrapper在多表关联时极易写出N1查询如orderService.list().forEach(o - flightService.getById(o.getFlightId()))而手写XML可精准控制resultMap和association一次SQL查出订单航班乘客全信息。4.2 手写高性能关联查询用resultMap解决N1问题需求在订单管理页显示“订单号、乘客姓名、航班号、出发时间、座位号”。若用MyBatis-Plus链式调用需查order_info→ 循环查passenger→ 循环查flight→ 循环查seat4次SQL。改用XML一次查出!-- OrderMapper.xml -- select idselectOrderWithDetails resultTypecom.airline.entity.vo.OrderDetailVO SELECT o.order_id, p.username AS passengerName, f.flight_no, f.departure_time, s.seat_no FROM order_info o JOIN passenger p ON o.passenger_id p.id JOIN seat s ON o.seat_id s.id JOIN flight f ON s.flight_no f.flight_no WHERE o.passenger_id #{passengerId} /select对应VO类非Entity专为查询设计Data // Lombok public class OrderDetailVO { private String orderId; private String passengerName; private String flightNo; private LocalDateTime departureTime; private String seatNo; }参数说明resultType指向VO类避免用Map导致类型不安全#{passengerId}用#而非$防止SQL注入LocalDateTime需在mybatis-config.xml中配置类型处理器课设可先用String代替后续再升级此SQL在MySQL中执行计划应显示typeALL不超过2次即最多全表扫描2张表用EXPLAIN验证。4.3 数据库同步课设阶段用mysqldump但必须理解binlog原理课设无需集群但老师常问“如何保证开发库和演示库数据一致”答案不是“复制粘贴”而是标准化导出导入流程# 导出结构数据课设够用 mysqldump -u root -p --databases airline airline_backup.sql # 导入到另一台机器先创建database airline mysql -u root -p airline airline_backup.sql # 若只要数据结构已存在加--no-create-info mysqldump -u root -p --no-create-info airline passenger flight data_only.sql为什么不用GUI工具mysqldump命令可写入README.md成为课设文档一部分--single-transaction参数能保证导出时数据一致性InnoDB引擎这是Navicat等工具未必默认启用的当老师问“如何做增量备份”你能答出mysqlbinlog --start-datetime2024-01-01 00:00:00 /var/lib/mysql/mysql-bin.000001——这比说“用XX软件同步”专业十倍。5. 课设答辩前必做的三件事让代码从“能跑”变成“值得讲”5.1 给每个Servlet加日志埋点用Log4j2输出可追溯的执行链路别用System.out.println()——答辩时老师问“用户下单时系统做了什么”你不能背代码。用Log4j2记录关键路径!-- log4j2.xml -- Configuration statusWARN Appenders File nameFileAppender fileNamelogs/airline.log PatternLayout pattern%d{HH:mm:ss.SSS} [%t] %-5level %logger{36} - %msg%n/ /File /Appenders Loggers Root levelinfo AppenderRef refFileAppender/ /Root /Loggers /Configuration在OrderCreateServlet中private static final Logger logger LogManager.getLogger(OrderCreateServlet.class); protected void doPost(HttpServletRequest request, HttpServletResponse response) { logger.info(订单创建请求开始参数passengerId{}, seatId{}, request.getParameter(passengerId), request.getParameter(seatId)); try { boolean success orderService.createOrder(...); logger.info(订单创建成功订单号{}, orderId); // ...返回响应 } catch (Exception e) { logger.error(订单创建失败, e); // 自动打印堆栈 // ...错误处理 } }答辩价值展示logs/airline.log文件指出“用户123下单时系统执行了3次SQL耗时217ms”当老师质疑“并发是否真能控制”你可打开日志找两笔相邻订单证明SELECT ... FOR UPDATE生效第二笔日志时间戳比第一笔晚500ms以上日志文件本身可作为课设附件证明你理解“可观测性”这一工程实践。5.2 用Postman做接口测试生成可演示的API文档课设不是黑盒系统。用Postman保存5个核心请求请求名方法URL参数预期响应查询航班GET/servlet/FlightSearchServletdepartureCity北京arrivalCity上海JSON数组含3个航班获取座位GET/servlet/SeatListServletflightNoCA1234JSON数组含50个座位其中2个isBookedtrue创建订单POST/servlet/OrderCreateServletpassengerId1seatId123payAmount1200.00{success:true,orderId:20240520123456}查询订单GET/servlet/OrderQueryServletorderId20240520123456完整订单详情JSON取消订单POST/servlet/OrderCancelServletorderId20240520123456{success:true}操作要点在Postman中为每个请求写明Description描述业务场景如“模拟用户张三购买CA1234航班12A座位”用Tests脚本验证响应pm.test(状态码200, function () { pm.response.to.have.status(200); });导出Collection为airline-api.postman_collection.json答辩时用Postman一键演示全流程这比“打开浏览器点点点”更体现工程素养——你知道API是契约不是界面。5.3 手绘一张部署架构图标注技术选型理由别交Visio画的精美图。用白纸手绘拍照插入报告只画三层┌─────────────────┐ ┌──────────────────┐ ┌──────────────────┐ │ Chrome浏览器 │───▶│ Tomcat 9.0.83 │───▶│ MySQL 5.7.42 │ │ (HTTP请求/响应) │ │ (Servlet容器) │ │ (InnoDB引擎) │ └─────────────────┘ └────────┬─────────┘ └──────────────────┘ │ ┌───────────▼───────────┐ │ Java代码分层结构 │ │ Controller → Service → Dao │ └───────────────────────┘在图旁手写三句话理由选Tomcat不选Jetty因课程教材用Tomcat且其webapps目录结构最直观便于理解WAR包部署选MySQL不选SQLite因课设要求“多用户并发”SQLite不支持真正的并发写入而MySQL的InnoDB行锁可满足不用Spring Boot因课设目标是掌握Servlet生命周期Spring Boot的自动配置会掩盖init()/service()/destroy()方法调用时机。这是我带课设十年养成的习惯所有技术选型必须能一句话说清“为什么是它而不是别的”。答辩时老师盯着这张图问“为什么不用Redis缓存航班数据”你答“课设范围是单机部署且航班数据变更频率低每日10次加缓存反而增加复杂度违背KISS原则。”——这句话比写一百行代码更有分量。最后想说这个系统不会让你成为架构师但它会给你一个锚点——当你未来看到“分布式事务”“最终一致性”这些词时能立刻想起当年为一个座位锁写的那行SELECT ... FOR UPDATE。那种“原来如此”的顿悟感就是软件工程最真实的馈赠。希望帮到你。本文还有配套的精品资源点击获取
觉得有用,分享给同行:

为您的企业打造数字门面

稳重轻奢商务风格,端正雅致视觉,长效耐看不易过时。

立即咨询 →