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

SpringBoot+Vue+MySQL影城管理系统设计与实现:选座购票并发控制核心解析

SpringBoot+Vue+MySQL影城管理系统设计与实现:选座购票并发控制核心解析 ★ FEATURED ARTICLE
每年到毕业设计答辩季我总能看到一批类似的影城管理系统项目。多数同学把前后端打通、能登录、能增删改查就算交差但真正把“选座购票”这条最核心的交易链路做顺的其实是少数。这个项目代号叫“小徐影城管理系统”整体采用SpringBootVueMySQL这套在国内中小型Web项目里最主流的组合覆盖电影管理、排片管理、选座购票、订单处理、票房统计等完整业务闭环。如果你是计算机类专业的应届生或者刚开始接触全栈开发这篇文章会告诉你这种系统真正值得花时间的地方在哪里哪些模块可以快速做出来哪些设计一旦想明白答辩时能讲得底气十足。1. 从零拆解影城管理系统的业务边界与技术选型1.1 影城系统到底在管什么很多同学拿到题目就开始建表这是顺序错了。影城管理系统的核心业务不是“维护电影表”和“维护订单表”而是一个用户从浏览影片到完成购票的全过程。把这个过程想清楚表结构、接口、页面自然就推导出来了。围绕“购票”这个主链路业务可以拆成下面几个闭环业务模块主要功能涉及角色内容侧电影信息、分类、上映状态、影片详情、评分评论管理员、用户场次侧影厅管理、排片计划哪个厅、哪个时段放哪部电影、座位布局管理员交易侧选座、锁定座位、生成订单、支付毕设通常模拟、取消/退票用户管理侧后台维护电影和排片、订单查询、票房统计管理员用户侧注册登录、个人信息、购票历史用户这里要特别注意一个容易被忽略的点排片是电影和影厅之间的多对多关联。如果不建独立的场次表而是把场次信息塞在电影表或影厅表里后面做选座、订单、统计都会非常别扭。场次表是整个交易环节的枢纽这个优先级要排在所有表设计之前。1.2 技术选型为什么是SpringBootVueMySQL这套组合在国内毕设里出现频率极高不是没有道理。SpringBoot内嵌Tomcat不需要单独配置容器写RESTful接口非常顺手Vue配合Element UI或Element Plus可以在短时间内搭建出像样的管理界面MySQL作为关系型数据库安装、备份、迁移都比较省心。毕设场景下选型的最重要标准不是“最新颖”而是稳定、可控、容易把原理讲清楚。SpringBoot的自动配置、Vue的组件化、MySQL的事务机制都是答辩时能展开讲的内容。如果你用了过于冷门的框架老师反问一句“为什么不用主流方案”反而把自己绕进去了。版本上给个建议如果参考项目用的是Vue 2 Element UI没必要强行升级Vue 3。Vue 2的生态资料极其丰富遇到问题基本都能搜到答案Vue 3 Element Plus更现代但部分组件用法有差异。毕设以能完整复现为先不要在版本迁移上消耗太多时间。2. 数据库建模先回答业务问题再写建表语句2.1 核心实体关系梳理我把影城系统的实体关系整理成下面这张思路图用文字表达用户User注册、登录、购票的主体一个用户对应多个订单。电影Movie影片基础信息一部电影对应多个场次。影厅Hall物理放映厅一个影厅包含多个座位一个影厅对应多个场次。场次Schedule某部电影在某个影厅的某一场放映一个场次对应多个订单。座位Seat每个影厅有一套座位编号座位在某个场次下会有“已锁定/已售出”的状态。订单Order用户购买某个场次中一个或多个座位的记录。这里最关键的设计决策是座位要不要跟场次绑定有两种常见做法独立座位表 场次座位关联表为每个场次生成一份座位状态副本字段如schedule_id、seat_row、seat_col、status。优点是状态清晰、便于并发控制缺点是数据量大一些比如10个厅×100个座位×30个场次≈3万行对MySQL来说毫无压力。影厅座位表 订单明细反推状态座位只挂在影厅下某个场次是否已售通过查订单明细来判断。优点是表少缺点是查询麻烦还得额外处理锁定状态。我自己写这个项目时用的是方案1因为它在“选座”这个动作上能直接提供原子性的状态判断后面做座位锁定逻辑会简单很多也更容易在论文里画清楚。2.2 关键表的字段设计与约束直接给核心建表SQL方便抄作业-- 用户表 CREATE TABLE user ( id bigint NOT NULL AUTO_INCREMENT, username varchar(50) NOT NULL, password varchar(255) NOT NULL COMMENT BCrypt加密存储, nickname varchar(50) DEFAULT NULL, phone varchar(20) DEFAULT NULL, role tinyint NOT NULL DEFAULT 0 COMMENT 0-普通用户1-管理员, create_time datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), UNIQUE KEY uk_username (username) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;-- 电影表 CREATE TABLE movie ( id bigint NOT NULL AUTO_INCREMENT, title varchar(100) NOT NULL, cover_url varchar(255) DEFAULT NULL, director varchar(50) DEFAULT NULL, actors varchar(255) DEFAULT NULL, duration int NOT NULL COMMENT 片长分钟, category_id bigint DEFAULT NULL, status tinyint NOT NULL DEFAULT 1 COMMENT 0-下架1-上映中, description text, create_time datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), KEY idx_status (status) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;-- 场次表 CREATE TABLE schedule ( id bigint NOT NULL AUTO_INCREMENT, movie_id bigint NOT NULL, hall_id bigint NOT NULL, start_time datetime NOT NULL, end_time datetime NOT NULL, price decimal(10,2) NOT NULL, create_time datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), KEY idx_movie (movie_id), KEY idx_hall (hall_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;-- 场次座位表每个场次生成一套座位状态 CREATE TABLE schedule_seat ( id bigint NOT NULL AUTO_INCREMENT, schedule_id bigint NOT NULL, seat_row varchar(10) NOT NULL COMMENT 排号如A、B、C, seat_col int NOT NULL COMMENT 列号如1、2、3, status tinyint NOT NULL DEFAULT 0 COMMENT 0-可选1-锁定2-已售, order_id bigint DEFAULT NULL, lock_time datetime DEFAULT NULL COMMENT 锁定时间用于超时释放, PRIMARY KEY (id), UNIQUE KEY uk_schedule_seat (schedule_id, seat_row, seat_col), KEY idx_order (order_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;几点经验补充密码字段一定要留255长度不要用50。BCrypt加密后的字符串长度超过60很多同学在这里栽过跟头。唯一索引是必加的。uk_schedule_seat能保证同一个场次不会重复生成同一个座位这是并发场景下的第一道防线。字段命名统一用下划线风格。代码里MyBatis开启驼峰映射后schedule_id自动对应scheduleId省掉一堆TableField注解。2.3 初始化数据与预留字段的坑项目里必须预置一个管理员账号比如admin / admin123。注意密码一定是在Java代码里用BCryptPasswordEncoder加密后再插入数据库不要直接用明文。我见过不少项目初始化SQL里写着password 123456登录代码一比对就直接失败排查半天才发现是密码加密方式不一致。还有一点不要为了“考虑以后扩展”加十几个预留字段。毕设答辩时老师问“这个字段是干什么的”你答不上来反而扣分。表设计做到够用、清晰、能解释明白已经比大多数项目强了。3. SpringBoot 后端把“买票”这件事做成可靠的状态流转3.1 分层结构与接口规划后端我采用的是最常见的四层结构controller 接收请求参数校验返回统一结果 service 业务逻辑事务控制 mapper MyBatis-Plus的BaseMapper简单SQL直接继承 entity 数据库实体类项目里再放一个common包统一放Result返回体、BizException异常、JwtUtil工具类。这样控制层看起来非常干净写接口的速度会快很多。接口规划建议用前缀区分端/api/user/**用户端接口如/api/user/login、/api/user/register、/api/user/order/list/api/admin/**管理端接口如/api/admin/movie/add、/api/admin/schedule/add这样在拦截器里可以按路径前缀做权限控制比给每个接口贴RequireRole注解要省事。3.2 用户鉴权用JWT还是Session毕设项目我推荐直接用JWT。它的原理好讲代码量也不大用户登录成功后用用户ID和角色生成一个带签名的token前端存到localStorage每次请求时放到Authorization头里。后端用一个拦截器统一解析token把用户信息放到ThreadLocal或RequestContext里业务代码直接取。关键代码示意Component public class JwtInterceptor implements HandlerInterceptor { Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { String token request.getHeader(Authorization); if (StringUtils.isBlank(token)) { throw new BizException(未登录); } // 解析token校验签名和过期时间 Long userId JwtUtil.parseToken(token); request.setAttribute(userId, userId); return true; } }再注册拦截器放行登录接口和电影列表等公开接口。这里要注意拦截器里抛异常后要由全局异常处理器统一转成JSON返回不要直接输出500页面。3.3 选座接口与并发座位锁定的实现这是整个系统技术含量最高的地方。先说不好的做法前端选好座位后直接调/api/order/create插入订单座位状态同步改为已售。看起来没问题但两个用户同时选同一个座位时就会出现“订单都创建了座位也卖给两个人”的情况。我的做法分两步第一步选座时锁定座位。用户提交三个座位号后端开启事务用UPDATE schedule_seat SET status1, lock_timeNOW() WHERE schedule_id? AND seat_row? AND seat_col? AND status0。这个SQL带上status0条件利用数据库行锁保证同一时刻只有一个请求能把某个座位改成锁定状态。如果更新行数为0说明座位已被别人抢走直接返回“座位已被购买”。第二步下单支付完成后再把座位改为已售。如果用户锁座后一直没支付就通过定时任务扫描lock_time超过15分钟的记录把状态改回0。代码示意Transactional public Long lockSeats(LockSeatRequest req) { for (SeatPosition pos : req.getSeats()) { int rows scheduleSeatMapper.lockSeat(req.getScheduleId(), pos.getRow(), pos.getCol()); if (rows 0) { throw new BizException(座位 pos.getRow() pos.getCol() 已被锁定); } } // 创建订单状态为待支付 Order order new Order(); order.setStatus(OrderStatus.WAIT_PAY); orderMapper.insert(order); return order.getId(); }这个方案不需要引入Redis也能讲得通关键是用数据库行锁 条件更新来保证并发安全。答辩时可以主动提一句“如果并发量上来可以换成Redis分布式锁或乐观锁”既展示了你懂扩展方向又不增加实现复杂度。3.4 订单状态机设计订单不能只有“待支付”和“已支付”两个状态。我设计了五个状态状态含义可流转到WAIT_PAY 待支付座位已锁定等待用户支付PAID、CANCELEDPAID 已支付支付成功电影未放映REFUNDED、FINISHEDCANCELED 已取消用户主动取消或锁座超时终止REFUNDED 已退票用户申请退票终止FINISHED 已完成电影已放映完终止状态流转放在OrderService里集中处理不要在前端页面里随意改状态。比如取消订单时要同时做两件事把订单状态改成CANCELED把schedule_seat里的座位状态改回0这两个操作必须在同一个事务里。如果只改订单状态不释放座位就会出现“订单取消了但座位一直锁着”的bug。4. Vue 前端页面拆解、组件通信与联调细节4.1 前端工程化与页面清单前端用Vue脚手架创建项目按功能拆分视图src/ api/ axios请求封装按模块拆分文件 router/ 前端路由配置 views/ Home.vue 首页电影列表 MovieDetail.vue 电影详情 SeatSelect.vue 选座页 OrderConfirm.vue 订单确认 UserCenter.vue 个人中心 admin/ MovieManage.vue 电影管理 ScheduleManage.vue 排片管理 OrderManage.vue 订单管理 Statistic.vue 票房统计 store/ 用户信息、选座状态管理页面之间通过路由跳转选座状态这类跨页面数据用VuexVue 2或PiniaVue 3管理。这里有个经验选座页里用户选中的座位不要只存在组件内部一旦用户刷新页面状态就丢了。我习惯把选中座位列表同步到全局store里这样从选座页跳到订单确认页再跳回来数据还在。4.2 几个核心页面的实现思路选座页是前端最值得讲的部分。座位图其实就是二维数组渲染后端返回某个场次的座位列表前端按row分组每一行用CSS Grid排布。座位按钮有三种样式对应可选、锁定、已售。点击可选座位时高亮再次点击取消。影厅通常中间是过道可以在渲染时对特定列做留白处理。排片管理页要注意日期和影厅的联动。我实现时是先选电影再选影厅最后选日期时间。日期用后端生成未来7天的列表时段校验放在服务端做比如同一个影厅同一时间段不能排两部电影。票房统计页可以接ECharts做柱状图或折线图展示每天的票房总额和各电影的上座率。这部分不用太复杂能把数据从后端接口拉出来渲染成图就足够了答辩时非常直观。4.3 联调阶段最容易出问题的几个点跨域问题前端开发服务器默认端口是8080后端是8081直接请求会跨域。最省事的办法是在vue.config.js里配置代理把/api转发到后端地址浏览器里看起来是同源请求后端也不用单独配CORS。devServer: { proxy: { /api: { target: http://localhost:8081, changeOrigin: true } } }日期格式问题后端的LocalDateTime默认序列化成数组格式前端拿到后没法直接用。解决方案是加全局Jackson配置或者给时间字段加JsonFormat(pattern yyyy-MM-dd HH:mm:ss)。一定要在联调开始前就统一约定好格式不然后面每个页面都在处理时间字符串。请求拦截器带上tokenaxios封装里加请求拦截器统一从localStorage读取token并放到请求头响应拦截器里统一处理401状态跳转到登录页。这样业务代码里完全不用关心鉴权细节。5. 毕业设计论文和部署文档怎么写得让人信服5.1 论文结构与“创新点”怎么找论文结构按学校模板走通常包含摘要、绪论、需求分析、系统设计、系统实现、系统测试、总结。最容易写空的部分是“需求分析”和“系统设计”很多同学把需求分析直接写成功能列表这是不对的。需求分析要写用户角色分析、业务流程分析、功能需求和非功能需求每一段都要能对应到后面的实现。“创新点”是答辩时的加分项不需要真的多前沿把合理的工程设计讲成亮点就行座位锁定与超时释放机制解决了并发购票场景下的超卖问题订单状态机设计使订单生命周期清晰可控基于角色的接口权限控制区分用户端和管理端票房数据可视化分析帮助影院运营做决策。写论文时每个章节最好带上数据库表结构说明 核心接口描述 关键代码片段截图要截真实运行效果的界面不要用原型图代替。5.2 部署文档怎么编写才不背锅一份合格的部署文档要包含六块内容环境要求、数据库初始化、后端配置与启动、前端构建与部署、访问方式、常见问题。我见过太多的部署文档只有“步骤1导入SQL步骤2启动后端步骤3启动前端”真到了服务器上每一步都可能出问题。必要条件版本尽量写清楚比如依赖版本建议JDK1.8 及以上Maven3.6MySQL5.7 或 8.0Node.js14.x 或 16.xNginx1.20数据库初始化要写清楚先创建数据库再导入sql文件最后确认管理员账号密码。后端配置要说明application.yml里的数据库连接、端口、密钥等需要修改的地方。前端构建要区分开发模式npm run serve和生产模式npm run build。6. 部署上线与踩坑复盘6.1 本地到服务器的完整部署步骤以一台Linux云服务器为例大致流程是这样安装JDK、MySQL、Nginx关闭防火墙或放行端口上传数据库脚本执行导入修改后端的application-prod.yml把数据库地址、密码改成服务器的使用mvn clean package打成jar包用nohup java -jar xxx.jar后台启动前端执行npm run build生成dist目录放到Nginx的html目录下Nginx配置一个server块location /指向前端distlocation /api反代到后端端口。Nginx关键配置server { listen 80; server_name your_domain; root /usr/share/nginx/html/dist; index index.html; location / { try_files $uri $uri/ /index.html; } location /api/ { proxy_pass http://127.0.0.1:8081; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; } }try_files那行很关键Vue是单页应用刷新路由时必须回退到index.html否则会出现“页面能打开一刷新就404”的情况。这个问题在答辩演示时最容易翻车。6.2 常见问题清单现象原因解决办法前端请求后端报404代理没配或Nginx反代路径不对检查vue.config.js和Nginx配置数据库中文乱码表字符集不是utf8mb4建库语句指定DEFAULT CHARSETutf8mb4启动jar包端口被占用上次进程没杀掉netstat -tlnp找到PID后kill登录失败但账号存在密码加密方式不一致统一用BCrypt加密本地正常服务器上接口超时数据库连接用了localhost改成服务器的内网IP或公网IP我自己的习惯是部署完成后先用curl命令验证后端接口通不通再开前端页面验证登录链路。这样出了问题能快速定位是在哪一层不会前后端互相甩锅。最后再说一点个人体会。影城管理系统这类项目难度不在技术栈有多深而在你能否把一条完整的业务链路从头到尾讲清楚。数据库表之间的外键关系、选座时的并发控制、订单状态的流转、前后端的数据对接这些点每一项都值得你亲手写一遍、跑一遍、故意让它出一次错。当你真的踩过“座位同时卖出两份”“订单取消座位没释放”这类问题后再去答辩回答老师提问时的底气完全不一样。如果你打算在这个项目基础上继续深挖可以尝试把锁定座位的逻辑改成Redis实现或者接入真实的微信支付模拟流程这两个方向都有得写。
阅读完成 · 觉得有帮助?
咨询建站