基于 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、statusstore门店表id、name、address、phone、business_hours、statusscript_info剧本表id、title、type、player_count、duration、cover_url、difficultyroom房间表id、store_id、name、capacitysession_schedule场次表id、store_id、room_id、script_id、start_time、end_time、price、total_seats、booked_seats、statusbooking_order预约订单表id、order_no、user_id、session_id、seats、amount、pay_status、verify_codeteam_room拼场队伍表id、session_id、leader_id、current_size、max_size、statuscomment评价表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 ResultT { private Integer code; private String message; private T data; public static lt;Tgt; Resultlt;Tgt; success(T data) { Resultlt;Tgt; r new Resultlt;gt;(); r.setCode(200); r.setMessage(success); r.setData(data); return r; } public static lt;Tgt; Resultlt;Tgt; fail(String message) { Resultlt;Tgt; r new Resultlt;gt;(); r.setCode(500); r.setMessage(message); return r; } }RestControllerAdvice public class GlobalExceptionHandler { ExceptionHandler(BusinessException.class) public Resultlt;Voidgt; handleBusiness(BusinessException e) { return Result.fail(e.getMessage()); } ExceptionHandler(Exception.class) public Resultlt;Voidgt; 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 RedisTemplatelt;String, Objectgt; 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 request6.2 场次列表与预约template div classsession-list el-card v-foritem in sessions :keyitem.id classsession-card div classtitle{{ item.scriptTitle }}/div div classmeta span{{ item.startTime }} - {{ item.endTime }}/span span剩余 {{ item.totalSeats - item.bookedSeats }} 位/span /div div classfooter span classprice{{ item.price }}/span el-button typeprimary :disableditem.bookedSeats item.totalSeats clickhandleBook(item)立即预约/el-button /div /el-card /div /template script setup langts import { ref, onMounted } from vue import { getSessionList, createBooking } from /api/booking const sessions refany[]([]) 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) /script6.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 托管并反向代理后端接口实现前后端同域部署简化跨域配置。九、总结与展望本文完成了剧本娱乐门店预约平台的需求分析、架构设计、数据库设计与核心功能实现重点解决了场次超卖、拼场座位一致性和状态自动流转三个关键问题。后续可从以下方向继续演进接入微信小程序端降低玩家使用门槛。引入推荐算法根据玩家历史偏好推荐剧本与场次。增加门店经营看板提供上座率、复购率等多维分析。引入消息队列处理支付回调与短信通知提升系统解耦程度。