SpringBoot+Vue台球厅管理系统设计与实现
简介这是一套面向计算机专业本科生的毕业设计级台球厅管理系统专为毕设选题、课程设计及Java全栈实战学习者打造解决台球厅日常运营中用户管理、预约调度、计费结算、设备维护与财务统计等核心业务需求。资源包共810个文件涵盖191个Java后端逻辑代码、134个Vue前端页面组件、159个SVG图标资源、111个JPG运营图片及70个JS交互脚本辅以SQL建库脚本、YML配置、BAT一键部署脚本等完整支撑SpringBootVue前后端分离架构的本地运行与二次开发压缩包大小36.66MB。已有81人下载学习适合零基础起步但需交付完整毕设成果的学生——开题报告、任务书、论文初稿、数据库设计文档与详细运行说明一应俱全且所有模块均经实机调试验证可稳定运行目录结构规范源码注释清晰便于快速理解分层架构与接口对接逻辑。1. 台球厅不是只靠记账和喊号——SpringBootVue 系统真正解决的是「人、台、时、钱」四维协同断点一家日均接待80客人的台球厅前台还在用Excel登记会员手机号、手写台费单、靠对讲机问球房空闲状态教练排班靠微信群接龙库存里的巧粉、球杆套、饮料补货全凭老板拍脑袋财务月底对账要花两天核对三套记录——这不是管理粗放而是典型的信息流割裂客户扫码预约后前端不知道球台实时状态教练确认上场后后端没触发计时器启动消费完成系统却无法自动关联时段、球台、人员、商品四要素生成经营看板。基于 SpringBoot Vue 的台球厅管理系统核心价值不在“把Excel搬上网”而在于用标准化接口打通预约、调度、计费、库存、报表五个关键链路。它适合中小型实体场馆的IT负责人、独立开发者或毕业设计选题者——不需要自研IoT硬件仅靠现有PC扫码枪基础网络就能在2周内跑通从客户下单到老板看数的最小闭环。标题中“设计与实现”四个字指向的是一套可验证、可调试、可扩展的前后端分离落地路径而非概念演示。2. 后端用 SpringBoot 搭建高内聚业务中枢为什么选 MyBatis-Plus 而非 JPA以及如何让球台状态变更原子化2.1 选型依据MyBatis-Plus 对「多状态流转」场景的天然适配性台球厅核心实体球台、会员、订单、教练存在强业务规则约束一张球台不能同时被两个订单占用会员余额不足时禁止下单教练排班需校验当日已排时长是否超限。JPA 的Version乐观锁在高并发抢台场景下易触发重试风暴而 MyBatis-Plus 的UpdateWrapper可直接在 SQL 层嵌入状态校验条件。例如更新球台状态时不依赖 Java 层判断再更新而是用一条带条件的 UPDATE 语句完成原子操作// 更新球台状态仅当当前状态为空闲时才允许设为使用中 LambdaUpdateWrapperBilliardTable wrapper new LambdaUpdateWrapper(); wrapper.eq(BilliardTable::getStatus, TableStatus.FREE) .eq(BilliardTable::getId, tableId) .set(BilliardTable::getStatus, TableStatus.OCCUPIED) .set(BilliardTable::getOccupiedTime, LocalDateTime.now()); int updated tableMapper.update(null, wrapper); if (updated 0) { throw new BusinessException(球台已被占用请刷新后重试); }提示此处updated 0表示 WHERE 条件未匹配到任何记录即球台状态已非空闲。该写法避免了先 SELECT 再 UPDATE 的两步操作彻底规避脏读与并发冲突是台球厅高频操作的刚需保障。2.2 表结构设计直击业务痛点用复合主键与状态机字段替代冗余标记传统设计常为“订单表”添加is_paid,is_used,is_closed等布尔字段导致状态逻辑散落在各处。本系统采用状态机模式以order_status字段统一管控生命周期并通过table_idstart_time组成联合唯一索引强制约束同一球台在同一时段不可重复预约CREATE TABLE billiard_order ( id bigint NOT NULL AUTO_INCREMENT, order_no varchar(32) NOT NULL COMMENT 订单号, member_id bigint NOT NULL COMMENT 会员ID, table_id bigint NOT NULL COMMENT 球台ID, start_time datetime NOT NULL COMMENT 开始时间, end_time datetime NOT NULL COMMENT 结束时间, order_status tinyint NOT NULL DEFAULT 0 COMMENT 0-待支付,1-已支付,2-使用中,3-已完成,4-已取消, total_amount decimal(10,2) NOT NULL COMMENT 应付金额, PRIMARY KEY (id), UNIQUE KEY uk_table_time (table_id,start_time) COMMENT 球台开始时间唯一 ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;2.2.1 状态流转图谱与事务边界定义订单状态变更必须满足业务规则例如仅当order_status 1已支付且当前时间 ≥start_time时才允许调用startUse()接口startUse()必须在一个数据库事务中同步更新订单状态为2、球台状态为OCCUPIED、生成计时任务若计时任务创建失败整个事务回滚确保数据一致性。对应 Service 层代码需显式标注Transactional并捕获特定异常Transactional(rollbackFor Exception.class) public void startUse(Long orderId) { BilliardOrder order orderMapper.selectById(orderId); if (!Objects.equals(order.getOrderStatus(), OrderStatus.PAID.getCode())) { throw new BusinessException(订单未支付无法开始使用); } if (LocalDateTime.now().isBefore(order.getStartTime())) { throw new BusinessException(未到预约开始时间); } // 更新订单状态 order.setOrderStatus(OrderStatus.USED.getCode()); orderMapper.updateById(order); // 更新球台状态复用2.1节原子更新逻辑 updateTableStatusToOccupied(order.getTableId()); // 创建计时任务存入定时任务表由Quartz调度 TimingTask task new TimingTask(); task.setOrderId(orderId); task.setTriggerTime(order.getStartTime()); timingTaskMapper.insert(task); }2.3 配置优化YAML 中隐藏敏感信息与动态切换环境生产环境数据库密码、微信支付密钥等绝不能明文写在application.yml。SpringBoot 2.4 支持spring.config.import引入外部配置结合 Jasypt 加密库实现密文存储# application-prod.yml spring: config: import: optional:configserver:http://config-server:8888/ datasource: url: jdbc:mysql://db-host:3306/billiard?useSSLfalseserverTimezoneAsia/Shanghai username: root password: ENC(8a7b9c1d2e3f4g5h6i7j8k9l0m1n2o3p) # Jasypt加密后密文注意需在pom.xml中引入jasypt-spring-boot-starter并在启动类添加EnableEncryptableProperties注解。密钥通过 JVM 参数传入-Djasypt.encryptor.passwordmySecretKey避免硬编码。3. 前端用 Vue 实现球台可视化调度台WebSocket 实时同步状态与防抖式预约提交3.1 球台状态看板用 Canvas 渲染动态热力图而非静态图片传统方案用 div 拼接球台卡片状态变更需全量重绘 DOM。本系统采用 Canvas 绘制球台布局每个球台为独立绘制区域状态变更仅重绘对应坐标块性能提升显著。核心逻辑封装为TableCanvas组件template canvas refcanvasRef width1200 height600 clickhandleCanvasClick / /template script setup import { ref, onMounted, onUnmounted } from vue import { useTableStore } from /stores/table const canvasRef ref(null) const tableStore useTableStore() onMounted(() { const canvas canvasRef.value const ctx canvas.getContext(2d) // 每秒拉取一次球台状态实际项目建议改用WebSocket const timer setInterval(() { tableStore.fetchTableStatus().then(() { drawTables(ctx, tableStore.tables) }) }, 3000) onUnmounted(() clearInterval(timer)) }) const drawTables (ctx, tables) { ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height) tables.forEach(table { const x table.x * 100 50 // 根据球台坐标计算画布位置 const y table.y * 80 30 ctx.fillStyle getTableColor(table.status) // 根据状态返回颜色 ctx.fillRect(x, y, 80, 60) ctx.fillStyle #fff ctx.font 14px Arial ctx.fillText(台${table.number}, x 10, y 35) }) } const getTableColor (status) { switch(status) { case FREE: return #4CAF50 // 绿色空闲 case OCCUPIED: return #2196F3 // 蓝色使用中 case MAINTAINING: return #FF9800 // 橙色维护中 default: return #9E9E9E } } /script3.2 WebSocket 实现实时状态推送避免轮询带来的服务器压力Canvas 看板若每3秒轮询一次100个在线用户将产生33次/秒的请求。改用 WebSocket 后服务端仅在球台状态变更时主动推送消息// SpringBoot 后端配置 WebSocket Configuration EnableWebSocketMessageBroker public class WebSocketConfig implements WebSocketMessageBrokerConfigurer { Override public void configureMessageBroker(MessageBrokerRegistry config) { config.enableSimpleBroker(/topic); // 订阅地址前缀 config.setApplicationDestinationPrefixes(/app); // 发送地址前缀 } }// Vue 前端建立连接并监听 import { onMounted, onUnmounted } from vue import SockJS from sockjs-client import Stomp from stomp/stompjs let stompClient null onMounted(() { const socket new SockJS(/ws) stompClient Stomp.over(socket) stompClient.connect({}, () { stompClient.subscribe(/topic/table-status, (message) { const data JSON.parse(message.body) // 触发 Pinia store 更新 useTableStore().updateTableStatus(data.tableId, data.status) }) }) }) onUnmounted(() { if (stompClient stompClient.connected) { stompClient.disconnect() } })3.2.1 预约提交的防抖与幂等控制用户频繁点击“预约”按钮易造成重复提交。前端用 Lodash 的debounce限制 1 秒内仅发送一次请求后端则通过 Redis 分布式锁保证幂等// Controller 层 PostMapping(/reserve) public Result reserve(RequestBody ReserveRequest request) { String lockKey reserve: request.getMemberId() : request.getTableId() : request.getStartTime(); Boolean locked redisTemplate.opsForValue() .setIfAbsent(lockKey, 1, Duration.ofSeconds(30)); if (!locked) { throw new BusinessException(操作过于频繁请稍后再试); } try { return reserveService.doReserve(request); } finally { redisTemplate.delete(lockKey); // 释放锁 } }4. 前后端联调关键路径从登录鉴权到经营报表生成的 5 个必验节点4.1 登录流程Vue 路由守卫拦截未授权访问SpringBoot JWT 校验双保险用户登录成功后后端返回 JWT Token前端将其存入localStorage并在 Axios 请求头中携带// utils/request.js service.interceptors.request.use(config { const token localStorage.getItem(token) if (token) { config.headers.Authorization Bearer ${token} } return config })SpringBoot 端通过OncePerRequestFilter解析 Token 并注入SecurityContextpublic class JwtAuthenticationFilter extends OncePerRequestFilter { Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { String token resolveToken(request); if (token ! null jwtUtil.validateToken(token)) { Long userId jwtUtil.getUserIdFromToken(token); UserDetails userDetails userDetailsService.loadUserById(userId); UsernamePasswordAuthenticationToken authentication new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities()); SecurityContextHolder.getContext().setAuthentication(authentication); } filterChain.doFilter(request, response); } }提示JWT 的exp字段应设为 2 小时配合前端定时刷新机制如在 Token 过期前10分钟自动调用/refresh接口获取新 Token避免用户操作中突然登出。4.2 预约下单链路Vue 表单校验 SpringBoot 参数分组校验预约表单需校验多项业务规则开始时间不能早于当前时间、时长必须为30分钟整数倍、会员余额是否充足。前端用 Element Plus 的rules进行实时提示后端用Validated分组校验确保穿透性// DTO 定义分组接口 public interface ReserveGroup {} // 预约请求DTO public class ReserveRequest { NotNull(groups ReserveGroup.class) private Long tableId; FutureOrPresent(groups ReserveGroup.class) // JSR-380 标准注解 private LocalDateTime startTime; Min(value 30, groups ReserveGroup.class) Max(value 480, groups ReserveGroup.class) // 最长8小时 private Integer durationMinutes; // 单位分钟 } // Controller 方法指定校验分组 PostMapping(/reserve) public Result reserve(Validated(ReserveGroup.class) RequestBody ReserveRequest request) { return reserveService.doReserve(request); }4.3 经营报表导出Vue 前端触发SpringBoot 流式生成 Excel 避免内存溢出日经营报表可能包含上千条订单记录若一次性查库转 Excel 易触发 OOM。采用分页流式导出前端传入日期范围后端用Poi-tl模板引擎 SXSSFWorkbook边查边写GetMapping(/export-daily-report) public void exportDailyReport(RequestParam LocalDate date, HttpServletResponse response) throws IOException { response.setContentType(application/vnd.openxmlformats-officedocument.spreadsheetml.sheet); response.setHeader(Content-Disposition, attachment; filenamedaily-report- date .xlsx); SXSSFWorkbook workbook new SXSSFWorkbook(1000); // 每1000行刷盘 Sheet sheet workbook.createSheet(日经营报表); // 写入表头 Row headerRow sheet.createRow(0); String[] headers {订单号, 球台号, 会员姓名, 开始时间, 结束时间, 时长(分钟), 金额}; for (int i 0; i headers.length; i) { headerRow.createCell(i).setCellValue(headers[i]); } // 分页查询并写入数据伪代码实际需用 PageHelper 或 MyBatis-Plus 分页 int pageNum 1; int pageSize 500; while (true) { PageReportItem page reportService.getPageByDate(date, pageNum, pageSize); if (page.getRecords().isEmpty()) break; for (int i 0; i page.getRecords().size(); i) { ReportItem item page.getRecords().get(i); Row row sheet.createRow((pageNum - 1) * pageSize i 1); row.createCell(0).setCellValue(item.getOrderNo()); row.createCell(1).setCellValue(item.getTableNumber()); row.createCell(2).setCellValue(item.getMemberName()); row.createCell(3).setCellValue(item.getStartTime().toString()); row.createCell(4).setCellValue(item.getEndTime().toString()); row.createCell(5).setCellValue(item.getDurationMinutes()); row.createCell(6).setCellValue(item.getAmount().doubleValue()); } pageNum; } workbook.write(response.getOutputStream()); workbook.close(); }4.4 库存预警Vue 使用 El-Alert 动态提示SpringBoot 定时任务扫描阈值球台配件巧粉、球杆套库存低于10件时需前台醒目提示。后端每日凌晨2点执行扫描任务将预警商品 ID 写入 Redis 的inventory:alertSetScheduled(cron 0 0 2 * * ?) // 每天2点执行 public void checkInventoryAlert() { ListLong lowStockIds inventoryMapper.selectLowStockIds(10); if (!lowStockIds.isEmpty()) { redisTemplate.opsForSet().add(inventory:alert, lowStockIds.stream().map(String::valueOf).toArray(String[]::new)); } }Vue 前端在首页onMounted时订阅该 Key利用redis-pubsub库监听变化// 首页组件 onMounted(() { const pubsub new RedisPubSub({ host: ws://your-redis-ws-proxy, channel: inventory:alert }) pubsub.on(message, (msg) { ElMessage.warning(库存告警ID为${msg}的商品已低于安全库存) }) })5. 生产部署避坑指南Nginx 配置跨域与静态资源缓存以及 SpringBoot Actuator 监控关键指标5.1 Nginx 反向代理配置精准控制 API 与静态资源策略Vue 打包后的dist目录需由 Nginx 托管但必须区分/api/前缀的请求代理至 SpringBoot与静态资源启用强缓存。错误配置会导致跨域或资源加载失败upstream backend { server 127.0.0.1:8080; } server { listen 80; server_name billiard.example.com; # 静态资源js/css/img 启用30天缓存HTML 不缓存 location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg)$ { expires 30d; add_header Cache-Control public, immutable; } location / { root /var/www/billiard/dist; try_files $uri $uri/ /index.html; } # API 请求代理至后端移除 /api 前缀 location /api/ { proxy_pass http://backend/; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; # 关键解决 WebSocket 连接升级问题 proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection upgrade; } }提示Vue Router 若使用history模式try_files $uri $uri/ /index.html是必需的否则刷新页面会返回 404。5.2 SpringBoot Actuator 暴露关键健康端点监控球台状态一致性默认 Actuator 仅暴露health和info需手动开启prometheus和metrics端点用于排查球台状态错乱问题# application.yml management: endpoints: web: exposure: include: health,info,metrics,prometheus,threaddump endpoint: health: show-details: always访问http://localhost:8080/actuator/metrics/jvm.memory.used可查看内存使用而http://localhost:8080/actuator/health返回 JSON 包含自定义检查项Component public class TableStatusHealthIndicator implements HealthIndicator { Autowired private BilliardTableMapper tableMapper; Override public Health health() { long inconsistentCount tableMapper.selectCount( new QueryWrapperBilliardTable().ne(status, FREE).ne(status, OCCUPIED).ne(status, MAINTAINING) ); if (inconsistentCount 0) { return Health.down() .withDetail(inconsistent_tables, inconsistentCount) .build(); } return Health.up().build(); } }5.3 数据库连接池参数调优HikariCP 防止球台高并发预约超时台球厅高峰时段19:00-22:00并发预约请求激增HikariCP 默认配置易导致连接等待超时。根据实测调整以下参数参数名建议值说明maximum-pool-size20按每台球台平均2个并发连接估算预约状态更新connection-timeout3000连接获取超时设为3秒避免前端长时间白屏idle-timeout600000空闲连接10分钟回收防止连接泄漏max-lifetime1800000连接最长存活30分钟规避数据库连接老化spring: datasource: hikari: maximum-pool-size: 20 connection-timeout: 3000 idle-timeout: 600000 max-lifetime: 1800000当HikariPool-1 - Timeout failure日志频繁出现时优先检查该配置而非盲目增加maximum-pool-size因过大的连接池会加剧数据库负载。本文还有配套的精品资源点击获取