资讯详情

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

📅 2026/10/7 18:42:58 | 华诺云谱 👁 阅读
航空票务系统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 架构选型JSPServlet 还是直接上框架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 LocalDeployment 选项卡里把刚才的 Artifact 加进去Application context 设成/aviation。// web.xml 中配置 Servlet 映射 servlet servlet-nameFlightSearchServlet/servlet-name servlet-classcom.aviation.flight.servlet.FlightSearchServlet/servlet-class /servlet servlet-mapping servlet-nameFlightSearchServlet/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(); ListFlight 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) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; -- 票务库存表 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) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;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 或 VueJSP 配合 JSTL 标签库足够把数据展示清楚。关键是理解 EL 表达式怎么取后端传来的数据以及 JSTL 怎么循环渲染表格。% taglib prefixc urihttp://java.sun.com/jsp/jstl/core % table classflight-table thead tr th航班号/th th出发城市/th th到达城市/th th起飞时间/th th票价/th th操作/th /tr /thead tbody c:forEach items${flightList} varflight 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 /tablec: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;charsetUTF-8);这两行代码必须放在doGet或doPost方法的第一行在获取任何参数之前执行。setCharacterEncoding只对 POST 请求体有效GET 请求的参数在 URL 里编码由 Tomcat 的server.xml决定。Tomcat 8 之后默认 URI 编码是 UTF-8但如果你用的老版本 Tomcat需要在server.xml的 Connector 标签里加URIEncodingUTF-8。第三种情况是响应内容乱码比如 Servlet 直接输出中文到页面。response.setContentType(text/html;charsetUTF-8)告诉浏览器用 UTF-8 解析响应体。如果 JSP 页面本身乱码检查page指令里的contentType和pageEncoding是否都是 UTF-8。数据库连接 URL 也要加useUnicodetruecharacterEncodingutf8否则存进去的中文会变成问号。提示如果所有编码都设了还是乱码检查 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-fileindex.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 配置里设置developmenttrue和modificationTestInterval1让 Tomcat 定期检查 JSP 是否修改。浏览器端按 CtrlF5 强制刷新或者在 JSP 的 head 里加 meta 标签禁用缓存。如果是 CSS 或 JS 文件没更新在引用路径后面加版本号参数比如style.css?v2。5.5 坑五中文航班号或城市名存进数据库变成问号现象前端输入“北京”保存到数据库查询出来显示??。原因数据库表的字符集不是 utf8mb4或者 JDBC 连接 URL 没有指定编码。解决建表时指定DEFAULT CHARSETutf8mb4JDBC URL 加上useUnicodetruecharacterEncodingutf8。如果表已经建好了用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?flightId1如果跳转到登录页说明拦截成功。再登录后访问同一路径控制台应该打印操作日志。这里有个细节要注意登录页面和静态资源不能拦截否则用户永远登不进去。urlPatterns里只写需要保护的路径或者用chain.doFilter之前判断 URI 是否在白名单里。我一般还会加一个 CharacterEncodingFilter 统一处理编码放在 AuthFilter 之前执行。过滤器的顺序由web.xml里filter-mapping的声明顺序决定用注解的话顺序不确定所以编码过滤器最好用web.xml配置确保最先执行。这套航空系统做完之后我最大的习惯是每加一个功能先想清楚事务边界在哪里。余票扣减和订单创建必须原子用户注册和发送邮件可以分开。另一个血泪教训是数据库连接一定要用连接池手动DriverManager.getConnection在并发稍高时性能惨不忍睹。课程设计演示前把 Tomcat 的 JVM 内存调大一点-Xms512m -Xmx1024m避免演示到一半卡顿。希望帮到你。本文还有配套的精品资源点击获取
📝

华诺云谱内容团队

资深建站顾问 · 行业研究员

10年+企业数字化服务经验,专注智能建站、SEO优化与品牌营销,持续输出建站技巧、行业洞察与营销干货,已帮助5000+企业实现数字化增长。

你可能需要的服务

订阅华诺云谱资讯周报

每周一封,精选建站技巧、SEO与营销干货,直达邮箱。已有 8,000+ 企业主订阅,助你少走弯路。

↑