简介:一套基于 Java 实现的电影院购票系统完整源码与配套数据库,面向需要完成 JavaWeb 课程设计或毕业设计的开发者。项目采用 MVC 分层思想,将 model 与 view 解耦以实现高内聚低耦合,可按 SSM/SpringBoot 思路改造,也可沿用自定义 Servlet 实现;前端整合 layui、bootstrap、jquery,后端涉及 JSP、Servlet、Filter 及 Spring/Hibernate/MyBatis 等常用技术栈,涵盖从功能分析、概要设计到编码实现的主要环节。资源包共 970 个文件,约 18.43MB,其中包含 251 个 gif、205 个 js、74 个 html、60 个 jpg、53 个 jsp、29 个 java 与 29 个 class 等;目录中同时提供项目配置文件、数据库 SQL 脚本及说明文档,方便导入 IDE 后快速搭建运行环境,开发环境建议使用 JDK1.8、MySQL5.6/5.7,若采用 MySQL8 需同步更换驱动版本。已有 2794 人浏览学习。通过这份源码可以系统掌握影院购票从数据表设计到前端页面展示的完整链路,尤其适合参考其 MVC 分层、登录鉴权、购票流程及后台管理等核心模块,配合本地 MySQL 调试即可理解整体实现思路。
1. 电影院购票系统为什么难做:选座、锁单与订单超时这个三角矛盾
一个电影院购票系统,最忙的时刻不是售票员上班,而是某部热门影片开场前30分钟:用户同时涌进来看排期、选座位、提交订单,后台还要处理一堆未支付订单的座位释放。这套「基于Java的电影院购票系统」本质上是在解决一个三角矛盾——选座要准,锁单要快,超时订单还要能自动把座位吐出来。整套java源码走的是JavaWeb经典路线:JSP做页面展示,Servlet和Filter接请求、做权限控制,MyBatis管数据持久化,前端用LayUI、Bootstrap和jQuery搭界面。它适合两类人:一类是JavaWeb课设或毕设,需要一套能跑通、能讲清MVC分层和事务控制的完整工程;另一类是还想看传统Servlet写法、想弄明白Filter白名单和并发锁座怎么落地的人。读这篇文章,你会得到一套从需求到验收都能直接照搬的方案。
2. 需求分析与技术选型:三类角色、两个技术方案与开发环境清单
2.1 先理清角色:三类用户和他们的操作场景
系统可以粗分为前台购票与后台管理两块。购票端面对的是普通用户:注册登录、浏览正在热映的影片、查看某个场次的排片时间、在座位图上选座、提交订单、模拟支付、退票。管理端面对的是影院运营人员:维护影片信息、维护影厅与排片场次、查看订单列表、按日期看售票统计。除此之外还有一个容易被忽略的角色——系统维护者,他关心数据库脚本能不能一键导入、JDK版本和Tomcat版本是否匹配、有无可配置的账户密码参数。我在做需求分析时,习惯先把这三类角色固定下来,然后用一句话给系统定边界:用户管「买」,管理员管「排」,系统管「座」。
这个边界直接决定了后面的表结构和接口划分,避免写着写着把排片逻辑塞进购票Servlet里。比如用户端要的「选座」页面,本质上是一个座位状态查询接口加一个下单接口;管理端要的「排片」页面,本质上是对排片表的增删改查。两者数据模型一致,但接口方向完全不同,分开设计才能让前后端各自独立修改。角色定了之后,再给每个角色写两三个典型操作场景,需求文档这一章基本就立住了。
2.2 技术选型对比:为什么不直接全家桶SpringBoot
这套资源里的技术栈是javascript、css、jsp、servlet、filter,配合spring、hibernate、mybatis。摘要描述给了两个方向:可以用SSM或SpringBoot,也可以自定义Servlet实现。这里我按「课程设计与毕设场景下最稳妥的路线」来推荐:控制器层继续用Servlet,业务层用Spring管理事务,持久层用MyBatis。原因是三个。第一,答辩时评委最爱问MVC里三层怎么分工,Servlet写控制器比SpringBoot的注解式Controller更容易讲清楚request生命周期,「请求先进Filter再进Servlet,Service开事务调Mapper」这条链路一句话就能讲完。第二,选座锁票这类逻辑需要精细控制事务边界,把事务注解放在Service方法上,比在SpringBoot里追一堆自动配置直观得多。第三,这套老组合对JDK8、MySQL5.6/5.7的支持非常稳,机器上装Eclipse EE或Idea都能直接导入运行,不会出现学生机拉不动全家桶的问题。
| 对比维度 | 本方案(Spring+Servlet+MyBatis) | 全SSM(Spring+SpringMVC+MyBatis) | SpringBoot |
|---|---|---|---|
| 请求链路 | 直观,适合答辩演示 | 注解映射,开发快 | 依赖自动装配,排查像个黑匣子 |
| 事务控制 | Service层注解清晰 | 与方案类似 | 自动配置,出错时难定位 |
| 部署难度 | 打War包丢进Tomcat即可 | 与方案类似 | 内嵌Tomcat,打包方式不同 |
| 适合场景 | 课设/毕设,重视讲原理 | 偏企业实习练习 | 工作中常用,答辩时不好讲底层 |
如果验收要求明确写了用SSM,可以在三层不变的前提下,把DispatcherServlet体系换成SpringMVC的控制器,业务层和Mapper基本不用动;如果老师要求SpringBoot,则需要改pom依赖和启动类,但选座锁座那部分SQL与Service代码可以直接搬过来。选型表的重点不是「哪个更好」,而是「为什么在这个场景下选它」,答辩时把这个理由讲清楚,比堆技术名词有用得多。
2.3 功能优先级:先保证能卖出票,再考虑管理好看
功能的推进顺序,我一般会分成P0、P1、P2三档。P0是系统的生命线:注册登录、影片与排片浏览、选座下单、支付模拟、后台影片与排片维护。P1是购票体验的关键补全:订单超时释放、退票、座位状态可视化、订单分页查询。P2是管理侧的加分项:按影片或日期的票房统计、导出Excel、操作日志。为什么要按这个顺序?因为答辩演示时只给你三五分钟,从首页一路点到「下单成功、订单列表能看到记录」这条主线打通,项目就是完整的。统计图表这类加分项放到最后,不要影响主流程的开发节奏。
用户侧的细节也要在需求阶段定死:单次购票最多选5张,超过就提示;订单生成后15分钟未支付自动关闭;支付是模拟的,不需要接真实支付渠道;退票要求开演前30分钟才允许。这些规则看似是业务细节,实际上每一句都会落到代码里的一个分支判断和表里的一个状态字段,前期不写清楚,后期全是补丁式改代码。把优先级写进设计文档还有一个好处:万一时间不够,砍掉P2功能也不会影响整体交付。
2.4 开发环境清单:JDK、Idea/Eclipse与MySQL版本怎么配
| 软件 | 推荐版本 | 说明 |
|---|---|---|
| JDK | 1.8 | 1.7也能跑,但推荐8,lambda语法与更好的时间API |
| IDE | Eclipse EE / IntelliJ IDEA | 两者都能导入,注意部署到Tomcat的Artifact包含lib |
| Tomcat | 8.5或9.0 | 对应Servlet 3.1/4.0,本项目的Servlet规范 |
| MySQL | 5.6或5.7 | 8.0需要改驱动和连接参数,详见第5章 |
| 前端依赖 | layui/bootstrap/jquery | 静态资源放webapp目录直接引用 |
验证环境最省事的做法是:先跑数据库脚本,再用Idea打开java源码,最后启动Tomcat。连接数据库的账号密码通常写在jdbc.properties里,拿到资源后第一步就是改这个文件,否则后面所有报错都会让你误以为是代码问题。编码方面,数据库连接URL记得带characterEncoding=utf8,创建库时用utf8mb4,否则中文影片名会出现乱码,这是一个看起来像bug、实际上是环境配置的经典问题。
3. 总体架构设计:MVC分层、五张核心表与一次购票的数据流转
3.1 包结构:Controller、Service、Dao三层的物理边界
MVC架构不是写到设计文档里就完事,它必须在代码目录上直接体现。这套系统的包结构通常长这样:
src/main/java ├─ com.cinema.filter // AuthFilter、EncodingFilter ├─ com.cinema.servlet // LoginServlet、FilmServlet、ScheduleServlet、OrderServlet、AdminServlet ├─ com.cinema.service // UserService、ScheduleService、OrderService、StatService ├─ com.cinema.dao // 各Mapper接口 ├─ com.cinema.entity // User、Film、Schedule、Order、Seat实体 └─ com.cinema.util // JdbcUtil、JsonUtil、PageBeanfilter只做请求前置处理,不写业务逻辑;servlet只接收参数、调用Service、返回JSON或转发JSP,不直接写SQL;service承载事务,是业务规则的唯一住所;dao接口对应MyBatis的XML Mapper,每个方法对应一条或一组SQL。这样分的直接好处是高内聚低耦合:换前端页面不用动Java代码,换数据库只要改Mapper XML,业务规则变动只集中在Service层。我见过最翻车的写法是把SQL写在Servlet里,然后Service层形同虚设,最后并发问题、事务问题全部堆在一起,改一处崩三处。
3.2 数据库设计:五张核心表的结构与关系
整个系统的核心数据模型是五张表:user用户表、film影片表、schedule排片表、orders订单表、order_seat订单座位明细表。影片和排片是一对多,排片和订单是一对多,订单和座位明细是一对多。座位明细表里记录的是「某个场次下哪些座位被占了」,而不是单独建一张影厅座位总表,这样设计的好处是排片创建时一次性初始化座位行,之后每个座位都在这张表里有唯一记录。
orders表的设计是关键,建表SQL通常这样写:
CREATE TABLE `orders` ( `id` INT NOT NULL AUTO_INCREMENT, `order_no` VARCHAR(32) NOT NULL COMMENT '订单号,前端展示用', `user_id` INT NOT NULL COMMENT '下单用户', `schedule_id` INT NOT NULL COMMENT '所属排片场次', `total_price` DECIMAL(10,2) NOT NULL DEFAULT 0 COMMENT '订单总金额', `status` TINYINT NOT NULL DEFAULT 0 COMMENT '0待支付 1已支付 2已退票 3超时关闭', `create_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, `pay_time` DATETIME DEFAULT NULL, PRIMARY KEY (`id`), UNIQUE KEY `uk_order_no` (`order_no`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;这里有几个设计点要说明:order_no用时间戳加随机数生成,不要用自增id当订单号展示,否则用户能通过订单号推断出你的销量;status用TINYINT而不是字符串,节省空间也方便在代码里定义常量,配合枚举类做映射;create_time和pay_time分开记录,超时释放判断只基于create_time,避免被pay_time的空值干扰。order_seat表则用schedule_id加seat_no做唯一索引,防止同一场次同一座位被初始化两次或并发下重复占座。
| 字段 | 类型 | 说明 |
|---|---|---|
| id | int | 主键 |
| order_id | int | 所属订单,空表示座位空闲 |
| schedule_id | int | 所属排片 |
| seat_no | varchar(16) | 座位编号,如A-05 |
| hold_time | datetime | 占座时间,用于超时释放 |
| status | tinyint | 0空闲 1锁定中 2已售出 |
座位状态不能只靠schedule表里的sold_seat计数,必须有一行行的座位明细,否则并发下两笔订单同时把sold_seat从0改成1,数据库就认为卖出两张,实际只卖出一张。明细表的存在,让「锁座位」变成了对具体行的更新操作,这是整个并发方案的地基。
3.3 前端资源组织:LayUI+Bootstrap+jQuery怎么搭页面骨架
页面方面,前台包括登录注册页、影片列表页、排片列表页、座位选择页、订单确认页、订单列表页;后台包括影片管理、排片管理、订单管理和统计页。静态资源统一放webapp/static目录,LayUI负责后台表格和弹窗,Bootstrap管栅格和响应式布局,jQuery统一处理Ajax请求。为什么用LayUI而不是自己写CSS?因为后台管理页的表格、分页、弹窗组件开箱即用,一套增删改查能节省一半工作量。前端资源全部本地化,不要走CDN,答辩现场万一断网,页面样式和脚本依然完整可用,这个细节能避免很多尴尬。
3.4 数据流转:一次购票请求从点击到落库的完整路径
理解了架构之后,要把「一次购票」这条链路在脑子里完整走一遍。用户打开座位图,jQuery发起Ajax请求到GET /schedule/seats?scheduleId=xx,Servlet返回该排片已占座位的JSON数组,前端把座位渲染成红灰格子。用户点击空闲座位并提交,Ajax POST到/order/create,携带scheduleId与seatNo数组。OrderServlet收到后调用OrderService.createOrder:Service开启事务,先逐座锁定座位,若更新行数为0说明被抢,抛异常回滚;然后插入orders主记录和order_seat明细,提交事务返回订单号。前端拿到订单号跳转支付页,模拟支付后再调/order/pay把status从0改成1。
注意:第4步的占座是并发安全的唯一保障。先查状态再改状态的写法在并发下必然超卖,正确做法是带条件的UPDATE,靠数据库行锁保证同一时刻只有一个请求能改成功。
4. 核心模块实现:登录过滤、选座锁票、订单事务与后台统计
4.1 登录鉴权与角色控制:一个Filter搞定两套权限
登录是系统的第一道门,用Filter做统一拦截,比在每个Servlet里重复判断用户是否登录要干净得多。核心实现如下:
public class AuthFilter implements Filter { // 白名单:不需要登录就能访问的路径 private static final List<String> WHITE_LIST = Arrays.asList( "/login", "/register", "/index", "/film/detail", "/captcha" ); @Override public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain) throws IOException, ServletException { HttpServletRequest request = (HttpServletRequest) req; HttpServletResponse response = (HttpServletResponse) resp; // 去掉上下文路径,得到类似 /order/list 的访问路径 String path = request.getRequestURI().substring(request.getContextPath().length()); HttpSession session = request.getSession(false); User user = session == null ? null : (User) session.getAttribute("loginUser"); // 白名单内的资源直接放行,同时放行静态资源目录 if (WHITE_LIST.contains(path) || path.startsWith("/static/") || user != null) { chain.doFilter(request, response); return; } // 未登录用户跳回登录页,携带来源路径方便登录后回跳 response.sendRedirect(request.getContextPath() + "/login.jsp?redirect=" + path); } }这段代码里有几个细节要解释:path.startsWith("/static/")必须加,否则CSS和JS文件也会被拦截,页面直接变成裸HTML;session判断要兼容session不存在的情况,直接getSession(true)会强制创建session,浪费内存;登录后的用户对象存在session里,角色判断则放在AdminServlet的服务端逻辑中,例如user.getRole()不等于2就返回403页面。Filter在web.xml里注册,mapping配成/*,这样所有请求都会先经过它。
4.2 选座锁票:为什么是UPDATE而不是SELECT
购票系统的并发核心在选座。多个人同时抢同一场次的最后一个座位,应用层必须保证只有一个请求成功。最常见也最有效的写法是带条件的UPDATE:
-- 尝试占座:只有status=0(空闲)的行才会被更新 -- affected rows等于1表示抢座成功,等于0表示座位已被别人占走 UPDATE order_seat SET status = 1, order_id = #{orderId}, hold_time = NOW() WHERE schedule_id = #{scheduleId} AND seat_no = #{seatNo} AND status = 0;在MyBatis的Mapper接口里,这个方法的返回值是int,调用方通过判断affected rows来决策。如果返回0,代码立刻抛RuntimeException触发事务回滚。一次选多个座位的场景,不要尝试用一条UPDATE批量更新多个座位,因为一旦其中某个座位被抢,整体成功和失败的状态会纠缠不清。更好的做法是循环执行,每个座位单独判断单独更新,任何一个失败就整体回滚,用户重新选座。
与「先SELECT status再UPDATE」的写法对比,关键差异在于原子性。两个请求同时SELECT到status=0,随后都执行UPDATE,后执行的那次会覆盖先执行的结果,导致两个订单对应同一个座位。而带条件的UPDATE里,InnoDB会对命中的行加锁,第二个请求的UPDATE必须等第一个请求提交或回滚后才能执行,此时行状态已变成1,条件不满足,更新行数为0,抢座失败。这就是行锁和条件更新配合的效果,比应用层加锁简单得多。
4.3 订单超时释放:迟到15分钟的座位怎么吐回来
锁座之后,用户可能不付款直接关掉页面,座位就会一直被占着。解决思路是给订单一个生命周期:生成订单时记录create_time,超过15分钟仍处于待支付状态,就把座位释放回空闲。释放逻辑用一条UPDATE JOIN就能完成:
-- 找出超时未支付的订单,把其座位全部释放回空闲状态 UPDATE order_seat s JOIN orders o ON s.order_id = o.id SET s.status = 0, s.order_id = NULL, s.hold_time = NULL WHERE o.status = 0 AND TIMESTAMPDIFF(MINUTE, o.create_time, NOW()) > 15;这条SQL可以放在定时任务里每5分钟跑一次,也可以做成懒清理——用户访问订单列表或座位图时顺带执行一次。课设阶段用懒清理更简单,不用引入Quartz或Spring Task,在查询排片座位前先调一次释放方法即可。有一点要注意:释放时要连同order_seat里的order_id一起置空,并且把订单状态改成超时关闭,否则用户订单列表里会出现一个永远待支付的幽灵订单。
4.4 订单生成的事务边界:Service层一个方法管到底
锁座和订单插入必须在一个事务里,否则会出现「订单生成了但座位没锁上」或「座位锁上了但订单没生成」的中间状态。事务边界放在Service层,代码如下:
@Service public class OrderService { @Autowired private OrderDao orderDao; @Autowired private OrderSeatDao seatDao; @Transactional(rollbackFor = Exception.class) public String createOrder(OrderDTO dto) { // 1. 插入订单主记录,初始状态为0(待支付) Order order = buildOrder(dto); orderDao.insert(order); // 2. 逐座尝试锁定,任何一个失败都会触发整体回滚 for (String seatNo : dto.getSeatNos()) { int rows = seatDao.tryLockSeat(dto.getScheduleId(), seatNo, order.getId()); if (rows == 0) { throw new RuntimeException("座位已被抢购:" + seatNo); } } // 3. 全部座位锁定成功,返回订单号给前端 return order.getOrderNo(); } }@Transactional注解管住整个方法,第二步抛出的异常会让第一步插入的订单一起回滚。dto.getSeatNos()来自前端勾选的座位数组,每次最多5个,这个限制在Servlet层就要校验,避免有人构造请求一次提交几百个座位。事务为什么放在Service层而不是Servlet层?因为Servlet是Web层,职责是参数解析和响应,如果事务写在Servlet里,一个请求里混入多个业务操作时,无法精确控制回滚范围。事务要跟着业务走,而不是跟着请求走,这是JavaWeb里最重要的一条经验。
4.5 后台统计与密码安全:两个容易忽视的实现细节
后台管理模块包含影片维护、排片管理、订单列表和销售统计。统计的核心是这张按日期分组的查询:
SELECT DATE(create_time) AS sale_date, COUNT(*) AS order_count, SUM(total_price) AS sale_amount FROM orders WHERE status = 1 -- 只统计已支付订单 GROUP BY DATE(create_time) ORDER BY sale_date DESC;SUM之前必须先过滤status,否则待支付订单的金额会算进营业额,统计结果直接失真。数据量不大的时候这条SQL不需要额外索引,但可以在create_time上加普通索引避免全表扫描。后台表格用LayUI的table模块渲染,前端请求后台接口拿到JSON数组即可。
密码存储方面,注册时不能把明文密码直接入库,登录时也不能用字符串比较。最简单可靠的方式是加盐后做不可逆散列:
String salt = "cinema"; String hashed = DigestUtils.md5Hex(password + salt); // 把hashed存入数据库,登录时再对输入密码做同样的散列后比对对课设而言MD5加盐已经够用,想更规范可以换BCrypt。这里最容易翻车的点是管理员密码写死在页面判断里,比如if(password.equals("123456")),一旦源码被看到,整个系统等于没有登录保护。密码散列的代码虽然只有两行,但答辩时提到「不可逆、加盐、防彩虹表」这几个词,技术深度会比别人高一个档次。
5. 部署避坑指南:JDK版本、MySQL驱动与连接池的五个高频问题
5.1 现象:Tomcat启动就报ClassNotFoundException,控制台一片红
原因很直接:依赖的jar包没有进入WEB-INF/lib目录。Idea里光是添加了Module依赖不够,还需要在Artifacts的Output Layout里把lib目录打包进去。很多人查了一下午ClassNotFound,最后发现是Artifacts里少了整个lib目录。解决方法是打开Project Structure -> Artifacts,找到部署包,点击加号把Library Files加入;或者直接检查target目录下WEB-INF/lib里是否有mysql-connector-java、mybatis、spring等jar。最稳妥的做法是统一用Maven管理依赖,打War包会自动带上所有lib,不会出现漏jar的问题。
5.2 现象:本机装了MySQL 8,连库报Public Key Retrieval is not allowed
原因在摘要里已经写得很清楚:如果用mysql8需要更改一下驱动版本。MySQL 8默认认证插件是caching_sha2_password,旧版驱动不认识,连接URL还要额外带上allowPublicKeyRetrieval=true。解决方法是把驱动换成mysql-connector-java 8.0.x,JDBC URL写成jdbc:mysql://localhost:3306/cinema?useSSL=false&allowPublicKeyRetrieval=true&serverTimezone=Asia/Shanghai。serverTimezone一定要配,否则时间会相差8个小时,订单里显示的下单时间全错。如果非要坚持用5.1.x驱动,也可以把数据库用户的认证插件改回mysql_native_password,但换驱动才是正规解法。
5.3 现象:两个人同时点同一个座位,两个订单都创建了
这是并发控制没做好的典型症状。原因有两层:第一层是业务方法没有放在事务里,锁座和插订单分成两次独立提交;第二层是锁座SQL没有带status=0条件,两个请求同时读到空闲状态,先后执行UPDATE,后执行的那次覆盖了先执行的结果。解决方法是把占座UPDATE带上status=0条件并检查affected rows,把整个流程包进@Transactional,再在order_seat表上建schedule_id加seat_no的唯一索引作兜底。三层防线全部到位的话,数据库层面的重复占座会直接抛唯一索引冲突,应用层捕获后返回「座位被抢」提示,不会产生脏数据。
5.4 现象:改完JSP不生效,页面永远是旧版本
原因多半是Tomcat的work目录缓存了编译后的JSP中间文件,或者开发模式下reloadable没有开启。解决方法是开发阶段把conf/server.xml里Context的reloadable设为true,这样JSP修改后会自动重新编译;Idea里改完JSP执行Build -> Rebuild Project;实在不行就清空Tomcat的work目录再重启。注意部署到正式环境时要把reloadable关掉,否则每次访问都检查文件更新时间,性能损耗很明显。
5.5 现象:跑一会儿就报Too many connections,数据库连接耗尽
原因是每次请求都新建Connection、用完不close,或者连接池参数配得太小。解决方法是接入Druid或C3P0统一管理数据源,在Spring配置里设置initialSize=5、maxActive=50、maxWait=60000。依赖MyBatis的工程,连接由框架自动归还连接池,不需要手动close;但如果代码里手写了JDBC操作,Connection和ResultSet必须在finally块里关闭,否则连接只借不还,几分钟就把连接池拖垮。Druid数据源的配置片段如下:
<bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource" init-method="init" destroy-method="close"> <property name="url" value="jdbc:mysql://localhost:3306/cinema?useSSL=false&characterEncoding=utf8"/> <property name="username" value="root"/> <property name="password" value="root"/> <property name="initialSize" value="5"/> <property name="maxActive" value="50"/> <property name="maxWait" value="60000"/> </bean>maxWait=60000的含义是拿不到连接时最多等60秒,超时直接报错而不是无限阻塞。这些参数看起来是配置问题,但在高并发场景下就是系统的性命线,答辩时能把这些参数说出来,说明你真的跑过并发测试,而不是只把项目跑通就交差。
6. 验收与答辩:从一条购票主流程到评委追问的应对方法
6.1 功能验收:按角色走一条完整购票流程
验收不要东点一下西点一下,要按角色走完整主线。我建议把下面这张用例表打印出来,演示时一步一步走:
| 步骤 | 操作 | 预期结果 |
|---|---|---|
| 1 | 注册一个新用户 | 数据库user表出现对应记录,密码为散列值 |
| 2 | 用户登录 | 跳转首页,显示影片列表 |
| 3 | 进入某影片排片,选两个座位提交 | 订单生成,状态为0待支付,order_seat两行status=1 |
| 4 | 模拟支付 | 订单status=1,座位status=2 |
| 5 | 另一个用户打开同一场次 | 已售座位显示灰色不可选 |
| 6 | 管理员登录后台 | 能看到订单与统计,按日期分组金额正确 |
并发测试可以开两个浏览器隐身窗口,同时提交同一场次同一座位,预期只有一个成功,另一个提示座位已被抢购。这条链路走通,项目的基本盘就稳了。
6.2 数据验收:看库表关联与订单状态
功能之外,还要查三处数据:第一,外键关系一致,order_seat里的每个order_id都能在orders表找到对应订单;第二,金额计算正确,orders.total_price等于该订单座位数乘排片单价;第三,超时释放生效,把一个待支付订单的create_time改成20分钟前,触发一次释放逻辑,座位恢复空闲。验收前把这些SQL查询结果打印出来,答辩时直接展示,比口头说「我测试过了」有说服力得多。如果你手头就是这份源码加数据库SQL的资源,第6章这套验收清单可以直接拿来做自测,按步骤走一遍就知道工程是否完整。
6.3 答辩高频问题与回答思路
评委最爱问的是三个问题。第一是「你为什么用Servlet而不是SpringBoot」,回答重点:课设阶段希望把请求生命周期和事务边界展示得更直观,SpringBoot把太多东西藏在自动配置里,不利于讲原理。第二是「并发下怎么防止超卖」,回答重点:条件UPDATE加事务加唯一索引三道防线,把affected rows的判断逻辑讲清楚。第三是「状态字段为什么用数字不用字符串」,回答重点:节省存储、常量集中管理、代码里定义枚举映射。这三个问题回答顺了,答辩的技术深度基本就过关了。
做好验收之后,还有一件小事比写代码更重要:把主流程的演示顺序固定下来,从注册到支付,中间不要点无关菜单。我后来每次做JavaWeb项目都会强制自己在验收前一晚把「注册→选座→下单→支付→后台统计」这条主线完整走三遍,窗口全部用隐身模式开,确保没有会话缓存干扰。这个方法帮我挡掉了大部分临场翻车的可能,希望你也能在答辩前一晚留出时间做这件事,而不是临时改代码。希望帮到你。
本文还有配套的精品资源,点击获取