简介:这是一套基于SSM框架实现的Java电影在线订票系统完整源码,面向Java Web初学者与课程设计学生,解决影院管理、用户选座购票、订单与评价全流程开发实践问题。资源共84个文件,包含17个JSP页面(如admin/index.jsp、seat.jsp等)、17个Java业务类、17个编译后Class文件、8个核心Jar包(含Spring、MyBatis依赖)、8张界面截图JPG及movieSQL.sql数据库脚本,整体压缩包仅5.15MB,轻量易部署。已有1767人学习下载,适合作为Java Web综合实训项目:提供可直接运行的MyEclipse/IDEA/Tomcat多环境兼容方案,涵盖管理员后台(影院、影片、订单、公告全生命周期管理)与用户前台(选座、购物车、订单查询、评价)双角色功能模块,目录结构规范,WebRoot与src分层清晰,SQL脚本开箱即用,便于理解MVC分层设计与前后端交互逻辑。
1. 这不是又一个“Hello World”Java Web项目:它是一套能跑通、能改、能交差的SSM电影订票系统实战包
你手头正缺一个能直接导入IDE、改两行配置就能登录后台、查订单、加场次的Java Web毕业设计?不是那种只有空壳Controller、连数据库都连不上的“教学演示版”,而是真实包含管理员增删影院/排片/订单、用户选座下单/评价/查看电子票的完整闭环——这个网上电影订票系统就是。它用的是SSM(Spring + SpringMVC + MyBatis)老但稳的组合,不是Spring Boot新潮但坑多的版本;数据库脚本(电影SQL.sql)已建好表结构和初始数据;前端JSP页面(show.jsp,seat.jsp,play.jsp)带真实座位图渲染逻辑;后端代码里连@Transactional事务控制、@Valid参数校验、MyBatis动态SQL都写好了。适合两类人:一是大三学生赶毕设答辩,3天内能跑起来+改UI+换Logo;二是刚转Java的初级工程师,拿它当SSM全流程“解剖标本”——看清楚Filter怎么拦截未登录、Interceptor怎么记录操作日志、MyBatis怎么写一对多映射(一场电影对应多个座位)。别被标题里“myeclipse_ideajava”吓住,它本质是标准Maven结构(虽没pom.xml但.classpath和WEB-INF/lib全齐),IntelliJ IDEA导入后点几下就能编译,比某些号称“兼容所有IDE”的项目更老实。
2. 从压缩包到Tomcat启动:五步落地SSM项目,每步都卡在真实痛点上
2.1 解压后第一眼必须确认的三个文件夹结构
拿到网上电影订票系统的设计与实现.zip,别急着双击打开。先用命令行或资源管理器展开,重点盯这三个路径:
# 必须存在且路径严格匹配(大小写敏感!) src/com/ # Java源码根目录,里面是controller/service/dao/model包 WebRoot/ # 标准Java Web静态资源根目录(不是webapp!) WEB-INF/web.xml # 配置核心,SpringMVC DispatcherServlet在此注册提示:如果看到
src/main/java或webapp目录,说明你下载的是别人二次改造过的版本,原始包就长这样。WebRoot是Eclipse/MyEclipse时代的老约定,IDEA默认不认,但可强制指定为Web资源根目录。
为什么强调这个?因为后续所有配置错误(404、ClassNotFoundException)80%源于路径错位。比如src/com下若混着.java和.class文件,说明编译过但没清理干净;WebRoot/WEB-INF/lib里若缺spring-webmvc-4.3.25.RELEASE.jar,Tomcat启动时会报NoClassDefFoundError: org/springframework/web/servlet/DispatcherServlet——这问题我当年在MyEclipse里调了6小时才定位到jar包名拼写少了个-。
2.2 数据库初始化:MySQL 5.7+执行电影SQL.sql的实操细节
别直接双击SQL文件用Navicat执行。先手动建库再导入,否则中文乱码和外键约束会让你怀疑人生:
-- 步骤1:创建数据库(关键!字符集必须utf8mb4) CREATE DATABASE movie_ticket CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; -- 步骤2:切换库并执行脚本(注意路径) USE movie_ticket; SOURCE /path/to/电影SQL.sql; -- Linux/macOS用斜杠,Windows用反斜杠参数说明:
utf8mb4是必须项,电影SQL.sql里movie_info表的description字段存的是中文剧情简介,用utf8会截断emoji和生僻字;COLLATE utf8mb4_unicode_ci确保排序按中文习惯(如“张三”排在“李四”前)。
执行后检查三张核心表:
user_info:含管理员账号admin/123456和普通用户test/123456cinema_info:预置了北京万达、上海影城等5家影院play_info:有《流浪地球2》《满江红》等真实排片数据,seat_status字段存JSON格式座位状态({"A1":"0","A2":"1"},0=空闲,1=已售)
2.3 IDE导入:MyEclipse/IDEA双路径配置指南(附截图级参数)
MyEclipse 10.7+ 用户(最省事)
File → Import → Existing Projects into Workspace- 选中解压后的根目录 → 勾选项目 → Finish
- 右键项目 →
Properties → Project Facets→ 确保Dynamic Web Module版本为3.0(对应Tomcat 7+) Deployment Assembly里确认WebRoot映射到/,src映射到/WEB-INF/classes
IntelliJ IDEA 2022.3+ 用户(需手动补救)
File → Open → 选择解压目录→ 弹窗选Open as Project- 右键项目 →
Add Framework Support → Web Application (2.4)→ 点OK - 关键一步:
Project Structure → Modules → Sources→ 将src标记为Sources,WebRoot标记为Resources Artifacts → + → Web Application: Exploded → From Modules→ 选中项目 →Output directory设为out/artifacts/movie_ticket_war_explodedRun → Edit Configurations → Tomcat Server → Deployment → + → Artifact→ 选刚建的exploded包
注意:IDEA里
WebRoot不会自动识别为Web资源根,必须手动在Modules里设置。否则login.jsp访问时会报HTTP Status 404——因为JSP根本没被编译进/WEB-INF/classes。
2.4 Tomcat 8.5 配置要点:避免8080端口冲突和乱码的硬核参数
别用IDE自带Tomcat,下载独立版( tomcat.apache.org )更可控:
- 解压后进入
conf/server.xml,修改Connector节点:
<Connector port="8080" protocol="HTTP/1.1" connectionTimeout="20000" redirectPort="8443" URIEncoding="UTF-8" <!-- 关键!解决GET请求中文参数乱码 --> useBodyEncodingForURI="true" />bin/catalina.bat(Windows)或catalina.sh(Linux)开头添加:
# Windows set JAVA_OPTS=-Dfile.encoding=UTF-8 -Xms512m -Xmx1024m # Linux export JAVA_OPTS="-Dfile.encoding=UTF-8 -Xms512m -Xmx1024m"为什么加
-Dfile.encoding=UTF-8?因为movieSQL.sql里INSERT INTO news_info(title, content)的content含中文,若JVM默认编码是GBK,MyBatis查询结果会变成????。useBodyEncodingForURI="true"则让POST提交的username=张三不变成username=%E5%BC%A0%E4%B8%89。
2.5 启动验证:五个必测URL及其预期响应
启动Tomcat后,别只刷首页。按顺序测这五个地址,任一失败立刻停机排查:
| URL | 预期响应 | 失败现象 | 定位方向 |
|---|---|---|---|
http://localhost:8080/ | 跳转到index.jsp,显示“欢迎光临电影订票系统” | HTTP 404 | 检查web.xml中<welcome-file-list>是否指向index.jsp |
http://localhost:8080/login.jsp | 渲染登录表单,含用户名/密码输入框 | 页面空白或报错javax.servlet.ServletException: java.lang.NoClassDefFoundError | WEB-INF/lib缺jstl-1.2.jar或standard-1.1.2.jar |
http://localhost:8080/admin/index.jsp | 管理员后台首页,含“电影管理”“订单查询”菜单 | 403 Forbidden | web.xml中<security-constraint>未注释或admin角色未配置 |
http://localhost:8080/show.jsp?id=1 | 《流浪地球2》详情页,含海报、评分、排片列表 | NullPointerException | MovieService.findById(1)返回null,检查movie_info表id=1是否存在 |
http://localhost:8080/seat.jsp?playId=1 | 座位选择页,渲染A1-A10/B1-B10网格,已售座位灰色不可点 | 全部座位可点或全灰 | play_info表seat_status字段JSON格式错误,用SELECT seat_status FROM play_info WHERE id=1查原始值 |
3. SSM层拆解:看懂Controller/Service/DAO三层如何协作完成一次购票
3.1 Controller层:SpringMVC如何把HTTP请求路由到Java方法
以用户下单为例,入口在com.controller.OrderController.java:
@Controller @RequestMapping("/order") public class OrderController { @Autowired private OrderService orderService; // 依赖注入Service @RequestMapping(value = "/create", method = RequestMethod.POST) public String createOrder(@RequestParam("playId") Integer playId, @RequestParam("seatIds") String seatIds, HttpSession session) { User user = (User) session.getAttribute("user"); // 从Session取当前用户 if (user == null) return "redirect:/login.jsp"; // 未登录跳转 try { orderService.createOrder(user.getId(), playId, seatIds); // 调用Service return "redirect:/user/orderList.jsp"; // 成功跳转订单页 } catch (Exception e) { session.setAttribute("errorMsg", "下单失败:" + e.getMessage()); return "redirect:/seat.jsp?playId=" + playId; // 失败回座位页 } } }逻辑说明:
@RequestMapping("/order/create")将POST请求/order/create映射到此方法;@RequestParam自动绑定URL参数(如?playId=1&seatIds=A1,A2);HttpSession对象用于跨请求存用户信息;try-catch捕获Service层异常并友好提示。
关键参数:method = RequestMethod.POST限定仅接受POST,防止恶意GET刷单;redirect:前缀表示重定向而非转发,避免重复提交。
3.2 Service层:事务管理与业务逻辑的黄金分割线
com.service.impl.OrderServiceImpl.java是核心:
@Service @Transactional // 整个方法在同一个数据库事务中执行 public class OrderServiceImpl implements OrderService { @Autowired private OrderDao orderDao; @Autowired private PlayDao playDao; @Autowired private SeatDao seatDao; @Override public void createOrder(Integer userId, Integer playId, String seatIds) { // 步骤1:检查场次是否存在且未结束 Play play = playDao.findById(playId); if (play == null || play.getEndTime().before(new Date())) { throw new RuntimeException("场次不存在或已结束"); } // 步骤2:检查座位是否可售(双重校验:DB锁+应用层判断) String[] seats = seatIds.split(","); for (String seatId : seats) { Seat seat = seatDao.findByPlayIdAndSeatId(playId, seatId); if (seat == null || !"0".equals(seat.getStatus())) { // status=0为空闲 throw new RuntimeException("座位" + seatId + "已被占用"); } } // 步骤3:生成订单(插入order_info表) Order order = new Order(); order.setUserId(userId); order.setPlayId(playId); order.setSeatIds(seatIds); order.setCreateTime(new Date()); order.setStatus("1"); // 1=已支付 orderDao.insert(order); // 步骤4:更新座位状态(关键!必须原子性) for (String seatId : seats) { seatDao.updateStatus(playId, seatId, "1"); // 1=已售 } } }为什么用
@Transactional?因为步骤3和4必须同时成功或同时失败。若插入订单成功但更新座位失败,会导致“钱扣了但没出票”的资损事故。Spring的事务代理会在方法结束时自动commit,异常时rollback。
避坑点:seatDao.updateStatus()必须用UPDATE seat_info SET status='1' WHERE play_id=? AND seat_id=?,不能先SELECT再UPDATE——高并发下会超卖。原始包里seatDao用的是MyBatis XML写法,<update>标签内直接写SQL,规避了ORM懒加载陷阱。
3.3 DAO层:MyBatis XML映射文件里的性能暗礁
src/com/dao/mapper/SeatMapper.xml定义座位操作:
<mapper namespace="com.dao.SeatDao"> <!-- 查询某场次所有座位状态 --> <select id="findAllByPlayId" resultType="com.model.Seat"> SELECT id, play_id, seat_id, status FROM seat_info WHERE play_id = #{playId} ORDER BY seat_id </select> <!-- 更新单个座位状态(带乐观锁) --> <update id="updateStatus"> UPDATE seat_info SET status = #{status}, update_time = NOW() WHERE play_id = #{playId} AND seat_id = #{seatId} AND status = #{oldStatus} <!-- 旧状态校验,防并发覆盖 --> </update> </mapper>参数说明:
#{playId}是预编译占位符,防SQL注入;ORDER BY seat_id确保前端渲染时A1,A2...A10顺序排列;AND status = #{oldStatus}是乐观锁实现——更新前检查当前状态是否仍为“空闲”,若已被他人抢走则updateStatus返回0行影响,Service层捕获后抛异常。
常见误用:有人把seat_id写成#{seatId}却传入"A1,A2"字符串,MyBatis会生成WHERE seat_id = 'A1,A2'导致无匹配。正确做法是循环调用updateStatus,每次传单个seatId。
3.4 前端JSP:座位图渲染的JavaScript黑匣子解析
seat.jsp里这段JS决定用户体验:
// 解析后端传来的seat_status JSON字符串 var seatStatus = JSON.parse('<%= play.getSeatStatus() %>'); // 如 {"A1":"0","A2":"1"} var rows = ["A","B","C","D","E"]; // 座位行号 var cols = [1,2,3,4,5,6,7,8,9,10]; // 座位列号 for (var i = 0; i < rows.length; i++) { for (var j = 0; j < cols.length; j++) { var seatId = rows[i] + cols[j]; var status = seatStatus[seatId] || "0"; // 默认空闲 var btn = document.createElement("button"); btn.className = "seat-btn"; btn.innerHTML = seatId; if (status === "1") { btn.disabled = true; // 已售禁用 btn.style.backgroundColor = "#ccc"; } else if (selectedSeats.includes(seatId)) { btn.style.backgroundColor = "#ff6b6b"; // 已选红色 } document.getElementById("seat-grid").appendChild(btn); } }玄学点:
<%= play.getSeatStatus() %>是JSP表达式,直接输出JSON字符串。但若seatStatus含单引号(如{"A1":"0's"}),JSON.parse会报错。原始包里getSeatStatus()方法做了replaceAll("'", "\\'")转义,这是血泪经验——曾因导演名含撇号导致整个座位页白屏。
3.5 配置文件链:从web.xml到applicationContext.xml的加载顺序
整个SSM的启动流程像一条流水线:
- Tomcat读取
WEB-INF/web.xml→ 加载ContextLoaderListener ContextLoaderListener读取WEB-INF/applicationContext.xml→ 创建Spring容器(IoC)- Spring MVC的
DispatcherServlet读取WEB-INF/spring-mvc.xml→ 创建MVC容器(只管Controller) - MVC容器通过
<import resource="classpath:applicationContext.xml"/>引用主容器Bean
applicationContext.xml关键片段:
<!-- 扫描Service和DAO包 --> <context:component-scan base-package="com.service,com.dao" /> <!-- 配置MyBatis SqlSessionFactory --> <bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean"> <property name="dataSource" ref="dataSource"/> <property name="mapperLocations" value="classpath:com/dao/mapper/*.xml"/> </bean> <!-- 事务管理器 --> <bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager"> <property name="dataSource" ref="dataSource"/> </bean>为什么
mapperLocations写classpath:com/dao/mapper/*.xml?因为MyBatis需要从类路径(即WEB-INF/classes)下找XML文件。若你把SeatMapper.xml放错位置(如放到WebRoot下),SqlSessionFactory初始化时会报Invalid bound statement (not found): com.dao.SeatDao.findAllByPlayId。
4. 避坑指南:SSM电影订票系统五大翻车现场与后悔药
4.1 现象:启动Tomcat后控制台疯狂刷java.lang.ClassNotFoundException: org.springframework.web.servlet.DispatcherServlet
原因:WEB-INF/lib目录下缺spring-webmvc-4.3.25.RELEASE.jar,或jar包名拼写错误(如spring-webmvc-4.3.25.RELEAS.jar少了个E)。
解决:
- 下载 Spring Framework 4.3.25 GA 完整包
- 解压后取
spring-webmvc-4.3.25.RELEASE.jar放入WEB-INF/lib - 在IDE中右键项目 →
Build Path → Configure Build Path → Libraries → Add JARs→ 选中该jar
血泪经验:MyEclipse有时会自动删除lib里jar,检查
WEB-INF/lib文件数是否为23个(原始包含spring-core、spring-context、spring-jdbc等23个jar)。
4.2 现象:登录管理员账号admin/123456后跳转admin/index.jsp显示空白,浏览器F12看Network发现admin/index.jsp返回状态码200但Response为空
原因:admin/index.jsp顶部<%@ page contentType="text/html;charset=UTF-8" %>缺失,或web.xml中<jsp-config>未配置<default-page-encoding>UTF-8</default-page-encoding>。
解决:
- 打开
admin/index.jsp,在第一行添加:
<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%>- 在
WEB-INF/web.xml的<web-app>根节点内添加:
<jsp-config> <jsp-property-group> <url-pattern>*.jsp</url-pattern> <page-encoding>UTF-8</page-encoding> </jsp-property-group> </jsp-config>4.3 现象:点击“下单”按钮后页面卡死,Tomcat日志出现org.apache.ibatis.exceptions.PersistenceException: Error executing SQL update
原因:seat_info表status字段类型为VARCHAR(1),但MyBatis XML里<update>语句传入status="1"时,数据库实际存的是"1 "(带空格),导致下次WHERE status = '1'查不到。
解决:
- 登录MySQL执行:
ALTER TABLE seat_info MODIFY COLUMN status VARCHAR(1) NOT NULL DEFAULT '0'; UPDATE seat_info SET status = TRIM(status); -- 清理历史脏数据- 检查
SeatDao.updateStatus()方法,确保传入的status参数是纯数字字符串(无空格)。
4.4 现象:在IDEA中运行项目,访问http://localhost:8080/login.jsp报HTTP Status 404 – Not Found,但http://localhost:8080/能显示首页
原因:IDEA未将WebRoot设为Web资源根,导致JSP文件未被编译进/WEB-INF/classes,Tomcat找不到login.jsp。
解决:
File → Project Structure → Modules → Sources→ 选中WebRoot文件夹 → 点击Resources图标Artifacts → movie_ticket:war exploded → Output Directory→ 确认路径末尾是/(如/path/to/out/artifacts/movie_ticket_war_exploded/)- 重新Build Artifacts(
Build → Build Artifacts → Build)
4.5 现象:用户下单成功后,order_info表新增记录,但seat_info表对应座位status仍是"0"(空闲)
原因:OrderServiceImpl.createOrder()方法里seatDao.updateStatus()调用后,未检查返回值。MyBatis的update方法返回int(影响行数),若为0说明更新失败(如座位已被抢),但代码没处理。
解决:
修改createOrder()方法中的更新逻辑:
for (String seatId : seats) { int updated = seatDao.updateStatus(playId, seatId, "1"); if (updated == 0) { throw new RuntimeException("座位" + seatId + "更新失败,请重试"); } }5. 进阶技巧:三招让老旧SSM项目焕发新生,适配现代开发流
5.1 把JSP页面一键转成Vue组件:用Thymeleaf做过渡桥
直接重写前端成本太高,但JSP维护痛苦(无热更新、调试难)。我的做法是引入Thymeleaf,保留原有Controller,只替换View层:
pom.xml添加依赖(若无pom则新建):
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-thymeleaf</artifactId> <version>2.7.18</version> </dependency>src/main/resources/application.yml配置:
spring: thymeleaf: prefix: classpath:/templates/ suffix: .html cache: false- 将
WebRoot/show.jsp复制为src/main/resources/templates/show.html,改JSP标签为Thymeleaf:
<!-- 原JSP --> <c:forEach items="${movieList}" var="movie"> <div>${movie.name}</div> </c:forEach> <!-- Thymeleaf --> <div th:each="movie : ${movieList}"> <div th:text="${movie.name}"></div> </div>价值点:Thymeleaf模板可直接在浏览器打开(静态HTML),无需启动Tomcat;支持
th:if、th:each等指令,比JSTL更直观;与Spring Boot无缝集成,未来升级Boot零成本。
5.2 MyBatis Plus替代原生MyBatis:10行代码搞定CRUD
原生MyBatis写DAO太繁琐。用MyBatis Plus只需定义Entity和Mapper接口:
// Entity @Data @TableName("user_info") public class User { @TableId(type = IdType.AUTO) private Integer id; private String username; private String password; } // Mapper(无需XML) public interface UserMapper extends BaseMapper<User> {}然后在Service里直接调用:
@Service public class UserServiceImpl implements UserService { @Autowired private UserMapper userMapper; @Override public List<User> findAll() { return userMapper.selectList(null); // 自动拼SELECT * FROM user_info } }参数说明:
@TableName指定表名,@TableId标注主键;BaseMapper提供selectList、insert、updateById等通用方法;selectList(null)等价于SELECT * FROM user_info。比手写XML快5倍,且支持LambdaQueryWrapper构建复杂条件。
5.3 用Docker打包部署:三步生成可移植镜像
告别“在我机器上能跑”玄学。用Docker封装整个环境:
Dockerfile内容:
FROM tomcat:8.5-jre8 COPY movie_ticket.war /usr/local/tomcat/webapps/ COPY mysql-connector-java-5.1.47.jar /usr/local/tomcat/lib/ EXPOSE 8080- 构建镜像:
# 先用IDEA导出war包(Build → Build Artifacts → Build) docker build -t movie-ticket .- 运行容器(连接宿主机MySQL):
docker run -d \ --name movie-ticket \ -p 8080:8080 \ --network host \ # 直接使用宿主机网络,MySQL用127.0.0.1访问 movie-ticket验证技巧:
docker logs movie-ticket看Tomcat启动日志;curl http://localhost:8080/login.jsp返回HTML源码即成功。镜像体积仅280MB,发给同事docker load -i movie-ticket.tar即可秒级部署。
从那以后我每次接手老SSM项目,都强制走一遍这三步:先用Thymeleaf解耦前端,再用MyBatis Plus重写DAO,最后用Docker固化环境。不是为了炫技,而是让“能跑通”的项目真正变成“能交付、能维护、能演进”的资产。希望帮到你。
本文还有配套的精品资源,点击获取