ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

SpringBoot+Vue影院购票系统毕设实战:从数据库设计到选座并发处理

SpringBoot+Vue影院购票系统毕设实战:从数据库设计到选座并发处理 每年到毕设季影院购票系统都是 Java Web 选题里的常客。原因很简单它业务链条完整从用户注册、电影列表、选座下单到支付回调每一环都有明确落点拿来写论文、做答辩都很容易讲清楚。但真要把一个 SpringBoot Vue 的影院购票平台从零搭起来让它跑得起来、讲得明白、还能应对老师追问里面的细节比想象中多得多。这篇内容就把我完整做过一轮的项目源码、SQL 脚本、接口文档拆开讲一遍重点放在别人文档里不会写的地方表结构怎么设计才不给自己挖坑、选座时并发怎么处理、接口文档写到什么程度答辩才加分、前端打包丢进 SpringBoot 的经典坑有哪些。适合正在做 Java Web 毕设、或者想拿一个完整项目练手的人照着这条线走能少走很多弯路。1. 做一个影院购票系统需要先想清楚哪些事1.1 项目定位毕设项目的重点不是创新是完整闭环很多同学一上来就想加各种花哨功能比如推荐算法、弹幕、会员等级结果做到一半发现核心链路都没通最后熬夜补漏洞。影院购票系统这种选题本质是考察你能不能把一个真实业务场景用技术完整落地。所谓完整就是从用户打开页面到订单落库、支付状态更新、座位状态变化每个环节都能对得上。我建议把功能边界先划清楚前台用户模块注册登录、电影浏览、场次选择、选座、下单支付、订单查询、后台管理模块电影管理、影厅管理、排片管理、订单管理、用户管理、公共模块首页轮播、搜索、接口鉴权、异常处理。把这些做完项目已经是一个能自洽的系统了。多余的功能等核心跑通再说宁可用一个深度的选座逻辑去体现工作量也别用十个半成品堆页面。1.2 技术选型为什么是 SpringBoot Vue以及它们各自承担什么SpringBoot 加 Vue 的组合在近三年 Java Web 毕设里几乎是标配原因不只是“网上资料多”而是它天然满足两个需求后端快速构建业务接口前端独立管理交互状态。SpringBoot 让配置变得很轻内嵌 Tomcat一个 Jar 包就能跑Vue 的组件化适合做影院这种多状态交互页面尤其是选座界面和排片切换用组件拆分会清晰很多。前后端分离的设计意味着你的项目要同时考虑跨域、鉴权、接口约定和打包部署。很多人做毕设死在跨域上就是因为没理解“分离”两个字背后的职责划分。前端只负责渲染和收集操作后端只负责校验和返回数据中间通过 JSON 通信所以接口设计必须一开始就规范后面才不会返工。1.3 功能范围与模块拆解先画一张业务流转图再动手动手写代码之前我强烈建议先把业务流转在纸上画一遍。影院购票系统的核心链路其实是一条线用户选电影 - 选场次 - 选座位 - 生成订单 - 支付 - 更新座位和订单状态。这条线上还有两个隐性分支一是如果用户长时间不支付锁定的座位什么时候释放二是后台管理员排片之后前端场次数据从哪里来。根据这条链路模块可以拆成五个部分用户模块负责注册登录和 JWT 鉴权电影模块负责影片信息和图片上传排片模块负责把电影、影厅、时间组合成场次购票模块负责座位锁定、订单生成和支付状态更新管理模块负责后台数据维护。每个模块之间尽量用 service 层隔离不要在一个 Controller 里写所有逻辑否则后面查 bug 会非常痛苦。2. 数据库设计——这张表结构能决定你后面省多少事2.1 核心表设计与字段说明影院购票系统的表不算多但非常讲究关联关系。我最终稳定的版本一共用了七张表用户表、电影表、影厅表、场次表、座位表、订单表、订单座位关联表。这里有一个容易忽略的点座位和订单不是直接挂关系而是通过一张关联表中间过渡因为一个订单可能包含多个座位一张票对应多个场次座位状态。用户表sys_user字段为 id、username、password、nickname、phone、avatar、role、status、create_time。密码存的是 BCrypt 加密后的值不是明文老师一旦问安全问题这是一个很好的加分点。电影表film字段为 id、title、cover、director、actors、duration、type、release_date、description。影厅表hall核心字段是 id、name、row_count、col_count因为座位生成需要知道影厅的排数和列数这个设计直接决定了后面选座组件怎么写。2.2 座位状态与余票的处理思路座位是最容易写崩的地方。很多同学用一张座位总表保存所有影厅的所有座位然后靠状态字段区分是否被占用这种做法在影院场景足够用但要注意座位必须和场次绑定而不是和影厅绑定。同一个影厅在不同时间放映不同电影同一把椅子在不同场次下应该有不同的状态所以座位表要冗余一个 schedule_id 字段。更合理的设计是影厅只存行列信息排片时按场次动态生成座位记录也就是说一个场次对应一批座位记录每个座位行内保存 row 和 col并用状态字段区分可售、锁定、已售。这样选座查询就变成“查某场次下 status 为可售的座位列表”天然隔离了不同场次之间的状态冲突。余票数也不必单独存字段座位总行数减去非可售状态的数量就是余票必要时可以用 Redis 做实时计数但毕设项目里 SQL 统计就够了。2.3 SQL 脚本里的关键点与初始化数据SQL 脚本不是把建表语句扔进去就完事还要包含测试数据否则老师打开你的项目时看到空页面初始印象会差很多。初始化数据至少要准备两部以上热映电影、两个影厅、三个场次包含今天和明天的、一部分已注册用户、一张后台管理员账号。场次时间要注意别跟现实时间差太远否则演示时显示已开场会显得很刻意。脚本里还有几个细节值得说所有表都用 InnoDB 引擎utf8mb4 字符集关联字段统一用 BIGINT订单表金额字段用 DECIMAL(10,2) 而不是 FLOAT避免精度问题外键可以建但我建议逻辑关联即可因为外键在删除数据时会拖累操作毕设里反而容易在删除电影时触发外键约束报错。如果你用 Navicat 导出了 SQL记得检查有没有带入绝对路径和临时表结构。3. 后端实现SpringBoot 项目结构、鉴权与核心接口3.1 工程结构与统一返回体后端工程我建议按 controller、service、mapper、entity、common、config 分包其中 common 包放统一返回体、异常处理、工具类。统一返回体几乎是必须的因为在前后端分离的项目里前端需要一种可预测的响应格式来判断请求是否成功。我习惯用{ code: 200, message: success, data: {} }这种结构code 非 200 时前端统一弹错误提示。这里有个实操经验不要在每个 Controller 里手动 new Result 对象而是封装一个Result.success(data)和Result.error(msg)静态方法。再配合RestControllerAdvice做全局异常捕获把参数校验异常、业务异常、系统异常分别映射到不同的 code。这样前端联调时拿到的错误信息永远是可读的而不是一大段堆栈信息。Data public class ResultT { private Integer code; private String message; private T data; public static T ResultT success(T data) { ResultT result new Result(); result.setCode(200); result.setMessage(success); result.setData(data); return result; } public static T ResultT error(String message) { ResultT result new Result(); result.setCode(500); result.setMessage(message); return result; } }3.2 JWT 登录与接口权限控制登录鉴权我用 JWTJSON Web Token实现原因是它无状态、适合前后端分离也方便在答辩时讲“无状态认证”这个概念。用户登录成功后后端生成一个包含用户 id 和角色信息的 token前端把它存到 localStorage之后每次请求在请求头里带上Authorization: Bearer token。后端通过一个拦截器HandlerInterceptor校验 token根据 token 中解析出的用户信息决定接口能否访问。这里要注意管理员接口和用户接口要分开控制不能只校验“登录了没”还要校验“是不是管理员”。我通常用两个注解区分角色比如RequireAdmin和RequireLogin拦截器里通过反射判断方法上有没有对应注解。Student 的毕设一般不会涉及 Spring Security 那么重的安全框架自己写拦截器反而更好讲。JWT 还有个容易被问的问题token 过期了怎么办我建议设置一个合理的过期时间比如 2 小时前端在请求收到 401 时自动跳转到登录页。如果担心用户操作到一半被踢出可以用刷新 token但毕设里不做也完全没问题把过期策略说清楚就行。3.3 核心接口设计排片、选座、下单、支付回调接口设计上我建议以资源为核心而不是以页面为核心。比如“选座页面”不是一个接口它拆成“获取某场次座位列表”和“创建订单”两个接口。核心接口大致有这几个GET /api/film/list获取电影列表、GET /api/schedule/list?filmIddate获取场次、GET /api/seat/list?scheduleId获取某场次座位图、POST /api/order/create创建订单、POST /api/order/pay模拟支付、GET /api/order/myOrders查询个人订单、后台的POST /api/admin/schedule/save保存排片。下单接口是整套系统里最锻炼人的地方因为它涉及事务和并发。我的逻辑是前端把选中的座位 id 列表传过来后端先检查这些座位在当前场次下是否都处于可售状态然后把状态改成锁定再生成订单。锁状态和查状态必须放在同一个事务里并且要给座位记录加行锁SELECT ... FOR UPDATE否则两个用户同时点同一个座位时会出现超卖。支付环节不需要真的接入支付宝微信支付那是商业能力毕设里做一个模拟支付接口即可。模拟支付的正确做法是前端调POST /api/order/pay?orderIdxxx后端把订单状态从待支付改成已支付同时把对应座位的状态从锁定改成已售然后把支付时间和支付流水号写进订单表。注意这两个操作必须在一个事务里否则会出现“订单已支付但座位还是锁定”的不一致状态。4. 前端实现Vue 路由、状态管理与页面拆解4.1 项目初始化与路由结构前端我用的 Vue 2 Element UI 的组合因为 Vue 3 的生态在毕设场景下反而容易踩坑Element UI 对 Vue 2 的支持更稳定网上现成的组件库例子也更多。项目初始化用 Vue CLI 3/4/5 都可以注意 Node 版本和 CLI 版本要匹配否则脚手架都起不来。路由结构建议分两块客户端页面和管理端页面。客户端有首页电影列表、电影详情页、场次选座页、订单列表页管理端有登录页、电影管理、影厅管理、排片管理、订单管理。如果全部写在一个路由表里管理端的权限控制会很麻烦所以要在路由守卫里统一判断访问/admin开头的路由时如果当前用户角色不是管理员直接重定向回首页并清掉 token。动态路由是 Vue 面试里经常问的经典问题放在影院系统里的实际用途是后台菜单按角色生成。我的做法是在路由表里只保留静态公共路由管理员路由在用户登录后根据角色动态addRoutes。这块代码不多但答辩被问“权限控制怎么做”时能答出“静态路由 动态路由 路由守卫”三层结构会比只说“我在按钮上加了 v-if”要高级很多。4.2 选座组件的核心实现选座页面是整个前端最值得写的部分。影院座位本质上是一个二维数组行数从影厅表里拿列数从影厅表里拿每个座位的状态从座位接口拿。前端把座位渲染成一张网格点击可选座位时把它加入选中集合再次点击取消选中然后实时计算总价最后把选中的座位 id 数组随订单提交。状态管理上座位有三种状态可售、锁定、已售。已售座位要有明显的灰化样式锁定座位在页面上其实应该不可见或显示为已售因为别人锁定的座位对你来说就是不可选的。很多同学在这个细节上做得不对导致页面看起来座位乱七八糟。选中状态的座位单独用一个selectedSeats数组维护每次渲染时根据座位 id 判断是否高亮。还有一个容易被忽略的交互逻辑前端要限制最大选座数量比如一次最多选 5 张。这个限制一方面是为了演示效果另一方面是防止用户一次选太多导致接口超时。前端选中座位时弹提示后端下单接口里也要校验同一订单的座位数量上限和所有座位是否属于同一场次否则会出现“两张票一个在 1 号厅一个在 2 号厅”的奇葩订单。4.3 接口封装与状态管理前端调用后端接口不建议在每个页面里直接写 axios而是封装一个 request.js统一处理 baseURL、token 注入、响应拦截和错误提示。baseURL 在开发环境指向http://localhost:8080生产环境如果前端被打包进 SpringBoot 后baseURL 要改成空字符串走相对路径否则会出现请求 404 或者跨域问题。import axios from axios const request axios.create({ baseURL: process.env.VUE_APP_BASE_URL || , timeout: 10000 }) request.interceptors.request.use(config { const token localStorage.getItem(token) if (token) { config.headers[Authorization] Bearer token } return config }) request.interceptors.response.use( response { const res response.data if (res.code ! 200) { this.$message.error(res.message) return Promise.reject(new Error(res.message)) } return res }, error { if (error.response error.response.status 401) { localStorage.removeItem(token) location.href /login } return Promise.reject(error) } )Vuex 在毕设项目里用不用取决于项目复杂度。影院购票系统里真正需要全局共享的状态其实有限用户信息、token、购物车选中的座位是否要跨组件传递。我建议至少建一个 user module 保存登录态因为首页、选座页、个人中心都要读取当前用户信息避免每个页面都去 localStorage 里手动取值。5. 接口文档怎么写一份答辩能直接用、后端能照着做的文档5.1 接口文档的必备要素接口文档不是给机器看的是给人看的它的价值在于减少前后端联通的成本也让你在答辩时有一个明确的展示对象。很多同学做完项目根本不写接口文档或者只在代码注释里写两句话这是很吃亏的。一份合格的接口文档每个接口至少应该包含接口名称、请求方式、请求路径、请求参数表、响应示例、错误码说明。响应示例最好给出完整的 JSON包括成功和失败两种情况。比如创建订单接口成功时返回订单 id 和待支付金额失败时返回“座位已被占用”之类的提示。错误码表可以单独列在文档末尾常见的有 401 未登录、403 无权限、500 服务器异常、6001 参数校验失败、6002 库存不足等不要把错误码写得五花八门。我习惯的模板是表格加代码块混排。参数表用表格列字段名、类型、是否必填、说明响应示例用 JSON 代码块。接口文档不一定要用 Swagger 那一套工具用 Markdown 写一份清爽的文档完全够用重点在结构清晰。如果你的学校要求提交设计文档这个接口文档还能直接改造成系统设计章节的素材。5.2 三个容易写砸的地方第一个容易写砸的是请求参数和实际代码不一致。文档里写filmId代码里接收的是film_id文档里写POST代码里用的是PUT前后端联调时就会一直报错而且这种问题肉眼很难发现。解决的办法就是写文档时对着 Controller 源码逐一核对字段名用驼峰还是下划线必须统一毕设里我建议全部用驼峰并与后端实体字段保持一致。第二个问题是分页参数没有说明。列表接口电影列表、订单列表几乎都要做分页文档里要写清楚pageNum和pageSize的默认值和最大值还要写清楚响应里的total是总记录数还是总页数。很多同学前端分页组件拿不到正确总数就是因为文档里没写响应结构前端只能猜字段。第三个问题是没画接口调用时序。文档里可以在开头用简短文字描述核心流程的接口调用顺序比如登录接口获取 token - 电影列表接口拿到电影 id - 场次列表接口拿到 scheduleId - 座位列表接口 - 创建订单接口 - 支付接口。答辩时把这个流程在文档里指着讲一遍比纯口头描述清楚很多。6. 部署运行从 IDEA 到服务器本地跑通和打包发布的区别6.1 本地运行步骤与配置项本地跑通一个前后端分离项目核心是五步导入后端工程、配置数据库、启动后端、启动前端、打开浏览器。后端工程导入 IDEA 后第一步先检查 Maven 仓库是不是能正常下载依赖很多环境问题出在 Maven 镜像上建议换成阿里云镜像否则下依赖能等到天亮。然后修改application.yml里的数据库连接、Redis 连接如果用了、文件上传路径。前端启动命令就两个npm install和npm run serve。npm install 是最容易翻车的一步node_modules 装不上或者版本冲突很常见。建议用cnpm install或者设置 registry 为淘宝镜像源安装完再看 package.json 里有没有版本对不上的依赖。启动时如果报端口被占用改vue.config.js里的 devServer 端口即可。# 后端运行 mvn spring-boot:run # 前端运行 npm install npm run serve本地跑通并不代表项目做完了因为还有打包部署这一关。当你把前后端分别跑起来时其实用的是开发模式前端页面由 node 服务提供后端接口由 SpringBoot 提供它们通过代理解决跨域。一旦要提交成品就必须考虑前端打包后如何和后端一起运行。6.2 前端打包放进 SpringBoot 的经典坑最稳妥的生产部署方案有两种一种是把前端打包后的静态文件复制到 SpringBoot 的src/main/resources/static目录随 Jar 一起启动另一种是用 Nginx 部署前端SpringBoot 只提供接口。毕设演示推荐第一种因为一个 Jar 包就能跑不用在答辩机器上额外装 Nginx。前端打包时最容易犯的错是接口地址写死了开发地址。打包后的 JS 文件里如果还有localhost:8080那部署到服务器后必然请求失败。正确做法是 vue.config.js 里设置 publicPath 为相对路径接口请求用相对路径让浏览器自动走当前域名。同时要注意 Vue Router 的 history 模式和 hash 模式的区别打包放进 SpringBoot 里尽量用 hash 模式否则刷新页面会出现 404因为 SpringBoot 默认不会把带路径的请求转发到 index.html。还有一个细节打包时如果 static 目录里有旧文件要先清空再复制否则会残留旧的 JS 文件名导致浏览器加载到旧缓存。部署后如果页面白屏打开浏览器控制台十有八九是静态资源路径问题改 publicPath 后重新打包即可。7. 常见问题排查与毕设答辩避坑7.1 启动报错、端口占用、跨域等高频问题把我在实操中遇到的高频问题整理成一张速查表按“现象 - 原因 - 解决”的思路排查能省很多时间。现象常见原因处理方式后端启动报数据库连接失败MySQL 未启动、密码错误、库名不存在检查 MySQL 服务、核对 application.yml 的 jdbc url前端 npm run serve 报端口占用8080 被其他进程占用修改 vue.config.js 里的 port 或杀掉占用进程前端请求接口报跨域后端未配置 CORS 或请求地址写错后端加 WebMvcConfigurer 跨域配置或前端接口走代理登录成功但获取用户信息 401token 未传或过期检查请求拦截器是否带 Authorization 头上传图片后刷新页面图片消失图片路径配置不对后端配置静态资源映射或用 base64 存数据库打包后刷新页面 404Vue Router 用了 history 模式改成 hash 模式或 SpringBoot 配置 forward 到 index.html跨域问题的正确处理方式是后端全局配置 CORS而不是在前端随便加代理。如果 API 走/api前缀后端可以只放开/api路径的跨域管理端接口和用户端接口共用一个策略。开发时前端用代理转发也能解决但打包部署后代理就失效了所以一定要在后端做好跨域兜底。还有一种很隐蔽的问题数据库里的中文变成问号。这多半是连接串里没有加characterEncodingutf8或者建表时字符集不是 utf8mb4。解决方式是连接串加参数同时建表语句里明确指定 DDL 字符集SQL 脚本导入后检查一下表结构再开始测试。7.2 答辩时最容易被问到的几个点答辩不要只讲做了什么要准备“为什么这么做”。老师最常问的几个问题我提前列一下每个都值得提前准备好答案。第一个问题是“项目为什么用前后端分离有什么好处”不要只说“方便开发”要讲清楚职责分工、并行开发、部署灵活、接口复用这几个层面。第二个是“JWT 和 Session 有什么区别为什么选 JWT”要讲到无状态、分布式友好、token 可以跨域同时坦然承认 JWT 的缺点比如无法主动失效。第三个高频问题是“两个用户同时买同一个座位怎么办”这是最容易暴露真实水平的问题。如果只回答“前端先选先得”老师会觉得你压根没考虑过并发。正确的答案是后端在下单事务里对座位行加锁查询座位状态后又执行一次更新锁定利用数据库行锁保证同一时刻只有一个事务能锁住同一行同时订单创建和座位状态变更放在同一事务里任一环节失败全部回滚。最后还有一个家常问题“你这个项目有哪些不足”千万别只回答“没不足”。诚实地讲比如支付是模拟的、没有做压力测试、图片上传用的是本地存储而不是对象存储、没有做座位区域的差异化定价。这些“不足”只要给出可行的改进方向就全是加分项因为老师想看到的是你对自己项目的理解深度。7.3 个人经验与后续扩展思路做完一遍之后我的整体体会是毕设的难度从来不在技术多新而在你怎么把已知的东西组织得扎实。影院购票系统这个题真正拉开差距的地方就是数据库设计的合理性、事务与并发处理、以及接口文档的规范度。你把这三块打磨好即便其他功能写得普通答辩的观感也会明显不一样。关于扩展我觉得有两条线性价比很高。一条是接入对象存储做电影封面和预告片管理后端用 MinIO 提供服务前端可以配合播放器直接预览 mp4 或 m3u8 流媒体这也是现在企业项目里的常见形态写在“不足与改进”里非常有说服力。另一条是把余票计数从 SQL 统计换成 Redis 缓存加预扣减再配合消息队列做订单超时取消这样你的系统就从“能跑”提升到了“有高并发意识”的层级。这两块不需要真做能在论文里讲清楚设计已经足够亮眼。最后再分享一个小技巧项目源码里一定要留一个 README.md把启动步骤、账号密码、技术栈、模块说明写清楚。老师拿到项目后第一件事就是看 README能不能一分钟之内把项目跑起来直接影响他对整个工作的第一印象。我见过太多源码写得不错、但因为没写 README 而被误判为“跑不起来”的案例这个细节真的值得花二十分钟好好写。
返回列表