简介:本资源是一套完整的体育馆使用预约平台毕业设计项目,面向计算机专业本科生及Java全栈初学者,旨在解决传统场馆预约流程不规范、人工管理效率低、数据容错性差等实际问题。系统基于Spring Boot后端框架与Vue前端框架构建,采用MySQL持久化数据,涵盖场地管理、用户预约、论坛互动、公告发布、订单处理等核心模块,具备工程可部署性与业务完整性。压缩包共750个文件,含101个Java后端逻辑文件、58个Vue组件页面、156个JS交互脚本、49个CSS样式资源及32个HTML模板,辅以SQL建表脚本、YML配置、BAT一键部署脚本(如install.bat、run.bat)等实用工具,整体大小为22.42MB。目前已有76人学习下载,读者可直接导入IDE运行调试,快速掌握前后端分离开发流程、权限控制实现、预约状态机设计及MySQL事务处理等关键技术点。
1. 项目缘起:为什么我们需要一个体育馆预约平台?
在高校或者大型社区里,体育馆资源紧张是个老生常谈的问题。我经历过无数次这样的场景:下午三点想约个羽毛球场,打开管理方的Excel表格或者微信群,发现晚上七点前的场地全被标红了;或者更糟,到了体育馆才发现,自己以为预约成功的场次,因为管理员忘了更新表格,已经被另一拨人占用了。这种混乱、低效、全靠人工和“人缘”的预约方式,不仅浪费了大家的时间,也造成了公共资源的闲置与冲突。
这就是我动手开发这个“体育馆使用预约平台”最直接的动力。我想用技术手段,把“谁先到谁得”或者“谁认识管理员谁优先”这种不透明的规则,变成一个公平、公开、可追溯的线上流程。这个平台的核心目标很简单:让用户能像在电商平台下单一样,清晰、便捷地查看场地空闲时段并完成预约,让管理员能从一个统一的、数据化的后台管理所有资源、订单和用户,彻底告别Excel和微信群接龙的原始时代。
整个项目采用了当前企业级Web应用开发中最主流、也最经得起考验的技术栈组合:Spring Boot作为后端API服务的基石,Vue.js作为构建用户界面的前端框架,MySQL作为持久化数据的数据库。这套组合拳的优势在于,前后端分离的架构让开发和部署可以并行且独立,Spring Boot的“约定大于配置”理念极大地简化了后端服务的搭建,而Vue的响应式和组件化特性则让前端交互体验变得流畅且易于维护。接下来,我会带你从零开始,拆解这个平台的每一个核心模块,分享我在设计、开发和部署过程中趟过的坑和积累的经验。
2. 后端基石:Spring Boot服务的设计与核心实现
后端是整个平台的大脑和规则执行者,它负责处理所有业务逻辑、数据校验以及与数据库的交互。选择Spring Boot,是因为它提供了一套完整的、开箱即用的解决方案,能让我们快速搭建一个稳健、可扩展的RESTful API服务。
2.1 项目结构与依赖配置
我采用的是经典的MVC分层架构,但结合Spring Boot的特性做了一些调整。项目根目录下,主要的包结构如下:
src/main/java/com/gymbooking/ ├── GymBookingApplication.java // 主启动类 ├── config/ // 配置类,如跨域、Swagger、安全等 ├── controller/ // 控制器层,接收HTTP请求 ├── service/ // 业务逻辑层接口 ├── service/impl/ // 业务逻辑层实现 ├── dao/ 或 mapper/ // 数据访问层,MyBatis的Mapper接口 ├── entity/ 或 model/ // 实体类,与数据库表对应 ├── dto/ // 数据传输对象,用于前后端交互 └── utils/ // 工具类,如日期处理、JWT工具等在pom.xml中,除了Spring Boot Web、MySQL Driver、MyBatis等基础依赖,有几个关键依赖值得特别说明:
- Spring Boot Starter Validation:用于对Controller层接收的参数进行注解式校验,比如
@NotNull、@Size、@Email等。这能确保进入业务逻辑的数据是基本合规的,将无效请求拦截在最外层。 - MyBatis Plus:这是一个强大的MyBatis增强工具。我强烈推荐使用它,而不是原生MyBatis。它提供了通用的
BaseMapper,让基础的CRUD操作无需编写XML,同时其强大的QueryWrapper可以让你用Java Lambda表达式优雅地构建复杂查询条件,极大提升了开发效率。 - Hutool:一个国人开发的Java工具库,集成了文件处理、加密解密、日期转换等众多实用功能。比如用它来生成订单号、处理日期区间重叠判断,代码会简洁很多。
- Spring Security + JWT:对于预约平台,用户认证和授权是必须的。我采用Spring Security整合JWT(JSON Web Token)的方案。用户登录成功后,后端生成一个加密的Token返回给前端,前端在后续请求的Header中携带此Token。后端通过一个自定义的过滤器(Filter)来校验Token的有效性。这样实现了无状态的认证,服务端压力小,也适合前后端分离的架构。
注意:在引入JWT时,务必妥善保管生成Token时使用的密钥(Secret),并且为Token设置合理的过期时间(如2小时)。对于刷新Token的逻辑,我通常采用“双Token”机制:一个短期的Access Token用于接口访问,一个长期的Refresh Token用于获取新的Access Token,这需要在业务逻辑层仔细设计。
2.2 核心业务逻辑与数据库设计
数据库设计是项目的根基,设计得好,后续开发事半功倍。核心表大概有这几张:
- 用户表 (user):存储用户基本信息。字段包括id、用户名、密码(加密存储)、手机号、邮箱、角色(普通用户、管理员)、注册时间等。
- 场馆/场地表 (venue):这是资源表。可以设计两级结构,比如先有“体育馆”作为场馆,其下再有“羽毛球场地1”、“篮球场A”等具体场地。字段包括id、名称、所属场馆ID、类型(羽毛球、篮球等)、状态(可用、维修中)、简介、图片等。
- 预约订单表 (booking_order):这是核心的业务表。字段包括:id、订单号(唯一,可用时间戳+随机数生成)、用户ID、场地ID、预约开始时间、预约结束时间、订单状态(待支付、已预约、进行中、已完成、已取消)、创建时间、总金额等。
这里最复杂的业务逻辑集中在“预约”这个动作上,它不是一个简单的插入操作,必须解决“资源冲突”问题。即,在同一时间段内,同一个场地只能有一个有效的预约订单。
我的解决方案是在Service层实现一个原子性的校验和创建过程。伪代码如下:
@Service public class BookingServiceImpl implements BookingService { @Autowired private BookingOrderMapper orderMapper; @Transactional(rollbackFor = Exception.class) // 开启事务 public BookingResult createOrder(BookingRequest request) { // 1. 参数基础校验(时间是否合法、场地是否存在等) validateRequest(request); // 2. 核心冲突检查:查询该场地在请求时间段内是否有其他“已预约”或“进行中”的订单 LambdaQueryWrapper<BookingOrder> wrapper = new LambdaQueryWrapper<>(); wrapper.eq(BookingOrder::getVenueId, request.getVenueId()) .eq(BookingOrder::getStatus, BookingStatus.BOOKED) // 已预约状态 .or().eq(BookingOrder::getStatus, BookingStatus.IN_PROGRESS) // 或进行中状态 .and(w -> w .lt(BookingOrder::getStartTime, request.getEndTime()) // 新订单结束时间 > 旧订单开始时间 .gt(BookingOrder::getEndTime, request.getStartTime()) // 新订单开始时间 < 旧订单结束时间 ); Long conflictCount = orderMapper.selectCount(wrapper); if (conflictCount > 0) { throw new BusinessException("该时间段内场地已被预约,请选择其他时间"); } // 3. 生成订单实体,设置状态为“待支付”或“已预约”(取决于是否需在线支付) BookingOrder newOrder = buildOrder(request); // 4. 插入订单 orderMapper.insert(newOrder); // 5. 可能触发的其他逻辑:发送短信通知、更新场地缓存等 sendNotification(newOrder); return BookingResult.success(newOrder); } }踩坑心得:时间冲突的SQL条件判断是新手极易出错的地方。上面代码中的
lt(小于) 和gt(大于) 组合,判断的是“时间区间有交集”,这是最标准的做法。千万不要写成BETWEEN start AND end,那会漏掉很多交叉情况。另外,务必在数据库层对venue_id、start_time、end_time和status字段建立复合索引,能极大提升冲突检查查询的性能,尤其是在高并发预约场景下。
2.3 接口安全与性能考量
除了基本的JWT认证,接口安全还需要注意以下几点:
- 防重复提交:用户可能连续点击“提交预约”按钮。前端可以做按钮禁用,但后端更可靠。我采用“幂等性”设计,为每个预约请求生成一个唯一的“请求ID”(如UUID),存入Redis并设置较短过期时间。在处理请求前,先检查该ID是否存在,存在则视为重复请求,直接返回之前的结果。
- 数据权限校验:在查询“我的订单”或取消订单时,必须在Service层校验当前登录用户ID是否与订单的用户ID匹配,防止越权操作。
@PreAuthorize注解可以帮我们优雅地实现方法级别的权限控制。 - 敏感信息脱敏:返回用户列表或订单详情时,手机号、邮箱等敏感信息需要部分隐藏,如“138****1234”。这可以在DTO对象返回前,通过Hutool的
DesensitizedUtil工具类处理。
性能方面,对于场馆列表、场地状态(某天某个场地的占用时间段)这类查询频繁、变化相对不频繁的数据,一定要引入缓存。我使用Redis作为缓存中间件。例如,将“场地ID+日期”作为Key,将该场地当天的已被预约时间段列表作为Value存入Redis,并设置过期时间为第二天凌晨。当用户查询某天场地状态时,先查缓存,没有则查数据库并回填缓存。当有新的预约成功或取消时,需要删除或更新对应的缓存数据,保证数据一致性。
3. 前端交互:用Vue 3构建动态且友好的用户界面
前端的目标是提供一个清晰、响应迅速的操作界面。我选择了Vue 3的Composition API配合<script setup>语法,以及Pinia状态管理,这套组合让代码组织更清晰,逻辑复用更方便。
3.1 项目初始化与核心组件设计
使用Vite初始化项目能获得更快的启动和热更新速度。核心的页面组件包括:
- 登录/注册页:表单验证使用VeeValidate或Element Plus自带的规则,提交时显示加载状态。
- 场馆场地浏览页:这是门户。左侧可以是场馆/场地类型的筛选器,右侧主区域以卡片或列表形式展示场地。每个场地卡片需要清晰展示名称、图片、类型、当前状态(如“可预约”、“已满”)。这里的关键是,状态需要根据后端接口返回的实时数据(或缓存数据)动态计算显示。
- 场地详情与预约页:点击具体场地进入。页面顶部展示场地详情,核心部分是一个可视化时间选择器。我推荐使用基于JavaScript的日历库(如
dayjs)配合UI框架的日期时间选择器(如Element Plus的DatePicker)自己封装一个。这个组件需要:- 默认显示未来几天(如一周)作为可选项。
- 用户选择日期后,向后端请求该场地在该日期已被预约的时间段。
- 在UI上,将已预约的时间段标记为不可选(如灰色禁用),将空闲时间段标记为可选。
- 用户选择开始和结束时间后,能实时计算并显示费用。
- 个人中心页:包含“我的预约”订单列表,提供查看详情、取消预约(在允许取消的时间段内)等功能。
3.2 状态管理与API交互
使用Pinia来管理全局状态,比如用户登录信息(userStore)和购物车/临时预约信息(bookingStore)。对于API请求,我使用axios进行封装,主要做了以下几件事:
- 请求/响应拦截器:在请求拦截器中,自动从
userStore或localStorage获取Token,并添加到请求头Authorization中。在响应拦截器中,统一处理HTTP状态码(如401跳转登录页)和业务逻辑错误码(如“场地冲突”),进行友好的消息提示。 - API模块化:将不同功能的接口按模块划分到不同的JS文件中,如
auth.api.js、venue.api.js、order.api.js,便于维护。 - 加载状态管理:为每个可能耗时的按钮操作(如提交预约)绑定一个加载状态,防止用户重复点击并提升体验。
一个典型的预约页面交互逻辑如下(Vue 3 Composition API风格):
<script setup> import { ref, computed, onMounted } from 'vue'; import { useRoute } from 'vue-router'; import { getVenueDetail, getBookedSlots, createOrder } from '@/api/venue.api'; import { useUserStore } from '@/stores/user'; import { ElMessage } from 'element-plus'; const route = useRoute(); const userStore = useUserStore(); const venueId = route.params.id; const venueDetail = ref({}); const selectedDate = ref(''); const bookedSlots = ref([]); // 格式如 [{start: '09:00', end: '10:00'}, ...] const timeRange = ref(['', '']); // 用户选择的时间范围 const loading = ref(false); // 获取场地详情 onMounted(async () => { venueDetail.value = await getVenueDetail(venueId); }); // 监听日期变化,获取该日期的已预约时段 watch(selectedDate, async (newDate) => { if (newDate) { bookedSlots.value = await getBookedSlots(venueId, newDate); } }); // 提交预约 const handleSubmit = async () => { if (!userStore.isLoggedIn) { ElMessage.warning('请先登录'); return; } if (!timeRange.value[0] || !timeRange.value[1]) { ElMessage.warning('请选择预约时间'); return; } loading.value = true; try { const orderData = { venueId: venueId, date: selectedDate.value, startTime: timeRange.value[0], endTime: timeRange.value[1], }; const result = await createOrder(orderData); ElMessage.success('预约成功!订单号:' + result.orderNo); // 跳转到订单详情页或我的订单列表 } catch (error) { ElMessage.error(error.message || '预约失败'); } finally { loading.value = false; } }; </script>3.3 用户体验优化细节
- 路由守卫:使用Vue Router的全局前置守卫
beforeEach,对需要登录的页面(如预约页、个人中心)进行拦截,未登录则重定向到登录页。 - 页面缓存:对于场馆列表页,可以使用
<keep-alive>组件进行缓存,用户从详情页返回时无需重新加载列表,体验更流畅。 - 错误边界:对于可能出错的组件(如依赖API数据的组件),可以使用Vue 3的
onErrorCaptured生命周期钩子或类似机制,捕获并展示友好的错误界面,而不是让整个页面白屏。 - 移动端适配:使用响应式CSS框架(如Element Plus本身是响应式的)或媒体查询,确保在手机和平板上也有良好的浏览和操作体验。
4. 前后端协同与数据流转
前后端分离开发,约定好API接口规范是关键。我采用RESTful风格,并使用Swagger(后端集成springfox或knife4j)自动生成API文档,前端开发时可以直接参考。
4.1 统一的响应体格式
前后端约定一个固定的JSON响应格式,便于前端统一处理。通常包含code(业务状态码)、message(提示信息)、data(业务数据)和timestamp(时间戳)。
// 后端统一返回对象 @Data public class R<T> implements Serializable { private Integer code; private String message; private T data; private Long timestamp = System.currentTimeMillis(); // 成功静态方法 public static <T> R<T> success(T data) { R<T> r = new R<>(); r.setCode(200); r.setMessage("success"); r.setData(data); return r; } // 失败静态方法 public static <T> R<T> error(Integer code, String message) { R<T> r = new R<>(); r.setCode(code); r.setMessage(message); return r; } }4.2 跨域问题处理
在开发阶段,前端运行在localhost:5173(Vite默认端口),后端在localhost:8080,浏览器会因同源策略阻止请求。在后端,可以通过配置一个全局的CORS过滤器来解决:
@Configuration public class CorsConfig { @Bean public CorsFilter corsFilter() { CorsConfiguration config = new CorsConfiguration(); config.addAllowedOriginPattern("*"); // 生产环境应替换为具体前端域名 config.setAllowCredentials(true); config.addAllowedMethod("*"); config.addAllowedHeader("*"); UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); source.registerCorsConfiguration("/**", config); return new CorsFilter(source); } }重要提示:在生产环境部署时,
addAllowedOriginPattern("*")这种允许所有源的配置是极不安全的,必须替换为确切的前端访问域名,例如config.addAllowedOrigin("https://booking.yourdomain.com")。
4.3 文件上传与访问
如果平台需要上传场地图片或用户头像,这部分需要单独处理。我通常的做法是:
- 后端提供
/api/upload接口,接收MultipartFile文件。 - 文件不直接存入数据库,而是保存到服务器的某个目录(如
/static/upload/)或对象存储服务(如阿里云OSS、腾讯云COS)。数据库中只保存文件的访问路径(URL)。 - 后端需要配置静态资源映射,使得存储的图片能被外部访问。例如,将
/static/upload/**路径映射到服务器的实际文件目录。 - 前端使用
<input type="file">或对应UI组件上传,获取到后端返回的URL后,再随其他表单数据一并提交。
5. 部署上线:从开发环境到生产环境的跨越
项目开发完成,最终要部署到服务器上对外提供服务。我以最经典的Linux服务器 + Nginx + Jar包部署方式为例。
5.1 后端服务打包与运行
- 打包:在Spring Boot项目根目录下,使用Maven命令
mvn clean package -DskipTests进行打包。完成后会在target目录下生成一个可执行的JAR文件(如gym-booking-0.0.1-SNAPSHOT.jar)。这个JAR包内嵌了Tomcat服务器,因此可以直接运行。 - 配置文件分离:切勿将包含数据库密码、Redis密码、JWT密钥等敏感信息的
application.yml或application.properties文件打包进JAR!正确的做法是,在JAR包同目录下,创建一个config文件夹,将生产环境的配置文件(如application-prod.yml)放在里面。Spring Boot会自动加载外部配置文件,优先级高于JAR包内部。这样也方便不同环境(测试、生产)切换配置。 - 服务器运行:将JAR包和外部配置文件上传到服务器。使用
nohup命令在后台运行服务,并将日志输出到文件:
这里nohup java -jar -Dspring.profiles.active=prod gym-booking-0.0.1-SNAPSHOT.jar > app.log 2>&1 &-Dspring.profiles.active=prod指定使用prod环境的配置。更专业的做法是使用systemd来管理服务,可以设置开机自启、自动重启等。
5.2 前端项目构建与Nginx配置
- 构建:在前端项目根目录下,运行
npm run build(Vite项目是npm run build)。这个命令会编译、压缩所有代码,生成一个dist目录,里面是纯粹的HTML、CSS、JS文件。 - 部署:将
dist目录下的所有文件,上传到服务器的某个目录,例如/var/www/gym-booking-frontend/。 - Nginx配置:Nginx有两个主要作用:一是作为Web服务器,托管前端静态文件;二是作为反向代理,将API请求转发给后端Spring Boot服务。
配置完成后,重启Nginx:server { listen 80; server_name your-domain.com; # 你的域名 # 前端静态文件 location / { root /var/www/gym-booking-frontend; index index.html; try_files $uri $uri/ /index.html; # 支持Vue Router的history模式 } # 反向代理后端API location /api/ { proxy_pass http://localhost:8080/; # 转发到后端服务 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; } # 代理上传的静态文件(如果文件存在服务器本地) location /upload/ { alias /path/to/your/upload/directory/; } }sudo systemctl restart nginx。
5.3 数据库初始化与数据迁移
在生产服务器上安装MySQL,创建对应的数据库和用户。将开发环境的数据库结构(表结构)导出为SQL文件,在生产环境执行。注意,不要直接导出全部数据(尤其是测试数据),只导出表结构即可。初始的管理员账号等基础数据,可以通过编写一个数据库初始化脚本(或使用Flyway/Liquibase这样的数据库版本管理工具)来插入。
5.4 域名、HTTPS与监控
- 域名解析:在域名服务商处,将你的域名(如
booking.yourdomain.com)解析到服务器的公网IP。 - HTTPS:使用Let‘s Encrypt的Certbot工具,为你的域名申请免费的SSL证书,并在Nginx中配置,将HTTP请求重定向到HTTPS。这是现代网站的标配,能提升安全性。
- 基础监控:至少需要监控服务器的CPU、内存、磁盘使用率。可以使用
htop、df等命令手动查看,或使用更专业的监控系统(如Prometheus+Grafana)。同时,要定期查看Spring Boot应用日志(app.log)和Nginx的访问/错误日志,以便及时发现和排查问题。
走到这一步,一个完整的、可运行的体育馆预约平台就已经部署上线了。从需求分析、技术选型、编码实现到部署运维,每一个环节都充满了选择和挑战。这个项目麻雀虽小,五脏俱全,涵盖了用户认证、资源管理、事务处理、前后端交互、缓存、部署等Web开发的常见核心问题,是一个非常棒的练手和学习的项目。希望我的这些拆解和经验,能帮你少走一些弯路。
本文还有配套的精品资源,点击获取