2026/10/21 6:57:08

SpringBoot+Vue体育馆管理系统:从场地预约到前后端分离部署全解析

SpringBoot+Vue体育馆管理系统:从场地预约到前后端分离部署全解析 做这套海滨体育馆管理系统最初的原因其实很朴素一个体育馆的前台每天要接几十通电话有人问羽毛球馆今晚还有没有场有人查游泳次卡还剩几次还有人打电话来改预约时间。电话记录、纸质台账、Excel 表格来回倒腾遇上高峰时段漏约、重约、超卖的情况屡见不鲜。所以当运营方提出想上一套信息化系统时我第一个想法就是先把场地预约这件事做成一个确定性的流程再逐步覆盖会员、课程、器材这些周边业务。这套系统最终采用 SpringBoot Vue MyBatis MySQL 的前后端分离架构后端专注业务逻辑和数据接口前端做页面交互数据库统一沉淀核心数据。整套源码加部署教程整理完之后它既是一个可以直接改造成商用的小型场馆信息化方案也是一套非常适合用来打通前后端技能树的实战项目。不管你是正在学 SpringBoot 和 Vue 的开发者还是想接中小型场馆外包项目的从业者这篇文章里讲的模块拆分、数据库设计、接口实现和部署细节都能直接拿去参考。1. 需求盘底海滨体育馆运营里那些不得不数字化的场景1.1 运营方最痛的四个业务场景做系统之前我花了差不多一周时间蹲在场馆里看他们怎么干活最后把痛点收敛成四块后续所有设计都围绕这四块展开。第一是场地预约。海滨场馆通常同时包含羽毛球馆、篮球场、游泳馆、网球场、乒乓球室等多种场地每种场地又有不同的计价方式工作日和周末价格不一样会员和非会员价格不一样有的按小时租有的按固定场次租。预约还要考虑到场地维护、闭馆时段、节假日的特殊安排。这个场景是整个系统的核心也是数据一致性要求最高的地方。第二是会员管理。场馆的主要营收来源其实是会员卡次卡、月卡、年卡、储值余额各种类型混着来。次卡要记录剩余次数储值卡要记录余额和每一笔消费会员在预约时还能享受折扣。这里最容易出问题的地方是账目对不上所以我把充值和消费全部做成流水记录任何人任何时候都能追溯。第三是课程培训。游泳培训班、少儿篮球班、网球私教课是很多海滨场馆的引流项目。课程涉及教练排课、学员报名、固定班级、请假补课如果没有系统支撑光靠排课表和微信群非常容易乱。第四是公告与运营通知。临时闭馆、设备维修、节假日营业时间调整这些信息必须能触达所有用户。最基础的做法是系统内公告 前端首页横幅再配合短信或公众号通知就能覆盖大多数场景。1.2 角色划分与权限边界根据实际使用的人我把系统角色分成四类系统管理员、运营前台、教练、普通用户会员。管理员拥有全部权限包括场地配置、角色分配、数据统计运营前台负责日常接单、场地排期、会员办卡、处理取消和退款教练只看得到自己的课程表和上课记录用户在微信端或网页端完成预约、充值、查看订单、取消预约。权限设计上没有做特别复杂的 RBAC 表而是用一个 user_role 字段 后端接口注解校验解决。前期角色少这种方案维护成本最低。如果未来要扩展再改成标准的角色-权限两张表也不迟接口层不需要大动。1.3 边界取舍哪些功能第一版先不做这是很多项目翻车的根源——什么都想要结果什么都做不透。我在第一版明确砍掉了三块不接真实第三方支付网关只做余额扣款和模拟支付回调避免资质和商户号问题不做闸机联动和硬件对接那需要单独的物联网方案不做实时视频监控那是另一个量级的产品。把这些边界提前讲清楚开发周期能从三个月压缩到一个月交付后也不会被需求淹没。2. 技术选型复盘前后端分离是趋势但这套组合更适配2.1 前后端分离的边界到底划在哪很多人一上来就分两个工程但说不清边界。我的判断标准很简单凡是涉及数据和钱的都不能交给前端价格计算、库存校验、权限判断、订单状态流转必须全部在后端完成前端只负责页面展示、表单校验和交互动作。后端以 RESTful API 的形式输出 JSON前端通过 HTTP 调用互不干扰。这样做的直接好处有两个一是前端可以独立开发后端接口 mock 好之后两边并行推进二是未来如果想出小程序端或者 App 端后端接口完全不用重写直接复用同一套 API。2.2 四个核心组件各自在解决什么问题先说说 SpringBoot。选择它不是因为大家都在用而是因为它确实适合这种业务系统内置 Tomcat打包成 Jar 就能跑依赖管理省心生态里要什么有什么。对于一个小团队维护的体量SpringBoot 的开发和运维成本是最低的。MyBatis 则是冲着 SQL 可控性去的。场馆预约系统的查询条件非常灵活——按场地类型、按日期、按时间段、按价格区间各种条件组合用 MyBatis 动态 SQL 能精确控制每一句查询也好做 SQL 级别的性能优化。相比之下JPA 在复杂查询时生成的 SQL 不可控反而让人心里没底。Vue 选它是因为后台管理系统是它的主场。组件化开发、响应式数据绑定、配合 Element Plus 这类现成的 UI 组件库一个功能完整的运营后台写起来速度飞快。我记得光靠表单组件和表格组件就节省了至少三分之一的页面开发时间。MySQL 就不用多说了稳定性好、成本低、运维资料多。以这家体育馆的规模——日预约量几百单、会员几千人——单机 MySQL 完全够用连分库分表的边都不用摸。2.3 为什么没上 Redis 和微服务是因为它们此刻不必要可能有人会问预约系统并发冲突不是应该上 Redis 分布式锁吗我的回答是要看量级。一个海滨体育馆的瞬时并发很难超过几十MySQL 的事务隔离 唯一索引 乐观锁完全能扛住引入 Redis 反而会给部署和运维增加一个中间件对教学项目和中小型项目来说都是多余的复杂度。微服务更是没必要。单体能解决的问题拆成微服务只会让开发、部署、排查都变得痛苦。前后端分离的单体应用是这个项目规模下效率最高的形态没有之一。真正的技术选型不是越复杂越好而是在满足业务的前提下做到最简单。3. 数据库设计一套经得起运营折腾的表结构3.1 核心域场地、场次、预约三张表联动预约系统的地基是三张表场地表、场次表、预约订单表。场地表存每个场地的名称、类型、容纳人数、工作日价格、周末价格、状态场次表把每个场地按固定的时间粒度比如每半小时或一小时预生成未来一段时间的可用场次预约订单表记录谁在什么时间占了哪个场地的哪个场次。这里有一个设计上的关键决策到底是预生成场次记录还是按时间区间动态判断。我最终采用的是预生成 按日占用的混合方案。管理员配置好场地和营业时间后系统每天定时生成未来七天的场次数据状态默认可约。用户下单时把对应场次状态改为已占同时写一条预约订单。这样查询某个场地今天还有哪些空场就是一条简单的按状态查列表前端展示也直观。预约订单表我加了状态、金额、折扣金额、实付金额、创建时间这几个核心字段。金额全部用 DECIMAL(10,2)杜绝浮点精度问题。3.2 会员域余额、卡次与消费流水如何保证对得上账会员这块的账目设计我吃过亏所以这次特意做得保守。会员主表只存基本信息、卡类型、会员等级和当前余额/剩余次数所有资金变动都拆成独立的流水表——充值流水和消费流水。充值加余额、预约扣款、取消退款每一笔都写流水并且带操作时间和操作人。这样的设计看起来多写了几行代码但对账的时候极其有用。运营人员只需要把流水的累加值和会员表余额一对比哪里不对马上能定位到具体某笔操作。我见过太多项目把金额直接写在订单表里就完事月底一对账全是糊涂账返工成本远高于当初多写的那几行。3.3 业务域教练、课程、器材的关联设计课程培训模块由四张表组成教练表、课程表、课程排期表、报名表。教练表存基本信息和擅长项目课程表定义课程的名称、时长、价格、学员上限课程排期表把课程和具体时间、教练绑定报名表记录学员报了哪一期课程。请假和补课我直接用报名表上的状态字段处理避免无限加表。器材模块更简单器材表 借用记录表。器材表维护名称、库存、状态借用记录表记录谁在什么时间借了什么、归还时间。因为这套系统不涉及硬件器材的借出/归还完全靠前台在界面上操作数据层面就是两条状态更新。3.4 冗余字段与索引写起来难看查起来真香我数了一下整个系统一共 21 张表。在设计上我刻意做了一些不规范的冗余例如预约订单表里冗余了场地名称、用户昵称而不是每次都去 join 场地表和用户表。这样做的原因是查询列表页时少几次联表响应速度快代码也更直白。数据冗余带来的更新一致性问题在这个量级下靠后端在同一个事务里同步更新就能解决不需要引入额外机制。索引方面预约订单表的 (venue_id, use_date, start_time) 联合索引、会员表的 user_id 索引、流水表的 member_id created_at 索引是必备的。尤其是场地时间冲突校验没有联合索引的话并发一高就可能出现慢查询。4. 后端落地SpringBoot MyBatis 的接口实践与状态机4.1 JWT 鉴权与接口权限控制后端所有接口统一走 REST 风格项目按模块分包controller、service、mapper、entity、common。鉴权用的是 JWT登录成功后签发 token前端在请求头里带 Authorization后端用一个 HandlerInterceptor 统一拦截。拦截器里做三件事解析 token 并校验有效期把用户信息放入当前请求上下文根据接口上的自定义注解 RequireRole 判断角色是否匹配。核心代码大致是这样public class AuthInterceptor implements HandlerInterceptor { Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { if (request.getMethod().equalsIgnoreCase(OPTIONS)) { return true; } String token request.getHeader(Authorization); if (token null || token.isEmpty()) { throw new BusinessException(401, 未登录); } // 解析并校验 token LoginUser user JwtUtil.parseToken(token); // 角色校验 if (handler instanceof HandlerMethod) { RequireRole requireRole ((HandlerMethod) handler).getMethodAnnotation(RequireRole.class); if (requireRole ! null !hasRole(user, requireRole.value())) { throw new BusinessException(403, 无权限); } } UserContext.set(user); return true; } }角色校验这步一定要放在后端不能只靠前端隐藏按钮。接口层的校验哪怕重复也是安全底线。4.2 预约订单的状态机设计预约订单我定义了四个状态待支付、已预约、已完成、已取消。用户创建预约后先生成订单并锁定场次状态是待支付支付或余额扣款成功后变成已预约场馆确认使用完成或系统按时间自动流转为已完成取消则进入已取消同时释放场次、回滚余额。状态机看着简单但有几个细节必须注意。取消操作只允许在待支付和已预约两个状态执行已完成和历史订单不能取消退款金额要按当时的折扣比例计算不能用当前会员等级套算否则账目会乱所有状态变更都必须在后端 Service 层完成前端只能发起动作不能直接改状态字段。我把状态变更封装成独立方法每个方法开头先校验当前状态是否允许变更这就是一个最朴素的状态机。好处是以后新增已过期等状态时只需要在方法里加分支不会把逻辑散落在各处。4.3 MyBatis 动态 SQL 在复杂筛选中的价值场馆列表和订单列表都有复杂的筛选条件。MyBatis 的if标签在这里非常好用比如查询可预约场次时要按场地类型、日期、时间段、价格上限动态拼接条件一段核心 SQL 如下select idselectAvailableSchedules resultTypeScheduleVO SELECT s.*, v.name AS venue_name, v.type AS venue_type FROM schedule s JOIN venue v ON s.venue_id v.id where if testvenueType ! null and venueType ! AND v.type #{venueType} /if if testuseDate ! null AND s.use_date #{useDate} /if if testmaxPrice ! null AND (v.price_weekday ![CDATA[ ]] #{maxPrice} OR v.price_weekend ![CDATA[ ]] #{maxPrice}) /if if testsportType ! null AND v.sport_type #{sportType} /if AND s.status 0 /where ORDER BY s.use_date, s.start_time /select这里有个小坑要注意和在 XML 里必须用![CDATA[]]包起来否则会被当成 XML 标签解析报错。我第一次写就栽在这里整整排查了一个小时。4.4 并发下防止场地超订的兜底方案防止同一场次被两个人同时抢到这是整个系统最关键的技术点。很多教程只讲在代码里先查状态再插入但并发场景下这样根本不够——两个请求同时查到空闲同时插入就超订了。我的方案是三层兜底。第一层是数据库唯一索引在预约订单表上建立 (venue_id, schedule_id) 联合唯一索引从数据库层面保证同一场次最多一条有效订单第二层是事务内先更新场次状态再插入订单利用行锁串行化操作第三层是捕获 DuplicateKeyException捕获到就明确向用户返回该场次已被抢订。这三层不是理论是我在生产环境真实遇到过超订事故后总结出来的。只看任何一层都有漏洞叠起来才能做到真正的可靠。5. 前端搭建Vue3 工程化、权限路由与预约交互5.1 基于角色的动态路由前端我用的 Vue3 Vite Element Plus Pinia。登录成功后后端返回用户信息和角色前端根据角色动态注册路由。管理员能看到全部管理页面前台看到订单处理和会员管理教练只看到课程表普通用户只看到预约和我的订单。这里的关键是路由守卫。所有页面跳转前都要检查本地是否有 token没有就跳登录页。但由于路由是动态添加的刷新页面时会出现路由还没注册好就跳转的时序问题。我的解决办法是在路由守卫里先判断 Pinia 里有没有用户信息没有就调一次获取用户信息的接口拿到后再继续导航而不是直接依赖本地缓存。router.beforeEach(async (to, from, next) { const token localStorage.getItem(token) if (!token) { if (to.path /login) return next() return next(/login) } if (!userStore.userInfo) { await userStore.fetchUserInfo() // 根据角色添加动态路由 addDynamicRoutes(userStore.role) return next({ ...to, replace: true }) } next() })这种方式确实会让页面初始化多一次请求但换来的是刷新不白屏、权限不乱跳值得。5.2 场地面板与时间冲突校验预约页面是用户用得最多的界面。我把它做成一个场地 × 时间段的面板横向是当天各场地纵向是时间点格子颜色表示可约、已约、维护中。用户点可选格子系统弹出确认框显示场地、时间、原价、会员折扣价和余额确认后生成订单。前端在点击时也会做一层简单的冲突校验如果格子已经被置灰就不允许点击。同时我会在用户选择的同一时间跨多个场地时做提示避免用户误以为一次能订多块场地。但必须强调前端校验纯粹是提升体验最终的冲突判定仍然以后端唯一索引和事务为准前端永远不能被当作安全边界。5.3 数据可视化让运营方愿意用起来的统计页管理系统如果只有增删改查运营方用两周就没兴趣了。我额外加了一个数据统计页用 ECharts 展示近七日预约量趋势、各场地使用率排行、会员增长曲线、收入结构饼图。数据全部来自后端聚合接口前端只负责渲染。别小看这个页面它直接决定了系统能不能在运营方那里活下来。当负责人能一眼看到网球场周末使用率只有 30%的时候他会主动去调整价格和排期这个系统才算真正产生了价值。6. 部署教程一台 Linux 服务器从零跑通前后端分离项目6.1 服务器与基础环境准备部署我建议直接准备一台 2核4G 的云服务器这个配置跑当前系统加 Nginx 非常轻松。系统盘用主流 Linux 发行版就行比如 CentOS、Ubuntu 均可。需要安装的东西列出来其实只有几个软件版本建议用途JDK1.8 或 17运行 SpringBoot 服务Maven3.6后端项目构建打包MySQL8.0数据库Nginx1.2 以上前端静态资源与反向代理Node.js16 以上前端构建后端项目打包前要确认 Maven 仓库能拉到依赖建议先把本地构建跑通一次再到服务器上操作避免在服务器上反复排错。6.2 后端 Jar 包的打包与启动后端打包很简单在项目根目录执行mvn clean package -DskipTests打完之后 target 目录下会生成一个 Jar 包。生产环境我推荐把它放到独立目录比如/opt/stadium然后用nohup启动nohup java -jar stadium-system.jar --spring.profiles.activeprod /opt/stadium/logs/stadium.log 21 --spring.profiles.activeprod用来加载生产配置文件里面是真实的数据库地址、账号密码。用 nohup 启动的好处是关闭 SSH 后服务不会退出但服务器重启后服务不会自动拉起。更稳妥的做法是写一个 systemd 服务文件让系统守护进程管理开机自动启动崩溃自动重启。服务文件核心内容如下[Unit] DescriptionStadium System Service Afternetwork.target mysql.service [Service] Typesimple Userroot ExecStart/usr/bin/java -jar /opt/stadium/stadium-system.jar --spring.profiles.activeprod Restarton-failure RestartSec10 [Install] WantedBymulti-user.target把文件放到/etc/systemd/system/stadium.service然后systemctl enable stadium systemctl start stadium之后所有管理都通过 systemctl 完成比 nohup 靠谱得多。6.3 前端构建与 Nginx 反向代理前端本地先执行npm install npm run build构建完成后 dist 目录就是纯静态文件。把它上传到服务器的/var/www/stadium目录。Nginx 配置里要做两件事一是把根路径指向静态目录并配置 history 路由回退二是把/api开头的请求反向代理到后端 8080 端口。server { listen 80; server_name _; root /var/www/stadium; index index.html; location /api/ { proxy_pass http://127.0.0.1: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; } location / { try_files $uri $uri/ /index.html; } }配好之后nginx -t检查语法再nginx -s reload生效。这里最容易漏的是最后一行try_files没有它前端路由在刷新页面时会直接 404。6.4 数据库初始化与备份策略数据库部分先在服务器上建库再把初始化 SQL 导入mysql -uroot -p stadium_init.sql生产库的用户名密码不要写在项目公共配置里放在 prod 配置文件中单独维护。备份是我强烈建议一开始就做好的事。写一个简单的脚本用 mysqldump 每天凌晨备份加上 crontab 定时任务0 2 * * * mysqldump -uroot -p密码 stadium /backup/stadium_$(date \%F).sql保留最近三十天的备份就够了数据量不大全量备份比增量简单也更可靠。7. 踩坑实录与排错链路从启动失败到并发超订7.1 MyBatis 日期参数比较导致的查询偏差第一次做按日期查可约场次功能时前端把2024-09-15这样的字符串传给后端后端直接用字符串和数据库datetime字段比较。结果当天没问题但第二天写死的日期也可能被查出来因为 MySQL 隐式类型转换把字符串转成了当天的 00:00:00导致范围边界计算错误。排查链路是这样的先在前端看到查 9 月 15 日却返回了 9 月 16 日的记录怀疑 SQL 条件写错打印 SQL 日志后看到use_date 2024-09-15确实生成了数据库里这条记录的 use_date 也是 9 月 15 日问题诡异最后翻到字段类型是datetime而不是date才明白是类型转换的锅。解决方案是后端统一把入参解析成LocalDate比较时精确到日期再配合![CDATA[ ]]处理符号问题彻底根治。7.2 跨域配置没问题前端却一直报 CORS 错误前后端分离开发时跨域是必经之路。我一开始在后端加了 CorsFilter配置看起来完全标准但前端请求就是被拦截报 CORS 错误。排了半天发现问题不在 CORS 配置本身而在于我给所有接口加了认证拦截器。浏览器预检请求是 OPTIONS 方法没有带 token被拦截器拦下直接返回 401预检失败导致浏览器认为跨域不通过。解决方法是两行代码的事拦截器放行 OPTIONS 请求。这个坑特别隐蔽但遇到一次之后再也不会忘。7.3 前端部署后刷新任意页面都是 404本地开发一切正常一上服务器用户从首页点进预约页也正常但只要按 F5 刷新就白屏 404。原因很清晰Vue Router 用的是 history 模式跳转路径是/reservation这样的真实 URL刷新时浏览器向服务器请求这个路径而服务器上根本没有对应的文件。最开始 Nginx 配置只有静态文件指向没有加try_files回退自然 404。加了try_files $uri $uri/ /index.html;之后所有未知路径都回到 index.html由前端路由接管问题立刻消失。7.4 同一场次被两个人同时抢到的并发事故这是上线后唯一一次真正让我睡不着的故障。预约功能稳定运行了两周突然有一天运营反馈一次性出现了两笔相同场地相同时间的订单。第一反应是查代码逻辑先查场次状态再插入订单逻辑上没问题。再用 jmeter 模拟并发 20 个请求抢同一个场次发现大概每 8 组就会复现一次超订。问题源头逐渐清晰两个事务并发执行时都先 SELECT 查到场次状态是空闲随后都执行 UPDATE 和 INSERT因为事务隔离级别下 Select 不加锁两个请求都认为自己是唯一合法预约。修复方案就是前面说的三层兜底数据库唯一索引、事务内锁场次、捕获 DuplicateKeyException 友好提示。上线后再用同样并发脚本压测超订消失重复请求被正确拦截并提示。这个案例告诉我并发问题不能靠我以为不会发生必须有数据库层面的硬约束。整个项目做下来我最大的体会是一个系统能不能长久用下去关键不在于功能多炫而在于关键流程上的数据是否可靠、账目是否清晰、部署是否省心。对想上手这套系统的朋友我的建议是先别急着抄代码把预约状态机和表结构关系看懂再去跑前后端联调最后落到部署上。代码层面碰到问题时优先看数据库约束和日志输出大部分诡异问题都能从这两处找到答案。如果这套源码和部署流程能让你少走几个我走过的弯路那就很值了。