ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

SpringBoot+Vue+MyBatis体育馆预约平台开发实战

2026/9/25 20:21:57 拓冰建站 浏览量
SpringBoot+Vue+MyBatis体育馆预约平台开发实战 1. 项目要解决什么问题模块怎么拆1.1 体育馆预约的三个真实痛点我为什么会专门去写一套体育馆预约平台起因是帮朋友学校的信息中心做调研时发现大部分校内体育馆的预约方式还停留在“微信群喊一声 前台手写登记表”的阶段。羽毛球馆、篮球馆、乒乓球室这些热门场地一到高峰期全靠管理员肉眼判断哪个场空着、哪个时段重叠了用户跑过去发现场地被占的情况时有发生。这种模式的核心问题有三个第一预约信息完全不透明。用户不知道场馆哪些时段可用只能反复打电话问或者到现场碰运气。管理员则要一遍遍回复相同的问题傍晚高峰期一个人根本忙不过来。第二冲突判断极其依赖人工。一个场地一天分十几个时段用户A约了18:00-19:00用户B想约18:30-19:30管理员如果没有及时登记这两个预约就会撞车。第三数据统计基本靠Excel。到了月底想算一下各场馆利用率、哪些时段最热门只能翻登记表手工核对效率低还容易出错。这三个痛点本质上指向同一个需求把预约流程从“人管”变成“系统管”。体育馆预约平台要做的就是让用户自己查看场馆、自己选时段、自己提交预约让管理员在后台上架场馆、设置时段、审核或取消预约同时把预约数据全部沉淀到数据库里形成可统计、可追溯的记录。一套这样的系统做完前台登记表可以彻底退休场馆利用率也终于有真实数据可以看了。1.2 角色权限与功能清单做预约类系统第一步不是写代码而是先把角色分清楚。这套系统按使用方拆成两类角色普通用户和管理员。普通用户是体育馆的使用者管理员是体育馆的运营人员。两边的操作边界完全不同功能设计上也必须分开。角色核心功能普通用户注册登录、浏览场馆列表、查看场馆详情、选择可用时段提交预约、取消自己的预约、查看个人预约记录管理员用户管理禁用/启用账号、场馆管理增删改查、场地与时段管理、预约审核、查看预约统计、发布公告用户端的核心链路我把它简化成四条登录后浏览场馆、选时段预约、查看我的预约、取消预约。管理端的核心链路则是上架场馆、维护可预约时段、审核预约、处理取消请求。这里有一个容易被忽视的设计点普通用户提交预约后到底需不需要管理员审核我见过不少项目直接把预约状态设为“已确认”跳过了审核环节这样做在真实场景里是会有问题的。体育馆管理员往往需要确认场地没有被临时占用、设备是否可用再决定是否接受预约。所以这套系统里我保留了审核状态预约状态分成“待确认”“已确认”“已取消”三种管理员可以在后台一键切换状态。功能清单理清楚之后整个项目的技术实现就有方向了后端需要提供用户、场馆、预约、公告四组核心接口前端需要对应地组织页面和路由。模块边界清爽开发起来才不会越改越乱。2. 技术选型SpringBoot Vue MyBatis 这一套为什么经得起折腾2.1 后端骨架选 SpringBoot 的理由这套系统采用前后端分离架构后端骨架选了 SpringBoot原因其实很直接它把 Spring 生态里的配置成本压到了最低。早年用 SSM 写项目光 spring-mvc.xml、mybatis-config.xml、web.xml 这些配置文件就要写半天环境搭好了心情也耗尽了。SpringBoot 的自动配置把这些默认值全部内置开发者只需要关注业务代码本身。具体到体育馆预约平台这种中小型系统SpringBoot 有几个身体能感知到的优势起步依赖机制一个 spring-boot-starter-web 就能把 Web 容器、Jackson、日志全部带进来不用自己一个个引 jar 包内嵌 Tomcat打完 jar 包直接扔服务器上运行省掉外部 Tomcat 安装和配置对 MyBatis 的集成非常顺滑mybatis-spring-boot-starter 一加SQL 映射和事务管理都能按习惯方式工作。另外写接口的时候RestController、RequestMapping、Service 这套注解开发效率很高一个预约功能从 mapper 到 controller 基本二十几行代码就搞定非常适合这种业务逻辑清晰、接口数量在二三十个规模的项目。选型的时候我也没考虑过 Spring Cloud 那套微服务方案原因很简单体育馆预约平台属于典型的单体业务系统用户量级在几千人、并发量在几十人同时操作这个水平单体架构足够稳定部署也更省心。微服务在这个场景里只会引入服务注册、配置中心、网关这些与业务无关的复杂度没有实际收益。能用单体解决的问题不要为“技术先进感”买单。2.2 Vue 到底用 2 还是用 3前端选型时争议最多的就是 Vue 2 还是 Vue 3。我的建议很务实如果你在学校课程里学的是 Vue 2或者手头参考资料大多是 Vue 2 Element UI 的组合那直接继续用 Vue 2 心安理得完全没有必要为了追新而升级。Vue 2 的生态非常成熟Element UI 组件库、vue-router、axios 这些周边库的资料铺天盖地遇到任何报错一搜就有答案。对课程设计、毕业设计或者练手项目来说快速完成一个稳定可运行的系统比“用了最新版本”重要得多。如果你的项目想上 Vue 3那配套就是 Element Plus组合式 API 写起来确实更清爽TypeScript 支持也更好。但要注意一个问题Vue 3 Element Plus 的资料量比 Vue 2 少不少很多细节问题要自己翻源码或者看 GitHub issues对新手来说排查成本会明显增加。我的做法是平时自己玩的项目用 Vue 3交付型、课程型的系统用 Vue 2 Element UI稳定性优先。这套系统里的前端页面结构我按 Vue 2 的经典方式组织main.js 里挂载 Vue 实例并注册 Element UIrouter 里配置路由表和路由守卫api 目录里封装 axios 请求views 目录放页面组件。页面组件只负责渲染和用户交互数据请求统一走 api 模块这样后端接口路径改了只需要在 api 文件里改一处不用全局搜索字符串。2.3 MyBatis 对比 JPA实际开发中的差异在哪里持久层框架选 MyBatis 而不是 Spring Data JPA是我在写预约冲突查询时做过对比后做出的决定。JPA 能让你少写很多基础 CRUD 的 SQL这对简单增删改查很友好但一旦遇到“查某个场地某个日期有没有重叠时段”这种带条件、带逻辑的查询用 JPA 的 Specification 拼接条件代码可读性会明显下降排查 SQL 性能问题也不太直观。MyBatis 的思路是 SQL 由开发者完全掌控写出来什么就是什么数据库的查询计划也能一目了然。在体育馆预约平台里MyBatis 最实用的三个特性都派上了用场第一是动态 SQL。预约列表查询要根据用户传入的场馆 id、预约状态、日期范围动态拼接 where 条件 标签可以把条件拼装逻辑写得很清晰避免在 Java 代码里做字符串拼接的蠢事。第二是 XML 文件管理 SQL。项目里所有 mapper 的 SQL 都放在 resources/mapper 目录下的 XML 文件里修改查询逻辑不用重新编译 Java 代码改完重启就能生效联调阶段特别方便。第三是结果映射灵活。数据库字段名用下划线风格如 create_time实体类属性用驼峰风格如 createTime开启 mapUnderscoreToCamelCase 配置后自动映射省掉一大批复赋值代码。另外提一句 MyBatis 的分页项目里列表页会用到分页插件 PageHelper直接在 XML 里写普通 SQLPageHelper 会通过拦截器在查询前自动拼接 LIMIT 语句使用起来是零侵入的。3. 数据库设计预约类系统的核心都在表结构上3.1 核心表结构设计数据库设计是预约系统最关键的环节。我常跟朋友说这类项目把表建对了后面写代码基本就是流水线作业表建错了后面会一遍遍改 SQL、改实体类折腾死人。这套系统的表结构我按“用户-场馆-预约”三条主线来设计再加一张公告表用户表 userid、username、password、real_name、phone、role0用户1管理员、status0正常1禁用、create_time。场馆表 venueid、name、type羽毛球馆、篮球馆、乒乓球室等、location、description、price_per_hour、cover_image、status0启用1停用、create_time。预约表 reservationid、user_id、venue_id、reserve_date预约日期、start_time、end_time、status0待确认1已确认2已取消、remark、create_time。再加一个 version 字段做乐观锁用。公告表 noticeid、title、content、author、create_time。预约表的字段设计有两个非常关键的决策点。第一个是时间段存法我用 start_time 和 end_time 两个字段存储时段而不是只存一个 time_slot 编码。虽然固定时段比如每整点一个场次用编码更省空间但体育馆的场地预约往往是半小时起订、可以连续订多个时段用时间区间的可扩展性明显更强。第二个是状态字段的数据类型我用 tinyint 存状态0、1、2 分别对应待确认、已确认、已取消而不是直接用 varchar 存中文状态。用数字做业务状态的优点是数据体积小、查询效率高、不会出现同名不同写法的脏数据缺点是需要开发人员记住状态含义。我的做法是后端专门写一个枚举类来定义这几个状态常量代码里可读性就有了保障。3.2 预约冲突判断 SQL 的写法预约系统里最重要的一条 SQL就是判断某个场地在某个时间段是否已经有预约。这条 SQL 写错了系统所有的并发问题都会在这一刻爆发。先看实现SELECT COUNT(*) FROM reservation WHERE venue_id #{venueId} AND reserve_date #{reserveDate} AND status 1 AND start_time #{endTime} AND end_time #{startTime}这条 SQL 的核心是两个时间区间的重叠判断。如果用一条一维数轴来理解已存在的预约区间是 [start_time, end_time]新提交的预约区间是 [startTime, endTime]两个区间重叠的条件就是老区间的开始小于新区间的结束同时老区间的结束大于新区间的开始。这个判断条件比“开始时间落在已有区间内”或者“结束时间落在已有区间内”更严谨能覆盖住新预约完全包含旧预约、新旧预约交叉包含等所有情况。这里我多次强调因为踩过坑只判断“新开始时间不在老区间内”是不够的一旦新预约范围比老预约大这种写法就会漏掉冲突。推荐在这条 SQL 之外再加一个数据库层面的兜底在预约表上建一个联合唯一索引不过 MySQL 对区间重叠没法直接建唯一索引所以业务层判断仍然是最主要的拦截手段。配合事务使用冲突控制的效果在小并发场景下完全够用了。3.3 软删除与时间字段的细节预约记录不应该物理删除用户取消预约后用 update 把 status 改成 2 就可以。这种软删除方案能保留完整的历史数据之后统计各个时段的预约量、各场馆的热门程度都有数据可查。同理场馆、用户记录我也不做物理删除用 status 字段区分启用和停用前端状态变化就能感知。时间字段我统一用 datetimecreate_time 设置默认值 CURRENT_TIMESTAMP插入时不用专门赋值。实体类里对应字段用 java.time.LocalDateTime和 MySQL 的 datetime 类型天然匹配。这里提醒一句不要用 java.util.Date 和 MySQL datetime 混用格式化、时区问题会折腾得让人怀疑人生。 JDK 8 以后 LocalDateTime 的序列化在 Jackson 里默认格式是类似 “2024-05-01T12:00:00” 的 ISO 格式我习惯在 application.yml 里统一配置格式为 “yyyy-MM-dd HH:mm:ss”前端展示时直接可用省去前端再格式化。4. 后端核心接口与业务逻辑实现4.1 创建预约接口从参数校验到落库整个后端最核心的接口就是创建预约这条接口的逻辑编排几乎是预约系统的“心电图”。我把完整流程拆成五步第一步参数校验。接收前端传来的 venueId、reserveDate、startTime、endTime 四个必填参数。先判断日期是否是未来日期再判断结束时间是否晚于开始时间。这两道校验在接口入口就执行用 Spring 的 Valid 注解或者手动 if 判断都行。第二步场地状态校验。根据 venueId 查场馆表确认场馆存在且 status 为启用状态。如果管理员已经下架了这个场馆用户端就不应该还能看到但接口层仍然要检查防止有人绕过前端直接调接口。第三步冲突查询。执行冲突判断 SQL如果 COUNT 结果大于 0直接抛业务异常提示“该时段已被预约”。异常通过全局异常处理器捕获后把中文提示返回给前端展示。第四步插入预约记录。把 user_id 从登录用户信息里取出status 初始为 0待确认create_time 由数据库自动填充。第五步返回结果。把创建成功的预约记录 id 返回给前端方便前端跳转到“我的预约”页面。整个方法加 Transactional 注解因为冲突查询和插入记录是两个 SQL 操作之间如果出现异常必须保证事务回滚不然会出现查询不到冲突但插入失败、用户收到错误提示但记录却残留一半的脏状态。4.2 并发预约场景与乐观锁方案体育馆预约平台的并发量通常不高但“两个人同时抢同一块场地”的场景是真实存在的。假设用户 A 和用户 B 同时提交 18:00-19:00 的预约请求后端两个线程同时执行冲突查询会发现数据库里都还没有记录然后同时插入结果就是两个预约都成功、场地被重复预约。解决思路有几种我根据自己的实际场景选了最合适的一种reservation 表增加 version 字段插入时先查询当前版本号更新状态时通过乐观锁语句检查版本号是否被改动。UPDATE reservation SET status #{targetStatus}, version version 1 WHERE id #{id} AND version #{version}如果 UPDATE 影响行数为 0说明版本号已经被别人改过预约状态更新失败需要返回“预约已取消或已修改请刷新后再试”。在这个系统里冲突控制最大的压力在“插入”而不是“更新”所以我还会在插入前用“查询 插入”配合唯一业务约束。由于场地和时间段的组合在业务上要求唯一插入时再补一个状态判断即可。对于这种量级的系统没有必要上 Redis 分布式锁JVM 内部的事务隔离加上乐观锁已经把问题控制住了。4.3 MyBatis XML 里的几个实际坑写 MyBatis XML 时有几个坑是新手反复踩的我这里集中记录下来。第一个是参数传递。Mapper 接口方法如果有多个参数必须加 Param 注解否则 XML 里用 #{userId} 或者 #{venueId} 是拿不到值的。加了 Param 后XML 里的参数名就用注解里的名字比如 Param(venueId) Long venueIdXML 里就写 #{venueId}。第二个是 XML 转义问题。SQL 里的小于号 和大于号 在 XML 里面会被当成标签解析直接写会报错。比如查询过期记录要写 create_time NOW()或者用 包起来。我建议写成 CDATA 形式可读性更好尤其是复杂 SQL 里同时有大于和小于时。第三个是动态 SQL 的 where 条件嵌套。用 标签包住条件列表时第一个 前面不要写 AND 或 OR 会自动处理多余的连接词。这个细节解决了“查询条件为空时 SQL 语法错误”的经典问题。第四个是结果映射自动驼峰。在 application.yml 里加上mybatis: configuration: map-underscore-to-camel-case: true这样数据库的 create_time 字段会自动映射到实体类的 createTime 属性不用每一个字段都写 resultMap。注意这种自动映射对单条记录的查询是有效的但涉及多表联查、字段名有歧义的时候还是要手动写 resultMap 指定映射关系。5. 前端 Vue 页面搭建与交互细节5.1 项目目录结构与路由设计前端项目用 Vue CLI 创建目录结构如下src/ api/ # axios 请求封装按模块拆文件 assets/ # 静态资源 components/ # 通用组件如头部的导航栏 router/ # 路由配置 store/ # vuex存登录用户信息 views/ # 页面组件 login.vue # 登录页 register.vue # 注册页 home.vue # 首页/场馆列表 venueDetail.vue # 场馆详情与预约 myReservation.vue # 我的预约 admin/ userManage.vue # 用户管理 venueManage.vue # 场馆管理 reservationManage.vue # 预约审核 noticeManage.vue # 公告管理路由设计有一个关键点管理端页面需要做权限控制。用户登录后根据 role 字段判断是否管理员是管理员才允许访问 /admin 开头的路由。这一步我用路由守卫处理router.beforeEach((to, from, next) { const token localStorage.getItem(token) if (to.path /login) { next() } else if (!token) { next(/login) } else if (to.path.startsWith(/admin) localStorage.getItem(role) ! 1) { next(/home) } else { next() } })这套守卫逻辑虽然简单但挡住了两个常见的访问问题未登录用户看任何页面都被踢回登录页普通用户手动输入 /admin 路径会被转移到首页。前端权限只是体验上的限制真正的权限校验必须在后端接口层做比如管理端接口统一检查当前登录用户的 role这一点我在后端写过滤器时一并处理了。5.2 预约页面的时间段选择逻辑预约页面是用户操作最密集的地方交互设计直接影响使用体验。我的做法是把“日期选择”“场馆信息”“时间段选择”三块内容放在一个页面里展示日期用 Element UI 的 el-date-picker预约日期只能选择今天及未来三天体育馆通常只开放近期预约太远的日期没有意义场馆信息部分展示名称、位置、价格、剩余场地数量时间段用 el-time-select 或者手写时间段列表展示。时间段选择是一个细节坑比较多的控件。我直接把场馆当日可约的时段处理成下拉选项数据来源于后端的“可用时段”接口。这个接口内部会执行冲突查询把所有与当前预约时间重叠的时段排除掉只返回仍可预约的时段。这样用户看到的时间段列表天然就是可约的体验上比“先全选再挨个试错”顺畅很多。前端还要处理一个校验环节如果用户选择了日期但没有选时段或者选了时段但场馆当天已经被约满提交时必须给出清晰的提示。我的做法是提交前先用前端逻辑做一次简单校验场馆状态、日期是否为空、结束时间是否大于开始时间。但请记住前端校验只是辅助核心的冲突判断必须以后端返回结果为准因为前端拿到的“可用列表”有可能在用户选择后的几秒钟内就已经被其他人抢先预约了。5.3 axios 封装与 token 携带前后端分离项目必然遇到 token 传递问题。用户在登录接口成功后后端返回一个 token 字符串前端把它存到 localStorage 里。之后所有的业务请求都需要在请求头里带上这个 token否则后端过滤器会直接返回 401。我在 api 目录里统一封了一个 request.jsimport axios from axios import { Message } from element-ui import router from ../router const service axios.create({ baseURL: /api, timeout: 10000 }) service.interceptors.request.use(config { const token localStorage.getItem(token) if (token) { config.headers[Authorization] token } return config }) service.interceptors.response.use( response response.data, error { if (error.response error.response.status 401) { localStorage.clear() router.push(/login) } Message.error(error.response?.data?.message || 请求失败) return Promise.reject(error) } )这个封装解决了三个问题每个请求不用手动写 Authorization 头后端返回统一结构 { code, message, data } 时业务代码直接拿到 datatoken 过期或者账号被禁用时前端统一跳回登录页。接口 mock 阶段我会关掉拦截器等后端接口稳定之后再打开。6. 部署配置与常见问题排查实录6.1 项目完整启动步骤很多朋友拿到的源码在自己电脑上跑不起来九成是环境问题。我这里把一套经过验证的启动步骤完整记录下来你照着走基本不会卡住。环境版本建议JDK 1.8MySQL 5.7 或 8.0Maven 3.6 以上Node 14 左右。第一步初始化数据库。在 MySQL 中执行项目提供的 init.sql 脚本建好名为 sports_booking 的数据库和所有表结构。注意 MySQL 8.0 和 5.7 的驱动写法不同8.0 的驱动类是 com.mysql.cj.jdbc.Driver5.7 是 com.mysql.jdbc.Driver用错会直接报 ClassNotFound。第二步修改后端配置。打开 application.yml把数据库地址、用户名、密码改成你自己本地的值。启动 SpringBoot 的 main 方法看到 “Started Application in x seconds” 说明后端已经就绪。第三步启动前端。在项目根目录执行 npm install装依赖成功后执行 npm run serve控制台会输出访问地址默认是 http://localhost:8081。第四步把浏览器打开访问前端地址注册一个账号登录即可。注意端口冲突问题如果本机 8080 端口被占用SpringBoot 可以改成 8081 或其他端口前端 vue.config.js 里的 proxy target 也要同步修改。6.2 高频报错内容报错信息可能的原因解决方案Access denied for user rootlocalhost数据库账号或密码不对检查 application.yml 中的 username 和 passwordUnknown database sports_booking数据库没有创建先在 MySQL 里执行 CREATE DATABASE sports_booking 再导入脚本Public Key Retrieval is not allowedMySQL 8.0 的加密规则问题jdbcUrl 后加 allowPublicKeyRetrievaltrueuseSSLfalse前端请求 404跨域地址或接口路径不对检查 vue.config.js 的 proxy 配置和后端 controller 注解路径LocalDateTime 序列化为数组Jackson 格式化问题在 application.yml 配置 spring.jackson.date-format 和 time-zone端口被占用其他程序占用了 8080换端口或者在配置文件中修改 server.port第二行的 Unknown database 是最简单最容易被忽视的问题很多新手理所当然地以为项目里自带建库功能实际并非如此建库脚本需要手动执行。 Public Key Retrieval 是 MySQL 8.0 特有的报错驱动连接时的加密方式导致网上搜索时一眼认出这个问题直接按表格里的参数修复就行。6.3 做这套系统最容易被低估的工作量我不止一次看到有人以为“需求想清楚、框架选明白”就万事大吉了觉得预约平台无非就是几个 CRUD 接口真正做起来才会发现工作量集中在大量细节上。接口统一返回结构就是一个被低估的工作点。如果每个接口都各自返回不同的 JSON 结构前端联调时会写大量兼容代码。我一开始就定义统一的 Result 类包含 code、message、data 三个字段成功 code 为 200业务异常 code 为 4xx后端所有接口都返回这个结构前端封装的 axios 拦截器只处理这一个结构项目后期改起来非常舒服。全局异常处理也是同样重要。业务代码里抛出的所有异常包括参数校验失败、场地已满、场馆不存在都应该统一在 RestControllerAdvice 里捕获并转成 Result 返回。如果没有这层处理SpringBoot 默认返回的 500 错误页会包含大量堆栈信息既不美观也不安全。接口文档我强烈建议先写再码。我做这套系统时把每个接口的路径、请求参数、返回示例写成一张表甚至可以写在代码注释里前后端并行开发时严格按这张表推进联调阶段的摩擦会小非常多。等到项目快结束时我发现自己最耗时间的不是写接口而是改接口——早期没有规划好参数结构后面好几个地方都在为当初的偷懒买单。这个教训反复出现值得每个做类似系统的人记住表设计多想十分钟后端少改两小时接口设计多想五分钟前端少改半小时。做完整套系统最深的体会是体育馆预约平台这类业务系统真正值钱的部分不在页面漂亮不漂亮而在数据模型是否清晰、冲突判断是否严谨、异常处理是否完善。把这三点做到位项目不管拿去答辩还是应付工作底气都会足很多。