简介:这是一份面向JavaWeb课程设计与初学者练手的航空系统完整源码,采用Java为核心、JavaWeb技术栈构建,前端以HTML、CSS和JavaScript实现用户交互界面,后端处理航班管理、订票、值机等业务逻辑,适合作为课程设计参考或JavaWeb入门实战项目。压缩包共120个文件,约2.46MB,其中81个Java源文件承担核心业务逻辑,14个HTML页面与1个CSS、1个JS文件构成前端界面,另有14张JPG图片用于视觉装饰,并包含SQL数据库文件、Maven构建文件、XML与YAML配置及Git忽略文件,覆盖从后端处理、前端展示到数据库设计与项目配置的完整链路。目前已有342人学习下载。通过该资源可掌握JavaWeb项目的目录组织方式、Maven依赖管理、数据库表结构设计以及前后端交互的实现思路,便于快速搭建可运行的航空系统原型并在此基础上二次开发。
1. 航空票务系统从零到一:为什么 JavaWeb 依然是课程设计的最优解
如果你正在找一套能跑通、能改、能写进简历的航空系统项目,大概率绕不开 JavaWeb 这条技术路线。航空票务系统本质上是一个典型的 B/S 架构信息管理系统,核心业务包括航班查询、余票管理、订单生成、用户权限控制这几块。它不像电商那么复杂,但业务闭环完整,正好卡在“能学到东西”和“能做完”之间的甜点区。我见过太多人一上来就冲 Spring Cloud 微服务,结果光环境配置就耗掉两周,最后连一个能演示的登录页面都没跑起来。航空系统这个题目,用 JSP + Servlet + MySQL 这套经典组合,反而能在有限时间内把业务逻辑吃透。这篇文章面向的是要做课程设计、毕业设计或者想拿一个完整项目练手的开发者,我会把从数据库设计到前端页面渲染的完整链路拆开讲,包括那些只有真正跑过项目才知道的坑。
2. 航空系统 JavaWeb 架构选型:JSP+Servlet 还是直接上框架
2.1 为什么航空票务场景适合经典三层架构
航空票务系统的业务特征决定了它不需要过度设计。航班信息查询是典型的读多写少,订单创建涉及简单的库存扣减,用户模块就是标准的增删改查。这种业务密度下,JSP + Servlet + JDBC 的三层架构(表现层、业务逻辑层、数据访问层)完全够用,而且每一层的职责边界非常清晰。
表现层用 JSP 负责页面渲染和表单提交,Servlet 作为控制器接收请求、调用业务方法、转发视图。业务逻辑层用普通的 Java 类封装航班查询、订单处理、用户认证这些核心流程。数据访问层用 JDBC 直接操作 MySQL,或者用 DBUtils 做轻量封装。这套结构的好处是调试链路短,出错了从 Servlet 到 Service 到 DAO 一层层跟下去,问题定位很快。
我一般会建议把包结构按功能模块划分,而不是按技术分层。比如com.aviation.flight、com.aviation.order、com.aviation.user,每个模块下面再分dao、service、servlet。这样改一个功能只需要在一个包里面找,不用在三个大包之间来回跳。航空系统里航班模块和订单模块的耦合点主要在余票扣减,把这两个模块的接口约定好,并行开发完全没问题。
数据库设计是航空系统的地基。核心表就四张:flight(航班信息)、ticket(票务库存)、order(订单)、user(用户)。航班表里放航班号、起飞到达城市、起降时间、机型、票价。票务表关联航班 ID,记录舱位等级和剩余数量。订单表关联用户和航班,记录订单状态和创建时间。这里有个容易翻车的地方:余票扣减必须用数据库行锁或者乐观锁,否则并发下单会出现超卖。课程设计演示时可能只有你一个人点,但面试官看到你考虑了并发问题,印象分直接拉满。
2.2 用 IDEA 跑通第一个 Servlet 的最小配置
很多人卡在第一步不是代码写不出来,而是 IDEA 里 Tomcat 跑不起来。下面这套配置流程我用了不下二十次,照着走基本不会出问题。
首先在 IDEA 里新建一个 Java Enterprise 项目,勾选 Web Application,确保生成web/WEB-INF/web.xml。然后在项目结构里配置 Artifacts,添加 Web Application: Exploded,输出目录设成target/项目名。接着在 Run/Debug Configurations 里添加 Tomcat Local,Deployment 选项卡里把刚才的 Artifact 加进去,Application context 设成/aviation。
// web.xml 中配置 Servlet 映射 <servlet> <servlet-name>FlightSearchServlet</servlet-name> <servlet-class>com.aviation.flight.servlet.FlightSearchServlet</servlet-class> </servlet> <servlet-mapping> <servlet-name>FlightSearchServlet</servlet-name> <url-pattern>/flight/search</url-pattern> </servlet-mapping>这段配置的作用是把/flight/search这个 URL 映射到FlightSearchServlet类。<servlet-name>是自定义标识,<servlet-class>必须写全限定类名,<url-pattern>决定浏览器访问路径。注意url-pattern如果写成/*会拦截所有请求包括静态资源,航空系统里 JSP 页面和 CSS 文件都会被吞掉,页面直接白屏。
// FlightSearchServlet.java 核心 doGet 方法 protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { String departure = request.getParameter("departure"); String arrival = request.getParameter("arrival"); // 调用 Service 层查询航班列表 FlightService flightService = new FlightService(); List<Flight> flights = flightService.searchFlights(departure, arrival); request.setAttribute("flightList", flights); // 转发到 JSP 页面渲染 request.getRequestDispatcher("/flight/list.jsp").forward(request, response); }request.getParameter获取前端表单提交的出发地和目的地参数。flightService.searchFlights返回航班列表,通过request.setAttribute存进请求域。forward是服务器内部转发,浏览器地址栏不变,JSP 页面可以直接用 EL 表达式${flightList}拿到数据。这里注意forward和redirect的区别:查询操作适合 forward,下单成功后跳转适合 redirect,否则刷新页面会重复提交订单。
提示:Tomcat 10 之后 Servlet API 包名从
javax.servlet变成了jakarta.servlet,如果你从网上复制的代码报 “cannot resolve symbol servlet”,先检查 Tomcat 版本和 import 语句是否匹配。
3. 航空系统数据库设计与 JDBC 操作:从建表到余票扣减
3.1 四张核心表的字段设计与索引策略
航空系统的数据库设计直接决定了后面代码好不好写。我见过有人把航班信息和票务库存塞在一张表里,结果查询航班时要锁整张表,并发一上来就崩。正确的做法是拆开,航班表存静态信息,票务表存动态库存。
-- 航班信息表 CREATE TABLE flight ( id INT PRIMARY KEY AUTO_INCREMENT, flight_no VARCHAR(10) NOT NULL COMMENT '航班号', departure_city VARCHAR(50) NOT NULL, arrival_city VARCHAR(50) NOT NULL, departure_time DATETIME NOT NULL, arrival_time DATETIME NOT NULL, price DECIMAL(10,2) NOT NULL, INDEX idx_route (departure_city, arrival_city) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; -- 票务库存表 CREATE TABLE ticket ( id INT PRIMARY KEY AUTO_INCREMENT, flight_id INT NOT NULL, cabin_class VARCHAR(20) DEFAULT '经济舱', remaining INT NOT NULL DEFAULT 0, version INT DEFAULT 0 COMMENT '乐观锁版本号', FOREIGN KEY (flight_id) REFERENCES flight(id) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;idx_route索引建在出发城市和到达城市的组合上,因为航空系统最频繁的查询就是“从 A 到 B 有哪些航班”。没有这个索引,数据量到几万条时查询会明显变慢。ticket表里的version字段是乐观锁的关键,每次扣减余票时检查版本号是否变化,防止并发超卖。
订单表需要记录订单状态,我一般用status字段:0 表示待支付,1 表示已支付,2 表示已取消。用户表里密码必须加密存储,用 MD5 加盐或者 BCrypt 都行,千万别明文存。课程设计里经常被忽略的是外键约束,加上FOREIGN KEY之后删除航班时如果有订单关联会报错,这其实是保护机制,但演示时要注意先删订单再删航班。
3.2 余票扣减的乐观锁实现与 JDBC 批量操作
余票扣减是航空系统里唯一有技术含量的并发点。假设两个用户同时下单最后一张票,没有锁的话两个订单都会创建成功,但库存变成 -1。解决方案有两种:悲观锁用SELECT ... FOR UPDATE锁住行,乐观锁用版本号比对。
// 乐观锁扣减余票 public boolean deductTicket(int flightId, int quantity) { String querySql = "SELECT remaining, version FROM ticket WHERE flight_id = ?"; String updateSql = "UPDATE ticket SET remaining = remaining - ?, version = version + 1 " + "WHERE flight_id = ? AND version = ? AND remaining >= ?"; try (Connection conn = DBUtil.getConnection()) { conn.setAutoCommit(false); // 查询当前库存和版本号 int currentVersion; try (PreparedStatement ps = conn.prepareStatement(querySql)) { ps.setInt(1, flightId); ResultSet rs = ps.executeQuery(); if (!rs.next()) return false; currentVersion = rs.getInt("version"); } // 带版本号条件更新 try (PreparedStatement ps = conn.prepareStatement(updateSql)) { ps.setInt(1, quantity); ps.setInt(2, flightId); ps.setInt(3, currentVersion); ps.setInt(4, quantity); int rows = ps.executeUpdate(); if (rows > 0) { conn.commit(); return true; } else { conn.rollback(); return false; // 版本冲突或库存不足 } } } catch (SQLException e) { e.printStackTrace(); return false; } }updateSql里的WHERE version = ?是乐观锁的核心,如果另一个线程已经修改过这条记录,版本号变了,更新影响行数为 0,事务回滚。remaining >= ?保证库存不足时不会扣成负数。conn.setAutoCommit(false)开启事务,扣减成功才提交。这套逻辑在课程设计答辩时能讲清楚,基本就是加分项。
批量查询航班时用PreparedStatement.addBatch()和executeBatch()可以减少数据库往返次数。比如管理员批量导入航班数据,逐条插入可能要几秒,批量执行通常在一秒内完成。注意批量操作要设置合理的批次大小,一次提交 500 条左右比较稳妥,太大容易内存溢出。
注意:JDBC 连接一定要用 try-with-resources 或者 finally 块关闭,否则连接池耗尽后整个系统会卡死。我见过一个项目就是因为 Servlet 里忘了关 Connection,演示到一半所有页面都转圈。
4. 前端页面实现与前后端数据交互:JSP 渲染和表单传参
4.1 航班列表页的 EL 表达式与 JSTL 循环渲染
航空系统的前端不需要 React 或 Vue,JSP 配合 JSTL 标签库足够把数据展示清楚。关键是理解 EL 表达式怎么取后端传来的数据,以及 JSTL 怎么循环渲染表格。
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %> <table class="flight-table"> <thead> <tr> <th>航班号</th> <th>出发城市</th> <th>到达城市</th> <th>起飞时间</th> <th>票价</th> <th>操作</th> </tr> </thead> <tbody> <c:forEach items="${flightList}" var="flight"> <tr> <td>${flight.flightNo}</td> <td>${flight.departureCity}</td> <td>${flight.arrivalCity}</td> <td>${flight.departureTime}</td> <td>${flight.price}</td> <td> <a href="${pageContext.request.contextPath}/order/create?flightId=${flight.id}"> 预订 </a> </td> </tr> </c:forEach> </tbody> </table><c:forEach>遍历flightList,每次循环把当前对象赋给flight变量。${flight.flightNo}等价于调用flight.getFlightNo(),EL 表达式会自动找对应的 getter 方法。${pageContext.request.contextPath}获取项目部署路径,避免硬编码/aviation导致换环境后链接失效。预订链接把flightId作为查询参数传给订单创建 Servlet,这是典型的前端传参方式。
如果航班列表为空,页面会显示一个空表格,体验很差。加一个<c:if test="${empty flightList}">判断,显示“暂无航班”的提示。JSTL 的fn:length函数可以获取列表长度,用来做分页统计。前端页面不需要花哨的样式,Bootstrap 引入 CDN 链接就能把表格和按钮弄得像样,课程设计演示时视觉效果够用。
4.2 表单提交与中文乱码的三种解法
航空系统里用户注册、登录、下单都是表单提交,中文乱码是绕不过去的坑。GET 请求和 POST 请求的乱码原因不同,解法也不一样。
// 统一设置请求和响应编码 request.setCharacterEncoding("UTF-8"); response.setContentType("text/html;charset=UTF-8");这两行代码必须放在doGet或doPost方法的第一行,在获取任何参数之前执行。setCharacterEncoding只对 POST 请求体有效,GET 请求的参数在 URL 里,编码由 Tomcat 的server.xml决定。Tomcat 8 之后默认 URI 编码是 UTF-8,但如果你用的老版本 Tomcat,需要在server.xml的 Connector 标签里加URIEncoding="UTF-8"。
第三种情况是响应内容乱码,比如 Servlet 直接输出中文到页面。response.setContentType("text/html;charset=UTF-8")告诉浏览器用 UTF-8 解析响应体。如果 JSP 页面本身乱码,检查page指令里的contentType和pageEncoding是否都是 UTF-8。数据库连接 URL 也要加useUnicode=true&characterEncoding=utf8,否则存进去的中文会变成问号。
提示:如果所有编码都设了还是乱码,检查 IDEA 的 File Encoding 设置,把 Global Encoding 和 Project Encoding 都改成 UTF-8,并且勾选 “Transparent native-to-ascii conversion”。
5. 航空系统开发避坑指南:从环境配置到并发问题的排查记录
5.1 坑一:Tomcat 启动报 404,页面死活出不来
现象:IDEA 里 Tomcat 启动日志没有报错,但浏览器访问http://localhost:8080/aviation/显示 404。
原因:最常见的是 Artifact 没有正确部署,或者web.xml里welcome-file-list配置的首页文件不存在。还有一种情况是 Application context 设成了/,但访问时多加了项目名。
解决:打开 Run/Debug Configurations,检查 Deployment 选项卡里 Artifact 是否在列,Application context 是否和访问路径一致。确认web目录下有index.jsp,并且web.xml里配置了<welcome-file>index.jsp</welcome-file>。如果用了 Maven,检查pom.xml的<packaging>是不是war。
5.2 坑二:数据库连接池耗尽,页面卡死转圈
现象:系统刚启动正常,点几次之后所有页面都加载不出来,Tomcat 日志里出现 “Cannot get a connection, pool exhausted”。
原因:DAO 层获取了 Connection 但没有关闭,每次请求泄漏一个连接,池子满了之后新请求拿不到连接就一直等。
解决:所有 JDBC 操作必须用 try-with-resources 自动关闭,或者在 finally 块里显式调用close()。如果用 DBUtils 的QueryRunner,它的方法内部会管理连接,但手动获取 Connection 时一定要自己关。检查代码里有没有Connection conn = DBUtil.getConnection()之后没有conn.close()的地方。
5.3 坑三:下单成功但余票没减,或者减了没生成订单
现象:用户点击预订后订单表多了一条记录,但票务表的remaining字段没变;或者反过来,余票减了但订单没创建。
原因:订单创建和余票扣减不在同一个事务里。如果先插订单再扣库存,扣库存失败时订单已经提交了;如果先扣库存再插订单,插订单失败时库存已经减了。
解决:把两个操作放在同一个 Connection 的事务里,setAutoCommit(false)之后依次执行,两个都成功才commit(),任何一个失败就rollback()。注意 Service 层方法要抛出异常让上层感知失败,不能吞掉异常返回 true。
5.4 坑四:JSP 页面修改后刷新浏览器没变化
现象:改了 JSP 文件里的文字,重新加载页面还是旧内容。
原因:Tomcat 默认会缓存编译后的 JSP Servlet,或者浏览器缓存了静态资源。
解决:在 Tomcat 配置里设置development="true"和modificationTestInterval="1",让 Tomcat 定期检查 JSP 是否修改。浏览器端按 Ctrl+F5 强制刷新,或者在 JSP 的 head 里加 meta 标签禁用缓存。如果是 CSS 或 JS 文件没更新,在引用路径后面加版本号参数,比如style.css?v=2。
5.5 坑五:中文航班号或城市名存进数据库变成问号
现象:前端输入“北京”保存到数据库,查询出来显示??。
原因:数据库表的字符集不是 utf8mb4,或者 JDBC 连接 URL 没有指定编码。
解决:建表时指定DEFAULT CHARSET=utf8mb4,JDBC URL 加上useUnicode=true&characterEncoding=utf8。如果表已经建好了,用ALTER TABLE flight CONVERT TO CHARACTER SET utf8mb4修改。注意 MySQL 的utf8和utf8mb4不一样,后者支持 emoji 和更多字符,航空系统里城市名用 utf8 够用,但统一用 utf8mb4 更省心。
6. 航空系统进阶技巧:用过滤器统一权限控制和日志记录
项目跑通之后,下一步是让代码更健壮。航空系统里最值得加的进阶功能是 Filter 过滤器,它能在一个地方统一处理权限校验和操作日志,不用在每个 Servlet 里重复写代码。
@WebFilter(urlPatterns = {"/order/*", "/admin/*"}) public class AuthFilter implements Filter { @Override public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain) throws IOException, ServletException { HttpServletRequest request = (HttpServletRequest) req; HttpServletResponse response = (HttpServletResponse) resp; HttpSession session = request.getSession(false); // 检查用户是否登录 if (session == null || session.getAttribute("user") == null) { response.sendRedirect(request.getContextPath() + "/login.jsp"); return; } // 记录操作日志 String uri = request.getRequestURI(); String username = (String) session.getAttribute("username"); System.out.println("[操作日志] 用户 " + username + " 访问 " + uri); chain.doFilter(req, resp); // 放行 } }@WebFilter注解声明拦截路径,/order/*和/admin/*表示订单和后台管理相关请求都要过这个过滤器。request.getSession(false)表示如果没有 session 就返回 null,不新建 session。chain.doFilter是放行到下一个过滤器或目标 Servlet,不调用这行请求就被截断了。日志记录用System.out.println只是演示,实际项目里应该用 Log4j 或 SLF4J 输出到文件。
验证过滤器是否生效的方法很简单:退出登录后直接访问/order/create?flightId=1,如果跳转到登录页说明拦截成功。再登录后访问同一路径,控制台应该打印操作日志。这里有个细节要注意:登录页面和静态资源不能拦截,否则用户永远登不进去。urlPatterns里只写需要保护的路径,或者用chain.doFilter之前判断 URI 是否在白名单里。
我一般还会加一个 CharacterEncodingFilter 统一处理编码,放在 AuthFilter 之前执行。过滤器的顺序由web.xml里<filter-mapping>的声明顺序决定,用注解的话顺序不确定,所以编码过滤器最好用web.xml配置确保最先执行。
这套航空系统做完之后,我最大的习惯是每加一个功能先想清楚事务边界在哪里。余票扣减和订单创建必须原子,用户注册和发送邮件可以分开。另一个血泪教训是数据库连接一定要用连接池,手动DriverManager.getConnection在并发稍高时性能惨不忍睹。课程设计演示前把 Tomcat 的 JVM 内存调大一点,-Xms512m -Xmx1024m,避免演示到一半卡顿。希望帮到你。
本文还有配套的精品资源,点击获取