1. 项目缘起与挑战为什么我们要“造轮子”最近几年但凡聊到Java后端开发尤其是面试或者技术分享高并发系统设计几乎成了一个绕不开的话题。大家似乎都在谈微服务、谈分布式、谈缓存、谈消息队列但真正把一个完整的高并发业务场景从头到尾、从零到一实现一遍的人其实并不多。很多人可能看过很多文章背过很多八股文比如“Redis缓存雪崩怎么解决”、“如何设计一个秒杀系统”但真让你动手写一个可能还是会觉得无从下手或者写出来的东西一压测就崩。这正是我决定启动这个系列实战项目的初衷。与其空谈理论不如找一个足够复杂、足够有挑战性的业务场景用最新的技术栈把它实实在在地做出来。而“12306售票系统”无疑是一个绝佳的“靶子”。它几乎涵盖了高并发系统设计的所有核心痛点瞬时超高流量、复杂的业务状态车次、座位、席别、严格的库存一致性、复杂的查询与计算余票查询、以及极致的响应速度要求。网上关于它的讨论很多但大多是零散的架构图或者某个技术点的分析很少有完整的、可运行的代码实现。所以这个系列的目标非常明确我们将基于Spring Boot 3.0从零开始一步步构建一个简化但核心逻辑完整的仿12306售票系统。这不是一个简单的CRUD项目而是一个深度探索Java高并发编程、分布式系统设计原理的实战沙盘。我会把我这些年在一线处理高并发问题的经验、踩过的坑、以及那些在官方文档里不会写的“野路子”技巧都揉进代码和讲解里。在正式敲下第一行代码之前我们必须把地基打牢。很多人一上来就急着搭框架、写接口结果写到一半发现核心模型设计错了或者对并发问题的复杂性预估不足导致推倒重来。这一篇我们就来聊聊那些至关重要的“前置知识”。这些知识是你能否理解后续每一行代码设计意图的关键。2. 核心业务模型抽象从现实车票到数据对象设计系统尤其是业务复杂的系统第一步永远是理解并抽象领域模型。12306的业务看似直观——卖票但其背后的数据关系错综复杂。我们不能直接照搬铁路系统的所有细节但必须抓住最核心的实体和它们之间的关系。2.1 核心实体定义我们需要抽象出几个最关键的实体并思考它们的属性和生命周期。车次Train这是调度和运行计划的核心。一个车次如G101代表一趟具体的列车运行安排。关键属性车次号、始发站、终点站、出发时间、到达时间、运行日期、列车类型高铁、动车、普快等。设计思考车次是一个“模板”或“计划”。它本身不直接参与售票但它定义了后续一切的基础。一个车次在特定日期会生成一个具体的“车次实例”。车次实例DailyTrain这是真正可售卖的主体。因为G101车次每天都会运行但2023年10月1日的G101和10月2日的G101是两趟不同的、独立的库存实体。关键属性关联车次ID、具体日期、一个最重要的字段——初始座位库存。这个库存需要根据列车类型如16节车厢的复兴号和席别商务、一等、二等在系统初始化时生成。设计思考这是库存管理的核心单元。所有的余票查询、扣减库存、座位选择都是基于某个日期的某个车次实例进行的。必须将它与“车次”模板分离否则库存无法按天管理。座位Seat与车厢Carriage为了支持“选座”功能我们需要更细粒度的库存管理而不是简单地用一个数字表示“二等座还剩100张”。关键属性Seat所属车次实例ID、车厢编号、排号、列号如“05车10F”、席别、座位状态可用、已售、锁定等。关键属性Carriage车厢编号、车厢类型、定员数、座位排布规则用于生成具体的座位记录。设计思考用“座位”作为库存的最小单位是实现“一人一票一座”和“选座”功能的基础。这比使用“余票数”计数器要复杂得多因为它涉及到对单个座位的状态进行并发修改是后续解决“超卖”问题的关键设计点。余票TicketStock这是一个衍生数据或缓存数据。虽然我们有每一个座位但每次查询余票时实时统计某个车次实例下某个区段、某个席别的可用座位数性能是无法接受的。设计思考我们需要一个独立的余票数据模型它可能是数据库汇总表定期或触发式更新某个车次实例-席别-出发站-到达站组合的可用座位计数。Redis缓存将上述计算结果放入内存提供毫秒级查询。这是高并发查询的标配方案。核心难点如何保证“座位”的真实状态与“余票”缓存数据的一致性这是一个经典的缓存与数据库一致性问题。购票订单TicketOrder用户下单后的产物。关键属性订单号、用户ID、车次实例ID、出发站、到达站、乘车人信息、所选座位信息、订单状态初始化、待支付、已支付、已完成、已取消等、订单总价。设计思考订单状态机是整个购票流程的驱动核心。状态的变化如“待支付” - “已支付”会触发库存的最终扣减、座位的最终占用。订单号必须全局唯一且具备业务含义如包含日期、渠道等信息。购票记录Ticket支付成功后生成的最终票务凭证。关键属性票号、关联订单ID、乘客姓名、身份证号、车次、座位号、票价、状态已出票、已改签、已退票等。设计思考一张票是订单的最终履约产物。它与订单是多对一的关系一个订单可能包含多张票。票的生命周期独立于订单例如改签、退票操作是针对票进行的。注意在实际的12306中模型远比这复杂涉及车站、线路、票价计算规则、学生票/儿童票等。在我们的简化版本中我们会聚焦于最核心的“查票-选座-下单-支付”流程因此上述模型已经足够支撑我们探索高并发核心技术。2.2 模型关系与状态流转理清实体后我们需要用一张图这里用文字描述来串联它们的关系和关键业务流程的状态流转初始化流程管理员创建车次- 系统在每天凌晨为未来N天的该车次生成车次实例- 根据车次类型和车厢配置为每个车次实例生成具体的座位记录 - 初始化或更新余票缓存。查询流程用户输入条件 - 系统查询余票缓存极快 - 返回车次列表和余票数量。购票流程简化核心步骤A占座用户选择车次、席别、座位 - 系统尝试锁定Lock选中的座位记录将其状态改为“锁定”并扣减余票缓存。步骤B下单锁定成功后创建购票订单状态为“待支付”并将锁定的座位与订单关联。这里会设置一个锁定时效如15分钟。步骤C支付用户支付成功 - 回调系统将购票订单状态改为“已支付” - 将关联的座位状态改为“已售” - 生成购票记录。步骤D超时释放如果用户在锁定时效内未支付定时任务会扫描状态为“待支付”且超时的订单 - 将关联的座位状态恢复为“可用” - 增加余票缓存。这个流程中从“步骤A”到“步骤C”涉及对同一座位记录的多次状态修改可用-锁定-已售且可能被成千上万的请求同时操作这就是高并发冲突最集中的地方。如何保证一个座位不被两个人同时买到超卖如何保证缓存里的余票数和数据库里真实的可用座位数一致这些问题是整个系统的“命门”。3. 高并发核心难题拆解库存一致性这座大山理解了业务模型我们就能清晰地看到技术挑战所在。所有挑战都围绕着一个核心在每秒数万甚至数十万次请求下如何保证车票库存数据特别是座位状态的绝对正确性3.1 超卖问题并发写的终极挑战“超卖”是指库存只有1件但成功卖出了2件或更多。在我们的场景里就是同一个座位卖给了两个人。这是电商、票务等系统的经典难题。为什么在并发下会发生超卖假设座位Seat#A状态为“可用”。两个用户几乎同时请求购买它。请求1查询Seat#A状态为“可用”。请求2查询Seat#A状态为“可用”。请求1执行UPDATE seat SET status 锁定 WHERE id A AND status 可用。请求2执行同样的UPDATE语句。在数据库层面如果只是简单的UPDATE ... SET status 锁定 WHERE id A那么两个请求都会执行成功因为它们查询时状态都是“可用”这就超卖了。解决方案思路悲观锁在查询时就直接用SELECT ... FOR UPDATE锁定行。这能解决问题但性能极差会把并发操作彻底串行化在超高并发下等于系统自杀。乐观锁给座位记录增加一个版本号字段version。更新时带上版本条件UPDATE seat SET status 锁定, version version 1 WHERE id A AND status 可用 AND version #{oldVersion}。这样只有第一个更新能成功第二个更新会因为version不匹配而返回0行受影响从而失败。这是更优的选择。分布式锁在应用层使用Redis或ZooKeeper等实现一个分布式锁确保在同一时间对同一个座位ID的写操作只有一个线程能执行。这引入了外部组件增加了复杂度但控制粒度更灵活。数据库唯一约束结合业务设计例如将“车次实例日期车厢座位号”设置为唯一索引并在创建订单或票记录时以此作为条件。如果两个请求试图插入相同的座位占用记录后者会因唯一冲突而失败。在我们的项目中会综合使用乐观锁和唯一约束。在座位状态变更时使用乐观锁在生成最终票务记录时利用唯一约束做最终防重。3.2 缓存与数据库的一致性问题为了应对海量查询余票信息必须放在Redis里。但用户购票写操作最终修改的是数据库里的座位状态。这就产生了数据不一致的窗口期。不一致场景示例数据库座位Seat#A状态为“可用”。Redis余票数100。请求1成功锁定Seat#A数据库将其状态改为“锁定”。此时如果Redis余票数没有及时更新仍然是100。请求2查询余票Redis告诉它还有100张于是它继续走购票流程尝试锁定另一个座位但实际上可能已经没有真正可用的座位了虽然数据库有锁定的但用户看来就是有票。这会导致大量请求穿透到数据库造成不必要的压力甚至引发误判。解决方案思路没有银弹只有权衡Cache Aside Pattern旁路缓存这是最常用的模式。读先读缓存命中则返回未命中则读数据库写入缓存。写先更新数据库再删除缓存。为什么是删除而不是更新缓存因为更新缓存可能引发并发写缓存的一致性问题且删除操作是幂等的。在我们的场景用户购票成功后直接删除该车次该席别的余票缓存。下次查询时缓存未命中会从数据库重新计算并加载最新的余票数。这个方案简单但在删除缓存后、新缓存建立前会有短暂的数据不一致并且可能引起“缓存击穿”大量请求同时打到数据库。定期刷新启动一个定时任务每隔很短时间如1秒扫描数据库重新计算所有车次的余票并刷新到Redis。这能保证最终一致性且刷新间隔内的数据是准确的。但对数据库有持续压力且存在最多1秒的延迟。基于Binlog的异步刷新使用Canal、Debezium等工具监听数据库的Binlog当seat表发生变更时实时计算并更新Redis缓存。这能实现准实时的一致性但对架构复杂度和运维要求较高。在我们的项目中初期会采用“Cache Aside 异步刷新兜底”的策略。即写操作后主动删除缓存同时有一个低频的定时任务比如每10秒做全量或增量刷新作为防止缓存长期不一致的兜底机制。3.3 高并发查询余票查询的优化即使解决了写一致性问题读的压力依然巨大。春运期间首页余票查询的QPS可能是天文数字。核心挑战复杂计算余票不是简单的COUNT(*)。它需要计算指定区段的可用座位。例如从北京到上海的车次用户查“南京到苏州”的余票需要计算那些全程中南京到苏州这一段未被售出的座位。这是一个复杂的空间计算。实时性数据必须尽可能新。解决方案思路预计算这是关键。我们不可能在每次查询时实时联表计算。必须在后台提前算好。维度为每个车次实例预计算所有可能出发站-到达站组合站序在前的作为出发站的每个席别的余票数。存储将计算结果以结构化的方式存入Redis。例如Key设计为ticket_stock:{daily_train_id}:{start_station_index}:{end_station_index}:{seat_type}Value就是可用座位数或可用座位ID列表如果要做选座。更新当某个座位被售出或释放时需要更新所有包含该座位区段的预计算结果。例如座位被从南京卖到苏州那么所有出发站序南京且到达站序苏州的区段其对应席别的余票数都要-1。这是一个O(n)的操作需要仔细设计数据结构和更新逻辑。多级缓存L1 - 本地缓存Caffeine/Guava Cache在应用服务器内存中缓存一些最热门的车次查询结果如未来2小时内出发的热门车次。设置很短的过期时间如1秒应对瞬时爆发流量。L2 - 分布式缓存Redis存储全量的预计算余票数据。L3 - 数据库作为最终的数据源和兜底。请求合并与降级对于完全相同的查询请求可以在应用层进行短时间如10毫秒的合并将多个请求合并为一个去查询缓存或数据库减少重复开销。当系统压力过大时可以降级余票查询的实时性比如返回稍早一些的缓存快照。在我们的实现中预计算Redis存储将是余票查询的基石。我们会详细设计如何高效地存储和更新这些预计算数据。4. 技术栈选型与Spring Boot 3.0的新特性明确了业务挑战我们来看看手中的“武器”。技术选型直接决定了实现的复杂度和系统的天花板。4.1 核心框架为什么是Spring Boot 3.0Spring Boot 3.0是一个重大升级版本它基于Spring Framework 6.0并要求最低Java 17。对于我们的高并发项目它带来了几个关键好处原生支持GraalVM原生镜像虽然我们初期不一定会用但这是未来性能压榨的一个重要方向。Spring Boot 3.0对AOTAhead-Of-Time编译提供了更好的支持能生成启动更快、内存占用更小的原生应用非常适合云原生和资源敏感场景。Java 17特性LTS版本的Java 17提供了很多现代语言特性如Records用于定义数据模型减少样板代码、Sealed Classes限制类的继承增强领域模型的安全性、Text Blocks更好的多行字符串支持等能让我们的代码更简洁、更安全。改进的观测性Spring Boot 3.0深度集成了Micrometer提供了开箱即用的可观测性Observability支持包括指标Metrics、追踪Tracing和日志Logging。这对于我们后期监控系统性能、定位高并发下的瓶颈至关重要。** 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有时不够优化在超高并发、需要精细控制数据库交互的场景下可能成为性能瓶颈。N1查询问题需要开发者小心规避。我们的选择MyBatis-Plus。理由很直接在这个项目中性能和控制力是第一位的。我们需要精确地控制每一次数据库操作尤其是那些涉及乐观锁更新、复杂条件查询的场景。MyBatis-Plus的UpdateWrapper可以非常方便地实现UPDATE ... WHERE ... AND version ?这样的乐观锁更新。同时它的代码生成器能快速生成实体、Mapper和基础XML弥补了开发效率的短板。4.3 缓存与分布式协调Redis的核心角色Redis在这个项目中不只是缓存更是系统状态的缓冲区和分布式协调器。缓存Cache存储预计算的余票数据使用Hash或String结构。考虑到需要按多种维度车次、日期、席别、起止站快速查询可能会采用精心设计的Key和Hash组合。存储热点数据如车站信息、车次基本信息等。分布式锁Distributed Lock场景虽然数据库乐观锁是核心但在一些更复杂的业务流程如“一个订单同时购买多张票需要保证要么全成功要么全失败”或非数据库操作如调用外部支付接口前做校验中可能需要一个应用层的全局锁。我们将使用Redis的SET key value NX PX timeout命令来实现简单的分布式锁并考虑使用Redisson客户端以获得更完善的锁功能。消息队列Message Queue场景将一些非实时强一致的操作异步化。例如用户支付成功后更新订单状态、扣减库存、生成车票是同步操作但发送出票通知短信、更新用户购票历史统计等可以异步处理。选型Redis的Stream数据结构或者ListBRPOP可以作为一个轻量级的消息队列使用适合我们这种项目内部、吞吐量不是极端高的场景。如果后续需要更强大的功能如消息持久化、确认机制、死信队列可以引入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 3Swagger 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安装并启动服务。IDEIntelliJ IDEA推荐或 Eclipse with STS。5.2 使用Spring Initializr创建项目最快捷的方式是访问 start.spring.io 选择以下配置Project: Maven ProjectLanguage: JavaSpring Boot: 3.0.x (选择最新的稳定版)Project Metadata:Group:com.yourdomain(例如com.ticket)Artifact:ticket-12306Name:ticket-12306Packaging: JarJava: 17Dependencies: 添加以下依赖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?useUnicodetruecharacterEncodingutf-8useSSLfalseserverTimezoneAsia/ShanghaiallowPublicKeyRetrievaltrue 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方法排序配置要点解析数据库时区serverTimezoneAsia/Shanghai至关重要避免日期时间数据出现时区错误。HikariCP配置maximum-pool-size不宜设置过大通常建议是CPU核心数 * 2 有效磁盘数。数据库连接是宝贵资源过大的连接池反而会降低性能。我们的初始配置比较保守后续压测后再调整。Lettuce连接池Spring Boot 2.x后默认使用Lettuce替代Jedis它基于Netty性能更好支持异步。连接池配置同理需要根据实际并发量调整。MyBatis下划线转驼峰这是非常实用的配置可以让数据库的seat_status字段自动映射到Java实体类的seatStatus属性上。5.4 引入MyBatis-PlusSpring Initializr只提供了基础的MyBatis依赖。我们需要手动替换为功能更强大的MyBatis-Plus。在pom.xml中找到mybatis-spring-boot-starter依赖替换为dependency groupIdcom.baomidou/groupId artifactIdmybatis-plus-boot-starter/artifactId version3.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实体。这里我们展示两种风格风格一使用传统的LombokData注解推荐兼容性好// 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 BaseMapperSeat { // 继承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然后进入最激动人心的部分——实现第一个核心功能车次与座位数据的初始化。