首页 / 资讯中心 / 文章详情

SpringBoot+Vue影院购票系统毕设实战:从数据库设计到并发一致性

SpringBoot+Vue影院购票系统毕设实战:从数据库设计到并发一致性 ★ FEATURED ARTICLE
每年毕业季Java Web方向的毕设题库里总有几道“常青树”题目影院购票系统就是其中之一。它之所以被反复选择是因为功能看起来足够直白——选电影、选场次、选座位、下单支付但真正动手做起来又牵扯到前后端分离、数据库约束、座位状态一致性和接口设计这些“说不清道不明”的细节。我手头正好整理过一套SpringBootVue影院购票系统的完整项目源码配套SQL脚本和接口文档前后端跑通的完整链路都摸过一遍。这篇内容不打算只贴目录而是把这套Java Web毕设项目为什么这么拆、表为什么这么建、接口为什么这么设计、跑起来的时候有哪些坑逐一讲清楚希望能让准备做类似题目的同学少走点弯路。适合看这篇文章的人我大致分三类一是正在做或准备做影院购票系统毕设的学生需要一套能真正理解、能改能扩展的骨架二是有Java基础但没完整做过前后端分离项目的人想借一个具体业务把SpringBoot和Vue串起来三是答辩前心里没底想系统复盘一遍项目设计逻辑的同学。无论你是哪一类下面的内容都按“需求拆解—工程搭建—数据库设计—核心业务—接口文档—部署演示”的顺序来保证看完能复述、能上手。1. 按毕设标准拆需求影院购票系统为什么能撑起一个前后端分离项目很多同学拿到题目后的第一反应是“这么普通的系统老师会不会觉得太简单”。实际上恰恰相反影院购票系统是一个被反复验证过的、非常适合做Java Web毕设的业务域。它的业务链条完整前端有选座交互、后端有订单和事务、数据库有多表关联还能拆出一个非常典型的并发场景同一个座位同一时间只能被一个人锁定。1.1 从题目覆盖的考核点反推系统边界毕设评分通常看四个方面功能完成度、技术栈合理性、代码规范性、文档完整性。影院购票系统在这四方面都能拿到较好的得分面。功能完成度用户注册登录、电影信息展示、场次选择、选座、生成订单、模拟支付、订单查询/取消这已经是一个完整的业务闭环。技术栈合理性后端SpringBoot负责接口和业务前端Vue负责页面交互MySQL存业务数据Redis和定时任务可作为加分项技术选型清晰不浮夸。代码规范性统一返回结果、全局异常处理、事务注解、分页插件这些都是面试官和答辩老师一眼能看出来的“专业感”。文档完整性SQL脚本、接口文档、README部署说明凑齐这三件套项目就已经具备“完整交付物”的形态。我当时在规划功能边界时特意把一些高频考点做了取舍支付功能做成模拟支付不接第三方支付SDK退款功能保留但只做逻辑退款不涉及真实资金流动会员等级、积分、优惠券这类“锦上添花”的功能一律砍掉把精力集中在购票主链路的稳定性上。这个判断很重要毕设不是功能越多越好而是每个做的功能都要能讲清楚设计理由。1.2 角色权限怎么定不做复杂权限但要能区分用户类型影院购票系统的用户角色一般分两种普通用户和管理员。管理员负责维护电影信息、影厅、场次和查看订单普通用户负责购票。权限控制不需要引入Spring Security重框架用拦截器或者简单的注解判断就能解决。我建议在用户表里加一个role字段用1表示管理员、0表示普通用户。后端写一个拦截器拦截所有非/api/admin/**的路径校验登录状态/api/admin/**路径再额外校验管理员身份。这样做的好处是代码量少逻辑直观答辩被问到“用户权限怎么控制”时能一句话说清楚而且后续想换成Spring Security也有清晰的迁移路径。2. 工程骨架怎么搭SpringBoot后端与Vue前端的分工与联调我见过不少毕设源码数据库表和接口写得都不错就是工程结构一团乱。controller里写SQL、service里拼HTML、前端一个页面几百行这种项目别说扩展连阅读都费劲。一个清晰的工程骨架是整套源码可维护的前提。2.1 后端工程结构按业务模块分包而不是按三层结构铺平很多教科书会告诉你“controller、service、mapper、entity四个包打天下”这种结构在demo里没问题但业务一多就会变成“controller包下20个类service包下30个类”。我习惯的做法是先横向分层再按业务模块分组。一个推荐的后端包结构com.cinema ├── common // 通用类返回结果、异常、工具类 ├── config // 配置类跨域、拦截器、MyBatis-Plus配置 ├── controller │ ├── admin // 管理员端接口电影管理、场次管理 │ └── user // 用户端接口查询、购票、订单 ├── service // 业务层接口实现 ├── mapper // 数据访问层 ├── entity // 数据库实体 ├── dto // 前端传入参数对象 └── vo // 返回给前端的视图对象这里有一点要特别说明dto和vo的分离是我做完几个项目后养成的习惯。前端传过来的参数对象不应该直接绑定到数据库实体上返回给前端的数据也不应该把实体所有字段一股脑暴露出去。比如创建订单时前端传scheduleId、seatIds这是OrderCreateDTO返回订单详情时需要包含电影名、影厅名、座位号这些冗余展示字段这是OrderDetailVO。字段隔离能避免很多“改了一个字段牵动整个接口”的尴尬。2.2 前端工程结构页面、组件、路由、状态请求分开前端我用的是Vue2 Element UI的组合这是目前Java Web毕设里兼容性最好、文档最全的方案。工程结构上我按照“页面—组件—路由—API”四个维度组织src ├── api // axios请求封装按模块拆文件 ├── assets // 静态资源 ├── components // 复用组件电影卡片、座位图等 ├── router // 路由配置含登录守卫 ├── store // 用户状态管理 ├── views // 页面首页、选座页、订单页、后台管理页 └── utils // 工具类时间格式化、状态映射axios封装是我特别想提醒的一点。不要在每次请求的地方直接this.$http.get(...)而是统一在api目录里定义方法。比如// api/order.js import request from /utils/request export function createOrder(data) { return request({ url: /api/order/create, method: post, data }) }这样做的直接好处是后端接口路径变动时只需要改api目录下的一个文件页面代码完全不用动。在联调阶段这个优势特别明显前后端接口对不上是常态集中管理能省掉极多排查时间。2.3 联调阶段最容易卡住的坑跨域与会话保持前后端分离项目第一次跑通时常见的报错就是跨域。前端localhost:8081调用后端localhost:8080浏览器默认是拦截的。解决方案我用的是Vue CLI的devServer代理后端不做任何跨域处理只在生产环境中由同域部署解决// vue.config.js module.exports { devServer: { port: 8081, proxy: { /api: { target: http://localhost:8080, changeOrigin: true } } } }代理配置好后前端请求/api/order/create会被转发到http://localhost:8080/api/order/create浏览器看到的始终是同源请求跨域问题就消失了。这里要注意代理只对开发环境有效如果把前端打包后放进SpringBoot的static目录就不存在跨域问题了但需要额外处理Vue Router的history模式路径问题。会话保持同样值得注意。用户登录后后端通过Session存放登录信息而前后端分离下两次请求的SessionId不同就会出现“登录成功后马上又失效”的假象。解决办法是后端跨域配置里加上allowCredentials(true)前端axios配置withCredentials: true。这套配置在开发环境下必须前后端同时配合只改一端无效。3. 数据库设计与SQL脚本五个核心表把业务边界划清楚整套系统的业务核心最终都会落到数据库表设计上。影院购票系统最忌讳的就是把座位状态直接存在影厅表里那样不同场次之间的座位状态会互相干扰。我最终确定的方案是引入“场次座位”的概念用五个核心表撑起全部业务。3.1 表结构拆解用户、电影、影厅、场次、座位、订单用户表 user主键、用户名、密码(BCrypt加密)、昵称、手机号、角色。密码加密是必须的毕设里明文密码是硬伤。电影表 film片名、封面图、类型、导演、主演、时长、上映日期、简介、状态。影厅表 hall影厅名、座位行数、座位列数。这张表不存具体座位只存影厅的尺寸。场次表 schedule关联电影和影厅记录开始时间、结束时间、票价。场次座位表 schedule_seat每新增一个场次就为这个场次生成一批座位记录每个座位包含行号、列号、座位状态。订单表 orders订单号、关联用户和场次、总金额、状态、创建时间、支付时间、过期时间。订单明细表 order_item一条订单对应多个座位明细表记录每个座位ID和座位标识。这个设计的关键在于schedule_seat。影厅本身只是一个模板真正的“座位状态”属于某个具体场次。同样一个5排8座的影厅下午3点场和晚上7点场的座位状态是相互独立的用户在下午场选了3排5座不影响晚上场同一个座位。这种设计是答辩时很加分的点因为逻辑严谨而且为后面做并发控制铺平了路。3.2 排片冲突检测让一个影厅在同一时间只放一部电影新开场次时必须校验同一个影厅的时间是否重叠。SQL写法如下SELECT COUNT(*) FROM schedule WHERE hall_id #{hallId} AND #{startTime} end_time AND #{endTime} start_time这段SQL的逻辑是只要已存在的场次区间和新场次区间有交集count就大于0此时不允许插入。这个判断条件本质上是两个区间不重叠的补集判断——两个区间重叠的充要条件是“新开始时间小于旧结束时间”且“新结束时间大于旧开始时间”。把这个校验写进ScheduleService的createSchedule方法里配合事务就能保证同一影厅同一时间只有一个场次。我在实际项目中还给这个查询加了FOR UPDATE避免两个管理员同时提交排片时产生幻读。毕设里虽然很少会真的出现并发排片但把这个细节写进设计说明能体现你对数据一致性的理解。3.3 初始数据怎么造让演示效果饱满而不失真SQL脚本里除了建表语句还必须包含初始数据。我推荐的组合是一套管理员账号、两套用户账号、六部左右电影、三个影厅、未来三天的场次数据、以及一个影厅的座位模板数据。需要注意两点。第一所有状态字段都用数字而不是字符串比如电影状态0未上映、1上映中、2已下架订单状态0待支付、1已支付、2已取消、3已退款前端用统一的枚举映射去显示对应中文不要直接在库里存中文。第二场次结束时间不要手算按电影时长15分钟广告时间自动计算初始SQL里可以直接写死但要注意自洽否则可能出现“电影都放完了订单还没开始”的尴尬。座位生成脚本值每个场次都手写40条INSERT那就是自己给自己找麻烦。我建议用一个存储过程或者在后端Service里写一个初始化方法创建场次成功后根据影厅的行列数批量插入schedule_seat。SQL脚本里保留一段示例INSERT即可完整数据由代码生成代码比手写数据可靠得多。4. 最硬的骨头选座购票的并发一致性与订单状态流转如果说这套项目里哪部分最能拉开和普通毕设的差距一定是座位锁定和订单超时处理。这两个问题的本质是并发一致性是面试和答辩最常被追问的点位。4.1 座位状态的生命周期可售、锁定、已售schedule_seat表里我设计了一个status字段0可售1锁定用户正在下单未支付2已售支付完成状态流转规则是可售→锁定→已售。锁定的座位不能被其他用户再选已售的座位不能退回到可售要退只能走订单退款。这个规则很简单难的是怎么保证并发下只有一个用户能抢到同一个座位。4.2 用事务行级锁防止“超卖”选座接口是并发风险最高的地方。如果两个用户同时点击同一个座位两个请求都读到“可售”然后都更新成“已锁”就出现了超卖。解决思路是让“检查—更新”成为一个原子操作。我实现的核心逻辑如下Transactional public boolean lockSeats(OrderCreateDTO dto) { // 1. 锁定选中的场次座位行 ListScheduleSeat seats scheduleSeatMapper.selectRowsForUpdate( dto.getScheduleId(), dto.getSeatIds()); // 2. 逐个检查状态有一个不是可售就抛异常 for (ScheduleSeat seat : seats) { if (seat.getStatus() ! 0) { throw new BusinessException(座位已被选购); } } // 3. 批量更新为锁定 scheduleSeatMapper.batchUpdateStatus(dto.getSeatIds(), 1); // 4. 创建订单状态为待支付写入过期时间 ordersMapper.insert(order); return true; }关键在第一步的selectRowsForUpdateSQL长这样SELECT * FROM schedule_seat WHERE schedule_id #{scheduleId} AND id IN (...) FOR UPDATEFOR UPDATE会对查出来的这些行加行级排他锁。第一个请求锁住这些座位后第二个请求再执行同样SQL时会被阻塞等第一个事务提交或回滚后才会读到最新状态。这样就能保证不会有两个用户同时拿到“可售”状态。事务在这个方法上必须加因为第1步加的锁要在第4步插入订单后统一提交时才释放。如果不加事务锁会提前释放依然存在并发窗口。这是一个典型的“悲观锁”方案在并发不算极端的场景下完全够用。答辩时如果被问“为什么不用乐观锁”可以回答座位抢购场景冲突率高悲观锁的阻塞等待实际上比乐观锁反复重试更可控。4.3 订单超时未支付状态要落地座位要释放用户锁定座位后迟迟不支付座位不能永远锁着。最常用的方案是“懒过期判断 定时任务兜底”。懒过期判断每次查询订单时检查当前时间是否超过expire_time如果超过且状态还是待支付就自动把订单改成已取消并把对应座位重置为可售。这个方案实现简单、实时性好但依赖用户主动触发查询。定时任务兜底SpringBoot里用Scheduled写一个定时任务每分钟扫描一次待支付且已过期的订单批量改状态并释放座位。这个方案不依赖用户行为但会有最长一分钟的延迟。我两个都做了查询时懒判断保证用户感知上的实时性定时任务保证系统状态最终一致。这个“双保险”的设计在答辩时很能体现工程思维而且代码量不大收益很高。4.4 模拟支付的边界处理真正对接支付SDK在毕设里不现实模拟支付的关键是让状态流转完整可信。我的做法是前端在订单详情页展示“待支付”状态和倒计时点击“模拟支付”按钮时调用后端/api/order/pay接口后端校验订单状态为待支付且未过期然后更新订单状态为已支付、关联座位更新为已售。这里容易忽略一个点支付接口也必须加校验不能允许用户对一个已取消的订单支付。我曾经见过有的项目支付接口只做“金额没变就更新状态”的逻辑结果出现了已取消订单被用户重新支付的脏数据。正确顺序一定是先校验订单状态再更新订单再更新座位这三步放在同一个事务里。5. 接口文档怎么组织让系统和人都能按约定工作接口文档在这套项目里不只是给答辩老师看的东西它是我前后端联调时的协作工具。很多学生项目写接口文档就是把Controller代码复制一遍那没什么价值。一份好接口文档应该是“不看源码也能把接口调通”的程度。5.1 接口文档必备的六个信息我给这份项目整理接口文档时每个接口都固定包含六项接口路径、请求方法、请求参数说明、响应结果说明、状态码说明、一个完整的响应示例。比如这个选座接口的文档条目POST /api/order/create Content-Type: application/json 请求参数: { scheduleId: 1, // 场次ID seatIds: [1, 2, 3], // 选中的座位ID列表 userId: 10 // 当前用户ID } 响应示例: { code: 200, message: success, data: { orderId: 10086, orderNo: 2024, totalAmount: 90.00, expireTime: 2024-..., seatCodes: [3排5座, 3排6座, 3排7座] } }做这一步的时候相当于把后端接口整体自测了一遍。文档里写的每个参数我都会实际调用验证一次这样可以顺手发现不少笔误和字段类型不一致的问题。5.2 统一返回结构前端少写很多废代码所有接口的返回结构必须统一我使用的是最常见的ResultT结构public class ResultT { private Integer code; private String message; private T data; }code200表示成功code400表示业务参数错误code401表示未登录code500表示服务器异常。前端axios响应拦截器里统一判断code等于200就直接返回data不等于200就弹出错误提示。这样一来每个页面的请求代码只需要关心成功分支错误处理全是公共逻辑代码量能少三分之一。状态码不用太多够用就好。太多状态码会让前后端都混乱回答问题时也说不清楚。我这里只用了四个边界场景全部能覆盖而且含义明确。6. 一键跑起来SQL导入、启动顺序与答辩演示检查单源码、SQL脚本、接口文档都齐了最后一步是把项目从0到1跑起来。这一步看似简单实际翻车率极高。我有一次给学生演示前端页面白屏了半小时最后发现是他Node版本太高Vue2项目编译报错。所以环境版本和启动顺序必须提前固定。6.1 环境版本不追求新追求稳这套项目我推荐的环境版本是JDK 1.8Maven 3.6SpringBoot 2.7.xVue2 Element UI Vue CLI 4.xNode 14.x 或 16.xMySQL 5.7 或 8.0Navicat或者任意MySQL客户端用于导入SQL脚本特别提醒不要用SpringBoot 3.x因为3.x最低要求JDK17且部分第三方依赖不兼容对毕设来说完全没必要冒这个险。同理Node版本过高会导致旧版本webpack编译报错我建议使用nvm管理Node版本在项目目录下固定一个可用的Node版本。6.2 SQL脚本导入与项目启动顺序导入SQL的顺序是先看脚本文件头部CREATE DATABASE语句在Navicat里新建连接后直接运行整个.sql文件它会自动建库、建表、插入初始数据。导入完成后重点检查三张表的数据量film有6条以上、schedule有10条以上、schedule_seat里每个场次有几十条座位数据。启动顺序上先后端再前端# 后端在项目根目录执行 mvn spring-boot:run # 前端在vue项目目录执行 npm install npm run serve后端启动后可以先在浏览器直接访问http://localhost:8080/api/film/list能返回JSON说明后端正常。前端启动后访问http://localhost:8081能通过代理请求到后端数据说明联调成功。这个检查顺序能帮你快速定位问题是出在后端还是出在前后端联调层。6.3 答辩演示前必须自己走一遍的闭环答辩演示最怕的就是现场出bug而绝大多数bug都集中在特定操作路径上。我建议演示前完整走一遍下面的闭环任何一步出问题都能提前发现注册一个新用户然后用这个账号登录。在首页选择一个场次进入选座页面选择三个座位。确认订单看到待支付状态和倒计时。模拟支付订单状态变为已支付。在订单列表确认座位已在“已售”状态。再选一个场次锁定座位后不支付等待过期后确认座位被释放。用管理员账号登录新建一部电影、新建一个场次确认排片冲突校验生效。用管理员账号查看订单列表确认新订单出现在列表里。这个流程覆盖了系统的全部核心功能而且精确指向最容易出问题的边界场景。我曾见过太多人演示时只点一遍“看起来正常”的路径结果老师一提问“如果用户不支付怎么办”就答不上来。把第6步在家自己走通这问题在答辩时就会变成你的加分项。最后说一个我自己的习惯拿到这种项目别急着跑一遍看效果先按“SQL脚本—接口文档—前端页面”的顺序读一遍。SQL让你知道系统里有哪些业务数据接口文档告诉你这些数据是怎么被操作的再看前端页面你就明白整个系统是怎么串起来的。这套流程看起来慢实际比对着报错日志瞎试快得多尤其是答辩前那一晚特别管用。
阅读完成 · 觉得有帮助?
咨询建站