
毕业设计年年都有电影院购票系统的影子这句话我做了这么多年项目辅导体会很深。原因很简单一个基于SpringBootVue的web电影院购票系统管理系统功能边界清晰、业务链条完整、技术栈主流正好把前端交互、后端接口、数据库设计、事务处理这些全栈基本功全都串起来了。你今天看到的这个标题——“基于SpringBootVue的web电影院购票系统管理系统源码MyBatisMySQL”本质上就是一套标准的全栈练手项目模板。但它背后的门道远不止“能跑起来”这么简单从版本搭配到表结构设计从选座逻辑到接口权限每一步都有讲究。这篇文章我会以实际做过的角度把整套系统的设计思路、关键代码、数据库建表方案、常见坑位全部拆开讲一遍。不管你是准备做课程设计、毕业设计还是刚学完SpringBoot和Vue想找个完整项目练手这篇文章都适配。我会特别把2025年这个时间点最容易踩的版本问题、MyBatis配置问题、Vue打包问题讲透你现在看到的热搜词里那些“springboot版本太高”“vue 打包后 布局异常”“mybatis 单个数字字符比较”之类的问题我都会在正文里实际演示排查和解决过程。1. 项目整体设计与功能拆解1.1 这个系统到底在做什么一句话概括用户通过浏览器访问前端页面看到电影列表和场次安排选择座位后生成订单完成模拟支付管理员则通过后台页面管理电影信息、排片场次、处理订单并查看基本的统计数据。这个业务模型非常经典它覆盖了一个真实电商类系统的主干流程浏览商品电影列表→ 选择SKU场次座位→ 下单订单→ 支付模拟→ 消费验证取票码。你以后做任何带有交易属性的系统比如酒店预订、健身房预约、自习室占座业务流程都是这个模式。前后端分离是这个项目最大的架构特点。SpringBoot只负责提供RESTful接口它不关心页面长什么样Vue项目独立启动通过axios向SpringBoot发请求拿JSON数据。两者之间通过HTTP协议通信前端开发时用代理转发请求生产环境把Vue打包后的静态文件扔到Nginx或者SpringBoot的static目录里都可以。1.2 角色权限与核心业务流程系统分两个角色普通用户和管理员。用户端流程是注册/登录 → 浏览影片 → 选择场次 → 选座 → 确认订单 → 模拟支付 → 查看订单 → 发表评论。管理员端流程是登录后台 → 维护电影信息 → 安排影厅场次 → 查看用户订单 → 上架/下架影片。这里的核心链路是选座和下单也是整个系统技术含量最高的部分。选座要考虑座位状态的实时性已售、锁定、空闲下单要考虑库存扣减和订单生成的原子性。很多课设项目在这里只是做了个假象前端选完座位后端随便存个订单没有任何一致性控制。如果你能把这块做好面试时聊项目会非常有底气。还有一个容易被忽略的功能点电影详情页的预告片播放。现在很多影院网站的视频源都是m3u8格式这涉及前端播放HLS流的问题后面我会专门讲解用hls.js播放m3u8的方法。1.3 功能模块清单速查模块功能点优先级用户模块注册、登录、个人信息维护核心电影模块电影列表、搜索、详情、预告片核心排片模块影厅管理、场次管理、座位初始化核心选座购票座位图渲染、锁座、生成订单核心订单模块订单列表、订单详情、模拟支付、取消核心评论模块发表评论、查看评论列表进阶统计模块票房统计、影片热度、订单量图表加分后台管理电影CRUD、场次CRUD、订单管理核心我的建议是先把核心链路走通再加评论和统计。很多同学一上来就想把所有功能全做掉结果数据库表设计互相牵扯反而做不完。2. 技术栈选型与2025年版本搭配实战2.1 为什么是SpringBoot Vue MyBatis MySQL这四件套是当前Java全栈开发最主流的基础组合。SpringBoot简化了Spring的配置内置Tomcat打jar包一键启动Vue作为渐进式前端框架组件化开发和响应式数据绑定非常适合管理后台这种表单表格密集的场景MyBatis胜在SQL可控写复杂查询时心里有底MySQL免费、轻量、生态成熟学生电脑跑起来毫无压力。有人会问为什么不用JPA我个人的经验是课设项目通常需要展示复杂查询能力比如“查询某部电影某天所有场次的剩余座位数”这种SQL用MyBatis写非常直观但用JPA写要么拼接QBC规范要么写Query注解反而绕。MyBatis的半自动特性对新手来说反而是优点——你至少知道SQL在执行什么。2.2 版本搭配是第一个大坑现在网上搜到的教程鱼龙混杂有教SpringBoot 2.3的有教SpringBoot 3.2的有教JDK 8的有教JDK 17的。我告诉你2025年的一个核心结论SpringBoot 3.x必须搭配JDK 17及以上SpringBoot 2.x可以兼容JDK 8。很多同学机器上装的是JDK 8然后照着新教程用Spring Initializr生成了SpringBoot 3.x项目一启动就报“UnsupportedClassVersionError”或者“IllegalArgumentException: Unsupported class file major version”。这就是“springboot版本太高”这个热搜词的来源。我的推荐组合有三个你按自己的JDK版本选环境JDK版本推荐组合稳定保守型JDK 8SpringBoot 2.7.18 MyBatis Spring Boot Starter 2.3.2 MySQL Connector/J 8.0.33中规中矩型JDK 11SpringBoot 2.7.18 同上2025主流型JDK 17SpringBoot 3.2.x MyBatis Spring Boot Starter 3.0.3 MySQL Connector/J 8.1.0这里特别注意SpringBoot 3.x对应的是Jakarta EE规范包名从javax.变成了jakarta.所以你的实体类里import javax.persistence要改成import jakarta.persistence。很多同学项目启动报ClassNotFoundException就是因为这里。另外MyBatis的starter版本和SpringBoot版本必须匹配不能拿2.x的mybatis-spring-boot-starter配SpringBoot 3.x会直接启动失败。2.3 开发环境的完整准备清单第一步是装JDK并配好JAVA_HOME。第二步装Maven把settings.xml里的镜像换成阿里云镜像不然下载依赖能让你怀疑人生。第三步装MySQL这里建议直接用MySQL 8.0版本。安装完以后记得在命令行里用mysql -u root -p测试一下很多人用Navicat连不上都是因为root用户默认的加密插件是caching_sha2_passwordNavicat老版本不支持。解决办法是在MySQL里执行ALTER USER rootlocalhost IDENTIFIED WITH mysql_native_password BY 你的密码; FLUSH PRIVILEGES;新版本Navicat其实已经支持这个插件了但我建议你在后端JDBC连接串里加上allowPublicKeyRetrievaltrue这个参数能解决很多诡异的连接报错。第四步装Node.js。Vue 3项目建议Node 16.20以上如果非要跑老项目用的Vue 2Node 14-16都可以。装完Node以后npm源改成淘宝镜像源安装速度能提升一个量级npm config set registry https://registry.npmmirror.com最后一步是装IDE。后端用IntelliJ IDEA社区版足够前端直接用IDEA打开Vue项目也行但是装一个VS Code专门写前端效率更高配合Volar插件做Vue3语法提示。3. 数据库设计与核心表结构3.1 设计原则先画业务流转图所有表的设计都围绕一条主线用户买了一张票这张票属于某部电影的某个场次场次安排在某个影厅影厅里有具体的座位。按这个逻辑拆出来的核心表是user用户表、movie电影表、hall影厅表、session场次表、seat座位表、orders订单表、order_item订单明细表、comment评论表。你发现没有我把订单分成了主表和明细表。这是因为一个订单可以包含多张票比如一次买3张连座订单主表存总金额和状态明细表存每一张票对应哪个场次哪个座位。分开的好处是以后要退款单张票也好操作。3.2 核心建表SQL参考我把最关键的五张表给你写出来字段类型和注释都标好你建库的时候可以直接抄CREATE TABLE movie ( id int NOT NULL AUTO_INCREMENT, title varchar(100) NOT NULL COMMENT 电影名称, cover_url varchar(255) DEFAULT NULL COMMENT 封面图地址, video_url varchar(255) DEFAULT NULL COMMENT 预告片地址m3u8格式, director varchar(50) DEFAULT NULL COMMENT 导演, actors varchar(255) DEFAULT NULL COMMENT 主演逗号分隔, genre varchar(50) DEFAULT NULL COMMENT 类型动作/科幻/爱情, duration int DEFAULT NULL COMMENT 时长分钟, release_date date DEFAULT NULL COMMENT 上映日期, description text COMMENT 剧情简介, status tinyint DEFAULT 1 COMMENT 1上架 0下架, create_time datetime DEFAULT CURRENT_TIMESTAMP, update_time datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (id), KEY idx_genre (genre), KEY idx_status (status) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT电影表;电影表的关键是cover_url和video_url这两个字段千万不要把图片或视频以二进制形式存进数据库只存路径。文件本身可以放本地目录或Nginx静态目录。CREATE TABLE session ( id int NOT NULL AUTO_INCREMENT, movie_id int NOT NULL COMMENT 电影ID, hall_id int NOT NULL COMMENT 影厅ID, start_time datetime NOT NULL COMMENT 开场时间, end_time datetime NOT NULL COMMENT 散场时间, price decimal(10,2) NOT NULL COMMENT 票价, remain_seats int DEFAULT 0 COMMENT 剩余座位数, status tinyint DEFAULT 1 COMMENT 1正常 0已取消, PRIMARY KEY (id), KEY idx_movie_id (movie_id), KEY idx_start_time (start_time) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT场次表;这里有一个设计细节remain_seats字段是冗余字段它可以用“影厅总座位数-已售座位数”算出来但为什么要单独存因为用户点进场次列表时需要展示“还剩多少座”如果每次都实时count订单明细数据库压力会大很多。冗余字段换查询性能这在互联网开发里很常见。CREATE TABLE seat ( id int NOT NULL AUTO_INCREMENT, session_id int NOT NULL COMMENT 场次ID, row_no int NOT NULL COMMENT 排号, col_no int NOT NULL COMMENT 列号, status tinyint NOT NULL DEFAULT 0 COMMENT 0空闲 1锁定 2已售, order_item_id int DEFAULT NULL COMMENT 锁定或售出时对应的明细ID, update_time datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (id), UNIQUE KEY uk_session_seat (session_id, row_no, col_no) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT座位表;座位表是整个系统设计的亮点也为每一个场次预先创建一排排的座位记录。比如一个8排10列的影厅有80个座位创建场次的时候就批量insert 80条记录。status字段用0/1/2表示空闲、锁定和已售。注意这里加了唯一索引uk_session_seat保证同一个场次同一个座位只能有一条记录。订单表建议这样建CREATE TABLE orders ( id int NOT NULL AUTO_INCREMENT, order_no varchar(32) NOT NULL COMMENT 订单号, user_id int NOT NULL COMMENT 用户ID, session_id int NOT NULL COMMENT 场次ID, total_amount decimal(10,2) NOT NULL COMMENT 总金额, status tinyint NOT NULL DEFAULT 0 COMMENT 0未支付 1已支付 2已取消 3已退款, pay_type tinyint DEFAULT 0 COMMENT 支付方式0模拟支付, create_time datetime DEFAULT CURRENT_TIMESTAMP, pay_time datetime DEFAULT NULL COMMENT 支付时间, PRIMARY KEY (id), UNIQUE KEY uk_order_no (order_no), KEY idx_user_id (user_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT订单表;订单号order_no必须唯一用UUID去掉横线或者用时间戳加随机数生成都行。status状态机设计要清晰未支付→已支付→已退款未支付→已取消。在课设里可以把支付做成一个模拟接口点击支付按钮后直接把状态改成已支付。3.3 关于更新子查询的一个坑这节解决热搜词里“mysql中更新子查询”的问题。假设你要把某个场次的所有座位重置为空闲可能会写UPDATE seat SET status 0 WHERE session_id (SELECT id FROM session WHERE movie_id 1);如果session表有多条movie_id1的记录这个SQL直接报错Subquery returns more than 1 row。但如果写成IN也报错UPDATE seat SET status 0 WHERE session_id IN (SELECT id FROM session WHERE movie_id 1);这不是语法问题而是MySQL不允许在更新同一个表时子查询里又引用了目标表相关的数据。解决办法是包一层临时表UPDATE seat SET status 0 WHERE session_id IN ( SELECT id FROM ( SELECT id FROM session WHERE movie_id 1 ) AS tmp );多包一层子查询MySQL就会生成一个派生表就不会报1093错误了。这个技巧在实际工作中也经常用到我印象里踩过好几次了包括清理垃圾数据、批量更新关联状态之类的场景。4. 后端核心功能与MyBatis实践4.1 项目包结构与分层规范后端项目结构是这种经典的分层src/main/java/com/example/cinema ├── controller // 接口层 │ ├── UserController.java │ ├── MovieController.java │ ├── SessionController.java │ └── OrderController.java ├── service // 业务层 │ └── impl ├── mapper // MyBatis的Mapper接口 ├── entity // 数据库实体类 ├── dto // 前端交互的传输对象 ├── common // 统一结果封装、全局异常处理 └── config // 跨域等配置类controller只做参数接收和结果返回不写业务逻辑service负责业务规则mapper只做数据访问。这个分层不是形式主义最大的好处是后期加功能的时候你知道该改哪个文件。比如要给订单加个“取消订单”功能你只需要在service里加一个方法controller里加一个接口mapper里写一条UPDATE语句改动边界非常清晰。4.2 SpringBoot整合MyBatis的配置细节在pom.xml里引入依赖时注意三个核心依赖dependency groupIdorg.mybatis.spring.boot/groupId artifactIdmybatis-spring-boot-starter/artifactId version2.3.2/version /dependency dependency groupIdcom.mysql/groupId artifactIdmysql-connector-j/artifactId scoperuntime/scope /dependency在application.yml里配置数据源和MyBatisspring: datasource: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3306/cinema?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/ShanghaiuseSSLfalseallowPublicKeyRetrievaltrue username: root password: 123456 jackson: date-format: yyyy-MM-dd HH:mm:ss time-zone: Asia/Shanghai mybatis: mapper-locations: classpath:mapper/*.xml type-aliases-package: com.example.cinema.entity configuration: map-underscore-to-camel-case: true log-impl: org.apache.ibatis.logging.stdout.StdOutImpl我重点讲三个点第一mapper-locations必须指向你的XML文件目录。很多人把Mapper接口写了XML也写了结果运行时报Invalid bound statement (not found)十有八九就是这个路径没配对。我把XML文件放在resources/mapper目录下所以写的是classpath:mapper/*.xml。第二map-underscore-to-camel-case这个配置非常关键。数据库字段create_time要映射到实体的createTime属性如果没有这个配置你得在SQL里每个字段都写AS别名累死。打开这个配置后MyBatis自动做驼峰转换。第三log-impl配置成StdOutImpl控制台会直接打印SQL语句。这个对调试来说很实用尤其是你想看MyBatis到底执行了什么SQL、传了什么参数的时候。我还配过MyBatis Log Plugin这个IDEA插件效果更直观SQL会用不同颜色标出来查问题非常方便。4.3 Mapper XML的写法动态SQL与批量操作下面给一个电影查询的Mapper示例包含动态SQL和结果映射的完整写法select idselectMovieList resultTypecom.example.cinema.entity.Movie SELECT * FROM movie where if testkeyword ! null and keyword ! AND (title LIKE CONCAT(%, #{keyword}, %) OR director LIKE CONCAT(%, #{keyword}, %)) /if if testgenre ! null and genre ! AND genre #{genre} /if if teststatus ! null AND status #{status} /if /where ORDER BY release_date DESC LIMIT #{offset}, #{pageSize} /select这里用了一个很常用的查法把关键词、类型、状态作为可选条件哪个不为空哪个就生效。如果不用 标签你得自己拼WHERE 11这种写法能跑但很丑。 标签会自动去掉多余的AND或OR这才是标准的动态SQL写法。批量插入座位也很有代表性。创建场次时要给这个场次生成所有座位记录用foreach循环处理比较好insert idbatchInsertSeats INSERT INTO seat (session_id, row_no, col_no, status) VALUES foreach collectionseats itemseat separator, (#{seat.sessionId}, #{seat.rowNo}, #{seat.colNo}, 0) /foreach /insert批量插入比一条条insert效率高得多。我见过有人用for循环调单条insert100个座位就要执行100次数据库请求而批量插入一条SQL就搞定。这里还要注意一个问题热搜词“mybatis 单个数字字符比较”。在XML的if判断里如果字段类型是char直接写 可能会出问题因为MyBatis在做OGNL表达式比较时把‘1’作为字符和数字1比较可能不相等。解决办法是写 外单内双把1作为字符串比较。这种坑很小但真的能卡人半天面试时跟面试官聊这个细节能看出你是真的写过代码而不是背出来的。4.4 购票与下单的事务处理现在说整个系统最核心的部分用户选中座位后点击“确认支付”后端到底做了什么。普通思路是三步第一步查座位是否空闲第二步给座位加锁/改状态第三步创建订单。这三步必须是一个整体中间任何一步失败都要全部回滚不然会出现座位卖了但订单没生成或者订单生成了但座位没锁住的脏数据。后端代码用Transactional注解Transactional(rollbackFor Exception.class) public Order createOrder(OrderCreateDTO dto) { // 1. 查询场次信息校验场次是否存在且未取消 Session session sessionMapper.selectById(dto.getSessionId()); if (session null || session.getStatus() ! 1) { throw new BusinessException(场次不存在或已取消); } // 2. 逐个锁定座位 for (Integer seatId : dto.getSeatIds()) { int rows seatMapper.lockSeat(seatId, session.getId()); if (rows 0) { throw new BusinessException(座位已被占用请重新选择); } } // 3. 创建订单主表 Order order new Order(); order.setOrderNo(generateOrderNo()); order.setUserId(dto.getUserId()); order.setSessionId(session.getId()); order.setTotalAmount(session.getPrice().multiply(new BigDecimal(dto.getSeatIds().size()))); order.setStatus(0); orderMapper.insert(order); // 4. 创建订单明细 // ... // 5. 扣减场次剩余座位数 sessionMapper.decreaseRemainSeats(session.getId(), dto.getSeatIds().size()); return order; }座位枷锁的SQL是关键是一条带条件的UPDATEUPDATE seat SET status 1, order_item_id #{orderItemId} WHERE id #{seatId} AND session_id #{sessionId} AND status 0这条SQL的巧妙之处在于WHERE条件里带了status 0如果这个座位已经被别人锁了或者买了UPDATE影响的行数就是0我们通过affected rows就能判断锁座是否成功。这其实是一种乐观锁的思想不需要额外加数据库行锁就能防止超卖。事务注解这里rollbackFor Exception.class一定要写。因为Spring默认只回滚RuntimeException如果代码里抛的是自定义的BusinessException继承自Exception不加这个参数事务不会回滚。4.5 MyBatis缓存机制与脏读陷阱热搜词里有“mybatis缓存”这里我讲得简单点因为课设项目里用到缓存的机会不多但面试爱问。MyBatis有两级缓存一级缓存是SqlSession级别的默认开启二级缓存是Mapper级别的需要手动开启。一级缓存在同一个SqlSession里执行两次相同SQL会直接命中缓存。但Spring整合MyBatis后每次查询都新建一个SqlSession所以一级缓存基本没啥感知。二级缓存开启后是Mapper级别的多个SqlSession可以共享。但要注意如果开了二级缓存表数据被其他会话修改了缓存不会自动失效就可能读到脏数据。我的建议是这个电影院购票系统不要开二级缓存。因为座位和订单的数据实时性要求非常高缓存带来的性能提升在这个小项目里根本体现不出来反而会引入各种不一致问题。面试时能把缓存的工作原理和为什么不适合这个场景说清楚比盲目开启要有说服力得多。5. 前端核心功能与Vue实现细节5.1 初始化Vue项目与Element组件库前端我建议直接用Vue 3 Vite Element Plus。Vite启动速度快配置比vue-cli简单2025年的新项目再回头用Vue CLI反而奇怪。创建项目命令npm create vitelatest cinema-front cd cinema-front npm install npm install vue-router4 axios element-plus element-plus/icons-vue项目结构src ├── api // 放所有接口请求 ├── assets ├── components // 公共组件如选座组件、文件上传组件 ├── router // 路由配置 ├── store // 状态管理低配版用Pinia ├── views // 页面按admin和user分目录 ├── utils // axios封装、工具函数 ├── App.vue └── main.js5.2 axios封装与跨域代理配置每个页面直接调axios不是不行但统一封装之后处理token、错误提示、状态码判断会方便很多。下面是一个简化版的request.jsimport axios from axios import { ElMessage } from element-plus const request axios.create({ baseURL: /api, timeout: 10000 }) // 请求拦截器自动带上token request.interceptors.request.use(config { const token localStorage.getItem(token) if (token) { config.headers[Authorization] token } return config }) // 响应拦截器统一处理后端返回的code request.interceptors.response.use( response { const res response.data if (res.code ! 200) { ElMessage.error(res.msg || 请求失败) return Promise.reject(new Error(res.msg)) } return res.data }, error { ElMessage.error(error.message || 网络异常) return Promise.reject(error) } ) export default requestbaseURL设为/api然后通过Vite的代理把/api开头的请求转发到SpringBoot的8080端口这样前后端联调时就绕过了跨域问题// vite.config.js export default defineConfig({ server: { port: 3000, proxy: { /api: { target: http://localhost:8080, changeOrigin: true, rewrite: (path) path.replace(/^\/api/, ) } } } })后端接口路径如果是/movie/list前端请求路径就是/api/movie/list。代理转发时把/api去掉最终请求到后端的是/movie/list。这个方案开发时很平稳不需要后端做任何CORS配置。如果你后端非要自己配跨域也有办法写一个WebMvcConfigurerConfiguration public class CorsConfig implements WebMvcConfigurer { Override public void addCorsMappings(CorsRegistry registry) { registry.addMapping(/**) .allowedOriginPatterns(*) .allowedMethods(GET, POST, PUT, DELETE, OPTIONS) .allowCredentials(true) .maxAge(3600); } }两种方案选一种就行开发环境建议用Vite代理省心部署到服务器时再用Nginx反向代理同样可以解决跨域。5.3 路由配置与登录拦截路由这块的核心需求是未登录用户不能访问个人中心和订单页面管理员才能进后台管理页面。用vue-router的全局前置守卫实现router.beforeEach((to, from, next) { const token localStorage.getItem(token) if (to.meta.requiresAuth !token) { next(/login) return } if (to.meta.role admin) { const role localStorage.getItem(role) if (role ! admin) { next(/) return } } next() })路由配置里加meta信息{ path: /admin, component: AdminLayout, meta: { role: admin }, children: [ { path: movies, component: AdminMovieList }, { path: sessions, component: AdminSessionList } ] }5.4 选座组件实现思路选座页是这个项目前端最复杂的部分。影院座位图本质上是一个二维网格每个座位有3个状态空闲、已选、已售。我建议用CSS Grid布局渲染template div classseat-map :style{ gridTemplateColumns: repeat(${cols}, 40px) } div v-forseat in seatList :keyseat.id classseat :classseatClass(seat) clickhandleSeatClick(seat) {{ seat.rowNo }}排{{ seat.colNo }}座 /div /div /template script setup const seatClass (seat) { if (seat.status 2) return sold if (selectedSeats.value.some(s s.id seat.id)) return selected return available } const handleSeatClick (seat) { if (seat.status 2) return const index selectedSeats.value.findIndex(s s.id seat.id) if (index -1) { selectedSeats.value.splice(index, 1) } else { if (selectedSeats.value.length maxSelect) { ElMessage.warning(最多选择5个座位) return } selectedSeats.value.push(seat) } } /script style scoped .seat-map { display: grid; gap: 8px; max-width: 600px; } .seat { width: 40px; height: 40px; display: flex; align-items: center; justify-content: center; background: #e4e7ed; border-radius: 6px; cursor: pointer; font-size: 12px; } .seat.selected { background: #409eff; color: #fff; } .seat.sold { background: #f56c6c; cursor: not-allowed; } /style核心逻辑从后端拉取当前场次的座位列表status2表示已售点击状态为2的座位直接return点击可选座位时判断是否已在selectedSeats数组中有则移除取消选择没有则加入并校验数量。5.5 预告片m3u8播放方案“vue播放m3u8”是我看到热搜词里比较高频的。m3u8是HLS协议使用的视频索引文件它本身不是视频里面存放的是一系列.ts视频分片的地址。浏览器原生video标签不支持直接播放m3u8所以需要引入库。推荐使用hls.js。安装后封装一个VideoPlayer组件template video refvideoRef controls autoplay classvideo-player/video /template script setup import Hls from hls.js import { ref, onMounted, watch } from vue const props defineProps({ src: { type: String, required: true } }) const videoRef ref(null) const playVideo () { const video videoRef.value if (Hls.isSupported()) { const hls new Hls() hls.loadSource(props.src) hls.attachMedia(video) hls.on(Hls.Events.MANIFEST_PARSED, () video.play()) } else if (video.canPlayType(application/vnd.apple.mpegurl)) { // Safari浏览器原生支持 video.src props.src video.play() } } onMounted(playVideo) watch(() props.src, playVideo) /scripthls.js的原理是前端拉取m3u8索引文件解析出所有ts分片地址然后用MediaSource ExtensionsMSE把分片拼接成连续的视频流给video标签播放。你只要后端把视频文件放在Nginx静态目录或者提供一个能返回m3u8的接口前端就能播。如果只是需要一个能跑的Demo也可以直接用video.js加videojs-contrib-hls插件不过现在videojs-contrib-hls已经不维护了新项目还是建议直接用hls.js轻量而且社区活跃。6. 常见问题与排查技巧实录6.1 数据库连接与启动类问题现象一启动报Communications link failure或者Access denied for user排查思路先确认MySQL服务启动了没有然后确认用户名密码对不对。一个很容易踩的坑是MySQL 8.x的驱动类名是com.mysql.cj.jdbc.Driver不是com.mysql.jdbc.Driver。如果你用的是老配置直接报ClassNotFoundException。现象二时区问题报错Server returns invalid timezone. Go to Advanced tab and set serverTimezone property。解决JDBC连接串加上serverTimezoneAsia/Shanghai。有人说不加也能跑那多半是你的MySQL和系统默认时区碰巧一致加上的话最稳妥。现象三中文乱码解决建库时指定utf8mb4连接串加characterEncodingutf8。MySQL里建库要专门设置字符集因为默认可能是latin1。6.2 SpringBoot版本与依赖冲突这个项目最经典的报错是springboot版本太高导致mybatis-spring-boot-starter版本不匹配。我用一张速查表说明症状原因解决方案启动报Invalid bound statementmapper-locations路径不对或XML不在resources下把XML文件移到resources/mapper目录检查配置启动报EmptyResultDataAccessException查询结果为空但代码用selectOne接收改用selectList或判断结果长度启动报Caused by: java.lang.NoClassDefFoundError: javax/servlet/...SpringBoot 3.x下用了2.x的starter统一升级mybatis starter到3.x接口无法访问请求404controller路径没写对或未加RestController检查类上的RequestMapping和方法上的GetMapping路径字段返回null实体类属性名与数据库列名不一致开启map-underscore-to-camel-case或写resultMap还要提醒一下生产部署时的坑。如果你把SpringBoot项目打包成jar然后想用java -jar直接跑记得打包时排除掉测试不然单元测试没写也能给你构建失败。在pom.xml里加plugin groupIdorg.springframework.boot/groupId artifactIdspring-boot-maven-plugin/artifactId configuration skiptrue/skip /configuration /plugin6.3 Vue打包后布局异常的排查热搜词“vue 打包后 布局异常”基本是两类问题。第一类是部署到Nginx后白屏或者刷新后404。这是因为Vue是单页应用路由默认用history模式页面跳转靠前端路由实现。部署后用户访问 /movie/1 这个地址时Nginx去磁盘找movie/1目录找不到就返回404。解决办法是改路由为hash模式或者配置Nginx的try_files全部指向index.htmllocation / { try_files $uri $uri/ /index.html; }最简单的做法是Vue路由里直接写createWebHashHistory()地址栏会带#号刷新不会404课设演示时更省心。第二类是打包后CSS样式错乱、背景图丢失。这是因为Vite的默认base是/部署到子目录时静态资源路径不对。修改vite.config.js里的baseexport default defineConfig({ base: ./, })这样生成的路由就是相对路径部署到任何子目录都能找到资源文件。6.4 MyBatis高频报错实战排查踩坑一MyBatis中if标签的字符串和数字比较我已经在4.3里讲过char类型和数字比较的问题。这里再补充一个实际案例如果status字段是varchar类型判断时应该这样写if teststatus ! null and status ! AND status #{status} /ifservice层接收参数时保证status是字符串类型这样就不会有类型不一致的问题。如果前端传数字1后端接收时是IntegerXML里的字段也是Integer那直接相等判断没问题。记住一个原则尽量保持Java类型和数据库类型一致不要做过多的隐式类型转换。踩坑二MyBatis Log Plugin插件配置这个IDEA插件可以像打印日志一样直接在控制台看到SQL参数值。安装后在MyBatis配置里开启StDOUT日志输出就能看到Preparing:和Parameters:两行一眼看出SQL绑定了什么参数。排查参数没传对的问题特别快。踩坑三使用MyBatis进行批量写操作时主键回填批量插入座位后如果需要拿到每个座位自动生成的id要在insert标签加useGeneratedKeys和keyPropertyinsert idbatchInsertSeats useGeneratedKeystrue keyPropertyid这样插入的对象集合里每个对象的id属性会被回填。但要注意MySQL的批量插入是支持这个功能的其他数据库不一定支持。6.5 权限与安全细节最后讲讲权限和登录安全问题这是很多课设项目做得最敷衍的地方。一个合格的登录逻辑是前端把用户名密码发给后端后端用MyBatis查询用户然后用BCrypt算法比对密码哈希。很多同学直接明文存密码虽然课设不涉及真实生产环境但数据库被导出后所有用户密码泄露面试聊起来也很尴尬。Spring Security的BCryptPasswordEncoder可以单独拿来用不需要引入整个Security框架BCryptPasswordEncoder encoder new BCryptPasswordEncoder(); // 注册时加密 String encodedPwd encoder.encode(rawPassword); // 登录时校验 boolean matches encoder.matches(rawPassword, user.getPassword());登录成功后后端生成一个token返回给前端。最简单的方案是用JWT也可以用UUID生成随机字符串存在MySQL的token表里。前端拿到token后存在localStorage每次请求放在Header里。后端写一个拦截器或过滤器从Header里解析token能解析出来就放行解析不出来就返回401。这个token方案虽然简单但它区分了“登录用户”和“匿名用户”没有它后面的“我的订单”接口任何人都能访问那就成了脱裤子放屁的演示项目。6.6 MySQL索引与查询优化我在建表SQL里已经加了几个索引——电影表的genre和status场次表的movie_id和start_time座位表的联合唯一索引。这种量级的项目索引够用即可不需要过度设计。但有一点要提醒如果写SQL时对索引列做了函数操作索引就失效了。比如查询某天上映的电影-- 这个查询会导致idx_start_time索引失效 SELECT * FROM session WHERE DATE(start_time) 2025-06-01;原因是对start_time用了DATE函数MySQL无法使用索引。正确做法是范围查询SELECT * FROM session WHERE start_time 2025-06-01 00:00:00 AND start_time 2025-06-02 00:00:00;这个也算高频基础面试题做了这个项目你就能讲清楚索引为什么会失效怎么避免失效。写在最后按照1-6的顺序把一个基于SpringBootVueMyBatisMySQL的电影院购票系统从零到一搭出来你的全栈能力会上一个台阶。我自己的体会是这类项目最怕的不是代码写不出来而是被环境问题耗光信心。我自己早年做项目时光是SpringBoot版本不兼容就折腾了一周后来才明白版本搭配要优先于功能开发。你先定JDK再定SpringBoot版本再定MyBatis starter版本最后再碰前端Node的版本这套顺序不要乱。最后再分享一个启动项目的顺序建议先启动MySQL确认数据库能连上再启动SpringBoot后端看到控制台打印“Tomcat started on port(s): 8080”后再启动前端。前端npm run dev跑起来以后先访问登录页走一遍注册登录流程确认能拿到token再去开发业务页面。只要登录这一环通了整个系统的地基就稳了后面再遇到Bug一步一步断点调试都比推倒重来要强。