news 2026/8/13 5:22:20

基于Spring Boot 3.0构建高并发仿12306售票系统:核心模型与一致性设计

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
基于Spring Boot 3.0构建高并发仿12306售票系统:核心模型与一致性设计

1. 项目缘起与挑战:为什么我们要“造轮子”?

最近几年,但凡聊到Java后端开发,尤其是面试或者技术分享,高并发系统设计几乎成了一个绕不开的话题。大家似乎都在谈微服务、谈分布式、谈缓存、谈消息队列,但真正把一个完整的高并发业务场景从头到尾、从零到一实现一遍的人,其实并不多。很多人可能看过很多文章,背过很多八股文,比如“Redis缓存雪崩怎么解决”、“如何设计一个秒杀系统”,但真让你动手写一个,可能还是会觉得无从下手,或者写出来的东西一压测就崩。

这正是我决定启动这个系列实战项目的初衷。与其空谈理论,不如找一个足够复杂、足够有挑战性的业务场景,用最新的技术栈,把它实实在在地做出来。而“12306售票系统”,无疑是一个绝佳的“靶子”。它几乎涵盖了高并发系统设计的所有核心痛点:瞬时超高流量、复杂的业务状态(车次、座位、席别)、严格的库存一致性、复杂的查询与计算(余票查询)、以及极致的响应速度要求。网上关于它的讨论很多,但大多是零散的架构图或者某个技术点的分析,很少有完整的、可运行的代码实现。

所以,这个系列的目标非常明确:我们将基于Spring Boot 3.0,从零开始,一步步构建一个简化但核心逻辑完整的仿12306售票系统。这不是一个简单的CRUD项目,而是一个深度探索Java高并发编程、分布式系统设计原理的实战沙盘。我会把我这些年在一线处理高并发问题的经验、踩过的坑、以及那些在官方文档里不会写的“野路子”技巧,都揉进代码和讲解里。

在正式敲下第一行代码之前,我们必须把地基打牢。很多人一上来就急着搭框架、写接口,结果写到一半发现核心模型设计错了,或者对并发问题的复杂性预估不足,导致推倒重来。这一篇,我们就来聊聊那些至关重要的“前置知识”。这些知识,是你能否理解后续每一行代码设计意图的关键。

2. 核心业务模型抽象:从现实车票到数据对象

设计系统,尤其是业务复杂的系统,第一步永远是理解并抽象领域模型。12306的业务看似直观——卖票,但其背后的数据关系错综复杂。我们不能直接照搬铁路系统的所有细节,但必须抓住最核心的实体和它们之间的关系。

2.1 核心实体定义

我们需要抽象出几个最关键的实体,并思考它们的属性和生命周期。

  1. 车次(Train):这是调度和运行计划的核心。一个车次(如G101)代表一趟具体的列车运行安排。

    • 关键属性:车次号、始发站、终点站、出发时间、到达时间、运行日期、列车类型(高铁、动车、普快等)。
    • 设计思考:车次是一个“模板”或“计划”。它本身不直接参与售票,但它定义了后续一切的基础。一个车次在特定日期会生成一个具体的“车次实例”。
  2. 车次实例(DailyTrain):这是真正可售卖的主体。因为G101车次每天都会运行,但2023年10月1日的G101和10月2日的G101是两趟不同的、独立的库存实体。

    • 关键属性:关联车次ID、具体日期、一个最重要的字段——初始座位库存。这个库存需要根据列车类型(如16节车厢的复兴号)和席别(商务、一等、二等)在系统初始化时生成。
    • 设计思考:这是库存管理的核心单元。所有的余票查询、扣减库存、座位选择,都是基于某个日期的某个车次实例进行的。必须将它与“车次”模板分离,否则库存无法按天管理。
  3. 座位(Seat)与车厢(Carriage):为了支持“选座”功能,我们需要更细粒度的库存管理,而不是简单地用一个数字表示“二等座还剩100张”。

    • 关键属性(Seat):所属车次实例ID、车厢编号、排号、列号(如“05车10F”)、席别、座位状态(可用、已售、锁定等)。
    • 关键属性(Carriage):车厢编号、车厢类型、定员数、座位排布规则(用于生成具体的座位记录)。
    • 设计思考:用“座位”作为库存的最小单位,是实现“一人一票一座”和“选座”功能的基础。这比使用“余票数”计数器要复杂得多,因为它涉及到对单个座位的状态进行并发修改,是后续解决“超卖”问题的关键设计点。
  4. 余票(TicketStock):这是一个衍生数据缓存数据。虽然我们有每一个座位,但每次查询余票时,实时统计某个车次实例下某个区段、某个席别的可用座位数,性能是无法接受的。

    • 设计思考:我们需要一个独立的余票数据模型,它可能是:
      • 数据库汇总表:定期或触发式更新某个车次实例-席别-出发站-到达站组合的可用座位计数。
      • Redis缓存:将上述计算结果放入内存,提供毫秒级查询。这是高并发查询的标配方案。
    • 核心难点:如何保证“座位”的真实状态与“余票”缓存数据的一致性?这是一个经典的缓存与数据库一致性问题。
  5. 购票订单(TicketOrder):用户下单后的产物。

    • 关键属性:订单号、用户ID、车次实例ID、出发站、到达站、乘车人信息、所选座位信息、订单状态(初始化、待支付、已支付、已完成、已取消等)、订单总价。
    • 设计思考:订单状态机是整个购票流程的驱动核心。状态的变化(如“待支付” -> “已支付”)会触发库存的最终扣减、座位的最终占用。订单号必须全局唯一且具备业务含义(如包含日期、渠道等信息)。
  6. 购票记录(Ticket):支付成功后生成的最终票务凭证。

    • 关键属性:票号、关联订单ID、乘客姓名、身份证号、车次、座位号、票价、状态(已出票、已改签、已退票等)。
    • 设计思考:一张票是订单的最终履约产物。它与订单是多对一的关系(一个订单可能包含多张票)。票的生命周期独立于订单,例如改签、退票操作是针对票进行的。

注意:在实际的12306中,模型远比这复杂,涉及车站、线路、票价计算规则、学生票/儿童票等。在我们的简化版本中,我们会聚焦于最核心的“查票-选座-下单-支付”流程,因此上述模型已经足够支撑我们探索高并发核心技术。

2.2 模型关系与状态流转

理清实体后,我们需要用一张图(这里用文字描述)来串联它们的关系和关键业务流程的状态流转:

  1. 初始化流程:管理员创建车次-> 系统在每天凌晨为未来N天的该车次生成车次实例-> 根据车次类型和车厢配置,为每个车次实例生成具体的座位记录 -> 初始化或更新余票缓存。
  2. 查询流程:用户输入条件 -> 系统查询余票缓存(极快) -> 返回车次列表和余票数量。
  3. 购票流程(简化核心)
    • 步骤A(占座):用户选择车次、席别、座位 -> 系统尝试锁定(Lock)选中的座位记录(将其状态改为“锁定”),并扣减余票缓存。
    • 步骤B(下单):锁定成功后,创建购票订单,状态为“待支付”,并将锁定的座位与订单关联。这里会设置一个锁定时效(如15分钟)。
    • 步骤C(支付):用户支付成功 -> 回调系统,将购票订单状态改为“已支付” -> 将关联的座位状态改为“已售” -> 生成购票记录
    • 步骤D(超时释放):如果用户在锁定时效内未支付,定时任务会扫描状态为“待支付”且超时的订单 -> 将关联的座位状态恢复为“可用” -> 增加余票缓存。

这个流程中,从“步骤A”到“步骤C”,涉及对同一座位记录的多次状态修改(可用->锁定->已售),且可能被成千上万的请求同时操作,这就是高并发冲突最集中的地方。如何保证一个座位不被两个人同时买到(超卖)?如何保证缓存里的余票数和数据库里真实的可用座位数一致?这些问题是整个系统的“命门”。

3. 高并发核心难题拆解:库存一致性这座大山

理解了业务模型,我们就能清晰地看到技术挑战所在。所有挑战都围绕着一个核心:在每秒数万甚至数十万次请求下,如何保证车票库存数据(特别是座位状态)的绝对正确性?

3.1 超卖问题:并发写的终极挑战

“超卖”是指库存只有1件,但成功卖出了2件或更多。在我们的场景里,就是同一个座位卖给了两个人。这是电商、票务等系统的经典难题。

为什么在并发下会发生超卖?假设座位Seat#A状态为“可用”。两个用户几乎同时请求购买它。

  1. 请求1:查询Seat#A状态,为“可用”。
  2. 请求2:查询Seat#A状态,为“可用”。
  3. 请求1:执行UPDATE seat SET status = '锁定' WHERE id = A AND status = '可用'
  4. 请求2:执行同样的UPDATE语句。

在数据库层面,如果只是简单的UPDATE ... SET status = '锁定' WHERE id = A,那么两个请求都会执行成功,因为它们查询时状态都是“可用”,这就超卖了。

解决方案思路:

  1. 悲观锁:在查询时就直接用SELECT ... FOR UPDATE锁定行。这能解决问题,但性能极差,会把并发操作彻底串行化,在超高并发下等于系统自杀。
  2. 乐观锁:给座位记录增加一个版本号字段version。更新时带上版本条件:UPDATE seat SET status = '锁定', version = version + 1 WHERE id = A AND status = '可用' AND version = #{oldVersion}。这样,只有第一个更新能成功,第二个更新会因为version不匹配而返回0行受影响,从而失败。这是更优的选择。
  3. 分布式锁:在应用层,使用Redis或ZooKeeper等实现一个分布式锁,确保在同一时间,对同一个座位ID的写操作只有一个线程能执行。这引入了外部组件,增加了复杂度,但控制粒度更灵活。
  4. 数据库唯一约束:结合业务设计,例如将“车次实例日期+车厢+座位号”设置为唯一索引,并在创建订单或票记录时以此作为条件。如果两个请求试图插入相同的座位占用记录,后者会因唯一冲突而失败。

在我们的项目中,会综合使用乐观锁和唯一约束。在座位状态变更时使用乐观锁,在生成最终票务记录时利用唯一约束做最终防重。

3.2 缓存与数据库的一致性问题

为了应对海量查询,余票信息必须放在Redis里。但用户购票(写操作)最终修改的是数据库里的座位状态。这就产生了数据不一致的窗口期。

不一致场景示例:

  1. 数据库:座位Seat#A状态为“可用”。Redis:余票数=100。
  2. 请求1:成功锁定Seat#A,数据库将其状态改为“锁定”。
  3. 此时,如果Redis余票数没有及时更新,仍然是100。
  4. 请求2:查询余票,Redis告诉它还有100张,于是它继续走购票流程,尝试锁定另一个座位,但实际上可能已经没有真正可用的座位了(虽然数据库有锁定的,但用户看来就是有票)。这会导致大量请求穿透到数据库,造成不必要的压力,甚至引发误判。

解决方案思路(没有银弹,只有权衡):

  1. Cache Aside Pattern(旁路缓存):这是最常用的模式。
    • :先读缓存,命中则返回;未命中则读数据库,写入缓存。
    • :先更新数据库,再删除缓存
    • 为什么是删除而不是更新缓存?因为更新缓存可能引发并发写缓存的一致性问题,且删除操作是幂等的。在我们的场景,用户购票成功后,直接删除该车次该席别的余票缓存。下次查询时,缓存未命中,会从数据库重新计算并加载最新的余票数。这个方案简单,但在删除缓存后、新缓存建立前,会有短暂的数据不一致,并且可能引起“缓存击穿”(大量请求同时打到数据库)。
  2. 定期刷新:启动一个定时任务,每隔很短时间(如1秒)扫描数据库,重新计算所有车次的余票并刷新到Redis。这能保证最终一致性,且刷新间隔内的数据是准确的。但对数据库有持续压力,且存在最多1秒的延迟。
  3. 基于Binlog的异步刷新:使用Canal、Debezium等工具监听数据库的Binlog,当seat表发生变更时,实时计算并更新Redis缓存。这能实现准实时的一致性,但对架构复杂度和运维要求较高。

在我们的项目中,初期会采用“Cache Aside + 异步刷新兜底”的策略。即写操作后主动删除缓存,同时有一个低频的定时任务(比如每10秒)做全量或增量刷新,作为防止缓存长期不一致的兜底机制。

3.3 高并发查询:余票查询的优化

即使解决了写一致性问题,读的压力依然巨大。春运期间,首页余票查询的QPS可能是天文数字。

核心挑战:

  • 复杂计算:余票不是简单的COUNT(*)。它需要计算指定区段的可用座位。例如,从北京到上海的车次,用户查“南京到苏州”的余票,需要计算那些全程中南京到苏州这一段未被售出的座位。这是一个复杂的空间计算。
  • 实时性:数据必须尽可能新。

解决方案思路:

  1. 预计算:这是关键。我们不可能在每次查询时实时联表计算。必须在后台提前算好。
    • 维度:为每个车次实例,预计算所有可能出发站-到达站组合(站序在前的作为出发站)的每个席别的余票数。
    • 存储:将计算结果以结构化的方式存入Redis。例如,Key设计为:ticket_stock:{daily_train_id}:{start_station_index}:{end_station_index}:{seat_type},Value就是可用座位数或可用座位ID列表(如果要做选座)。
    • 更新:当某个座位被售出或释放时,需要更新所有包含该座位区段的预计算结果。例如,座位被从南京卖到苏州,那么所有出发站序<=南京且到达站序>=苏州的区段,其对应席别的余票数都要-1。这是一个O(n)的操作,需要仔细设计数据结构和更新逻辑。
  2. 多级缓存
    • L1 - 本地缓存(Caffeine/Guava Cache):在应用服务器内存中,缓存一些最热门的车次查询结果(如未来2小时内出发的热门车次)。设置很短的过期时间(如1秒),应对瞬时爆发流量。
    • L2 - 分布式缓存(Redis):存储全量的预计算余票数据。
    • L3 - 数据库:作为最终的数据源和兜底。
  3. 请求合并与降级:对于完全相同的查询请求,可以在应用层进行短时间(如10毫秒)的合并,将多个请求合并为一个去查询缓存或数据库,减少重复开销。当系统压力过大时,可以降级余票查询的实时性,比如返回稍早一些的缓存快照。

在我们的实现中,预计算+Redis存储将是余票查询的基石。我们会详细设计如何高效地存储和更新这些预计算数据。

4. 技术栈选型与Spring Boot 3.0的新特性

明确了业务挑战,我们来看看手中的“武器”。技术选型直接决定了实现的复杂度和系统的天花板。

4.1 核心框架:为什么是Spring Boot 3.0?

Spring Boot 3.0是一个重大升级版本,它基于Spring Framework 6.0,并要求最低Java 17。对于我们的高并发项目,它带来了几个关键好处:

  1. 原生支持GraalVM原生镜像:虽然我们初期不一定会用,但这是未来性能压榨的一个重要方向。Spring Boot 3.0对AOT(Ahead-Of-Time)编译提供了更好的支持,能生成启动更快、内存占用更小的原生应用,非常适合云原生和资源敏感场景。
  2. Java 17+特性:LTS版本的Java 17提供了很多现代语言特性,如Records(用于定义数据模型,减少样板代码)、Sealed Classes(限制类的继承,增强领域模型的安全性)、Text Blocks(更好的多行字符串支持)等,能让我们的代码更简洁、更安全。
  3. 改进的观测性:Spring Boot 3.0深度集成了Micrometer,提供了开箱即用的可观测性(Observability)支持,包括指标(Metrics)、追踪(Tracing)和日志(Logging)。这对于我们后期监控系统性能、定位高并发下的瓶颈至关重要。
  4. ** Jakarta EE 9+**:命名空间从javax.*迁移到了jakarta.*,这是面向未来的变化。虽然会带来一些依赖库的升级阵痛,但长远看是必要的。

注意:选择Spring Boot 3.0也意味着我们要面对其相对较新的生态。一些旧的、未更新的第三方库可能不兼容。但考虑到这是一个学习型项目,拥抱最新稳定版技术栈是值得的。

4.2 持久层:MyBatis-Plus vs. Spring Data JPA

这是一个经典的抉择。两者都是优秀的ORM/持久化框架。

  • MyBatis-Plus
    • 优势:对SQL的控制力极强,可以精细优化每一条查询语句,这对于高性能、复杂查询的场景(如我们的余票预计算、复杂联表)非常有利。它的Wrapper查询条件构造方式也很灵活。国内生态活跃,文档丰富。
    • 劣势:需要手动编写或生成XML映射文件,相较于JPA,开发速度略慢,类型安全性稍弱(虽然Wrapper改善了这一点)。
  • Spring Data JPA
    • 优势:基于Hibernate,遵循JPA规范,通过方法名或@Query注解即可完成大部分查询,开发效率高。强大的级联和对象关系管理。
    • 劣势:对于极端复杂的自定义SQL,不如MyBatis直接。生成的SQL有时不够优化,在超高并发、需要精细控制数据库交互的场景下,可能成为性能瓶颈。N+1查询问题需要开发者小心规避。

我们的选择:MyBatis-Plus。理由很直接:在这个项目中,性能和控制力是第一位的。我们需要精确地控制每一次数据库操作,尤其是那些涉及乐观锁更新、复杂条件查询的场景。MyBatis-Plus的UpdateWrapper可以非常方便地实现UPDATE ... WHERE ... AND version = ?这样的乐观锁更新。同时,它的代码生成器能快速生成实体、Mapper和基础XML,弥补了开发效率的短板。

4.3 缓存与分布式协调:Redis的核心角色

Redis在这个项目中不只是缓存,更是系统状态的缓冲区和分布式协调器

  1. 缓存(Cache)
    • 存储预计算的余票数据:使用Hash或String结构。考虑到需要按多种维度(车次、日期、席别、起止站)快速查询,可能会采用精心设计的Key和Hash组合。
    • 存储热点数据:如车站信息、车次基本信息等。
  2. 分布式锁(Distributed Lock)
    • 场景:虽然数据库乐观锁是核心,但在一些更复杂的业务流程(如“一个订单同时购买多张票,需要保证要么全成功,要么全失败”)或非数据库操作(如调用外部支付接口前做校验)中,可能需要一个应用层的全局锁。我们将使用Redis的SET key value NX PX timeout命令来实现简单的分布式锁,并考虑使用Redisson客户端以获得更完善的锁功能。
  3. 消息队列(Message Queue)
    • 场景:将一些非实时强一致的操作异步化。例如,用户支付成功后,更新订单状态、扣减库存、生成车票是同步操作,但发送出票通知短信、更新用户购票历史统计等可以异步处理。
    • 选型:Redis的Stream数据结构或者List+BRPOP可以作为一个轻量级的消息队列使用,适合我们这种项目内部、吞吐量不是极端高的场景。如果后续需要更强大的功能(如消息持久化、确认机制、死信队列),可以引入RabbitMQ或Kafka。我们初期会使用Redis Stream。

4.4 其他关键组件

  • 数据库连接池:使用高性能的HikariCP,它是Spring Boot的默认选择,足够优秀。
  • 数据库:MySQL 8.0。对于关系型数据存储,它是可靠的选择。我们需要合理设计索引(特别是对daily_train_id,seat_status,carriage_index,seat_index等字段的组合索引),以及考虑未来可能的分库分表(虽然本系列可能不深入,但设计时要留有余地)。
  • API文档:使用Spring Doc OpenAPI 3(Swagger UI)来生成和展示API文档,便于前后端协作和接口调试。
  • 构建工具:Maven或Gradle。Spring Boot对两者支持都很好,选择自己熟悉的即可。
  • 压测工具:JMeter。在后续篇章中,我们将用它来模拟高并发抢票场景,检验我们的系统是否真的扛得住。

5. 环境准备与项目初始化:从零搭建脚手架

理论说得再多,不如动手开始。让我们先把开发环境搭建起来,创建一个干净的Spring Boot 3.0项目骨架。

5.1 基础环境要求

确保你的开发机已安装:

  • JDK 17或更高版本:这是Spring Boot 3.0的硬性要求。可以在命令行输入java -version确认。
  • Maven 3.6+ 或 Gradle 7.x+:用于依赖管理和项目构建。
  • MySQL 8.0:安装并启动服务,创建一个空的数据库,例如ticket_system
  • Redis 6.0+:安装并启动服务。
  • IDE:IntelliJ IDEA(推荐)或 Eclipse with STS。

5.2 使用Spring Initializr创建项目

最快捷的方式是访问 start.spring.io ,选择以下配置:

  • Project: Maven Project
  • Language: Java
  • Spring Boot: 3.0.x (选择最新的稳定版)
  • Project Metadata:
    • Group:com.yourdomain(例如com.ticket)
    • Artifact:ticket-12306
    • Name:ticket-12306
    • Packaging: Jar
    • Java: 17
  • Dependencies: 添加以下依赖:
    • Spring Web(构建Web接口)
    • Spring Data Redis(访问Redis)
    • MyBatis Framework(Spring Boot官方对MyBatis的集成)
    • MySQL Driver(连接MySQL)
    • Lombok(减少Getter/Setter等样板代码,强烈推荐)
    • Spring Doc OpenAPI(用于API文档)

点击“Generate”下载项目压缩包,解压后用IDE打开。

5.3 关键配置详解

项目创建后,我们需要配置application.yml(或application.properties)来连接数据库和Redis。

# application.yml server: port: 8080 spring: application: name: ticket-12306 # 数据源配置 datasource: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3306/ticket_system?useUnicode=true&characterEncoding=utf-8&useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true username: root password: your_password hikari: # 连接池配置,根据压测调整 maximum-pool-size: 20 minimum-idle: 10 connection-timeout: 30000 idle-timeout: 600000 max-lifetime: 1800000 # Redis配置 redis: host: localhost port: 6379 # password: 如果你的Redis有密码 database: 0 # 使用0号数据库,生产环境建议为不同业务分配不同db lettuce: pool: max-active: 20 # 连接池最大连接数 max-idle: 10 min-idle: 5 max-wait: -1ms # 连接池最大阻塞等待时间(负值表示没有限制) # MyBatis配置 mybatis: # mapper.xml文件位置,如果放在resources下需要配置 mapper-locations: classpath:mapper/*.xml configuration: map-underscore-to-camel-case: true # 自动将下划线命名映射为驼峰命名 log-impl: org.apache.ibatis.logging.stdout.StdOutImpl # 控制台打印SQL,调试用,生产环境关闭 # Spring Doc OpenAPI配置 springdoc: api-docs: path: /api-docs swagger-ui: path: /swagger-ui.html operations-sorter: method # 按HTTP方法排序

配置要点解析:

  • 数据库时区serverTimezone=Asia/Shanghai至关重要,避免日期时间数据出现时区错误。
  • HikariCP配置maximum-pool-size不宜设置过大,通常建议是CPU核心数 * 2 + 有效磁盘数。数据库连接是宝贵资源,过大的连接池反而会降低性能。我们的初始配置比较保守,后续压测后再调整。
  • Lettuce连接池:Spring Boot 2.x后默认使用Lettuce替代Jedis,它基于Netty,性能更好,支持异步。连接池配置同理,需要根据实际并发量调整。
  • MyBatis下划线转驼峰:这是非常实用的配置,可以让数据库的seat_status字段自动映射到Java实体类的seatStatus属性上。

5.4 引入MyBatis-Plus

Spring Initializr只提供了基础的MyBatis依赖。我们需要手动替换为功能更强大的MyBatis-Plus。在pom.xml中,找到mybatis-spring-boot-starter依赖,替换为:

<dependency> <groupId>com.baomidou</groupId> <artifactId>mybatis-plus-boot-starter</artifactId> <version>3.5.3.1</version> <!-- 使用与Spring Boot 3.0兼容的最新版本 --> </dependency>

同时,移除可能存在的mybatis-spring-boot-starter依赖。MyBatis-Plus几乎完全兼容MyBatis的配置,所以之前的application.yml配置仍然有效。

5.5 创建核心包结构与第一个实体

良好的包结构是项目可维护性的基础。我们按功能模块进行划分:

src/main/java/com/ticket/ ├── Ticket12306Application.java ├── config/ # 配置类(Redis配置、MyBatis-Plus分页插件配置等) ├── controller/ # 控制器层 ├── service/ # 业务逻辑层 │ ├── impl/ # 业务逻辑实现类 ├── mapper/ # MyBatis Mapper接口层 ├── entity/ # 数据库实体类 ├── dto/ # 数据传输对象(用于API入参出参) ├── vo/ # 视图对象(用于返回给前端的数据封装) ├── enums/ # 枚举类(订单状态、座位状态等) ├── utils/ # 工具类 └── common/ # 通用类(常量、异常、响应封装等)

现在,让我们创建第一个实体类Seat,并感受一下Java 17 Record和Lombok的便利。

首先,在enums包下创建座位状态枚举:

// com.ticket.enums.SeatStatusEnum package com.ticket.enums; import lombok.Getter; @Getter public enum SeatStatusEnum { AVAILABLE(0, "可用"), LOCKED(1, "已锁定"), SOLD(2, "已售出"), ; private final int code; private final String desc; SeatStatusEnum(int code, String desc) { this.code = code; this.desc = desc; } }

然后,在entity包下创建Seat实体。这里我们展示两种风格:

风格一:使用传统的Lombok@Data注解(推荐,兼容性好)

// com.ticket.entity.Seat package com.ticket.entity; import com.baomidou.mybatisplus.annotation.IdType; import com.baomidou.mybatisplus.annotation.TableId; import com.baomidou.mybatisplus.annotation.TableName; import com.ticket.enums.SeatStatusEnum; import lombok.Data; import java.time.LocalDateTime; @Data @TableName("t_seat") // 指定对应的数据库表名 public class Seat { /** * 主键ID */ @TableId(type = IdType.AUTO) // 主键自增 private Long id; /** * 关联的每日车次ID */ private Long dailyTrainId; /** * 车厢编号(如:01, 02) */ private String carriageNumber; /** * 排号 */ private Integer rowNum; /** * 列号(如:'A', 'B', 'C', 'D', 'F') */ private String colNum; /** * 座位类型(如:商务座、一等座、二等座) */ private String seatType; /** * 座位状态:0-可用,1-锁定,2-已售 */ private Integer status; /** * 乐观锁版本号 */ private Integer version; /** * 创建时间 */ private LocalDateTime createTime; /** * 更新时间 */ private LocalDateTime updateTime; // 提供一个便捷的方法获取状态枚举 public SeatStatusEnum getStatusEnum() { return SeatStatusEnum.of(this.status); } // 需要在SeatStatusEnum中添加一个`of`方法,根据code返回枚举 }

风格二:使用Java 17 Record(不可变,更简洁)

// 使用Record,但注意MyBatis-Plus等ORM框架对Record的支持可能不完善,需要测试。 // 这里仅作展示,当前项目我们仍使用传统的类+Lombok。 public record SeatRecord( Long id, Long dailyTrainId, String carriageNumber, Integer rowNum, String colNum, String seatType, Integer status, Integer version, LocalDateTime createTime, LocalDateTime updateTime ) {}

实操心得:在现阶段,虽然Record很诱人,但考虑到MyBatis-Plus的代码生成器、动态SQL构造器(如UpdateWrapper)对传统Java Bean的支持最成熟,我们选择使用@Data注解的方式。这能避免在集成过程中遇到不必要的麻烦。等生态更成熟后再迁移也不迟。

创建对应的Mapper接口:

// com.ticket.mapper.SeatMapper package com.ticket.mapper; import com.baomidou.mybatisplus.core.mapper.BaseMapper; import com.ticket.entity.Seat; public interface SeatMapper extends BaseMapper<Seat> { // 继承BaseMapper后,基本的CRUD方法已自动拥有 // 可以在此定义自定义的复杂查询方法 }

别忘了在启动类上添加@MapperScan注解,告诉MyBatis-Plus去哪里扫描Mapper接口:

// Ticket12306Application.java package com.ticket; import org.mybatis.spring.annotation.MapperScan; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; @SpringBootApplication @MapperScan("com.ticket.mapper") // 添加这行 public class Ticket12306Application { public static void main(String[] args) { SpringApplication.run(Ticket12306Application.class, args); } }

至此,一个最基础的项目骨架就搭建完成了。你可以运行启动类,如果控制台没有报错,并且能看到Spring Boot的启动Logo,说明环境基本没问题。下一篇文章,我们将开始设计数据库表,并利用MyBatis-Plus的代码生成器快速创建所有核心实体和Mapper,然后进入最激动人心的部分——实现第一个核心功能:车次与座位数据的初始化。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/8/13 5:12:17

mTLS双向认证原理与Java微服务安全实践

1. 从一次真实面试题看mTLS的核心价值去年帮团队招聘中级Java开发时&#xff0c;我设计了一道关于mTLS的压轴题。令人惊讶的是&#xff0c;20位候选人中仅有3人能说清双向TLS与普通TLS的本质区别。这反映出多数开发者对现代安全通信的理解仍停留在表面——而这恰恰是企业级开发…

作者头像 李华
网站建设 2026/8/13 5:11:56

模逆元:从RSA加密到算法竞赛,理解现代计算的数学基石

1. 模逆元&#xff1a;一个看似抽象却无处不在的“数字钥匙”在密码学、计算机安全、乃至我们日常使用的二维码和银行卡交易背后&#xff0c;都隐藏着一个关键的数学概念——模逆元。我第一次真正理解它的重要性&#xff0c;不是在数学课本上&#xff0c;而是在调试一个RSA加密…

作者头像 李华
网站建设 2026/8/13 5:07:40

基于微信小程序未成年人被侵犯案件分析与普法教育系统设计与实现

课题背景 随着移动互联网的普及&#xff0c;微信小程序凭借其轻量化、便捷化的特点&#xff0c;已成为未成年人日常使用的重要工具。然而&#xff0c;近年来未成年人通过微信小程序遭遇侵犯的案件数量呈上升趋势&#xff0c;包括网络诈骗、隐私泄露、性侵害等多种形式。这些案件…

作者头像 李华
网站建设 2026/8/13 5:07:05

基于SpringBoot+Vue的校园交流信息化管理平台设计与实现

背景随着信息技术的迅猛发展和教育信息化的深入推进&#xff0c;校园交流信息化管理平台的建设成为高校数字化转型的重要环节。当前&#xff0c;高校师生在日常教学、科研、社团活动等场景中面临信息传递效率低、资源整合不足、跨部门协作困难等问题&#xff0c;传统线下或单一…

作者头像 李华
网站建设 2026/8/13 5:06:58

基于微信小程序的乒乓社交平台的设计与实现

背景乒乓球作为一项广受欢迎的体育运动&#xff0c;具有广泛的群众基础和社会影响力&#xff0c;然而传统的乒乓球社交方式受限于场地、时间和人际关系的约束&#xff0c;难以满足现代人群对便捷、高效社交的需求。随着移动互联网技术的快速发展&#xff0c;微信小程序凭借其轻…

作者头像 李华