ARTICLE DETAIL

资讯详情

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

SpringBoot+Vue前后端分离KPL售票系统实战:从设计到部署

SpringBoot+Vue前后端分离KPL售票系统实战:从设计到部署 先说结论这个SpringBoot Vue 的 KPL 比赛网上售票系统是我见过最适合拿来练手前后端分离完整流程的实战项目之一。它麻雀虽小但五脏俱全——从用户登录、赛事浏览、选座购票、订单支付模拟、到后台的赛事管理、票务统计几乎覆盖了一个真实互联网业务系统从零到一的所有核心环节。如果你正处于学完 SSM / SpringBoot 基础但没写过完整项目的阶段或者你正在准备毕业设计、找实习需要拿得出手的项目经验这个题目都非常合适。这篇文章我直接用过来人的视角把它的核心设计思路、数据库怎么建、前后端怎么分工、哪些地方容易踩坑全部拆开揉碎讲清楚。内容会有点长但保证每一段都是实操经验不是教科书式的废话。1. 先搞清楚这套系统到底在解决什么问题很多新手拿到这类XX管理系统、XX售票系统的题目第一反应就是不就是增删改查吗。这话对但也不完全对。增删改查是表皮真正的业务逻辑和设计难点藏在细节里。我们得先把需求想明白代码才有地方落地。1.1 售票系统的核心痛点并发与库存KPL王者荣耀职业联赛比赛售票最大的特点是什么一是票量少一场热门比赛可能就几千张票二是抢票时间集中开售瞬间流量是平时的几十倍甚至上百倍。这直接带来了一个技术问题怎么保证票不会被超卖这是售票系统和普通管理系统的本质区别也是面试官最喜欢追问的点。在单机应用阶段我们可以通过数据库的行锁SELECT ... FOR UPDATE或乐观锁版本号机制来保证库存扣减的原子性。在前后端分离的架构下虽然前端是独立部署的但后端的这个并发控制逻辑完全不受影响。这也是为什么我建议你哪怕只是做毕业设计也一定要在业务层面把超卖这个问题考虑进去。哪怕你用的是最简单的方案也比你完全不设防要好得多。1.2 用户角色与核心业务流程这套系统里存在两种完全不同的用户视角这决定了我们前后端页面和接口的设计方式普通用户C端注册登录、浏览赛事列表、查看赛事详情时间、场馆、座位图、票价、选座并下单、在线支付通常做模拟、查看个人订单、退票。管理员B端登录后台、维护赛事信息新增、修改、上下架、维护场馆和座位分区、处理用户的退票申请、查看售票统计报表。这两种角色对应的操作路径是完全独立的。如果只做一套页面用下拉框去切换身份那是单体老项目的做法。既然我们用了前后端分离 Vue就应该做成两个独立的前端应用或者一个应用内做完整的权限路由隔离分别部署在不同端口或不同路径下。这样不仅逻辑清晰也能在简历上体现出你理解多端适配的含义。1.3 为什么非要用前后端分离架构说句实在话一个售票系统的业务量级用传统的服务端渲染技术比如 Thymeleaf完全能实现甚至开发速度更快。那为什么标题里特意强调Vue 前后端分离我个人的理解是这个项目形态的真实价值在于它贴近当前企业级开发的主流协作模式。现在公司里做项目前端团队和后端团队是分开的甚至前后端排期都不一样。后端把接口写好返回 JSON 数据前端通过 Axios 请求数据、渲染页面两边只通过接口文档Swagger/YApi对接。这种模式下职责边界特别清楚后端不关心页面长什么样前端不关心 SQL 怎么优化。你通过这个项目把这种各司其职的感觉建立起来熟悉了接口约定、跨域调试、环境切换这些问题以后进团队会非常容易融入。2. 技术选型与项目结构不堆砌新技术只求合理我见过不少同学一上来就整微服务、Redis 集群、消息队列把系统搞得很重。对于毕设或个人练手项目这完全没必要。我们的原则是在满足核心业务需求的前提下用最成熟、最不折腾的技术组合。2.1 基础框架组合为什么选这些JDK 8 / 11当前生产环境最普及的版本。如果你电脑装了 17 也没问题但注意 Spring Boot 版本别选太低不然有兼容问题。Spring Boot 2.7.x 或 3.x建议 2.7.x稳定且网上资料最多。如果用 3.x必须要 JDK 17而且部分老教程里的配置方式会有差异碰到问题排查成本会高一些。MyBatis Plus比纯 MyBatis 开发效率高一大截内置分页插件、代码生成器AutoGenerator写单表 CRUD 真的是零 SQL。注意是 MyBatis Plus不是 MyBatis别搞混了。MySQL 8.x没啥好说的主流选择。字符集统一 utf8mb4排序规则 utf8mb4_general_ci能避免中文乱码和表情符号存储问题。Vue 3 Element Plus Axios Vue Router PiniaVue 3 是当前绝对主流Element Plus 的 UI 组件能让你把后台管理界面快速搭起来。Pinia 是 Vue 3 官方推荐的状态管理比 Vuex 更轻量。JWTjjwt做前后端分离不能依赖 Session因为前端可能部署在另一个端口下跨域携带 Cookie 很麻烦。JWT 是目前最主流、也最简单易懂的认证方案。这套组合的最大好处是各组件之间配合极其默契社区资料多遇到报错随便一搜就有答案。比你去研究什么自研 RPC 框架、分布式事务要省心一百倍。2.2 工程目录规划前后端彻底分开我建议你从项目创建开始就严格区分目录这体现的是工程化思维。一个很典型的目录规划如下kpl-ticket-backend # 后端项目Spring Boot ├── src/main/java/com/example/kplticket │ ├── controller # 控制层只做参数接收和结果返回 │ ├── service # 业务逻辑层核心代码都在这 │ │ └── impl │ ├── mapper # 数据访问层MyBatis Plus 接口 │ ├── entity # 数据库实体类 │ ├── dto # 数据传输对象Request/Response │ ├── config # 配置类跨域、拦截器等 │ ├── common # 公共类统一返回结果、异常处理、常量 │ └── utils # 工具类JWT 工具等 ├── src/main/resources │ ├── application.yml │ └── mapper # XML 文件复杂查询时用 kpl-ticket-frontend # 前端项目Vue ├── src │ ├── api # 所有接口请求函数集中管理 │ ├── views # 页面组件用户端/管理端 │ ├── router # 路由配置 │ ├── store # 全局状态管理 │ ├── components # 公共组件 │ ├── utils # 封装 Axios、token 处理等 │ └── assets # 静态资源 └── vite.config.js # 开发环境代理配置后端按分层组织前端按功能组织这是两个生态各自的标准约定。别把前后端混在一个仓库的同一个目录里虽然也能跑但看起来极不专业。2.3 为什么推荐用 Vite 而不是 Vue CLI 作为前端构建工具Vue 官方早就把 Vite 当成默认推荐了。Vite 开发服务器启动速度飞快秒级热更新也是毫秒级反馈体验比 Webpack 时代的 Vue CLI 舒服太多。用npm create vuelatest一条命令就能完成脚手架搭建还能顺手把 TypeScript、Vue Router、Pinia 一起勾选上。不过如果你完全没接触过 TS也可以先去掉 TypeScript 选项纯 JavaScript 先跑通流程别让语言特性本身成为理解业务的障碍。3. 数据库设计售票系统的心脏这个环节我建议多花时间。数据库设计的好坏直接决定了后面开发是行云流水还是举步维艰。售票系统的核心表我一般推荐至少要有这六张。3.1 核心表结构及其设计意图解析user 表用户表。字段不必过多id,username,passwordBCrypt 加密,phone,create_time就够用。注意密码绝对不要明文存储这是基本的安全底线。match_info 表赛事信息表。核心字段包括title赛事名称比如2024 KPL 春季赛总决赛、game_time比赛时间、venue场馆、status0未开售 1售票中 2售罄 3已结束、cover_url宣传海报、description赛事介绍。这是 C 端用户浏览的核心数据。seat_zone 表座位分区表。一个赛事下分内场区、看台A区、看台B区等。字段match_id,zone_name,price,total_count,sold_count。这里藏着售票系统最关键的两个字段总票数和已售票数所有的防超卖逻辑都围绕这两个字段展开。orders 表订单表。字段order_no唯一订单号、user_id,match_id,zone_id,seat_info座位信息可以是内场A区-3排5座的字符串或 JSON、amount实付金额、status0待支付 1已支付 2已退票 3已失效、create_time,pay_time。订单号建议用时间戳随机数或雪花ID生成别用自增ID直接暴露业务量。seat 表可选如果要精细到选座支持用户点击座位图选具体位置需要这张表。每个座位一条记录包含zone_id,row_no,col_no,status0可售 1已售。如果不做选座只做选择区域自动分配座位这张表可以省掉。对毕设来说按区域购票 阈值控制复杂度更合适。admin 表管理员表。可以独立建表也可以复用 user 表加个role字段。独立建表更清晰后台登录入口单独一套界面权限隔离更彻底。为了简化我倾向于直接在 user 表增加role字段0普通用户1管理员少一张表的维护成本。3.2 表关系与关键索引设计表关系其实很直观match_info一对多seat_zoneseat_zone一对多seat如果有的话orders多对一user多对一match_info。索引设计上有两个地方必须建索引不然数据量稍微上来一点查询会明显变慢orders表的user_id用于我的订单列表查询。orders表的order_no用于支付回调或订单查询应该建唯一索引保证不重复。另外seat_zone表最好建一个联合索引(match_id, id)这样每次查某场比赛的所有分区时不会全表扫描。别小看索引这块面试时问到数据库优化这就是你的实际素材。3.3 细说防超卖字段设计sold_count 和 total_count 的妙用这是这套系统的灵魂设计。传统电商扣减库存的常用方案有两种我直接给你分析它们的区别方案一应用层判断库存// 伪代码千万不要这么写 int soldCount zone.getSoldCount(); if (soldCount zone.getTotalCount()) { zone.setSoldCount(soldCount 1); zoneService.updateById(zone); // 创建订单... }这种方式在并发环境下稳挂。两个线程同时读到soldCount 99都判断小于总票数100然后都执行更新最终soldCount变成 100但订单却创建了两个超卖了两张票。方案二数据库行锁悲观锁SELECT * FROM seat_zone WHERE id #{zoneId} FOR UPDATE; -- 拿到锁之后再判断库存、扣减这种方式能解决超卖问题。只要事务不提交其他线程都会阻塞在这个 SELECT 语句上排队等待。它的缺点是性能相对有损耗但对于我们这种量级的项目完全够用而且最好理解。方案三乐观锁CASUPDATE seat_zone SET sold_count sold_count 1 WHERE id #{zoneId} AND sold_count total_count;这是最推荐的方式。这一条 UPDATE 语句本身是原子的数据库在内部会加行锁。只要sold_count total_count这个条件成立更新就成功通过updateById返回的影响行数判断是否抢票成功。如果返回 0说明库存已经满了直接提示用户已售罄。这段逻辑是你在面试介绍项目时必须能脱口而出的细节。一个简单的 UPDATE 条件语句就能让面试官看到你动了脑子而不是停留在会用 CRUD的层面。3.4 初始化数据与状态流转细节决定演示效果如果你想让系统演示效果流畅就不要只建空表。提前插入几场赛事数据一场报名中一场售票中一场已结束再加上几个座位分区和导演示的用户账号。这样登录系统后第一眼就有内容可看不会空荡荡。订单状态流转这个细节也要闭环用户下单 - 待支付状态0- 模拟支付 - 已支付状态1- 用户申请退票 - 管理员审核 - 已退票状态2。如果用户下单后 15 分钟未支付应该有一个定时任务把订单状态改为已失效并把库存回补。这里就涉及 Spring Boot 自带的Scheduled定时任务你不需要引入 Quartz一个注解就能实现。4. 后端实现要点从登录鉴权到核心下单流程后端是系统的大脑Spring Boot 的开发效率优势在这里体现得最明显。我不打算贴全部代码重点挑几个核心动作讲讲思路和关键代码片段。4.1 统一返回结果与全局异常处理前后端分离之后后端返回的数据格式必须统一前端才能写一套通用的解析逻辑。我强烈建议你定义两个类Result统一结果封装包含code状态码,message提示信息,data数据体三个字段。GlobalExceptionHandler全局异常处理器用RestControllerAdvice注解统一捕获业务异常自定义的 BizException和系统异常。好处立竿见影前端只管判断code 200就取data否则弹message报错。你不用再每个接口里都写try-catch代码瞬间变得干净很多。这一点是很多新手容易忽略的但它正是区分代码质量的关键。4.2 基于 JWT 的登录认证流程前后端分离的登录流程区别于传统 Session 方式核心逻辑是这样用户提交用户名密码到/api/auth/login。后端校验通过后生成一个 JWT Token把它放在 Result 的 data 里返回给前端。前端把这个 Token 存在 Pinia 里同时持久化到 localStorage刷新页面不丢。前端在 Axios 请求拦截器里从 localStorage 取到 Token并把它塞进请求头Authorization: Bearer token。后端写一个拦截器HandlerInterceptor拦截除了/api/auth/login、/api/register等白名单之外的请求。拦截器里的逻辑从请求头解析 Token如果解析失败直接返回 401成功则放行并把用户信息塞到 ThreadLocal 或请求 Attribute 里方便后续业务代码取用。这个流程中你需要一个 JWT 工具类封装生成和解析 Token 的逻辑。Token 的过期时间建议设置 7 天过期后前端需要做重新登录的跳转。注意要把密码字段从返回给前端的用户对象里剔除可以在实体类的password字段上加JsonIgnore注解或者在返回前手动把密码置空。4.3 核心下单逻辑事务与防超卖下单接口是整个系统最核心的方法大概可以拆成下面几步Transactional(rollbackFor Exception.class) public Result createOrder(OrderCreateRequest request, Long userId) { // 1. 校验赛事状态是否为售票中 MatchInfo match matchInfoMapper.selectById(request.getMatchId()); if (match null || match.getStatus() ! 1) { throw new BizException(该赛事当前不可购票); } // 2. 查询座位分区信息 SeatZone zone seatZoneMapper.selectById(request.getZoneId()); if (zone null || !zone.getMatchId().equals(request.getMatchId())) { throw new BizException(座位区不匹配); } // 3. 核心乐观锁扣减库存 int rows seatZoneMapper.deductStock(zone.getId()); if (rows 0) { throw new BizException(该区票已售罄请选择其他区域); } // 4. 生成订单号并插入订单数据 String orderNo generateOrderNo(); Orders order new Orders(); order.setOrderNo(orderNo); // ... 其他字段赋值 ordersMapper.insert(order); // 5. 更新赛事总销量可选 return Result.success(orderNo); }其中deductStock的 SQL 长这样update iddeductStock UPDATE seat_zone SET sold_count sold_count 1 WHERE id #{id} AND sold_count lt; total_count /update注意Transactional注解一定要加这样扣库存和创建订单才能在一个事务里要么全成功要么全失败。这里的细节是第 3 步用数据库乐观锁完成扣减第 4 步再把订单插入这个顺序不能颠倒。如果先插入订单再扣库存可能会产生有订单但库存扣减失败的脏数据。订单号生成我习惯用时间戳 随机数的组合比如20231210103045123 4位随机数够用且实现简单。如果需要全局唯一且更优雅的方案可以引入雪花算法这里不过多展开。4.4 模拟支付与订单状态管理真实支付要对接微信/支付宝 SDK对毕设和练习来说太重了。我们做模拟支付就行用户在订单列表里点立即支付后端直接把订单状态从待支付改成已支付再记录支付时间模拟流程完成。这里要注意订单状态机的完整闭环待支付-已支付-在看票时已消费或者待支付-已申请退票-管理员同意-已退票。定期清理超时未支付订单的定时任务可以放在Scheduled里每 5 分钟执行一次。4.5 管理员端接口CRUD 也讲究规范管理员端的接口逻辑相对简单但也要注意权限控制。核心接口有赛事的新增、修改、上下架逻辑删除不要真正 DELETE用is_deleted字段标记就行这样数据有追溯性订单的查询、退票审核核心数据的简单统计总票房、已售订单数、热门赛事 Top5 等统计 SQL 可以用 MyBatis Plus 的QueryWrapper聚合也可以直接写自定义 SQL。如果你想让系统看起来更专业一点可以引入 ECharts 做可视化图表展示每场比赛的售票率走势。前端图表组件从 ECharts 官网复制示例改改就能用视觉效果提升一个档次。5. 前端路由设计与开发联调前端部分的工作量其实不比后端少尤其是页面多、交互流程长。下面我把 Vite Vue 3 项目里的关键要点拆解一下。5.1 路由守卫与页面骨架因为存在用户端/管理端两种身份前端的路由分组就很关键。我通常的做法是用户端页面首页、赛事列表、赛事详情、个人中心、订单结算的路由放在一个对象组里布局用 UserLayout顶部导航 主内容区。管理端页面登录页独立、赛事管理、订单管理、报表统计放在 AdminLayout左侧菜单栏 顶栏 主内容区。用 Vue Router 的路由守卫来控制跳转在router.beforeEach里检查当前地址是否以/admin开头如果是就从 store 里取出用户角色信息如果不是管理员则重定向到登录页或提示无权限。Token 不存在时直接跳登录页。5.2 Axios 二次封装与环境切换前后端分离联调有一个绕不开的问题跨域。开发环境下最简单的方案是在vite.config.js里配置代理server: { port: 5173, proxy: { /api: { target: http://localhost:8080, // 后端地址 changeOrigin: true } } }这样前端请求/api/xxx时Vite 开发服务器会把它代理到后端的 8080 端口绕过了浏览器的跨域限制。注意后端也需要配置允许跨域CorsConfiguration不要只依赖前端代理。同时Axios 实例统一维护 baseURL、请求头并在响应拦截器里统一处理 HTTP 401Token 过期、500后端异常等情况。这个封装极其重要它决定了你前端代码的整洁度。别每个页面都自己写一遍axios.get加catch太乱了。5.3 关键页面拆解这 3 个页面最考验工程能力赛事详情页含购票操作展示赛事信息 座位分区卡片。用户选择分区 - 点击购买 - 弹窗确认数量 - 跳转订单确认页。这页的重点是座位分区可视化管理如果后端返回的数据包含价格、余票数前端直接根据余票数 0 禁用按钮即可。订单结算页展示用户姓名、手机号可改、订单金额、支付按钮。模拟支付成功后跳转支付成功页失败则展示错误提示。这页对状态反馈要求高需要仔细处理加载状态和按钮防重复点击。管理端赛事管理页表格 弹窗表单用 Element Plus 的el-table展示数据每一行有编辑、上架/下架、查看订单按钮新增/编辑用一个el-dialog加表单完成。这页模板化比较重你可以把反射后端的 CRUD 接口和表单校验规则写好就行。5.4 前后端分离开发时接口字段命名一致性一定要统一命名风格。后端用驼峰如matchId、soldCount前端也用驼峰。如果后端字段是数据库下划线风格match_id要么在后端实体类用TableField映射时保持驼峰要么返回之前做一个统一转换。最简单的方式后端实体字段直接用驼峰数据库列名下划线MyBatis Plus 开启驼峰映射map-underscore-to-camel-case: true这样返回的 JSON 就是天然驼峰前端不用做任何额外处理。6. 常见问题与排查技巧实录这部分是我最想说的。项目从零搭起来报错是必然的关键是怎么高效地排查和解决。我把实操中频率最高的几个问题列出来对应给出处理思路。6.1 跨域请求直接凉凉的 3 种表现与根治方法现象 1浏览器 F12 控制台报CORS policy: No Access-Control-Allow-Origin。这说明后端没配置跨域。解决方法写一个 CorsConfig 配置类实现WebMvcConfigurer在addCorsMappings里允许所有来源。同时注意如果用了 Spring Security跨域配置要在 SecurityConfig 里同样处理不然会被拦截。现象 2请求发出了但响应是 404。这种情况大概率是前端代理没配好。检查vite.config.js里的代理路径是否和后端接口请求路径一致。现象 3请求直接变为OPTIONS方法。这是浏览器的预检请求Preflight。后端要对OPTIONS请求直接放行尤其是拦截器配置时不要一刀切拦截。6.2 刷新页面后 404 / 白屏怎么排查前端项目打包之后的dist目录如果扔到 Nginx 下刷新非首页路由时很容易出现 404原因是 Vue Router 的 history 模式需要服务器配置try_files重写到index.html。你在本地调试时不太会遇到但在部署环境里是高频问题。如果是在本地启动的时候刷新页面 404一般是路由配置里createWebHistory的 base 路径问题。部署层面解决方式是在 Nginx 配置里加location / { try_files $uri $uri/ /index.html; }这个知识点写进简历或介绍里很能体现你完整的工程能力。6.3 后端接口明明通了前端拿不到数据多半是 JSON 格式问题常见情况是后端返回的日期字段是Timestamp数字串前端没法直接用。解决办法在application.yml里配置全局 JSON 序列化规则把LocalDateTime格式化为yyyy-MM-dd HH:mm:ss。这也是很多新手容易忽略的细节直接影响前端展示时间的效果。另外一个问题后端实体类里有循环引用比如订单里有用户用户里有订单列表会导致 JSON 序列化死循环栈溢出。解决办法是在关联字段上加JsonIgnore注解或使用JsonIdentityInfo。6.4 模拟并发测试时库存变成负数了怎么办如果你用 JMeter 或 Postman 对下单接口做了并发测试发现库存被扣成负数说明防超卖逻辑没有真正生效。常见原因有两种一是deductStock的 SQL 条件写错了可能少了AND sold_count total_count这个条件。 二是事务没有正确开启或者Transactional加在了没有经过 Spring 代理调用的方法上。排查逻辑很简单把数据库里的库存人为改成一个很小的数字然后用两个并发请求同时下单看最后库存会不会变成负数。不会变负说明就稳了会变负回到逻辑里找问题。7. 部署交付与扩展方向基于实际经验写到这里系统的主要模块和技术细节基本都过了一遍。最后再聊聊部署交付和未来可以继续深挖的方向。7.1 部署时最容易忽略的 3 个点数据库连接配置外置把application.yml里的数据库地址、密码等配置独立出来用application-prod.yml区分环境。否则每次部署都要改代码。前端打包后放到后端静态目录还是 Nginx这是一个很经典的取舍。简单粗暴的做法是前端npm run build生成dist目录拷贝到 Spring Boot 的src/main/resources/static下一个后端服务直接托管所有资源方便省事。但正经的部署方式是用 Nginx 托管前端静态资源反向代理/api请求到后端服务前后端分离的运维架构也方便后续独立扩容。对于练习项目我会建议你选 Nginx 方式更能体现你对分离的理解。服务端口与防火墙后端服务如果要让同一局域网的其他人访问注意服务器安全组和防火墙要放行对应端口。学习阶段把端口放开就行但如果你考虑上线要注意安全防护如访问限流、密码加盐等。7.2 这个项目还能怎么长胖如果做完核心功能后你想让这个项目明显高人一等有几条非常自然的扩展路径引入 Redis 缓存赛事信息和热点数据降低数据库压力。这本质是读多写少场景非常适合用缓存。引入消息队列RabbitMQ / Kafka做订单超时自动取消。下单后发送延迟消息延迟时间一到消费者检查订单状态并关单。这是电商类项目非常典型的业务闭环面试也爱问。座位图可视化选座。之前我把让用户按区域购票作为核心逻辑因为简单可靠。如果你想让系统更有亮点可以引入 Canvas 或 SVG 画出一个简陋的场馆座位图座位可点击选中这涉及大量的前端交互逻辑和坐标系换算但视觉效果和专业度会大幅加分。增加数据统计大屏。用 ECharts 在管理端做一个总裁驾驶舱展示总售票数、销售额按日趋势、热门赛事 Top10 等。数据来源就是订单表和赛事表开发成本不高视觉效果非常好特别适合答辩或项目演示时撑场面。我个人的建议是核心功能先扎实做完再多做一两个亮点功能。不要试图在一开始就把所有方案都堆上去那样反而容易让项目失衡陷入学了一堆技术但每样都不深的尴尬。最后说点实际的体会。做这类带源码、文档、调试标签的项目最忌讳的是代码跑起来就觉得自己会了。你有没有认真看过订单表的索引有没有手动试过把数据库连接断开看后端的错误提示是否友好有没有模拟过 50 个并发用户同时抢票这些细节才是你真正和只会照抄的选手拉开差距的地方。把这个系统从头到尾完整做一遍再把你踩过的坑和解决方案写成文档或 README你的收获会比看 100 个视频教程都大。
返回列表