搞定景区门票预订系统:3个核心模块完整示例
搞定景区门票预订系统:3个核心模块完整示例 刚把老版本的 Spring Boot 升到 2.7 准备上线,结果一跑测试,API 全变了。@Autowired 报红,RestTemplate 的构造方法也没了,直接懵圈。这种“版本升级后 API 全变了”的痛,谁懂? 别急,我整理了一套基于 Spring Boot 3.0 + MyBatis-Plus 的完整示例,专门针对景区门票预订系统。这套代码在掘金技术社区被不少同行验证过,不仅解决了依赖冲突,还把并发扣库存的坑填平了。 1. 概念速懂:为什么选这套技术栈 很多初学者问,做个门票系统,为什么非要上 Spring Boot 3.0? 以前用 Spring Boot 2.x,很多配置是隐式的,现在 3.0 强制要求你显式声明。比如,以前 new RestTemplate() 随便用,现在必须注入 Bean。这看似麻烦,实则规范。 景区门票预订系统的核心不是卖票,而是高并发下的数据一致性。 想象一下,五一假期,某热门景区门票只剩 500 张,1000 人同时点击购买。如果代码写得烂:超卖,或者两个人买到同一张票。 如果代码写得稳:一人一票,库存精确扣减。本教程基于以下环境:Java: 17 (LTS版本,性能优于11) Spring Boot: 3.0.5 Database: MySQL 8.0 ORM: MyBatis-Plus 3.5.3.1注意:MyBatis-Plus 官方文档明确支持 Spring Boot 3,但需要引入 mybatis-plus-spring-boot3-starter,而不是老版本的 starter。这点搞错,直接启动失败。 2. 环境准备:避坑指南 2.1 依赖配置 (pom.xml) 这是最关键的一步。很多报错源于依赖版本不兼容。 dependencies!-- Web 启动器 --dependencygroupIdorg.springframework.boot/groupIdartifactIdspring-boot-starter-web/artifactId/dependency!-- MyBatis-Plus for Spring Boot 3 --!-- 注意:这里必须用 boot3 后缀,否则 Bean 注入失败 --dependencygroupIdcom.baomidou/groupIdartifactIdmybatis-plus-spring-boot3-starter/artifactIdversion3.5.3.1/version/dependency!-- MySQL 驱动 --dependencygroupIdcom.mysql/groupIdartifactIdmysql-connector-j/artifactIdscoperuntime/scope/dependency!-- Lombok,减少 getter/setter --dependencygroupIdorg.projectlombok/groupIdartifactIdlombok/artifactIdoptionaltrue/optional/dependency /dependencies核心要点:mysql-connector-j:MySQL 8 的驱动包名变了,老版本叫 mysql-connector-java,现在叫 mysql-connector-j。搜不到包?那就是版本号没写对。 mybatis-plus-spring-boot3-starter:这是解决“API 全变了”的关键。它自动配置了 SqlSessionFactory,适配了新的 jakarta.persistence 包名。2.2 数据库设计 创建一个简单的门票表 ticket: CREATE TABLE `ticket` (`id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '主键',`scenic_name` VARCHAR(100) NOT NULL COMMENT '景区名称',`price` DECIMAL(10, 2) NOT NULL COMMENT '价格',`stock` INT NOT NULL DEFAULT 0 COMMENT '剩余库存',`create_time` DATETIME DEFAULT CURRENT_TIMESTAMP,PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='门票表';-- 插入测试数据 INSERT INTO `ticket` (`scenic_name`, `price`, `stock`) VALUES ('黄山风景区', 190.00, 100), ('故宫博物院', 60.00, 50);3. 核心语法:Entity 与 Mapper 3.1 实体类 (Entity) 使用 Lombok 简化代码。注意,MyBatis-Plus 需要 @TableName 注解映射表名。 package com.example.tickets.entity;import com.baomidou.mybatisplus.annotation.IdType; import com.baomidou.mybatisplus.annotation.TableId; import com.baomidou.mybatisplus.annotation.TableName; import lombok.Data; import java.math.BigDecimal; import java.time.LocalDateTime;@Data @TableName(ticket) public class Ticket {@TableId(type = IdType.AUTO)private Long id;private String scenicName;private BigDecimal price;// 库存字段,后续并发处理的关键private Integer stock;private LocalDateTime createTime; }细节解读:@Data:自动生成 getter、setter、toString 等方法。 @TableId(type = IdType.AUTO):指定主键策略为数据库自增。如果是雪花算法,这里要改。 BigDecimal:处理金额时,严禁使用 double 或 float,否则会有精度丢失问题。190.00 变成 190.000000000001,客诉就来了。3.2 Mapper 接口 MyBatis-Plus 的强大之处在于,你几乎不需要写 XML。 package com.example.tickets.mapper;import com.baomidou.mybatisplus.core.mapper.BaseMapper; import com.example.tickets.entity.Ticket; import org.apache.ibatis.annotations.Mapper; import org.apache.ibatis.annotations.Update;@Mapper public interface TicketMapper extends BaseMapperTicket {/*** 自定义 SQL:扣减库存* 关键:WHERE stock 0 防止超卖* * @param id 门票ID* @param amount 扣减数量* @return 影响行数*/@Update(UPDATE ticket SET stock = stock - #{amount} WHERE id = #{id} AND stock = #{amount})int deductStock(@Param(id) Long id, @Param(amount) int amount); }为什么用 @Update 而不是代码里 setStock(stock - 1)? 这是并发安全的核心。错误写法:线程A 查询 stock = 10 线程B 查询 stock = 10 线程A 计算 10-1=9,更新 stock=9 线程B 计算 10-1=9,更新 stock=9 结果:卖了2张票,库存只扣了1张。超卖!正确写法(数据库层原子操作):线程A 执行 UPDATE ... WHERE stock = 1,成功,stock=9 线程B 执行 UPDATE ... WHERE stock = 1,成功,stock=8 结果:卖了2张票,库存扣了2张。安全!这种利用数据库行锁 + 条件更新的方式,比加 synchronized 或 Redis 锁更简单、更可靠,适用于中等并发场景(QPS 5000)。 4. 完整代码示例:Service 与 Controller 4.1 Service 层:业务逻辑 这里我们模拟一个购买流程:查询 - 扣库存 - 返回结果。 package com.example.tickets.service;import com.example.tickets.entity.Ticket; import com.example.tickets.mapper.TicketMapper; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional;import java.math.BigDecimal;@Slf4j @Service @RequiredArgsConstructor public class TicketService {private final TicketMapper ticketMapper;/*** 购买门票* * @param ticketId 门票ID* @param buyCount 购买数量* @return 购买是否成功*/@Transactional(rollbackFor = Exception.class)public boolean buyTicket(Long ticketId, int buyCount) {// 1. 校验库存Ticket ticket = ticketMapper.selectById(ticketId);if (ticket == null) {log.warn(Ticket not found, id: {}, ticketId);return false;}if (ticket.getStock() buyCount) {log.warn(Stock not enough, id: {}, current: {}, required: {}, ticketId, ticket.getStock(), buyCount);return false;}// 2. 扣减库存 (核心原子操作)int rows = ticketMapper.deductStock(ticketId, buyCount);if (rows == 0) {// 扣减失败,可能是并发竞争导致log.error(Deduct stock failed, possible concurrency conflict, id: {}, ticketId);return false;}// 3. 这里可以插入订单记录// orderService.createOrder(ticket, buyCount);log.info(Purchase success, ticketId: {}, count: {}, ticketId, buyCount);return true;}public BigDecimal getPrice(Long ticketId) {Ticket ticket = ticketMapper.selectById(ticketId);return ticket != null ? ticket.getPrice() : BigDecimal.ZERO;} }避坑点:@Transactional:确保事务一致性。虽然本例只有两步,但生产环境中,扣库存和创建订单必须在同一事务中。 rows == 0 的判断:这是兜底机制。即使前面校验了库存,高并发下也可能瞬间变为0。必须依赖 SQL 的执行结果来判断是否成功。4.2 Controller 层:REST API package com.example.tickets.controller;import com.example.tickets.service.TicketService; import lombok.RequiredArgsConstructor; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.*;@RestController @RequestMapping(/api/tickets) @RequiredArgsConstructor public class TicketController {private final TicketService ticketService;/*** 购买门票* POST /api/tickets/{id}/buy?count=1*/@PostMapping(/{id}/buy)public ResponseEntityString buy(@PathVariable Long id, @RequestParam int count) {if (count = 0 || count 5) {return ResponseEntity.badRequest().body(Invalid count, must be between 1 and 5);}boolean success = ticketService.buyTicket(id, count);if (success) {return ResponseEntity.ok(Purchase successful);} else {return ResponseEntity.status(500).body(Purchase failed: Out of stock or system error);}}/*** 查询价格* GET /api/tickets/{id}/price*/@GetMapping(/{id}/price)public ResponseEntityString price(@PathVariable Long id) {var price = ticketService.getPrice(id);return ResponseEntity.ok(price.toString());} }5. 常见报错与解决 在实际部署中,以下三个报错出现频率最高: 5.1 BeanCreationException: Error creating bean with name 'sqlSessionFactory' 现象:应用启动失败,日志里提示 sqlSessionFactory 无法创建。 原因:使用了错误的 MyBatis-Plus 依赖(用了 starter 而不是 boot3-starter)。 application.yml 中数据源配置错误,导致无法连接数据库。解决: 检查 pom.xml,确保是 mybatis-plus-spring-boot3-starter。 检查 application.yml: spring:datasource:url: jdbc:mysql://localhost:3306/ticket_db?useSSL=falseserverTimezone=UTCcharacterEncoding=utf8username: rootpassword: 123456driver-class-name: com.mysql.cj.jdbc.Driver注意:MySQL 8 驱动类是 com.mysql.cj.jdbc.Driver,老版本是 com.mysql.jdbc.Driver。写错驱动类,连接直接失败。 5.2 NullPointerException: Cannot invoke Ticket.getStock() because ticket is null 现象:运行时抛出空指针异常。 原因: 查询了一个不存在的 ticketId,selectById 返回 null,后续调用 getStock() 报错。 解决: 在 Service 层增加判空逻辑(如 4.1 节代码所示)。永远不要假设数据库查询一定有结果。 5.3 BadSqlGrammarException: You have an error in your SQL syntax 现象:执行 deductStock 时报错。 原因:SQL 语法错误。 数据库表结构与设计不符(例如字段名拼写错误,stock 写成了 stok)。解决:打印完整 SQL 日志,复制到 MySQL 命令行执行,查看具体错误行。 检查 @Update 注解中的 SQL 语句,特别是 #{amount} 和 #{id} 的参数名是否与 @Param 一致。6. 小结与进阶 这套景区门票预订系统的完整示例,覆盖了从环境配置到核心业务逻辑的全流程。 核心收获:Spring Boot 3 迁移:注意依赖包名变更,特别是 MyBatis-Plus 和 MySQL 驱动。 并发安全:利用数据库的 UPDATE ... WHERE 原子操作,避免应用层锁的性能瓶颈。 防御式编程:对数据库查询结果进行判空,对业务参数进行边界校验。进阶方向:Redis 缓存:将门票价格、剩余库存(非精确值)放入 Redis,减少数据库读压力。 消息队列:将“创建订单”操作异步化,先扣库存,再发消息给 MQ,由消费者创建订单。 分布式锁:如果 QPS 超过 5000,数据库行锁可能成为瓶颈,此时引入 Redisson 分布式锁。掘金技术社区上有不少关于高并发票务系统的深度剖析,建议结合本文代码,搜索“Redis 库存扣减”或“分布式事务”进一步学习。 最后,抛出一个问题给大家讨论: 在门票预订场景中,你更倾向于使用 Redis + Lua 脚本 进行库存预扣,还是直接依赖 MySQL 行锁? 考虑到低延迟和高可用的权衡,你实际项目中更常用哪种写法?评论区交流,看看有多少同行踩过类似的坑。

相关新闻

5个避坑点解析抢淘宝优惠券软件核心逻辑速查手册

5个避坑点解析抢淘宝优惠券软件核心逻辑速查手册

5个避坑点解析抢淘宝优惠券软件核心逻辑速查手册 版本升级后 API 全变了,你的爬虫脚本是不是直接报 403 Forbidden ?别急着骂平台反爬升级快,先看看你手里的 速查手册 是不是还停留在去年的 Cookie…

2026/9/22 10:51:33 阅读更多 →
微信运动修改踩坑实录

微信运动修改踩坑实录

3步搞定微信运动数据同步实战项目避坑指南 别再盯着语法手册发呆,把“微信运动修改”当成一个 实战项目 来拆解,你才真正懂开发。很多兄弟学了 Python 或…

2026/9/22 10:51:33 阅读更多 →
www.znhr.com源码解析:3步搞定官方文档痛点

www.znhr.com源码解析:3步搞定官方文档痛点

www.znhr.com源码解析:3步搞定官方文档痛点 别再对着几百页的官方文档发呆抓瞎了。 很多开发者拿到 www.znhr.com 的相关资料,第一反应是头大。 页面层级深、术语堆砌多,根本抓不住核心重点。…

2026/9/22 10:51:33 阅读更多 →

最新新闻

搞定电子邮件号码大全:图解原理与3倍性能优化实战

搞定电子邮件号码大全:图解原理与3倍性能优化实战

搞定电子邮件号码大全:图解原理与3倍性能优化实战 你是不是也这样?Python语法书翻了三遍,LeetCode刷了上百题,可一旦要落地一个处理百万级邮件数据的真实项目,脑子瞬间一片空白。…

2026/9/22 11:33:04 阅读更多 →
Windows开发避坑:3年踩坑经验总结的保姆级教程

Windows开发避坑:3年踩坑经验总结的保姆级教程

Windows开发避坑:3年踩坑经验总结的保姆级教程 面试被问“Windows消息循环底层是怎么转发的”,90%的应届生只能回答“PostMessage然后WndProc处理”,却说不清线程亲和性、窗口句柄哈希表结构。这就是典型的…

2026/9/22 11:33:04 阅读更多 →
原创的英文手写实现:3个步骤搞定复制代码报错难题

原创的英文手写实现:3个步骤搞定复制代码报错难题

原创的英文手写实现:3个步骤搞定复制代码报错难题 复制来的代码跑不通,报错信息看得人头皮发麻,却不知从何下手。别慌,这正是 手写实现 价值所在。今天不讲虚的,直接拆解【原创的英文】底层逻辑,让你彻底摆脱“调参救火”的困境。…

2026/9/22 11:33:04 阅读更多 →
opencodex 代理 Codex 流式错误根因分析:从 `ApiError::Stream` 触发器到 RC1–RC5 修复全景

opencodex 代理 Codex 流式错误根因分析:从 `ApiError::Stream` 触发器到 RC1–RC5 修复全景

opencodex 代理 Codex 流式错误根因分析:从 ApiError::Stream 触发器到 RC1–RC5 修复全景 【免费下载链接】opencodex Universal provider proxy for OpenAI Codex & Claude Code — use any LLM (Claude, Gemini, Grok, DeepSeek, Ollama…) with Codex CLI, …

2026/9/22 11:33:04 阅读更多 →
2026最新管理评论性能优化:3步解决接口卡顿面试难题

2026最新管理评论性能优化:3步解决接口卡顿面试难题

2026最新管理评论性能优化:3步解决接口卡顿面试难题 面试被问原理答不上来,是不是让你当场冷汗直流?特别是遇到“管理评论”这类高并发场景,代码写得跑得通,一压测就崩,面试官眉头一皱,这单基本就没了。2026最新的技术栈里,大家不再满足于C…

2026/9/22 11:33:04 阅读更多 →
qq头像带字的男生伤感避坑指南:5个坑让性能提升3倍

qq头像带字的男生伤感避坑指南:5个坑让性能提升3倍

qq头像带字的男生伤感避坑指南:5个坑让性能提升3倍 刚接手项目,配置环境就卡半天?别急着骂人。 很多开发者在搭建本地开发环境时,都会遇到各种“玄学”问题。依赖冲突、版本不兼容、端口占用,这些问题往往比业务逻辑更让人头疼。 今天这篇…

2026/9/22 11:32:04 阅读更多 →

日新闻

3台商务办公笔记本实测:手写实现环境配置,告别卡半天

3台商务办公笔记本实测:手写实现环境配置,告别卡半天

3台商务办公笔记本实测:手写实现环境配置,告别卡半天 配置环境就卡半天?别怪机器慢,多半是你没选对工具链。在Java、Go或Python的项目现场, 手写实现…

2026/9/22 0:00:41 阅读更多 →
剑帝加点速查手册:3分钟搞懂核心逻辑

剑帝加点速查手册:3分钟搞懂核心逻辑

剑帝加点速查手册:3分钟搞懂核心逻辑 面试被问原理答不上来,是不是常态?别慌。很多开发者对着 GitHub 开源仓库里的代码发呆,看似简单实则暗藏玄机。今天这份【剑帝加点】速查手册,直接带你拆解核心实现,把面试必考的原理讲透。…

2026/9/22 0:00:41 阅读更多 →
手写实现图片压缩网站核心:搞定WebP转换与质量调优

手写实现图片压缩网站核心:搞定WebP转换与质量调优

手写实现图片压缩网站核心:搞定WebP转换与质量调优 复制来的代码跑不通不知道怎么调?别慌,这种“复制粘贴地狱”在开发圈太常见了。尤其是做 图片压缩网站…

2026/9/22 0:00:41 阅读更多 →

周新闻

Flutter for OpenHarmony游戏卡片渐变背景实战:从原理到性能优化

Flutter for OpenHarmony游戏卡片渐变背景实战:从原理到性能优化

直接铺开项目本身吧。这几个月我一直在折腾一件事:用Flutter给OpenHarmony做一款游戏集合类的App,说白了就是把若干小游戏塞进一个壳里,用统一入口分发。这个方向本身不算新鲜,真正让我花了不少心思的,是首页那堆游戏卡…

2026/9/22 4:32:41 阅读更多 →
Word表格编号全攻略:从列表编号到题注交叉引用

Word表格编号全攻略:从列表编号到题注交叉引用

写Word文档,最让人头疼的往往是那些“看起来不起眼”的小问题。比如表格编号这事:今天在表后面多加了两个空白行,明天给客户交稿前发现整个章节的编号全部错位,光是挨个改序号就能耗掉大半个下午。我前阵子帮人整理一份上百页的技…

2026/9/22 4:38:57 阅读更多 →
从第一个站到第二个站:独立开发者的静态网站选型与落地实践

从第一个站到第二个站:独立开发者的静态网站选型与落地实践

1. 项目概述1.1 核心需求解析做独立开发者这几年,说实话,第一个网站上线的那天晚上我兴奋得没睡着。但等它跑了半年,流量惨淡、功能臃肿、代码自己都懒得看第二遍之后,我才慢慢琢磨明白一个道理:第一个网站是练手&…

2026/9/22 8:51:04 阅读更多 →

月新闻

持续集成 流水线自动化与 声明式交付 实践:原型怎样变成可用功能

持续集成 流水线自动化与 声明式交付 实践:原型怎样变成可用功能

持续集成 流水线自动化与 声明式交付 实践:原型怎样变成可用功能分类:[AI/大模型]细分主题:AI 增强型 CI/CD 流水线自动化与 GitOps 实践:Agent 工作流、工具调用与任务拆解:从原型到生产的验收清单很多团队在尝试用大…

2026/9/21 15:36:51 阅读更多 →
容器编排 生产环境运维与排障实战:复盘记录怎样真正派上用场

容器编排 生产环境运维与排障实战:复盘记录怎样真正派上用场

容器编排 生产环境运维与排障实战:复盘记录怎样真正派上用场分类:[工程技术]细分主题:Kubernetes 生产环境运维与排障实战:可复制的项目复盘模板与决策记录大部分团队的事故复盘报告,最后都变成了躺在 Confluence 或钉…

2026/9/21 15:36:51 阅读更多 →
容器 容器化技术与镜像安全管理:核心链路应该先拆哪一步

容器 容器化技术与镜像安全管理:核心链路应该先拆哪一步

容器 容器化技术与镜像安全管理:核心链路应该先拆哪一步分类:[工程技术]细分主题:Docker 容器化技术与镜像安全管理:核心链路的逐步实现与关键代码取舍面对一个积累了五六年历史包袱的单体架构应用(包含 Web 接口、后台…

2026/9/22 2:43:42 阅读更多 →