SpringBoot+Vue影院选座系统:并发锁座与订单状态流转实战

发布时间:2026/9/26 5:44:46

SpringBoot+Vue影院选座系统:并发锁座与订单状态流转实战 简介这是一套面向Java Web全栈学习者的电影售票及影院管理系统完整源码基于SpringBoot与Vue.js前后端分离架构实现适合课程设计、毕业设计或技术进阶练习。系统覆盖用户注册登录、电影信息维护、影院与放映厅管理、排片场次设定、可视化选座购票、订单支付与电子票生成以及管理员权限下的订单与用户管理并涉及Spring Security权限控制、Redis缓存优化与前端懒加载等实践点。压缩包共314个文件约16.14MB以81个Java后端源码、41个Vue组件、15个XML配置、15个JavaScript脚本及102张图片资源为主另含SQL建表脚本与项目说明文档目录结构清晰便于按模块阅读与二次开发。目前已有71人学习下载读者可借此完整梳理前后端接口设计、组件化开发与数据库建模思路快速搭建可运行的售票管理平台。1. 电影售票系统为什么总在选座那一步翻车影院售票系统的核心链路其实很短排片、选座、下单、支付、出票。但真正做过的人都知道最容易出问题的不是支付回调而是选座。一个座位被两个人同时点中后台没锁住订单就重复了锁住了但没设过期时间用户关掉页面座位就永远灰着。这类问题在单体架构里靠数据库行锁能扛一阵但一旦前后端分离、并发上来就必须在 SpringBoot 层做显式控制。这个标题指向的是一套典型的 SpringBoot Vue 前后端分离影院管理系统覆盖影片管理、影厅排片、在线选座、订单支付、后台统计这几块。它适合两类人一是想拿一个完整业务闭环练手前后端分离的开发者二是需要一套可跑通的影院业务骨架做二次开发的人。下面我按实际落地顺序把选型理由、建表、接口、锁座、前端渲染和踩坑一条条拆开讲。2. 技术选型与数据库设计为什么是 SpringBoot Vue 而不是别的2.1 后端选 SpringBoot 的四个现实理由影院系统的业务复杂度中等但模块边界清晰用户、影片、影厅、排片、订单、支付。这种场景下 SpringBoot 的优势很直接。第一起步依赖把 Web、MyBatis、Redis、事务管理一次性拉齐不用在 XML 里堆配置。第二内嵌 Tomcat 让部署变成一条java -jar对个人开发者和中小团队足够。第三生态成熟分页插件、连接池、缓存这些常见需求都有稳定方案。第四前后端分离后后端只负责 JSON 接口SpringBoot 的RestController写起来很顺。我一般会把项目拆成controller、service、mapper、entity、dto、config、common这几层。common里放统一返回体和全局异常处理这是后面所有接口的基础。// common/Result.java public class ResultT { private Integer code; // 200 成功500 失败401 未登录 private String msg; private T data; public static T ResultT ok(T data) { ResultT r new Result(); r.code 200; r.msg success; r.data data; return r; } public static T ResultT fail(String msg) { ResultT r new Result(); r.code 500; r.msg msg; return r; } // getter/setter 省略 }这段代码的关键在于统一返回结构。前端拿到code就能判断成败不用每个接口写一套解析逻辑。code的取值建议固定200 成功、401 未登录、403 无权限、500 业务失败。参数说明上data用泛型是为了让不同接口复用同一个包装类避免为每个返回值单独建类。2.2 前端选 Vue 的取舍Vue 在这个项目里的价值集中在选座页。选座是一个典型的状态密集型界面几十上百个座位每个座位有可选、已售、选中三种状态还要实时反映库存。用 Vue 的响应式数据配合v-for渲染座位矩阵比手写 DOM 操作省太多事。路由用vue-router管理从影片列表到排片详情再到选座页参数通过路由传递。// router/index.js 片段 const routes [ { path: /movies, component: () import(/views/MovieList.vue) }, { path: /schedule/:movieId, component: () import(/views/ScheduleList.vue) }, { path: /seat/:scheduleId, component: () import(/views/SeatSelect.vue) } ]路由参数用:scheduleId这种动态段选座页通过this.$route.params.scheduleId拿到排片 ID再请求座位数据。这里要注意动态路由参数在组件复用时不会自动触发重新请求需要在watch里监听$route变化否则从 A 场次切到 B 场次会显示旧座位。2.3 数据库表设计六张核心表影院系统的表不用多但字段要想清楚。下面是我常用的六张核心表结构。表名作用关键字段user用户id, username, password, phone, rolemovie影片id, name, duration, poster, statushall影厅id, name, row_count, col_countschedule排片id, movie_id, hall_id, start_time, priceseat座位库存id, schedule_id, row_num, col_num, statusorder订单id, user_id, schedule_id, seats, amount, status座位库存表是重点。常见做法是排片创建时批量生成该场次的所有座位记录status用 0 表示可售、1 表示锁定、2 表示已售。这样选座时查的是seat表而不是实时计算性能可控。CREATE TABLE seat ( id BIGINT PRIMARY KEY AUTO_INCREMENT, schedule_id BIGINT NOT NULL, row_num INT NOT NULL, col_num INT NOT NULL, status TINYINT DEFAULT 0 COMMENT 0可售 1锁定 2已售, lock_time DATETIME DEFAULT NULL, UNIQUE KEY uk_schedule_seat (schedule_id, row_num, col_num) );uk_schedule_seat这个唯一索引是防重复的底线。即使应用层锁失效数据库也会拦住同一场次同一座位的重复插入。lock_time用来做锁过期判断后面会讲。3. 选座与订单接口把并发锁座这件事做对3.1 选座接口的锁座逻辑选座的核心矛盾是用户点中座位到支付完成之间座位必须被占住但不能永久占住。我的做法是「先锁后付」锁定时长 15 分钟超时自动释放。// service/SeatService.java public ResultString lockSeats(Long scheduleId, ListString seats, Long userId) { // 1. 先清理该场次过期的锁 seatMapper.releaseExpired(scheduleId, LocalDateTime.now().minusMinutes(15)); // 2. 逐个尝试锁定 for (String seat : seats) { String[] rc seat.split(-); int row Integer.parseInt(rc[0]); int col Integer.parseInt(rc[1]); int updated seatMapper.lockOne(scheduleId, row, col, userId); if (updated 0) { // 有一个座位被占回滚已锁的 seatMapper.unlockByUser(scheduleId, userId); return Result.fail(座位 seat 已被占用); } } return Result.ok(锁定成功请在15分钟内支付); }对应的 Mapper 用条件更新保证原子性update idlockOne UPDATE seat SET status 1, lock_time NOW(), user_id #{userId} WHERE schedule_id #{scheduleId} AND row_num #{row} AND col_num #{col} AND status 0 /update逻辑说明lockOne的WHERE status 0是关键只有当前可售的座位才能被更新返回影响行数为 0 就说明被别人抢先了。参数上scheduleId定位场次row、col定位座位userId记录是谁锁的方便回滚。这种「条件更新 影响行数判断」比先查再改可靠避免了查询和更新之间的时间窗口。3.2 订单创建与支付回调锁座成功后创建订单状态为「待支付」。支付回调里把订单改为「已支付」同时把座位状态从 1 改成 2。Transactional public ResultString paySuccess(String orderNo) { Order order orderMapper.findByNo(orderNo); if (order null || order.getStatus() ! 0) { return Result.fail(订单状态异常); } orderMapper.updateStatus(orderNo, 1); seatMapper.confirmSold(order.getScheduleId(), order.getSeats()); return Result.ok(支付成功); }Transactional保证订单状态和座位状态一起变不会出现订单已付但座位没确认的情况。confirmSold把对应座位的status置为 2并清空lock_time。3.3 前端选座页的渲染与交互选座页拿到座位列表后按行列渲染成矩阵。每个座位根据status绑定不同样式点击时切换选中态。// SeatSelect.vue 片段 data() { return { seats: [], // 后端返回的座位列表 selected: [] // 用户选中的座位 } }, methods: { toggleSeat(seat) { if (seat.status ! 0) return; // 已售或锁定不可选 const key seat.rowNum - seat.colNum; const idx this.selected.indexOf(key); if (idx -1) { this.selected.splice(idx, 1); } else { if (this.selected.length 4) { alert(一次最多选4个座位); return; } this.selected.push(key); } } }逻辑上status ! 0直接拦截不可选座位避免无效请求。selected存的是行-列字符串提交时直接传给后端。限制最多 4 个座位是业务约束防止恶意占座。参数上seats来自接口selected是本地状态提交订单时把selected和scheduleId一起发出去。4. 排片管理与后台统计让运营能自己改数据4.1 排片冲突检测排片最容易出的问题是同一影厅同一时间段排了两场电影。解决思路是在插入前查重叠。public boolean hasConflict(Long hallId, LocalDateTime start, LocalDateTime end) { int count scheduleMapper.countConflict(hallId, start, end); return count 0; }select idcountConflict resultTypeint SELECT COUNT(*) FROM schedule WHERE hall_id #{hallId} AND start_time lt; #{end} AND DATE_ADD(start_time, INTERVAL duration MINUTE) gt; #{start} /select这里用影片时长算出结束时间做区间重叠判断。参数start、end是新排片的起止时间hallId锁定影厅。注意duration存在movie表里实际查询要 join这里简化了写法。冲突检测放在 service 层插入前调用返回 true 就拒绝。4.2 后台统计接口运营需要看每日票房、上座率、热门影片。这些用聚合查询直接出。-- 每日票房 SELECT DATE(create_time) AS day, SUM(amount) AS total FROM order WHERE status 1 GROUP BY DATE(create_time) ORDER BY day DESC;上座率则是已售座位数除以总座位数按场次分组。这类统计接口建议加缓存因为数据变化不频繁每次实时聚合在数据量大时会拖慢后台。常见做法是用 Redis 缓存 5 分钟或者定时任务预计算。4.3 权限控制后台接口必须区分普通用户和管理员。用拦截器校验 token 里的角色。public boolean preHandle(HttpServletRequest req, HttpServletResponse resp, Object handler) { String token req.getHeader(Authorization); if (token null || !jwtUtil.validate(token)) { resp.setStatus(401); return false; } String role jwtUtil.getRole(token); if (req.getRequestURI().startsWith(/admin) !ADMIN.equals(role)) { resp.setStatus(403); return false; } return true; }逻辑是先验 token 有效性再判断路径是否属于后台最后比对角色。参数上Authorization头携带 JWTrole从 token 载荷里取。这样普通用户即使拿到后台 URL 也进不去。5. 避坑与排查那些让我加班到凌晨的问题5.1 座位锁了但用户没支付座位一直灰着现象是用户选座后关闭页面座位状态停在 1别人选不了。原因是锁没有过期释放机制。解决是加定时任务或每次选座前清理过期锁。我一般两者都做选座接口入口先调releaseExpired再配一个每分钟跑一次的定时任务兜底。Scheduled(fixedRate 60000) public void releaseExpiredLocks() { seatMapper.releaseExpiredAll(LocalDateTime.now().minusMinutes(15)); }5.2 前端路由参数变了但页面没刷新现象是从一个场次切到另一个场次座位图还是旧的。原因是 Vue 复用组件实例created不再触发。解决是在watch里监听$route.params.scheduleId变化时重新拉数据。watch: { $route.params.scheduleId(newId) { if (newId) this.loadSeats(newId); } }5.3 支付回调重复执行导致座位状态错乱现象是支付平台重试回调订单被处理两次。原因是回调没有幂等控制。解决是在paySuccess里先判断订单状态只有「待支付」才处理已支付直接返回成功。这就是 3.2 里order.getStatus() ! 0判断的作用。5.4 跨域请求被浏览器拦截现象是前端调后端接口报 CORS 错误。原因是前后端不同端口。解决是在 SpringBoot 加全局跨域配置而不是在每个 Controller 上写CrossOrigin。Configuration public class CorsConfig implements WebMvcConfigurer { Override public void addCorsMappings(CorsRegistry registry) { registry.addMapping(/**) .allowedOriginPatterns(*) .allowedMethods(GET, POST, PUT, DELETE) .allowCredentials(true) .maxAge(3600); } }注意allowedOriginPatterns和allowCredentials(true)同时用时不能用allowedOrigins(*)否则浏览器会拒绝。5.5 分页插件失效导致全表查询现象是影片列表接口返回了全部数据分页参数没生效。原因是 MyBatis 分页插件没配置或配置顺序不对。解决是在配置类里注册PageInterceptor并确认它排在 SQL 拦截器之前。这个坑很隐蔽因为接口不报错只是慢。6. 把选座并发压到 500 并发的验证方法系统能跑通不等于能扛住。影院售票的高峰是热门影片开售那几分钟选座接口会瞬间承压。我一般用 JMeter 或 wrk 对锁座接口做压测重点看两个指标重复锁定率和响应时间。验证重复锁定可以写一个并发测试100 个线程同时锁同一个座位正确结果是只有 1 个成功其余 99 个返回失败。如果成功数大于 1说明锁有问题。Test public void testConcurrentLock() throws Exception { int threads 100; ExecutorService pool Executors.newFixedThreadPool(threads); CountDownLatch latch new CountDownLatch(threads); AtomicInteger success new AtomicInteger(0); for (int i 0; i threads; i) { final long userId i; pool.submit(() - { try { latch.await(); ResultString r seatService.lockSeats(1L, Arrays.asList(3-5), userId); if (r.getCode() 200) success.incrementAndGet(); } catch (Exception e) { // 忽略 } }); latch.countDown(); } pool.shutdown(); pool.awaitTermination(30, TimeUnit.SECONDS); System.out.println(成功锁定数 success.get()); // 期望为 1 }这段测试的关键是CountDownLatch让所有线程同时发起请求模拟真实并发。success期望值为 1大于 1 就说明条件更新没起作用要回去检查 SQL 的WHERE status 0是否被优化掉或者事务隔离级别是否被改过。压测时还要注意连接池大小。SpringBoot 默认 HikariCP 最大连接数是 10500 并发下会大量等待。我一般把maximum-pool-size调到 50 左右同时确认数据库的max_connections够用。响应时间方面锁座接口在正常负载下应该在 50ms 以内超过 200ms 就要查慢 SQL。另一个容易忽略的点是座位数据的加载方式。选座页一次性拉整场座位如果影厅有 300 个座位每次请求返回 300 条记录并发高时网络和序列化都是负担。我的习惯是座位数据加一层 Redis 缓存key 用seat:schedule:{id}锁座成功后主动删缓存下次请求重建。这样读多写少的选座场景能明显降负载。最后说个我自己的习惯任何涉及库存和状态的接口我都会先写并发测试再写业务代码。因为这类 bug 在功能测试里几乎测不出来只有并发才暴露。血泪经验是选座系统的坑不在功能多而在状态流转的每一个边界。把锁、过期、幂等这三件事做扎实这套 SpringBoot Vue 的影院系统就能真正拿去用而不是只停在演示。希望帮到你。本文还有配套的精品资源点击获取
延伸阅读

更多相关文章

2026/9/26 5:44:46

基于C#的Fanuc数控系统上位机开发:Focas通信与车间设备联网实践

简介:面向工业自动化与数控机床运维场景,这是一套基于C#开发的FANUC系统上位机管理工具,聚焦车削类机床的多设备数据采集、运行状态监控、故障报警与刀具寿命管理。工程覆盖FANUC FOCAS通信库调用(fwlib32.cs)、轴控制…

2026/9/26 5:39:46

飞鸟云邀请码获取指南:从注册机制到激活避坑全解析

1. 从一枚邀请码说起:飞鸟云到底在火什么最近“飞鸟云邀请码”这个词的热度一路走高。说实话,做这个领域的内容这么久,我明显感觉到邀请码这种模式已经从小众圈子的“暗号”变成了大众眼里的“入场券”。飞鸟云不是第一个这么玩的&#xff0c…

2026/9/26 5:39:46

GIF制作零基础教程:视频转GIF到逐帧动画,参数调优与避坑指南

前几天有个朋友发来一段产品演示视频,问怎么发到工作群里最方便。我还没来得及说“直接传视频”这句话,他又跟了一句:“网上都说用GIF,但我感觉是不是得装PS啊?听着就麻烦。”我把那段视频丢进在线转换工具&#xff0c…

2026/9/26 6:44:48

PHP+MySQL报修网站源码:工单状态流转与部署实战教程

简介:面向电脑维修公司的报修网站源代码,内置完整的在线报修功能与前台展示页面,可帮助传统维修商搭建品牌官网,让客户通过网页直接提交故障信息,减轻电话沟通成本,适应数字化获客需求。资源包共包含570个文…

2026/9/26 6:44:48

Claude CLI工程化底座:基于MCP协议的跨平台代码模板框架

1. 项目概述:这不是一个“模板库”,而是一套面向Claude生态的CLI工程化底座你搜“claude-code-templates”时,大概率会撞上一堆零散的GitHub仓库、知乎碎片帖、VS Code插件评论区里的抱怨,还有人把这当成某个现成的“代码生成器”…

2026/9/26 6:44:48

WeLive PHP在线客服系统实战:从LNMP部署到二次开发与运维调优

简介:面向需要自建在线客服系统的企业站点或个人开发者,WeLive是一款基于PHP与WebSocket开发的免费开源客服系统。它采用请求与推送全双工通信,支持Web与移动端访问,内置中英文双语切换、五套配色方案、AI机器人自动回复与访客文件…

2026/9/26 6:44:48

Coder自托管云IDE:Terraform部署+GPU加速AI编码实战

1. 项目概述:为什么一个“能自己装在服务器上的VS Code”突然成了开发者圈的硬通货最近两周,我在三个不同行业的技术群——一个做智能硬件固件的、一个搞金融风控模型的、还有一个专注教育SaaS的——都被人反复问同一个问题:“Coder咋下载&am…

2026/9/25 21:00:17

GAMP 5 基于风险的计算机化系统验证:软件分类与审计追踪实践

简介:《A Risk-Based Approach to Compliant GxP Computerized Systems》即业内熟知的GAMP 5指南,面向制药企业质量与IT合规人员、验证工程师及计算机化系统管理者,用于解决GxP法规环境下系统合规性难以科学落地的问题。文档以风险管理为主线…

2026/9/25 20:59:52

安全托管MSSP实战:从静态防御到人机协同的攻防运营与应急响应

简介:这份PPT围绕互联网业务安全托管服务展开,面向企业安全负责人、IT运维人员及关注MSSP/MSS选型的读者,重点回应传统安全过度依赖人工、碎片化静态防御难以对抗产业化攻击等痛点。资源共1个pptx文件,包体约30.63MB,以…

2026/9/26 0:04:28

画质修复APP怎么选?Wink影像修复能力与产品实力解析

现如今手机拍摄场景愈发丰富,演唱会直拍、漫展记录、老视频翻新、日常vlog录制,都会遇到画面模糊、噪点多、曝光失衡等问题,不少用户在挑选工具时比较在意一款画质修复APP能够兼顾修复效果与自然质感。Wink作为美图公司推出的全球化AI影像增强…

2026/9/26 0:04:28

超低能耗建筑K值要求能否满足?浙东铝业建筑型材解析

核心摘要浙东铝业的超低能耗系统门窗产品,资料显示保温性能可达 K≤1.4W/(㎡K),能够对应上海地区超低能耗住宅对门窗保温性能的应用需求。判断建筑是否满足超低能耗要求,不能只看铝型材本身,还需要结合玻璃、隔热条、密封系统、开…

2026/9/25 20:55:38

USB Type-C PCB布局分区设计:电源、高速信号与PD协议全攻略

做硬件这行,Type-C接口算是典型的“看着简单,做起来全坑”的东西。光引脚就24个,高低速信号、电源、控制线全部塞在一个小小的连接器里,如果PCB布局不做规划,打样回来基本就是“插上没反应”、“高速掉线”、“静电一打…

2026/9/25 18:41:36

系统编程学习原型如何补齐稳定性边界

系统编程学习原型如何补齐稳定性边界预算有限时&#xff0c;我先优化明显多余的复制&#xff0c;而不是猜测性地换容器。用借用传递只读数据通常就能减少分配&#xff1a; fn parse(line: &str) -> Result<Item, Error> { /* ... */ }用基准确认热点确实在分配&am…

2026/9/25 18:34:56

雨花区哪家财务公司代理记账比较好?

在雨花区&#xff0c;企业处理财税事务常常面临诸多挑战&#xff0c;选择一家靠谱的财务公司至关重要。湖南巨勤财务管理咨询有限公司就是本地正规实体财税服务机构&#xff0c;深耕本地工商财税行业多年&#xff0c;熟悉当地工商局、税务局最新政策与申报流程。主营公司注册、…

还想了解更多?直接咨询顾问

免费诊断 + 免费方案 + 透明报价。

全国咨询热线400-8866-253
免费获取方案
☎咨询二维码 ☎ ↑