JAVA JSP车票预订系统:查票下单事务与部署实战

发布时间:2026/11/6 2:08:59

JAVA JSP车票预订系统:查票下单事务与部署实战 简介「基于WEB的车票预订信息系统设计与实现」是一套面向高校学生与Java Web入门者的综合项目案例可用于毕业设计、课程设计及Web开发实践重点解决车票查询、预订与订单管理等业务场景中的完整实现问题。压缩包共1337个文件、约55.82MB包含57个JSP页面、47个HTML/HTM页面及9个Java源文件、16个class文件另有31个jar依赖js、css与png、gif、jpg等前端资源占比较高SQL脚本和配置文件用于支撑数据库与工程结构。已有1106人学习说明其在教学与实训场景中具有一定参考价值。项目以Servlet处理请求、JSP承载界面、MySQL存储车次与订单数据并借助JDBC完成增删改查配套讲解可帮助读者梳理表现层与逻辑层分离、页面交互及数据库连接等关键知识点适合用来对照理解Java Web应用的模块划分与开发流程。1. 从排队买票说起这套基于 WEB 的车票预订信息系统到底适合谁很多人解压这个基于 WEB 的车票预订信息系统看到满屏 .jsp 和 Servlet第一反应是「这玩意儿现在还能跑吗」。能跑而且跑得挺稳——它就是一套典型的 JAVA JSP 车票预订管理系统前台查车次、看余票、选座下单、查订单后台维护车站班次、改票价、核销退票。它撑不起高并发票务中台但拿来做课程设计、中小客运站的订票后台、校园班车或景区接驳车预约功能覆盖是完整的。往下按「它是什么、怎么跑起来、一次查票下单走了哪些层、哪儿最容易翻车、怎么改成你自己的」这条线拆看完你至少能把它在本机 Tomcat 里跑通并且知道哪几个文件是真要动手的。2. 技术选型与工程骨架JSP、Servlet、JavaBean、MySQL 这四件套怎么摆解压之后先别急着开 IDE去看WEB-INF/lib和web.xml这两个地方。lib 里躺着 mysql-connector-java、jstl、standard可能还有 commons-dbutils 和 log4jweb.xml 里写着 Servlet 版本、welcome-file 和过滤器声明。这两处信息比任何说明文档都准它们直接决定这套 JAVA 车票预订管理系统能不能在你机器上活过来。我见过太多人上来就点运行然后被一堆 ClassNotFoundException 拦住其实问题全在这两个目录里。2.1 为什么是 JSP Servlet而不是上来就 Spring Boot这套代码的定位很明确教学型 / 中小型项目。JSP 在第一次被访问时编译成 Servlet请求进来就是service()参数从request.getParameter()拿结果往request.setAttribute()一塞转发给页面渲染。整条链路没有框架黑匣子你打断点能一行行看过去。代价也很清楚路由、参数绑定、编码处理、事务边界、连接管理全得自己写。下面这张表是我自己的判断标准选型纠结的时候直接对照维度裸写 JSP ServletSpring Boot 脚手架启动成本一个 Tomcat 一个 war 包5 分钟依赖下载 配置半天起步链路可见性每一层都能单步调试要熟悉框架源码才看得透代码量样板代码多重复度高注解省掉大量样板适合场景看懂原理、课程项目、内部小系统需要长期迭代的生产系统如果你的目标是「彻底搞明白一个请求从浏览器到数据库再回来」的完整过程这套代码比任何脚手架都直观。反过来如果目标是三个月上线一个要扛大促的系统就别在这上面折腾了直接换技术栈别给自己找罪受。2.2 目录结构src 分包与 WebRoot 的职责边界解压后典型的结构长这样不同版本的分包名可能不一样但层次基本一致ticket-booking/ ├── src/ │ └── com/ticket/ │ ├── entity/ # 实体类和数据库表一一对应 │ │ ├── User.java │ │ ├── Station.java │ │ ├── Schedule.java │ │ └── Order.java │ ├── dao/ # 只碰 SQL不写业务判断 │ │ ├── ScheduleDao.java │ │ ├── OrderDao.java │ │ └── UserDao.java │ ├── service/ # 业务规则 事务边界 │ │ ├── ScheduleService.java │ │ └── OrderService.java │ ├── servlet/ # 取参、调 service、转发 │ │ ├── SearchServlet.java │ │ └── OrderServlet.java │ ├── filter/ # 编码过滤、登录拦截 │ └── util/ # DBUtil、MD5Util、DateUtil ├── WebRoot/ │ ├── WEB-INF/ │ │ ├── web.xml │ │ ├── lib/ # 第三方 jar │ │ └── views/ # 只能被转发访问的 jsp │ ├── css/ js/ images/ │ ├── index.jsp │ └── login.jsp └── sql/ └── ticket_db.sql # 建库建表 初始数据entity就是纯 POJO属性和字段一一对应千万别往里面塞业务方法塞进去以后这个类会变成谁都不敢改的怪物。dao只负责把 SQL 打出去、把 ResultSet 映射回来不要在里面判断「这个用户能不能退票」——那是 service 的活。service才是业务规则的落脚点也是唯一该出现setAutoCommit(false)的地方。servlet尽量薄标准动作只有三步取参数、调 service、转发或重定向。我第一次接手这类项目时看到某个 Servlet 里写了 200 行 SQL改一个字段要翻三个文件那才叫血泪经验。WEB-INF目录值得单独说一句。放在它下面的 jsp 文件浏览器访问不到必须由 Servlet 用request.getRequestDispatcher(/WEB-INF/views/list.jsp).forward(...)转发进去。这其实就是最原始的控制器隔离思路页面不能绕过业务层被直接打开参数校验和权限判断才有地方落。2.3 数据表设计六张表撑起查票、下单、退票表结构是这类系统的地基我把核心几张列出来实际压缩包里如果有出入按这个思路对着看就行表名作用关键字段user前台注册用户user_id、phone、password、real_namestation车站字典station_id、station_name、cityschedule班次某车次某天某区间的运行计划schedule_id、train_no、from/to_station_id、depart_time、price、statusseat_inventory分座位类型的余票schedule_id、seat_type、total、remainorders订单主表order_no、user_id、schedule_id、num、amount、statuspassenger乘车人可挂多个passenger_id、user_id、id_card、name建表语句里有两个地方我建议你重点看CREATE TABLE schedule ( schedule_id int(11) NOT NULL AUTO_INCREMENT, train_no varchar(16) NOT NULL COMMENT 车次号如 K1024, from_station_id int(11) NOT NULL, to_station_id int(11) NOT NULL, depart_time datetime NOT NULL COMMENT 发车时间, arrive_time datetime NOT NULL COMMENT 到达时间可能跨天, price decimal(10,2) NOT NULL COMMENT 基础票价, status tinyint(4) DEFAULT 1 COMMENT 1 上架 0 下架, PRIMARY KEY (schedule_id), KEY idx_route_time (from_station_id,to_station_id,depart_time) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT班次表; CREATE TABLE seat_inventory ( id int(11) NOT NULL AUTO_INCREMENT, schedule_id int(11) NOT NULL, seat_type varchar(16) NOT NULL COMMENT 硬座/软座/卧铺, total int(11) NOT NULL, remain int(11) NOT NULL, PRIMARY KEY (id), UNIQUE KEY uk_schedule_seat (schedule_id,seat_type) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT余票表;idx_route_time这个联合索引的顺序不能随便调查询条件永远是「出发站 到达站 日期」把选择度高的两列车站放前面、时间放后面才能走到索引。很多人只给主键建索引数据量一过十万查票页面就开始转圈。余票单独拆一张表而不是在schedule里加个remain字段原因是座位类型不止一种硬座、软座、卧铺各有各的库存一行塞不下。拆开之后还有个隐性收益——余票是高频 UPDATE 的热点行独立成表后锁的粒度更小不至于连车次信息一起被锁住。uk_schedule_seat这个唯一索引保证一个班次的同一种座位只有一行库存避免重复插入导致余票算错。orders表里status用 tinyint 而不是 varchar 存中文状态这一点在后台做统计和状态流转时优势很明显后面第 4 章会细说。3. 核心链路拆解从查票到下单一次请求到底走了哪些层车票预订系统真正有技术含量的地方只有两处查票多条件动态查询 分页和下单并发下的余票扣减。其余都是围绕这两件事的增删改查。3.1 车次查询动态 SQL 拼装和手写分页用户在前台填出发站、到达站、日期点查询。条件可能只填一个也可能全填所以 SQL 不能写死。这种场景最稳妥的写法是「结构拼接 值占位」public ListScheduleVO search(String fromStation, String toStation, String date, int page, int size) throws SQLException { StringBuilder sql new StringBuilder( SELECT s.schedule_id, s.train_no, s.depart_time, s.arrive_time, s.price, si.seat_type, si.remain FROM schedule s JOIN station fs ON fs.station_id s.from_station_id JOIN station ts ON ts.station_id s.to_station_id JOIN seat_inventory si ON si.schedule_id s.schedule_id WHERE s.status 1 ); ListObject params new ArrayList(); // 只拼 SQL 结构值一律走 ? 占位符防注入的底线 if (fromStation ! null !fromStation.trim().isEmpty()) { sql.append(AND fs.station_name LIKE ? ); params.add(% fromStation.trim() %); // % 在 Java 侧拼不能写进 SQL } if (toStation ! null !toStation.trim().isEmpty()) { sql.append(AND ts.station_name LIKE ? ); params.add(% toStation.trim() %); } if (date ! null !date.trim().isEmpty()) { sql.append(AND DATE(s.depart_time) ? ); params.add(date); } sql.append(ORDER BY s.depart_time ASC LIMIT ?, ?); params.add((page - 1) * size); // 偏移量不是页码 params.add(size); return DBUtil.query(sql.toString(), params, ScheduleVO.class); }这段代码有三个点必须理解透。第一StringBuilder拼的只是 WHERE 子句的结构所有用户输入都以?占位、通过PreparedStatement传入数据库会先编译执行计划再绑值注入字符串进不来。第二params的顺序必须和?出现的顺序严格一致错一位不会报错只会查出莫名其妙的数据这种 bug 最难查。第三LIKE %?%是错的写法占位符在引号里会被当成字面量%一定要在 Java 里拼到参数值上。分页用的是LIMIT ?, ?第一个参数是偏移量(page-1)*size第二个是每页条数。这是最朴素的手写分页配合一条SELECT COUNT(*)算总条数就能渲染页码。MySQL 深分页比如LIMIT 100000, 20会扫描前十万行再丢弃几万条以内感觉不到真要上量了就得换成「记住上一页最后一条的 depart_time 再往下取」的游标方式。还有个小细节DATE(s.depart_time) ?会让idx_route_time索引在时间列上失效。数据量大的时候改成s.depart_time ? AND s.depart_time ?传当天 00:00 和次日 00:00 两个边界值索引用得上速度差一个数量级。3.2 下单事务余票扣减与订单落库必须绑在一起这是整个系统最容易出事的地方。用户点了预订你至少要干两件事把余票减掉、把订单写进去。这两步必须同生共死中间断一个就会出现「票没了但订单没生成」或者「订单生成了但余票没扣」的脏数据。public String createOrder(Integer userId, Integer scheduleId, String seatType, int count) { Connection conn null; try { conn DBUtil.getConnection(); conn.setAutoCommit(false); // 关掉自动提交事务边界从这里开始 // 1. 扣余票判断条件写进 WHERE靠影响行数判断有没有抢到 String deduct UPDATE seat_inventory SET remain remain - ? WHERE schedule_id ? AND seat_type ? AND remain ?; int affected DBUtil.update(conn, deduct, count, scheduleId, seatType, count); if (affected 0) { conn.rollback(); return null; // 余票不足直接返回让上层提示用户 } // 2. 订单号时间戳 用户尾号避免纯自增被人顺着猜 String orderNo T System.currentTimeMillis() String.format(%03d, userId % 1000); // 3. 订单落库status0 表示待支付 BigDecimal amount DBUtil.queryPrice(conn, scheduleId) .multiply(new BigDecimal(count)); String insert INSERT INTO orders(order_no, user_id, schedule_id, seat_type, num, amount, status, create_time) VALUES(?,?,?,?,?,?,0,NOW()); DBUtil.update(conn, insert, orderNo, userId, scheduleId, seatType, count, amount); conn.commit(); // 两步都成功才提交 return orderNo; } catch (Exception e) { if (conn ! null) { try { conn.rollback(); } catch (SQLException ignored) { } } throw new RuntimeException(下单失败, e); } finally { if (conn ! null) { try { conn.setAutoCommit(true); conn.close(); } catch (SQLException ignored) { } } } }关键在UPDATE ... WHERE remain ?这一句。很多人习惯先SELECT remain查一下够不够够了再UPDATE remain remain - ?这个「检查-使用」的两步在并发下必然出事两个线程同时查到剩 1 张都判定够都去减余票就变成 -1 了。把判断条件塞进 WHERE让数据库在行锁里完成「比较 更新」这个原子动作affected 0就说明没抢到天然防超卖。这批数据全靠 InnoDB 的行级锁兜底所以表引擎必须是 InnoDBMyISAM 只有表锁压测时你会看到整张表排队。参数和边界也得说清楚count建议在 service 层就限制在 15 之间别让用户一次订 999 张那是给自己找麻烦。finally里的conn.close()不能省早期版本漏掉这一句压测跑到两百来个请求就开始报Too many connections这个坑我踩过不止一次。事务里不要做耗时操作。有人在下单事务里顺手调了个短信接口接口一超时行锁就一直不释放后面所有同班次的下单全堵在那儿表现是「页面转圈日志里全是 lock wait timeout」。3.3 登录态与拦截一个 Filter 管住后台所有入口前台订票要登录后台管理更要。这种「一批路径必须登录才能访问」的需求用 Filter 是最省事的做法别在每个 Servlet 里重复写判断WebFilter(urlPatterns {/admin/*, /order/*}) public class LoginFilter implements Filter { // 白名单登录页、静态资源不拦 private static final String[] WHITE {/login.jsp, /login, /css/, /js/, /images/}; Override public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain) throws IOException, ServletException { HttpServletRequest request (HttpServletRequest) req; HttpServletResponse response (HttpServletResponse) resp; String uri request.getRequestURI(); for (String w : WHITE) { if (uri.contains(w)) { chain.doFilter(req, resp); return; } } // getSession(false)没有会话就返回 null不新建 HttpSession session request.getSession(false); Object loginUser (session null) ? null : session.getAttribute(loginUser); if (loginUser null) { response.sendRedirect(request.getContextPath() /login.jsp); return; // 必须 return否则请求会继续往下走到 Servlet } chain.doFilter(req, resp); } }几个容易忽略的点。getSession(false)和getSession()差一个参数后者在没有 session 时会新建一个等于每次匿名访问都往内存里丢对象时间长了 Tomcat 内存曲线会很难看。sendRedirect之后一定要return漏掉的话请求会继续往下执行用户明明没登录Servlet 里的业务代码还是跑了只是响应被覆盖了而已。白名单判断用contains有风险更严谨的写法是uri.endsWith(.css)这类后缀判断别让/admin/css/xxx这种路径被误放行。另外WebFilter注解要求 Servlet 3.0 以上并且web.xml里不能有metadata-completetrue。有些老项目留了这个属性注解全部失效你以为是过滤器没生效其实是根本没被扫描。这种情况要么删掉那个属性要么老老实实在web.xml里配filter和filter-mapping。4. 后台与前端落地车次维护、订单状态流转和 JSTL 渲染前台是给乘客看的后台是给运营用的。后台代码的复杂度往往比前台高因为它涉及更多约束删车次前要检查有没有人在卖、退票要把库存加回去、下架不能物理删。4.1 车次维护的 CRUD三个最容易漏的细节新增和修改车次其实就是对schedule表做 insert/update写起来不难难的是边界。下面这段删除校验是我一般会加的public boolean removeSchedule(Integer scheduleId) throws SQLException { // 1. 还有未完成订单待支付/已支付就不能删只能下架 String check SELECT COUNT(*) FROM orders WHERE schedule_id ? AND status IN (0, 1); long pending DBUtil.count(check, scheduleId); if (pending 0) { throw new IllegalStateException(该班次还有 pending 笔未完成订单不能删除); } // 2. 物理删除余票记录但班次本身只做状态位下架 DBUtil.update(DELETE FROM seat_inventory WHERE schedule_id ?, scheduleId); return DBUtil.update(UPDATE schedule SET status 0 WHERE schedule_id ?, scheduleId) 0; }第一个细节车次不要物理删。真实业务里订单会引用schedule_id你把这个主键删了历史订单查出来就是一堆空值。用status字段做软删除前台查询只捞status 1的后台列表全都捞出来但把下架的标灰这才是能上线的做法。第二个细节跨天班次的时间比较。23:40 发车、次日 06:00 到达arrive_time depart_time是正常的做「行程时长」计算时不能直接相减得先判断有没有跨天跨了就加 24 小时。这种车次在长途客运里非常常见测试时一定要造一条这样的数据。第三个细节修改车次时间时要同步什么。如果这个班次已经有人订了票把发车时间改到凌晨三点用户是会炸的。常见做法是已开售的班次只允许改票价和余票上限时间字段锁死要调时间就新建一个班次把老订单退掉。这个规则不写在文档里但每个做票务的人都懂。4.2 订单状态把散落的 if-else 收成一张流转表订单状态有四个0 待支付、1 已支付、2 已取消、3 已退票。如果不加约束代码里很快会出现「已退票的订单又点了取消」这种脏操作库存被加回去两次余票比总票数还多。用枚举把合法流转固下来public enum OrderStatus { UNPAID(0, 待支付), PAID(1, 已支付), CANCELLED(2, 已取消), REFUNDED(3, 已退票); private final int code; private final String label; OrderStatus(int code, String label) { this.code code; this.label label; } public int getCode() { return code; } // 只允许这些流转其余一律拒绝 private static final MapInteger, SetInteger ALLOWED new HashMap(); static { ALLOWED.put(UNPAID.code, new HashSet(Arrays.asList(PAID.code, CANCELLED.code))); ALLOWED.put(PAID.code, new HashSet(Arrays.asList(REFUNDED.code))); ALLOWED.put(CANCELLED.code, Collections.emptySet()); // 终态 ALLOWED.put(REFUNDED.code, Collections.emptySet()); // 终态 } public static boolean canTransfer(int from, int to) { SetInteger targets ALLOWED.get(from); return targets ! null targets.contains(to); } }在 service 里先canTransfer判定再执行更新同时把UPDATE orders SET status ? WHERE order_no ? AND status ?的旧状态也写进 WHERE靠影响行数兜住并发重复提交。退票时要在同一个事务里把seat_inventory.remain加回去和下单扣减对称别一边加一边忘了减。这套状态机的价值在于新来一个人要加「改签」功能他只要在枚举里加两条流转规则所有校验点自动生效不用去 grep 全项目找 if-else。4.3 用 JSTL 和 EL 把 Java 代码从页面里赶出去老 JSP 项目里最刺眼的就是页面顶部一大段% ... %脚本段连数据库查询都写在里面。这种代码改一个字段要动三层还没法写单元测试。JSTL EL 是标准解法% page contentTypetext/html;charsetUTF-8 pageEncodingUTF-8 % % taglib prefixc urihttp://java.sun.com/jsp/jstl/core % % taglib prefixfmt urihttp://java.sun.com/jsp/jstl/fmt % table classticket-table c:forEach items${pageInfo.list} vars tr td${s.trainNo}/td td fmt:formatDate value${s.departTime} patternMM-dd HH:mm/ c:if test${s.arriveTime.time s.departTime.time}次日/c:if /td tdfmt:formatNumber value${s.price} pattern0.00/ 元/td td c:choose c:when test${s.remain 0} a href${pageContext.request.contextPath}/order/create ?scheduleId${s.scheduleId}seatType${s.seatType}预订/a /c:when c:otherwisespan classsold-out无票/span/c:otherwise /c:choose /td /tr /c:forEach /tablec:forEach遍历的是 Servlet 塞进 request 域的对象vars就是循环变量EL 表达式${s.trainNo}会自动调 getter不用你手写强转。fmt:formatDate解决时间格式化fmt:formatNumber解决金额保留两位小数这两件事在脚本段里写起来又臭又长。c:choose相当于 if-else用来做有无余票的展示分支。${pageContext.request.contextPath}这个前缀不能省。部署时如果 war 包名字不是 ROOT应用会被挂到/ticket这样的上下文路径下写死/order/create的链接会 404而且这种错误在本地测的时候往往发现不了一上服务器就集体失灵。还有个常见问题页面中文全变成问号或者方块。三处都要对上——JSP 顶部pageEncodingUTF-8、Servlet 里response.setContentType(text/html;charsetUTF-8)、JDBC 连接串里带characterEncodingutf8。少一处就有一处乱码等着你。5. 部署跑通与排查Tomcat 起来之后最容易翻车的五件事代码看懂是一回事能在自己机器上跑起来是另一回事。这一章按「装环境 → 导数据 → 起服务 → 出问题怎么查」的顺序来。5.1 环境准备JDK、Tomcat、MySQL 的版本对位这类 JSP 项目对版本极度敏感尤其是 Servlet 规范从 javax 换成 jakarta 那一次选错了怎么改都跑不起来。对照着选组件推荐版本说明JDK8 或 11老项目用 8 最稳别上 17Tomcat8.5 或 9.09.0 及以下用 javax.servlet10 开始换 jakartaMySQL5.7 或 8.08.0 要配新的驱动和时区mysql-connector-java5.1.x 配 5.78.0.x 配 8.0驱动版本和数据库要对位JSTL1.2 standard 1.1两个 jar 都要放少一个报 taglib 解析失败如果压缩包里的WEB-INF/lib是 javax 命名空间的 jstl一定要配 Tomcat 9 及以下。放到 Tomcat 10 里页面会直接报「无法在 web.xml 或绝对 uri 中解析 taglib」改依赖比重装 Tomcat 更麻烦。5.2 从建库到启动的完整命令先改src/db.properties这四行是全项目的命门jdbc.drivercom.mysql.cj.jdbc.Driver jdbc.urljdbc:mysql://127.0.0.1:3306/ticket_db?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/ShanghaiuseSSLfalse jdbc.usernameroot jdbc.password你的密码serverTimezoneAsia/Shanghai不能省MySQL 8.0 的驱动对时区很敏感不配的话查出来的发车时间会整体差 8 小时而且是那种「数据在库里是对的页面上就是不对」的玄学问题。useSSLfalse只是省掉本地连接时的警告生产环境该开还得开。然后是建库、编译、部署这一串# 1. 建库并导入结构和初始数据 mysql -uroot -p -e CREATE DATABASE ticket_db DEFAULT CHARSET utf8mb4; mysql -uroot -p ticket_db sql/ticket_db.sql # 2. 手工编译用 IDEA/Eclipse 的话可以跳过直接配 Artifact javac -encoding UTF-8 -cp WebRoot/WEB-INF/lib/* \ -d WebRoot/WEB-INF/classes $(find src -name *.java) # 3. 打包成 war丢进 Tomcat jar -cvf ticket.war -C WebRoot . cp ticket.war $CATALINA_HOME/webapps/ # 4. 启动 $CATALINA_HOME/bin/startup.sh # Windows 用 startup.bat # 5. 看日志确认没异常再访问 tail -f $CATALINA_HOME/logs/catalina.out-encoding UTF-8这个参数别丢源码里有中文注释时不指定编码在 Windows 上会编译失败。-cp WebRoot/WEB-INF/lib/*里的引号和星号是让 javac 把 lib 下所有 jar 都加进 classpath漏了引号在 Linux 上会被 shell 提前展开成多个参数。部署后访问http://localhost:8080/ticket/应该能看到首页后台入口一般在/ticket/admin/login.jsp。5.3 五条踩坑记录现象、原因、解决现象一所有页面中文变问号数据库里存进去的也是问号。原因编码在三处断了——JSP 没写pageEncoding、请求参数没设编码、JDBC 连接串缺characterEncoding。只修其中一处剩下的地方继续乱。 解决加一个CharacterEncodingFilter全局设request.setCharacterEncoding(UTF-8)JSP 顶部统一写pageEncodingUTF-8连接串补齐useUnicodetruecharacterEncodingutf8。三处对齐后已经存进去的脏数据要清掉重导。现象二启动报ClassNotFoundException: com.mysql.cj.jdbc.Driver或者驱动加载了但连不上。原因jar 没放进WEB-INF/lib或者驱动版本和连接串不匹配——用 5.1.x 的驱动配com.mysql.cj.jdbc.Driver这个类名类是找不到的。 解决5.1.x 驱动对应com.mysql.jdbc.Driver8.0.x 对应com.mysql.cj.jdbc.Driver。把 jar 复制进WEB-INF/lib后记得重新打包光放进 IDE 的 module 依赖里war 包中是不会有这个 jar 的。现象三登录成功刷新页面又跳回登录页。原因一是 Filter 把/css/、/js/也拦了静态资源被重定向二是 session 超时时间设得太短三是用了sendRedirect跳到外域session cookie 带不过去。 解决白名单里放行静态资源和登录接口检查web.xml里的session-config超时配置跳转统一用带request.getContextPath()的相对路径。现象四压测时余票变成负数或者订单数比卖出去的票多。原因下单逻辑写成了「先 select 查余票再 update 减余票」并发下两个请求同时通过检查。或者事务没开扣减和落库各提交各的。 解决改成UPDATE ... WHERE remain ?靠影响行数判断整段包在setAutoCommit(false)里表引擎确认是 InnoDB。改完用 50 个线程压一轮直接SELECT remain FROM seat_inventory WHERE remain 0能查出来就是没改干净。现象五跑一阵子报Too many connections重启 Tomcat 又能撑一会儿。原因DAO 里Connection没在 finally 关闭或者ResultSet、PreparedStatement没关。每次请求漏一个连接池子很快见底。 解决统一用 try-with-resources 包住这三个资源或者干脆上连接池下一章会说。临时止血的办法是在 MySQL 里SHOW PROCESSLIST看有多少 Sleep 状态的连接堆着数字一直在涨就说明确实在漏。6. 二次开发把它改成「敢上线」的样子跑通之后多数人的下一步是改。我一般按「收益 / 改动量」这个比值排序先做最划算的。第一优先级是换连接池。现在这套代码大概率是DriverManager.getConnection()每次新建连接这是性能瓶颈的源头之一。把DBUtil改成从 Druid 或 HikariCP 拿连接外部接口不变改动集中在一个文件里压测 QPS 能翻倍// 只改 DBUtil 内部DAO 层一行都不用动 private static final DataSource DS; static { DruidDataSource ds new DruidDataSource(); ds.setUrl(Props.get(jdbc.url)); ds.setUsername(Props.get(jdbc.username)); ds.setPassword(Props.get(jdbc.password)); ds.setInitialSize(5); ds.setMaxActive(20); // 别超过数据库 max_connections ds.setMaxWait(3000); // 拿不到连接最多等 3 秒快速失败 DS ds; } public static Connection getConnection() throws SQLException { return DS.getConnection(); }第二优先级是补验证。改完余票逻辑别靠肉眼看写个main方法起 50 个线程同时下单跑完SELECT remain FROM seat_inventory和SELECT COUNT(*) FROM orders WHERE status 0两个数对不上就说明还有竞态。这比任何测试框架都快。第三优先级才是架构层面的动作把 JSP 里的业务逻辑往 service 抽、给热点车次加 Redis 缓存、把验证码和密码加盐补齐。这些改动大、回归范围广一定要在前两步稳定之后再做否则出了问题根本分不清是新代码的锅还是老代码的锅。验证方式我习惯用一条 SQL 兜底每次改完下单或退票逻辑都跑一遍SELECT schedule_id, total, remain FROM seat_inventory WHERE remain 0 OR remain total返回空才敢提交。这个习惯是从一次线上事故之后养成的——那会儿余票被扣成 -3客服系统里三个用户同时显示「预订成功」最后挨个打电话道歉。从那以后我每次动库存相关的代码都会强制走一遍「压测 这条 SQL 对账」的流程不管改动多小。这套 JAVA 车票预订管理系统本身不复杂它的价值在于把「查票 - 下单 - 支付 - 退票」这条完整业务链摊开给你看每一层都能动手改。希望帮到你。本文还有配套的精品资源点击获取
延伸阅读

更多相关文章

2026/11/6 2:08:59

C# Modbus TCP源码:MBAP、功能码与粘包处理实战

简介:面向工业自动化通信学习与C#网络编程实践,这套基于TCP的Modbus源码示例适合需要理解PLC及自动化设备数据交换机制的初中级开发者,也可作为上位机通信模块与协议调试工具的参考实现。代码围绕TcpClient连接管理、请求与响应报文构造、Net…

2026/11/6 2:03:59

轻量化行为识别:ST-GCN参数量砍4.4倍,精度反升的改造方案

简介:这份PDF面向深度学习与计算机视觉方向的研究者和工程实践者,聚焦行为识别中模型参数量大、计算复杂度高、精度与时效难以兼顾的痛点,给出一种基于时空图卷积网络(ST-GCN)的轻量级改进方案。资源包共1个PDF文件&am…

2026/11/6 2:08:59

SVM三分类实战:SMO求解、OvO投票与决策边界可视化

简介:这份资料围绕支持向量机完成数据三分类的MATLAB实现,面向已了解SVM二分类基础、希望进一步掌握多分类策略与代码落地的学习者,也可供课程实验和算法对比使用。包内共6个文件,以5个m脚本和1个data数据文件为主,脚本…

2026/11/6 2:08:59

C#实现单像空间后方交会:共线方程解算外方位元素与精度评定

简介:C#摄影测量学习工程以单向空间后方交会为主线,面向摄影测量学、计算机视觉课程学习者以及需要单像空间后方交会实现的开发者,重点解决由单张影像和若干控制点反求外方位元素、完成坐标解算的问题,适合课程实验、算法对照与小…

2026/11/6 2:08:59

JAVA JSP车票预订系统:查票下单事务与部署实战

简介:「基于WEB的车票预订信息系统设计与实现」是一套面向高校学生与Java Web入门者的综合项目案例,可用于毕业设计、课程设计及Web开发实践,重点解决车票查询、预订与订单管理等业务场景中的完整实现问题。压缩包共1337个文件、约55.82MB&am…

2026/11/6 2:08:59

C# Modbus TCP源码:MBAP、功能码与粘包处理实战

简介:面向工业自动化通信学习与C#网络编程实践,这套基于TCP的Modbus源码示例适合需要理解PLC及自动化设备数据交换机制的初中级开发者,也可作为上位机通信模块与协议调试工具的参考实现。代码围绕TcpClient连接管理、请求与响应报文构造、Net…

2026/11/5 15:39:06

面向复杂光照与角度的宠物面部识别鲁棒性增强策略

在智慧养宠时代,基于视觉的宠物身份识别技术正逐步从实验室走向家庭应用。然而,与配合度高的人类不同,宠物是典型的“非合作生物识别对象”——它们不会主动配合摄像头调整姿态,更无法控制光照环境。实际应用中,我们常…

2026/11/4 21:04:25

SSM电子书店源码全解析:结构、原理、部署与排错实战

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/11/6 1:03:39

MCU晶振电路设计全解析:从负载电容计算到PCB布局调试

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/11/5 15:39:06

面向复杂光照与角度的宠物面部识别鲁棒性增强策略

在智慧养宠时代,基于视觉的宠物身份识别技术正逐步从实验室走向家庭应用。然而,与配合度高的人类不同,宠物是典型的“非合作生物识别对象”——它们不会主动配合摄像头调整姿态,更无法控制光照环境。实际应用中,我们常…

2026/11/4 21:04:25

SSM电子书店源码全解析:结构、原理、部署与排错实战

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

还想了解更多?直接咨询顾问

免费诊断 + 免费方案 + 透明报价。

全国咨询热线400-8866-253
免费获取方案
☎咨询二维码 ☎ ↑