资讯详情 电影院订票选座小程序:从选座锁座到订单落库全链路实战
📅 2026/10/8 10:18:40
简介这份毕业设计资源面向计算机相关专业学生与Java全栈初学者提供一套基于微信小程序SSMMySQL的电影院订票选座系统完整方案解决传统APP安装成本高、用户留存难的问题将订票、选座、支付等核心流程迁移到小程序端。资源包共1138个文件约45.81MB包含103个Java后端源码、114个Vue前端组件、160个JS脚本、75个WXSS与73个WXML小程序页面文件以及2个SQL数据库脚本、开题报告、毕业论文和mp4视频演示另附png、svg、jpg等界面素材与bat启动脚本覆盖从环境搭建到功能演示的完整链路。系统实现管理员对影院、电影、资讯及多状态订单的统一管理用户可查看、收藏、评论影院与电影完成选座支付与在线充值。目前已有160人学习适合需要完整赛题方案、可运行源码与论文参考的读者便于快速理解SSM与小程序前后端协作方式并二次开发。1. 电影院订票选座小程序从选座锁座到订单落库一套能跑通的毕业设计全链路影院选座和普通电商下单最大的区别在于座位是有限库存而且必须“先锁后付”。你点开一场《流浪地球3》看到 8 排 6 座是灰的那不是样式问题是有人已经占了或者正在付款。这个项目要解决的核心就是微信小程序端把座位图渲染出来用户点选后服务端原子性地锁座支付成功后落订单超时未付自动释放。技术栈是微信小程序 SSMSpring SpringMVC MyBatis MySQL属于计算机毕业设计里最典型也最容易翻车的一类——看着简单真做起来锁座并发、座位图坐标、订单状态机全是坑。适合正在做基于 Java 的毕业设计选题、想找一个有真实业务复杂度的同学也适合已经写完 CRUD 想补一块“能讲清楚”的亮点模块的人。2. 选座业务的数据模型座位、场次、订单三张表怎么设计才不返工很多人一上来就写代码表结构拍脑袋定做到锁座那一步发现没法表达“这个座位在这场次被谁锁了多久”只能推倒重来。这一章把数据模型讲透后面所有逻辑都从这里长出来。2.1 影院座位图的本质物理座位 场次座位状态先分清两个概念。物理座位是影厅里固定的椅子1 号厅有 10 排每排 12 座这是不变的。场次座位状态是“某一场次里某个座位当前是否可售”这是随每场电影变化的。如果你只建一张 seat 表把状态直接写在座位上那同一影厅放两场电影就冲突了。常见做法是拆成三张核心表表名作用关键字段hall影厅id, name, row_count, col_countseat物理座位id, hall_id, row_num, col_num, seat_typeschedule场次id, movie_id, hall_id, start_time, priceschedule_seat场次座位状态id, schedule_id, seat_id, status, lock_user, lock_time, order_idorder订单id, order_no, user_id, schedule_id, total_price, status, create_timeschedule_seat是整张图的核心。每排一场电影就根据seat表批量生成这个场次的所有座位记录初始status 0可售。用户选座后把对应记录改成status 1锁定并写入lock_user和lock_time支付成功改成status 2已售超时释放回0。提示schedule_seat的数据量是 场次 × 座位数。一个影厅 120 座一天排 20 场一个月就是 7 万多条毕业设计规模完全扛得住但别忘了给(schedule_id, seat_id)建唯一索引这是后面防重复锁座的底牌。2.2 建表 SQL 与唯一索引直接给可执行的建表语句MySQL 5.7 和 8.0 都能跑CREATE TABLE schedule_seat ( id BIGINT NOT NULL AUTO_INCREMENT, schedule_id BIGINT NOT NULL COMMENT 场次id, seat_id BIGINT NOT NULL COMMENT 物理座位id, status TINYINT NOT NULL DEFAULT 0 COMMENT 0可售 1锁定 2已售, lock_user BIGINT DEFAULT NULL COMMENT 锁定用户id, lock_time DATETIME DEFAULT NULL COMMENT 锁定时间, order_id BIGINT DEFAULT NULL COMMENT 关联订单, PRIMARY KEY (id), UNIQUE KEY uk_schedule_seat (schedule_id,seat_id), KEY idx_status_time (status,lock_time) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;逻辑说明唯一索引uk_schedule_seat保证同一场次同一座位只有一条记录从数据库层面杜绝重复插入。idx_status_time是给超时释放任务用的——定时扫描status1 且 lock_time 超过 15 分钟的记录没有这个索引场次一多扫描就慢。参数说明status用 TINYINT 而不是字符串省空间且比较快lock_time用 DATETIME释放任务里用DATE_SUB(NOW(), INTERVAL 15 MINUTE)做条件。锁定时长 15 分钟是行业常见值太短用户来不及付太长座位被占死。2.3 场次座位批量初始化每新增一个场次要把它对应的影厅座位全部复制一份到schedule_seat。用一条 INSERT ... SELECT 就能搞定不用在 Java 里循环INSERT INTO schedule_seat (schedule_id, seat_id, status) SELECT #{scheduleId}, id, 0 FROM seat WHERE hall_id #{hallId};逻辑说明#{scheduleId}是刚插入场次返回的自增 id#{hallId}是该场次所属影厅。这条语句把该影厅所有物理座位一次性铺成场次座位初始全部可售。参数说明MyBatis 里用Insert或 XML 都行注意useGeneratedKeystrue keyPropertyid拿到场次 id 后再执行这条。如果影厅座位是 200 个这条语句插入 200 行毫秒级完成。3. 微信小程序选座页座位图渲染与点击交互后端数据铺好了前端要把座位图画出来。这一章讲小程序端怎么把二维座位数组渲染成可点击的座位图以及点击时怎么和状态联动。3.1 从接口拿到座位数据并转成二维数组后端返回的是一维列表前端要按排分组才能画成影院那种一行一行的效果。接口返回结构大致是{ scheduleId: 1001, rows: 10, cols: 12, seats: [ {seatId: 1, row: 1, col: 1, status: 0}, {seatId: 2, row: 1, col: 2, status: 2} ] }小程序页面里处理成二维数组// pages/selectSeat/selectSeat.js Page({ data: { seatMap: [], selected: [] }, onLoad(options) { const scheduleId options.scheduleId; wx.request({ url: https://your-domain/api/schedule/${scheduleId}/seats, success: (res) { const { rows, cols, seats } res.data; // 初始化 rows x cols 的二维数组 const map Array.from({ length: rows }, () Array.from({ length: cols }, () ({ status: -1 })) ); seats.forEach(s { map[s.row - 1][s.col - 1] { seatId: s.seatId, status: s.status // 0可售 1锁定 2已售 }; }); this.setData({ seatMap: map }); } }); } });逻辑说明Array.from先铺一个 rows×cols 的空矩阵status: -1表示过道或空位影院座位图不是矩形边角可能有空缺。遍历接口返回的座位填充进去行列出后端给的是 1 开始数组下标 0 开始所以减 1。参数说明status三个值前端要区分样式——0 白色可点1 黄色锁定不可点2 灰色已售不可点-1 透明占位。selected数组存用户当前选中的 seatId用于底部结算栏显示。3.2 座位点击与选中态切换点击座位要判断状态可售才能选已选再点取消onSeatTap(e) { const { row, col } e.currentTarget.dataset; const seat this.data.seatMap[row][col]; if (seat.status ! 0) return; // 锁定或已售直接忽略 const key ${row}-${col}; let selected this.data.selected.slice(); const idx selected.indexOf(key); if (idx -1) { selected.splice(idx, 1); // 取消选中 } else { if (selected.length 4) { wx.showToast({ title: 最多选4个座位, icon: none }); return; } selected.push(key); } // 更新该座位在 map 里的选中标记 const map this.data.seatMap; map[row][col].selected idx -1; this.setData({ selected, seatMap: map }); }逻辑说明用row-col拼成 key 存进 selected避免存对象引用导致 setData 比较失效。最多选 4 个是影院常见限制防止恶意占座。map[row][col].selected是前端临时标记和数据库 status 无关只控制样式。参数说明e.currentTarget.dataset里的 row/col 来自 wxml 的>// app.js 或页面 onLoad 里 const sysInfo wx.getSystemInfoSync(); const statusBarHeight sysInfo.statusBarHeight; // 状态栏高度 const menuButton wx.getMenuButtonBoundingClientRect(); // 胶囊按钮位置 const navBarHeight (menuButton.top - statusBarHeight) * 2 menuButton.height; this.setData({ statusBarHeight, navBarHeight });逻辑说明微信小程序顶部导航栏高度不是固定值不同机型不一样。用胶囊按钮的 top 减去状态栏高度得到导航栏内容区高度乘 2 加上胶囊高度就是完整导航栏高度。这是社区里验证过的通用算法。参数说明statusBarHeight用于占位navBarHeight用于自定义导航栏容器高度。座位图容器设padding-top: {{statusBarHeight navBarHeight}}px就不会被盖。4. 锁座并发为什么你的选座会出现两个人抢同一个座位这是整个项目最容易翻车的地方也是答辩时老师最爱问的点。两个人同时点同一个座位如果代码写得随意两张订单都会创建成功到了影院发现座位重了。这一章讲清楚怎么用数据库事务和乐观锁把这个问题摁死。4.1 锁座的正确姿势UPDATE 带状态条件错误做法是先 SELECT 查状态Java 里判断可售再 UPDATE。这两步之间有时间窗口并发下必然出问题。正确做法是把判断和更新合并成一条 SQLUPDATE schedule_seat SET status 1, lock_user #{userId}, lock_time NOW() WHERE schedule_id #{scheduleId} AND seat_id #{seatId} AND status 0;逻辑说明WHERE status 0是关键。数据库执行 UPDATE 时会加行锁两个并发请求只有一个能匹配到status 0并更新成功另一个匹配 0 行。Java 里判断affectedRowsTransactional public boolean lockSeat(Long scheduleId, Long seatId, Long userId) { int affected scheduleSeatMapper.lockSeat(scheduleId, seatId, userId); if (affected 0) { throw new BizException(座位已被选走); } return true; }参数说明Transactional保证锁座和后续写订单在同一事务失败一起回滚。affected 0说明座位已被别人锁走直接抛业务异常前端提示“座位已被选走请重新选择”。注意多个座位要一起锁时必须按 seat_id 排序后再依次 UPDATE否则两个请求锁座顺序相反会死锁。这是血泪经验答辩被问到能加分。4.2 超时未支付自动释放定时任务怎么写用户锁了座不付款座位不能一直占着。常见做法是定时任务扫描超时锁定记录释放Scheduled(cron 0 */1 * * * ?) // 每分钟执行 public void releaseExpiredSeats() { // 释放超过15分钟仍未支付的锁定座位 int released scheduleSeatMapper.releaseExpired(15); log.info(释放超时座位 {} 个, released); }对应 SQLUPDATE schedule_seat SET status 0, lock_user NULL, lock_time NULL WHERE status 1 AND lock_time DATE_SUB(NOW(), INTERVAL #{minutes} MINUTE);逻辑说明cron 0 */1 * * * ?表示每分钟第 0 秒执行。DATE_SUB(NOW(), INTERVAL 15 MINUTE)算出 15 分钟前的时间点早于它的锁定记录全部释放。参数说明释放时长 15 分钟要和前端提示一致。如果项目用了 Redis更优雅的做法是下单时写一个 15 分钟过期的 key过期回调释放但毕业设计用定时任务足够别过度设计。4.3 订单状态机从待支付到已完成订单状态不能乱改要有明确流转状态值含义可流转到0待支付1 已支付 / 2 已取消1已支付3 已完成 / 4 退款中2已取消无3已完成无支付回调里更新订单状态并同步座位状态Transactional public void paySuccess(String orderNo) { Order order orderMapper.selectByNo(orderNo); if (order.getStatus() ! 0) return; // 幂等防止重复回调 orderMapper.updateStatus(orderNo, 1); // 座位从锁定变已售 scheduleSeatMapper.soldByOrder(order.getId()); }逻辑说明if (order.getStatus() ! 0) return;是幂等保护微信支付回调可能重复推送不加这句会重复处理。soldByOrder把该订单关联的座位 status 从 1 改成 2。参数说明订单号order_no建议用时间戳 用户 id 后几位生成保证唯一且可读。状态值用数字存前端映射成文案。5. 避坑与排查那些让毕业设计卡三天的具体问题这一章全是实操里真会遇到的坑每条按现象、原因、解决写照着排查能省大量时间。5.1 座位图渲染出来全是灰的现象小程序选座页打开所有座位都是不可点的灰色。原因接口返回的 status 字段类型不对或者前端判断条件写反了。常见是后端返回status为字符串0前端用 0严格比较永远 false。解决后端统一返回数字类型前端判断前先Number(seat.status)。另外检查seatMap初始化时status: -1是否被误判成不可售-1 应该单独处理成占位不渲染。5.2 锁座成功但订单没生成现象用户选座提示成功但订单列表里没有记录。原因锁座和创建订单不在同一事务里锁座提交了创建订单抛异常回滚了座位却被锁死。解决把锁座和创建订单放进同一个Transactional方法任何一步失败整体回滚。检查 Spring 事务是否生效——同类内部方法调用不会走代理事务会失效这是经典翻车点。5.3 超时释放把已支付订单的座位也释放了现象用户付完款过一会座位又变回可售被别人买了。原因释放任务的 SQL 只判断了status 1和超时没排除已经生成订单的情况。支付成功后如果座位状态没及时从 1 改成 2就会被误释放。解决释放 SQL 加条件AND order_id IS NULL或者支付回调里确保座位状态同步更新。更稳妥的是释放前先查关联订单状态已支付的不释放。5.4 小程序请求后端报域名不合法现象开发者工具里接口正常真机预览报“不在以下 request 合法域名列表中”。原因微信小程序正式环境要求所有请求域名在后台配置且必须是 HTTPS。解决开发阶段在开发者工具“详情 - 本地设置”勾选“不校验合法域名”。上线前在微信公众平台配置 request 合法域名。毕业设计答辩演示用开发者工具即可但要知道这个限制。5.5 MySQL 8.0 连接报时区错误现象Spring 连 MySQL 8.0 报The server time zone value xxx is unrecognized。原因MySQL 8.0 默认时区配置和 JDBC 驱动不匹配。解决JDBC URL 加参数?serverTimezoneAsia/ShanghaiuseSSLfalseallowPublicKeyRetrievaltrue。这是 MySQL 8.0 安装配置后最常见的连接问题加上就好。6. 让答辩加分的一个技巧把锁座过程做成可演示的并发验证毕业设计答辩最怕老师说“你这个没难度”。锁座这块如果只是讲说服力有限。我一般会准备一个能现场演示的并发验证用两个浏览器窗口或者 Postman 同时发锁座请求展示只有一个成功、另一个返回“座位已被选走”。这个演示比讲十分钟原理都管用。具体做法是写一个简单的压测脚本用 Java 的 CountDownLatch 模拟同时发起public void concurrentLockTest() throws InterruptedException { int threads 10; CountDownLatch latch new CountDownLatch(threads); ExecutorService pool Executors.newFixedThreadPool(threads); AtomicInteger success new AtomicInteger(0); for (int i 0; i threads; i) { final long userId i 1; pool.submit(() - { try { latch.await(); // 所有线程在此等待同时放行 boolean ok seatService.lockSeat(1001L, 5L, userId); if (ok) success.incrementAndGet(); } catch (Exception e) { // 锁座失败正常 } }); latch.countDown(); } pool.shutdown(); pool.awaitTermination(10, TimeUnit.SECONDS); System.out.println(成功锁座数 success.get()); // 期望输出 1 }逻辑说明CountDownLatch让 10 个线程在latch.await()处阻塞主线程countDown到 0 时同时放行制造真实并发。AtomicInteger统计成功次数因为数据库行锁和status 0条件最终只有 1 个线程能成功。参数说明threads 10模拟 10 人抢同一座位scheduleId1001, seatId5是测试数据。期望输出“成功锁座数1”如果输出大于 1说明锁座逻辑有并发漏洞回去检查 UPDATE 语句的 WHERE 条件。这个测试跑通答辩时直接展示控制台输出再配合数据库里那条status 2的记录老师基本不会再追问并发问题。我自己的习惯是每个涉及库存的项目都留这么一个测试类改完锁座逻辑就跑一遍比事后拍脑袋靠谱。希望帮到你。本文还有配套的精品资源点击获取