Spring Boot剧本杀预约系统:场次排期与并发防超卖实战解析

发布时间:2026/10/22 5:10:19
Spring Boot剧本杀预约系统:场次排期与并发防超卖实战解析
我做过的Spring Boot项目里剧本杀预约管理系统算是比较有代表性的一个。它并不复杂但刚好把真实业务里最容易踩坑的“场次排期、并发预约、状态流转”都覆盖到了。不管你是准备拿它当毕业设计还是想帮一家实体剧本杀店做一套预约工具这套Springboot剧本杀预约管理系统都能省掉很大一部分从零开发的工作量。项目包含程序、源码、数据库脚本、调试部署步骤、开发环境配置还附带一万字以上的设计文档。接下来我不打算把文档内容复述一遍而是从设计者的角度把这套系统的关键决策、核心实现、部署过程和测试结果讲清楚。哪怕你刚接触Spring Boot跟着后面的步骤也能把项目跑起来。1. 剧本杀门店的预约困境这个系统到底解决什么问题1.1 线下预约的三个典型痛点剧本杀门店的预约场景比影院卖票还要特殊一点。一方面一个剧本对应一个房间房间有容量上限同一时间段不能安排两场另一方面玩家动辄五六人一组临时取消很常见。早期很多门店靠微信群接龙或者前台手写登记问题非常明显。第一个痛点是信息不实时。玩家在群里问“今晚还有没有位”老板得翻聊天记录、查本子回复不及时就流失客人。第二个痛点是重复占位。同一组客人可能在群里接龙后又打电话确认或者两个玩家各帮全队订了同个场次后台根本没有校验能力。第三个痛点是经营数据一团乱。每个剧本被预约过几次、取消率多少、哪个房间利用率高全靠脑子记月底盘点对不上账。这套系统要解决的就是这三件事让玩家能实时看到可约场次让门店能管理剧本和房间让订单和统计都能自动沉淀下来。1.2 系统角色的划分与使用场景项目按使用场景拆成了三类角色。玩家端面对的是C端流程注册登录、浏览剧本列表、查看某个剧本的场次、提交预约、查看自己的预约记录、取消预约。商家端面对的是门店运营维护剧本信息、管理房间、设置场次、审核或取消订单、看经营数据。管理员端负责基础配置和用户管理确保系统本身不会出乱子。这种角色划分不是拍脑袋。预约业务天然需要“用户-场次-订单”三条主线商家要在中间承接管理员做兜底。后续如果要接小程序、接支付也只需要在这三个角色上做增量不需要推倒重来。1.3 为什么技术栈选Spring Boot全家桶这个项目选型时我一开始也纠结过要不要做成前后端分离。后来还是决定用Spring Boot Thymeleaf MyBatis-Plus MySQL的传统单体方案。原因很直接门店规模就那么多不需要独立的前端工程和Nginx集群单体应用部署简单一个jar包加一个数据库就能跑对于学习或毕业设计来说Controller-Service-Mapper三层结构特别清晰写论文也好画架构图。Spring Boot 2.7搭配JDK 8是目前最稳的组合。MyBatis-Plus负责单表CRUD和条件构造器省掉大量XML。Thymeleaf做服务端渲染页面直接用Bootstrap修一下样式整个开发周期可以控制在一周左右。这套组合不是“最潮”的但绝对是“最容易跑通最容易解释”的。2. 整体架构与功能模块一套标准单体应用的正确姿势2.1 技术栈与项目结构项目代码结构比较常规但分层清晰方便二次开发。我的项目结构如下src/main/java/com/xxx/jubensha ├── common // 统一返回结果、全局异常、常量 ├── config // 拦截器、跨域、WebMvc配置 ├── controller // 前端接口层 ├── service // 核心业务逻辑层 ├── mapper // MyBatis-Plus Mapper接口 ├── entity // 数据库实体 └── util // 订单号、日期处理等工具common层我放了一个Result所有接口返回封装成{code, msg, data}三字段结构业务异常统一用BusinessException抛出再由全局异常处理器转成JSON。这样做的好处是前端不管是页面跳转还是AJAX请求处理响应都只用关心一个格式。config层的拦截器做登录鉴权不引入Spring Security——对预约系统来说拦截器已经够用安全框架反而增加学习成本。2.2 前端页面与服务端渲染的分工前端页面用Thymeleaf模板放在src/main/resources/templates下静态资源放在static。页面跳转走Controller返回视图名涉及异步操作提交表单、取消订单就走axios或fetch调接口拿到Result再更新页面。整体交互体验不输给前后端分离。这套做法的好处是有问题好排查F12看到请求发出后端日志一打就知道是哪一层出了错。页面上的数据回显用Thymeleaf表达式例如${script.name}不用单独为接口联调写文档。如果你打算改成Vue或小程序后端接口其实已经具备直接复用/api/**接口即可。2.3 三大端点的功能与API设计下面列一下主要接口后面调试的时候可以直接对照功能请求方式路径说明用户注册POST/api/auth/register用户名、密码、手机号用户登录POST/api/auth/login返回登录状态写入Session剧本列表GET/api/script/list分页、按类型/价格筛选剧本详情GET/api/script/detail含剧本基本信息与场次场次列表GET/api/session/list按剧本、日期查询可约场次提交预约POST/api/order/create创建预约订单我的预约GET/api/order/my当前用户订单列表取消预约POST/api/order/cancel玩家侧取消商家审核POST/api/admin/order/confirm确认或拒绝订单房间维护POST/api/admin/room/save新增或编辑房间这套接口是前后端约定的基准。实现顺序建议先做剧本、房间、场次这些基础模块再做预约单否则你会发现自己连测试数据都没法造。3. 预约业务的核心逻辑场次、排期与并发防超卖3.1 场次模型设计的关键预约的核心不是直接约“剧本”而是约“场次”。一个场次代表某天某个时间点、在某个房间、开某个剧本并且定义了这场能坐几个人。如果没有场次模型就会出现“有人约了下午三点的《某某本》”这种很模糊的描述商家还得自己去推算房间和人数。在表结构上场次表session持有script_id、room_id、start_time、end_time、capacity、booked_count。查询可约场次时条件就是booked_count capacity且start_time now()。因为同一房间同一时间段只能开一场我会在业务层对新增场次做重叠校验查一下该房间是否已存在start_time new_end AND end_time new_start的记录。这个区间重叠判断是排期系统里最常见的公式。3.2 创建预约的完整流程玩家从场次列表进入详情页看到剩余可约人数点击预约后填写联系人和手机号提交到后端。后端处理流程分四步校验场次是否存在且在可预约时间范围内校验当前用户是否已经约过这个场次原子更新booked_count确认没有超出容量插入预约订单返回订单号。核心代码简化后是这样Transactional(rollbackFor Exception.class) public Result createOrder(CreateOrderRequest req) { Session session sessionMapper.selectById(req.getSessionId()); if (session null || session.getStatus() ! 1) { return Result.error(场次不存在或已下架); } if (session.getStartTime().isBefore(LocalDateTime.now())) { return Result.error(该场次已开始无法预约); } Integer count orderMapper.selectCount( new LambdaQueryWrapperAppointmentOrder() .eq(AppointmentOrder::getUserId, req.getUserId()) .eq(AppointmentOrder::getSessionId, req.getSessionId()) .in(AppointmentOrder::getStatus, PAID, CONFIRMED)); if (count 0) { return Result.error(你已经预约过这个场次); } int updated sessionMapper.incrBookedCount(req.getSessionId(), session.getCapacity()); if (updated 0) { return Result.error(手慢了这个场次刚刚被约满); } AppointmentOrder order new AppointmentOrder(); order.setOrderNo(generateOrderNo()); order.setUserId(req.getUserId()); order.setSessionId(req.getSessionId()); order.setContactName(req.getContactName()); order.setContactPhone(req.getContactPhone()); order.setStatus(CONFIRMED); orderMapper.insert(order); return Result.success(order.getOrderNo()); }这里的Transactional保证原子更新和插入订单在同一次事务里要么全成功要么全回滚。这个接口就是整套系统最核心的代码建议仔细读一遍。3.3 并发预约如何避免超卖很多初学者会把预约写成“先查剩余人数再插入订单”我在第一次写的时候也是这么干的。结果用JMeter模拟20个人同时抢一个只剩1个名额的场次出现了超卖。原因很简单两个请求同时查到booked_count7 8然后都去插入订单各自都认为自己是第8个。解决方式我选的是数据库原子更新而不是加Java锁。MyBatis-Plus里写一个自定义更新方法update idincrBookedCount UPDATE session SET booked_count booked_count 1 WHERE id #{id} AND booked_count lt; #{capacity} /update这条SQL在数据库层面用行锁保证同一时刻只有一个事务能成功更新影响行数为1才代表预约成功。在此基础上再给订单表加一个(user_id, session_id)唯一索引彻底防止同一用户重复预约。对单店项目来说这套方案比引入Redis分布式锁简单得多也足够可靠。4. 数据库设计实战核心表与关键索引4.1 六张核心表的关系梳理整个系统的表不多核心就六张。为了让没看过源码的人也能快速上手我把每张表的作用列出来表名作用关键字段user用户表username, password, phone, rolescript剧本表name, type, difficulty, price, min_players, max_playersroom房间表name, capacity, statussession场次表script_id, room_id, start_time, end_time, capacity, booked_countappointment_order预约订单表order_no, user_id, session_id, contact_name, contact_phone, statusevaluation评价表order_id, user_id, score, content关系上一个剧本可以开设多个场次一个房间可以承载多个场次一个场次对应一个剧本和一个房间。预约订单挂在场次下面同时记录预约人。评价表再挂到订单上保证只有真正到场的玩家才能评价。4.2 关键字段与索引设计有几个字段需要特别说清楚。order_no我建议用“时间戳随机数”生成比如202506141530001234并且一定要建唯一索引。订单号一旦重复后面所有对账、客服查询都会出乱子。预约订单表的(user_id, session_id)建唯一索引是防重复预约的底线。就算业务代码里漏了判断数据库也能兜住。场次表的(script_id, start_time)建普通索引因为按剧本查可约场次是最常用的操作。再给start_time建索引后台按时间范围统计会快很多。为什么不建物理外键我在实际项目里基本都是逻辑外键。物理外键在插入、删除时会有额外校验开销而且订单表这种高频写入表很容易和外键锁打架。只要应用层维护好关联关系逻辑外键完全够用。4.3 经营统计SQL是怎么写的商家端要看的报表基本离不开三张表订单、场次、剧本。统计近7天预约量SELECT DATE(create_time) AS d, COUNT(*) AS cnt FROM appointment_order WHERE create_time DATE_SUB(CURDATE(), INTERVAL 6 DAY) GROUP BY DATE(create_time) ORDER BY d;热门剧本Top5SELECT s.script_id, sc.name, COUNT(*) AS cnt FROM appointment_order o JOIN session s ON o.session_id s.id JOIN script sc ON s.script_id sc.id WHERE o.status IN (CONFIRMED,PAID) GROUP BY s.script_id, sc.name ORDER BY cnt DESC LIMIT 5;写这种SQL最需要注意的是别漏掉状态过滤。取消的订单如果也算进销量报表会明显失真。另一个容易被忽略的是日期函数的使用建议测试环境先造一批不同日期的数据验证分组条件和时区是否符合预期。5. 调试部署全记录从本地环境到服务器上线5.1 开发环境准备清单拿到项目后第一件事不是急着打开代码而是先把环境对齐。项目默认的开发环境是JDK 1.8建议用8u202之后的版本可以正常支持Maven编译Maven 3.6.3 或更高版本MySQL 8.05.7也能跑但要留意SQL语法差异IntelliJ IDEA 2021 以上版本社区版也可以Git用来跟踪代码改动数据库初始化脚本放在doc/sql目录下先执行create_database.sql建库再执行init_data.sql导入初始账号和测试数据。本地开发可以直接用root账号连接MySQL不过如果要部署到服务器建议单独建一个数据库账号只授予业务库权限。5.2 配置文件中最容易翻车的三个点我见过太多人在这三个地方卡住。第一是数据库连接地址。application.yml里的spring.datasource.url必须配serverTimezoneAsia/Shanghai和useUnicodetruecharacterEncodingutf8否则会出现中文乱码或者时区报错。8.0的驱动还强制要求driver-class-namecom.mysql.cj.jdbc.Driver。第二是端口冲突。Spring Boot默认8080端口如果本地有别的进程占用启动会直接报端口被占用。要么关掉占用进程要么改server.port比如改成8081。第三是文件上传路径。如果商家要维护剧本封面图项目里通常配置了本地上传目录比如D:/upload/Windows或/usr/local/uploadLinux。不存在的目录会导致上传接口报错部署时记得先创建目录并检查写入权限。5.3 打包部署与常见异常排查部署流程其实很短。进入项目根目录执行mvn clean package -DskipTests构建完成后target目录下会生成jubensha-0.0.1-SNAPSHOT.jar。把jar包、数据库脚本传到服务器先导入数据库再启动nohup java -jar jubensha-0.0.1-SNAPSHOT.jar app.log 21 启动日志正常情况下会有一个Spring Boot Banner最后出现“Started SpringBootApplication”。如果没有出现去app.log看异常。我这里列几个常见问题和处理办法报错现象原因处理方法Access denied for user root...数据库账号密码不对检查application.yml与数据库用户授权Unknown database jubensha数据库没建执行建库脚本Port 8080 was already in use端口被占用换端口或释放占用Failed to configure a DataSource驱动或URL配置缺失检查spring.datasource配置ClassNotFoundException: Lombok编译环境缺Lombok依赖检查pomIDEA装Lombok插件部署到服务器还有一个细节如果用的是云服务器记得在安全组放行项目端口。这不是代码问题但是漏掉的人非常多。6. 测试与实测结果预约接口到底扛不扛得住6.1 功能测试用例设计先别急着看性能功能测试过了再说。我按业务主流程列了一张用例表测试时逐一核对用例操作预期结果用户注册填用户名/密码/手机号注册成功可登录重复用户名注册用同一个用户名再注册提示用户名已存在剧本列表筛选按类型/价格筛选返回符合条件的数据正常预约选一个未满场次下单生成订单号库存1重复预约同一用户再约同一场次提示已预约过约满场次满员后继续下单提示约满取消预约商家/玩家取消状态更新库存-1未登录访问不登录直接调接口拦截器返回未登录我在实际测试中发现最容易出问题的反而是“取消预约”。因为刚开发时只处理了订单状态忘了把booked_count减回去。这个Bug如果不测商家端很快就会发现场次明明有空位却约不进去。6.2 并发压测的实测表现性能测试我用JMeter跑了一个场景一个场次容量8人已经预约7人然后用50个并发线程同时请求预约接口。正常情况应该只有1个请求成功其余全部返回“已约满”。实测结果是成功1个失败49个数据库里没有出现超过容量的记录。这说明原子更新方案在单机数据库下能扛住这个量级的并发。对一个单店预约系统来说这个结果已经够了。如果你非要追求更大的并发再考虑Redis预扣库存但那些都是后话。6.3 我在测试中发现的两个隐性Bug第一个Bug是用户快速双击提交按钮产生了两个重叠请求。由于唯一索引的兜底没有被写入重复订单但是接口返回给用户的提示却是“系统繁忙”。后来我在前端增加了按钮禁用后端也加了一个幂等判断如果订单表中已经存在该用户对应场次的有效订单直接返回“你已经预约过”。双保险之后体验才正常。第二个Bug和状态流转有关。商家拒绝订单时如果订单状态是“待确认”可以直接拒绝但如果是“已确认”拒绝前需要先减回库存。一开始我在两个分支里都写了减库存逻辑结果导致数据错乱。最后我把状态机明确画出来每个状态只允许特定的下一跳才算真正稳定下来。7. 拿到项目之后怎么快速跑通和二次开发7.1 跑起来的五个步骤如果你刚拿到源码不想看文档按下面五步走用IDEA打开Maven项目等待依赖下载完成。创建空数据库执行doc/sql/create_database.sql和doc/sql/init_data.sql。修改application.yml里的数据库密码和上传目录。启动JubenshaApplication浏览器访问http://localhost:8080。使用初始账号登录玩家账号和商家账号在初始化脚本里都有备注。这五步做完你应该能看到系统首页、剧本列表和管理后台。如果哪一步卡住参照第5节的异常排查表。7.2 代码结构与二次开发建议代码结构在前面已经讲过。接下来怎么二次开发我给几个方向想做小程序端直接复用现有/api接口写一套微信小程序前端即可。后端需要补充接口返回的JSON字段比如把每个场次的剩余座位数提前算好。想接在线支付可以对接微信支付或支付宝支付订单增加“待支付”状态配合支付回调更新状态。想做短信提醒在订单确认和临近开场时调用短信服务商接口。需要注意的是任何一次改动都先改数据库脚本再改代码。别在本地库里手工加字段回头部署到服务器就会漏掉。7.3 完整包内容与最后一点经验这个项目整理好后源码、数据库脚本、调试部署说明、开发环境配置、系统界面截图以及一万字以上的设计文档都在同一个包里。需要用到的朋友可以看文末获取方式。最后说一点我自己的体会做预约系统最重要的不是页面多好看而是把“场次、订单、状态”这三者的关系想清楚。状态机画明白了代码怎么写都不会乱。文档和截图都在包里先把整体流程走一遍再动手改代码比上来就东改一个页面、西改一个字段要高效得多。