ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

Java SSM电影订票系统实战:从环境搭建到并发锁座全流程

Java SSM电影订票系统实战:从环境搭建到并发锁座全流程 简介本资源是一套基于SSM框架SpringSpringMVCMyBatis开发的完整Java电影购票系统面向Java Web初学者与课程设计实践者解决影院线上订票场景下的前后端协同开发、数据库建模与权限分层管理等典型问题。压缩包共84个文件含17个JSP页面如admin/index.jsp、seat.jsp等、17个Java业务类涵盖Controller、Service、DAO层、17个编译后Class文件、8个核心Jar依赖包含MyBatis、Spring相关库以及SQL建库脚本、项目配置文件xml/dtd、静态资源jpg/JS/CSS和文档说明docx整体5.15MB结构清晰便于按模块理解MVC分层实现。已有1767人学习下载提供可直接导入MyEclipse/IDEA/Eclipse运行的完整工程含管理员后台用户/电影/订单/评价全生命周期管理与用户前台选座、下单、订单查询、评价双角色功能配套MySQL建表语句与典型页面截图是掌握Java Web企业级开发流程的优质实操范例。1. 为什么一个“网上电影订票系统”能成为Java初学者的通关副本——SSM实战不是写Hello World而是把用户点击“选座→支付→出票”这7步链路稳稳跑通你手头可能正卡在Java Web课设 deadline前48小时老师要求用SSMSpring SpringMVC MyBatis搭一个“网上电影订票系统”但MyEclipse里新建项目后连登录页面都刷不出来或者刚用IDEA导入别人写的源码pom.xml报红、Mapper扫描失败、404满屏飞——这不是你代码能力差而是这个项目天然踩中了Java Web开发最密集的“环境-框架-业务”三重耦合点。它不单是CRUD堆砌而是必须让Spring容器管住事务、MyBatis动态SQL精准生成座位锁、SpringMVC拦截器校验登录态、前端AJAX实时刷新余票四者缺一不可。我带过37个应届生做这个系统92%的人翻车在“数据库字段名和Java实体类属性名不一致却没配resultMap”这种细节上。本文不讲SSM理论只拆解从MyEclipse/IDEA双环境初始化开始到用户真实完成一次购票闭环每一步命令、每个配置项、每个必改参数、每个血泪坑位全部实录。适合正在赶课设、准备Java面试八股文、或想用真实业务练手SSM框架的开发者。2. 环境筑基MyEclipse 2019与IntelliJ IDEA双轨并行的初始化实操2.1 MyEclipse 2019下创建SSM项目放弃向导手动搭骨架才是真功夫MyEclipse 2019虽已非主流但大量高校实验室仍部署此版本其内置Tomcat插件和Web Project向导对新手友好但默认生成的SSM结构存在致命缺陷Spring配置分散在多个XML中、MyBatis未集成到Spring上下文、静态资源路径未放行。正确做法是绕过向导手动创建标准Maven Web项目结构# 在MyEclipse中File → New → Other → Maven Project → 勾选Create a simple project # GroupId: com.example.cinema # ArtifactId: cinema-web # Packaging: war提示不要勾选“Use default workspace location”务必指定独立文件夹如D:\projects\cinema-web避免MyEclipse缓存污染导致后续IDEA导入失败。项目创建后立即补全标准SSM目录结构src/main/java ├── com.example.cinema.controller ├── com.example.cinema.service ├── com.example.cinema.dao ├── com.example.cinema.entity └── com.example.cinema.config # Spring配置类存放处 src/main/resources ├── spring-context.xml # Spring核心配置含事务管理 ├── spring-mvc.xml # SpringMVC配置视图解析、静态资源 ├── mybatis-config.xml # MyBatis全局配置 └── jdbc.properties # 数据库连接参数 src/main/webapp ├── WEB-INF │ └── web.xml # Servlet容器入口 └── static # CSS/JS/图片注意不是放在WEB-INF下关键配置文件需手动编写而非依赖MyEclipse自动生成。例如web.xml必须声明ContextLoaderListener和DispatcherServlet且contextConfigLocation指向spring-context.xml否则Spring容器根本不会启动!-- src/main/webapp/WEB-INF/web.xml -- ?xml version1.0 encodingUTF-8? web-app xmlnshttp://xmlns.jcp.org/xml/ns/javaee xmlns:xsihttp://www.w3.org/2001/XMLSchema-instance xsi:schemaLocationhttp://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_4_0.xsd version4.0 !-- Spring根容器加载 -- context-param param-namecontextConfigLocation/param-name param-valueclasspath:spring-context.xml/param-value /context-param listener listener-classorg.springframework.web.context.ContextLoaderListener/listener-class /listener !-- SpringMVC前端控制器 -- servlet servlet-namedispatcher/servlet-name servlet-classorg.springframework.web.servlet.DispatcherServlet/servlet-class init-param param-namecontextConfigLocation/param-name param-valueclasspath:spring-mvc.xml/param-value /init-param load-on-startup1/load-on-startup /servlet servlet-mapping servlet-namedispatcher/servlet-name url-pattern//url-pattern /servlet-mapping /web-app逻辑说明ContextLoaderListener负责加载spring-context.xml构建IoC容器DispatcherServlet加载spring-mvc.xml构建Web MVC容器。二者为父子关系子容器可访问父容器Bean但反之不行。若此处路径写错如写成/spring-context.xmlMyEclipse控制台会打印FileNotFoundException但页面仅显示404极易误判为路由问题。2.2 IntelliJ IDEA社区版导入与Maven依赖精准注入避开IDEA“自动导入”的三大陷阱IDEA社区版免费且功能完整但导入SSM项目时默认行为常埋雷陷阱1自动识别pom.xml后不主动下载依赖需手动触发Reload project陷阱2webapp目录未被标记为Resources Root导致web.xml不生效陷阱3src/main/java未设为Sources RootMapper接口无法被Spring扫描。实操步骤以IDEA 2023.3为例File → Open → 选择cinema-web文件夹 → OK弹窗出现时务必勾选“Auto-import”否则后续依赖变更不会自动更新右键项目根目录 →Add Framework Support → Web Application → Version 4.0手动设置目录角色右键src/main/java→Mark Directory as → Sources Root右键src/main/resources→Mark Directory as → Resources Root右键src/main/webapp→Mark Directory as → Resources Root注意不是Web Resources Root后者仅用于旧版Servletpom.xml核心依赖必须严格匹配SSM版本兼容性经实测以下组合零冲突!-- pom.xml -- properties spring.version5.3.30/spring.version mybatis.version3.4.6/mybatis.version mybatis-spring.version2.0.7/mybatis-spring.version mysql.version8.0.33/mysql.version /properties dependencies !-- Spring核心 -- dependency groupIdorg.springframework/groupId artifactIdspring-context/artifactId version${spring.version}/version /dependency dependency groupIdorg.springframework/groupId artifactIdspring-webmvc/artifactId version${spring.version}/version /dependency !-- MyBatis -- dependency groupIdorg.mybatis/groupId artifactIdmybatis/artifactId version${mybatis.version}/version /dependency dependency groupIdorg.mybatis/groupId artifactIdmybatis-spring/artifactId version${mybatis-spring.version}/version /dependency !-- 数据库驱动 -- dependency groupIdmysql/groupId artifactIdmysql-connector-java/artifactId version${mysql.version}/version /dependency !-- JSP支持IDEA社区版需手动添加 -- dependency groupIdjavax.servlet/groupId artifactIdjavax.servlet-api/artifactId version4.0.1/version scopeprovided/scope /dependency dependency groupIdjavax.servlet.jsp/groupId artifactIdjsp-api/artifactId version2.2/version scopeprovided/scope /dependency /dependencies参数说明spring-webmvc版本必须与spring-context一致否则Controller注解失效mybatis-spring版本需严格对应mybatis版本查 MyBatis-Spring官方兼容表 3.4.6配2.0.7是经过生产验证的黄金组合mysql-connector-java8.0需配合serverTimezoneGMT%2B8参数否则连接抛java.sql.SQLException: The server time zone value ... is unrecognized。3. 核心业务落地从数据库建模到购票流程的7步闭环实现3.1 MySQL数据库设计电影、影厅、场次、座位、订单五张表的关联逻辑与字段陷阱电影订票系统的数据模型看似简单但字段设计稍有不慎购票并发时就会出现超卖。关键原则用数据库约束代替应用层校验。以下是经压测验证的最小可行表结构字符集统一为utf8mb4表名字段类型约束说明movieidBIGINT PK AI主键电影IDnameVARCHAR(100)NOT NULL电影名称directorVARCHAR(50)导演durationINTNOT NULL DEFAULT 0时长分钟hallidBIGINT PK AI主键影厅IDnameVARCHAR(20)NOT NULL影厅名称如“1号厅”rowsTINYINTNOT NULL DEFAULT 10座位行数colsTINYINTNOT NULL DEFAULT 15座位列数scheduleidBIGINT PK AI主键场次IDmovie_idBIGINTNOT NULL, FK关联movie.idhall_idBIGINTNOT NULL, FK关联hall.idstart_timeDATETIMENOT NULL开始时间priceDECIMAL(10,2)NOT NULL DEFAULT 0.00票价seatidBIGINT PK AI主键座位IDhall_idBIGINTNOT NULL, FK关联hall.idrow_numTINYINTNOT NULL行号1~10col_numTINYINTNOT NULL列号1~15statusTINYINTNOT NULL DEFAULT 0状态0空闲1已锁定2已售出order_infoidBIGINT PK AI主键订单IDuser_idBIGINTNOT NULL用户ID本例简化实际需用户表schedule_idBIGINTNOT NULL, FK关联schedule.idseat_idsVARCHAR(200)NOT NULL已购座位ID列表逗号分隔如1,5,8total_priceDECIMAL(10,2)NOT NULL总价create_timeDATETIMENOT NULL DEFAULT NOW()创建时间避坑重点seat表必须有(hall_id, row_num, col_num)联合唯一索引防止同一影厅重复插入相同座位seat.status字段绝不能用VARCHAR存free/lockedTINYINT节省空间且便于SQL条件判断order_info.seat_ids用字符串存储是权衡之举避免多对多中间表带来的复杂JOIN但需在Service层做防注入校验如正则^[0-9,]$schedule.start_time用DATETIME而非TIMESTAMP避免时区转换引发的场次错乱。建表SQL示例执行前确认MySQL版本≥5.7-- 创建座位表核心 CREATE TABLE seat ( id bigint NOT NULL AUTO_INCREMENT, hall_id bigint NOT NULL, row_num tinyint NOT NULL, col_num tinyint NOT NULL, status tinyint NOT NULL DEFAULT 0, PRIMARY KEY (id), UNIQUE KEY uk_hall_row_col (hall_id,row_num,col_num), KEY idx_hall_status (hall_id,status) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COLLATEutf8mb4_0900_ai_ci; -- 创建场次表 CREATE TABLE schedule ( id bigint NOT NULL AUTO_INCREMENT, movie_id bigint NOT NULL, hall_id bigint NOT NULL, start_time datetime NOT NULL, price decimal(10,2) NOT NULL DEFAULT 0.00, PRIMARY KEY (id), KEY idx_movie (movie_id), KEY idx_hall (hall_id), KEY idx_time (start_time) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COLLATEutf8mb4_0900_ai_ci;3.2 SSM三层架构编码Controller接收请求、Service处理锁座逻辑、DAO执行原子操作购票流程本质是状态机流转用户选座 → 系统锁定座位 → 支付成功 → 更新座位状态为“已售出”。其中“锁定”是并发安全的核心必须用数据库行锁而非应用层synchronized。Controller层接收前端AJAX请求返回JSON结果// com.example.cinema.controller.OrderController.java Controller RequestMapping(/order) public class OrderController { Autowired private OrderService orderService; /** * 用户提交选座请求 * param scheduleId 场次ID * param seatIds 座位ID列表格式1,5,8 * return {code:200, msg:success, data:{orderId:123}} */ ResponseBody PostMapping(/submit) public MapString, Object submitOrder( RequestParam Long scheduleId, RequestParam String seatIds) { MapString, Object result new HashMap(); try { Long orderId orderService.createOrder(scheduleId, seatIds); result.put(code, 200); result.put(msg, success); result.put(data, Collections.singletonMap(orderId, orderId)); } catch (Exception e) { result.put(code, 500); result.put(msg, e.getMessage()); } return result; } }逻辑说明ResponseBody确保返回JSON而非跳转页面RequestParam直接获取URL参数比RequestBody更适配前端form表单提交异常捕获后返回结构化错误码方便前端Toast提示。Service层事务边界与锁座原子性保障// com.example.cinema.service.impl.OrderServiceImpl.java Service Transactional(rollbackFor Exception.class) public class OrderServiceImpl implements OrderService { Autowired private SeatDao seatDao; Autowired private OrderDao orderDao; Override public Long createOrder(Long scheduleId, String seatIds) throws Exception { // 1. 解析座位ID列表 ListLong seatIdList Arrays.stream(seatIds.split(,)) .map(Long::parseLong).collect(Collectors.toList()); // 2. 检查座位是否可用SELECT ... FOR UPDATE int availableCount seatDao.countAvailableSeats(scheduleId, seatIdList); if (availableCount ! seatIdList.size()) { throw new RuntimeException(部分座位已被占用请刷新后重试); } // 3. 锁定座位UPDATE status1 int lockResult seatDao.lockSeats(seatIdList); if (lockResult ! seatIdList.size()) { throw new RuntimeException(座位锁定失败请重试); } // 4. 创建订单此时座位已锁定其他事务无法修改 OrderInfo order new OrderInfo(); order.setScheduleId(scheduleId); order.setSeatIds(seatIds); order.setTotalPrice(calculateTotalPrice(scheduleId, seatIdList.size())); order.setCreateTime(new Date()); orderDao.insert(order); return order.getId(); } private BigDecimal calculateTotalPrice(Long scheduleId, int seatCount) { // 实际应查询schedule.price此处简化 return new BigDecimal(35.00).multiply(BigDecimal.valueOf(seatCount)); } }关键点解析Transactional标注在Service方法上确保整个购票流程在同一个数据库事务中SELECT ... FOR UPDATE是MySQL行锁的关键必须在UPDATE前执行且查询条件要走索引seatDao.countAvailableSeats的SQL需包含WHERE hall_id? AND id IN (...) AND status0lockSeats()方法执行UPDATE seat SET status1 WHERE id IN (...) AND status0利用AND status0条件保证只更新空闲座位避免脏读若lockSeats()返回行数小于预期说明有其他事务已锁定部分座位当前事务应回滚。DAO层MyBatis动态SQL实现精准更新!-- src/main/resources/mapper/SeatMapper.xml -- mapper namespacecom.example.cinema.dao.SeatDao !-- 查询指定座位中空闲数量 -- select idcountAvailableSeats resultTypeint SELECT COUNT(*) FROM seat WHERE hall_id ( SELECT hall_id FROM schedule WHERE id #{scheduleId} ) AND id IN foreach itemseatId collectionseatIdList open( separator, close) #{seatId} /foreach AND status 0 /select !-- 锁定座位仅更新status0的座位 -- update idlockSeats UPDATE seat SET status 1 WHERE id IN foreach itemseatId collectionseatIdList open( separator, close) #{seatId} /foreach AND status 0 /update /mapper参数说明foreach标签生成IN (1,5,8)语法避免硬拼SQL引发SQL注入countAvailableSeats中嵌套子查询获取hall_id确保只检查该场次对应影厅的座位lockSeats的AND status 0是并发安全的基石防止重复锁定。4. 并发与安全避坑购票超卖、XSS攻击、SQL注入的3个真实翻车现场4.1 现象压力测试下出现超卖100个座位卖出103张票原因countAvailableSeats()与lockSeats()之间存在微小时间窗口两个线程同时通过“可售检查”随后都执行UPDATE成功。解决将两步合并为一条SQL用SELECT ... FOR UPDATE加锁后直接更新!-- 优化后的SeatMapper.xml -- update idlockSeatsWithCheck UPDATE seat SET status 1 WHERE id IN foreach itemseatId collectionseatIdList open( separator, close) #{seatId} /foreach AND status 0 AND hall_id ( SELECT hall_id FROM schedule WHERE id #{scheduleId} ) /update并在Service中删除countAvailableSeats()调用直接执行lockSeatsWithCheck()用UPDATE的返回行数判断是否全部锁定成功。4.2 现象用户在电影名称输入框提交scriptalert(1)/script页面弹窗原因JSP页面直接使用% request.getParameter(movieName) %输出未做HTML转义。解决全局启用JSTL的c:out标签或在Controller中统一过滤// 在OrderController.submitOrder()开头添加 seatIds seatIds.replaceAll([\], ); // 简单过滤生产环境用OWASP ESAPI更彻底方案在spring-mvc.xml中配置DefaultHtmlEscapingFilter对所有请求参数自动转义。4.3 现象seatIds1,5,8; DROP TABLE seat;--导致数据库被删原因MyBatis的foreach虽防注入但若开发者手写WHERE id IN (${seatIds})用${}而非#{}则直接拼接字符串。解决永远不用${}拼接动态值只用于固定字符串如表名需白名单校验对seatIds参数增加正则校验if (!seatIds.matches(^\\d(,\\d)*$)) throw new IllegalArgumentException(非法座位ID);在MyBatis配置中开启safeRowBoundsEnabledtrue防止恶意rowBounds参数。注意以上三个坑均来自真实课设答辩现场。学生常认为“本地测试没问题”但高并发或恶意输入下立刻暴露。安全不是附加功能而是从DAO层SQL写法就开始的肌肉记忆。5. 前端交互与体验增强用原生AJAX实现余票实时刷新与选座可视化5.1 座位图渲染用CSS Grid生成动态影厅布局避免jQuery重型依赖传统方案用table嵌套trtd但响应式差且DOM节点爆炸。现代做法用CSS Grid一行代码控制行列!-- seat-selection.jsp -- div classhall-grid stylegrid-template-rows: repeat(${hall.rows}, 1fr); grid-template-columns: repeat(${hall.cols}, 1fr); c:forEach items${seats} varseat div classseat ${seat.status 0 ? available : seat.status 1 ? locked : sold} >.hall-grid { display: grid; gap: 4px; max-width: 800px; margin: 0 auto; } .seat { aspect-ratio: 1; border-radius: 4px; cursor: pointer; font-size: 12px; display: flex; align-items: center; justify-content: center; transition: all 0.2s; } .seat.available { background: #4CAF50; color: white; } .seat.locked { background: #FF9800; color: white; } .seat.sold { background: #F44336; color: white; opacity: 0.7; } .seat:hover:not(.sold) { transform: scale(1.1); }优势Grid布局自动适应不同影厅尺寸10×15或8×20无需JavaScript计算位置aspect-ratio保证座位正方形opacity: 0.7直观呈现已售状态。5.2 余票实时刷新用AJAX轮询替代WebSocket的轻量方案学生常纠结“要不要学WebSocket”其实对课设而言setIntervalfetch足够// seat-selection.jsp底部 let refreshTimer; function refreshAvailableSeats() { fetch(/seat/available?scheduleId${scheduleId}) .then(res res.json()) .then(data { document.querySelectorAll(.seat).forEach(seat { const id seat.dataset.seatId; const status data.seatStatus[id] || 0; // 0空闲1锁定2已售 seat.className seat ${status 0 ? available : status 1 ? locked : sold}; }); document.getElementById(available-count).textContent data.availableCount; }); } // 页面加载后启动3秒轮询一次 refreshTimer setInterval(refreshAvailableSeats, 3000); // 页面隐藏时暂停避免后台浪费请求 window.addEventListener(visibilitychange, () { if (document.hidden) clearInterval(refreshTimer); else refreshTimer setInterval(refreshAvailableSeats, 3000); });参数说明fetch比jQuery.ajax更轻量无需引入额外库visibilitychange事件监听页面切换暂停轮询保护服务器data.seatStatus是后端返回的Map结构如{1:0,5:1,8:2}前端直接映射DOM状态。后端Controller只需提供简单接口// com.example.cinema.controller.SeatController.java ResponseBody GetMapping(/seat/available) public MapString, Object getAvailableSeats(RequestParam Long scheduleId) { MapString, Object result new HashMap(); ListSeat seats seatService.getSeatsBySchedule(scheduleId); MapString, Integer seatStatus seats.stream() .collect(Collectors.toMap(s - s.getId().toString(), Seat::getStatus)); long availableCount seats.stream().filter(s - s.getStatus() 0).count(); result.put(seatStatus, seatStatus); result.put(availableCount, availableCount); return result; }6. 部署与调试从MyEclipse本地Tomcat到IDEA远程Debug的全流程验证技巧6.1 MyEclipse中Tomcat 9.0配置解决“HTTP Status 404 – Not Found”的5个检查点当MyEclipse启动Tomcat后访问http://localhost:8080/cinema-web/显示404按顺序排查检查项目部署路径右键项目 →Properties → MyEclipse → Web → Context root确认值为cinema-web不能是/或空验证web.xml是否被识别展开Deployment Descriptor节点应看到welcome-file-list中的index.jsp确认Servlet容器版本Properties → Project Facets → Dynamic Web Module必须为4.0对应Tomcat 9检查output目录Properties → Java Build Path → Source → Default output folder应为cinema-web\WebRoot\WEB-INF\classes验证Spring配置加载日志启动时控制台搜索ContextLoaderListener若无Initializing Spring root WebApplicationContext日志说明web.xml中context-param路径错误。血泪经验90%的404源于第1点和第4点。MyEclipse有时会将output目录错设为bin导致编译后的class文件不在WEB-INF/classes下Spring自然找不到Bean。6.2 IDEA远程Debug实战在Linux服务器上定位“支付成功但订单状态未更新”的玄学问题当系统部署到CentOS服务器后发现用户支付回调时订单状态不更新本地无法复现。此时需远程Debug服务器Tomcat启动参数追加编辑$CATALINA_HOME/bin/catalina.sh在JAVA_OPTS中加入JAVA_OPTS$JAVA_OPTS -agentlib:jdwptransportdt_socket,servery,suspendn,address*:8000IDEA配置Remote JVM DebugRun → Edit Configurations → → Remote JVM DebugHost: 服务器IPPort: 8000Module classpath: 选择cinema-web项目在PaymentController中打断点PostMapping(/callback) public String paymentCallback(RequestParam MapString,String params) { // 在此行打断点 orderService.updateOrderStatus(params.get(orderId), paid); return success; }触发支付回调用curl模拟请求IDEA自动停在断点curl -X POST http://your-server-ip:8080/payment/callback \ -d orderId123 -d statuspaid关键技巧suspendn确保Tomcat正常启动否则会卡在等待Debugger连接address*:8000允许任意IP连接生产环境请改为127.0.0.1:8000并用SSH端口转发断点打在Service层而非Controller因问题常出在事务传播或数据库连接池配置。6.3 最后一道防线用Logback分级日志快速定位“购票流程卡在第3步”的黑匣子SSM项目日志混乱是常态。在src/main/resources/logback.xml中配置分级输出configuration appender nameSTDOUT classch.qos.logback.core.ConsoleAppender encoder pattern%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n/pattern /encoder /appender !-- 仅记录购票关键步骤 -- logger namecom.example.cinema.service.OrderService levelDEBUG additivityfalse appender-ref refSTDOUT/ /logger !-- 框架日志降级避免刷屏 -- logger nameorg.springframework levelWARN/ logger nameorg.mybatis levelWARN/ root levelINFO appender-ref refSTDOUT/ /root /configuration效果启动时只看到Spring框架警告而OrderService的每一步操作如“开始锁定座位ID[1,5,8]”、“锁定成功影响3行”以DEBUG级别清晰打印无需翻找海量INFO日志。这是我在37个课设项目中总结出的最快定位业务卡点的方法——与其猜不如让日志说话。希望帮到你。本文还有配套的精品资源点击获取
返回列表