1. 为什么“火车订票”是毕业设计里的常青树
每年到了选题季,总有一批同学在“图书管理”“宿舍管理”“班级管理系统”里打转,然后对着导师问:“有没有稍微有点区分度、又不至于做不完的题目?”我一般都会提到一个方向——基于Spring Boot的火车订票管理系统。
理由其实很朴素:火车订票这个业务域,天然带了三样东西——高并发查询、库存一致性约束、订单状态流转。 这三样恰恰是企业级Java开发里最常被问到的核心能力。图书管理通常只涉及单表CRUD,宿舍管理考验的是关联查询,而火车票系统,它要求你在“一张票能不能卖两次”这个问题上给出严谨的答案。这个“答案”的形成过程,就是你从“会写代码”到“会设计系统”的分水岭。
再说现实一点。2026年的选题库里,Spring Boot依然是绝大多数高校Java方向的首选框架。它的生态足够成熟,资料铺天盖地,遇到问题很容易搜到解决方案,但正因为资料多,杂音也多——很多人照着博客搭出了CRUD,却回答不了“为什么事务没生效”“为什么两个人同时买最后一张票时超卖了”这类问题。本文想跟你聊的,不是再贴一遍配置文件,而是把一套完整的火车订票系统从设计到落地的全过程拆开,讲清楚那些数据库表为什么这么建、订单状态为什么这么流转、并发扣减为什么选这个方案。
适合谁看?两类人:一是正在做这个课题的在校生,想从“能用”做到“能讲”;二是初级后端开发者,想通过一个完整业务闭环理解Spring Boot在实际项目中的工程化写法。文里的代码都是按主流稳定版本写的,环境是Spring Boot 2.7.x + JDK 1.8,这也是目前兼容性最稳的组合。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 技术选型与工程结构:别做“全家桶式”堆砌
2.1 Spring Boot版本选型:版本太高真不是好事
先说一个在热搜词里反复出现的点:“springboot版本太高”。“2026精选课题”这个字眼很容易让人想直接上Spring Boot 3.x,但你得清楚一个问题——很多毕业设计所需的第三方整合资料,都是基于2.x和JDK 1.8的。 一旦上了Spring Boot 3,JDK要求17起步,部分老版本MyBatis、PageHelper的兼容方案都要改,网上能搜到的报错解决方案锐减。
我给你的建议是:能用2.7.x就不要上3.x。Java 8 + Spring Boot 2.7.18,这是目前Java就业市场上存量项目最常见的组合,也是你在答辩时最有底气的组合——因为面试官或评委大概率就在用这套。
如果确实想尝试Spring Boot 3,也请评估好一个前提:你的MyBatis版本、连接池、Redis客户端是否都已经升级到兼容版本,不然调试成本会直接翻倍。
2.2 依赖清单与实际版本对照
我翻了一下手头维护的一个同类型项目,pom.xml 里的核心依赖是这样配的:
| 依赖 | 版本 | 用途 |
|---|---|---|
| Spring Boot | 2.7.18 | 基础框架 |
| MyBatis Spring Boot Starter | 2.3.1 | ORM层 |
| MySQL Connector/J | 8.0.33 | 数据库驱动 |
| Druid | 1.2.20 | 数据库连接池 |
| Lombok | 1.18.30 | 减少样板代码 |
| Hutool | 5.8.25 | 工具类库,生成订单号等 |
| JWT | 0.9.1 | 用户登录令牌 |
| Spring Boot Starter Validation | 2.7.18 | 参数校验 |
有些同学会问:为什么不用MyBatis Plus?用也行,但毕业设计里用原生MyBatis + XML,反而更容易展示你对SQL的掌控力。尤其“余票扣减”这个核心场景,手写UPDATE语句能清楚看到乐观锁的条件拼接,换成MyBatis Plus的Wrapper反而把关键逻辑包在了一层语法糖里,答辩时不好展开。
2.3 工程分层:包结构直接影响答辩陈述逻辑
我见过很多项目,所有代码一股脑塞在controller和service里,一个方法写了两三百行。这种代码跑起来没问题,但答辩时你很难讲清楚“系统的核心逻辑在哪里”。我的习惯是按照下面的分包方式组织:
code复制com.example.train
├── common
│ ├── result // 统一返回结果封装
│ ├── exception // 全局异常处理
│ └── utils // 工具类
├── config // 配置类
├── controller // 控制层
├── service // 业务逻辑层
│ └── impl
├── mapper // MyBatis的Mapper接口
├── entity // 实体类
├── dto // 前端入参/出参对象
└── vo // 视图对象
你可能觉得这只是文件夹名字的差别,但答辩时你完全可以说是“遵循了阿里巴巴Java开发规范的分层思想”。更重要的是,当你要排查一个bug时,能快速定位是参数问题、SQL问题还是业务逻辑问题,这在实际项目里节省的时间远超想象。
3. 数据库设计:车票余量是系统的心脏
3.1 核心表结构:不要一上来就设计“票表”
火车订票系统的数据库设计,最容易犯的错是——直接建一张ticket表,每条记录对应一个座位的一张票,谁买了就插一条记录。这种设计的优点是直观,但缺点是查询余票时要COUNT,卖票时要INSERT,锁定座位时要处理行级锁,一张车次有上千个座位,并发一高这张表就变成了热点中的热点。
更合理的做法,是借鉴12306的经典思路——把“车次+日期”作为库存维度,而不是“座位”维度。也就是说,一张表记录车次的余票数,买票时对其做原子扣减,而不是去插入一张张具体的“票”。这样做会大幅降低锁竞争和数据量膨胀。
核心表大致有这几张:
- train(车次表):车次编号、出发站、到达站、出发时间、到达时间、历时、车型等
- train_stock(余票表):车次ID、乘车日期、座位类型(二等座/一等座/硬卧/软卧)、余票数量、票价
- station(车站表):站名、城市、所属线路
- train_route(车次经停表):车次ID、站序、到站时间、发站时间、里程
- user(用户表):用户名、加密密码、手机号、身份证号
- orders(订单表):订单号、用户ID、车次ID、乘车日期、出发站、到达站、座位类型、票价、订单状态(待支付/已支付/已出票/已取消/已退票)
- orders_ticket(乘车人表):订单ID、乘车人姓名、身份证号、座位号
为什么要单独建train_route?因为一个车次经过5个站,任意两站之间的区间就构成了一个“可售区间”。比如G101次从北京到上海,途经南京,那“北京到上海”“北京到南京”“南京到上海”是三段不同的销售区间,它们的余票相互影响。如果只存起点终点,这个逻辑根本没法实现。这是火车票系统区别于飞机票系统的一个关键点,也是设计的亮点。
3.2 余票表为什么要用“乐观锁”而不是“悲观锁”
“两个人同时买最后一张票”是火车票系统绕不开的经典问题。最简单粗暴的方案是SELECT ... FOR UPDATE,也就是悲观锁,但它会让一行记录在整个事务期间被锁住,查询和下单都在竞争同一把锁,容易成为瓶颈。
我采用的是乐观锁方案:在train_stock表中加一个version字段,每次扣减时执行:
sql复制UPDATE train_stock
SET stock = stock - 1, version = version + 1
WHERE train_id = #{trainId}
AND travel_date = #{travelDate}
AND seat_type = #{seatType}
AND stock > 0
AND version = #{version};
注意这个SQL里的两个关键条件:stock > 0 从数据层面直接杜绝了超卖;version = #{version} 保证扣减基于的是你查询到的快照。如果更新影响行数为0,说明版本号不匹配或没余票了,业务层再提示“余票不足或已售罄”。
有人会问:更新行数为0时要不要重试?我的做法是:不自动重试,直接返回失败。因为火车票库存的竞争窗口其实很短,如果频繁重试会放大写冲突,而且用户看到“余票不足”后重新搜索一次本身就是一次新的查询,体验上并没有损失太多。
这里要特别提醒一个常见错误:不要把version当成普通字段只塞在UPDATE条件里而不更改它,或者把version加在SELECT里但UPDATE时忘记带条件。前者会让乐观锁失去意义,后者会让你的“乐观锁”变成普通的减法,超卖照样发生。提交代码前最好用JMeter模拟50个并发请求同时买1张票,看看最终订单数和余票数是否一致。
3.3 数据库索引设计:索引不对,查询慢到怀疑人生
余票查询的典型SQL是根据出发站、到达站、出发日期去关联车次表和余票表。很多同学把索引建得很随意,或者干脆不建,结果数据量一大,一条查询几百毫秒甚至秒级。我建议在以下几列建立索引:
orders表:user_id(查用户订单)、order_no(唯一索引,按订单号支付回调)train_stock表:(train_id, travel_date, seat_type)联合唯一索引,既保证了一列车一天的某个坐席类型只有一条记录,又覆盖了查询余票的最常用条件train_route表:(train_id, station_seq)联合索引,用于快速得到经停顺序
索引的原理可以简单类比成书的目录,没有目录你要把整本书翻一遍才能找到目标页码,有了目录可以直奔主题。在答辩时如果能主动说出“我为常用的查询条件建了联合索引”,会是一个加分项,因为这证明你考虑的不只是“功能实现”,还有“性能表现”。
4. 核心模块的实现逻辑与关键代码
4.1 注册登录与JWT无状态认证
登录模块大多数课题都会做,但火车订票系统我个人推荐用JWT(JSON Web Token)而不是传统的Session。原因有两点:
- 毕业设计里通常会做一个“记住我”功能,JWT天然无状态,前端拿到Token后存到
localStorage,每次请求带在Authorization头里即可,后端不需要维护Session。 - 答辩时更好讲——“无状态认证可以水平扩展,多个服务节点之间不需要同步会话状态”。
JWT在Spring Boot里的整合并不复杂。我在config包下定义一个JwtInterceptor,实现HandlerInterceptor接口,在preHandle中解析Header里的Token,解析失败返回401,成功则把用户ID放入ThreadLocal或RequestContextHolder,供后续业务逻辑里获取当前登录用户。
密码存储方面,至少要用BCryptPasswordEncoder。这是Spring Security里自带的加密器,同一密码每次加密生成的密文都不同,可以有效抵抗彩虹表攻击。千万不要用MD5直接存密码,这是我能想到的毕业设计里最不值得踩的安全坑。
4.2 车次查询:多条件组合与分页
车次查询是整个系统的门面功能,用户输入出发站、到达站、出发日期,系统返回所有符合条件的车次及余票信息。在接口设计上,我习惯用GET /api/train/search,参数为:
fromStation:出发站toStation:到达站travelDate:乘车日期pageNum、pageSize:分页参数
Service层按下面几步执行:
- 通过
train_route表查所有经过fromStation的车次ID集合A - 查所有经过
toStation的车次ID集合B - 取A和B的交集,再检查
fromStation的站序 <toStation的站序,剔除方向反的 - 用车次ID集合去关联
train_stock表,查对应日期的余票 - 返回给前端时,补充每个车次的出发到达时间和历时
这里有个细节:如果你把查询SQL写成一个大JOIN,可能在数据量变大后性能下降得比较明显。我的习惯是先用一个轻量查询拿到车次ID集合,再批量查询余票,虽然是两次查询,但每次查询单一职责明确,也容易加缓存。
当你以后在工作中接手真实系统时,会发现这种“拆小查询”的模式非常常见,因为分布式环境下表与表甚至不在同一个库,连JOIN的机会都没有,提前养成这种思路有好处。
4.3 下单与余票扣减:事务的边界到底该画在哪
下单流程是:
code复制1. 校验用户登录状态
2. 校验乘车人信息
3. 查询余票,判断余量是否充足
4. 乐观锁扣减余票
5. 插入订单记录(待支付)
6. 返回订单号,引导用户支付
这里最关键的思路是:步骤3只是预检查,步骤4才是真正的“锁”。你不能在步骤3查到余票=1后,直接认为就一定能买到,步骤4必须用我们前面那条UPDATE ... WHERE stock > 0做原子扣减。
关于@Transactional的作用范围,我给你讲一个很容易犯的错。很多同学会把扣减余票和生成订单放在一个Service方法里,并在此方法上加@Transactional。这本身没问题,但如果方法内部有try-catch吞掉了异常,事务会因为异常被捕获而不会回滚。
例如下面这个反例:
java复制@Transactional
public void createOrder(OrderDTO dto) {
try {
// 扣减余票
trainStockMapper.deductStock(dto);
// 生成订单
orderMapper.insert(order);
} catch (Exception e) {
log.error("下单失败", e);
// 什么都不做,异常被吞掉
}
}
如果insert失败但异常被吞了,事务不会回滚,余票被扣了但订单没生成。这种bug非常隐蔽,而且只在异常场景下出现。所以最后我采用的做法是:不在Service层捕获异常,而是让它向上抛出,由全局异常处理器统一返回错误信息。事务方法尽量保持“无try-catch”的状态,确保任何RuntimeException都会触发回滚。
4.4 订单状态机:从待支付到已出票的流转
订单状态如果只是简单存一个字符串,代码里到处写着if ("1".equals(status)),项目阶段倒是能跑,但后续一旦要加“改签”“退票”,逻辑就会乱成一团。我建议在一开始就定义好订单状态枚举:
| 状态 | 枚举值 | 说明 |
|---|---|---|
| 待支付 | PENDING_PAYMENT | 下单后2分钟内需完成支付 |
| 已支付 | PAID | 支付成功,等待出票 |
| 已出票 | ISSUED | 出票完成 |
| 已取消 | CANCELLED | 用户主动取消,或超时未支付系统自动取消 |
| 已退票 | REFUNDED | 用户发起退票,已完成退款 |
| 已改签 | CHANGED | 原订单已改签到新车次 |
有了枚举后,所有状态更新都走一个中心化的OrderStateMachine组件,禁止在业务代码里散落地直接UPDATE status = 'xxx'。这样做有两个好处:一是非法状态流转(比如从“待支付”直接跳到“已出票”)会被拦截;二是答辩时你可以说“我参考了状态机设计模式,将订单生命周期收敛到一个组件中”,这比面面俱到的业务描述更显专业。
超时取消的定时方案,我选了Spring Boot自带的@Scheduled,每30秒扫描一次“待支付且下单时间超过2分钟”的订单,将状态置为“已取消”,同时回滚对应车次的余票。这里要注意:取消订单后要恢复余票,同样要基于乐观锁做UPDATE stock = stock + 1,否则用户的票被“冻结”了但卖不出去,系统就出现票额不一致。
4.5 支付对接的替代方案:虚拟支付回调
真正的支付对接需要商户号和证书,毕业设计阶段通常不具备条件。我的处理方式是用虚拟支付:用户点击支付后,系统生成一条支付记录,随机模拟支付成功或失败,然后走与真实支付完全一致的回调处理逻辑。
具体做法是:在/api/pay/mock接口里,接收订单号,然后:
- 将订单状态修改为“已支付”
- 调用
orderService.paySuccess(orderNo),同一个方法里处理“更新状态、生成出票信息”等逻辑 - 如果以后接支付宝或微信支付,只需要把这个Mock接口替换成真实回调地址,
paySuccess方法完全复用
你可以在答辩时说:“支付模块我做了可扩展设计,用Mock方式模拟了真实支付回调流程,后期切换成真实支付只需替换回调入口。”这句话说明你考虑到了系统演进,会给评委留下不错的印象。
5. 开发中那些“搜不到答案”的坑
5.1 MyBatis的<if>标签里,逗号到底该放哪
写MyBatis的动态UPDATE时,新手经常纠结逗号的位置。以一个更新订单信息的语句为例:
xml复制<update id="updateOrder" parameterType="order">
UPDATE orders
<set>
<if test="contactName != null">
contact_name = #{contactName},
</if>
<if test="contactPhone != null">
contact_phone = #{contactPhone},
</if>
</set>
WHERE order_no = #{orderNo}
</update>
<set>标签会自动处理末尾的逗号,所以每个<if>内部结尾带逗号没有关系,但如果set标签内的内容为空,会直接生成UPDATE orders WHERE ...,语法错误。防这个问题的办法是保证至少有一个字段会被更新,或者先查询一次判断是否有变更再决定是否更新。
5.2 事务自调用失效:同一个类里的方法调用,事务管不着
Spring的@Transactional是基于AOP代理实现的,也就是说,只有通过外部调用进入代理对象时,事务才生效。如果你在同一个类里写了两个方法,methodA调用methodB,而methodB上有@Transactional,这个事务不会生效,因为调用发生在对象内部,没有经过代理。
我的一个实际教训是:把“查询订单+扣减库存+生成支付记录”拆到了三个方法里,然后在同一个Service里按顺序调用,结果扣库存成功但生成支付记录失败时,库存没回滚。排查了半天,最终发现就是自调用导致的。
解决方案两种:
- 把需要事务的方法放到另一个Service类中,从外部注入后调用
- 在当前类注入自身的代理对象
@Autowired private OrderService self;,用self.methodB()调用
方案一更简洁。所以在设计Service时,我建议把一个独立的业务闭环拆成一个单独的类,比如OrderFlowService专门处理“下单到支付成功”的完整流程,内部再调用OrderService和TrainStockService这样的小Service,事务边界清晰,也方便测试。
5.3 过滤器和拦截器:到底用哪个
“springboot创建过滤器”是热搜词里的高频词,因为在Spring Boot里,Filter和HandlerInterceptor都可以做登录校验,但很多人不清楚两者的区别。
Filter是Servlet层面的, 在请求进入DispatcherServlet之前执行,可以处理静态资源、字符编码、跨域等。HandlerInterceptor是Spring MVC层面的, 在Controller方法执行前后执行,可以拿到HandlerMethod对象,从而做细粒度的权限判断。
在火车订票系统里,我用HandlerInterceptor做登录校验,因为我在校验完Token后还要把用户信息存入ThreadLocal,方便Controller层直接取用。拦截器里可以通过registry.addPathPatterns精确指定要拦截的URL,比如/api/order/**、/api/user/**,而像/api/train/search这种公开查询接口就不拦截。
这里有个关键经验:拦截器的preHandle里返回false后,一定要把响应内容写好(比如返回JSON),并设置Content-Type为application/json;charset=UTF-8, 不然前端收到的是空响应,排查半天还以为是后端接口挂了。
5.4 资源映射:上传的头像和图片怎么让前端访问到
“springboot 如何做资源映射”也是热搜中的高频问题。火车订票系统通常涉及用户头像上传、车次图片展示等静态资源需求。Spring Boot默认的静态资源目录是classpath:/static/,但上传的图片存在这个目录里,每次重新打包就会丢失,而且也不方便运维管理。
我的做法是配置一个本地磁盘路径映射:
yaml复制spring:
web:
resources:
static-locations: file:D:/upload/, classpath:/static/
再配合一个配置类实现WebMvcConfigurer,添加自定义资源映射:
java复制@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
registry.addResourceHandler("/upload/**")
.addResourceHandler("file:D:/upload/");
}
这样,前端通过http://localhost:8080/upload/avatar.png就可以访问到D盘upload目录下的文件,而且前端路径和后端存储路径被解耦了,迁移服务器只需要改配置。
6. 系统性能与安全:答辩中拉开差距的加分项
6.1 余票查询接口的Redis缓存策略
用户搜索车次是系统里最高频的操作。就算数据库的联合索引建得再好,每秒钟几百次查询也一定会让MySQL压力变大。我给“车次+日期”的余票查询结果加了Redis缓存,逻辑是这样:
- key:
train:search:{fromStation}:{toStation}:{travelDate} - value:车次列表和余票量的JSON
- 过期时间:30秒
30秒的过期时间意味着极端情况下用户看到的余票可能滞后30秒,但换来了数据库QPS的大幅下降。这个策略其实12306也在用,只不过人家是秒级甚至毫秒级推送。在毕业设计里,你可以明确说明“用牺牲极小实时性的代价换取整体性能提升”,这体现的是工程思维,而不是一味追求绝对正确。
6.2 接口幂等性:防止用户疯狂点击下单
如果用户手抖点了两次“提交订单”,你的系统会不会生成两笔订单并扣两次余票?在设计时我通过前端按钮置灰和后端幂等校验双重保证。
后端的做法是,在下单接口里增加一个requestId参数。当用户进入订单确认页时,前端向后端请求一个唯一的requestId,提交订单时带上它。后端在Redis里以requestId为key执行SETNX(不存在则设置),只有第一次请求才能拿到“锁”,重复请求直接返回“请勿重复提交”。
这个方案比单纯用订单号去查重更可靠,因为订单号还没生成时就已经拦截了重复请求。这也是“springboot 如何做防重提交”类问题的标准答案之一。
6.3 参数校验与统一异常处理
在Controller层,我全部使用Spring Boot自带的@Validated注解做参数校验,配合@NotBlank、@Pattern等约束注解。比如乘车人身份证号,用@Pattern(regexp = "^[1-9]\\d{5}(18|19|20)\\d{2}(0[1-9]|1[0-2])(0[1-9]|[12]\\d|3[01])\\d{3}[0-9Xx]$")做格式校验。
全局异常处理器实现@RestControllerAdvice,拦截三类异常:
MethodArgumentNotValidException:参数校验失败,返回400和具体错误字段BusinessException:业务异常,比如“余票不足”“订单不存在”,返回200加业务码Exception:兜底异常,返回500,同时用日志记录堆栈,前端只展示“系统繁忙,请稍后重试”
有了统一异常处理,Service里就不要再写一堆try-catch返回Result.error()了,逻辑简洁很多。
6.4 数据库连接池参数怎么调
Druid连接池刚配置时的默认参数对高并发并不友好。我做了以下调整:
| 参数 | 值 | 理由 |
|---|---|---|
| initialSize | 5 | 启动时建立的连接数 |
| minIdle | 5 | 最小空闲连接数 |
| maxActive | 50 | 最大活跃连接数 |
| maxWait | 60000 | 获取连接超时1秒返回异常 |
| testWhileIdle | true | 空闲时定期检测连接有效性 |
| validationQuery | SELECT 1 | 探活SQL |
在答辩时可以坦诚说明:“这些参数不是拍脑袋定的,而是通过压测逐步调整的结果。”如果时间充裕,建议用JMeter做一轮简单的并发测试,记录下调整前后的响应时间对比,这个数据比任何口头描述都有说服力。
7. 部署上线与数据库初始化:一次讲透
7.1 环境准备:JDK、MySQL、Redis一台机器跑起来
前端和后端分离的形式,最省事的部署方式是把后端打成可执行JAR包,放在一台Linux服务器上,前端用Nginx托管,再把MySQL和Redis也装在同一台机器。对毕业设计来说,一台2核4G的云服务器完全够用,不需要搞K8s,也不建议在答辩前引入太复杂的部署架构。
启动命令很简单:
bash复制nohup java -jar train-system-0.0.1-SNAPSHOT.jar \
--spring.profiles.active=prod \
--server.port=8080 \
> /var/log/train-system.log 2>&1 &
7.2 初始化脚本与测试数据
我在项目里放了一个sql/init.sql,除了建表语句,还包含插入的车站数据、车次数据、经停关系数据和余票初始数据。这里有几个细节需要注意:
- 车次数据至少要插3条以上,经停站至少要有3个站,这样演示时才能搜出“有经停”的车次,而不是一条直达线路打天下
- 余票初始数据要有的多有的少,方便演示“余票充足”和“余票紧张”两种状态
- 用户表至少准备两个测试账号,一个普通用户,一个管理员用户
测试数据要贴近真实,比如车次编号用G101、D301这样的格式,不要写ABC123这种一看就是随手敲的。
7.3 常见打包问题:静态资源丢失与端口被占用
打包时最惊悚的现象是:本地启动完美,打成JAR后前端页面样式全没了。原因通常是前端静态资源没有正确打进static目录。如果前后端分离,前端包由Nginx托管,这个问题就不存在;如果前后端不分离,确认前端构建产物被拷贝到src/main/resources/static/目录后再打包。
端口被占用也很常见。用netstat -tlnp | grep 8080查进程,然后根据PID用kill -9 PID杀掉。如果你是Windows本地开发,用netstat -ano | findstr 8080,最后一位是PID,再用taskkill /F /PID 8080杀掉。
7.4 部署后的日志查看与快速排错
部署后务必养成看日志的习惯。我常用的排查命令:
bash复制# 查看实时日志
tail -f /var/log/train-system.log
# 查询错误日志上下文
grep -n "Exception" /var/log/train-system.log | tail -20
# 按时间范围过滤
sed -n '/2026-05-01 10:00:00/,/2026-05-01 10:30:00/p' /var/log/train-system.log
日志里最常看到的一类错误是DataIntegrityViolationException,这通常是数据库约束违反的外键或唯一索引冲突。比如插入订单时忘记插入orders_ticket,或者车次ID写错了导致外键失败。看堆栈时优先看Caused by部分,那才是根因。
8. 答辩准备的几个临门一脚
关于这部分,我不想给你罗列“项目亮点”“遇到的问题”这种模板清单。只提醒一点:评委看重的从来不是“你用了多少技术”,而是“你做的方案是否经得起追问”。
因此,我强烈建议你在答辩前自己“拷问”自己几个问题:
- 为什么用乐观锁不用悲观锁?——前提是数据竞争不算极端激烈,乐观锁能降低锁持有时间
- 余票表和订单表为什么要分开?——因为订单是流水,余票是状态,业务生命周期不同
- 如果用户支付超时,系统怎么处理?——定时任务扫描待支付订单,取消并回滚库存
- 如果同一个用户恶意重复下单怎么办?——requestId幂等,前端防重 + 后端SETNX双重保障
- 查询余票是实时的吗?——为了性能引入了Redis,有延迟,但可接受
这些问题在文中基本都有答案,关键是能用自己的话表达出来。技术栈只是工具,真正体现专业素养的是“针对每个设计决策,你能解释清楚权衡的代价与收益”。
我自己做这个课题时的最大体会是:火车订票管理系统不是“图书管理系统加了个新页面”,它是一个完整业务闭环的缩影,涉及库存扣减、订单流转、支付状态、定时任务、缓存一致性、并发控制。把这些知识点整合到一个系统里,比刷十个课程设计都涨经验。如果你能把这个系统的代码从头到尾自己写一遍,不只是复制粘贴,那么Spring Boot框架的掌握程度会有一个质的提升。
最后再分享一个小建议:写代码前先把数据库表设计出来,表关系画清楚,再动手写Java代码。 我见过太多同学边写代码边改表,到后面Service里的字段与数据库对不上,排查问题花的时间是写代码的好几倍。好的表设计是系统质量的基石,值得你花时间。
