
简介电影院售票管理系统是一套基于Java技术栈的完整毕业设计源码采用ServletJSPJDBCMysql实现B/S架构覆盖用户查询场次、选座购票、订单支付以及后台电影排期、场次管理、用户与管理员管理等核心模块。代码包含详细注释界面简洁实用适合Java初学者学习参考也可直接作为毕业设计或期末大作业提交。压缩包共103个文件总大小2.31MB其中包含23个Java源码文件、23个class编译文件、8个SQL数据库脚本、10个XML配置以及若干png界面截图、docx说明文档、properties配置文件等目录结构清晰能直观对应各功能模块。已有86人学习下载。研究这套源码可掌握ServletJSPJDBC的经典分层写法、数据库表设计思路、用户权限控制与订单流程实现对理解Java Web项目从搭建到调优的完整过程有实际帮助。1. 电影院售票管理系统一套经典的 JavaWeb 毕业设计源码能给你什么如果你在找一份能跑通、能答辩、能写进简历的 JavaWeb 项目这套基于 JavaServletJSPJDBCMySQL 的电影院售票管理系统是我见过比较省心的一类模板。它不碰 Spring 全家桶纯粹用 Servlet 处理请求、JSP 渲染页面、JDBC 操作数据库技术栈老旧但五脏俱全用户注册登录、电影列表、场次排片、在线选座、生成订单、后台管理这些毕业设计里高频出现的功能点全都有对应代码。对于还在用 IDEA 写 JSP 作业、或者正在准备 JavaWeb 头歌实训的人来说这份源码最大的价值不是“能抄”而是它能让你在最短时间内看懂一个 Web 项目从建表到部署的完整闭环。你拿到的不是一堆孤立文件而是一条可以照着画葫芦的业务链路。适合谁马上要交毕设的本科生、复习 Servlet 生命周期和 JDBC 增删改查的求职者以及想快速搭一个练手项目的中级开发者。接下来我把它的架构、数据库、核心代码、部署顺序和常见翻车点逐一拆给你看。2. 选型逻辑为什么毕业设计里还在用 ServletJSP 而不是 Spring Boot这套项目选择 ServletJSP 而不是 Spring Boot并不是作者不会 Spring而是这类毕设选题本身就有自己的场景要求。JSPServlet 是 JavaWeb 最底层的运行模型所有请求都要经过 Servlet 的 service/doGet/doPost 方法处理页面返回需要 JSP 编译成 Servlet 再执行。这套机制虽然笨重但它把 HTTP 请求处理、Session 管理、请求转发与重定向这些底层过程全部暴露在你面前。用 Spring Boot 时一个 RequestMapping 注解就搞定了路由但你根本看不到容器是怎么把请求映射到方法的更看不懂 Session 的生命周期是谁在管理。而毕设答辩的评委恰恰喜欢问这些底层问题Servlet 是单例吗线程安全吗JSP 和 Servlet 谁先执行你用过这套项目源码这些问题就都能言之有物。我用这套项目给你的建议是不要只把它当作业交而是把它当成一个“透明黑匣子”去读。项目里每个页面请求走了什么样的链路、JDBC 连接什么时候打开什么时候关闭、事务加在了哪一层这些都是面试八股文的活素材。2.1 JDBC 连接管理最容易被忽略却决定项目能不能跑的关键很多初学者拿到一套源码第一时间打开 JSP 页面开始改样式结果发现页面能打开但数据全是空的或者点登录直接报 500。原因几乎都出在 JDBC 连接配置上。这套项目里通常会有一个 db.properties 或 jdbc.properties 配置文件里面包含数据库地址、用户名、密码、驱动类名。拿最常见的 MySQL 8.x 版本来说驱动类已经不是 com.mysql.jdbc.Driver而是 com.mysql.cj.jdbc.Driver。很多老项目的源码里写的还是旧驱动你拿到手必须改这一行才能连上数据库。另外MySQL 8.x 之后 URL 里最好加上 serverTimezoneAsia/Shanghai 和 useSSLfalse否则会出现时区报错或者 SSL 连接错误。// db.properties 配置文件示例放在 src 目录下 jdbc.drivercom.mysql.cj.jdbc.Driver jdbc.urljdbc:mysql://localhost:3306/movie_cinema?useUnicodetruecharacterEncodingutf8useSSLfalseserverTimezoneAsia/Shanghai jdbc.usernameroot jdbc.passwordyourpassword这段配置里的 jdbc.url 我建议你把 characterEncoding 一定要带上不然后台添加电影名称时中文写入数据库会变成问号。serverTimezone 是 MySQL 8.x 必须参数旧版本不加可能不报错但新版驱动一旦缺了它连接建立时必然抛异常。useSSLfalse 则是为了跳过 SSL 握手校验本地开发环境去掉它可以减少一层出错概率。2.2 DAO 模式封装为什么这套项目的代码结构值得照抄这套源码的数据访问层基本都遵循 DAO 模式每一张表对应一个 DAO 接口和实现类。比如 FilmDao 接口定义增删改查方法FilmDaoImpl 里写具体 JDBC 代码。这样做最直接的好处是控制层 Servlet 不需要知道 SQL 长什么样它只需要调用 DAO 方法拿到结果再决定转发到哪个 JSP 页面。Controller 层只做两件事收请求参数和调 DAO。页面跳转用的方式也分两种需要携带数据到页面就做 request.setAttribute 请求转发新增或者删除操作后就做重定向。为什么会这样区分请求转发地址栏不变刷新页面会重复提交表单导致数据库插入两条脏数据重定向则重新发起一次 GET 请求从根本上避免了表单重复提交问题。这套源码里如果某个功能出现刷新后数据翻倍的怪现象十有八九就是转发和重定向用混了。3. 数据库设计实战从 E-R 图到建表语句的完整拆解电影院售票系统的数据库设计是答辩时老师重点追问的部分。一张表定少了功能实现不了定多了项目过度设计反而不好讲。这套源码的典型做法是拆成五张核心表用户表、电影表、场次表、订单表、订单明细表再加上一个可选的座位表。用户表存账户信息和密码摘要密码字段不能存明文项目里一般用 MD5 加密后存入。电影表存片名、导演、主演、时长、类型、上映日期和海报路径。场次表是用户表和电影表之间的桥梁一个电影多个场次、一个场次属于一个电影它还记录了放映时间、影厅编号、票价和剩余座位数。订单表记录哪个用户买了哪个场次、买了几张票、总价多少、订单状态是什么。订单明细表则是为了支持“一次订单包含多个座位”的设计一条订单可以对应多条明细。-- 电影院售票管理系统建表核心脚本MySQL 5.7 / 8.0 均兼容 CREATE DATABASE IF NOT EXISTS movie_cinema DEFAULT CHARSET utf8mb4; USE movie_cinema; CREATE TABLE t_user ( user_id INT PRIMARY KEY AUTO_INCREMENT COMMENT 用户ID, username VARCHAR(50) NOT NULL UNIQUE COMMENT 用户名, password VARCHAR(64) NOT NULL COMMENT MD5加密后的密码, real_name VARCHAR(50) COMMENT 真实姓名, phone VARCHAR(20) COMMENT 手机号, create_time DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT 注册时间 ) ENGINEInnoDB COMMENT用户表; CREATE TABLE t_film ( film_id INT PRIMARY KEY AUTO_INCREMENT COMMENT 电影ID, film_name VARCHAR(100) NOT NULL COMMENT 电影名称, film_type VARCHAR(50) COMMENT 电影类型, director VARCHAR(50) COMMENT 导演, actor VARCHAR(100) COMMENT 主演, duration INT COMMENT 片长分钟, poster_path VARCHAR(255) COMMENT 海报图片路径, status TINYINT DEFAULT 1 COMMENT 1上架 0下架 ) ENGINEInnoDB COMMENT电影表; CREATE TABLE t_session ( session_id INT PRIMARY KEY AUTO_INCREMENT COMMENT 场次ID, film_id INT NOT NULL COMMENT 所属电影ID, hall_name VARCHAR(30) COMMENT 影厅名称, play_time DATETIME NOT NULL COMMENT 放映时间, price DECIMAL(8,2) NOT NULL COMMENT 票价, remaining_seats INT NOT NULL COMMENT 剩余座位数, FOREIGN KEY (film_id) REFERENCES t_film(film_id) ) ENGINEInnoDB COMMENT场次表; CREATE TABLE t_order ( order_id INT PRIMARY KEY AUTO_INCREMENT COMMENT 订单ID, user_id INT NOT NULL COMMENT 下单用户ID, session_id INT NOT NULL COMMENT 场次ID, total_price DECIMAL(8,2) NOT NULL COMMENT 总价, status TINYINT DEFAULT 1 COMMENT 1已支付 0待支付, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (user_id) REFERENCES t_user(user_id), FOREIGN KEY (session_id) REFERENCES t_session(session_id) ) ENGINEInnoDB COMMENT订单表; CREATE TABLE t_order_detail ( detail_id INT PRIMARY KEY AUTO_INCREMENT, order_id INT NOT NULL, seat_row INT NOT NULL COMMENT 排号, seat_col INT NOT NULL COMMENT 列号, FOREIGN KEY (order_id) REFERENCES t_order(order_id) ) ENGINEInnoDB COMMENT订单明细表;这套建表脚本值得注意的一点是场次表没有单独建“影厅表”而是直接用 hall_name 字符串字段存影厅名称。这个设计有它的简化逻辑因为该项目不需要精细管理影厅的容量配置只需要知道这个场次在哪个厅、还剩多少个座位。remaining_seats 字段在每次购票成功后要记得做原子性更新用 SQL 语句的SET remaining_seats remaining_seats - #{count}而不是先查出来再减这样可以避免并发超卖。外键的保留也是答辩加分项。很多企业开发为了性能会去掉外键约束但毕设项目我个人建议你把外键留着因为老师问到“如何保证数据一致性”时你可以直接拿外键和事务回答这是最直观的讲解素材。3.1 MySQL 安装与初始化连接报错 2002 的排查方向很多人在部署这套项目时卡在 MySQL 环境上最常见的错误是ERROR 2002 (HY000): Cant connect to local MySQL server through socket /tmp/mysql.sock。这个问题在 Linux 服务器上出现频率最高在 Windows 上用 IDEA 连接时也有类似表现。现象的本质是MySQL 服务没有启动或者客户端连接时使用的套接字路径与实际不符。解决步骤很简单先确认服务是否在运行再看配置文件里 socket 路径和客户端期望值是否一致。如果你用的是 Windows大概率不是 socket 而是服务没启动打开服务管理器找到 MySQL 服务点启动即可。这套源码的 SQL 脚本导入我一般建议用命令行方式而不是 Navicat因为命令行导入出错信息更直观。先创建数据库再导入表结构然后插入几条测试数据验证编码没有问题。# 命令行导入数据库脚本 mysql -uroot -p123456 -e source /path/to/movie_cinema.sql; # 验证表结构是否正确 mysql -uroot -p123456 -e USE movie_cinema; SHOW TABLES;导入完成后一定要验证一下中文数据是否乱码。检查三个方面数据库和表是否 utf8mb4 编码、JDBC 连接 URL 是否配置 characterEncodingutf8、JSP 页面第一行是否声明 pageEncodingUTF-8。这三个环节只要有任何一个不是 UTF-8页面显示就会出现问号或乱码这是这套老牌运维场景里最经典的问题。3.2 文件上传与图片回显海报路径的设计边界电影管理模块里有一个常见的文件上传功能用于上传电影海报。这套项目里一般用的是 commons-fileupload 组件它会解析 multipart 请求并把文件写入服务器某个目录然后把相对路径保存到数据库 t_film 表的 poster_path 字段。这里你需要注意一个路径设计的细节。如果存的是绝对路径比如/Users/xxx/Downloads/apache-tomcat-9.0.x/webapps/upload/xxx.jpg那么项目换一台电脑部署就全部失效了因为绝对路径绑定了原机器。正确做法是存相对路径比如upload/xxx.jpg然后页面用 contextPath 拼接访问。JSP 页面里通过${pageContext.request.contextPath}来取项目根路径这样才能保证部署到任何一台机器上图片都能正常回显。这个文件上传组件在实际运行中还有一个隐蔽的坑Tomcat 默认配置下POST 请求的大小是有限制的超过 2MB 的图片会被 Tomcat 直接拒绝抛出异常。解决方法是修改 web.xml 配置或者在 Servlet 里手动设置request.setAttribute(org.apache.tomcat.context, context)这种方式但更实际的办法是限制上传图片大小在 1MB 以内。你可以在前端用 JavaScript 校验也可以在 Servlet 端判断 FileItem 的 size 属性。4. 核心代码拆解从 JDBC 封装到售票业务的完整链路这章是整篇最值得你花精力的部分。我按照一个用户从注册到购票成功的完整流程把源码里最典型的几段代码摘出来每一段都会告诉你它在项目里承担什么角色、参数怎么改、踩坑时应该看什么。4.1 数据库连接工具类所有 DAO 代码的地基这套项目的所有 DAO 实现类第一行几乎都是DBUtil.getConnection()所以这个工具类的质量决定了整个数据访问层会不会报连接错误。标准写法是静态代码块加载驱动然后提供 getConnection 和 close 方法。import java.sql.Connection; import java.sql.DriverManager; import java.sql.ResultSet; import java.sql.SQLException; import java.sql.Statement; import java.util.ResourceBundle; public class DBUtil { private static String driver; private static String url; private static String username; private static String password; // 静态代码块类加载时执行一次读取配置文件 static { try { ResourceBundle bundle ResourceBundle.getBundle(db); driver bundle.getString(jdbc.driver); url bundle.getString(jdbc.url); username bundle.getString(jdbc.username); password bundle.getString(jdbc.password); Class.forName(driver); } catch (ClassNotFoundException e) { e.printStackTrace(); } } public static Connection getConnection() throws SQLException { return DriverManager.getConnection(url, username, password); } // 关闭资源统一入口 public static void close(Connection conn, Statement stmt, ResultSet rs) { try { if (rs ! null) rs.close(); if (stmt ! null) stmt.close(); if (conn ! null) conn.close(); } catch (SQLException e) { e.printStackTrace(); } } }这段代码有三个细节值得你注意。第一ResourceBundle.getBundle 不写文件后缀它默认从 classpath 根目录找db.properties文件如果你的配置文件名和代码不一致会直接报 NullPointerException。第二Class.forName 在 MySQL 8.x 的驱动包下其实可以省略因为 JDBC 4.0 之后驱动会自动注册但我建议保留它为了兼容老版本驱动也为了答辩时能讲清楚驱动加载机制。第三close 方法的顺序必须是 ResultSet、Statement、Connection 倒着关如果只关 Connection 不关 Statement连接池模式下会逐渐耗尽连接。4.2 用户登录模块MD5 加密与 Session 会话跟踪登录是每个 Web 项目的门面。这套源码的用户登录模块通常会这样实现用户提交用户名和密码Servlet 接收后用 MD5 加密密码再拿加密后的字符串去数据库查询查到了就放 Session查不到就返回错误信息。// LoginServlet 核心代码 import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; public class LoginServlet extends HttpServlet { // 对密码做 MD5 加密 public static String md5(String input) { try { MessageDigest md MessageDigest.getInstance(MD5); byte[] digest md.digest(input.getBytes(UTF-8)); StringBuilder sb new StringBuilder(); for (byte b : digest) { // 转成十六进制字符串 String hex Integer.toHexString(b 0xff); if (hex.length() 1) { sb.append(0); } sb.append(hex); } return sb.toString(); } catch (Exception e) { e.printStackTrace(); return null; } } Override protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { request.setCharacterEncoding(UTF-8); String username request.getParameter(username); String password request.getParameter(password); if (username null || password null || username.trim().isEmpty()) { request.setAttribute(error, 用户名和密码不能为空); request.getRequestDispatcher(/login.jsp).forward(request, response); return; } // 密码加密后与数据库比对 String encrypted md5(password); UserDao userDao new UserDaoImpl(); User user userDao.findByUsernameAndPassword(username, encrypted); if (user ! null) { // 登录成功保存会话 HttpSession session request.getSession(); session.setAttribute(loginUser, user); response.sendRedirect(request.getContextPath() /index.jsp); } else { // 登录失败回显错误 request.setAttribute(error, 用户名或密码错误); request.getRequestDispatcher(/login.jsp).forward(request, response); } } }这段登录代码在面试中被问到的概率极高。第一个问题是 MD5 加密为什么不安全标准回答是 MD5 存在彩虹表碰撞风险真实项目中要加盐比如 MD5(password salt)。第二个问题是为什么登录成功用 sendRedirect 而失败用 forward原因是成功后必须让浏览器重新请求一次首页避免刷新时重复提交登录表单失败时则要在 request 范围内传递错误消息用 forward 才能把数据带到 JSP 页面里。Session 的默认空闲超时时间是 30 分钟这在 Tomcat 的 web.xml 里配置。如果你希望用户登录后长时间保持在线可以修改这个值但我建议毕设项目不修改保留默认就够用了。另外注意 Session 的并发问题同一个用户打开两个浏览器标签页会有两个不同的 Session 对象所以不要用实例变量保存登录用户信息每次都从 request.getSession() 获取。4.3 选座购票与事务控制防止半单出现的核心方案这套项目里最重要的业务操作是用户选定场次、选择座位、生成订单。这个过程涉及三个操作检查座位是否已被占用、更新场次剩余座位数、插入订单和订单明细。这三个操作必须放在一个事务里任何一步失败都要全部回滚否则会出现“票扣了但订单没生成”或者“订单生成了但座位没扣”的脏数据。// 购票事务核心代码 import java.sql.Connection; import java.sql.PreparedStatement; import java.sql.ResultSet; import java.sql.SQLException; public class BuyTicketService { private DBUtil dbUtil new DBUtil(); public boolean buyTicket(int userId, int sessionId, int seatRow, int seatCol, double price) { Connection conn null; try { conn DBUtil.getConnection(); // 关键点1关闭自动提交 conn.setAutoCommit(false); // 1. 检查座位是否已售出 String checkSql SELECT COUNT(*) FROM t_order_detail d INNER JOIN t_order o ON d.order_id o.order_id WHERE o.session_id ? AND d.seat_row ? AND d.seat_col ?; PreparedStatement checkPs conn.prepareStatement(checkSql); checkPs.setInt(1, sessionId); checkPs.setInt(2, seatRow); checkPs.setInt(3, seatCol); ResultSet rs checkPs.executeQuery(); rs.next(); int count rs.getInt(1); if (count 0) { conn.rollback(); return false; } rs.close(); checkPs.close(); // 2. 插入订单主记录 String orderSql INSERT INTO t_order(user_id, session_id, total_price, status) VALUES (?, ?, ?, 0); PreparedStatement orderPs conn.prepareStatement(orderSql, PreparedStatement.RETURN_GENERATED_KEYS); orderPs.setInt(1, userId); orderPs.setInt(2, sessionId); orderPs.setDouble(3, price); orderPs.executeUpdate(); // 获取自增主键 ResultSet keys orderPs.getGeneratedKeys(); int orderId -1; if (keys.next()) { orderId keys.getInt(1); } keys.close(); orderPs.close(); // 3. 插入订单明细 String detailSql INSERT INTO t_order_detail(order_id, seat_row, seat_col) VALUES (?, ?, ?); PreparedStatement detailPs conn.prepareStatement(detailSql); detailPs.setInt(1, orderId); detailPs.setInt(2, seatRow); detailPs.setInt(3, seatCol); detailPs.executeUpdate(); detailPs.close(); // 4. 扣减剩余座位数 String updateSql UPDATE t_session SET remaining_seats remaining_seats - 1 WHERE session_id ? AND remaining_seats 0; PreparedStatement updatePs conn.prepareStatement(updateSql); updatePs.setInt(1, sessionId); int rows updatePs.executeUpdate(); if (rows 0) { conn.rollback(); return false; } updatePs.close(); // 全部成功提交事务 conn.commit(); return true; } catch (SQLException e) { e.printStackTrace(); if (conn ! null) { try { conn.rollback(); } catch (SQLException rollbackEx) { rollbackEx.printStackTrace(); } } return false; } finally { try { if (conn ! null) { // 关键点2恢复自动提交避免连接归还连接池后状态污染 conn.setAutoCommit(true); conn.close(); } } catch (SQLException e) { e.printStackTrace(); } } } }这段代码里最关键的是第四个操作——更新剩余座位数时加了AND remaining_seats 0条件并且检测受影响行数。这是防超卖的最后一道防线即使两个用户同时买同一个场次的最后一张票数据库层面也只有一个 update 能成功另一个 affected rows 为 0事务回滚。这个写法比先 select 再判断再 update 的方式并发安全得多是整套源码里最有含金量的一行。另外两个细节也需要留意PreparedStatement.RETURN_GENERATED_KEYS 用于获取自增主键适用于 MySQL 的 AUTO_INCREMENT 字段finally 块里把 autoCommit 恢复成 true是为了保证连接归还给连接池时状态是干净的否则下一次复用这个连接时setAutoCommit(false) 的状态残留可能导致后续操作事务行为异常。如果你在跑项目时发现某个更新操作莫名其妙地不生效检查是否在这里没恢复自动提交。4.4 列表分页PreparedStatement 的 SQL 注入防线电影列表和场次列表一般都有分页功能。这套项目常见的实现是 PageBean 模型包含当前页码、每页条数、总记录数、总页数和数据列表。分页 SQL 用的是 MySQL 特有的 LIMIT 语法。public ListFilm findPage(int pageNum, int pageSize) { ListFilm list new ArrayList(); // 计算起始位置 int offset (pageNum - 1) * pageSize; String sql SELECT film_id, film_name, film_type, director, actor, duration, poster_path FROM t_film WHERE status 1 ORDER BY film_id DESC LIMIT ?, ?; try (Connection conn DBUtil.getConnection(); PreparedStatement ps conn.prepareStatement(sql)) { ps.setInt(1, offset); ps.setInt(2, pageSize); ResultSet rs ps.executeQuery(); while (rs.next()) { Film film new Film(); film.setFilmId(rs.getInt(film_id)); film.setFilmName(rs.getString(film_name)); film.setFilmType(rs.getString(film_type)); // 剩余字段省略 list.add(film); } rs.close(); } catch (SQLException e) { e.printStackTrace(); } return list; }在 JavaWeb 分页里LIMIT 后的两个参数必须是整数绝不能从字符串拼接 SQL 传入。你如果写成String sql ... LIMIT offset , pageSize看起来没问题但一旦页码参数是从用户输入读取的恶意构造的字符串就能改变整个 SQL 语义。PreparedStatement 的占位符会在驱动层做转义这是项目里最基础但最重要的安全防线。第 2 页和第 3 页分页最常出的 Bug 是当前页为空或超出最大值时页面直接白屏。处理办法是 Servlet 接收到 pageNum 参数后做一次默认值处理小于 1 就按 1 算大于总页数就按总页数算。5. 部署复现与避坑指南从 Tomcat 到 MySQL 的常见翻车现场毕设项目拿到手最怕的不是代码看不懂而是跑不起来。我把这套电影售票系统最常见的五个坑按“现象 → 原因 → 解决”的顺序列出来这些全部是我实际帮人调试时遇到的你照着走可以节省大量时间。5.1 IDEA 部署 Tomcat 时显示 404 找不到 Servlet现象项目启动后访问/login返回 404但 index.jsp 能打开。原因IDEA 的 Artifact 打包配置里没有把依赖库包含进 war或者 Servlet 映射路径和访问路径不一致。还有一种可能Servlet 类的 WebServlet 注解没生效因为你用的 Servlet 版本是旧的web.xml 里没有配置 metadata-completefalse。解决先确认访问路径和注解值完全一致注意大小写和斜杠。然后在 Project Structure → Artifacts 里检查 Output Layout 是否包含所有 lib 依赖。如果你的项目用的是 web.xml 配置 Servlet检查servlet-mapping的 url-pattern 是否以斜杠开头。5.2 数据库中文乱码控制台看到问号现象往数据库插入“复仇者联盟”查出来是“??????”。原因连接 URL 没有 characterEncoding或者 MySQL 表本身是 latin1 编码JSP 页面也没有统一编码。解决按数据库编码、连接 URL、页面编码三处逐一排查。数据库表改成 utf8mb4连接 URL 加 characterEncodingutf8每个 JSP 页面第一行写% page languagejava contentTypetext/html; charsetUTF-8 pageEncodingUTF-8%前端页面里加meta charsetUTF-8。这三处统一后乱码基本消失。5.3 MySQL 8.x 连接报 SSL 连接错误现象启动项目时控制台出现SSL connection error: java.net.SocketException或者Public Key Retrieval is not allowed。原因MySQL 8.x 默认开启 SSL本地开发环境没有有效证书useSSLfalse参数没配置时驱动默认尝试加密连接导致握手失败。解决这一步是这套老项目最常见的坑。打开 db.properties把 URL 改成jdbc:mysql://localhost:3306/movie_cinema?useUnicodetruecharacterEncodingutf8useSSLfalseserverTimezoneAsia/ShanghaiallowPublicKeyRetrievaltrue重点是要加上allowPublicKeyRetrievaltrue因为 MySQL 8.x 在使用 caching_sha2_password 认证时如果不允许公钥获取就会一直握手失败。5.4 上传海报后图片无法显示现象后台添加电影时上传了图片前台页面图片位置是裂图。原因图片存到了某个绝对路径下但项目打包部署后实际路径和开发时不同或者 JSP 页面里的 img 标签 src 是写死的绝对路径。解决检查数据库里的 poster_path 存的是upload/xxx.jpg还是/Users/xxx/upload/xxx.jpg。如果是绝对路径导出一张正确的相对路径数据做对照如果是相对路径看页面上是否用了${pageContext.request.contextPath}拼接。Tomcat 的 webapps 目录下的项目文件夹名也要确认否则 contextPath 不对。5.5 表单重复提交刷新页面多出一条订单现象用户支付成功后按 F5 刷新订单列表多了一条一模一样的订单。原因Servlet 里用了请求转发而不是重定向。用户提交 POST 请求后服务器执行 forward浏览器地址栏还是原表单的 URL刷新时会重新提交同样的 POST 请求。解决修改订单和注册这类有写操作的 Servlet在操作成功后改用response.sendRedirect(request.getContextPath() /orderList)重定向到列表页。这是一个代码质量考察点能做到这一点说明你对 HTTP 状态语义确实理解到位了。6. 加分项与验证技巧从能跑到能答辩的进阶路径如果你想让这套电影院售票系统在答辩时更出彩我建议不要停留在“能跑通”阶段而是继续做几个针对性地增强。第一个建议是为 JDBC 连接层引入一个连接池比如 DBCP 或 C3P0。原因很简单现有的 DBUtil 每次连接都通过 DriverManager 新建物理连接并发量稍微上来就会很慢。引入连接池后连接复用能显著提升性能而且很容易展示你的性能优化意识。!-- C3P0 连接池配置片段替换原有 DBUtil -- c3p0-config default-config property namedriverClasscom.mysql.cj.jdbc.Driver/property property namejdbcUrljdbc:mysql://localhost:3306/movie_cinema?useSSLfalseamp;serverTimezoneAsia/Shanghai/property property nameuserroot/property property namepassword123456/property property nameinitialPoolSize5/property property namemaxPoolSize20/property property namemaxIdleTime60/property /default-config /c3p0-config这个配置解释清楚为什么改就行。黑板上的核心变化是原来每次 SQL 操作都要经历“建立连接、执行、关闭”三步现在连接建立后保存在池里用完归还而不是销毁。我在验证连接池是否生效时有个习惯做法开启连接池后连续执行 20 次查询对比 DBUtil 原写法和连接池写法的耗时同时打开 MySQL 的 general_log 查看实际建立了多少次连接。这样写进测试报告里答辩老师看到的数据对比比任何口头描述都有说服力。第二个建议是给用户密码加盐。你可以定义自己的规则比如用户名反转加固定盐值再和密码拼接后做 MD5把这个规则写成一个工具类。这份工具的代码量不大但它能反映出你对安全基础的理解。回想一下毕设答辩时“MD5 为什么不安全”这个问题被问到的概率极高你提前把这个应对策略写在设计文档里提问环节能省下很多胶着时间。第三个建议是补全管理员模块的数据图表展示用原生 Servlet 传 JSON 到 ECharts。只需要再写一个统计 Servlet 返回 JSON 字符串前端页面通过 JSP 内置 JS 发起 AJAX 请求拿到数据再渲染。这个功能在电影票房的场景下特别合适——按电影分组统计各影片售票数量画个柱状图整个项目立刻变得像一个完整的产品而不只是单个流程的业务练习。最后说回验证技巧。所有代码改完之后我强烈建议你走一遍全流程测试而不是只测登录和查询。完整流程测试顺序是注册新用户 → 登录 → 查看电影列表 → 进入场次详情 → 选座购票 → 查看订单 → 管理员登录 → 添加电影 → 上传海报 → 下架电影。每一步都记录测试结果发现异常就按第五章的排错清单去处理。因为这整套流程里每一步都可能出现问题而每个问题的根因大概率不是孤立的比如场次不显示可能是电影没上架图片打不开可能是相对路径拼接错误。从那以后我每次拿到一套不熟悉的 JavaWeb 源码都会强制走一遍“从注册到下单”的链路测试这是项目价值判定的最快方式。希望你也能用这套方式把这套电影售票系统彻底吃透祝你的项目和答辩一切顺利。本文还有配套的精品资源点击获取