news 2026/10/26 17:39:42

基于 Spring Boot + Vue3 的剧本娱乐门店预约平台设计与实现

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
基于 Spring Boot + Vue3 的剧本娱乐门店预约平台设计与实现

温馨提示:本人主页置顶文章(点我)开头有 CSDN 平台官方提供的学长联系名片!

一、项目背景与意义

剧本杀、密室逃脱等剧本娱乐业态近年来快速扩张,门店数量增多、场次排布复杂、玩家组队需求频繁,传统依靠微信群接龙、电话预约、纸质排期表的管理方式已经难以支撑。门店普遍面临排期冲突、场次空置、玩家爽约、订单对账困难等问题。

本文设计并实现一个基于 Spring Boot + Vue3 的剧本娱乐门店预约平台,面向玩家、门店店长和平台运营三类角色,提供剧本浏览、场次排期、在线预约、组队拼场、订单支付与核销、评价互动等能力,帮助门店提升场次利用率,降低人工排期成本。

二、需求分析

2.1 角色与权限

角色核心诉求主要功能
玩家快速找到合适的剧本和场次剧本浏览、场次查询、在线预约、拼场组队、订单管理、评价
门店店长管理门店资源与排期门店信息维护、剧本上架、房间管理、场次排期、订单核销、数据统计
平台运营审核与全局运营门店审核、剧本审核、用户管理、投诉处理、运营数据看板

2.2 核心业务流程

flowchart TD A[玩家浏览剧本] --> B[选择门店与场次] B --> C{是否拼场} C -- 是 --> D[加入拼场队伍] C -- 否 --> E[整场预约] D --> F[提交订单并支付] E --> F F --> G[门店确认排期] G --> H[到店核销] H --> I[评价与积分]

2.3 非功能性需求

  • 并发控制:同一场次座位有限,需防止超卖,采用数据库乐观锁或 Redis 分布式锁。
  • 响应性能:剧本列表、场次查询等高频接口引入 Redis 缓存,热点数据响应控制在 200ms 内。
  • 安全性:基于 JWT 的无状态鉴权,接口按角色做细粒度权限校验。
  • 可扩展性:前后端分离,模块化分层,便于后续接入小程序端。

三、系统架构设计

3.1 总体架构

系统采用前后端分离架构。前端使用 Vue3 + Vite + Element Plus 构建管理端与用户端,后端使用 Spring Boot 提供 RESTful 接口,数据层采用 MySQL 存储业务数据、Redis 缓存热点数据与分布式锁、MinIO 存储剧本封面与门店图片。

flowchart LR U[玩家端 Vue3] --> G[Nginx 网关] M[门店管理端 Vue3] --> G G --> S[Spring Boot 应用] S --> R[(Redis)] S --> D[(MySQL)] S --> O[(MinIO 对象存储)] S --> P[支付/短信第三方服务]

3.2 技术选型

层次技术栈说明
前端框架Vue3 + Vite + TypeScript组合式 API,按需加载
UI 组件Element Plus管理端与用户端统一组件库
状态管理Pinia用户信息、购物车式预约单管理
后端框架Spring Boot 3.x自动装配、内嵌 Tomcat
持久层MyBatis-Plus简化 CRUD,支持分页插件
数据库MySQL 8.0业务主库
缓存Redis 7热点缓存、分布式锁、验证码
鉴权Spring Security + JWT无状态认证
对象存储MinIO图片与剧本资料存储

四、数据库设计

核心表围绕门店、剧本、房间、场次、订单、拼场队伍展开,主要表结构如下。

表名说明关键字段
sys_user用户表id、username、password、phone、role、status
store门店表id、name、address、phone、business_hours、status
script_info剧本表id、title、type、player_count、duration、cover_url、difficulty
room房间表id、store_id、name、capacity
session_schedule场次表id、store_id、room_id、script_id、start_time、end_time、price、total_seats、booked_seats、status
booking_order预约订单表id、order_no、user_id、session_id、seats、amount、pay_status、verify_code
team_room拼场队伍表id、session_id、leader_id、current_size、max_size、status
comment评价表id、order_id、user_id、script_id、score、content

其中session_schedule.booked_seats是并发敏感字段,预约时通过带条件的更新语句保证不超卖:

UPDATE session_schedule SET booked_seats = booked_seats + #{seats} WHERE id = #{sessionId} AND booked_seats + #{seats} <= total_seats AND status = 1;

当影响行数为 0 时说明座位不足,直接返回预约失败,避免加锁带来的性能损耗。

五、后端核心实现

5.1 项目结构

script-booking ├── script-common 公共模块:统一响应、异常、工具类 ├── script-security 鉴权模块:JWT、权限校验 ├── script-admin 平台运营端接口 ├── script-store 门店端接口 ├── script-portal 玩家端接口 └── script-job 定时任务:场次状态刷新、超时订单关闭

5.2 统一响应与全局异常

@Data public class Result<T> { private Integer code; private String message; private T data; public static &lt;T&gt; Result&lt;T&gt; success(T data) { Result&lt;T&gt; r = new Result&lt;&gt;(); r.setCode(200); r.setMessage("success"); r.setData(data); return r; } public static &lt;T&gt; Result&lt;T&gt; fail(String message) { Result&lt;T&gt; r = new Result&lt;&gt;(); r.setCode(500); r.setMessage(message); return r; } }
@RestControllerAdvice public class GlobalExceptionHandler { @ExceptionHandler(BusinessException.class) public Result&lt;Void&gt; handleBusiness(BusinessException e) { return Result.fail(e.getMessage()); } @ExceptionHandler(Exception.class) public Result&lt;Void&gt; handleException(Exception e) { log.error("系统异常", e); return Result.fail("系统繁忙,请稍后重试"); } }

5.3 JWT 鉴权

@Component public class JwtTokenProvider { @Value("${jwt.secret}") private String secret; @Value("${jwt.expire}") private long expire; public String createToken(Long userId, String role) { Date now = new Date(); return Jwts.builder() .setSubject(String.valueOf(userId)) .claim("role", role) .setIssuedAt(now) .setExpiration(new Date(now.getTime() + expire)) .signWith(Keys.hmacShaKeyFor(secret.getBytes(StandardCharsets.UTF_8))) .compact(); } public Claims parse(String token) { return Jwts.parserBuilder() .setSigningKey(Keys.hmacShaKeyFor(secret.getBytes(StandardCharsets.UTF_8))) .build() .parseClaimsJws(token) .getBody(); } }

5.4 预约下单服务

@Service @RequiredArgsConstructor public class BookingService { private final SessionScheduleMapper scheduleMapper; private final BookingOrderMapper orderMapper; private final RedisTemplate&lt;String, Object&gt; redisTemplate; @Transactional(rollbackFor = Exception.class) public String book(Long userId, Long sessionId, Integer seats) { String lockKey = "lock:session:" + sessionId; Boolean locked = redisTemplate.opsForValue() .setIfAbsent(lockKey, userId, Duration.ofSeconds(10)); if (Boolean.FALSE.equals(locked)) { throw new BusinessException("当前场次预约火爆,请稍后重试"); } try { int updated = scheduleMapper.increaseBookedSeats(sessionId, seats); if (updated == 0) { throw new BusinessException("剩余座位不足"); } SessionSchedule schedule = scheduleMapper.selectById(sessionId); BookingOrder order = new BookingOrder(); order.setOrderNo(OrderNoGenerator.next()); order.setUserId(userId); order.setSessionId(sessionId); order.setSeats(seats); order.setAmount(schedule.getPrice().multiply(BigDecimal.valueOf(seats))); order.setPayStatus(0); order.setVerifyCode(RandomUtil.randomNumbers(6)); orderMapper.insert(order); return order.getOrderNo(); } finally { redisTemplate.delete(lockKey); } } }

5.5 拼场组队

拼场是剧本娱乐场景的特色功能。玩家可加入未满员的队伍,队伍满员后自动转为待开场状态。加入队伍时同样需要校验人数上限,使用与预约一致的乐观更新思路:

UPDATE team_room SET current_size = current_size + 1 WHERE id = #{teamId} AND current_size < max_size AND status = 0;

六、前端核心实现

6.1 请求封装

import axios from 'axios' import { ElMessage } from 'element-plus' import { useUserStore } from '@/stores/user' const request = axios.create({ baseURL: import.meta.env.VITE_API_BASE, timeout: 10000 }) request.interceptors.request.use((config) => { const userStore = useUserStore() if (userStore.token) { config.headers.Authorization = Bearer ${userStore.token} } return config }) request.interceptors.response.use( (res) => { const { code, message, data } = res.data if (code === 200) return data ElMessage.error(message) return Promise.reject(new Error(message)) }, (error) => { if (error.response?.status === 401) { useUserStore().logout() } return Promise.reject(error) } ) export default request

6.2 场次列表与预约

<template> <div class="session-list"> <el-card v-for="item in sessions" :key="item.id" class="session-card"> <div class="title">{{ item.scriptTitle }}</div> <div class="meta"> <span>{{ item.startTime }} - {{ item.endTime }}</span> <span>剩余 {{ item.totalSeats - item.bookedSeats }} 位</span> </div> <div class="footer"> <span class="price">¥{{ item.price }}</span> <el-button type="primary" :disabled="item.bookedSeats >= item.totalSeats" @click="handleBook(item)">立即预约</el-button> </div> </el-card> </div> </template> <script setup lang="ts"> import { ref, onMounted } from 'vue' import { getSessionList, createBooking } from '@/api/booking' const sessions = ref<any[]>([]) const loadSessions = async () => { sessions.value = await getSessionList({ status: 1 }) } const handleBook = async (item: any) => { await createBooking({ sessionId: item.id, seats: 1 }) ElMessage.success('预约成功') loadSessions() } onMounted(loadSessions) </script>

6.3 路由与权限控制

const routes = [ { path: '/', component: () => import('@/views/home/index.vue') }, { path: '/script/:id', component: () => import('@/views/script/detail.vue') }, { path: '/store', component: () => import('@/layout/StoreLayout.vue'), meta: { roles: ['STORE'] }, children: [ { path: 'schedule', component: () => import('@/views/store/schedule.vue') }, { path: 'order', component: () => import('@/views/store/order.vue') } ] } ] router.beforeEach((to) => { const userStore = useUserStore() const roles = to.meta.roles as string[] | undefined if (roles && !roles.includes(userStore.role)) { return { path: '/403' } } })

七、关键难点与解决方案

7.1 场次超卖

采用「Redis 分布式锁 + 数据库条件更新」双重保障。锁用于削峰,条件更新作为最终一致性兜底,即使锁失效也不会出现超卖。

7.2 场次状态自动流转

使用 Spring Task 定时扫描场次表,将开始时间已过的场次置为进行中,结束时间已过的置为已完成,并释放未支付订单占用的座位。

@Scheduled(cron = "0 */5 * * * ?") public void refreshSessionStatus() { scheduleMapper.finishExpiredSessions(); orderMapper.closeTimeoutOrders(LocalDateTime.now().minusMinutes(15)); }

7.3 拼场与整场预约的座位一致性

拼场队伍人数与场次已订座位数需要保持同步。方案是在同一事务内先更新队伍人数,再更新场次座位,任一失败整体回滚,避免出现「队伍满员但场次仍有余位」的脏数据。

八、系统测试与部署

8.1 测试要点

  • 使用 JMeter 对预约接口做 500 并发压测,验证无超卖且响应时间稳定。
  • 对 JWT 过期、越权访问、重复提交等场景编写接口测试用例。
  • 前端使用 Vitest 对核心工具函数与状态管理做单元测试。

8.2 部署方案

version: '3.8' services: mysql: image: mysql:8.0 environment: MYSQL_ROOT_PASSWORD: root123 MYSQL_DATABASE: script_booking ports: - "3306:3306" redis: image: redis:7 ports: - "6379:6379" minio: image: minio/minio command: server /data --console-address ":9001" ports: - "9000:9000" - "9001:9001" app: build: . depends_on: - mysql - redis ports: - "8080:8080"

前端通过npm run build产出静态资源,交由 Nginx 托管并反向代理后端接口,实现前后端同域部署,简化跨域配置。

九、总结与展望

本文完成了剧本娱乐门店预约平台的需求分析、架构设计、数据库设计与核心功能实现,重点解决了场次超卖、拼场座位一致性和状态自动流转三个关键问题。后续可从以下方向继续演进:

  • 接入微信小程序端,降低玩家使用门槛。
  • 引入推荐算法,根据玩家历史偏好推荐剧本与场次。
  • 增加门店经营看板,提供上座率、复购率等多维分析。
  • 引入消息队列处理支付回调与短信通知,提升系统解耦程度。
版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/26 17:37:59

Luma AI:手机秒扫生成可商用3D模型的原理与落地

1. Luma AI 不是“又一个AI绘图工具”&#xff0c;而是三维内容生产范式的切换开关 最近在几个创意工作群和3D建模社区里&#xff0c;频繁看到同事发来一段旋转的、带真实材质反光的咖啡杯模型&#xff0c;配文&#xff1a;“刚用Luma拍的&#xff0c;没建模没贴图&#xff0c…

作者头像 李华
网站建设 2026/10/26 17:37:01

C语言类型系统与隐式转换的实战解析

1. 为什么C语言的类型系统如此重要&#xff1f; 在嵌入式开发领域&#xff0c;我曾遇到一个真实案例&#xff1a;某工业设备在-1℃环境下出现异常重启。经过三天排查&#xff0c;最终发现是温度传感器的uint8_t类型数据在比较运算时发生了隐式整型提升&#xff0c;导致比较逻辑…

作者头像 李华
网站建设 2026/10/26 17:36:56

C#与.NET API开发实战:从入门到部署

1. C#与.NET API编程入门指南第一次接触C#和.NET API开发时&#xff0c;我被微软这套生态系统的完整性震撼到了。作为从Java转过来的开发者&#xff0c;我发现C#在语法糖和开发效率上确实有不少惊喜。记得最早用ASP.NET Web API做项目时&#xff0c;自动生成的Swagger文档让我省…

作者头像 李华
网站建设 2026/10/26 17:35:35

微信小程序封装H5域名实战:web-view业务域名配置与避坑指南

简介&#xff1a;这份资源面向微信小程序开发者与H5站长&#xff0c;提供了一套将已有H5域名快速封装为小程序的完整工程模板。内容围绕WXML、WXSS、JS重构展开&#xff0c;涵盖页面结构转换、静态资源路径调整、页面路由配置、合法域名设置与真机调试等关键环节&#xff0c;可…

作者头像 李华
网站建设 2026/10/26 17:35:35

2025高性能Web架构:核心优化技术与实践指南

1. 项目概述&#xff1a;2025高性能Web架构的批判性解构 "Optimizing the Digital Horizon"这个标题背后&#xff0c;隐藏着对下一代Web性能优化技术的深度解构。作为经历过三次技术迭代周期的老手&#xff0c;我见过太多关于"高性能"的过度承诺。2025年的…

作者头像 李华