☰
Spring Boot电影院售票系统实战:从数据模型到选座并发与部署全解析
2026/9/28 12:57:07 网站建设 项目流程

每到毕业设计季节,总有人问我"电影院售票系统怎么做",其实这个题目在网上热度一直不低。Springboot基于Web的电影院售票系统,说白了就是一套围绕"影片展示、场次查询、在线选座、订单支付"这条业务链展开的Java Web项目,常被选作毕设或课程设计。但问题是:网上资源要么只有源码没数据库脚本,要么环境配置文档写得云里雾里,照着做还各种报错。这篇东西就是把我自己做过的一版完整实现记录下来,从需求拆解、数据模型设计、核心售票流程,到开发环境搭建、调试部署,尽量把每一步的来龙去脉讲透,适合正在做相关题目的同学,也适合想拿Springboot练手搞一个完整项目的人参考。

1. 电影院售票系统的核心需求拆解与Springboot选型分析

1.1 这个系统到底要解决什么业务问题

先别急着写代码。很多人一上来就建工程、建表,结果做到一半发现业务逻辑对不上。电影院售票系统,本质上是线下售票窗口的线上化。我们要解决的无非是这么几件事:用户能看到正在上映的电影,选择某个场次,看到这个场次里哪些座位是空的,挑一个座位下单,完成支付,生成一张电子票据。反过来,管理员要维护电影信息、安排场次、管理座位状态,还需要处理订单变动。把这条业务链想清楚,系统的边界就出来了。

这个系统我按角色拆成两块:用户端和管理端。用户端包括注册登录、电影列表与详情、场次查询、选座页面、提交订单、订单列表;管理端则负责影片信息管理、场次排片、订单管理以及基础的统计数据。这里多说一句,座位管理不要设计成单独的维护界面,座位本身是跟着场次走的,管理员只需要选好影厅和场次,座位就自动生成,这样设计最省事,后面实现也简单。

1.2 为什么选Springboot而不是传统的SSM

如果纯粹为了完成功能,SSH、SSM甚至纯Servlet都能做。但实际做下来你会发现,SSM的配置成本太高了,各种XML、注解混在一起,搭环境的时间比写代码还多。Springboot的核心优势就是自动配置和约定优于配置,内嵌Tomcat,打一个jar包就能跑,整个开发调试的节奏会快很多。

具体到我们这个系统,Springboot带来几个直观的好处:Web场景、MyBatis、数据库连接池这些组件通过starter依赖一键引入,不用自己写复杂的配置文件;项目启动时自动扫描并配置数据源,省掉Spring容器那一堆装配代码;内置Tomcat让本地调试不用单独装Servlet容器,写完直接启动Application类就行。对于毕设或者课程设计这种交付周期偏紧的项目,这些优势非常实际。

当然,Springboot并不能替你解决所有问题,数据库设计、事务控制、并发处理这些核心业务能力,还是要靠自己写扎实。这个后面会专门展开讲。

1.3 技术栈清单与版本选择建议

这一版我用的技术组合如下,都是比较主流、资料多的方案:

组件版本说明
JDK1.8最稳的选择,毕设环境兼容性最好
Springboot2.7.x2.x配JDK8,避免javax/jakarta命名空间的坑
MyBatis Plus3.5.x单表CRUD不用写SQL,内置分页插件
MySQL5.7或8.05.7更稳,8.0注意时区配置
Maven3.6+依赖管理,打包部署需要
IDEA2023.x社区版够用,旗舰版更顺手

版本匹配这个事我一直很看重。Springboot 3.x虽然出来挺久了,但它要求JDK17,且原有的javax.servlet要迁移成jakarta.servlet,很多老教程里的代码直接跑不起来。如果你是最新装的环境,JDK只能装17以上,也可以选Springboot 3.x,但搜问题时要带上关键词,别拿2.x的解法套上去。我下面写的实现细节,默认基于JDK8 + Springboot 2.7.x这套组合。

2. 数据库设计:售票系统的数据模型是怎么搭出来的

2.1 五张核心表的设计思路

电影院售票系统说复杂也复杂,说简单也简单。数据库层面,我最终只保留了五张核心表:用户表、电影表、场次表、座位表、订单表。这里有人会问,为什么不把座位设计成影厅维度的固定座位,而是挂在场次下面?这是我在做第一版时踩过的坑——座位是跟着场次走的,同一个影厅在不同场次可能有不同的开放区域,而且每个场次的卖座情况各不相同,所以座位必须记录在场次维度。

五张表的核心字段我列一下,方便对照着建:

-- 用户表 CREATE TABLE `user` ( `user_id` int NOT NULL AUTO_INCREMENT, `username` varchar(50) NOT NULL, `password` varchar(255) NOT NULL, `nickname` varchar(50) DEFAULT NULL, `role` tinyint DEFAULT '0', -- 0普通用户,1管理员 `create_time` datetime DEFAULT NULL, PRIMARY KEY (`user_id`) ); -- 电影表 CREATE TABLE `movie` ( `movie_id` int NOT NULL AUTO_INCREMENT, `title` varchar(100) NOT NULL, `poster` varchar(255) DEFAULT NULL, `duration` int DEFAULT NULL, -- 片长,单位分钟 `genre` varchar(50) DEFAULT NULL, `release_date` date DEFAULT NULL, `status` tinyint DEFAULT '0', -- 0上映中,1下架 PRIMARY KEY (`movie_id`) ); -- 场次表 CREATE TABLE `schedule` ( `schedule_id` int NOT NULL AUTO_INCREMENT, `movie_id` int NOT NULL, `hall_name` varchar(20) DEFAULT '1号厅', `show_time` datetime NOT NULL, `price` decimal(10,2) NOT NULL, PRIMARY KEY (`schedule_id`) ); -- 座位表 CREATE TABLE `seat` ( `seat_id` bigint NOT NULL AUTO_INCREMENT, `schedule_id` int NOT NULL, `row_num` int NOT NULL, -- 排号 `col_num` int NOT NULL, -- 列号 `status` tinyint DEFAULT '0', -- 0可选,1已锁定,2已售出 PRIMARY KEY (`seat_id`) ); -- 订单表 CREATE TABLE `orders` ( `order_id` varchar(32) NOT NULL, `user_id` int NOT NULL, `schedule_id` int NOT NULL, `seat_id` bigint NOT NULL, `amount` decimal(10,2) NOT NULL, `status` tinyint DEFAULT '0', -- 0待支付,1已支付,2已取消 `create_time` datetime DEFAULT NULL, `pay_time` datetime DEFAULT NULL, PRIMARY KEY (`order_id`) );

这里有个细节要注意:订单表的主键我没用自增ID,而是用了一个业务订单号。为什么?因为自增ID一旦暴露在URL或者接口返回值里,很容易被遍历出订单数据,存在安全隐患。订单号我采用"yyyyMMddHHmmss + 用户ID + 四位随机数"的拼法,例如"20250614123000012x3f2",既保证查询索引效率,又不好被猜出来。

2.2 关键字段的取舍和设计理由

先说说seat表的status字段。我用0、1、2三个值表示"可选、已锁定、已售出"。为什么要单独设一个"已锁定"状态?因为在选座下单的过程中,用户有几分钟时间去支付,这段时间座位不能被其他人选走,但又不能直接标记成"已售出"。这个中间状态非常关键,它对应的是订单表里的"待支付"。两个表的状态是联动的:座位已锁定对应订单待支付;座位已售出对应订单已支付;订单取消后座位回到可选状态。这套状态机的逻辑后面在售票流程里会细讲。

amount字段用decimal(10,2)而不是float/double,这是老生常谈但经常有人忽略。金额在Java里用BigDecimal,数据库里用decimal,否则会出现0.1+0.2 != 0.3这种精度问题,做支付相关功能时绝对不允许。

movie表的poster字段存的是图片URL路径,不是二进制图片数据。把图片直接存进数据库是非常糟糕的做法,会让数据库表体积膨胀,查询变慢。正确做法是图片文件放本地目录或者OSS,数据库里只存访问路径。

2.3 初始化数据你需要准备哪些

空表没法演示功能,所以我建了一套初始化数据,包含一个管理员账号、一个普通测试用户、三四部电影、以及每个场次的座位数据。管理员账号密码不用加密算法搞太复杂,用MD5加盐足够了,但普通用户的密码建议用BCrypt,Spring Security自带的加密工具就能生成。

场次生成座位这块,我是在管理端保存场次的同时,根据影厅规模用循环语句批量生成座位记录。比如影厅配置为8排10列,一个场次就会生成80条座位记录,status全部为0。这个逻辑写在场次的Service层,生成座位和保存场次放在同一个事务里,保证不会出现场次保存成功但座位没生成的情况。

3. 选座、下单与支付状态:售票核心流程的实现逻辑

3.1 观影选座的座位锁定策略

这是整个系统技术含量比较高的地方。用户在选座页面点击一个座位,前端先拿到座位号,然后向后端发起锁定请求。后端拿到scheduleId和seatId时,不能直接更新状态,而是要做两层判断。

第一层,检查这个座位当前状态是否为0。如果是0,继续;如果不是,直接提示"该座位已被选择"。第二层,用条件更新语句原子地把状态从0改成1,这一步尤其关键。为什么强调原子性?因为如果先查再改,两个请求同时查到status=0,就会双双判断成功,然后各自执行更新,最终造成一票多卖。条件更新SQL是这么写的:

int rows = seatMapper.update( new LambdaUpdateWrapper<Seat>() .eq(Seat::getSeatId, seatId) .eq(Seat::getStatus, 0) .set(Seat::getStatus, 1) ); if (rows == 0) { throw new BizException("座位已被锁定,请重新选择"); }

update返回的影响行数等于1,才说明是我抢到了这把锁;返回0,说明期间状态已经变化,座位不可用。这个写法比Java代码里Synchronized靠谱,后面会解释为什么。

锁定座位之后,后端创建一条待支付订单。订单创建和座位锁定这两个操作必须放在同一个事务里,任何一个失败都要回滚,否则会出现订单没建上但座位锁死了,或者座位没锁上但订单已经生成的情况。用@Transactional注解包住整个方法即可。

3.2 订单状态机与支付超时处理

订单状态从创建到最终结束,一共经历"待支付→已支付"或者"待支付→已取消"两条路径。用户主动取消自然不必说,更麻烦的是用户下了单但一直不支付的情况——座位一直被锁着,别人就看不到了。这种问题要靠超时关单来处理。

我的做法很简单,没有引入消息队列之类的重武器,直接用了Spring自带的定时任务:

@Scheduled(fixedDelay = 60000) public void timeoutCancelOrders() { List<Orders> timeoutOrders = ordersMapper.selectList( new LambdaQueryWrapper<Orders>() .eq(Orders::getStatus, 0) .lt(Orders::getCreateTime, LocalDateTime.now().minusMinutes(15)) ); for (Orders order : timeoutOrders) { cancelOrder(order.getOrderId()); } }

每分钟扫一次,找出所有超过15分钟还没支付的待支付订单,逐单调用取消逻辑。取消逻辑里先更新订单状态为已取消,再把对应座位状态改回0。这两步也在同一事务里,保证不会出现订单取消了座位还锁着的情况。做完这些,在manage层的定时任务workdir上再加@EnableScheduling,项目启动时自动开启。

15分钟这个值可以做成配置文件参数,方便根据业务调整。毕设演示时如果觉得等待太久,可以改成2分钟,临时效果好很多。

3.3 高并发下防止超卖的一点经验

前面提到的条件更新,本质上就是数据库行锁机制。MySQL的UPDATE语句自带行级锁,更新时会锁住被更新的那一行,直到事务提交。这也是我不推荐用Java内存锁的原因——项目如果部署成多实例,A机器上的锁和B机器上的锁根本不是同一把,照样超卖。而数据库行锁天然跨实例生效。

另外,这里有一个容易踩的坑:在MyBatis Plus里执行条件更新时,如果用的UpdateWrapper只拼了set条件,忘了在eq条件里带上status=0,那更新的影响行数就永远是1,所谓的并发保护就失效了。我第一次写的时候也掉进过这个坑,测试时开了两个浏览器窗口同时抢同一个座位,一个成功另一个也显示成功,排查了半天才发现是条件没带全。所以这块代码写完后一定要做并发验证。

事务隔离级别也有讲究。默认的REPEATABLE_READ在MySQL InnoDB引擎下做这种行锁更新是安全的,不需要刻意改成READ_COMMITTED,除非你做了多表关联的复杂操作。毕设场景保持默认即可。

4. 从零搭建开发环境:JDK、Maven、MySQL的版本匹配问题

4.1 本地开发环境配置清单

很多同学拿到源码第一步就卡壳,不是代码有问题,而是环境配置各种不匹配。我把整套本地环境的配置顺序捋一遍,照着做基本不会错。

先装JDK8。注意配置JAVA_HOME环境变量,并在PATH里加%JAVA_HOME%\bin。验证方法就是命令行输入java -version,显示1.8.x说明成功。接着装Maven,同样配好MAVEN_HOME,在settings.xml里配阿里云镜像,否则从中央仓库下载依赖会慢到怀疑人生。IDEA安装后,在Settings里把Maven的User settings file指向我们改过的settings.xml,然后设置项目的JDK版本为8,这一步最好在新建工程前就确认好,避免导入后代码报错。

MySQL我用的是5.7版本,安装时选择utf8mb4字符集。5.7和8.0在JDBC驱动上有一点区别:MySQL 5.7用com.mysql.jdbc.Driver,MySQL 8.0需要用com.mysql.cj.jdbc.Driver,同时在URL里必须带上serverTimezone=Asia/Shanghai参数,否则会出现时区报错。如果你用的是8.0,最简单的方式是到项目pom里确认mysql-connector-java的版本号,8.0.x对应的驱动类就是cj结尾的。

4.2 项目导入与配置文件修改点

项目导入IDEA后,第一件事是看application.yml里的配置是否和你本地一致:

server: port: 8080 spring: datasource: url: jdbc:mysql://localhost:3306/cinema?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai username: root password: 123456 driver-class-name: com.mysql.cj.jdbc.Driver mybatis-plus: configuration: map-underscore-to-camel-case: true global-config: db-config: id-type: auto

端口、用户名、密码这三个是最常改的。数据库名字默认是cinema,如果你建的库名不一样也要同步改。数据库连接这块还有一个小技巧:如果发现连接成功后中文乱码,多半是URL里少了characterEncoding=utf8参数,补上重启即可。

4.3 第一次启动的完整流程

启动的顺序,我建议按照"建库→执行SQL脚本→启动Springboot→浏览器访问"来走。

建库很简单,命令行登进MySQL后执行create database cinema default character set utf8mb4;,或者用Navicat/DataGrip这类图形化工具新建。然后找到项目里的init.sql,把脚本里的建表语句和初始化数据一次性执行完。这里提醒一句:执行前确认当前连接的是cinema库,有些同学直接全局执行脚本,结果表建到了系统库,后面启动时就报找不到表。

一切就绪后,找到主类CinemaApplication,右键Run。看到类似"Tomcat started on port(s): 8080"的日志就说明启动成功。打开浏览器访问http://localhost:8080,正常能看到登录界面。如果页面打不开,先排查端口冲突和数据库连接,这两类问题占了启动失败的八成以上。

5. 调试部署实战:从能跑到能交付的完整过程

5.1 常见启动报错的排查链路

我把这个项目调试过程中遇到的高频报错整理成一张表,方便你对照排查:

报错信息原因解决办法
Failed to configure a DataSource数据库连接配置错误,或MySQL没启动检查application.yml的url、用户名、密码
Access denied for user 'root'@'localhost'MySQL密码不对修改配置为本地实际密码
Port 8080 was already in use端口被其他进程占用换端口,或netstat找到占用进程杀掉
Unknown database 'cinema'没建库或库名不一致执行建库语句,确认库名
java.sql.SQLException: The server time zone value时区未配置URL末尾加serverTimezone=Asia/Shanghai
Invalid bound statement (not found)Mapper接口和XML映射路径不匹配检查@MapperScan注解和XML的namespace
ClassNotFoundException: com.mysql.cj.jdbc.Driver驱动版本和数据库不匹配调整pom中mysql驱动版本

这里重点说一下Invalid bound statement这个问题。如果你在代码里用的是MyBatis Plus的BaseMapper,那基本不会碰到XML路径问题;但如果自己写了自定义的XML映射方法,就要确保application.yml或properties里配置了mapper-locations,指向xml文件所在的目录。IDEA里xml文件放在src/main/resources/mapper目录下是最省心的,打包时resources目录会自动带上。放在java源码目录下反而容易被打包过程过滤掉,等部署到服务器就疯狂报找不到mapping,这个坑我替你在本地踩过了。

5.2 前端界面与后端接口的联调问题

这个项目的前端页面,我用的是Thymeleaf加原生Bootstrap,没搞前后端分离。好处是部署简单,一个jar包全搞定,不需要额外部署Nginx和前端静态资源服务。联调时主要关注的接口有三个:登录鉴权接口、场次座位查询接口、订单创建接口。

登录鉴权我封装了一个简单的拦截器,继承HandlerInterceptor,在preHandle里从Session取用户信息,取不到就跳转到登录页并提示"请先登录"。校验时放行登录接口、注册接口和静态资源路径,其余接口全部拦截。这里有一个必须注意的匹配规则:静态资源的放行路径是/css/、/js/、/img/**,如果你漏了这些,页面样式会全部丢失,界面奇丑无比,看起来像系统出Bug一样。我第一次遗漏的时候排查了很久,最后发现是拦截器把静态资源也拦截了,加上放行路径立刻恢复正常。

座位查询接口返回的数据格式,我这里设计成二维JSON数组,比如:

[ [0, 0, 1, 0, 0], [0, 1, 1, 0, 0], [2, 0, 0, 1, 0] ]

其中0代表可选,1代表已锁定,2代表已售出。前端拿到这个二维数后,逐格渲染成红色、灰色、绿色三种状态的座位块,点击可选座位时再调用锁定接口。这种纯二维数组的设计,前端渲染逻辑非常简洁,不需要做数据转换,实用性很强。

5.3 打包部署到服务器时的注意事项

本地跑通只是第一步,交付的时候往往要求打包部署到服务器上。我用的是最常规的Springboot部署方式,打成可执行jar包,服务器用java -jar命令运行。

先清理再打包:

mvn clean package -DskipTests

target目录下会生成一个c0abc的jar文件。把这个jar上传到服务器的指定目录,然后命令行运行:

java -jar cinema-0.0.1-SNAPSHOT.jar

这里有几个注意事项。一是服务器上JDK版本要和本地一致,你本地用JDK8打的包,服务器如果只装了JDK17,大概率会出现各种奇怪的运行时异常,尤其是涉及反射的框架(比如Spring、MyBatis),这种问题排查起来非常费劲。二是数据库脚本要在服务器MySQL上重新执行一遍,且要注意配置中的账号权限,项目使用的账号要对cinema库有增删改查的权限,只给了sel权限的话,登录之后各种操作都会报权限不足。三是部署后默认端口如果你设成8080,需要在服务器安全组里把8080端口放行,否则外部访问不到。

如果你服务器上没有图形界面,想快速管理数据库,可以装一个Docker版的MySQL管理工具,或者直接用命令行执行导出的SQL脚本。SQL脚本导入的命令是:

mysql -u root -p cinema < init.sql

输入密码之后脚本就会自动执行,表结构和数据全部导入,比图形化工具还快。部署完成之后,启动项目,浏览器输入服务器IP加端口,看到登录页就说明成功。

5.4 个人实际操作中的两个提醒

最后分享两个我在实际跑这个项目时觉得很有用的细节。

第一个是日志监控。项目一旦部署到服务器,日志就成了排查问题的唯一入口。Springboot自带的logback默认会输出控制台,但服务器上你不可能一直开着终端盯控制台。我在application.yml里加了一段配置,把日志输出到data/logs/cinema.log文件,并按天滚动。这样即使项目夜间出问题,第二天也能从历史日志里找到线索。配置很简单,网上搜个logback-spring.xml模板改一下路径即可。

第二个是演示环境的数据库准备。如果你需要答辩演示,最好准备一个"已发生部分业务数据"的数据库状态,比如有几张订单处于已支付状态、有几张处于待支付状态、部分座位已经售出。这样现场点开系统,界面是充盈的,不会一片空白。初始化用户和订单数据时把时间设置合理一些,或者直接改数据库字段,保证演示时订单状态看得清楚。

这套项目的完整度,做一个毕设绰绰有余。如果你后续还想加功能,可以考虑在订单支付环节接入支付宝沙箱,或者把座位锁定状态换成Redis缓存来提升并发能力。不过那是进阶玩法了,先把核心业务链路跑通,把选座并发、状态机这些基础打好,比什么都强。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询