资讯详情

资讯详情

Spring Boot毕设实战:亚冬会门票销售系统设计与并发控制方案

在Java方向的计算机毕业设计里翻来覆去能看到的无非就是那几个老面孔图书管理系统、宿舍管理系统、班级管理系统、在线商城。说实话这些题目不是不能做而是很难做出差异化答辩的时候老师一眼就能看出你写的是培训作业还是自己动过脑子。我最近一直在整理Spring Boot方向的毕设项目其中基于Spring Boot的亚冬会网上门票销售系统这个题目我前前后后带着好几届学生完整跟过从选题到数据库设计、从并发控制到答辩演示整个过程踩过不少坑。这篇博客我想把整套设计方案和实现思路掏出来讲讲尤其是那些教程文档里不会写、只有真正动手实践过才知道的细节一次性说清楚。这个题目适合谁适合那些Java基础还行、不想再做一遍图书管理系统、想在技术点上有东西可讲的同学。票务系统的业务闭环非常完整用户登录注册、浏览赛事场次、选座下单、支付、生成电子票二维码、后台管理、销售数据统计一套下来几乎是一个真实产品的缩影。更关键的是它天然带着两个技术亮点抢票场景下的并发控制和订单超时状态的一致性处理。这两个点随便拿出一个做深了答辩的时候都够你讲三分钟不冷场。1. 我为什么建议选票务系统当Spring Boot毕设选题逻辑与评审关注点很多同学选题目的时候只考虑好不好做很少有人反向思考答辩老师坐在下面他到底想听到什么其实评审老师关心的就三件事第一这个系统有没有真实业务场景你是不是在为了CRUD而CRUD第二你有没有解决一个稍微有点难度的问题第三你自己写的东西你能不能讲清楚原理。1.1 票务系统的天然优势业务闭环与真实感图书管理系统为什么越来越不吃香因为它整个业务就是增删改查管理员加书、学生借书、还书所有操作都是单用户、低并发、无状态的。老师随便问一句假如一万人同时抢同一本书怎么办你当场就接不上话。但票务系统不一样冰雪赛事一开票热门场次基本就是秒空这个业务场景本身就是高并发的代名词。你再怎么简化系统里都天然存在库存有限、多人抢购的矛盾这就逼着你必须去想并发控制的问题。亚冬会这个场景还有一个额外的优势——赛事结构丰富。亚冬会的比赛项目非常多短道速滑、花样滑冰、冰球、冰壶、高山滑雪、越野滑雪、单板滑雪等每个项目都有不同的比赛场馆、不同的场次时间、不同的票价档位。这意味着你的数据库设计必须足够灵活要能表达赛事—场馆—场次—票档—座位这一整条业务链路而不是简单的一张表搞定。冲着这一点系统建模的可写空间就比普通商城大不少。1.2 从评审视角反推哪些功能必须做哪些功能做了是加分项我一般建议学生把功能分成三个层级别平均用力功能模块优先级核心技术点答辩价值用户注册登录、JWT鉴权必须JWT、密码加密、拦截器展示安全设计意识赛事场次浏览、票档展示必须多表关联查询、缓存数据库建模能力选座购票、库存扣减必须乐观锁、防超卖核心技术亮点订单管理与超时关单强烈建议定时任务、状态机系统思维能力模拟支付与回调强烈建议回调验签、幂等处理对真实业务的理解销售数据可视化大屏加分项ECharts聚合统计演示效果好直观Excel导出、验证码加分项第三方工具类集成体现工程化习惯这套分级思路的核心逻辑是必须做的是地基地基不能塌加分项才是你和其他同学拉开差距的地方。很多学生把时间花在把后台管理界面做得特别花哨结果核心的抢票并发逻辑完全没考虑这完全是本末倒置。评委真正会追着问的永远是那几个深入的技术问题而不是你的按钮好不好看。1.3 亚冬会不只换了个名字业务场景化设计有些同学换题目就是改个名字把电影院售票系统改成亚冬会门票系统内容原封不动。这样做其实浪费了这个题目的价值。亚冬会赛事的特征非常鲜明热门赛事开票即空、同一用户需要限购防止囤票、不同票档价格差异大例如预赛票、决赛票、VIP包厢票、不同场馆的座位排布不同。基于这些特征你在业务规则上可以做几个很明显的差异化设计热门场次单人限购3张防止黄牛囤票开票时间之前只允许浏览不允许下单赛前24小时以上允许退票并按比例收取手续费决赛场次与预赛场次采用不同的票档定价策略冷门场次还可以有组合套票折扣。这些小规则单个拎出来都不难实现但组合在一起你的系统就变成了为亚冬会量身定做的系统而不是一个通用商城模板的换皮。答辩的时候主动说一句我根据赛事特征设计了限购策略老师的印象会立刻不一样。2. 功能边界与用户角色先把业务范围定清楚动代码之前最怕的就是需求模糊。票务系统看上去简单实际把角色和场景列出来之后工作量并不小所以一定要先拆边界。2.1 三类用户角色与对应的功能视图这个系统我建议做三类角色普通游客、注册用户、系统管理员。游客没有登录也能浏览首页、看赛事列表、看场次详情和票价但一走到下单这一步就必须注册登录。这么做的好处是浏览-下单之间有一个自然的身份切换点也让JWT令牌的颁发有明确的业务触发时机。注册用户是核心角色能完成完整购票流程包括选场次、选票档或座位、提交订单、模拟支付、查看电子票、退票。这里注意订单和电子票是两个概念用户先有订单支付成功后才生成对应票品一张订单可以包含多张票。管理员负责后台维护赛事项目、管理场馆和座位区域配置、发布场次、设置票档价格、查看每个场次的实时销售进度、管理用户、查看销售数据大屏。后台的实时销售进度特别重要它是管理员理解整个系统状态的窗口也能体现你前端联动的能力。2.2 前台购票的核心路径与状态流转整个前台业务的关键链路是浏览赛事场次 → 选择票档或座位 → 确认订单页 → 提交订单状态为待支付 → 跳转模拟收银台 → 模拟支付成功 → 回调通知后端 → 状态改为已支付并生成电子票 → 用户在我的订单里查看票码。每一步都要有明确的状态标识因为这里是不可避免的状态机设计问题。我常说一句话做这类业务系统你写的其实不是CRUD而是一个状态机。每个接口只是把某个实体从状态A合法地推向状态B。如果这一步想清楚了后面处理超时关单、退票、支付回调都不会乱。2.3 后台管理的核心操作管理员界面看起来就是几个表格但这里有一个主要的设计点赛事、场次、票档三个实体的层级关系。赛事是抽象的项目名称比如花样滑冰单项赛场次是具体某个时间点在某场馆举办的比赛比如花样滑冰决赛场次票档则是该场次下不同价格的座位等级VIP、A区、B区、普通区。后台的流程就是先建赛事再为赛事创建场次再为场次配置票档和库存。这个三层结构一旦理清了整个系统的骨架就立住了。2.4 限购规则与开票时间体现业务理解深度的细节我特别想强调两处细节设计。第一处是热门场次限购我建议在订单创建接口里增加一个查询统计当前用户当前场次已购且未退票的票数超过3张就不允许再下单。这个查询很简单但它阻止了刷票这种真实业务诉求。第二处是开票时间设计场次表里加一个open_time字段早于这个时间的用户下单请求直接拒绝。现在的实际项目几乎都有抢票倒计时这个设计你的毕设里有说明你见过真实的抢票业务是怎么做的。3. 数据库设计是票务系统的命门核心表关系与字段细节如果让我只给一条毕设建议我会说多花时间在数据库设计上。票务系统的表结构一旦建歪后面写代码全是补丁。建对了后面所有业务逻辑都是顺水推舟。3.1 核心表清单与关系我建议至少设计这些核心表用户表、赛事表、场次表、票档表、库存表、座位表、订单表、订单明细表、票品表、支付记录表。下面这张表是核心关系的骨架表名主要字段职责与谁关联event 赛事表赛事名称、项目类别、图标、简介一对多场次session 场次表所属赛事、场馆、比赛时间、开票时间一对多票档/座位ticket_category 票档表票档名称、价格、单场限购数关联场次inventory 库存表总库存、已售数量、乐观锁版本号按场次票档唯一seat 座位表场馆座位号、座位状态、锁定时间关联场次和票品orders 订单表订单号、用户、金额、状态、超时时间关联用户ticket 票品表唯一票码、座位、状态关联订单payment 支付记录表支付流水号、金额、状态、回调时间关联订单3.2 订单与票品为什么要拆成两张表这个是新手最容易想不明白的地方。很多同学会把订单和票合并成一张表每买一张票就生成一条订单记录。表面上看没什么问题但一旦用户一次买3张票你就得生成3条订单订单和订单之间失去关联后续退票、退款对账会非常痛苦。正确的做法是一个订单对应多个票品。订单表存用户、总金额、订单状态、下单时间、超时时间票品表存单张票的座位信息、票码、使用状态。这样订单是聚合根票品是订单下的明细子项。用户退票时可以选择退整单还是退部分票逻辑都清晰很多。票码字段也建议好好设计一下。票码作为电子票的唯一标识建议编码规则包含场次、区域、座位、随机数等信息。比如SE-20260310-01-A-021-8F3K2这样就算数据库里的随机部分被泄露也不容易被批量猜到有效的票码。这个细节虽然小但答辩时讲出来别人会觉得你考虑过真实业务的防伪需求。3.3 库存表设计一张被很多人低估的表真正做过票务系统的人都知道库存设计是整个系统的核心。具体实现上可以有两套方案。一套是直接在场次表里放total_stock和sold_count两个字段另一套是拆一张独立的库存表。我强烈建议拆表如表所示CREATE TABLE inventory ( id BIGINT PRIMARY KEY AUTO_INCREMENT, session_id BIGINT NOT NULL COMMENT 场次ID, category_id BIGINT NOT NULL COMMENT 票档ID, total_stock INT NOT NULL DEFAULT 0 COMMENT 总库存, sold_count INT NOT NULL DEFAULT 0 COMMENT 已售数量, version INT NOT NULL DEFAULT 0 COMMENT 乐观锁版本号, UNIQUE KEY uk_session_category (session_id, category_id) ) COMMENT 场次票档库存表;这样设计的好处非常直接同一个场次下VIP、A区、B区、普通区各有各的库存并发扣减互不干扰。UNIQUE约束保证同一个场次和票档只对应一行库存记录不会重复。version字段是后面做乐观锁防超卖的关键下面会细讲。3.4 订单状态机与字段级避坑订单状态我建议定义为待支付(0)、已支付(1)、已出票(2)、已使用(3)、已退票(4)、已关闭(5)。整个生命周期用一张状态流转表来表达从状态到状态触发动作注意点待支付已关闭定时任务扫描超时仅当状态仍为待支付时才能关单待支付已支付支付回调回调需要幂等已支付已出票支付成功后立即生成票品同一订单不能重复出票已支付/已出票已退票用户申请退票赛前24小时限制已出票已使用扫码入场核销票品状态同步变化字段层面的经验我总结几条金额一律用decimal(10,2)不要用double浮点数的精度问题在做金额累计时一定会坑你时间字段要区分创建时间和最后更新时间很多表都需要别偷懒订单号不要用自增id否则用户能从数字大小猜出你的销量建议用yyyyMMddHHmmss 随机数或者雪花算法生成。4. 抢票并发与防超卖毕设答辩里的技术高潮我敢说票务系统这个题目90%的答辩老师都会问到同一个问题这么多人同时抢票你怎么保证不超卖这个问题答得好整个答辩的基调就起高了。下面这段是我实际调下来的完整方案。4.1 为什么先查再扣在大并发下必死很多第一次写票务系统的同学库存扣减的代码长这样Inventory inv inventoryMapper.selectBySessionAndCategory(sessionId, categoryId); if (inv.getSoldCount() inv.getTotalStock()) { inv.setSoldCount(inv.getSoldCount() 1); inventoryMapper.updateById(inv); }这个逻辑在单线程下完全正确。但一旦并发上来两个线程同时读到sold_count0同时判断0小于100然后都执行1最终sold_count变成1实际却卖出去2张票。这就是读改写竞争条件。问题出在检查和更新不是原子操作中间隔着一个网络往返和业务计算。4.2 方案一乐观锁扣库存最稳的保底方案你不需要一上来就上RedisMySQL自己就能解决这个问题。把上面的逻辑改成一条条件更新语句UPDATE inventory SET sold_count sold_count 1, version version 1 WHERE session_id ? AND category_id ? AND sold_count total_stock;这里的核心是把判断库存是否充足这个条件直接写进UPDATE的WHERE里。MySQL的行锁会保证同一时刻只有一条UPDATE能成功修改同一行其他并发线程的UPDATE受影响行数为0你只需要根据这个行数判断是否抢票成功。这个方案不需要引入任何中间件数据库能力就能扛住对于毕设的并发量来说完全够用还特别好讲原理。我在本地用JMeter压过100张库存的场次500个并发线程同时抢最终数据库里的sold_count恰好是100成功生成的订单也是100条没有一条超卖记录。这个压测数据放PPT里是很好的证据建议你做完之后截图保存。4.3 方案二Redis分布式锁给答辩增加高级感如果说乐观锁是保底那Redis分布式锁就是让你在答辩台上能额外发挥五分钟的加分项。思路是抢票前先尝试获取一把场次票档维度的分布式锁拿到锁才允许执行库存扣减否则直接返回正在排队。锁的Key设计成 lock:sale:{sessionId}:{categoryId}Value用UUID随机串设置过期时间大约5秒。实现时我建议你用Redisson的RLock而不是自己用setnx硬写因为Redisson自带看门狗机制会自动续期避免业务执行时间超过锁过期时间导致锁提前释放。手动实现的时候新人最容易犯两个错误一个是忘了设置过期时间一旦服务宕机就死锁另一个是释放锁时不判断归属结果把自己设置的锁被别人释放了。4.4 锁和事务在一起时的经典陷阱这里有一个我踩过好几轮的坑分布式锁放在事务外面还是事务里面正确的做法是先获取锁再开启事务执行库存扣减和订单创建最后提交事务再释放锁。如果你把锁放在事务里面事务还没提交锁就被释放了其他线程拿到锁进来读到的是未提交的旧库存数据防超卖就又失效了。锁的粒度要覆盖整个查库存、扣库存、建订单的完整过程而不是只包住一条UPDATE语句。4.5 座位锁定与超时释放选座场景的关键闭环如果你的系统支持精确选座那么座位表里每一个座位都要有状态字段。座位状态建议设计为空闲(0)、锁定(1)、已售(2)。用户选座提交订单后先把座位置为锁定15分钟内未支付定时任务把锁定的座位重新置为空闲支付成功后座位置为已售并生成票品。这里务必注意一个关联细节释放座位前要校验该座位关联的订单状态确实还是待支付绝不能把已经支付成功的订单座位释放掉。所以定时任务里要关联查询订单表和座位表这个联动校验写好了整个超时释放逻辑才算安全。我在这个环节见过太多次翻车尤其注意千万别只按锁定时间超过15分钟这一个条件去做释放。5. 支付回调与订单超时关闭一致性处理的完整思路门外的同学总以为支付就是调一下接口实际上支付环节是系统里最容易出隐蔽Bug的地方。毕设不要求你接入真实第三方支付但模拟支付也要按照真实支付的思路来设计这样才能在答辩时讲出专业性。5.1 模拟支付网关体现真实支付的回调验签机制我建议的模拟支付流程是这样用户提交订单后跳转到本地的一个模拟收银台页面页面显示订单号、商品明细和应付金额用户点击确认支付前端请求支付接口支付接口直接返回成功然后由后端主动调用自己的回调接口或者由前端通知后端支付成功。关键点是回调接口要带签名参数后端接收到回调后先按约定规则重新计算签名比对一致才处理业务这就是真实支付里的验签环节。虽然毕设不需要真的对接第三方支付平台但验签这个环节把这个业务逻辑体现了出来让人感觉你懂支付。回调处理的核心要求是幂等同一个订单的支付回调无论被调用多少次最终结果都应该是已支付且只出一张票。实现方式很简单处理前先查订单状态如果已是已支付就直接返回成功不再重复执行出票逻辑。这个幂等判断在支付系统里是基本素质能讲出来非常加分。5.2 订单超时关闭定时任务是最适合毕设的方案订单超时关闭有两个常见方案定时任务扫描和延时队列。毕设强烈建议选定时任务用Spring自带的Scheduled就能完成不要一上来就用 RocketMQ 延迟消息、Redis 延迟队列这些重方案一个是代码量大再一个是你在有限时间内很难把方案的细节讲圆。具体实现订单表加一个timeout_at字段创建订单时设置为当前时间加15分钟。定时任务每分钟跑一次把所有状态为待支付且timeout_at小于当前时间的订单批量置为已关闭同时释放对应的座位和未扣减的库存如果是锁定库存模式需要回补。注意定时任务要加分布式锁避免集群环境下多个节点重复扫描同一批订单毕设单机部署可以不用但你要能说出这个问题。5.3 关单与支付回调之间的竞争最容易出的Bug这可以说是整个支付流程里最阴险的一个Bug。用户在下单后第14分59秒点击了确认支付而定时任务恰好在这一分钟扫描到这笔超时订单并将它关闭。支付回调随后到达发现订单已经是已关闭状态这时候怎么办如果让回调直接把订单改回已支付就会造成已关闭订单被重新激活的业务歧义。正确的处理方式是关单必须使用条件更新UPDATE orders SET status 5 WHERE id ? AND status 0 AND timeout_at NOW()受影响行数为0说明订单已经不在可关闭状态直接跳过。同样支付回调更新订单时也要带状态条件UPDATE orders SET status 1 WHERE id ? AND status 0。两边都做条件更新谁先执行都只有一个人的更新能成功另一个人的更新走不到业务分支这样订单状态才不会错乱。这个细节值得在答辩时专门拿出来讲。5.4 对账设计从功能完成走向系统可靠如果你还想再加一道保险可以写一个每日对账的定时任务凌晨2点扫描支付记录表和订单表找出支付记录为已支付但订单状态不是已支付的数据。这类数据如果出现就说明系统某个链路出了问题任务里可以选择自动修复或写入告警日志。一个毕设能主动做数据对账说明你已经在用生产系统的标准要求自己这种工程意识很能打动评委。6. 技术选型与项目结构让代码目录一眼就专业代码写得对不对老师不一定每一行都看但项目目录结构、技术栈选择、有没有写单元测试、有没有用日志规范这些一眼就能看出差距。6.1 选型清单与理由我的建议组合是Spring Boot 2.7.x Java 8 MyBatis-Plus 3.5.x MySQL 5.7 Redis 5.0 JWT Hutool Knife4j/Swagger ECharts。前端可以是Thymeleaf服务端渲染也可以是Vue3Element Plus的前后端分离。这里我重点说一句为什么推荐Spring Boot 2.7而不是3.x3.x最低要求Java17很多教学实验室的机器还在Java8环境不一致会让演示出问题。毕设第一要务是稳定别拿自己的毕业设计去赌环境兼容性。Pyhton、Node.js当然也能做类似系统但Spring Boot在这个场景下的优势是生态成熟度——MyBatis-Plus处理复杂SQL查询、Spring Security处理鉴权、Spring Task处理定时任务每个方向都有大量成熟方案。更重要的是国内Java岗位需求量仍然大用Java做毕设对未来简历的技术栈连续性是有帮助的。6.2 后端包结构与分层原则我的建议目录如下所示src/main/java/com/example/ticket/ ├── controller/ # REST接口层 ├── service/ # 业务逻辑层重点都在这里 ├── mapper/ # MyBatis-Plus的Mapper接口 ├── entity/ # 数据库实体类 ├── dto/ # 请求和响应的数据传输对象 ├── config/ # 配置类如Swagger、跨域、拦截器 ├── common/ # 统一返回结果、全局异常处理、常量 └── task/ # 定时任务核心原则是Controller层只做参数接收和结果包装业务判断全部下沉到Service层。全局异常处理用RestControllerAdvice统一拦截这些看起来是常规操作但在毕设评分里属于工程化素养的加分项很多人会忽略。6.3 三个能让演示效果翻倍的小功能第一个是数据可视化大屏。标题里提到的大屏数据可视化在毕设里落地并不复杂后端写一个统计接口按场次聚合销售数量、按日聚合销售额、按票档聚合销量排行前端用ECharts画柱状图和折线图放在后台首页。大屏这个东西评委特别吃视觉冲击数据一出来整个系统的完成度瞬间拉高。第二个是订单导出Excel。用EasyExcel封装一个导出接口管理员在订单管理页点一下就能把订单明细导成Excel文件代码量很少但演示效果意外地好。第三个是图形验证码登录。用Hutool的CaptchaUtil生成验证码图片登录接口校验防暴力破解。这个小功能单独做一个类就够但它能展示你对安全性的基本考虑。6.4 现场演示的环境踩坑点下面这些是我每次强调的查错清单数据库编码务必用utf8mb4否则赛事的生僻字或者emoji图标会存不进去前端引用的JS和CSS资源要下载到本地别用CDN在线地址否则现场断网时页面直接白板application.yml里数据库连接池参数预先调好8个并发可能看不出问题但你演示时一旦多开页面连接池不够会直接报错演示前先把Swagger页面打开答辩时可以直接调接口展示请求和响应比在页面上点来点去更稳。7. 答辩现场最容易被问倒的5个问题提前准备好腹稿我参加过不少答辩也模拟过很多次下面这几个问题是这个题目几乎绕不过去的提前准备好思路别在现场现想。7.1 你怎么证明你的系统真的防住了超卖参考思路分三步先讲悲观锁和乐观锁的原理差异说明你为什么选乐观锁然后展示你那条条件更新的SQL强调扣减和判断合并为一条原子操作最后拿出JMeter压测数据100库存、500并发、最终订单数恰好100。有原理、有代码、有数据这个回答就完整了。7.2 订单超时关闭之后用户又支付成功了怎么办这个问题考察的就是5.3里的竞争场景。回答思路关闭订单用的是条件更新status待支付 AND timeout_atNOW()支付回调更新订单同样携带状态条件两个操作只有一方的更新能成功。再加上回调接口的幂等设计保证同一笔订单不会出现两种终态。7.3 Redis分布式锁如果Redis挂了怎么办如实回答毕设场景下的取舍单机Redis在极端情况下的确会失效所以我在核心库存扣减上保留了数据库乐观锁作为最后防线分布式锁只是排队和限流的优化手段。如果Redis不可用乐观锁依然能保证不超卖只是并发能力会下降。这样回答既诚实又有深度不会把自己绕进去。7.4 为什么用JWT做登录鉴权不用Session要点一是无状态服务端不需要维护会话方便水平扩展二是适合前后端分离部署模式三是JWT里可以携带用户ID、角色等声明信息鉴权时直接解析。同时要主动补一句JWT的局限性token一旦签发无法在服务端主动失效所以我在代码里设置了合理的过期时间并且敏感操作会再校验用户状态。7.5 你把表和表之间的关系画一下为什么要这样设计这个问题要求你必须画得出ER图。重点讲三条线赛事→场次→票档→库存的配置线用户→订单→订单明细→票品的交易线支付记录→订单的对账线。讲清楚订单和票品为什么分两张表、库存为什么单独拆表基本就能让老师信服你是认真设计过模型的。答完这五问整个答辩的技术深度就已经拉开了。做这个题目做下来我最大的感受是同样的周期做图书管理系统的同学最后收获的是一堆CRUD做票务系统的同学收获的是一整套对并发、状态机、一致性这些真实工程问题的思考方式。哪怕毕业之后不碰Java这套解决问题的思路也是通用的。最后分享一个被我用过无数次的实操小技巧答辩演示前一晚把一次完整的购票流程用录屏软件录成短视频包括登录、选场次、选座、下单、支付、查看电子票。万一现场环境或者网络出了任何幺蛾子直接放录像效果比你满头大汗地现场点一遍好非常多。祝各位都能顺利答辩把毕业设计做成真正能写进简历的作品。
觉得有用,分享给同行:

为您的企业打造数字门面

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

立即咨询 →