
每年做课程设计或者毕业设计的季节总有人问我有没有一个既能体现技术栈又不算太复杂、最好还带完整界面的项目。老实说这类需求里出镜率最高的就是各种管理系统而其中电影订票管理系统又是一直没凉过的经典选题。它的业务链路够完整——有用户端、有管理端、有选座、有订单、有支付状态流转比单纯的学生管理系统有看点又不像电商、外卖那种动辄几十张表的项目那样让人望而却步。我之前完整搭建过一版基于 Java Spring Boot Vue 的电影订票管理系统从数据库设计到前后端联调再到最后打包部署整套流程跑通之后整理出了一份可以照着抄的运行步骤。这篇就把项目的完整结构、核心代码逻辑、环境要求以及我实际踩过的坑一次说清楚。不管你是拿它当毕设题目、课程设计还是单纯想练一个全栈项目这篇文章应该都能帮你省下不少事。1. 这个订票系统到底做成了什么样先聊聊项目本身最终呈现的样子这样后面讲代码和步骤的时候你不至于一头雾水。整个系统分成了用户端和管理端两块是典型的前后端分离结构。用户端主要面向购票人群核心功能包括账号注册登录、浏览正在热映的电影、查看某部电影的场次和影厅信息、选择座位、提交订单、模拟支付、查看历史订单以及退票。管理端则给了运营人员维护整个系统的入口包括电影的上下架、影片分类管理、排片场次安排、影厅和座位管理、订单查询与处理等。我在实际的实训带教项目里加上了一个简单看点每场电影的真实座位状态会和前端座位图实时联动你选中的座位在后端会被标记为锁定别人在同一时间看不到这个座位。这个逻辑别看说起来简单真正把并发、锁、座位状态表串在一起之后整个项目的技术含量一下子就上来了——这也是很多简单版的订票系统里缺失的一块。项目的技术栈是这样的后端用 Spring Boot 2.x MyBatis-Plus MySQL前端用 Vue 3 Element Plus Axios认证用的是 JWT。权限上区分了普通用户和管理员接口层面做了拦截处理。另外为了方便演示没有引入过于复杂的消息队列和微服务Redis 在基础版本里也不是必需的——但如果你想把选座并发这块做得更漂亮一点我后面会讲怎么用它来优化。2. 技术选型和数据库设计的逻辑很多人一开始会犹豫要不要用 Spring Boot Vue 这套组合。我的评价是这是目前最适合一个人独立完成的中型项目组合。Spring Boot 的好处是启动快、生态成熟、社区资料多遇到问题基本一搜就有答案。MyBatis-Plus 又省去了大量手写 SQL 的重复劳动配合 Lombok 用起来非常顺手。Vue 3 Element Plus 做后台管理页面速度极快组件都是现成的省下来的时间足够你去打磨业务细节而不是死磕样式。数据库设计是整个系统的地基。我最终拆出了这些核心表用户表、电影表、影厅表、场次表、座位表、场次-座位关联表、订单表和订单明细表。其中最关键的是场次-座位关联表。它的存在是为了解决一个问题同一个影厅同一场次每个座位的状态必须单独跟踪状态分为可用、锁定、已售出。你不可能直接在座位表上改全局状态因为座位在不同场次里的占用情况是不一样的。场次-座位关联表的字段我大致是这样设计的主键 ID场次ID关联场次表座位ID关联座位表座位行号、座位列号冗余存储方便前端渲染状态0 可售、1 锁定、2 已售版本号用于乐观锁解决并发重复购买问题订单相关的处理我额外加了一张订单明细表因为一个订单理论上可能包含多张票虽然常见业务里一场一个人买一张但设计上留这个口子会让数据模型更合理。订单主表记录订单号、总金额、支付状态、创建时间、用户ID等订单明细表记录每张票对应的场次、座位、单价。这样一个结构下来所有业务都能闭环了用户选座时修改关联表里对应座位行的状态为锁定同时插入一条订单记录订单状态为待支付用户支付后订单状态改为已支付座位状态改为已售出退票则反向操作。在实际实现中我遇到过一个很有意思的问题电影表要不要单独存一个当前是否上映的字段还是通过场次表的日期来判断。我的选择是两套都保留——电影表里有一个上线状态字段用于列表展示同时场次的放映时间在业务层做二次校验。这样管理端下架一部电影场次也一并失效不会出现前端展示下线电影、后端还能下单的尴尬。3. 核心业务代码拆解选座、订单与支付状态机的实现3.1 选座接口为什么必须用事务和锁选座是整个系统里最容易写崩的地方。先看后端的核心 Service 方法逻辑。用户从前端提交座位行列号和场次ID后后端要做三件事校验场次是否存在、校验座位是否属于该场次、在事务里把座位状态从可售改成锁定。这里最关键的坑在于并发。两个用户同时在选同一个座位如果代码只是在 Java 层判断该座位是不是可售那在高并发下两个请求都可能读到可售状态然后一起改状态最后出现一票多卖。基础版本我用了数据库层的select ... for update加行锁来处理。逻辑上是先通过主键或者唯一索引锁住这行场次-座位记录再去做判断和更新。因为 InnoDB 的行锁机制在这里能保证同一行数据的修改操作是串行的。如果不想写原生 SQL用 MyBatis-Plus 也能实现乐观锁方案——在更新时加上版本号条件update ... set status1, versionversion1 where id? and version?受影响行数为 0 就说明座位已经被别人抢走了返回一个友好提示。乐观锁的优点是性能好、代码简单缺点是并发量特别高时会有大量无效更新请求但对订票系统的真实流量来说完全够用。前端座位渲染的逻辑同样有讲究。我采用的是前端用二维数组模拟座位图每个元素包含行号、列号和状态。渲染时根据后端返回的座位状态决定显示成可点选绿色、锁定中灰色、已售出红色。提交选座的时候前端把选中的座位行列号传给后端后端校验通过则返回成功同时前端本地把该座位标记为锁定。3.2 订单状态流转与超时未支付处理订票业务的订单状态不能只做两个值已支付、未支付。实际运营场景里还需要区分待支付已支付已取消已退票使用完毕。我实现了一个简单的状态机辅助类专门管状态流转的合法性。比如待支付只能流向已支付或者已取消已支付可以流向已退票但已取消不能再变成已支付。未支付订单的超时释放是另一个很多人容易忽略的点。我最初的版本里用户下了单不支付座位就永久锁着其它用户就永远买不了。后来改成了在订单表里加一个锁定到期时间字段并通过 Spring 的定时任务每五分钟扫描一次数据库把超过十五分钟仍未支付的订单状态改成已取消同时把对应座位的状态释放回可售。这个方案是演示级的够用方案如果你的项目想更进一步可以把释放动作改成用延迟队列来做。另外模拟支付不要过度设计。前端弹出一个支付确认对话框点击确认支付之后后端直接把订单状态改为已支付、座位状态改成已售出然后用Transactional保证这两步在同一个事务里。你看看整个流程会发现订单模块最核心的不是支付本身而是各个状态切换之间的数据一致性。3.3 管理端的排片逻辑管理端排片这块看起来是普通的 CRUD实际上有一个点很容易被遗漏。排片信息包括选哪部电影、选哪个影厅、什么时间放映、票价多少钱。这里需要校验同一影厅的放映时间不能重叠。我最早做的时候没加这个校验结果测试数据出现了一个影厅同时放两场电影的笑话——前端下拉框里同一时间能看到两部片子。解决方案也很简单。新增场次的时候在数据库层查询条件里把影厅ID、开始时间、结束时间做重叠判断。SQL 条件大致是目标场次开始时间在已有场次的时间段内或者结束时间在已有场次的时间段内或者已有场次完全被包在目标时间段内。这个逻辑写好之后配合前端在提交时做一次异步校验基本就能彻底避免这个问题。4. 前端架构与前后端联调的关键细节4.1 Vue 项目的目录划分和路由守卫前端项目我用 Vue 3 的 Composition API 来写配合 Vue Router 和 Pinia或者 Vuex看你顺手用哪个。目录结构上我把视图组件分成两个大目录views/user放用户端页面views/admin放管理端页面。公共组件放在components底下比如选座组件、电影卡片组件、订单状态标签组件。路由守卫这里值得单独拿出来说。因为接口层面已经用 JWT 做了认证前端再用路由守卫做一层拦截体验会好很多。我的做法是在 axios 请求拦截器里统一从 localStorage 取 token并加到请求头路由守卫里判断当前访问管理端页面时是否存在 admin 角色标识没有就直接跳回登录页。这样做的核心价值在于后端拦截是安全底线前端路由守卫是体验优化。两条线各管各的事缺一个都会出问题。只做前端不做后端别人绕过前端直接调接口就能拿到管理数据只做后端不做前端管理端页面刷新一下就会被弹回登录页体验很糟。4.2 axios 封装和响应码的约定前端请求封装我统一放到了utils/request.js。里面固定配置了基础 URL 和超时时间然后在响应拦截器里做统一解包。这里有一个非常实用的小技巧后端返回结构我用的是统一的 Result 对象包含code、message、data三个字段。axios 响应拦截器里判断code 200时直接返回data这样业务代码里就不用每个请求都去重复处理错误分支。错误类型我约定了这么几种未登录返回 401前端清空本地 token 并跳登录页参数校验失败返回 400前端直接把 message 内容弹出来业务错误返回 500比如座位已被抢、订单已超时等。这种统一的约定让前后端联调省了很多通信成本不然每加一个接口都要对一遍返回结构很痛苦。4.3 Element Plus 在管理端的使用心得管理端页面使用 Element Plus 的表格组件和表单组件比较省力。但我用下来有一个体会很深表格里的按钮不要把操作状态绑死在接口返回的一次性数据上最好在拿到后端数据之后由前端根据状态字段派生出一套可操作按钮集合。举个例子用户订单列表里如果订单是未支付状态就渲染去支付和取消订单两个按钮如果订单是已支付状态就渲染退票按钮但禁用去支付。这个逻辑如果用v-if分散写在模板里后期订单状态多了会非常乱。我把状态的展示规则和操作权限集中做成一个配置对象模板里只做一层循环渲染清晰很多。5. 从零到一跑起来完整运行步骤与验证方法这一节是纯实操按顺序走完项目就能在你本地跑起来。环境要求简单列一下JDK 1.8 或更高版本注意 17 以上需要处理 Lombok 兼容问题Maven 3.6 以上MySQL 5.7 或 8.0Node.js 14 以上推荐 16实测最稳前端包管理器 npm 或 pnpm 都可以第一步准备数据库。我提供的 SQL 脚本文件在项目根目录的db文件夹下直接执行source导入即可。导入后检查一下表是否成功生成默认会有 8 张表左右。如果你用的是 MySQL 8.0连接时要注意驱动版本和时区配置后面排查问题那里会细说。第二步修改后端配置文件。文件在src/main/resources/application.yml把数据库地址、账号、密码改成你自己的。同时建议把 JWT 的密钥改掉默认的密钥只是演示用的。第三步启动后端。在项目根目录执行mvn spring-boot:run或者在 IDE 里直接运行启动类的 main 方法。看到 Spring Boot 启动成功的日志说明后端已经就绪。默认端口是 8080可以在application.yml里改。第四步安装前端依赖。进入frontend目录执行npm install。这一步在部分网络环境下可能会很慢可以配置 npm 镜像加速。第五步启动前端开发服务器。执行npm run serve终端里会显示一个本地访问地址默认是localhost:8081。浏览器打开这个地址能看到系统的登录页说明前后端已经连通了。第六步验证核心流程。用预设的管理员账号登录管理端先添加一部电影和一个影厅排一场测试场次然后退出注册一个普通用户进入购票页面选择场次、选座、提交订单、模拟支付再去管理端查看订单状态。这整套流程走通项目就基本验证完毕了。6. 运行过程中最常见的六个问题与排查思路开发阶段大家问得最多的翻来覆去就是下面这几个问题我一个个说清楚。第一个是后端启动直接失败最常见原因是数据库连接失败导致的报错信息里一般能看到Access denied for user或者Communications link failure。前面的基本都是账号密码不对后面的基本是 MySQL 服务没启动或者application.yml里host/port写错了。第二个是 MySQL 8.0 的连接报错提示Unable to load authentication plugin caching_sha2_password。这是因为 MySQL 8 默认认证方式和 5.7 不一样。解决办法有两个一个是换用新版 MySQL 驱动另一个是连接串里加上useSSLfalseserverTimezoneAsia/Shanghai。我在项目里用的是第二种顺手把时区问题也解决了不然你往后查数据的时候会发现时间总是差八个小时。第三个是前端npm install时报错大部分情况是 Node 版本太高或者太低导致某个依赖编译不过。我的建议是先看报错信息和package.json里要求的 Node 版本范围实在不行就把 Node 降到 16 再试一次。很多看起来莫名其妙的依赖安装失败换版本之后都能解决。第四个是前端访问接口报跨域错误。开发环境下我在 Spring Boot 的配置类里加了全局的 CORS 配置允许localhost的指定端口跨域访问。如果你看到浏览器控制台有No Access-Control-Allow-Origin header这类报错查一下是不是 CORS 配置没生效或者前端请求是怎么发出去的。还有一种偷懒的办法是在前端vue.config.js里配代理服务器用/api前缀转发到后端地址开发体验更好。第五个是选座时明明看到座位可选提交时后端提示座位已被锁定。这个不是 bug而是别人在你之前提交了同一排座位后端通过状态校验把它拦下来了。但有一个值得检查的点你的前端在提交之后有没有立刻更新座位的本地状态。如果前端没更新用户连续点击同一个座位就会触发多次无效请求体验会变差。第六个是打包部署的问题。本地开发跑得很欢一到打包部署就崩最常见的就是静态资源路径问题。前端npm run build生成的dist目录直接丢给 Nginx 使用但要注意在vue.config.js里设置好publicPath为相对路径或者你的部署子路径否则打开页面会发现空白只有控制台里一堆资源 404。写在最后这个项目我从最初的单机版做到前后端分离版过程中最大的收获不是学会了某个框架的哪个 API而是理解了一个完整业务系统是怎么一层层组织起来的。如果你刚接触全栈我不建议一上来就堆技术先把这个项目的核心流程——选座、下单、支付、管理——老老实实跑通再去思考 Redis 缓存、消息队列、分布式锁这些进阶的东西。每加一层技术都要清楚它是用来解决什么问题的而不是为了简历上多一行字。有一点我觉得很重要的是哪怕是课程设计级别的项目也尽量按真实项目的规范来写统一返回结果、统一异常处理、状态用枚举、配置外置。这些习惯养成了后面进团队接真实业务会轻松非常多。我最后还想分享一个小技巧开发阶段把 Spring Boot 的日志级别调到 DEBUG打印一下 SQL看看 MyBatis-Plus 生成的语句和你预期的是否一致很多联调难题其实一眼就能看出来。