尧图精选

SpringBoot+Vue+MySQL:阳光音乐厅订票系统从零到部署完整实战

🕒 发布时间:2026/10/2 9:25:43 📁 来源:尧图网络
一到毕设季总有学弟学妹跑来问我有没有一个既不算太复杂、又能把前端后端技术全部串起来的项目每次我都会把“阳光音乐厅订票系统”从仓库里翻出来当例子讲。这是一个用SpringBoot做后端、Vue做前端、MySQL存数据的完整票务管理平台覆盖用户注册登录、演出场次展示、在线选座、下单支付、后台票务管理等一整套业务闭环。这篇文章不是给你念需求文档而是把我从零搭这个项目时的技术选型逻辑、数据库设计取舍、核心模块的实现过程以及最后怎么部署到Linux服务器上的完整记录全部摊开写给你看。不管你是拿它做毕设、课设还是单纯想学SpringBootVue的实战套路这篇文章应该能帮你少走至少半个月的弯路。1. 项目整体设计与技术选型思路1.1 为什么是SpringBootVue这对组合很多人刚接触课设项目时会纠结用ServletJSP老一套还是用SSM模板引擎又或者干脆前后端不分我的建议很直接如果你没有必须在老技术上做文章的硬性要求SpringBootVue是现阶段性价比最高的组合。原因有三点。第一前后端分离是当前企业级开发的标配答辩时你可以明确说出“前端只负责渲染和交互后端只提供JSON接口”这句话面试官会认为你有工程化意识。第二SpringBoot把人从繁重的XML配置里解放出来内嵌Tomcat后一个java -jar就能把后端跑起来开发体验接近“写完即启动”Vue的组件化开发方式让页面代码组织清晰一个选座组件可以独立维护改起来不牵连其他页面。第三这套组合的资料密度极大你踩过的坑大概率别人也踩过搜一下就能找到解决方案对新手极其友好。有人会担心Vue是不是应该上3代我的看法是如果是自己学习Vue3Element Plus当然没问题如果是时间紧的毕设Vue2Element UI反而更稳。因为Vue2经过多年沉淀社区插件兼容性几乎没有暗坑Element UI的表单、表格、对话框组件足够覆盖管理后台90%的页面需求。技术选型不是越新越好而是越顺手越好这个原则在后面的部署环节还会被反复验证。1.2 系统功能模块如何拆解阳光音乐厅订票系统从使用角色上天然分成两端面向购票者的前台和面向运营者的后台。前台要解决的核心问题是“让用户能快速找到想看的演出并且顺畅完成选座下单”后台要解决的核心问题是“让管理员能对演出场次和订单状态做到可控可查”。前台模块我拆成了五个部分用户注册登录、演出列表与详情、场次选择、在线选座、订单中心。注册登录是一切业务的前提我用JWT生成登录令牌前端把token存在localStorage里演出列表页负责展示所有上架状态的演出详情页展示演员介绍、场次时间和价格区间选座是系统的灵魂功能点击某个场次后进入座位图空闲座位可点击已售座位不可操作订单中心则负责订单列表、支付模拟和退票操作。后台模块拆为四个部分演出管理、场次管理、订单管理、用户管理。演出管理维护演出名称、海报、简介等信息场次管理需要和座位表联动——新增场次时自动生成该场次的全部座位记录这一条是很多课设容易漏掉的设计订单管理支持按订单号或用户查询并可以手动处理异常订单用户管理则是简单的列表和角色查看不需要开放角色编辑权限免得给自己挖坑。1.3 技术版本与开发环境怎么选版本选择这块我直接给出一套我实测下来最省心的组合背景是JDK 1.8后端Spring Boot 2.7.xORM用MyBatis-Plus 3.5.x数据库MySQL 5.7或8.0均可前端Vue 2.7.x组件库Element UI 2.15.xNode环境控制在16.x。这个组合的兼容性是我踩过一轮坑之后确定下来的比如Spring Boot 3.x强行要求JDK 17很多同学本机装了JDK 8直接换版本会引发连锁问题又比如vue-cli新版本配合Node 18以上时Node-sass这类依赖极容易安装失败反而Node 16配合旧版本更顺滑。层级技术选型版本建议用途说明后端基础Spring Boot2.7.x提供依赖注入、Web MVC、内嵌TomcatORMMyBatis-Plus3.5.x减少简单增删改查代码量自带分页插件鉴权JWT HandlerInterceptorjjwt 0.9.x无状态登录认证退出或过期后需重新登录前端框架Vue2.7.x使用Options API资料多、学习成本低UI组件库Element UI2.15.x表格、表单、弹窗开箱即用数据库MySQL5.7 / 8.0存储全部业务数据InnoDB引擎开发工具方面我用的IDEA社区版加VSCodeIDEA负责Java后端VSCode负责Vue前端两个窗口分屏工作。数据库客户端用Navicat或DBeaverDBeaver免费且开箱即用对新手没有破解方面的坑。最后强调一下全局依赖版本在项目一开始就要锁死固守在pom.xml和package.json里否则后期“为什么别人能跑我不能跑”的问题大多出在版本不一致上。2. 数据库设计订票系统的地基2.1 核心表结构设计与关系梳理阳光音乐厅订票系统的核心表我最终精简为五张用户表、演出表、场次表、座位表、订单表。在设计时要抓住一个关键业务概念演出和场次要分开。演出是“《月光奏鸣曲》钢琴独奏音乐会”它只有一个标题、一套介绍但这个演出可以在12月档、1月档安排多个场次每个场次有独立的演出时间、上架状态和票价策略。如果不拆开你就得在演出表里重复维护标题与介绍一旦演出信息写错所有场次都会跟着错。用户表t_user的字段是id、username、password、real_name、phone、role、create_time。password字段我直接存BCrypt加密后的哈希值明文密码绝不落库这一点不仅是安全要求也是答辩时的加分项。role字段只有两个值0代表普通用户1代表管理员不搞复杂的角色表。演出表t_concert包含id、title、performer、poster_url、description、start_date、end_date、status、create_time。status同样是整数状态0下架1上架只有上架状态的演出才会出现在前台列表里。场次表t_session是核心业务桥梁包含id、concert_id、session_time、venue、duration、status其中concert_id外键指向演出表一个演出对应多个场次。座位表t_seat的粒度是整个系统里最细的包含id、session_id、row_no、col_no、seat_type、status。这里有个设计细节seat_type表示座位类型VIP座、A区、B区、C区不同票价策略实际上由场次里的seat_price_map配置而座位本身的记录是在创建场次时批量生成的。订单表t_orders包含id、order_no、user_id、session_id、seat_id、seat_desc、price、status、create_time、pay_time它把用户、场次、座位三个维度连成一条线一个座位在同一场次下只能出现一条非取消状态的订单。2.2 用状态机管理座位和订单状态状态机这个词听起来高大上其实逻辑非常简单一个字段的值从哪几个状态流转到哪几个状态是有明确规则的。座位只有三个状态0空闲、1锁定、2已售。用户在选座页面点击一个空闲座位时如果没有立即提交订单这个座位先标记为1锁定防止别人同时选中订单创建成功后在限定时间内完成模拟支付座位才流转为2已售如果超时未支付座位要回滚成0空闲。订单的状态比座位多一个环节0待支付、1已支付、2已取消、3已使用。下单成功后状态是0模拟支付接口调用成功之后变成1用户主动取消订单时状态变成2同时释放对应座位管理员或者系统定时任务在演出结束后可以把已支付的订单批量置为3。理解状态机对写后端代码很有帮助因为你可以把每个状态变化封装成独立的service方法而不是在controller里堆if-else代码的清晰度会显著提高。2.3 一段真实的设计演进记录这个项目我其实写了不止一版第一版为了省事把座位存成“A区1排1座”这样的字符串直接塞进订单表还觉得挺聪明。后来做选座页面时发现问题用户选座时我根本不知道哪些座位空闲只能把所有订单的座位字符串捞出来在内存里比对比对。数据量小的时候还能跑一旦场次多了性能明显变差而且“哪个座位属于哪个场次”的判断逻辑写起来非常痛苦。第二次重构才引入独立的座位表。每次创建场次时按照场馆座位布局循环插入座位记录例如15排每排20个座位一场就有300条记录。这样设计带来的好处是立竿见影的点击选座时一条SQL就能查出场次下的全部座位和状态下单时通过带条件的UPDATE语句直接锁定座位数据库层面就能防止超卖。这是在真实开发中“先写能用版本、再重构到合理结构”的一个缩影比一开始纸上谈兵设计半天要有效得多。3. 后端接口设计与关键业务实现3.1 后端项目结构与通用封装后端我采用的是经典四层结构controller、service、mapper、entity再加上一个config包放配置类、一个common包放通用返回结果和异常处理。实体类对应数据库五个表通过MyBatis-Plus的BaseMapper获得基础的增删改查能力省掉的代码量相当可观。Mapper层主要写选座时那种自定义SQLService层处理业务逻辑Controller层只做参数接收和结果返回不做具体业务判断。通用返回结果我封装了一个Result类结构是{ code: 200, message: 操作成功, data: {} }。code为200时前端认为成功非200时前端直接弹出message提示消息。这套约定在前端axios的响应拦截器里统一处理不需要每个接口各自判断成功与否。另外还配了一个全局异常处理器RestControllerAdvice捕获参数校验异常、业务异常、数据库异常把错误信息整理成统一的Result返回前端拿到之后弹出错误提示即可。有了这两个基础封装后面写所有接口的速度都会明显加快。3.2 登录鉴权与权限控制怎么做登录鉴权我没有用Spring Security而是选择轻量方案JWT加Spring MVC拦截器。原因很简单这个项目的权限模型只有“用户”和“管理员”两种角色用Spring Security虽然更“标准”但学习成本高配置不当反而会挡住自己。JWT的核心原理是用户登录成功后后端用密钥生成一个包含用户id和角色的token字符串返回给前端前端在后续请求的Header里带上token后端写一个拦截器在请求进入Controller之前解析token校验通过就放行。代码结构上我实现了一个AuthInterceptor实现HandlerInterceptor接口在preHandle方法里读取Header中的Authorization字段调用JwtUtil解析用户信息存入ThreadLocal。然后注册到WebMvcConfigurer中并且设置好放行名单登录注册接口、演出列表和详情接口直接放行订单相关接口必须登录管理后台相关接口不仅要求登录还要求用户角色是管理员。权限判断这一块我采用一个简单做法在需要管理员权限的接口上加自定义注解RequireAdmin拦截器里检测到该注解后校验角色字段实现起来干净利落。3.3 选座下单的并发控制是全场最难点毕设答辩时老师最爱问的问题多半围绕这一块展开两个人同时点击同一个座位你系统怎么保证只有一个能抢到我的方案是用数据库乐观锁思想。具体来说座位表中status字段是0空闲、1锁定、2已售用户点击选座时后端执行一条Update语句UPDATE t_seat SET status 1 WHERE id #{seatId} AND status 0这条语句的关键在于WHERE条件带上了status0。执行后MyBatis-Plus会返回受影响行数如果返回值是1说明这条Update成功把一个空闲座位改成了锁定状态这个座位被你抢到了如果返回值是0说明座位已经被别人锁定或售出本次选座失败。这种“原子性条件更新”在并发场景下能有效防止超卖因为数据库的行锁机制保证同一时刻只有一条Update能真正修改这条记录不需要引入Redis也不需要手动加synchronized。对课设来说这是一个性价比极高的方案既体现了对并发的理解又不会增加架构复杂度。下单时需要在一个事务里完成三件事锁定座位、创建订单、扣减余票信息。我用Transactional注解把这个动作包在同一个事务方法内任何一个步骤抛异常都会回滚前面已经执行的写操作避免出现座位锁了但订单没生成的数据脏状态。注意如果用了MyBatis-Plus默认事务管理器是不需要额外配置的只要你的数据源配置正确Transactional就能正常工作。3.4 订单超时与票退回的实现细节订单生成后如果用户一直不支付座位不能一直被锁定下去否则其他用户永远买不到这个票。我用三种方案可选最终选了最简单可靠的定时任务方案。在Spring Boot中开启EnableScheduling然后写一个ScheduledTask每隔一分钟扫描一次订单表找出“创建时间早于当前时间三分钟且状态为待支付”的订单把它们的状态批量改为已取消同时将对应的座位状态回滚为0空闲。Scheduled(fixedDelay 60000) public void handleTimeoutOrders() { ListOrder expiredOrders orderMapper.findExpiredPendingOrders(3分钟前的时间点); expiredOrders.forEach(order - { // 1. 把订单状态修改为已取消 // 2. 把对应座位状态改回空闲 }); }这套方案虽然叫“轮询”但它胜在朴素、无额外中间件并且完全满足课设的业务场景。如果项目想更进一步可以提“延迟队列”但你需要为此引入Redis或MQ复杂度会明显上升。我的建议是如果回答“能不能用Redis实现”这类追问你可以说“Redis的过期键通知机制配合监听可以做延迟释放但会有极端情况丢通知的问题需要补偿标记处理”这句话能体现你对技术边界有思考。4. 前端Vue工程化与核心页面实现4.1 前端项目初始化和工程结构前端我用vue-cli脚手架初始化项目。创建一个Vue2项目后首先要安装Element UI和相关依赖再把目录调成自己习惯的结构src下分为api、router、views、components、utils五个目录。api目录统一存放所有接口请求函数每个函数对应一个后端接口router目录管理页面路由views目录放页面级组件比如Login.vue、ConcertList.vue、SeatSelect.vuecomponents目录放被复用的子组件比如座位图组件utils目录放axios实例和token存储等工具函数。这套目录结构的核心价值在于“约定优于配置”不用每次写代码前纠结文件放哪里。api目录里的请求函数命名我习惯和后端Controller接口路径保持一致比如fetchConcertList对应GET /api/concert/listcreateOrder对应POST /api/order/create这样前后端联调时找接口特别快几乎不用翻文档。页面组件和路由文件的关系也一目了然新增一个页面无非是在views里新建文件再到router里注册一条记录。4.2 核心页面拆解选座页是技术含量最高的地方前台几个页面的技术含量差别很大。注册登录页和演出列表页基本是Element UI的常规用法重点在于和blob图片、接口返回的POST请求配合。技术含量最高的是选座页面因为它需要把数据库里的一维座位记录渲染成直观的二维座位图。我的实现思路是这样的从后端一次性获取一个场次下的全部座位列表每条记录包含rowNo、colNo、status字段前端用computed属性把一维数组分组为二维数组seats[rowNo][colNo]渲染时外层循环遍历排数内层循环遍历列数。每个座位的状态由status字段决定空闲状态渲染为可点击的绿色按钮点击后进入选中状态并变成橙色再次点击取消锁定状态渲染为灰色不可点击已售状态渲染为红色且带一个小图标。用户点完座位确认后调用下单接口把sessionId和seatId传给后端。为了提升用户体验我在座位图上方加了一个图例说明绿色代表可选、橙色代表选中、灰色代表锁定、红色代表已售。这个细节看起来简单但对于票务系统来说没有图例的用户引导等于劝退用户。座位区域用flex布局包在可滚动容器内适应不同大小的屏幕。管理后台里我还复用了这个选座页面但切换为只读模式方便管理员查看各个场次的售票情况。4.3 axios封装与路由守卫的细节axios如果不做封装前端代码会陷入重复写URL、重复处理token的泥潭。我的做法是在utils/request.js中创建一个axios实例设置baseURL为/api并加上两个拦截器。请求拦截器统一注入tokenservice.interceptors.request.use(config { const token localStorage.getItem(token) if (token) { config.headers[Authorization] token } return config })响应拦截器负责统一处理业务码和HTTP异常如果后端返回401状态说明token过期直接清掉本地token并跳转登录页。这样一来每个业务页面里的接口调用代码变得非常精简拿选座接口举例调用时只需要写createOrder(data).then(res { ... })不用关心token怎么加也不用关心错误弹窗怎么处理。路由守卫是另一个容易被忽略的细节。我用Vue Router的全局前置守卫beforeEach来实现两个功能一是未登录用户跳转到需要登录的页面时自动重定向到登录页二是已经登录的用户访问登录页时直接跳到首页。实现方式是在路由配置里给需要鉴权的页面加上meta: { requiresAuth: true }然后在前置守卫中判断router.beforeEach((to, from, next) { const token localStorage.getItem(token) if (to.meta.requiresAuth !token) { next(/login) } else { next() } })这个守卫看起来简单但它是一个项目的“门禁系统”。如果没有它前端所有“需要登录才能下单”的约束都只是一纸空文任意用户手动改一下localStorage就能绕过后台页面。虽然真正安全的后端接口本身有拦截器校验但前端路由守卫仍然是提升体验和防御纵深的重要一环。5. 前后端联调、打包与部署上线5.1 开发环境的跨域问题与反向代理配置前后端分离开发时最困扰新手的问题就是跨域。前端页面运行在localhost:8081后端接口跑在localhost:8080浏览器同源策略默认拦截这种跨端口请求。我采用的最佳实践是后端不开启全局CORS跨域配置而是在前端的vue.config.js里配置devServer代理。Vue开发服务器接收到前端请求后将带/api前缀的请求转发到后端地址前端看起来就像在请求自己的域名浏览器不触发跨域。具体配置很简单devServer: { port: 8081, proxy: { /api: { target: http://localhost:8080, changeOrigin: true } } }这个方案的好处是开发环境和生产环境可以采用同一套接口路径只在代理层做转发。如果后端接口还没有写完我甚至可以在proxy里加一个mock字段或者手工给对应的URL写死返回数据前端开发完全不依赖后端进度。注意一点源后端的接口路径里不需要再写/api前缀由代理层统一加这样才能保证生产环境Nginx转发时路径规则一致。5.2 生产构建与Linux服务器部署当代码开发完毕后部署上线有两个打包动作。前端在项目根目录执行npm run build生成的静态文件存放在dist目录里。后端在项目根目录执行mvn clean package -DskipTests生成一个可执行的jar包。我建议在打包前仔细检查后端配置文件确认数据库连接地址生产环境是否使用独立库、日志文件路径是否可写、文件上传目录是否存在这些细节在本地跑的时候不容易察觉部署到服务器后一旦缺失就是一场排查大会。服务器我选择一台轻量Linux云主机系统CentOS 7或Ubuntu 20.04均可。部署流程大致分为五步安装JDK1.8并配置环境变量安装MySQL并导入数据库脚本安装Nginx上传jar包和dist目录启动后端服务。启动后端我用nohup java -jar music-hall.jar nohup.log 21 这样关闭SSH终端后服务不会退出日志会滚动写入nohup.log文件后续排查问题有据可查。5.3 Nginx配置静态资源与反向代理Nginx的核心职责有两个托管前端静态文件并把/api开头的请求反向代理到后端jar包进程。配置片段如下server { listen 80; server_name your_domain_or_ip; location / { root /usr/share/nginx/html; index index.html; try_files $uri $uri/ /index.html; } location /api/ { proxy_pass http://127.0.0.1:8080; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; } }try_files $uri $uri/ /index.html这行非常关键。因为前端路由使用了history模式用户在浏览器里直接访问/user/orders这样的地址时Nginx在磁盘上找不到对应文件需要把请求回退到index.html由前端路由接管并渲染对应页面。如果不加这一行刷新页面或直接访问深层链接就会出现404。部署完成后通过浏览器输入服务器IP访问首页再完整走一遍注册、登录、浏览演出、选座、下单、后台管理的流程确认无异常后这套系统才算真正上线。6. 常见问题排查与避坑实录6.1 数据库连接与时区类问题这类问题出现频率最高。我整理了一个速查表几乎覆盖了我帮别人排查时遇到的所有情况。现象原因解决方法启动报错Access denied for user数据库账号密码错误或权限不足检查yml配置给用户授权GRANT ALL ON db.* TO root%报错SSL connection errorMySQL 8默认开启SSL加密JDBC连接串加useSSLfalse关闭并指定serverTimezoneAsia/Shanghai中文插入数据库变成问号数据库或表字符集不是utf8mb4建库语句显式指定DEFAULT CHARSETutf8mb4 COLLATEutf8mb4_general_ci插入当前时间差8小时JDBC时区未处理服务器与本地时区不一致连接串加serverTimezoneAsia/ShanghaiLinux执行timedatectl set-timezone Asia/Shanghai另外一个我特别提醒的点是MySQL驱动版本。如果你用的Spring Boot 2.7.x依赖管理的驱动会自动是8.0.x驱动类名要写com.mysql.cj.jdbc.Driver如果你从老项目复制了一段基于5.x的配置驱动类名写的是com.mysql.jdbc.Driver启动时会报找不到类的异常。这类问题定位很容易但新手往往会在网上越搜越乱建议第一时间检查和后端版本关联的配置。6.2 前端开发调试过程的高频坑前端的问题主要集中在三个方面。第一个是Element UI的按需引入和全量引入的区别我在项目里用了全量引入方式虽然包体积大一些但省心避免某个组件忘记注册导致页面空白。第二个是axios拦截器导致的偶发登录失效如果token在localStorage里的key值和后端要求的不一致或者前端存储了过期token请求会被拦截器放行但后端校验失败。我的解决办法是统一封装token的存取方法不散落在各个组件里。第三个是Vue打包后页面白屏的问题。这个坑往往出在publicPath上vue-cli默认publicPath是/如果你的前端文件没有部署在服务器根路径下而是放在了某个子目录那么静态资源的路径就会全部失效。我的方案是统一部署在根路径并且在Nginx里配置好根目录指向dist目录基本避免这个问题的出现。如果确实要部署在子路径就要显式设置publicPath为对应的子路径前缀。6.3 部署上线后的运维小技巧服务上线后有些小技巧能让你省下大量排查时间。第一个是养成看日志的习惯。后端看nohup.log前端看浏览器Console和Network大部分问题都会在控制台里留下线索。启动失败时用java -jar加--debug参数能输出更详细的日志。第二个是一定要设置好防火墙的安全组规则只开放80端口和SSH端口8080端口不要直接暴露给公网否则接口可能被扫描到并被恶意调用。第三个是定期备份数据库即使这是个课设项目也可以用mysqldump做一个简单的定时备份脚本这个习惯养成了对以后工作也是加分项。最后一个建议给正在熬夜改Bug的你这个项目里最有价值的东西不是那几千行代码而是你踩过坑之后建立起来的排查思路。比如“先看报错信息、再查配置、最后才怀疑代码逻辑”的路径很多人头两次做项目时是反着来的。把这套思考方式记录下来比源码本身珍贵得多。我当年第一次部署这个项目时光是解决一个MySQL时区问题就折腾了两天现在回头看那两天换来的是对数据源配置、JDBC驱动、Linux时间体系三个知识点的透彻理解。所以别怕踩坑坑踩得越深你学到的东西才越牢。
上一篇/下一篇内容由系统自动关联 返回资讯列表 →