
1. 项目概览与技术选型思路做毕设选题的时候很多人第一反应就是“找个能跑通、能演示、好答辩的”这话没毛病但实际操作起来容易两个极端要么选了个超级管理系统增删改查四件套做完自己都觉得虚要么盲目上微服务、分布式、中间件全家桶结果六个月搭环境三个月调 Bug最后连个完整流程都跑不通。“基于 Spring Boot 的亚冬会网上门票销售系统”这个题目属于典型的“看起来简单做起来有货”的类型。表面上是卖票但往深了挖它涉及用户认证、商品票务管理、库存控制、订单状态机、支付回调对接、数据统计分析甚至还能延伸出大屏可视化展示。麻雀虽小五脏俱全技术点能撑起一篇像样的毕业设计论文也能在答辩时让老师觉得“这孩子是真做过东西的”。这个项目主技术栈是 Spring Boot MyBatis Plus MySQL前端可以配 Vue 或者直接用 Thymeleaf 模板引擎再引入 ECharts 做大屏可视化。为什么推荐这套组合原因很现实Spring Boot 是目前 Java 系毕业设计最主流的选择生态成熟资料多遇到报错一搜全有解MyBatis Plus 把单表 CRUD 的代码量压缩到极致省下来的时间可以砸在业务逻辑上ECharts 是纯前端图表库不需要额外的后端绘图服务大屏直接引 CDN 就能出效果。前端部分如果你 Java 基础一般不建议硬上前后端分离。用一个轻量级的 Vue3 Element Plus 简单页面配后端接口是够用的但如果你连 Vue 都还没摸过老实选 Thymeleaf把页面和服务端渲染玩明白整体完成度一样很高。我见过太多人死在“前后端分离”这四个字上其实项目完成度看的是业务闭环不是技术栈多新。2. 系统整体设计与核心模块拆解在动手写代码之前先把系统的整体结构捋清楚。亚冬会门票销售系统的用户角色至少要有三种普通用户购票者、票务管理员后台维护、系统管理员用户管理数据总览。如果有余力可以再加一个“场馆管理员”角色负责对接实体检票但作为毕设三种角色已经完全够用了。从功能模块上看系统应该分成两大部分用户端和后台管理端。用户端核心功能是赛事浏览、门票选购、下单支付模拟、订单查询后台端核心功能是赛事管理、场次管理、票档管理价格/库存、订单管理、用户管理、数据统计。大屏可视化则是一个独立的展示页面通常挂在后台首页或者作为一个独立路由放出来给答辩演示用。![系统功能结构图——这里用文字描述用户端赛事列表、票务选择、购物车/下单、订单中心、个人中心后台端赛事/场次/票档管理、库存管理、订单管理、用户管理可视化大屏实时销售数据、赛事热度排行、票价分布、区域销售占比]这里重点说说“场次-票档”这个设计很多新手一上来就设计一张“票表”字段是一堆票价、座位、库存然后所有操作都怼在这一张表上看着简单实际上后期扩展一碰就碎。正确的做法是把票务拆成三级结构赛事比赛名称、时间、地点—场次某一场具体的比赛时间、场馆区域—票档票价、座位区域、总库存余量。举个例子亚冬会冰球项目先在“赛事”里建一个冰球赛事然后在“场次”里建小组赛/决赛若干场再在“票档”里给每个场次划分 A类、B类、C类三档票价。用户购票的时候选的是某个场次下的某个票档而不是笼统的“冰球赛门票”。这样设计的好处非常明显库存管理精确到票档方便对不同区域设置不同价格座位逻辑清晰后续如果要扩展具体的座位号直接在票档下再加一张“座位表”即可统计报表也更灵活可以按场次、票档维度分别出数据。3. 数据库设计与表结构规划数据库是整套系统的地基地基歪了后面业务代码写多少都是白费。这套系统的核心表并不算多但每张表的字段设计都有讲究我在设计的时候踩过不少坑分享一下最终成型的表结构方案。user 表用户表核心字段包括 id、username、passwordBCrypt 加密存储、real_name、id_card身份证号购票需要实名、phone、role1 普通用户 / 2 票务管理员 / 3 系统管理员、create_time。注意 password 字段一定不要明文存储至少用 BCrypt 加密这既是安全底线答辩的时候也能作为一个加分的亮点讲。event 表赛事表记录基础赛事信息字段包括 id、event_name、event_type比如冰球、花样滑冰、短道速滑等、start_date、end_date、location举办场馆、description、cover_url赛事封面图、status未开始/进行中/已结束。这张表是纯基础数据表可以预先导入几条测试数据方便前后端联调。session 表场次表一场赛事下会有多个比赛场次字段包括 id、event_id、session_name小组赛/半决赛/决赛、session_time具体开赛时间、venue_name具体场馆名称、session_status。注意一个赛事下挂多个场次所以 event_id 加普通索引查询某个赛事的所有场次是高频操作。ticket_type 表票档表这是整张票务体系的定价核心字段包括 id、session_id、type_nameA类票/B类票/C类票、price、total_stock总库存、remaining_stock剩余库存、sale_status售卖中/停售。这里有一个关键设计total_stock 和 remaining_stock 分开存每次售出一张票就扣减 remaining_stock同时校验它不能为负数这是后期做购买逻辑的核心字段。![订单状态流转图——用文字描述待支付Pending→ 已支付Paid→ 已出票Issued→ 已使用Used/已退票Refunded以及待支付超时作废Cancelled]order 表订单表核心字段包括 id、order_no订单号唯一、user_id、session_id、ticket_type_id、quantity购票数量、total_amount总金额、status0 待支付 / 1 已支付 / 2 已出票 / 3 已使用 / 4 已退票 / 5 已取消、pay_time、expire_time、create_time。订单状态机是后面逻辑实现的重头戏很多隐藏 Bug 都出在状态流转没约束好。payment_log 表支付日志表模拟支付回调时记录支付信息字段包括 id、order_no、pay_amount、pay_method、pay_status、callback_time。毕设不接真实支付渠道可以用一个“模拟支付”接口替代但是日志表建议保留一方面让流程序列更完整另一方面论文里能多写一节的实现思路。从整体设计来看这套表结构既能满足正常的业务流转又留了足够的扩展口子。如果后续想增加座位级别的精细管控只需要在 ticket_type 下挂一张 seat 表想增加营销功能加一张 coupon 表关联 user_id 和 order_id 即可。好的表结构不是一步到位设计出来的而是给未来留了可演进的余地。4. 后端业务流转与核心逻辑实现后端核心逻辑的编写顺序建议是构建项目骨架 → 完成用户认证 → 实现赛事/票档管理 → 实现购票下单流程 → 完成订单管理与状态流转 → 实现数据统计接口。如果你第一次写这种完整项目千万不要一上来就想把大屏做出来先把业务流水线跑通让所有操作串成一个闭环可视化只是把闭环里的数据“展示”出来而已。4.1 用户认证与权限控制用户认证这块建议直接用 JWT不是因为它比 Session 高级多少而是因为如果你后期想拆前后端分离JWT 是无状态的不依赖服务端存储会话重构成本低。实现也不复杂用户登录成功后后端根据 userId 和 role 生成一个带过期时间的 Token返回给前端前端每次请求在 Header 里带上 Token后端用一个拦截器统一解析校验把 userId 放到请求上下文里。关键代码片段如下Component public class JwtInterceptor implements HandlerInterceptor { Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { // 放行登录接口 if (request.getRequestURI().contains(/login) || request.getRequestURI().contains(/register)) { return true; } String token request.getHeader(Authorization); if (token null || token.isEmpty()) { throw new BusinessException(401, 未登录或登录已过期); } // 解析token异常说明token无效或过期 Claims claims JwtUtil.parseToken(token); request.setAttribute(userId, claims.get(userId)); request.setAttribute(role, claims.get(role)); return true; } }这里有两个细节容易踩坑。第一拦截器一定要放行登录/注册接口否则会自己拦自己第二判断用户角色时不要每次都查数据库直接从 Token 里取 role 字段减少一次无谓的 IO。用个简单的 RequiresRole 注解配合拦截器做接口权限控制会比在每个 Controller 方法里手写 if 判断整洁得多。4.2 购票流程与库存扣减购票是整个系统的业务核心也是最容易出并发问题的地方。很多新手一开始会这么写先查询剩余库存判断库存大于购买数量再执行 UPDATE 扣减库存。这在低并发下跑起来没问题但一旦两个人同时抢最后一张票就会出现“库存超卖”——两个人都查到剩余 1 张票都通过校验都扣减成功最后库存变成负数。解决超卖的核心思路是“让数据库自己判断”而不是“先查后改”。改造后的逻辑是执行 UPDATE 操作时在 WHERE 条件里加上“剩余库存大于购买数量”的判断通过受影响行数来判断本次扣减是否成功。具体 SQL 可以写成UPDATE ticket_type SET remaining_stock remaining_stock - #{quantity} WHERE id #{ticketTypeId} AND remaining_stock #{quantity}如果受影响行数为 0说明库存不足本次抢购失败直接返回“库存不足”的提示然后回滚整个事务。这套方案本质上是用数据库行锁来保证扣减的原子性代码简单逻辑清晰性能也够。注意不要在扣库存前后做无谓的查询操作减少事务持有时间下单接口的事务里把库存扣减放在最后一步执行其他操作在前面完成这样整个事务持有行锁的时间最短。4.3 订单生成与状态流转订单状态的设计是这套系统的灵魂所在。建议用明确的常量类做状态定义避免魔法数字散落在代码里。订单的生成流程是这样的用户在前端提交购票请求场次 ID、票档 ID、数量后端先做参数校验然后查询票档价格计算订单总额生成唯一订单号创建订单记录状态为待支付同时设置超时时间比如 30 分钟。这里注意订单号的生成不能用简单的 UUID因为答辩时老师可能会让你展示订单列表UUID 太长太丑看起来不真实。比较常见的做法是用时间戳 随机数组合或者用雪花算法生成一个纯数字的 19 位订单号视觉效果和专业感都会好很多。订单创建后用户发起支付。毕设里当然不能接真实支付宝或微信支付但可以做“模拟支付”接口前端点“立即支付”后后端生成一条支付日志把订单状态从待支付更新为已支付再异步触发“出票”操作。如果你想让流程更真实一点可以写一个简单的回调模拟接口用 Thread.sleep 模拟支付处理延迟再调用自己的“支付成功回调”方法完成状态更新这也是论文里一个很好的亮点描述。退票流程则是反方向的状态流转已支付的订单提交退票申请管理员审核通过后将订单状态改为已退票同时把购买数量返还到票档的剩余库存里。这里有一个细节容易忽略退票后返还库存要用增量操作而不是直接赋值即remaining_stock remaining_stock #{quantity}避免并发下覆盖其他用户的购票结果。![用户购票完整时序逻辑——用文字描述用户选择赛事项→选择场次→选择票档数量→提交订单→创建待支付订单并锁定库存→模拟支付→支付回调更新订单状态→出票成功→我的订单查看电子票→入场核销]4.4 查询性能与热门接口优化门票系统的查询压力主要集中在赛事列表、场次详情、票档余票这三个接口上。尤其是临近比赛日用户频繁刷新余票数量如果每次都实时查库数据库压力会明显增大。最直接的优化手段是引入 Redis 缓存这是毕设里非常值得写的一个技术亮点。具体做法是在查询赛事列表时先把赛事基本信息和对应的场次数据序列化成 JSON 存到 Redis设置一个较短的过期时间比如 5 分钟查询时先走缓存缓存未命中再查数据库余票数这种变动频繁的数据可以设置 30 秒的过期时间或者干脆在购票成功和退票成功时主动删除相关的缓存 Key让下一次查询强制回源。虽然只是两行 Redis 代码的事但在答辩时能让你的系统从“玩具”直接晋升为“有点工程意识”。5. 大屏可视化模块的思路与实现要点大屏数据可视化是这个项目的“颜值担当”也是答辩时的加分项。但要注意它不是一个独立的功能模块而是对现有业务数据的二次加工和展示。先理清大屏要展示哪些数据再去写接口千万别为了做图表而造数据。结合门票销售系统的业务特征大屏上适合展示的指标至少有这几块总销售额、今日新增订单数、总出票量这些是核心业务指标各赛事销售占比饼图用来直观看出哪个项目最受欢迎近 7 日销售额趋势折线图展示销售波动各票档销量分布柱状图分析价格区间受欢迎程度实时订单滚动列表让大屏有“实时”的动感效果。这些数据在后台的 order 表和 ticket_type 表里都能统计出来本质就是几个 GROUP BY 聚合查询。前端实现上ECharts 是最稳妥的选择。5 分钟就能上手配置项网上有海量案例直接把 option 里的 data 换成后端返回的数据即可。布局上建议采用“中间主图 两侧辅图”的经典大屏模式顶部放核心业务指标卡片中间放一个大的赛事销售占比环形图左侧放近 7 日趋势折线图右侧放票档销量柱状图底部放实时订单滚动列表。背景用深色渐变卡片带微边框阴影大屏的氛围感立刻就出来了。这里要提醒的是数据接口的设计要给前端省事。后端一次性提供一个getDashboardData()聚合接口返回一个包含所有图表数据的 Map 结构前端一个请求拉到全部数据分别填充各个图表。这个接口内部依次调用各统计数据的 Service 方法再组装成统一的返回结构。对于大屏展示单个大聚合接口比一堆零散接口实用得多前端代码也更整洁。无论你入库的是真实数据还是造出来的测试数据都要保证几个表之间的数据是自洽的。比如 order 表的销售额要和 payment_log 表的支付金额能对上ticket_type 的 remaining_stock 要等于 total_stock 减去所有有效订单的数量。只有数据自洽大屏上的数字才有说服力答辩时被深挖数据来源也从容。为了让演示效果更丰满可以写一个“模拟数据生成器”随机生成一批用户、订单记录时间跨度拉长到近 30 天这样趋势图看起来就非常自然。6. 典型问题排查与避坑经验整个开发过程中我遇到了不少问题有些是低级的有些是隐蔽的统一整理成速查表你做到这套系统时大概率能碰到其中几个。问题现象根因分析解决方案与预防启动时提示端口被占用本机有多个服务占用了 8080 端口在 application.yml 里换端口比如 8081或者用kill -9清理占用进程登录后访问接口一直 401JWT 拦截器没有放行预检请求或者 Token 解析异常检查前置的跨域配置OPTIONS 请求直接放行解析 Token 的异常要 catch 住返回 401 而不是 500库存扣成了负数先查询后修改并发下超卖改成条件 UPDATE 原子扣减受影响行数为 0 直接报错回滚事务订单表数据越来越多列表查询变慢没有分页甚至全表查询MyBatis Plus 分页插件走起订单列表强制分页大屏数据展示 NaN 或 NULL统计 SQL 里 SUM 空值或者取模除数为 0聚合函数用 COALESCE(SUM(x), 0)除法前先判断分母是否为 0模拟支付后订单状态不更新回调接口没有按事务处理支付回调模拟接口加 Transactional状态更新失败会整体回滚实体类属性查出来后全是 null数据库字段是下划线实体是驼峰没有开启映射MyBatis Plus 确保 map-underscore-to-camel-case 为 true通常默认开启除了表里列出的问题再额外分享两个独家经验。第一建表语句一定要手动写好保存下来不要总是依赖自动生成。答辩时老师如果问“你有几张表”你得回答得如数家珍而不是打开数据库现查。第二明确的字段注释不要省字段含义、状态枚举、数值单位都写清楚。我见过不少同学的数据库表字段名看不懂是干嘛的自己写的代码过两周都不认识了。顺手的注释能让你在准备论文和答辩时省下大量时间。还有一个隐蔽的问题容易出现订单超时未支付的状态处理。如果你只设置了 expire_time 但没有任何定时任务来扫描过期订单就会出现一堆挂着“待支付”的脏数据会直接影响大屏统计的准确性。最简单的方案是写一个定时任务每分钟扫描一次待支付且超过 30 分钟的订单将它们改为已取消状态同时把锁定的库存返还回去。一个 Scheduled 注解就能解决别看不起这几行代码它能让整个系统的数据严谨性上一个台阶。7. 最后补充一点个人建议我在最后再分享一个小技巧。整个项目做完之后一定不要急着写论文先回到整体设计层面重新审视一遍哪些表是多余的哪些逻辑是可以合到一起的哪些接口是根本用不到的。我当初做完第一版时功能全跑通了但代码里有几个完全没被用到的工具类还有几个冗余的查询接口清理掉之后代码整体清爽不少。写论文的时候会更加顺利答辩时被问到代码细节也能更有底气。源码和教程资料网上确实很多成套的、上万套的都有但真正把它变成你自己的东西核心不在于“下载”而在于“重新过一遍”。哪怕你在别人源码基础上改的也要做到随便指一个方法你都能说清楚它的输入输出和调用链路。能做到这一点毕设答辩基本就是稳稳的过了。