
简介一套完整的Java电影票购票管理系统桌面应用采用Swing构建图形用户界面配合MySQL数据库管理电影、场次与座位数据完整模拟了用户选座、购票、订单生成的交互流程主要面向Java初学者、数据库入门者以及需要课程设计参考的学生。资源包共二百三十六个文件大小约二百三十二兆内含五十二个源代码文件、一百二十八个编译类文件、数据库脚本、视频运行教程、多张运行截图和运行环境说明源码、部署与演示资料一应俱全。目前已有1855人学习下载具有一定的学习参考价值。视频教程从环境配置到项目启动逐步演示能帮助快速跑通系统数据库脚本预置了演示数据可直接体验购票全过程通过阅读源码和对照截图可以清晰理解Swing事件监听、JDBC数据库访问、MVC分层设计等核心知识点有效提升Java桌面应用开发能力。1. 为什么一个 java 电影票购票管理系统能逼你把 Web 开发真正串起来很多课程设计里的 Java Web 项目最后验收标准就是“能增删改查”写完也就忘了。但电影票购票管理系统不一样它同时涉及用户注册登录、影片和场次展示、选座、下单、支付状态流转、订单超时释放座位这套业务放在一起会逼着你去处理并发冲突和数据一致性而不只是拼凑 CRUD 页面。对正在学 Java 的开发者来说拿到一份带视频讲解和源码的课设项目首要目标不是抄着交差而是跑通以后把它拆开、改掉、补上自己的边界处理。这个系统适合三类人准备交 Java 课程设计的学生、想积累一个完整项目经验的初级开发、以及面试前需要把业务状态机讲清楚的求职者。下面我按自己的落地经验把表设计、后端逻辑、并发坑位和改造方向一次讲完。2. 先设计数据表再写 Java 代码把购票流程拆成四张表和三个状态2.1 一次购票背后有哪些数据在参与先从用户的一次真实操作看。用户打开购票页面先选一部电影然后选择某个影厅的场次接着在座位图上点一个座位再提交订单并支付。这个流程至少会涉及用户、影片、影厅场次、座位、订单五种信息。很多新手容易犯的错是只做一张订单表把影片名、场次时间、座位号全塞在订单里。这样做不是不能跑但一旦要查“某个场次哪些座位已售出”就会非常痛苦只能一条条订单去解析字符串。所以我在设计课设源码时第一件事不是写 Java 类而是把表拆干净。常见做法是拆出五张表用户表、影片表、场次表、座位表、订单表。座位表不和影片直接关联而是和场次关联因为同一个影厅在不同时间放映的电影不同座位状态必须属于某一场次。这个设计的核心思想是座位不是物理座位而是某场次下的一张可售凭证。订单表在这里承担的是交易记录职责它需要记住用户、场次、座位、金额和状态。如果把订单表做成一张宽表虽然查询简单了但座位释放、退款、改签等后续逻辑都会无从下手。一个比较稳妥的判断标准是凡是可能发生状态流转的数据都应该有自己的独立字段和独立表而不是拼在字符串里。座位有“可选 / 锁定 / 已售”的状态订单也有“待支付 / 已支付 / 已取消”的状态这两类状态要分开管理。2.2 建表 SQL字段设计的几个关键决定下面这组 SQL 是按 MySQL 写的也兼容大多数课设环境。我特意把 seat 表和 orders 表分开order 表里再用 schedule_id 和 seat_id 做唯一索引这是后面防止并发超卖的关键设计。CREATE TABLE user ( id INT NOT NULL AUTO_INCREMENT, username VARCHAR(50) NOT NULL, password VARCHAR(64) NOT NULL COMMENT 加盐后的哈希值, salt VARCHAR(16) NOT NULL, phone VARCHAR(20) DEFAULT NULL, created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), UNIQUE KEY uk_username (username) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; CREATE TABLE film ( id INT NOT NULL AUTO_INCREMENT, title VARCHAR(100) NOT NULL, duration_minutes INT NOT NULL, release_date DATE DEFAULT NULL, poster_url VARCHAR(255) DEFAULT NULL, PRIMARY KEY (id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; CREATE TABLE schedule ( id INT NOT NULL AUTO_INCREMENT, film_id INT NOT NULL, hall VARCHAR(20) NOT NULL COMMENT 影厅号, start_time DATETIME NOT NULL, price DECIMAL(10,2) NOT NULL, PRIMARY KEY (id), KEY idx_film_id (film_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; CREATE TABLE seat ( id INT NOT NULL AUTO_INCREMENT, schedule_id INT NOT NULL, row_no INT NOT NULL, col_no INT NOT NULL, status TINYINT NOT NULL DEFAULT 0 COMMENT 0可选 1已售 2锁定, PRIMARY KEY (id), UNIQUE KEY uk_schedule_row_col (schedule_id, row_no, col_no) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; CREATE TABLE orders ( id INT NOT NULL AUTO_INCREMENT, order_no VARCHAR(32) NOT NULL, user_id INT NOT NULL, schedule_id INT NOT NULL, seat_id INT NOT NULL, amount DECIMAL(10,2) NOT NULL, status TINYINT NOT NULL DEFAULT 0 COMMENT 0待支付 1已支付 2已取消, created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, paid_at DATETIME DEFAULT NULL, PRIMARY KEY (id), UNIQUE KEY uk_schedule_seat (schedule_id, seat_id), KEY idx_user_id (user_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;这段 SQL 里有两个必须理解的参数。第一个是 seat 表里的 UNIQUE KEY uk_schedule_row_col它保证同一个场次下不会出现重复的座位记录也就是说你初始化座位时写错了循环次数数据库也会把重复数据拦住。第二个是 orders 表里的 UNIQUE KEY uk_schedule_seat它保证同一个场次的同一个座位只能有一条订单记录即使你的 Java 代码里忘了加锁数据库也会让其中一条并发插入失败。我不建议在 seat 表里直接存“已购买用户的用户名”因为座位状态只关心是否被占用谁买的应该由订单表来回答。如果强行耦合后面做退款和释放座位时就需要对两张表做同步更新事务边界会变长。初始化座位时也容易翻车。我见过有人用手写 200 条 INSERT 的源码换一个影厅就废了。更稳的做法是给 schedule 表生成完成后用一条 INSERT SELECT 批量生成座位INSERT INTO seat (schedule_id, row_no, col_no) SELECT s.id, r.n, c.n FROM schedule s CROSS JOIN ( SELECT 1 AS n UNION ALL SELECT 2 UNION ALL SELECT 3 UNION ALL SELECT 4 UNION ALL SELECT 5 ) r CROSS JOIN ( SELECT 1 AS n UNION ALL SELECT 2 UNION ALL SELECT 3 UNION ALL SELECT 4 UNION ALL SELECT 5 ) c;这段 SQL 的逻辑是把 5 个行号和 5 个列号做笛卡尔积再和所有场次做关联一次性给每个场次生成 25 个座位。行数和列数可以根据影厅规模调整但保持这种数据驱动的方式后续改影厅容量只需要改子查询不需要动业务代码。2.3 为什么源码里普遍用 Servlet JSP而不是直接用 Spring Boot拿到这套“视频源码”的读者打开工程大概率会看到 Servlet JSP 的组合而不是 Spring Boot。这会让不少人疑惑现在外面面试不都是问 Spring Boot 吗这里我说一下自己的判断。最常见的情况是学校 Java Web 课程还在用原生 Servlet 讲请求处理、会话管理和过滤器课设题目也是围绕这些知识点出的。用 Servlet JSP 能让你看清一次 HTTP 请求从 Tomcat 到 Servlet 再到 JSP 输出的完整链路而 Spring Boot 内置 Tomcat自动配置会把这些过程包起来初学者反而容易把“堵在 Filter 里的逻辑”当成玄学。我更建议先把 Servlet 版本跑通改完课题后再对照迁移到 Spring Boot而不是一开始就换框架。源码里如果连接数据库用的是原生 JDBC跑通后可以把连接管理替换成 Druid 或 HikariCP 连接池。这个替换动作很小但会让简历上多一条“数据库连接池优化”的实践记录。迁移时只需要改工具类的取连接方式DAO 层接口不用动。视频教程的价值就在这里它讲的是 Tomcat 部署和 JSP 运行机制这部分知识换成 Spring Boot 之后依然适用。3. 从零跑通最小版本JDBC 连接、登录注册与影片列表3.1 JDBC 连接 MySQL驱动、URL 参数和连接池的取舍源码里常见的工具类长这样。这个类负责加载驱动和返回连接是所有 DAO 的基础。public class JdbcUtil { private static final String URL jdbc:mysql://localhost:3306/cinema?useSSLfalsecharacterEncodingutf8serverTimezoneAsia/Shanghai; private static final String USER root; private static final String PASSWORD 123456; static { try { Class.forName(com.mysql.cj.jdbc.Driver); } catch (ClassNotFoundException e) { throw new RuntimeException(加载MySQL驱动失败, e); } } public static Connection getConnection() throws SQLException { return DriverManager.getConnection(URL, USER, PASSWORD); } }这段代码里有几个参数值得解释。useSSLfalse 是为了避免本地开发时 MySQL 自动开启 SSL 握手不填会看到一条警告但不一定会报错。serverTimezoneAsia/Shanghai 是给 MySQL 8.0 用的因为新版连接驱动要求客户端明确提供时区否则会直接抛异常。characterEncodingutf8 则是让写入数据库的中文不乱码。如果源码里带的还是 com.mysql.jdbc.Driver要在 MySQL 8.0 下跑通建议更新驱动包到 mysql-connector-java 8.0.x并把类名改成 com.mysql.cj.jdbc.Driver。很多运行时报错问来问去最后其实就卡在这一个类名上。到了项目验收阶段我一般会建议把 DriverManager 换成连接池。最简单的方式是用 HikariCP因为它的依赖只有一个 jar配置起来又比 Druid 少。下面这段是替换后的取连接逻辑。public class DbPool { private static HikariDataSource dataSource; static { HikariConfig config new HikariConfig(); config.setJdbcUrl(jdbc:mysql://localhost:3306/cinema?useSSLfalsecharacterEncodingutf8serverTimezoneAsia/Shanghai); config.setUsername(root); config.setPassword(123456); config.setMaximumPoolSize(10); config.setMinimumIdle(2); dataSource new HikariDataSource(config); } public static Connection getConnection() throws SQLException { return dataSource.getConnection(); } }这段代码的逻辑是在类加载时创建连接池后续每次 getConnection 都从池里借连接用完调用 close 后连接归还而不是真断掉。调参的两个重点是 maximumPoolSize 和 minimumIdle。对本系统来说10 和 2 足够应付几百个用户同时刷页面别再往大了调连接数过多反而会让 MySQL 的线程切换变慢。3.2 注册登录的密码处理加盐哈希与 Session 登录态很多课设源码在 user 表里直接存明文密码这能跑但很不好看。只要把密码处理改一下整个项目的安全完成度会立刻不同。密码不存明文常见做法是存“盐 哈希”也就是每个用户注册时生成一段随机字符串把密码和盐拼起来再做 MD5。public class PasswordUtil { public static String generateSalt() { byte[] bytes new byte[8]; new SecureRandom().nextBytes(bytes); return HexFormat.of().formatHex(bytes); } public static String md5WithSalt(String rawPassword, String salt) { String toHash salt : rawPassword; try { MessageDigest md MessageDigest.getInstance(MD5); byte[] digest md.digest(toHash.getBytes(StandardCharsets.UTF_8)); return HexFormat.of().formatHex(digest); } catch (NoSuchAlgorithmException e) { throw new RuntimeException(e); } } }这里的重点是数据库里的 password 字段保存的是 md5WithSalt 的结果salt 字段保存的是随机串。登录时你要做的是从数据库取出这条用户记录把输入的明文密码和库里的盐重新算一次哈希再和库里的密码字段比对。如果一张 user 表里两条记录的盐相同说明生成随机盐的代码有很大概率出了问题需要检查 SecureRandom 的初始化位置。登录成功之后用 Session 保存用户身份是最直接的做法。在 Servlet 里登录接口的大致逻辑是下面这样。protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { String username req.getParameter(username); String rawPassword req.getParameter(password); User user userDao.findByUsername(username); if (user ! null user.getPassword().equals(PasswordUtil.md5WithSalt(rawPassword, user.getSalt()))) { req.getSession().setAttribute(loginUser, user); resp.sendRedirect(req.getContextPath() /film/list); } else { req.setAttribute(error, 用户名或密码错误); req.getRequestDispatcher(/login.jsp).forward(req, resp); } }这个接口里的校验方式对读代码的人很友好先查用户名再比对哈希。比对成功后把整个 User 对象放 Session后续页面都能通过 session.getAttribute(loginUser) 拿到当前用户。你还可以顺手把用户 id 单独存进去因为后面下单时订单表只关心 user_id 这个数值不需要把整个对象卷入业务逻辑。3.3 影片列表与 JSP 渲染JSTL 和分页参数影片列表页是整套系统里最直观的一个页面。它要做的事是从数据库取当前可购票的电影循环渲染成卡片或表格。原生 JSP 里写 Java 代码虽然能跑但页面会很乱更好的做法是用 JSTL 标签。% taglib prefixc urihttp://java.sun.com/jsp/jstl/core % table tr th影片名称/th th时长/th th上映日期/th th操作/th /tr c:forEach varfilm items${filmList} tr td${film.title}/td td${film.durationMinutes} 分钟/td td${film.releaseDate}/td tda href${pageContext.request.contextPath}/schedule/list?filmId${film.id}选择场次/a/td /tr /c:forEach /table这段 JSP 只做一件事把 Servlet 放进 request 作用域里的 filmList 遍历输出。这里有一个新手常踩的坑Servlet 里必须用 request.setAttribute(filmList, list)如果写的是 request.getSession().setAttribute页面也能显示但会在浏览器里多留存一份数据导致退出登录后还能看到上一个账号刷过的列表。列表页的主要责任是展示数据应该放在一次请求里用完就丢。分页参数也遵循这个思路。Servlet 里接收 page 和 pageSize 两个参数page 默认为 1pageSize 默认为 10然后执行 SQL 的 LIMIT 语句。这里的核心参数是 page 的边界处理page 小于 1 时按 1 算pageSize 超过 50 时按 50 算防止有人用 pageSize 很大的请求直接把整个表拖出来。分页 SQL 放在 DAO 层大概长这样public ListFilm findPage(int page, int pageSize) { String sql SELECT id, title, duration_minutes, release_date FROM film ORDER BY id LIMIT ? OFFSET ?; int offset (page - 1) * pageSize; // 用 PreparedStatement 绑定 pageSize 和 offset注意类型是 Integer }这条 SQL 的逻辑是LIMIT 控制取多少行OFFSET 控制从第几行开始。页面里点击下一页时只需要把当前页码加一传给 Servlet。你不需要在 JSP 里自己拼复杂的分页条先把参数传对再优化样式。视频教程里讲的“跳转第几页”本质上就是改 page 参数理解这一点比抄一套分页组件更重要。4. 选座下单与支付状态机并发和超时才是这套系统的重头戏4.1 座位和订单的状态流转模型把业务状态画清楚写代码才不会改成一团浆糊。这里只讲两个核心状态机。第一个是座位状态。对一个场次下的某个座位它只有三种状态0 可选1 已售2 锁定。用户打开选座页时看到的是可选和已售用户确认要买但还没支付时座位进入锁定这是为了给当前用户一段预留时间防止别人抢走用户支付成功或者订单超时取消后座位变成已售或回到可选。第二个是订单状态。orders 表里的 status 字段也有三个取值0 待支付1 已支付2 已取消。从待支付到已支付通常发生在支付回调时从待支付到已取消有两种途径一是用户主动取消二是系统定时扫描超时订单后自动取消。这里必须强调一个顺序座位状态和订单状态要放在同一个事务里更新。最常见的错误是先把订单状态改成已取消再去把座位改成可选这两个操作如果之间隔了一次网络调用或跨了方法就有可能在中间状态里出现“订单已取消但座位还是锁定”的情况。我一般会在 Service 层用一个事务方法把两件事包起来不让它们拆开。注意不要把订单状态和座位状态拆到两个事务里一旦第二个事务执行失败订单和座位的数据就会不一致而且很难通过日志发现。4.2 防止同一座位被重复卖出唯一索引和条件更新在课设场景里同一个座位在同一场次被两个人同时下单这是最容易翻车的地方。原因并不神秘两个请求并发地读到座位状态为 0然后各自插入一条订单如果数据库层没有任何约束两条订单都会成功。代码层加锁可以解决一部分问题但最常见也最可靠的防线是先建数据库约束再用条件更新。建表 SQL 里已经加了 orders 表的 uk_schedule_seat 唯一索引下一步是让插入订单前先对 seat 表做一次条件更新。Transactional public boolean buyTicket(int userId, int scheduleId, int seatId) { String updateSeatSql UPDATE seat SET status 2 WHERE id ? AND schedule_id ? AND status 0; int rows jdbcTemplate.update(updateSeatSql, seatId, scheduleId); if (rows 0) { return false; // 座位已被别人抢先 } String insertOrderSql INSERT INTO orders (order_no, user_id, schedule_id, seat_id, amount, status) VALUES (?, ?, ?, ?, ?, 0); // 金额从 schedule 表读取后再绑定 return true; }这段代码的关键在于 UPDATE 语句的 WHERE 条件它把 status 0 当成乐观锁来用。两个并发事务同时执行这条 UPDATE 时MySQL 会以行锁让其中一条先执行后执行的那条会因为 status 已经不是 0 而影响 0 行返回 false。这比先 SELECT 再 UPDATE 要简洁得多也不需要引入多余的版本号字段。另一个很容易被忽视的点是事务里先更新 seat 再插入 orders 的顺序。如果先插入订单再更新座位事务 B 插入订单时会受唯一索引影响但可能已经抓取了旧数据导致事务嵌套时行锁升级为表锁吞吐量明显下降。先更新座位失败后直接返回不 insert不污染订单表这是成本最低的序列。4.3 订单超时未支付定时扫描与状态补偿支付回调在课设里经常是模拟的但订单超时释放座位是真要做的。如果不做用户选座后一直不支付座位会永远锁定影厅虚拟座位会越来越少。最常见的做法是开一个定时任务每隔一段时间扫描待支付订单把超时订单取消并把对应座位释放回可选。这里涉及两个参数值得注意扫描间隔和超时时间。超时时间一般设 15 到 30 分钟扫描间隔设 1 到 2 分钟。扫描太频繁会把数据库 CPU 打高太慢会让用户等不到座位释放。下面是一条比较常见的补偿 SQLUPDATE orders o JOIN seat s ON o.schedule_id s.schedule_id AND o.seat_id s.id SET o.status 2, s.status 0 WHERE o.status 0 AND o.created_at NOW() - INTERVAL 30 MINUTE;这条 SQL 把两个更新合并成一次执行利用 JOIN 一次锁住订单和座位两行避免了“订单改了但座位没释放”的中间态。它通常由一个简单的 Java Timer 或 Spring Scheduled 方法触发我建议把它封装成独立 service单独调用不要在用户请求线程里执行。如果你追求更可靠的方案可以把这种补偿机制换成延迟消息队列。但课设阶段完全没有必要定时扫描已经能覆盖绝大多数需求。在项目说明里写清“为什么用扫描而不是消息队列”面试官反而会认可你考虑过复杂度边界这是加分项。5. 运行购票系统最常见的五个问题从编译失败到并发超卖5.1 现象Tomcat 启动报 ClassNotFoundException原因是驱动 jar 没有部署现象Tomcat 启动后访问页面控制台报 java.lang.ClassNotFoundException: com.mysql.cj.jdbc.Driver。原因数据库驱动 jar 放在了 IDE 的 Library 里但没有复制到项目的 WEB-INF/lib 目录。Tomcat 运行时按 Web 应用的类路径加载驱动IDE 的 Library 不会自动打包进部署目录。解决把 mysql-connector-java 的 jar 放进 WEB-INF/lib或者在构建工具里显式声明依赖后重新打包。用 Maven 的话检查 pom.xml 里依赖的 scope 是不是 compile不要选 provided否则打包时也会漏掉。5.2 现象数据库连接报 Communications link failure原因是时区和 SSL 参数缺失现象在 MySQL 8.0 环境下运行源码连接数据库时报 Communications link failure 或 The server time zone value is unrecognized。原因MySQL 8.0 的驱动要求客户端提供时区参数而老源码里的 URL 只写了 jdbc:mysql://localhost:3306/cinema没带 serverTimezone。同时 SSL 握手也会让部分环境出现连接不稳定。解决把 JDBC URL 改成下面这样重点看 serverTimezone 和 useSSL 两个参数jdbc:mysql://localhost:3306/cinema?useSSLfalsecharacterEncodingutf8serverTimezoneAsia/Shanghai这看起来只是参数问题其实已经属于运行环境适配问题。任何一份旧源码拿到新数据库上跑第一件事就是检查 URL 参数不要先去改业务代码。5.3 现象两个用户同时下单同一座位都成功了原因是缺少唯一索引现象用两个浏览器同时操作或者用测试工具并发请求发现同一场次的同一座位生成了两条待支付订单座位状态也没有正确变化。原因源码只在 Java 层面做了“先查询座位状态再下单”的逻辑没加唯一索引也没用条件更新。高并发下两个请求都读到状态为 0各自插入订单数据库层没有约束拦住。解决先给 orders 表加唯一索引再把下单逻辑改成先 UPDATE seat 条件更新后插入订单。索引是最后一道防线代码逻辑是主要防线两者缺一不可。ALTER TABLE orders ADD UNIQUE KEY uk_schedule_seat (schedule_id, seat_id);5.4 现象JSP 页面中文全部变成问号原因是请求和响应编码不一致现象数据库里中文显示正常JSP 页面输出却是一串问号或者表单提交的中文到后台变成了乱码。原因Tomcat 默认的请求体编码不是 UTF-8不同 JSP 文件头声明的 pageEncoding 也不一致导致请求、响应、存储三个环节各用各的字符集。解决三个环节保持统一。第一JSP 顶部声明。第二在 Servlet 中显式设置请求和响应编码。第三最好写一个 CharacterEncodingFilter 统一处理避免每个 Servlet 重复设置。WebFilter(/*) public class EncodingFilter implements Filter { public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain) throws IOException, ServletException { req.setCharacterEncoding(UTF-8); resp.setCharacterEncoding(UTF-8); chain.doFilter(req, resp); } }这里有一个关键顺序先设置编码再调用 chain.doFilter。如果顺序反了请求参数已经被容器按默认编码解析过过滤器再设置也不会生效。5.5 现象模拟支付成功后订单还是待支付原因是事务边界不对现象在支付回调里更新了订单状态返回页面也提示支付成功但数据库里 status 字段还是 0需要等一段时间才变化。原因常见的是支付回调方法没有加事务或者事务方法内部 catch 住了异常后继续执行事务被标记为 rollback-only最终提交时被回滚。还有一种隐蔽的错误更新订单状态和更新座位状态写在两个不同事务里第二个事务失败后第一个事务已经提交了。解决把支付成功的状态更新和座位状态更新合并到一个 Transactional 方法里任何异常直接抛出让事务真正回滚。代码层面要做的是把两个 DAO 更新放在同一个 service 方法同时检查方法是否被 Spring 代理。如果是在类内部直接调用 this.payOrder()事务注解不会生效这是一个容易忽略的细节。注意类内部自调用时Spring 事务代理不生效需要把业务方法拆到另一个 bean 里或者用事务模板手动管理。6. 把课设源码改造成简历项目三个能落地的进阶点6.1 用纯 JavaScript 画一个座位矩阵选座页源码里最容易被做成静态图的部分是选座页。我建议自己写一个座位矩阵不需要前端框架只要加载场次数据后用后端返回的已售座位构造一个对象再渲染格子。const soldMap {}; fetch(api/schedule/ scheduleId /seats) .then(res res.json()) .then(list { list.forEach(s { if (s.status ! 0) soldMap[s.rowNo - s.colNo] true; }); renderSeatMap(); });这个改造的验证标准很简单选中的座位高亮已售的座位不可点击提交后刷新页面能看到状态更新。它不会影响后端代码但会让整个项目看起来像真实产品。6.2 用 Filter 统一处理登录校验而不是每个 Servlet 重复写很多源码在每个页面 Servlet 里都写一遍“session 里有没有 loginUser”这个做法能通过验收但不适合作为简历项目。用一个登录过滤器把不需要登录就能访问的路径放行其余请求统一检查代码会整洁很多。WebFilter(/*) public class LoginFilter implements Filter { public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain) throws IOException, ServletException { HttpServletRequest httpReq (HttpServletRequest) req; HttpSession session httpReq.getSession(); if (/login.equals(httpReq.getRequestURI()) || session.getAttribute(loginUser) ! null) { chain.doFilter(req, resp); } else { ((HttpServletResponse) resp).sendRedirect(httpReq.getContextPath() /login.jsp); } } }这段代码的核心是放行规则和拦截规则分开配置不要在过滤器里写一串复杂的 if。放行路径可以抽成一个集合后续加注册页、影片列表页都很方便。6.3 用 JMeter 压一下并发下单接口验证超卖是否真的解决了用 JMeter 建一个线程组模拟 50 个并发用户同时选同一个座位观察返回结果和最终数据库里的订单数量。如果返回成功数大于 1说明唯一索引或条件更新没生效如果数据库里只有 1 条订单说明防超卖逻辑站得住。这个验证过程写进简历的项目亮点里比单纯写“使用了 MVC 架构”更有说服力。我自己在做类似项目时踩过两个坑一是定时扫描订单的间隔设得太短把数据库连接池打满二是座位初始化脚本写错了行号导致一整个影厅只有半边座位能选。这些都是可以在验收前用脚本暴露出来的问题。希望这些经验能帮你在交作业之前就把项目做成一个真正扛得住追问的系统。本文还有配套的精品资源点击获取