简介这是一套面向高校计算机专业毕业设计的电影院订票选座小程序完整项目基于微信小程序、SSM框架、Java与MySQL开发适合正在准备毕设或需要小程序后台管理系统实战案例的学生与开发者。资源包共1138个文件约45.81MB包含230个png与57个jpg界面素材、160个js与114个vue前端脚本、103个java后端代码、75个wxss与73个wxml小程序页面、2个sql数据库脚本以及开题报告、毕业论文和mp4视频演示等文档资料。系统前端借助微信开发者工具实现影院与电影浏览、收藏评论、在线选座支付及账户充值后台由SSM框架支撑管理员对影院、电影、资讯和不同状态订单进行统一管理。已有160人学习下载可帮助读者快速理解小程序与SSM前后端分离的目录结构、数据库表设计与核心业务逻辑为毕设开发与答辩提供完整参考。1. 电影院订票选座小程序从选座锁座到订单落库一套能跑通的毕业设计技术栈影院选座和普通电商下单最大的区别在于座位是有时效的稀缺资源。用户点开《流浪地球3》的场次看到的那张座位图不是静态图片而是实时状态——已售、可选、锁定中三种状态混在一起还要防止两个人同时选中同一个座位。这就是电影院订票选座小程序的技术核心也是毕业设计答辩时老师最爱追问的点。这套方案基于微信小程序 SSMSpring SpringMVC MyBatis MySQL 开发覆盖影片浏览、场次选择、可视化选座、下单支付、订单管理完整链路配套源码、数据库脚本、开题报告和论文。适合正在做计算机毕业设计、想找一个业务闭环完整又不过于烂大街的选题的同学也适合想练手小程序前后端联调的开发者。下面按「业务怎么跑 → 库表怎么设计 → 后端怎么写 → 小程序怎么调 → 坑在哪」的顺序拆开讲。2. 选座业务到底难在哪先想清楚状态流转再动手2.1 座位状态机是整个系统的地基很多人一上来就画页面、建表结果做到下单环节发现座位状态对不上。正确顺序是先定义座位在生命周期里的状态。一张座位在某个场次下只会处于这几种状态之一状态值含义触发动作可流转到0可选场次初始化1被锁定1锁定中用户点击选座0超时释放或 2支付成功2已售支付回调成功终态3退票中用户申请退票0退票完成关键点锁定中是有时效的。用户选了座但没付款通常给 15 分钟超时后系统要自动把座位放回可选。这个「自动」不能靠前端定时器必须后端有兜底机制否则用户关掉小程序座位就永远锁死了。常见做法是下单时写一条锁定记录带过期时间再用定时任务扫描过期记录释放或者查询座位时顺带判断锁定是否过期。2.2 为什么选 SSM 而不是 SpringBoot这是毕业设计里绕不开的选型问题。SSM 是 Spring SpringMVC MyBatis 的整合配置量大、XML 多但恰恰因为「麻烦」答辩时能讲的东西多IOC 容器怎么装配 Bean、AOP 事务怎么切、MyBatis 的 Mapper 映射怎么写。SpringBoot 自动配置把很多细节藏起来了老师一问「事务是怎么生效的」容易答不上来。对于毕业设计这种要体现「你确实动手写了」的场景SSM 的显式配置反而是加分项。数据库选 MySQL 5.7 或 8.0 都行5.7 兼容性好、安装教程满地都是8.0 支持窗口函数但毕业设计基本用不上。2.3 整体模块划分系统分两端。小程序端负责展示和交互首页影片列表、影片详情、场次选择、选座页、订单确认、个人中心。后端提供 REST 接口影片查询、场次查询、座位图查询、锁座、下单、支付回调、订单列表。管理端可以做成简单的 Web 后台负责影片、影厅、场次、座位的基础数据维护。数据流是管理端录入影厅座位布局 → 生成场次时按布局初始化该场次的座位记录 → 用户选座触发锁座 → 下单 → 支付回调改状态。3. 数据库设计五张核心表撑起整个订票链路3.1 影片、影厅、场次三张基础表先建基础数据表。影片表存电影信息影厅表存放映厅和座位布局场次表把影片和影厅在某个时间点关联起来。-- 影片表 CREATE TABLE film ( id INT PRIMARY KEY AUTO_INCREMENT, name VARCHAR(100) NOT NULL COMMENT 影片名, poster VARCHAR(255) COMMENT 海报图URL, duration INT COMMENT 时长(分钟), release_date DATE COMMENT 上映日期, status TINYINT DEFAULT 1 COMMENT 1上映 0下架 ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; -- 影厅表seat_layout用JSON存行列布局 CREATE TABLE hall ( id INT PRIMARY KEY AUTO_INCREMENT, name VARCHAR(50) NOT NULL COMMENT 影厅名, row_count INT NOT NULL COMMENT 座位行数, col_count INT NOT NULL COMMENT 座位列数 ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; -- 场次表 CREATE TABLE schedule ( id INT PRIMARY KEY AUTO_INCREMENT, film_id INT NOT NULL, hall_id INT NOT NULL, start_time DATETIME NOT NULL COMMENT 放映开始时间, price DECIMAL(10,2) NOT NULL COMMENT 票价, INDEX idx_film (film_id), INDEX idx_start (start_time) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;字段说明duration用来算散场时间避免同一影厅场次时间冲突price放在场次表而不是影片表因为不同时段票价不同黄金场贵、早场便宜。schedule上的两个索引分别服务「查某部影片的场次」和「查某天的场次」两种查询。3.2 座位表与订单表状态和唯一约束是重点座位表是核心它记录的是「某个场次的某个座位」的状态不是物理座位本身。所以座位记录是随场次生成的一个场次有多少座位就有多少条记录。-- 场次座位表 CREATE TABLE seat ( id BIGINT PRIMARY KEY AUTO_INCREMENT, schedule_id INT NOT NULL, row_num INT NOT NULL COMMENT 第几排, col_num INT NOT NULL COMMENT 第几列, status TINYINT DEFAULT 0 COMMENT 0可选 1锁定 2已售 3退票中, lock_time DATETIME COMMENT 锁定时间, order_id BIGINT COMMENT 关联订单, UNIQUE KEY uk_schedule_seat (schedule_id, row_num, col_num), INDEX idx_schedule_status (schedule_id, status) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; -- 订单表 CREATE TABLE orders ( id BIGINT PRIMARY KEY AUTO_INCREMENT, order_no VARCHAR(32) NOT NULL UNIQUE COMMENT 订单号, user_id INT NOT NULL, schedule_id INT NOT NULL, seat_ids VARCHAR(255) COMMENT 座位id逗号分隔, total_price DECIMAL(10,2), status TINYINT DEFAULT 0 COMMENT 0待支付 1已支付 2已取消 3已退款, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, pay_time DATETIME, INDEX idx_user (user_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;uk_schedule_seat这个唯一约束是防重复选座的最后一道防线——即使并发逻辑有漏洞数据库层面也会拒绝插入重复座位。idx_schedule_status让「查某场次所有可选座位」走索引。订单表用seat_ids存逗号分隔的座位 id毕业设计够用生产环境一般会拆一张订单座位关联表但这里为了简化可以接受。提示座位记录不要预先全量生成到一张大表里而是创建场次时按影厅布局批量插入该场次的座位否则数据量会爆炸。4. 后端接口实现锁座、下单、超时释放三件事4.1 锁座接口用乐观更新防并发锁座是整个系统最容易翻车的地方。两个用户同时点同一个座位如果先查后改中间有窗口期就会超卖。正确做法是用带条件的 UPDATE让数据库的行锁来保证原子性。// SeatMapper.xml 中的锁座SQL // UPDATE seat SET status1, lock_timeNOW(), order_id#{orderId} // WHERE id#{seatId} AND status0// SeatServiceImpl.java Override Transactional(rollbackFor Exception.class) public boolean lockSeats(ListLong seatIds, Long orderId) { for (Long seatId : seatIds) { // 返回受影响行数为0说明座位已被别人抢走 int rows seatMapper.lockSeat(seatId, orderId); if (rows 0) { // 事务回滚前面锁的座位全部释放 throw new RuntimeException(座位已被占用请重新选择); } } return true; }逻辑说明lockSeat的 SQL 条件是status0只有当前状态是可选才能更新成功。如果返回 0 行说明这个座位已经被别人锁了直接抛异常触发Transactional回滚前面已经锁成功的座位也会一起回滚。参数上orderId先由订单服务生成一个临时订单号传入锁座和建订单在同一个事务里。这里用Transactional的默认传播行为 REQUIRED保证锁座和订单插入要么全成功要么全失败。4.2 下单与支付回调下单接口接收场次 id 和座位 id 列表先校验座位状态再锁座最后写订单。支付回调是模拟的毕业设计一般不接真实支付但流程要写对回调里把订单状态改成已支付同时把关联座位状态从 1 改成 2。PostMapping(/order/create) public Result createOrder(RequestBody OrderDTO dto) { // 1. 生成订单号 String orderNo CIN System.currentTimeMillis(); // 2. 锁座内部事务 orderService.lockSeats(dto.getSeatIds(), orderNo); // 3. 写订单 Orders order new Orders(); order.setOrderNo(orderNo); order.setUserId(dto.getUserId()); order.setScheduleId(dto.getScheduleId()); order.setSeatIds(StringUtils.join(dto.getSeatIds(), ,)); order.setTotalPrice(dto.getTotalPrice()); order.setStatus(0); orderMapper.insert(order); return Result.ok(orderNo); }参数说明seatIds是前端选中的座位主键列表totalPrice前端算好传过来后端最好再校验一遍防止篡改。返回orderNo给前端前端拿它去调支付。4.3 超时未支付自动释放座位用户下单后不付款座位不能一直锁着。用 Spring 的定时任务每 5 分钟扫一次。Component public class SeatReleaseTask { Autowired private SeatMapper seatMapper; // 每5分钟执行一次 Scheduled(cron 0 0/5 * * * ?) public void releaseExpiredSeats() { // 释放锁定超过15分钟且订单未支付的座位 int count seatMapper.releaseExpired(15); System.out.println(释放超时座位数 count); } }!-- SeatMapper.xml -- !-- UPDATE seat s JOIN orders o ON s.order_ido.order_no SET s.status0, s.lock_timeNULL, s.order_idNULL WHERE s.status1 AND o.status0 AND s.lock_time DATE_SUB(NOW(), INTERVAL #{minutes} MINUTE) --逻辑说明这条 SQL 用 JOIN 把座位和订单关联起来只释放「座位锁定中」且「订单待支付」且「锁定时间超过 15 分钟」的记录。参数minutes就是超时阈值改成 10 或 20 都行看业务容忍度。注意定时任务要配EnableScheduling才生效这是新手常忘的一步。5. 小程序端选座页渲染、交互与接口联调5.1 座位图渲染用 grid 布局动态生成选座页的核心是把后端返回的座位数组渲染成一张座位图。用 flex 或 grid 都行关键是行列要对齐。// pages/seat/seat.js Page({ data: { seats: [], // 后端返回的座位列表 selected: [], // 已选座位id maxSelect: 4 // 最多选4个 }, onLoad(options) { const scheduleId options.scheduleId; wx.request({ url: http://localhost:8080/api/seat/list, data: { scheduleId }, success: (res) { // 按行分组方便wxml里嵌套渲染 const grouped this.groupByRow(res.data.data); this.setData({ seats: grouped }); } }); }, groupByRow(list) { const map {}; list.forEach(s { if (!map[s.rowNum]) map[s.rowNum] []; map[s.rowNum].push(s); }); return Object.keys(map).map(k ({ row: k, cols: map[k] })); }, onSeatTap(e) { const seat e.currentTarget.dataset.seat; if (seat.status ! 0) return; // 非可选直接忽略 let selected this.data.selected.slice(); const idx selected.indexOf(seat.id); if (idx -1) { selected.splice(idx, 1); } else { if (selected.length this.data.maxSelect) { wx.showToast({ title: 最多选4个座位, icon: none }); return; } selected.push(seat.id); } this.setData({ selected }); } });逻辑说明groupByRow把一维座位数组按排号分组wxml 里用两层循环渲染外层排、内层列。onSeatTap里先判断seat.status只有 0可选才响应点击已售和锁定的座位点了没反应。maxSelect限制一次最多选 4 个这是影院常见规则也防止用户一次锁太多座位。参数scheduleId从页面跳转时带过来。5.2 座位样式与状态区分wxml 里根据status和是否选中给不同 class颜色区分状态。view classscreen银幕中央/view view wx:for{{seats}} wx:keyrow classrow view wx:for{{item.cols}} wx:keyid classseat {{item.status0 ? (selected.indexOf(item.id)-1 ? selected:free) : taken}} >wx.login({ success: (res) { wx.request({ url: http://localhost:8080/api/user/login, method: POST, data: { code: res.code }, success: (r) { // 后端返回用户id和token存本地 wx.setStorageSync(userId, r.data.data.userId); } }); } });后端拿到 code 后调微信接口换 openid需要小程序 appid 和 secret查用户表没有就插入新用户。这一步是「微信小程序登录获取手机号」的前置手机号授权是另一个按钮触发getPhoneNumber毕业设计里可以只做登录不做手机号。6. 避坑与排查五个让答辩翻车的真实问题6.1 座位超卖并发下两个用户选到同一座位现象压测或演示时两个账号同时点同一个座位都能下单成功。原因锁座逻辑写成了「先 SELECT 查状态再 UPDATE」两步之间有窗口期。解决改成带条件的 UPDATEWHERE status0用返回行数判断是否抢到配合Transactional回滚。这是最经典的坑答辩老师十有八九会问。6.2 座位永远锁死用户关掉小程序后座位不释放现象用户选了座没付款直接退出再进来发现座位还是锁定状态。原因只在前端做了定时释放用户关页面定时器就没了。解决后端定时任务扫lock_time超时的记录或者查询座位时实时判断锁定是否过期。定时任务记得加EnableScheduling。6.3 跨域请求失败小程序调后端报 request:fail现象小程序开发者工具里请求后端接口报错真机预览也失败。原因小程序默认校验合法域名本地开发要在开发者工具里勾选「不校验合法域名」。解决开发阶段勾选该选项上线前把后端域名配到小程序后台的 request 合法域名里。注意本地localhost真机访问不到要用局域网 IP。6.4 时间格式错乱场次时间显示成时间戳现象后端返回的start_time是2024-01-01T10:00:00.00000:00这种格式小程序直接显示很难看。原因SpringMVC 默认用 Jackson 序列化 Date格式是 ISO8601。解决在实体类的日期字段加JsonFormat(patternyyyy-MM-dd HH:mm:ss, timezoneGMT8)或者在配置文件里全局配。时区一定要写 GMT8否则会差 8 小时。6.5 数据库中文乱码影片名存进去变成问号现象插入中文影片名查出来是???。原因数据库、表、连接三处字符集不一致。解决建库建表用utf8mb4JDBC 连接串加?useUnicodetruecharacterEncodingutf8MySQL 配置文件里character-set-serverutf8mb4。三处都对齐才行只改一处没用。7. 让答辩加分的一个技巧把选座并发做成可演示的对比毕业设计答辩最怕的是「你讲了一堆老师看不出你到底做了什么」。选座并发这个点我一般会准备一个可现场演示的对比写一个简单的压测脚本模拟 10 个线程同时抢同一个座位先跑「先查后改」的旧逻辑控制台打印出「10 个请求全部锁座成功」的异常结果再跑「条件更新」的新逻辑打印出「1 个成功9 个失败」。这个对比一出来老师立刻明白你懂并发。// 用 CountDownLatch 模拟并发抢座 public void concurrentTest() 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) { pool.submit(() - { try { latch.await(); // 等所有线程就绪 boolean ok seatService.lockSeats( Collections.singletonList(1L), 999L); if (ok) success.incrementAndGet(); } catch (Exception e) { // 锁座失败忽略 } finally { latch.countDown(); } }); } latch.await(); Thread.sleep(1000); System.out.println(抢座成功数 success.get()); // 正确逻辑应为1 }参数说明threads是并发数latch.await()让所有线程同时起跑success统计成功数。正确逻辑下应该只有 1 个成功如果打印出大于 1说明锁座逻辑有并发漏洞。这个脚本不用接前端直接跑 main 方法就能演示答辩时投屏效果很好。另外论文里写「系统测试」章节时把这段并发测试的截图放进去比干巴巴写「系统运行正常」有说服力得多。数据库脚本、源码、开题报告这些材料要保证能一键跑起来——我见过太多同学答辩时环境起不来当场翻车。建议提前在一台干净电脑上完整走一遍装 MySQL、导脚本、改数据库连接配置、启动后端、编译小程序确认无误再上场。希望帮到你。本文还有配套的精品资源点击获取
阅读完成 · 觉得有帮助?