简介:一份基于Java的电影购票系统毕业设计论文,面向计算机相关专业学生与毕业设计开发者。该选题针对传统电影票务管理难度大、容错率低、处理数据费工费时等痛点,给出了完整的系统设计与实现方案。全文采用文档形式呈现,共1个docx文件,压缩包大小约3.43MB,内容结构清晰,包含中英文摘要、绪论、开发环境与技术、系统分析设计等常规论文章节,并覆盖电影管理、电影场次管理、电影评价管理、电影收藏管理、电影订单管理、用户管理、电影类型管理等核心功能模块。技术上以MySQL数据库、Java语言和SSM框架为主,阐述了从数据库设计到后端开发的主要过程。读者可借此了解一个可运行的Web购票系统的构建思路,也能为撰写毕业设计、设计同类信息系统提供结构参考与代码层面的实现借鉴。目前已有87人学习,适合需要快速把握系统设计与论文框架的学习者。
1. 一份基于 Java 的 SSM 电影购票系统毕设,拆完到底能拿到什么
一套基于 Java 的 SSM 电影购票系统,在 Spring Boot 满天飞的今天拆起来还有意义吗?有,而且比想象中大。这份资源的价值不在「新」,而在「全」:电影管理、场次管理、评价管理、收藏管理、订单管理、用户管理、电影类型管理,七个模块串起一条完整购票链路,覆盖登录鉴权到下单支付的闭环。
对写毕设的人来说,它解决的是「不知道该建哪些表、写哪些接口」的起点问题;对在职开发者,它是一份 SSM 调试样本,能看清 SpringMVC 拦截、MyBatis 映射和 MySQL 事务在真实业务里怎么配合。Java + MySQL + SSM,IDEA 导入即可运行,适合毕设、课设,也适合补 Java Web 基本功。
2. 技术选型与工程骨架:Java + SSM + MySQL 的分工边界与搭建清单
2.1 为什么毕设场景里 SSM 比 Spring Boot 更适合讲原理
很多人在选题时会纠结:都什么年代了,毕设还用 SSM 是不是显得过时?我的看法恰好相反——正因为 SSM 每一层都是"手动挡",它才是最适合讲清楚原理的组合。SSM 是三个框架的合称:Spring 负责对象管理和事务,SpringMVC 负责请求分发,MyBatis 负责 SQL 映射。用户从浏览器发起请求,先被 SpringMVC 的 DispatcherServlet 拦下,根据 URL 匹配到 Controller,Controller 调 Service 里的业务方法,Service 再通过 MyBatis 的 Mapper 接口与 MySQL 交互,结果逐层返回并渲染成页面或 JSON。
这条链路每一步都是显式的,没有自动配置帮你藏掉细节。答辩时考官问"一个请求是怎么处理完的",你可以直接对着代码讲完整路径,这在 java 面试题里也是高频考点。选型上为什么是这三件套?原文里的理由很实在:业务逻辑不算复杂,但涉及电影、场次、订单、用户多张表的增删改查,需要大量自定义 SQL,MyBatis 的 XML 写法比 JPA 自动映射更好控制;MySQL 是行式存储,事务和关联查询可靠,对毕设这种数据量级完全够用;Java 的跨平台特性让你在 Windows 上开发、在 Linux 服务器上部署都不用改代码。如果你非要用 Spring Boot,当然也可以,但很多配置变成黑匣子,出了问题反而难排查。
2.2 工程目录与 Maven 依赖:一份能直接导入 IDEA 的最小骨架
先贴一份最小可运行的 pom.xml 核心依赖。我一般会锁定 Spring 5.2.x 和 MyBatis 3.5.x,这两个版本在 Tomcat 9 下配合得很稳定,网上能找到的报错案例也最多,踩坑时搜得到答案。
<properties> <spring.version>5.2.25.RELEASE</spring.version> <mybatis.version>3.5.10</mybatis.version> </properties> <dependencies> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-webmvc</artifactId> <version>${spring.version}</version> </dependency> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-jdbc</artifactId> <version>${spring.version}</version> </dependency> <dependency> <groupId>org.mybatis</groupId> <artifactId>mybatis</artifactId> <version>${mybatis.version}</version> </dependency> <dependency> <groupId>org.mybatis</groupId> <artifactId>mybatis-spring</artifactId> <version>2.0.7</version> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>8.0.33</version> </dependency> </dependencies>依赖这里有几个关键点:spring-webmvc 和 spring-jdbc 分开引入,是因为这个项目只用到了 MVC 和 JDBC 事务,不需要拉整个 Spring 全家桶;mybatis-spring 的作用是把 MyBatis 的 SqlSessionFactory 交给 Spring 容器管理,这样 Service 里才能直接 @Autowired Mapper 接口。mysql-connector-java 用 8.0.x,如果用 5.x 老驱动连 MySQL 8,会直接报认证协议错误,这个问题后面避坑章节单独说。
工程目录我习惯按 controller / service / dao / entity 四层拆:
src/main/java/com/cinema/ ├── controller/ # 页面与 JSON 接口的入口 ├── service/ # 业务逻辑,接口 + impl 实现 ├── dao/ # MyBatis Mapper 接口 ├── entity/ # 与数据表对应的 POJO ├── interceptor/ # 登录拦截器 └── config/ src/main/resources/ ├── mapper/ # 每个 Mapper 接口对应的 XML ├── spring-mvc.xml # 控制器、拦截器配置 ├── spring-mybatis.xml └── jdbc.properties src/main/webapp/ ├── WEB-INF/web.xml ├── admin/ # 管理后台页面 └── front/ # 前台用户页面这个结构的好处是 Controller 薄、Service 厚、SQL 全在 XML 里,哪一层出问题都能快速定位。新手最容易犯的错误是业务逻辑全写在 Controller 里,当时觉得省事,后面加需求、写测试的时候就很被动。
2.3 登录与权限过滤:用拦截器统一鉴权,而不是散落在每个 Controller
系统里有管理员和普通用户两种角色,如果每个 Controller 方法开头都写一遍"取 session、判空、跳转",代码冗余不说,漏写一个就是安全隐患。常见做法是写一个拦截器统一处理,登录接口和静态资源放行,其余后台路径全部拦截。
public class LoginInterceptor implements HandlerInterceptor { @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { HttpSession session = request.getSession(); // 后台管理员会话,名称与登录 Controller 里 setAttribute 的 key 保持一致 Object admin = session.getAttribute("admin"); if (admin == null) { // 未登录跳转到登录页,而不是直接返回 401,前端体验更友好 response.sendRedirect(request.getContextPath() + "/login.html"); return false; } return true; } }这段代码的逻辑:从 session 里取登录时存入的 admin 对象,取不到就重定向到登录页并返回 false,请求到此终止;取到了就放行,进入后续的 Controller 处理。这里有一个容易忽略的点——session 的 key 必须和登录成功时session.setAttribute("admin", admin)的 key 完全一致,大小写都不能错,否则会一直认为自己没登录。
拦截器要在 spring-mvc.xml 里注册,并声明拦截路径和放行路径:
<mvc:interceptors> <mvc:interceptor> <mvc:mapping path="/admin/**"/> <mvc:exclude-mapping path="/admin/login"/> <mvc:exclude-mapping path="/static/**"/> </mvc:interceptor> </mvc:interceptors>提示:拦截器只解决"有没有登录"的问题,不解决"角色权限够不够"的问题。如果还要区分管理员和普通用户的可访问范围,可以在拦截器里再判断一次角色字段,或者用更细的权限框架,毕设场景一般到拦截器这层就够用。
3. 数据库设计:从 E-R 图到四张核心表的建表 SQL 与关联查询
3.1 实体关系梳理:电影、场次、订单、用户之间怎么挂
数据库设计是整个系统的地基,原文花了很大篇幅画 E-R 图,这一步不能省。从 E-R 图能提炼出四个核心实体:电影、电影订单、管理员、用户。电影实体带名称、照片、类型、原价、现价、座位数、热度、简介、创建时间;订单实体带订单号、购买的电影、场次、座位、用户、实付价格、支付类型、订单类型、订购日期;用户实体带账号、手机号、密码、姓名、身份证号、头像、性别、邮箱、余额。
它们之间的关系是:用户和订单是一对多,一个用户能下多张订单;电影和场次是一对多,一部电影有多个放映场次;场次和订单也是一对多,一个场次对应多张已售座位;用户和电影收藏是典型的多对多,靠一张收藏中间表关联。画 E-R 图时把这些关系标清楚,后面建表就不会出现外键乱挂、字段重复的情况。
3.2 建表 SQL:字段类型、精度与索引的取舍
建表是直接决定后面代码好不好写的一步。下面两张表是从 E-R 图落地的核心建表 SQL,字段命名和类型都是 SSM 项目里最常见的写法:
CREATE TABLE movie ( id INT PRIMARY KEY AUTO_INCREMENT COMMENT '电影ID', name VARCHAR(100) NOT NULL COMMENT '电影名称', photo VARCHAR(255) DEFAULT NULL COMMENT '封面图片路径', type_id INT NOT NULL COMMENT '电影类型ID', original_price DECIMAL(8,2) NOT NULL COMMENT '原价', current_price DECIMAL(8,2) NOT NULL COMMENT '现价', seats INT NOT NULL DEFAULT 80 COMMENT '座位总数', hot INT DEFAULT 0 COMMENT '热度', description TEXT COMMENT '电影简介', create_time DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', KEY idx_type (type_id) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='电影表'; CREATE TABLE movie_order ( id INT PRIMARY KEY AUTO_INCREMENT, order_no VARCHAR(32) NOT NULL UNIQUE COMMENT '业务订单号', movie_id INT NOT NULL COMMENT '电影ID', screening_id INT NOT NULL COMMENT '场次ID', user_id INT NOT NULL COMMENT '用户ID', seat_info VARCHAR(100) NOT NULL COMMENT '座位信息,如 3排5号', pay_price DECIMAL(8,2) NOT NULL COMMENT '实付价格', pay_type TINYINT DEFAULT 0 COMMENT '支付类型 0余额 1其他', order_type TINYINT DEFAULT 0 COMMENT '订单类型 0正常 1已退', order_date DATE NOT NULL COMMENT '订购日期', create_time DATETIME DEFAULT CURRENT_TIMESTAMP, KEY idx_user (user_id), KEY idx_movie (movie_id), CONSTRAINT fk_order_movie FOREIGN KEY (movie_id) REFERENCES movie(id) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='电影订单表';这里有几个选型上的讲究。金额字段必须用 DECIMAL 而不是 FLOAT,FLOAT 是浮点存储,算多了会出 0.1 + 0.2 不等于 0.3 的精度问题;订单号 order_no 用业务单号而不是自增 id,是因为自增 id 一旦暴露,别人能通过遍历 id 猜出你的订单量;seat_info 直接存 "3排5号" 这种组合字符串,比单独拆排号列号字段更省事,查询需求也不涉及按排号筛选。字符集统一 utf8mb4,不要用老掉牙的 utf8,否则用户头像昵称里带个 emoji 直接入库失败。
外键约束 fk_order_movie 是否要建,业界有争议。我一般建议毕设项目建上,因为答辩时能讲清楚"数据完整性靠数据库约束保证";如果以后要分库分表,再考虑去掉外键,把关联逻辑上移到 Service 层。
3.3 订单关联查询:LEFT JOIN 与 resultMap 的配合
订单表里存的是 movie_id、screening_id、user_id 这种外键,但页面要展示的是电影名、场次时间、影厅名。一张表查不出来时,就需要关联查询。这是 MyBatis 最典型的用法:
<select id="selectOrderDetailByUser" resultType="com.cinema.entity.OrderVO"> SELECT o.id, o.order_no, m.name AS movie_name, s.show_time, s.hall_name, o.seat_info, o.pay_price, o.pay_type, o.order_type FROM movie_order o LEFT JOIN movie m ON o.movie_id = m.id LEFT JOIN screening s ON o.screening_id = s.id WHERE o.user_id = #{userId} ORDER BY o.create_time DESC </select>用 LEFT JOIN 而不是 INNER JOIN,是为了保证即使场次被删了,订单记录依然能查出来——订单属于交易数据,不能因为关联的场次没了就连订单都消失。这里的 OrderVO 是一个视图对象,字段比实体类多出 movie_name、show_time、hall_name 这几个展示字段。实体类严格对应表字段,视图对象对应查询结果,这个习惯能避免把 VO 字段混进实体类里。如果查询结果涉及嵌套对象,可以用 resultMap 做一对一或一对多的映射,但毕设场景里平铺字段基本够用,不需要把关联对象嵌套进来。
注意:多表关联时字段名重复的情况很常见,比如 movie 和 movie_order 都有 create_time。SELECT 里必须写别名或者显式指定 o.create_time,否则 MyBatis 映射时会取到歧义字段,数据看起来"对不上号"。
4. 功能模块落地:电影管理、场次座位与订单状态流转的代码实现
4.1 电影管理:Controller + Service + Mapper 三层的标准写法
电影管理是管理员后台最核心的模块,负责电影的增删改查、上架下架。三层写法是 SSM 项目的标配,下面这段 Controller 展示了分页查询和新增两个接口的典型形态:
@Controller @RequestMapping("/admin/movie") public class MovieController { @Autowired private MovieService movieService; // layui 表格的分页请求约定:page=当前页 limit=每页条数 @RequestMapping("/list") @ResponseBody public Map<String, Object> list(int page, int limit, String name) { Map<String, Object> result = new HashMap<>(); List<Movie> list = movieService.findPage(page, limit, name); int total = movieService.count(name); result.put("code", 0); result.put("msg", ""); result.put("count", total); result.put("data", list); return result; } // 前端以 JSON 形式提交电影信息 @RequestMapping("/add") @ResponseBody public JsonResult add(@RequestBody Movie movie) { movieService.add(movie); return JsonResult.ok(); } }Controller 只做参数接收和结果包装,业务逻辑全部下沉到 Service。findPage 和 count 这两个方法必须分开写,批量查询和总数统计是两条 SQL,合在一起会导致分页组件算不出总页数。返回的 Map 结构是 layui 表格的固定格式,code=0 表示成功,count 是总条数,data 是当前页数据,前端模板按这个格式解析即可。
对应的 Mapper XML 写动态 SQL,支持按电影名称模糊搜索:
<select id="findPage" resultType="com.cinema.entity.Movie"> SELECT * FROM movie <where> <if test="name != null and name != ''"> name LIKE CONCAT('%', #{name}, '%') </if> </where> ORDER BY create_time DESC LIMIT #{offset}, #{pageSize} </select>这里的 offset 和 pageSize 是分页参数,Controller 传入的 page 是页码,需要在 Service 里换算成(page - 1) * limit。很多新手直接把 page 当 offset 用,第一页没事,翻到第二页就只显示出 10 条之后的数据。要么在 Service 里手动换算,要么引入 PageHelper 插件,用PageHelper.startPage(page, limit)自动处理,两种方式都行,但别混着用。
热点和推荐逻辑也在这个模块里。电影表的 hot 字段会在用户浏览、收藏、下单时累加,列表页按 hot 倒序就是"热门电影";电影类型表单独一张,电影和类型是多对一,前端按类型筛选时传 type_id 即可。注意类型删除前要检查是否还有电影挂在它下面,否则会删掉一个类型导致一批电影变成无头数据。
4.2 场次与座位:先锁行再卖票,避免超卖的两种写法
场次管理的核心不只是场次的增删改查,还有一个所有售票系统都躲不开的问题:超卖。两个用户同时买同一个场次的最后一张票,如果代码不控制并发,两个人都会下单成功,座位就卖出去了两张。这套系统里座位数直接维护在电影表或场次表的 seats 字段上,下单时先检查再扣减,中间必须加锁。
// 下单前先锁定场次记录,seat 字段记录已售座位数 @Transactional public boolean createOrder(OrderDTO dto) { // 1. 查询场次并加行锁,防止并发下同时读到同一个剩余座位数 Screening screening = screeningMapper.selectByIdForUpdate(dto.getScreeningId()); if (screening.getSoldSeats() + 1 > screening.getTotalSeats()) { throw new BusinessException("该场次座位已售罄"); } // 2. 更新已售座位数,返回受影响行数 int rows = screeningMapper.increaseSoldSeat(dto.getScreeningId()); if (rows == 0) { throw new BusinessException("下单失败,请重试"); } // 3. 创建订单 orderMapper.insert(dto.toOrder()); return true; }selectByIdForUpdate 对应 SQL 里的SELECT ... FOR UPDATE,这是 InnoDB 的行级锁机制,两个并发事务同时执行时,后到的那一个会阻塞在查询上,等前一个事务提交或回滚后才继续。配合 @Transactional 注解,查询、扣减、插订单三个操作绑定在同一个事务里,任何一步失败整体回滚。
不想用行锁的话,可以用乐观锁:表里加 version 字段,扣减时UPDATE screening SET sold_seats = sold_seats + 1, version = version + 1 WHERE id = ? AND version = ?,更新行数为 0 说明版本对不上,下单失败让用户重试。这两种方案毕设里都能讲,行锁实现直观,乐观锁更能体现你对并发控制的理解,建议两个都写在论文里做对比。
座位选择这里还有个细节:前端展示座位图时,已售座位要回传,用户选了座位后前端把座位号拼成字符串提交,后端只校验这个座位是否已经被占。校验的时机一定在事务内,因为"查询座位是否空闲"和"占用座位"两步之间如果隔着网络请求,并发窗口就大了。
4.3 订单状态流转:下单、支付、退票的事务边界
订单模块是另一个容易翻车的地方。订单表用 order_type 字段区分状态:0 正常、1 已退票。支付方式 pay_type 区分余额支付和其他支付。业务上要处理的场景有两个:支付成功后锁定座位,退票后释放座位并退款。退票是状态流转里最复杂的,它涉及三处数据变更:
@Transactional public void refund(Integer orderId) { MovieOrder order = orderMapper.selectById(orderId); // 1. 校验订单存在且未退票,防止重复退 if (order == null || order.getOrderType() != 0) { throw new BusinessException("订单不存在或不可退"); } // 2. 余额支付的退给用户,其他支付方式走对应退款接口 if (order.getPayType() == 0) { userMapper.increaseBalance(order.getUserId(), order.getPayPrice()); } // 3. 释放座位,把已售数量减回去 screeningMapper.decreaseSoldSeat(order.getScreeningId()); // 4. 更新订单状态 orderMapper.updateOrderType(orderId, 1); }这段代码的顺序是有讲究的:先查订单做状态校验,再退余额,再释放座位,最后更新订单状态。每一步都依赖于前一步成功,所以整体必须在一个事务里。如果哪一步抛了异常,事务回滚,余额不会少退、座位不会少释放、订单状态不会变成已退。
订单号生成也值得讲一句。用 java.util.UUID 去掉横杠,或者用时间戳加随机数拼 32 位字符串,都能满足唯一性要求。不要用System.currentTimeMillis()直接当单号,同一毫秒内并发下单会撞号。订单列表按创建时间倒序展示,管理员后台可以筛选支付类型和订单类型,这些都是订单管理模块的标配功能。
用户侧的收藏和评价模块相对简单:收藏表和评价表都挂在用户和电影之下,收藏是用户与电影的多对多中间表,评价是用户在电影下的多条记录。评价展示时关联用户昵称和头像,注意评价删除时管理员只能软删或直接删,但一定要同步判断评价是否已经关联到电影的评分统计,否则热度统计会失真。
5. 避坑与常见问题:SSM 项目本地跑不起来的五类排查清单
5.1 中文乱码:入库全是问号,页面显示却正常
现象:新增电影后,列表页中文正常,但打开 MySQL 命令行查看 movie 表,name 字段存的是???。
原因:连接数据库的 URL 没加字符集参数,MySQL 收到的是默认 latin1 编码的字节,存进去就变问号。还有一种情况是 JSP 页面本身是 GBK 编码,表单提交的中文到后端已经被转码成乱码,但页面显示时又经过一次编码转换,看起来反而正常。
解决:三处一起改。jdbc.properties 里连接 URL 加上useUnicode=true&characterEncoding=utf8;web.xml 里配置 Spring 的 CharacterEncodingFilter,强制请求和响应都用 UTF-8;建表语句统一用DEFAULT CHARSET=utf8mb4。改完重启 Tomcat,再插入一条中文数据验证。
5.2 Invalid bound statement:Mapper 接口和 XML 对不上
现象:项目能启动,但一调用 dao 层方法就报org.apache.ibatis.binding.BindingException: Invalid bound statement (not found)。
原因:常见有三个——Mapper 接口的全限定名和 XML 的 namespace 不一致;XML 里方法的 id 和接口方法名不一致;spring-mybatis.xml 里的 mapper-locations 没指向 XML 所在的目录,MyBatis 压根没加载到 XML。
解决:先检查mapper-locations配置,写成classpath:mapper/*.xml,确认 resources 下的 mapper 目录和 XML 文件存在;再检查 XML 的 namespace 是否等于接口的全限定类名,比如接口是com.cinema.dao.MovieMapper,namespace 就必须是这个字符串;最后看接口方法名和 XML 的 id 是否一致。这套检查流程我每次配新模块都会完整走一遍。
5.3 Communications link failure:MySQL 连接时的时区与驱动坑
现象:启动 Tomcat 正常,第一次请求接口时报Communications link failure,或者报The server time zone value '???ú' is unrecognized。
原因:驱动和数据库版本不匹配,或连接 URL 缺少 serverTimezone 参数。MySQL 8 的默认时区是 CST,Java 侧驱动拿不到合法时区信息就直接抛异常。
解决:把 mysql-connector-java 换成 8.0.x,连接 URL 补上serverTimezone=Asia/Shanghai。如果还是连不上,用命令行mysql -uroot -p本地验证 MySQL 服务是否真的在跑、密码是否正确、3306 端口是否被占用。还有一个隐藏原因:MySQL 8 默认的认证插件是 caching_sha2_password,老驱动不支持,换新驱动即可。
5.4 页面 404 但接口能通:SpringMVC 静态资源放行问题
现象:访问/admin/movie/list返回 JSON 正常,但浏览器访问/admin/index.html报 404,或者 CSS、JS 全部加载不出来。
原因:DispatcherServlet 的 servlet-mapping 配成/时,所有请求都先进 SpringMVC,静态资源找不到对应的 Controller 映射就返回 404。
解决:spring-mvc.xml 里加静态资源放行配置:
<mvc:resources mapping="/static/**" location="/static/"/>页面文件放在 webapp 根目录下,静态资源统一放 static 目录,路径不要带 WEB-INF。WEB-INF 下的文件只能服务端转发访问,直接浏览器访问一律 404,这是很多新手最容易踩的路径坑。
5.5 @Transactional 不生效:同类调用与事务注解的经典组合
现象:退票时订单状态更新了,但用户余额没退;或者 Service 方法抛了异常,数据库数据照样被改了。
原因:最常见的是同类内部调用——Service 里的方法 A 调用同一个类里的方法 B,B 标了 @Transactional,但 Spring 事务代理只对从外部进入的调用生效,内部调用走的是 this 对象,代理拦截不到。另一个原因是 spring 配置里没开启注解事务驱动。
解决:配置类或 spring 配置文件里加<tx:annotation-driven/>;内部调用改成注入自身代理,或者把事务方法单独拆到另一个 Service 里。事务注解建议写成@Transactional(rollbackFor = Exception.class),因为默认只在 RuntimeException 时回滚,受检异常不会触发回滚,这个细节答辩时能讲出来会很加分。
6. 从能跑到能答辩:五组测试用例把核心链路验一遍
6.1 测试用例:覆盖登录、下单、退票三条主链路
系统跑起来之后,别急着截图画界面,先把核心业务链路用测试用例过一遍。下面是这个项目必须验证的五组用例,我在拆这套资源时就是用这个顺序验的:
| 编号 | 操作步骤 | 预期结果 |
|---|---|---|
| TC01 | 管理员输入正确账号密码登录 | 进入后台首页;输错密码提示账号或密码错误 |
| TC02 | 新增电影,填写名称、类型、价格、简介 | 列表出现新记录,前台页面可见该电影 |
| TC03 | 用户选场次、选座位、提交订单 | 订单生成,场次已售座位数加 1 |
| TC04 | 用户对已支付订单执行退票 | 订单状态变为已退,余额按实付金额回退 |
| TC05 | 管理员修改自己密码 | 提示成功,退出后用新密码重新登录 |
TC03 和 TC04 是这只系统的命门,也是答辩时考官最喜欢追问的地方。测试时建议同时开两个浏览器窗口模拟两个用户,先后点下单,验证座位数是否正确,如果出现两个人都买到同一座位,说明 4.2 节的锁没有生效。TC04 退票后去 MySQL 里查三张表——movie_order 的状态、user 的余额、screening 的已售座位数,三处必须同步变化。
6.2 用 curl 和 MySQL 命令行做回归验证
页面点一遍还不够,接口层也要验证,特别是前端还没写好时,curl 是验证后端的最好工具:
# 登录并保存会话 Cookie curl -X POST http://localhost:8080/cinema/admin/login \ -d "username=admin&password=123456" -c cookies.txt # 带会话请求分页列表 curl -b cookies.txt \ "http://localhost:8080/cinema/admin/movie/list?page=1&limit=10" # 直接查数据库确认下单后的数据落库 mysql -uroot -p -e \ "SELECT order_no, pay_price, order_type FROM movie_order WHERE user_id=1 ORDER BY create_time DESC;"curl 加-c存 Cookie、-b带 Cookie,是为了模拟浏览器会话,不带会话去访问受拦截器保护的接口,会看到重定向后的登录页,这能顺带验证拦截器是否生效。MySQL 命令行查数据是最后一道保险,页面显示"下单成功"不代表数据真的落库了,只有数据库里查得到记录才算数。
这套系统的完整源码、建表 SQL 和毕业设计论文文档都在资源包里,拿到之后先别急着导入 IDEA,按第 5 章的清单把环境过一遍,再按第 6 章的用例验一遍业务链路,能省掉大半的报错时间。我每次拆这种 SSM 毕设项目,都会强制走一遍"清浏览器缓存 → 重启 Tomcat → 看 Tomcat 日志尾部 → 查一条数据库记录"这四步,前十分钟看着像在浪费时间,其实是在把环境变量、端口、依赖这些隐形变量一次性排干净——从那以后我再没被"本地能跑、重启就挂"这种玄学问题坑过,希望帮到你。
本文还有配套的精品资源,点击获取