资讯详情

资讯详情

SSM+Vue电影院在线售票系统:从数据库设计到前后端部署全解析

做过课程设计和毕业设计的同学看到“SSM412”这种编号应该会心一笑——这就是当年我在蚌埠学院做项目时的归档编号。这个编号底下存的是一个基于SSM加Vue的电影院在线售票系统用今天的话说就是一套前后端分离的Web全栈小项目。前后端的技术栈选择很经典后端用Spring、SpringMVC、MyBatis前端用Vue全家桶数据库用MySQL。这类项目最大的价值不在于技术多新而在于它把企业级系统里常见的那套东西用最精简的方式串了起来非常适合用来做课设、毕设或者作为练手的第一个完整全栈项目。我实际做完之后的体会是这个系统麻雀虽小五脏俱全。它覆盖了用户端从浏览电影、选择场次、在线选座、下单支付的全流程也覆盖了管理端从电影排片、影厅管理、订单管理到基础数据维护的一整套闭环。对想通过一个真实项目把SSM后端和Vue前端真正打通的人来说这个选题非常合适。下面我会把整个项目的设计思路、数据库建模、后端接口、前端实现和常见坑点全部拆开讲一遍全程基于我实际做过、踩过的真实经历。1. 先拆需求电影院售票系统到底要解决什么问题1.1 核心业务流程梳理做这类系统最忌讳一上来就写代码。我的习惯是先把业务流程画一遍站在用户的角度走一遍完整的购票链路用户打开系统首页看到正在热映的电影列表点进某部电影后能看到它的详情介绍、预告片和两三天内的排片场次选一个合适的场次后进入影厅座位图挑好座位提交订单支付成功后生成电子票。这条主链路跑通项目已经完成百分之八十的工作量。管理端的业务流程相对简单但更加繁琐排片员要维护电影信息、影厅信息、每日场次和票价还要处理用户下单后释放座位、订单超时这些状态变化。用户端和管理端看似是两个界面但背后的业务数据必须完全一致。这也是我最后选择前后端分离架构的直接原因——管理端做成独立页面通过权限字段控制入口用户端界面保持简洁干净两边通过同一套接口访问数据库。1.2 角色与功能边界划分这个系统我分成了三个角色普通用户、售票员、系统管理员。普通用户能做的操作是注册登录、浏览电影、查看影厅座位、下单支付、查看历史订单售票员负责电影信息导入和排片管理说白了一个电影上映之后在哪个厅放、每天放几场、票价定多少都由这个角色控制系统管理员则掌握着最大权限可以管理用户账号、查看全部订单、统计票房数据。很多同学在课设里会忽略角色划分把所有操作堆在一个页面上答辩时老师一问“谁的权限是什么”就答不上来。我这里特意把权限做成了一张user_role表用普通数字区分角色等级前端拿到角色后决定显示哪些菜单和按钮后端则在每次请求里校验当前登录用户的角色双重保障。1.3 技术选型为什么选了SSM而不是SpringBoot团队这两年其实已经全面转向SpringBoot了我做这个项目时也犹豫过要不要直接用Boot。但后来想通了SSM虽然配置繁琐但它能逼你把Spring容器、SpringMVC请求流程、MyBatis映射关系逐个手写一遍把底层机制搞明白。SpringBoot给你做好了一切反而少了这个过程。我用SSM做完整个后端以后再去接触Boot的自动配置就很容易理解——很多注解只是帮你省掉了原先那些XML配置。Vue作为前端框架是另一个维度的选择。影院售票界面对交互性要求高尤其是座位选择页需要动态渲染二维座位图并响应点击事件如果用传统的jQuery模板拼接会非常痛苦。Vue的响应式数据绑定和组件化能很自然地管理座位状态、订单对象、用户信息这些公共数据。我在开发时把座位区域、电影卡片、订单流程都抽成了独立组件后期修改某个模块完全不影响其他部分。2. 数据库设计是整个系统的地基2.1 七张核心表的建模思路数据库设计直接决定后面开发的顺不顺。这个项目我用到了七张核心表users用户表、films电影表、halls影厅表、sessions场次表、sessions_seats座位状态表、orders订单表、order_items订单明细表。表与表之间的关系很清晰一个电影对应多个场次一个影厅对应多个场次一个场次对应多个座位状态记录一个订单属于一个用户且包含多个订单明细。以users表为例我保留了最基本的id、username、password、phone、role、create_time六个字段密码使用MD5加密后再存库。films表则包含title、cover、duration、director、actors、description、status等字段。这里一个取值区间要特别注意status字段我设计为0代表未上映、1代表正在上映、2代表已下映首页只查询status为1的数据排片员在后台操作上下映。2.2 座位与订单一对多的状态联动sessions_seats是这套设计的核心表字段包括id、session_id、row_no、col_no、seat_no、status。status字段是整个系统的灵魂——我用0代表座位可售1代表已被锁定正在支付中2代表已售出3代表影厅过道或维修座位。为什么不用单独一张表记录已卖出的座位而是将座位状态直接内嵌在场次座位表中因为这样最有利于并发控制。具体逻辑是这样的用户选中座位后后端执行一条UPDATE语句将status从0改为1同时带上session_id和seat_no的条件。MyBatis返回的影响行数如果为1说明抢座成功如果为0说明这个座位已经被别人锁定直接提示座位不可选。这种基于数据库行锁的方式比纯前端控制靠谱得多也方便实现订单超时释放。orders表记录的是订单主信息包含order_no、user_id、session_id、total_amount、status、create_time、pay_time。status同样区分多个状态0未支付、1已支付、2已完成、3已取消。订单明细表order_items则记录每一张电影票的具体信息包括场次、座位、单价和数量。两张表用order_no关联查询用户历史订单时先查主表再查子表。2.3 设计阶段踩过的坑这里必须说几个我实际踩过的坑。第一外键约束不要加太多。刚开始我为了“严谨”在每个表之间都加外键结果测试时删除一条电影数据会被订单表挡住每次都要先删除关联数据特别麻烦。后来为了演示方便我去掉了大部分物理外键改由MyBatis在代码层维护关联关系。课设项目里这样做完全没问题。第二所有时间字段我统一用datetime而不是timestamp。timestamp有2038年的限制而且受时区影响datetime在直接写SQL时更省心。第三订单号我用了时间戳加随机数的组合order_no长度20位前14位是当前时间后6位是随机数字避免了并发下单时订单号重复。3. 后端SSM框架搭建与接口设计3.1 Maven项目结构与SSM配置要点后端项目我按标准Maven结构搭建src/main/java存放Java源码按照controller、service、mapper、entity四层分包src/main/resources存放配置文件其中spring-mvc.xml、spring-mybatis.xml、jdbc.properties、log4j.properties各司其职。SSM配置有几个关键点spring-mybatis.xml里要配置数据源、SqlSessionFactory和MapperScannerConfigurer让MyBatis自动扫描mapper接口spring-mvc.xml里必须配置注解驱动、Controller扫描和静态资源放行否则前端页面会全部被DispatcherServlet拦下来。配置静态资源放行这个坑特别经典。我第一次部署时把config里的/*.html和.js、.css都拦截了后端启动正常但访问页面就是404。后来在spring-mvc.xml加了下面这段才解决。mvc:resources location/static/ mapping/static/** / mvc:annotation-driven /还有一个容易忽略的点是事务管理器。售票系统里下单并扣减座位状态必须放在同一个事务里否则可能订单创建成功了座位却没锁住。我在spring-mybatis.xml里配置了DataSourceTransactionManager并在OrderServiceImpl类的lockSeatsAndCreateOrder方法上标注Transactional让整个方法成为原子操作任何一个环节失败都会整体回滚。3.2 核心接口清单与返回格式约定后端接口我统一使用RESTful风格返回JSON格式数据。这里只列一下最核心的接口方便大家参考功能描述请求方式接口路径获取正在热映的电影列表GET/api/films/releasing获取电影详情GET/api/films/{id}获取某场次的排片信息GET/api/sessions/movie/{filmId}获取某场次的座位图GET/api/sessions/{id}/seats锁定座位并创建订单POST/api/orders/lock支付订单POST/api/orders/{orderNo}/pay获取用户历史订单GET/api/orders/user/{userId}返回格式我统一封装为一个Result类包含code、msg、data三个字段。code为200表示成功其他值对应业务错误比如50001代表座位已被锁定。这种约定让前端axios拦截器能够统一处理所有异常情况而不需要在每个请求回调里单独判断。3.3 选座防并发和订单超时释放锁定座位的方法值得展开细讲。用户提交锁定请求后后端首先遍历用户提交的座位编号集合对每个座位执行更新操作SQL大致如下UPDATE sessions_seats SET status 1 WHERE session_id #{sessionId} AND seat_no #{seatNo} AND status 0如果更新影响行数等于座位数量说明全部锁定成功继续创建订单如果某个座位更新影响行数为0说明有人先手一步此时立即抛出业务异常并释放本次已经锁定的其他座位避免产生僵尸数据。订单超时释放是我最初遗漏的功能。用户锁座后如果一直不支付座位永远处于锁定状态对真实业务来说不可接受。我这里用了一个最简单的方案在OrderMapper里编写查询超过十五分钟仍未支付的订单再由一个调度线程定期执行释放操作把订单状态改为已取消同时把对应座位状态改回可售。没有引入Redis和消息队列因为这个量级的课设项目用数据库定时轮询完全足够而且代码量少好理解。3.4 日期和金额处理的细节问题后端与前端交互时日期序列化和金额计算最容易出问题。Jackson默认会把java.util.Date序列化为时间戳数字前端拿到没法直接展示我在spring-mvc.xml里配置了日期格式化器统一输出成yyyy-MM-dd HH:mm:ss格式。金额字段我直接用BigDecimal类型接收和计算避免使用double带来精度误差数据库中金额字段设计为DECIMAL(10,2)既满足精度要求又方便报表统计。4. 前端Vue侧实现细节4.1 环境准备和项目初始化前端部分我是用Vue CLI创建的项目。这里有版本选择的问题我使用的是Vue 2.6加Vue CLI 4版本配合Element UI组件库。之所以不用Vue 3是因为当时Element Plus还不够稳定而且网上的SSM配合Vue的参考资料大部分都是Vue 2的写法。实际运行下来Vue 2的生态最成熟适合课设周期比较短的项目。Node版本需要注意当时Vue CLI对Node版本有要求我在开发机上遇到过npm run serve报错的情况后来把Node从16降到14就正常了。初始化命令很简单vue create cinema-frontend选择预设时我勾选了Router和VuexCSS预处理器选择了SCSS其他保持默认。项目创建完成后先整理目录结构主要分成views、components、router、store、api五个目录。4.2 目录结构与组件划分views目录按页面拆分HomeView.vue是电影列表首页FilmDetail.vue是电影详情和场次选择SeatSelect.vue是选座页面OrderConfirm.vue是订单确认页PayResult.vue是支付结果页LoginView.vue和RegisterView.vue负责登录注册管理端的FilmManage.vue、SessionManage.vue、OrderManage.vue三个页面放在sub目录里。components目录则放置可复用的公共组件比如SeatMap.vue座位图组件、FilmCard.vue电影卡片组件、HeaderBar.vue顶部导航栏。组件划分有个原则页面级组件只负责数据获取和交互事件展示细节交给子组件。比如SeatMap组件只接收一个座位数组作为props再通过emit向外抛出选中座位的事件父组件收到事件后更新订单数据。这样设计的好处是选座逻辑和座位渲染完全可以独立测试不用反复启动整个项目。4.3 Axios封装、跨域代理和路由模式axios我是封装在api/request.js里的设置baseURL为/api并在拦截器中统一处理token添加、错误提示和登录跳转。开发环境用Vue CLI的代理解决跨域问题vue.config.js中的配置如下devServer: { proxy: { /api: { target: http://localhost:8081, changeOrigin: true } } }这里必须说明的是前端的/api路径前缀在后端Controller的RequestMapping里并没有对应我让后端统一以/api开头定义接口路径前端代理时只需把/api转发到后端服务地址即可不需要去掉前缀。生产环境则是通过Nginx将同一个域名下的/api请求反向代理到后端端口彻底规避跨域。路由这块我遇到过一个坑。刚开始用了HTML5 History模式路由路径很干净但部署到Tomcat后一刷新页面就404因为后端没有对应的路径返回index.html。后来改成hash模式就不存在这个问题了。如果要坚持使用history模式需要在Nginx中配置try_files $uri $uri/ /index.html;。对课设项目来说使用hash模式最省心URL里多一个#号完全不影响演示。4.4 Vuex管理订单状态订单相关的状态我全部放在了Vuex中管理。用户从选座页点击选座按钮后将选中的座位信息存入Vuex的orderState模块然后跳转到订单确认页确认页读取Vuex中的座位和金额数据用户点击支付后调用支付接口支付成功后清空Vuex中对应的state。为什么不直接用路由参数传递因为选座页和订单确认页之间不仅要传座位编号数组还要传场次信息、影厅信息、单价和总金额对象数据用路由参数传递既繁琐又不安全页面刷新就会丢。Vuex中的数据虽然刷新也会丢失但订单确认页本身不应该被刷新刷新了重新选座就行。5. 选座组件整个前端的重难点5.1 座位图渲染逻辑SeatMap组件是整个前端最核心的部分。影厅座位数据从后端接口获取后是这样一个数组结构每条记录包含row_no、col_no、seat_no和status字段。组件里我首先把一维数组转换成二维嵌套数组以row_no作为外层index以col_no作为内层index然后通过两层嵌套渲染出座位图。渲染时CSSTable布局最外层是一个相对定位的div内部用CSS Grid或Table布局屏幕上每一排是一个tr每个座位是一个td单元格。银幕位置我放在座位图上方用一条弧形渐变模拟幕布效果视觉上非常直观。每个座位格子的样式由status属性决定0号状态渲染为绿色可点击1号状态渲染为灰色不可点击2号状态渲染为红色不可点击3号状态渲染为白色不占座。我同时在座位格子上绑定了点击事件只有status为0的座位才能触发selectSeat方法。5.2 选座数量控制和合计金额计算实际电影院通常限制一次最多选四张票我在组件内部维护了一个selectedSeats数组每次点击先判断数量是否已达上限超过则弹出提示并阻止选中。系统通过this.selectedSeats.length * Number(this.session.price)实时计算合计金额在座位图下方的信息栏中同步显示。这里有一个体验细节每次点击座位后需要触发一次重新渲染。Vue的响应式系统会自动检测数组变化但如果直接使用this.selectedSeats[index] value这种方式修改数组Vue可能无法检测到变化。我统一使用this.selectedSeats.push(seatNo)或this.selectedSeats.splice(index, 1)来增删座位确保视图能正确刷新。5.3 提交订单前的座位二次校验用户完成选座点击提交后前端虽然已经拿到了座位状态但仍需将座位编号发送到后端做二次校验。因为用户在页面上停留的时间可能很长在这个时间内座位可能已被其他人锁定。后端二次校验通过后才创建订单否则回传错误信息告诉用户哪些座位已不可选。我在前端提交按钮上做了防重复提交处理调用提交方法后立即将按钮置为禁用状态同时显示加载动画接口响应成功或失败后再恢复按钮。这个细节看起来不起眼却是演示现场最容易丢分的地方——如果出现连续点击导致出现两笔订单评委老师印象立刻打折扣。5.4 电影预告片播放的扩展点如果要在电影详情页加上预告片功能前端可以直接用video标签播放MP4格式的视频文件。课设阶段把视频文件放在前端public目录下引用相对路径即可。网上流传的m3u8在线流媒体播放方案需要额外的播放器依赖和跨域配置这类格式适合后续进阶研究课设演示加上反而增加不确定性。我当时是直接存了几个MP4文件在本地演示效果足够好又不需要担心网络和格式问题。6. 常见问题与排查技巧实录6.1 后端启动失败和404的排查顺序后端启动报错大多集中在Tomcat端口被占用和Bean创建失败两类。端口占用用命令行解决在Windows下执行netstat -ano查找占用8080端口的进程PID再用taskkill /PID /F强制结束。Bean创建失败则要结合日志从下往上找Caused by信息常见原因是Mapper接口扫描路径配置错了导致SqlSessionFactory创建失败。接口访问404时我建议按三层排查先看Controller类有没有加上Controller注解再看RequestMapping路径和前端请求路径是否一致最后检查Mapper层返回的JSON字段名是否和小程序端使用的字段名完全对应。排查到字段名不同是最隐蔽的表现为浏览器请求返回200但页面全是undefined。6.2 前端白屏和跨域报错的常见原因前端白屏最常见原因是Node服务没启动、启动报错、或者浏览器访问的端口和devServer配置不一致。我在开发期习惯让后端跑8081端口前端通过代理访问如果后端不在8081启动前端所有请求都会报ERR_CONNECTION_REFUSED。页面加载出来但接口报错则优先检查vue.config.js中的proxy配置是否生效修改代理配置后必须重启npm run serve热更新不会重新加载webpack配置。6.3 MyBatis映射和JSON序列化的坑查询结果字段为null是初学者最容易困惑的问题。原因几乎都是数据库字段名是下划线风格user_id而Java实体类属性是userId驼峰风格。我在mybatis-config.xml中配置了mapUnderscoreToCamelCase属性为trueMyBatis会自动完成下划线与驼峰之间的映射这个配置务必加上否则每个查询都要手动写resultMap。日期序列化的问题在订单接口中特别明显。后端返回的createTime如果被Jackson转成时间戳数字前端显示出来就是一串1388888888888。我在spring-mvc.xml中配置了jackson的dateFormat为yyyy-MM-dd HH:mm:ss同时设置timezone为GMT8这样前端拿到的就是格式化后的字符串直接用插值语法展示即可。6.4 提交源码和依赖安装的几个细节给答辩老师或同学分享前端源码时记得删掉node_modules文件夹再把整个项目压缩包发出去接收方本地执行npm install还原依赖。如果对方项目启动后报failed to load tsconfig这类错误基本都是原始项目使用了不同于本机的Vue版本或构建工具版本比较省事的办法是从头用vue create新建一个同样版本的项目模板将源码中的src目录和关键配置文件复制过去。6.5 常见问题速查表故障现象可能原因解决办法后端启动立刻退出端口被占用netstat -ano查找PID并终止前端页面白屏无报错Node版本过低或ES语法不支持切换Node版本查看编译日志前端请求后端超时代理配置未重启修改vue.config.js后重跑devServer查询结果字段为null下划线与驼峰不对应开启mapUnderscoreToCamelCase提交订单重复按钮未防抖提交期间禁用按钮刷新页面404history模式未做回退改用hash模式vue项目发给他人报配置错误mismatched Vue版本新建同版本模板再拷入源码7. 打包部署与答辩演示的经验7.1 前后端分离部署的具体操作项目要完整跑起来并演示我采用的方案是用Maven将后端打成WAR包部署到Tomcat前端npm run build打出dist目录交给Nginx托管并配置反向代理。Nginx配置的核心部分是需要把/api前缀的请求转发到Tomcatserver { listen 80; server_name localhost; root /usr/share/nginx/html/dist; index 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; } location / { try_files $uri $uri/ /index.html; } }这种部署形式能完美模拟真实的前后端分离发布场景答辩时也更容易说明系统具备实际部署能力。如果不想装Nginx也可以直接把dist目录重命名为ROOT放进Tomcat的webapps下但API接口路径需要改成绝对路径或者额外处理代理稍微麻烦一点。7.2 演示前一定要提前准备的三件事第一件事数据库初始化。我在项目启动时会把后端SQL脚本里加一些预置数据包含六部电影、三个影厅、两天内的场次以及一部分已售出的座位状态。这样演示时首页不会空荡荡选座时也能直观看到两种不同颜色的座位证明系统状态管理正常。第二件事测试购票全流程。从头到尾跑通一遍选座下单支付操作确认支付成功后订单列表状态变为已支付座位变为已售。第三件事清理开发期的调试代码。尤其是console.log和System.out.println留着会显得很不专业。7.3 给系统增加三个实用的小功能如果项目做完后想再加分我建议从三个方向入手。一是票房统计报表后端用SQL按电影维度聚合订单金额和数量前端用表格和柱状图展示二是用户邮箱或手机号验证码注册避免直接用明文密码注册三是排片冲突检测在新增场次时自动校验同影厅时间段是否有重叠有重叠就提示排片员换其他时间。这三个功能都不复杂但能明显提升系统完整度和答辩展示性。最后说一点我个人的经验。做这类全栈课设项目最大的成就感来自打通前后端的那一刻——当你从数据库的一条记录一路看到它变成页面上一个可点击的座位格子时整个系统的轮廓才真正清晰起来。如果你也正卡在SSM整合或者Vue跨域这类问题上不用急照着上面的流程一步步排查先把架构跑通再倒回去优化细节。项目做完了记得打个压缩包备份好答辩前再完整走一遍购票流程你一定能成为同学里面最从容的那一个。
觉得有用,分享给同行:

为您的企业打造数字门面

稳重轻奢商务风格,端正雅致视觉,长效耐看不易过时。

立即咨询 →