选毕业设计题目的时候,很多人一听“演唱会订票系统”就觉得太老套,年年有人做。但真等到答辩,一批人却挂在同一个地方:票卖超了、订单状态对不上、数据库导出后怎么都跑不起来。原因很简单——这个题目表面上是增删改查,实际却要求你把Java后端、数据库设计和部署交付整条线都走通。下面这套基于Java的演唱会订票系统,包含完整运行源码、数据库脚本和部署教程,正好适合当模板去复现,再按自己的思路扩展。无论你是第一次做完整项目,还是想在演示环节少翻车,这篇实战拆解都能帮上忙。
我先把话说在前头:做这类系统,最忌讳把精力全花在界面上然后忽略业务完整性。订票系统的核心从来不是“能下单”,而是“在多人同时抢票时还能保证不超卖、订单不丢失、库存可恢复”。所以这篇文章我不会只贴代码,还会把数据库怎么拆、事务怎么控制、并发怎么验证、最后怎么打包交付给答辩环境,一步步讲清楚。你跟着走完,拿到的是一套能运行、能演示、能讲出设计思路的完整项目。
1. 为什么毕业设计选择演唱会订票系统——选题解读与整体技术路线
1.1 题目背后真正想考察的能力
很多同学选这个题,是因为觉得“订票”听起来比“图书管理”更有场景感,而且演唱会、演出这些词写进摘要里也好看。但从老师的角度,这个题目真正考察的并不是你有没有追星的热情,而是你有没有把一个完整业务闭环落地。
所谓业务闭环,指的是从用户注册登录、浏览演唱会信息、选择票档、提交订单、模拟支付,到后台管理用户和场次、处理退票、统计已售数量,这一整条链路必须能走通。任何一个环节断裂,答辩演示就会卡壳。比如用户下单之后订单永远停留在“待支付”,或者支付成功后库存却没扣减,这种问题一旦被当场问出来,基本很难圆场。
我看到的实际情况是:选这个题的人很多,但能把闭环走完的人不到一半。大部分卡在三个地方——库存扣减没有并发保护、订单状态随意修改、部署文档写得过于简陋导致换台电脑就起不来。这篇文章后半部分会逐一对应解决。
1.2 技术选型:为什么是Spring Boot + MyBatis + MySQL
关于技术栈,我先给出一个比较稳妥的组合,这也是目前最容易找到参考、答辩时也最好解释的方案:
| 层级 | 选型 | 说明 |
|---|---|---|
| 后端框架 | Spring Boot 2.7.x | 简化配置,自带内嵌Tomcat,部署时一个jar包搞定 |
| ORM框架 | MyBatis或MyBatis-Plus | 手写SQL便于在答辩时讲清查询逻辑 |
| 数据库 | MySQL 8.0 | 生态成熟,GUI工具多,演示和导出都方便 |
| 前端 | Thymeleaf模板或Vue3 | 二选一,建议后端为主则选Thymeleaf,省去跨域问题 |
| 构建工具 | Maven | 依赖管理清晰,打包命令简单 |
| 运行环境 | JDK 8或JDK 11 | 配合Spring Boot 2.7最不折腾 |
这里需要解释几个“为什么”。Spring Boot不用多夸,选择它的核心原因是内嵌Tomcat和自动装配,能让部署流程压缩到“装好JDK和MySQL、放一个jar包、启动”三步。如果选传统的Servlet/JSP项目,虽然老师看得懂,但环境配置和部署会比Spring Boot麻烦不少,而且一旦用了奇怪的Tomcat版本,可能出现javax.servlet和jakarta.servlet包名不一致的问题,浪费大量时间。
至于前端,每年都有人纠结要不要做一个完整的前后端分离项目。我的建议是:如果毕业设计以开发一个可运行的软件系统为目标,Thymeleaf模板引擎就够了。为什么?因为前后端分离意味着你至少要配置跨域、打包静态资源、处理Nginx反向代理,这些内容很容易在答辩时被追问到“你如何配置部署”。一旦你讲不清楚接口路径和反向代理的关系,反而暴露短板。
1.3 功能模块划分:给项目搭一个清晰骨架
根据“演唱会订票”这个业务,我把整个系统划分成用户端和管理端两块。用户端面向普通购票者,主要包含:
- 用户注册与登录,密码必须加密存储,不能明文落库
- 演唱会列表与详情展示,包括场次、时间、场馆、票档和剩余库存
- 票档选择与下单,下单时锁定库存并生成待支付订单
- 模拟支付与订单查询,支持取消超时未支付订单
- 个人中心,查看历史订单并处理退票
管理端面向运营人员,主要包含:
- 演唱会信息管理,可以新增、上下架演出场次
- 票档库存管理,可手动调整票档价格和初始库存
- 订单管理,查询所有订单并按订单状态过滤
- 数据统计,简单统计各场次销量和销售额
这个模块清单看起来很常规,但每一个点背后都对应一个数据库表的设计和一组接口。千万不要在项目开始时就直接写Controller,我见过太多人先写接口再补表结构,结果订单和库存对不上,最后推倒重来。正确顺序是先完成数据库设计,再写实体类,再写Mapper,最后才是接口和控制层。
2. 数据库模型设计:从票档、场次到订单的状态流转
2.1 核心表结构:为什么这么拆
订票系统的表结构并不是越多越好,而是要能支撑住业务状态的变化。我设计的这套项目包含以下核心表:
- user(用户表)
- concert(演唱会表)
- session(场次表)
- ticket_type(票档表)
- orders(订单表)
- payment_record(支付记录表,可选但推荐)
这里重点解释一下concert、session和ticket_type这三张表为什么必须分开。演唱会本身是“一场活动”,比如“某歌手巡回演唱会城市站”,它可能有同一个晚上开一场,也可能分为下午场和晚场,这就是两个session。而每个session下面通常有看台票、内场票、VIP票等不同票档,每个票档有独立的库存和价格。
如果把“演唱会、场次、票档”全部揉进一张表,库存记录会非常混乱。比如同一个演唱会不同场次的同一票档,价格可能不同,库存也不同。拆开之后,每次下单只需要锁定ticket_type表某一行记录的库存即可,逻辑非常清晰。
订单表是整张数据模型里最容易出问题的地方。我把订单拆成orders主表和可选订单项明细,但在这个毕设场景下,一个订单只对应一个票档,所以也可以不拆明细表。这里保留一个order_item表会让扩展更自然,也方便答辩时展示“为什么你要增加这个表”。
orders表的核心字段包括:订单号、用户ID、场次ID、票档ID、购买数量、订单总金额、订单状态、创建时间、支付时间、取消时间。其中订单号一定要独立生成,格式建议带日期和随机序列,比如“20241215 + 时间戳 + 用户ID后四位”。不要用数据库自增主键当订单号,因为那样太容易被人猜到订单规模,而且不便统一格式。
用户表和演唱会的字段没有太多悬念,但有一个细节要强调:用户表存密码时不要用MD5直接加密,MD5现在已经很容易被碰撞破解。要么用Spring Security自带的BCrypt,要么用JDK自带的SHA-256加盐处理。演示时老师不一定会看密码加密,但如果你主动讲这一点,会很加分。
2.2 订单状态字段设计与状态流转规则
订单状态是整个系统的灵魂。我在项目里使用字符串状态码,包括:
| 状态值 | 含义 | 流转方向 |
|---|---|---|
| PENDING | 待支付,下单后默认状态 | 可转为PAID,超时可转为CANCELLED |
| PAID | 已支付 | 可转为REFUNDED |
| CANCELLED | 已取消 | 终态 |
| REFUNDED | 已退款 | 终态,库存需回补 |
为什么不用数字0、1、2来表示状态?当然可以用,但字符串状态码在日志排查时更直观,而且写进代码里不会“0和1突然记不清代表什么”。不过如果你的数据库规范要求用TINYINT,那就对应改成0、1、2、3,这个没有绝对标准。
关键点在于:状态之间的转换一定要在服务层做,不能直接在Controller里写死几条update语句,更不应该把前端传来的状态值直接覆盖到数据库。正确的做法是,订单模块提供一个统一的状态变更方法,比如cancelOrder、payOrder、refundOrder,每个方法内部校验当前状态是否允许转换。例如一个已经支付的订单不能直接变成待支付,否则账务就对不上了。
为了实现“待支付超时自动取消”,我在数据库里不会依赖定时器,而是在Java应用里用Spring的@Scheduled任务扫描。这里最核心的SQL是更新那些创建时间超过15分钟且仍处于PENDING状态的订单,把它改成CANCELLED,同时把对应的票档库存加回去。这一步必须放在同一个事务里执行,否则会出现“订单取消成功但库存没恢复”的问题。
2.3 初始化数据:让演示看起来像一个真实项目
很多人的数据库脚本里只有表结构,没有数据。结果程序跑起来之后,页面上空空如也,演示时只能现场去后台添加数据,非常尴尬。我更建议把初始化脚本写完整,至少包括一个管理员账号、两个普通测试用户、三场不同状态的演唱会、每个演唱会下若干场次和票档。
初始化数据要遵循两个原则。第一,时间尽量设置成“相对当前日期动态偏移”,因为如果写死2024年12月31日,等到6月答辩可能已经过期。可以利用MySQL的DATE_ADD和NOW()函数来生成未来7天到30天的演出时间,这样不管什么时候导入数据库,都有一条“即将开始”的演唱会。第二,演唱会名称不要使用真实明星的名字,否则容易被觉得有侵权或不专业的问题,用“某歌手巡回演唱会”“城市音乐节特别场”这类虚拟名称即可。
我在项目里初始化了一个admin/admin123的管理员账号,不过这里有个小提醒:初始化脚本里的密码也不能直接是明文,最好通过项目里的工具类生成一个BCrypt密文再放进去。否则你说“密码加密存储”,结果脚本里一眼就看到明文,这个逻辑就穿帮了。
3. 核心业务代码拆解:库存扣减、下单事务与支付模拟
3.1 下单接口的设计与事务边界
下单是整个系统最需要谨慎的地方。我设计的下单接口接收四个参数:用户ID、场次ID、票档ID、购买数量。Controller层只做参数校验,业务逻辑全部放在OrderService里。关键代码如下:
@Transactional(rollbackFor = Exception.class) public Order createOrder(Long userId, Long sessionId, Long ticketTypeId, Integer quantity) { // 1. 校验用户是否存在 User user = userMapper.selectById(userId); if (user == null) { throw new BizException("用户不存在"); } // 2. 校验票档是否存在且已上架 TicketType ticketType = ticketTypeMapper.selectById(ticketTypeId); if (ticketType == null || ticketType.getStatus() != 1) { throw new BizException("票档不存在或已下架"); } // 3. 通过条件更新扣减库存,影响行数为0说明库存不足 int rows = ticketTypeMapper.reduceStock(ticketTypeId, quantity); if (rows == 0) { throw new BizException("库存不足,抢票失败"); } // 4. 生成订单号并插入订单记录 String orderNo = generateOrderNo(userId); Order order = new Order(); order.setOrderNo(orderNo); order.setUserId(userId); order.setSessionId(sessionId); order.setTicketTypeId(ticketTypeId); order.setQuantity(quantity); order.setTotalPrice(ticketType.getPrice() * quantity); order.setStatus("PENDING"); order.setCreateTime(new Date()); orderMapper.insert(order); return order; }对应的Mapper SQL是核心中的核心:
UPDATE ticket_type SET stock = stock - #{quantity}, sold = sold + #{quantity} WHERE id = #{ticketTypeId} AND stock >= #{quantity} AND status = 1这是一个典型的“条件更新扣库存”,靠数据库行锁来保证并发安全。为什么不用Java里的synchronized锁或者ReentrantLock?因为这种锁只在单个应用实例内有效,一旦项目部署成多实例集群,锁就失效了。而通过带条件的UPDATE SQL,数据库本身会对对应行加锁,多个并发请求同时执行时,只有一个能成功把stock减下去,其余请求因为stock >= quantity不成立,更新行数为0,事务回滚。这是目前小微企业和高并发秒杀场景里最常用的方案之一。
3.2 为什么事务不能跨方法,也不能把扣库存放在查询后面
有一个非常经典的Bug:先查库存是否充足,再执行扣减。比如“先select stock,判断stock > 0,然后再update ticket_type set stock = stock - 1”。在并发场景下,两个请求同时查到库存是1,都认为可以买,然后都执行update,最终库存变成-1,超卖发生。原因就在于“查”和“扣”不是一个原子操作。
正确的思路是放弃“先查再扣”,直接把“库存充足”这个条件写进UPDATE的WHERE子句里。当update影响行数为1时,说明本次扣减成功;影响行数为0时,说明库存不足或票档已下架。这个设计在代码注释里一定要写清楚,否则写完过两周你自己都可能改成“先查再扣”的老写法。
另外要注意@Transactional注解的调用方式。如果在一个类内部的另一个方法里调用createOrder,事务是失效的,因为Spring的事务代理只对通过代理对象的外部调用生效。如果是自调用,需要把方法拆分到不同Service,或者通过AopContext拿到代理对象。这个问题也是面试问答里常被问到的点,一定要能讲明白。
3.3 模拟支付:不接真实支付渠道的做法
毕业设计阶段很难通过支付宝或微信支付的商户审核,所以项目里要做一套“模拟支付”。模拟支付不是乱写一个接口把订单状态改成PAID就叫完成,而是尽量模拟真实支付的流程。
我的实现方式是增加一张支付记录表payment_record。用户点击支付后,后端先生成一条支付记录,状态为“支付中”,然后返回给前端一个模拟的支付二维码图片。二维码里面其实是一段URL链接,内容指向一个“确认支付”接口。用户扫码或在页面上点击“我已支付”,后端再异步调用一次订单状态变更,将订单从PENDING改为PAID,同时更新支付记录。
这样做的好处是:答辩时可以演示“订单待支付→调起支付→支付成功后订单状态变化→个人中心能看到已支付订单”的完整链路,同时还能讲解支付记录与订单状态的一致性。如果上来就做成了点一下直接改状态,老师大概率会问“支付结果怎么确认?订单状态和支付状态如何保持一致?”到时候你就很难展开讲了。
模拟支付接口里还要注意幂等性:同一个待支付订单,点击多次“确认支付”,不能重复把状态从PENDING改成PAID,更不能重复充值累计金额到订单里。加一个状态校验即可,已经是PAID就直接返回成功,不执行任何更新。
3.4 超时取消与库存回补:定时任务的正确打开方式
超时未支付订单自动取消,我用的是Spring的@Scheduled注解。为了不干扰主流程,我把定时任务单独放一个类,用fixedDelay每30秒扫描一次。扫描逻辑分为两步:
-- 第一步:查出所有超时待支付订单 SELECT id, ticket_type_id, quantity FROM orders WHERE status = 'PENDING' AND create_time < DATE_SUB(NOW(), INTERVAL 15 MINUTE) LIMIT 200; -- 第二步:逐条取消订单,并回补库存 UPDATE orders SET status = 'CANCELLED', cancel_time = NOW() WHERE id = #{orderId} AND status = 'PENDING'; UPDATE ticket_type SET stock = stock + #{quantity}, sold = sold - #{quantity} WHERE id = #{ticketTypeId};这里又出现一个关键点:两步操作必须放在同一个事务内,否则第二步回补库存失败时,订单已经取消了但库存没恢复。我在定时任务方法上同样加了@Transactional,并且使用“先更新订单状态再回补库存”的顺序,尽量避免产生瞬时不一致。
如果你想让项目看起来更有技术含量,可以引入Redis的过期键监听来取消订单。但Redis方案需要额外安装、配置和解释,对毕业设计来说性价比不高。把定时任务讲清楚,再说明为什么选择扫描式实现而不是Redis监听,这本身就是一次不错的答辩回答。
4. 部署全流程:从本机运行到可演示的项目交付
4.1 环境准备:最容易被忽略的版本匹配
部署环节是每年翻车最集中的区域。我看过太多人代码在自己电脑上跑得好好的,一到机房或者换一台笔记本就疯狂报错。为了减少这类问题,我先给出一份明确的环境配置清单:
- JDK版本:8u201以上或JDK 11
- Maven版本:3.6.3或更高
- MySQL版本:5.7或8.0均可,但不同的连接配置有差异
- IDEA或Eclipse均可,IDEA更顺手
- 浏览器:Chrome或Edge
这里要特别提醒版本匹配问题。Spring Boot 2.7.x配JDK 8非常稳定,如果非要用JDK 17,就选择Spring Boot 3.x,但Spring Boot 3.x的包名结构和部分配置跟2.x不同,网上很多教程都是2.x的,你会发现复制粘贴后报一堆找不到类。对于毕业设计,没必要追新,稳定压倒一切。
4.2 数据库初始化与数据源配置
拿到项目之后,第一步一定是初始化数据库,而不是先启动程序。我用MySQL命令行或者图形化工具执行项目根目录下的db_concert.sql脚本。这个脚本里包含了建库、建表、插入初始化数据三个部分。如果你看到别人给的脚本只有建表没有数据,一定要补数据,否则演示的时候页面是空的。
数据源配置在src/main/resources/application.yml,这里有一个超高频报错点:MySQL 8的时区问题。如果你连接MySQL 8报错“The server time zone value 'Öйú±ê׼ʱ¼ä' is unrecognized”,或者出现拒连,多半是因为没有在连接串上加上时区参数。
spring: datasource: url: jdbc:mysql://localhost:3306/concert_db?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true username: root password: your_password driver-class-name: com.mysql.cj.jdbc.DriverallowPublicKeyRetrieval=true也是MySQL 8连接时常见的问题来源,不少教程没写这个参数,导致连接报错。这个参数连同serverTimezone一起写进去,基本能解决掉90%的数据源连接问题。
4.3 本地启动三步走
数据库准备好之后,启动项目就非常简单。我习惯分成三步,方便排查问题:
- 在项目根目录执行mvn clean package -DskipTests,先确认项目能正常打包。这一步如果报错,优先看有没有缺依赖或者下载失败。
- 用java -jar target/concert-system-1.0.0.jar启动打包出来的jar包,观察控制台日志。
- 浏览器访问http://localhost:8080,看到登录页说明项目已经起来了。
如果你用的是IDEA,也可以直接从主启动类的main方法运行。两种方式的区别在于:IDEA运行时用的是编译后的classes目录,而打包运行用的是可执行jar。我建议至少在答辩前用jar包方式完整跑一遍,因为这样更接近“交付给别人的运行形态”。
4.4 打包交付物清单与给别人的机器部署
毕设交付不只是给老师看代码,还需要提供一套别人能部署的完整材料。我建议按以下结构整理交付包:
concert-system/ ├── db_concert.sql # 数据库初始化脚本 ├── README.md # 部署说明和项目介绍 ├── target/concert-1.0.0.jar # 可执行jar包 ├── docs/演示视频.mp4 # 完整操作录屏 └── src/ # 源码这份交付清单对应了题目里的“部署教程+可完整运行源码+数据库”。这里有一点心得:README.md一定要写清楚“环境要求、启动步骤、默认账号、常见问题”。不要让接收人自己猜,更不要只给一句“打开IDEA运行”。机房里的电脑通常没有Maven、可能也没有配置好JDK环境变量,有些甚至没有图形化MySQL工具。遇到这种情况,最省事的方案是把MySQL也做成绿色版,或者提前在机房里装好环境再导出演示视频备用。
4.5 常见运行错误与速查解决办法
部署阶段遇到最多的问题,我整理成一个速查表:
| 错误现象 | 可能原因 | 处理办法 |
|---|---|---|
| 启动报数据库连接失败 | 数据库服务没开、账号密码错误、URL时区参数缺失 | 检查MySQL服务,核对application.yml |
| Table 'xxx' doesn't exist | 没有执行初始化脚本或连错库 | 重新导入db_concert.sql,核对库名 |
| Unknown column 'xxx' | 实体类字段与表字段不一致 | 检查Mapper的SQL与表结构 |
| 端口被占用 | 8080被其他程序占用 | 在application.yml改server.port |
| jar包启动即退出 | 缺少数据库驱动或配置错误 | 看日志第一段异常,一般是数据源问题 |
| 登录后中文乱码 | 数据库、连接串、页面编码不一致 | 统一为UTF-8,连接串加characterEncoding=utf8 |
这里再补一个建议:不要使用多个自定义端口,不要开启context-path,不要加奇怪的Servlet配置。老老实实默认8080和根路径“/”,部署时最省心。花里胡哨的配置到了答辩机器上全是坑。
5. 毕业设计答辩前必须踩平的坑:并发、一致性与演示准备
5.1 用JMeter压测验证库存不超卖
很多人的程序在“单人操作”时完全正常,一旦老师问“多人同时抢票怎么办”,就开始紧张。为了证明你的系统确实能抗住并发,必须提前做一次压测,并记录下测试结果,最好截图放进答辩PPT里。
推荐使用JMeter,不用写代码,图形化配置就行。我一般配置200个线程同时访问下单接口,让它们抢同一个票档的10张库存。压测结束后检查两件事:第一,订单表里成功生成的订单数量是否等于10;第二,票档的stock字段是否从10变成了0,没有变成负数。
在压测之前,你必须先把“扣库存”SQL改成带条件更新那种写法。否则你会看到sold数量超过初始库存,这就是超卖。等到答辩现场再改,基本来不及。压测截图会是你讲“并发控制”这段内容时最有力的证据。
JMeter设置的大致思路是:创建线程组,线程数200,Ramp-Up Period设为0,表示同时打进来;添加HTTP请求,填写下单接口的URL、参数;再添加断言和聚合报告,查看错误率。如果错误率很高,先看是因为库存不足而正常抛出异常,还是因为代码本身存在异常。这两种情况含义完全不同。
5.2 数据一致性检查清单:哪些地方最容易偷偷不一致
压测只是其中一环,还有几个数据一致性细节,很多人做完项目都不会主动检查。我列出几个自己在测试时必查的点:
- 订单取消后,票档库存是否回补,sold是否回减
- 退款成功后,对应票档的库存是否重新加回
- 用户重复提交同一个下单请求,是否生成了多个订单
- 同一账号在订单处于PENDING时,是否还能继续下单
- 管理端手动下架票档后,用户是否还能通过直接访问URL下单
- 支付成功回调多次,订单状态是否仍然正确
这些点每个都对应一个真实业务场景。比如“退款后库存回补”:我在退款接口里使用了与下单相反的条件更新,订单状态改为REFUNDED时,执行“UPDATE ticket_type SET stock = stock + quantity, sold = sold - quantity WHERE id = ?”。这里同样要求两个操作在同一个事务里,顺序不能调换。
在答辩前,建议写一个小的测试脚本,把这些场景依次跑一遍,每跑一步就检查一次数据库。不要嫌麻烦,我见过太多学生演示到一半,发现上一轮测试产生的脏数据还留在库里,导致页面数字看起来不对。所以还要准备一个“恢复演示数据”的脚本,一键把订单表和库存表重置到初始状态。
5.3 答辩演示路径设计与提问预演
演示并不是打开系统随机点几下就能结束的。你需要提前设计一条“演示故事线”,让每一步操作都服务于讲解你的设计亮点。我推荐的演示顺序是:
- 先以用户身份注册并登录,展示密码加密存储的数据库截图
- 进入演唱会详情页,找一个库存充足的票档下单
- 下单后停留在待支付页面,向老师说明订单状态是PENDING
- 立刻到数据库或后台查看该票档的库存变化,证明库存已被锁定
- 点击模拟支付,展示订单状态变为PAID,个人中心出现已支付订单
- 再找一个票档,下单后不去支付,演示超时取消与库存回补的过程
- 管理员进入后台,查看订单列表和演唱会销量统计
这套路线里每一步都对应一个技术点。比如第4步是为了证明“下单时即扣库存”的业务逻辑而不是“支付时才扣库存”;第6步是为了证明定时任务和事务回滚的有效性。
答辩提问预演这块,最常被问到的问题大概是:
- “如果库存只剩1张,但两个人同时下单,你怎么处理?”答:数据库条件更新,影响行数判断。
- “订单状态是谁来修改的?前端能不能直接改?”答:前端只能调接口,状态转换逻辑全部在服务层定义,接口内部校验状态。
- “为什么不在支付时再扣库存?”答:为了防止超卖和超时未支付导致的库存锁死,下单时扣减并配合超时回补。
- “你的项目数据库有哪些表?为什么这样设计?”答:按用户、演唱会、场次、票档、订单、支付记录拆分,保证库存独立和订单状态流转清晰。
这些回答必须能脱口而出,而不是背稿。你不用把所有代码背下来,但要能立刻在白板上画出核心表关系图和一个典型的“下单→支付→取消”状态图。
6. 从这份毕设源码里还能扩展出什么——我个人最推荐的三条进阶路线
如果你做完基础版本之后还想加点亮点,我建议优先考虑下面三条路线,它们都是在现有代码结构上做加法,不伤筋动骨。
第一,加一个简单的选座功能。现在系统是按票档购票,不涉及具体座位。如果要做选座,可以增加一个座位表和座位锁定记录,页面点击座位时先锁定,超过一定时间未支付自动释放。这个功能实现起来并不难,但能显著提高项目的完整度。
第二,把超时取消从定时扫描换成消息队列或Redis监听。定时扫描最简单,但会存在最多30秒的延迟。如果换成延迟队列,订单支付超时后能更及时释放库存,这个点作为拓展方向在答辩里讲出来会很有价值。
第三,引入简单的Redis缓存演唱会列表或库存热点数据。当门票初始库存充足时,先把库存写入Redis,扣减时使用Lua脚本保证原子性。这会让你在答辩时可以讲一讲“缓存与数据库的一致性”这个常考主题。
我自己在实际做完这类项目后的最大体会是:毕业设计最宝贵的收获不是“会了Spring Boot”,而是把“从需求分析、数据库设计、接口开发到部署交付”这整条链路走了一遍。只有真正去处理过超卖、遇到过时区报错、在别人的电脑上从头部署过一次,你才会明白工程里那些看似琐碎的细节到底有多重要。建议你做完后找一个不懂这个项目的人,让他只凭README去部署一遍,那是一面最真实的镜子。
最后再分享一个小技巧:正式答辩前,把数据库导出脚本和恢复演示数据的脚本放同一个文件夹里,并且自己在离线环境完整走两遍演示流程。你可以不依赖网络、不依赖任何IDE,纯靠jar包和MySQL把系统跑起来。做到这一步,这个项目才算是真正从纸上落到了地上。