基于SpringBoot+Vue的影院购票管理系统设计与部署实践

发布时间:2026/10/13 15:52:19
基于SpringBoot+Vue的影院购票管理系统设计与部署实践
最近我把一套基于SpringBootVue的影院购票管理系统完整整理了一遍从源码到部署教程都补齐了。这个项目采用前后端分离架构后端用SpringBoot整合MyBatis操作MySQL前端用Vue全家桶功能上覆盖了电影列表、场次排片、在线选座、订单支付、后台管理这些核心模块源码可以直接跑起来部署教程也踩过坑了所以拿出来分享一波。这种项目在业务上很完整技术上又踩中了前后端分离、JWT鉴权、事务控制、跨域联调、Nginx部署这些硬知识点非常适合拿来练手或者作为课程设计、毕业设计的底子。如果你是刚学完SpringBoot和Vue想找一个“能写进简历”的项目这个是很不错的参考如果你已经有基础想快速套一套完整系统来改业务逻辑那直接拿源码改造也行。下面我把整个项目的设计思路、核心代码实现、数据库表结构、部署步骤和常见坑一次性讲清楚。1. 项目整体设计与技术选型1.1 为什么选前后端分离架构最早我也想过用传统的Thymeleaf模板引擎做一套SpringBoot把页面和接口全包了。但做到后面发现影院购票这个业务前后端交互特别密集选座、轮播、实时余票、订单倒计时这些场景都需要页面频繁刷新局部数据。如果还用服务端渲染每次切换都要刷新整页体验很一般而且前端改样式、后端改接口会互相干扰。改成前后端分离之后后端只负责输出JSON前端用Vue动态渲染交互流畅度完全不一样。更关键的是这套架构在后期部署时可以分开扩展接口压力大了就多挂几个后端实例静态资源放在Nginx上用缓存加速。面试聊起来也更有东西可讲光“为什么采用前后端分离”这个问题就能展开讲好几句。1.2 技术栈选型背后的考量后端选SpringBoot不用多说生态成熟、起步快、整合第三方库方便。持久层选了MyBatis而不是Spring Data JPA原因是这个系统的查询逻辑比较复杂比如“根据影院、日期、电影三个条件联合查排片”、“统计某个场次的已售座位”这种动态SQL在XML里维护起来比JPA的自动推导直观很多DBA出身的人也能直接接手。前端选了Vue 2 Vue Router VuexElement UI做后台管理页面。选Vue 2不是因为它比Vue 3好而是这套源码想兼容更多人的电脑环境。如果你自己用同样代码迁移到Vue 3改动成本也不高后面我会提到关键的改动点。MySQL存业务数据Redis用于缓存热点电影列表和存放验证码这个组合在现阶段足够稳。1.3 项目核心功能清单影院购票系统拆开来看用户端和管理端加起来大概十几个页面但核心业务环环相扣我列一下功能模块模块功能点用户模块注册、登录、个人信息维护、密码加密电影模块电影列表、详情、搜索、正在热映/即将上映切换场次排片不同影院、影厅、时间的场次查询与展示选座购票座位状态可视化、选座、创建订单、倒计时支付订单模块订单列表、订单详情、取消订单、支付回调处理管理后台电影管理、场次管理、影厅管理、订单管理、电影统计分析用户端用Vue写单页应用管理端拆了一套单独的路由和页面共用同一套后端接口。这样用户端和管理端后续可以分开迭代也是很多真实项目的做法。2. 数据库设计与核心表结构2.1 电影信息表设计电影表是整个系统的基础字段不多但类型要选对。我之前见过有人把电影时长存成字符串“120分钟”后来做排序和筛选的时候非常痛苦。正确做法是存分钟数用int封面图存URL字符串上映日期用date类型状态用tinyint做逻辑标记。CREATE TABLE film ( id bigint(20) NOT NULL AUTO_INCREMENT, film_name varchar(100) NOT NULL COMMENT 电影名称, film_type varchar(50) DEFAULT NULL COMMENT 类型 如动作/爱情, duration int(11) NOT NULL COMMENT 时长(分钟), cover_url varchar(255) DEFAULT NULL COMMENT 封面图地址, description text COMMENT 简介, release_date date DEFAULT NULL COMMENT 上映日期, status tinyint(4) NOT NULL DEFAULT 1 COMMENT 状态 1上映 0下架, create_time datetime DEFAULT NULL, PRIMARY KEY (id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;2.2 场次与座位设计场次表要关联电影、影厅、开始时间、票价和余票信息。影厅需要单独建表每个影厅有行数和列数这样前端才能动态渲染座位图。座位状态的判断我建议不要存“剩余座位数”而是根据订单明细动态计算否则并发下很容易数据不一致。场次表大概长这样CREATE TABLE session ( id bigint(20) NOT NULL AUTO_INCREMENT, film_id bigint(20) NOT NULL, hall_id bigint(20) NOT NULL, start_time datetime NOT NULL, end_time datetime DEFAULT NULL, price decimal(10,2) NOT NULL, status tinyint(4) DEFAULT 1 COMMENT 1有效 0取消, PRIMARY KEY (id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;座位的状态我是通过订单表反查的。每个订单会生成一条订单明细里面记录唯一的座位编号比如“A排05座”。查询座位状态时只要查这个场次下有效订单中已锁定的座位集合就行。为了性能可以加一个redis缓存把已选座位的集合缓存在key里60秒过期。2.3 订单与支付表设计订单表字段比较多订单号、用户ID、场次ID、总金额、状态、创建时间、支付时间。这里特别要注意订单号不要用自增ID因为太容易被人遍历猜测我用的是“时间戳随机数用户ID”拼接生成。订单状态我设计了五个待支付、已锁定、已支付、已取消、已退款。用户选中座位后先创建待支付订单同时锁定座位给15分钟支付时间。如果超时未支付定时任务自动把订单置为已取消座位释放。这个流程在影院、演出票务里很常见实现起来不算复杂但事务边界一定要处理好。2.4 数据一致性如何处理选座购票最怕两个问题多人抢同一个座位、支付成功但订单状态没更新。第一个问题靠“先锁座位再创建订单”解决。我在程序里使用MySQL的select ... for update对场次记录加锁然后检查座位是否已经被有效订单占用没占用才插入订单。虽然锁粒度有点大但场次维度的并发量完全够用。第二个问题靠事务解决。创建订单、锁定座位、扣减余票这三个操作放在同一个事务里。支付回调成功时更新订单状态和支付流水同样在一个事务里。项目里还加了幂等处理支付回调如果收到重复请求先根据订单号查状态已经支付就直接返回成功不影响业务数据。3. 后端SpringBoot核心实现3.1 工程结构与分层规范后端工程我按标准的三层结构拆Controller、Service、Mapper。Controller只负责参数接收和结果封装不写业务代码Service层写核心逻辑事务注解标在实现类上Mapper层只写数据访问接口和XML映射。com.yangzhou.cinema ├── controller │ ├── FilmController.java │ ├── SessionController.java │ ├── OrderController.java │ └── UserController.java ├── service │ ├── FilmService.java │ ├── OrderService.java │ └── impl ├── mapper │ ├── FilmMapper.java │ ├── SessionMapper.java │ └── OrderMapper.java ├── entity ├── dto ├── vo ├── config ├── common └── util分层看起来是老生常谈但真正落地的时候能省很多事。比如后来我要给所有接口统一返回格式只要在common里定义Result对象然后把Controller的返回类型统一改掉就行不需要动业务逻辑。DTO和Entity分开也很关键User实体里存了密码hash但如果直接用User返回给前端密码就泄露了。所以查询用户信息时我会转成UserVO再返回。3.2 认证授权方案JWT集成细节登录这块我没用传统的Session而是用JWT。用户登录成功后后端生成一个token字符串返回给前端前端存到localStorage中每次请求带上Authorization: Bearer token。JWT的好处是服务端无状态多实例部署时不需要同步Session坏处是token一旦签发到期前无法主动失效。所以我在JWT里设置了过期时间一般登录态给24小时然后配合拦截器校验。管理员的token和普通用户的token通过自定义claim区分权限拦截时直接解析出来判断即可。String token Jwts.builder() .setSubject(String.valueOf(user.getId())) .claim(role, user.getRole()) .setExpiration(new Date(System.currentTimeMillis() 24 * 3600 * 1000)) .signWith(SignatureAlgorithm.HS256, secretKey) .compact();拦截器配置要注意排除登录、注册和获取电影列表等公开接口否则前端还没登录首页数据都拉不到。这里我踩过一个坑获取token时写了从header取Authorization但前端调试时把token放到了请求体里结果一直401后来统一规范才解决。3.3 购票核心流程实现要点用户选完座位点击“立即购买”前端会把场次ID和座位编号集合发给后端。后端要做的第一步是参数校验座位编号不能重复不能超过影厅行数和列数。第二步就是加锁查询。第三步创建订单和订单明细。伪代码如下Transactional public OrderVO createOrder(Long userId, Long sessionId, ListString seats) { Session session sessionMapper.selectByPrimaryKey(sessionId); if (session null || session.getStatus() 0) { throw new BusinessException(场次不存在); } ListString occupied orderItemMapper.selectOccupiedSeats(sessionId); for (String seat : seats) { if (occupied.contains(seat)) { throw new BusinessException(座位已被锁定); } } // 生成订单 Order order new Order(); order.setOrderNo(generateOrderNo(userId)); order.setUserId(userId); order.setSessionId(sessionId); order.setTotalAmount(seats.size() * session.getPrice()); order.setStatus(OrderStatus.PENDING_PAYMENT.getValue()); orderMapper.insert(order); // 批量插入座位明细 orderItemMapper.batchInsert(order.getId(), sessionId, seats); return buildOrderVO(order); }座位锁定的范围是整场的有效订单如果一场有200个座位查询一次select ... for update要扫一遍订单明细表性能谈不上极致但业务逻辑简单可靠。等以后并发量真的上去了再改成基于Redis分布式锁的精细控制即可。3.4 MyBatis动态SQL的使用场景MyBatis的XML动态SQL在这个项目里用得很频繁。比如后台电影管理支持多条件筛选电影名、类型、状态都可以组合查询。如果写死SQL每个条件都要单独写一条代码冗余量大。用where和if标签可以自动拼SQLselect idselectFilmList resultTypecom.yangzhou.cinema.entity.Film select * from film where if testfilmName ! null and filmName ! and film_name like concat(%, #{filmName}, %) /if if testfilmType ! null and filmType ! and film_type #{filmType} /if if teststatus ! null and status #{status} /if /where order by id desc /select这样一条SQL就解决了三种条件组合。注意where标签会自动去掉首个多余的and写条件时不要在第一个条件前加and这个细节新手容易搞错。4. 前端Vue实现与交互细节4.1 Vue项目搭建与路由规划前端项目用Vue CLI脚手架创建。用户端路由和管理端路由分开我用了懒加载这样首页加载时不会一次性把所有页面脚本都拉下来。const routes [ { path: /, component: () import(/views/Home.vue) }, { path: /film/:id, component: () import(/views/FilmDetail.vue) }, { path: /session/:sessionId, component: () import(/views/SeatSelect.vue) }, { path: /order/list, component: () import(/views/OrderList.vue) }, { path: /login, component: () import(/views/Login.vue) }, { path: /admin, component: () import(/layout/AdminLayout.vue), children: [ { path: film, component: () import(/views/admin/FilmManage.vue) }, { path: session, component: () import(/views/admin/SessionManage.vue) }, { path: order, component: () import(/views/admin/OrderManage.vue) } ] } ];路由守卫里加了全局前置守卫判断是否需要登录、是否需要管理员权限。这个功能很简单但很多人会直接在组件里写死mounted里判断token刷新页面就会失效。全局前置守卫是更稳妥的写法页面加载时就能拦截跳转。4.2 Axios封装与接口联调前端所有请求统一走axios。我在utils/request.js里封装了请求实例配置了基础URL、超时时间以及请求拦截器和响应拦截器请求拦截器把localStorage中的token取出来加到请求头。响应拦截器统一处理后端返回的Result结构如果code不为200直接弹出错误提示如果code是401清除本地token并跳转登录页。service.interceptors.request.use(config { const token localStorage.getItem(token); if (token) { config.headers[Authorization] Bearer token; } return config; }); service.interceptors.response.use( response { const res response.data; if (res.code ! 200) { Message.error(res.message); return Promise.reject(new Error(res.message)); } return res.data; }, error { if (error.response error.response.status 401) { localStorage.removeItem(token); router.push(/login); } return Promise.reject(error); } );开始联调时经常会遇到跨域问题开发环境我在Vue.config.js里配了devServer的proxy代理把/api前缀的请求转发到后端8080端口。部署时前端走Nginx再把Nginx里的/api代理到后端这样前端请求地址不写死方便切换环境。4.3 选座组件的实现思路在线选座是这个项目前端最核心的交互。我通过后端返回的二维数组来渲染座位状态数组每个元素是一个对象包含行号、列号、状态。座位状态有三种可选、已选、已售。用户点击空闲座位时先判断当前已选数量是否达到上限通常一场最多选4张。选中后再点击一次可以取消。提交前前端会做二次确认。为了提升体验我在选座页里加了一个“已选座位”的汇总栏实时显示座位编号和总价。整个选座组件不依赖第三方库就是一个双层的v-for循环每个座位是一个div通过状态不同绑定了不同的class点击事件里维护selectedSeats数组。考虑到购票流程有支付倒计时我在订单创建成功后启动一个15分钟的倒计时时间到就提示用户订单超时同时刷新座位状态。4.4 权限控制在前端的落地前端权限不只靠路由守卫页面上的按钮也要做控制。比如后台的电影管理页面只有管理员能新增、编辑、下架电影。我在按钮上通过v-if判断当前用户角色角色信息在登录时已经存到了Vuex里。还有一个细节是路由懒加载切页面时偶尔会出现白屏原因是异步组件加载失败。我在路由配置里加了webpackChunkName注释把每个页面的chunk命名清楚排查起来方便很多。另外在Nginx部署时如果前端路由用的是history模式必须配置try_files指向index.html否则用户刷新子页面会报404。这个问题非常经典几乎每个部署history模式项目的人都会遇到。5. 部署教程与线上踩坑记录5.1 环境准备JDK、Node、MySQL部署这套系统需要准备以下环境软件版本建议JDK1.8及以上推荐8或11MySQL5.7或8.0Node.js14或以上用于前端构建Nginx1.18以上Maven3.6及以上服务器如果是空的先把JDK装好配置环境变量。MySQL建库时要注意字符集选utf8mb4不然存emoji或者生僻字会出现乱码。把项目里的cinema.sql导入数据库再修改application.yml里的数据库账号密码。5.2 后端打包与jar部署后端工程是标准的Maven项目在项目根目录执行mvn clean package打包完成后target目录下会生成一个cinema-0.0.1.jar。上传到服务器后用nohup命令启动nohup java -jar cinema-0.0.1.jar --spring.profiles.activeprod cinemalog.out 21 如果要停服务先找到进程ID再killps -ef | grep cinema kill -9 进程ID这里要提醒一下如果用kill -9强杀进程可能丢失未写入磁盘的日志正常停服建议先用kill发送终止信号等Spring完整关闭后再确认。实际我维护时为了方便写了一个start.sh和stop.sh脚本启停一键搞定。5.3 前端构建与Nginx配置前端在本地先执行依赖安装和构建npm install npm run build构建完成后dist目录就是静态资源文件。把它上传到服务器的/var/www/cinema目录然后配置Nginxserver { listen 80; server_name your_domain_or_ip; root /var/www/cinema; index index.html; location /api/ { proxy_pass http://127.0.0.1:8080; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; } location / { try_files $uri $uri/ /index.html; } }这个配置里最关键的是location /api/的proxy_pass。如果proxy_pass后面带了路径转发时会把匹配到的前缀去掉再拼接如果没带路径就是完整的URL转发。我刚开始写的时候想当然地以为带不带都一样结果接口一直404。后来确认必须写成http://127.0.0.1:8080不带斜杠才能把/api/xxx完整转发给后端。5.4 部署常见问题排查部署过程中最容易遇到三类问题一是后端端口被占用。启动jar包时报Port 8080 was already in use用lsof -i:8080查看占用进程杀掉后重启。二是数据库连接失败。检查MySQL账号权限确认远程连接权限是否开启如果数据库和项目在同一台服务器就用localhost别写成公网IP。三是前端页面能打开但接口报错。先在浏览器F12看Network里的请求地址如果请求的IP端口和Nginx配置的server_name不一致大概率是代理地址写错。再用curl http://127.0.0.1:8080/api/film/list直接访问后端接口确认后端本身通不通。这样一步步缩小问题范围比瞎猜快得多。6. 常见问题速查与优化建议6.1 高频问题排查清单我整理了项目使用过程中大家问得最多的问题做成一张速查表问题现象可能原因解决办法登录接口返回401token过期或未传token重新登录检查拦截器排除路径跨域请求被拦截Nginx代理配置错误检查proxy_pass是否正确选座提交提示座位已被锁定座位被其他订单占用刷新页面重新获取座位状态订单一直处于待支付支付回调地址配置错误检查回调接口是否能被外部访问前端刷新页面404history路由未配置try_filesNginx配置try_files指向index.html数据库中文乱码字符集用成了utf8数据库和表统一使用utf8mb46.2 性能优化建议项目跑起来之后如果想进一步优化可以从这几个方向入手电影首页列表加Redis缓存缓存key设计成film:recommend缓存时间10分钟避免每次刷新都查数据库。场次查询可以加索引重点在film_id和start_time字段上建组合索引。订单状态更新时用乐观锁机制控制并发在订单表加version字段更新时校验版本号能避免一些极端情况下的脏写。6.3 后续功能扩展方向这套系统后续可以扩展的方向不少。比如接入真实的微信支付或支付宝支付用一个支付服务商的标准SDK替换掉现在的模拟支付。比如增加会员积分体系观影后自动累计积分。再比如做推荐模块根据用户历史订单推荐同类型电影。如果想把前端升级到Vue 3主要改动点在Vuex改成Pinia、路由配置方式微调、Element UI换成Element Plus这几个地方其余业务代码基本可以复用。整体改造工作量不大适合想练一练迁移能力的同学尝试。最后说点实在的做这套系统我自己最大的体会是真正的难点不在某个技术本身而在流程串联。从用户选座到生成订单从支付回调到座位释放任何一环掉了数据就乱了。如果只是把代码抄一遍可能跑通就结束了但如果能把自己带入“一个真实影院运营者”的角色去思考你就会发现座位锁定、超时释放、订单幂等这些设计都是刚需不是炫技。建议动手实践的朋友拿到源码后先别急着跑打开数据库看一下表结构再跟着购票流程把一条完整链路走一遍最后再动手改一个小功能比如增加一个会员折扣。这样折腾下来你对SpringBoot Vue这套组合的理解会比我直接讲一百句话都深。