news 2026/11/4 17:22:15

航空票务系统JavaWeb实战:从架构选型到余票扣减的完整开发指南

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
航空票务系统JavaWeb实战:从架构选型到余票扣减的完整开发指南

简介:这是一份面向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,避免演示到一半卡顿。希望帮到你。

本文还有配套的精品资源,点击获取

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/11/2 2:28:57

常驻 Agent 四层治理架构:声明、校验、版本、预演与 72 条实测

做 Agent 开发这几年&#xff0c;我的体会是&#xff1a;一次性对话式 Agent 好写&#xff0c;常驻 Agent 难养。常驻 Agent 挂着定时任务、持着文件权限、连着外部工具&#xff0c;一天自动跑几百次&#xff0c;出一次问题就是批量误操作。最近我把手头的常驻运维 Agent 重构了…

作者头像 李华
网站建设 2026/11/2 1:58:22

JSP网上书店系统毕业设计:从环境配置到答辩避坑全攻略

简介&#xff1a;面向毕业设计场景的基于JSP的网上书店系统完整项目包&#xff0c;整合了源码、论文与操作视频&#xff0c;适合计算机相关专业学生进行Java Web方向的学习或二次开发。系统采用B/S结构&#xff0c;以JSP和SQL Server数据库为核心&#xff0c;围绕图书销售业务实…

作者头像 李华
网站建设 2026/11/2 4:16:01

Cadence Allegro排阻快速创建Xnet完整流程与TAB技巧

搞过高速PCB设计的人&#xff0c;基本都绕不开Xnet这个东西。尤其在Cadence Allegro里&#xff0c;当你手里握着一堆排阻、滤波电阻、串阻&#xff0c;却怎么都建不出合格的Xnet时&#xff0c;那种挫败感谁做谁知道。我今天就把给排阻快速创建Xnet的完整流程掰开了讲&#xff0…

作者头像 李华
网站建设 2026/11/2 15:10:06

LangChain Agent结构化输出实战:Pydantic强制校验指南

1. 这不是“问答器”&#xff0c;是让AI交出标准答卷的监考系统你有没有遇到过这样的场景&#xff1a;用LangChain搭了个问答Agent&#xff0c;用户问“请列出最近三个月销售额Top 5的客户&#xff0c;附带各自订单数和平均客单价”&#xff0c;结果模型返回一段自由发挥的散文…

作者头像 李华
网站建设 2026/11/2 11:32:22

JavaWeb增删改查源码包怎么跑通?从技术栈到部署避坑全解析

简介&#xff1a;面向JavaWeb初学者的MVC增删改查完整项目源码&#xff0c;基于JSPServlet三层架构演示数据管理流程&#xff0c;适合正在学习Servlet、JSP及JDBC开发的读者。压缩包共40个文件&#xff0c;约920KB&#xff0c;包含9个JSP视图页面、9个Java控制类及对应class编译…

作者头像 李华
网站建设 2026/11/2 10:18:27

法律文书NER实战:BERT+BiLSTM+CRF抽取交通肇事案事件要素

简介&#xff1a;一份面向法律文本挖掘与自然语言处理学习者的完整项目包&#xff0c;基于BERTBiLSTMCRF实现法律文书命名实体识别&#xff0c;并针对交通肇事案进行事件要素抽取&#xff0c;适合用作课程设计、期末大作业或毕设参考资料。压缩包共48个文件、约693KB&#xff0…

作者头像 李华