ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

基于 Spring Boot + Vue3 的体育场馆在线预约系统设计与实现

基于 Spring Boot + Vue3 的体育场馆在线预约系统设计与实现

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

一、项目背景与意义

随着全民健身意识的提升,羽毛球、篮球、网球、游泳等体育场馆的使用需求快速增长。传统场馆预约多依赖电话登记或现场排队,存在信息不透明、时段冲突、人工对账繁琐等问题。基于 Spring Boot + Vue3 构建一套体育场馆在线预约系统,可以让用户随时查看场馆空闲时段、在线下单支付、扫码入场,同时帮助场馆管理者实现场地排期、订单统计和运营分析,具有较高的实用价值。

本文将从需求分析、技术选型、数据库设计、核心功能实现到部署上线,完整介绍该系统的设计与实现过程,适合作为 Java 全栈方向的课程设计或毕业设计参考。

二、系统需求分析

2.1 角色划分

  • 普通用户:注册登录、浏览场馆、查看场地时段、在线预约、支付订单、取消预约、查看历史订单、评价场馆。
  • 场馆管理员:维护场馆与场地信息、设置开放时段与价格、审核预约、核销入场、查看经营数据。
  • 系统管理员:用户管理、角色权限管理、系统参数配置、日志审计。

2.2 核心业务流程

用户选择场馆与日期后,系统按场地维度展示可预约时段;用户选定时段提交预约,系统通过数据库唯一约束与分布式锁防止同一时段被重复占用;预约成功后生成订单并进入支付流程,支付回调完成后订单状态变为「已支付」;用户到场后由管理员核销,订单状态流转为「已完成」。

2.3 非功能性需求

  • 并发预约场景下保证时段不超卖。
  • 接口响应时间控制在 300ms 以内,列表查询支持分页。
  • 前后端分离,接口统一使用 JSON 交互,并具备完善的鉴权机制。

三、技术选型

层次技术栈说明
后端框架Spring Boot 3.x快速构建 RESTful 服务,内置 Tomcat
持久层MyBatis-Plus简化 CRUD,支持分页与条件构造器
数据库MySQL 8.0存储用户、场馆、场地、订单等数据
缓存Redis缓存时段库存、分布式锁、登录令牌
鉴权Spring Security + JWT无状态认证与接口权限控制
前端框架Vue3 + Vite组合式 API,构建速度快
UI 组件库Element Plus后台管理界面快速搭建
状态管理Pinia管理用户信息与全局状态
网络请求Axios统一封装请求拦截与错误处理

四、系统架构设计

系统采用前后端分离架构,整体分为表现层、接口层、业务层、数据层四部分。前端 Vue3 应用通过 Axios 调用后端 RESTful 接口,后端按模块拆分为用户模块、场馆模块、预约模块、订单模块与统计模块。

flowchart TD A[Vue3 前端应用] -->|HTTP/JSON| B[Spring Boot 接口层] B --> C[Spring Security + JWT 鉴权] C --> D[业务服务层] D --> E[MyBatis-Plus 持久层] E --> F[(MySQL)] D --> G[(Redis 缓存与分布式锁)] D --> H[支付回调接口]

五、数据库设计

核心表包括用户表、场馆表、场地表、时段表、预约订单表与支付记录表。以下给出关键表结构。

CREATE TABLE venue ( id BIGINT PRIMARY KEY AUTO_INCREMENT, name VARCHAR(64) NOT NULL COMMENT '场馆名称', address VARCHAR(255) NOT NULL COMMENT '场馆地址', open_time TIME NOT NULL COMMENT '开放开始时间', close_time TIME NOT NULL COMMENT '开放结束时间', status TINYINT DEFAULT 1 COMMENT '1营业 0停业', create_time DATETIME DEFAULT CURRENT_TIMESTAMP ); CREATE TABLE court ( id BIGINT PRIMARY KEY AUTO_INCREMENT, venue_id BIGINT NOT NULL COMMENT '所属场馆', name VARCHAR(64) NOT NULL COMMENT '场地名称', type VARCHAR(32) NOT NULL COMMENT '场地类型', price DECIMAL(10,2) NOT NULL COMMENT '每小时价格', status TINYINT DEFAULT 1 ); CREATE TABLE booking_order ( id BIGINT PRIMARY KEY AUTO_INCREMENT, order_no VARCHAR(32) NOT NULL UNIQUE COMMENT '订单号', user_id BIGINT NOT NULL, court_id BIGINT NOT NULL, booking_date DATE NOT NULL, start_time TIME NOT NULL, end_time TIME NOT NULL, amount DECIMAL(10,2) NOT NULL, status TINYINT DEFAULT 0 COMMENT '0待支付 1已支付 2已完成 3已取消', create_time DATETIME DEFAULT CURRENT_TIMESTAMP, UNIQUE KEY uk_court_slot (court_id, booking_date, start_time) );

其中uk_court_slot唯一索引是防止同一场地同一时段被重复预约的最后一道防线,配合 Redis 分布式锁可在高并发下保证数据一致性。

六、后端核心功能实现

6.1 项目结构

sports-booking ├── sports-common 公共模块:统一响应、异常、工具类 ├── sports-security 鉴权模块:JWT、Security 配置 ├── sports-admin 管理端接口 ├── sports-api 用户端接口 └── sports-application 启动模块

6.2 统一响应与全局异常处理

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

6.3 预约核心逻辑

预约接口需要完成时段校验、加锁、库存判断、订单落库四个步骤。下面使用 Redis 的SETNX实现简易分布式锁。

@Service @RequiredArgsConstructor public class BookingService { private final StringRedisTemplate redisTemplate; private final BookingOrderMapper orderMapper; private final CourtMapper courtMapper; public String book(Long userId, BookingDTO dto) { String lockKey = "lock:court:" + dto.getCourtId() + ":" + dto.getBookingDate() + ":" + dto.getStartTime(); Boolean locked = redisTemplate.opsForValue() .setIfAbsent(lockKey, "1", Duration.ofSeconds(10)); if (Boolean.FALSE.equals(locked)) { throw new BusinessException(400, "该时段正在被预约,请稍后重试"); } try { Court court = courtMapper.selectById(dto.getCourtId()); if (court == null || court.getStatus() == 0) { throw new BusinessException(400, "场地不可用"); } Long count = orderMapper.selectCount(new LambdaQueryWrapper<BookingOrder>() .eq(BookingOrder::getCourtId, dto.getCourtId()) .eq(BookingOrder::getBookingDate, dto.getBookingDate()) .eq(BookingOrder::getStartTime, dto.getStartTime()) .in(BookingOrder::getStatus, 0, 1, 2)); if (count > 0) { throw new BusinessException(400, "该时段已被预约"); } BookingOrder order = new BookingOrder(); order.setOrderNo(IdUtil.getSnowflakeNextIdStr()); order.setUserId(userId); order.setCourtId(dto.getCourtId()); order.setBookingDate(dto.getBookingDate()); order.setStartTime(dto.getStartTime()); order.setEndTime(dto.getEndTime()); order.setAmount(court.getPrice()); order.setStatus(0); orderMapper.insert(order); return order.getOrderNo(); } finally { redisTemplate.delete(lockKey); } } }

6.4 JWT 鉴权

@Component public class JwtUtil { private static final String SECRET = "sports-booking-secret-key"; private static final long EXPIRE = 7 * 24 * 3600 * 1000L; public String generate(Long userId, String username) { return Jwts.builder() .setSubject(String.valueOf(userId)) .claim("username", username) .setExpiration(new Date(System.currentTimeMillis() + EXPIRE)) .signWith(SignatureAlgorithm.HS256, SECRET) .compact(); } public Claims parse(String token) { return Jwts.parser().setSigningKey(SECRET).parseClaimsJws(token).getBody(); } }

七、前端核心功能实现

7.1 请求封装

import axios from 'axios' import { ElMessage } from 'element-plus' import { useUserStore } from '@/stores/user' const request = axios.create({ baseURL: '/api', 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)) }, err => { ElMessage.error(err.message || '网络异常') return Promise.reject(err) } ) export default request

7.2 场地时段选择组件

<template> <div class="slot-grid"> <div v-for="slot in slots" :key="slot.startTime" :class="['slot', { disabled: slot.booked, active: selected === slot.startTime }]" @click="choose(slot)" > {{ slot.startTime }} - {{ slot.endTime }} </div> </div> </template> <script setup> import { ref, onMounted } from 'vue' import request from '@/utils/request' const props = defineProps({ courtId: Number, date: String }) const emit = defineEmits(['select']) const slots = ref([]) const selected = ref('') const loadSlots = async () => { slots.value = await request.get('/court/slots', { params: { courtId: props.courtId, date: props.date } }) } const choose = slot => { if (slot.booked) return selected.value = slot.startTime emit('select', slot) } onMounted(loadSlots) </script> <style scoped> .slot-grid { display: grid; grid-template-columns: repeat(4, 1fr); gap: 12px; } .slot { padding: 10px; border: 1px solid #dcdfe6; border-radius: 6px; text-align: center; cursor: pointer; } .slot.disabled { background: #f5f7fa; color: #c0c4cc; cursor: not-allowed; } .slot.active { border-color: #409eff; color: #409eff; } </style>

八、系统部署与运行

  1. 准备 MySQL 8.0 与 Redis 环境,执行初始化 SQL 脚本。
  2. 修改后端application.yml中的数据库与 Redis 连接配置。
  3. 执行mvn clean package打包后端,使用java -jar启动。
  4. 前端执行npm install与npm run build,将产物部署到 Nginx。
  5. Nginx 配置/api反向代理到后端服务,解决跨域问题。

九、总结与展望

本文基于 Spring Boot 3 + Vue3 实现了一套体育场馆在线预约系统,覆盖了用户预约、订单支付、场地管理与数据统计等核心功能,并通过 Redis 分布式锁与数据库唯一索引解决了并发预约的时段冲突问题。

后续可继续扩展的方向包括:接入微信小程序端、引入消息队列处理支付回调与超时取消订单、增加场馆地图与导航、基于历史数据做热门时段推荐等。

返回列表