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、password(BCrypt 加密存储)、real_name、id_card(身份证号,购票需要实名)、phone、role(1 普通用户 / 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_name(A类票/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(总金额)、status(0 待支付 / 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清理占用进程 |
| 登录后访问接口一直 401 | JWT 拦截器没有放行预检请求或者 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. 最后补充一点个人建议
我在最后再分享一个小技巧。整个项目做完之后,一定不要急着写论文,先回到整体设计层面重新审视一遍:哪些表是多余的,哪些逻辑是可以合到一起的,哪些接口是根本用不到的。我当初做完第一版时功能全跑通了,但代码里有几个完全没被用到的工具类,还有几个冗余的查询接口,清理掉之后,代码整体清爽不少。写论文的时候会更加顺利,答辩时被问到代码细节也能更有底气。
源码和教程资料网上确实很多,成套的、上万套的都有,但真正把它变成你自己的东西,核心不在于“下载”,而在于“重新过一遍”。哪怕你在别人源码基础上改的,也要做到随便指一个方法,你都能说清楚它的输入输出和调用链路。能做到这一点,毕设答辩基本就是稳稳的过了。