ARTICLE DETAIL

资讯详情

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

SpringBoot+Vue实战:体育馆管理系统设计与实现

SpringBoot+Vue实战:体育馆管理系统设计与实现 每年七八月海滨城市的体育馆基本是满负荷运转前台电话响个不停场地预订靠一张Excel传来传去会员卡余额要人工核对到了月底对账更是头疼。我之前帮一个体育馆做过一套管理系统用的就是SpringBootVue这套组合数据库走MySQL持久层用MyBatis整套项目从零搭起来到上线运营前后折腾了不少时间也踩了不少坑。这篇文章把我整个设计与实现过程梳理一遍从需求拆解、数据库设计到后端接口和前端页面再到联调阶段容易翻车的细节全部拿出来聊聊。不管是做毕业设计还是接外包项目或者单纯想系统走一遍Java全栈流程这篇文章都可以当作一份参考资料。我先把这套系统的最终形态说清楚管理员可以在后台维护场地、会员、教练和课程普通用户可以登录后在线预约场地、查看自己的订单和余额流水教练可以看课表和学员信息收银台在手机上就能完成开卡、充值和退款操作。数据大屏会实时展示今天的人流量、场地使用率和营收情况。所有功能都是浏览器访问管理员用电脑前台和教练用手机也能操作这一点在部署时非常实用。1. 体育馆管理的业务到底长什么样为什么要用SpringBootVue这套组合很多人一上来就开始写代码结果写到一半发现权限关系理不清、场地和订单状态对应不上返工成本特别高。我建议先花半天时间把业务模型想明白再动手建工程。1.1 放前台场景里一琢磨系统其实要管六件大事体育馆的业务看着简单实际拆开之后涉及的角色和流程比想象中复杂。我给这个系统划分了六个核心模块分别对应体育馆日常运营中最常接触的六类事务场地管理羽毛球、篮球、网球、游泳馆等不同场馆需要维护场地编号、类型、可容纳人数、按时段计费的价格策略还要支持租赁状态和定期维护状态的标记。会员管理实体卡和电子会员并存需要区分散客、次卡会员、年卡会员、VIP会员四档每一档的折扣率和权限都不一样。在线预约用户选场地、选时间段、提交订单、在线支付或余额扣款整个流程要处理场地冲突检测、爽约处理、退款和改签。教练排课每位教练可以带多个私教班和团课班需要管理课程时间、学员名单、课程消耗的课时数。收银与财务管理充值、退款、消费流水、发票记录这块必须做到每一笔钱都能追溯。统计报表各球馆的时段利用率、会员消费排行、课程收入趋势管理层要看这些数据来做运营决策。模块划分清楚之后整个系统的表结构和接口设计就有了底盘后面写代码基本不会被业务绕晕。1.2 技术选型不是赶时髦是卡着业务需求来的技术栈选型我见过太多翻车案例不是用了太重的东西把自己拖死就是选太冷门的框架出了问题找不到人问。SpringBoot Vue MySQL MyBatis这套组合胜在稳而且每个环节都有大量案例可参考。我给这套系统设计的技术组件如下技术组件用途为什么选它SpringBoot 2.7.x后端基础框架自动配置机制能省掉大量XML配置内嵌Tomcat让部署变成一个jar包的事MyBatis持久层框架SQL由自己掌控复杂动态查询好优化比全自动ORM更可控MySQL 8.x数据库稳定、免费、文档多体育场馆这类中小型业务量完全够用Vue 2.7 Element UI前端框架和组件库渐进式上手快Element UI的表格和表单组件能覆盖80%的后台管理页面AxiosHTTP请求库封装简单拦截器能统一处理token刷新和错误弹窗ECharts数据可视化做场馆利用率、营收趋势图轻量又高效后端分层用标准的 Controller-Service-Mapper 三段式前端就是组件化的页面。有人可能会问为什么不用MyBatis-Plus我个人的看法是这类系统中原生MyBatis的灵活性和可控性更好尤其是统计类SQL原生写法调整起来不绕弯子。如果不想手写太多基础CRUD引入MyBatis-Plus也完全没问题核心业务表的增删改查能省不少代码量。2. 需求拆解前台、会员、教练和管理员眼里各自需要什么一套系统能不能落地关键看每个角色打开页面后能不能快速完成自己的事。我实际去体育馆蹲了两天跟前台、店长和教练分别聊过需求可以归纳成四类主视角。2.1 会员端预约要快记录要准会员端是普通用户在手机上使用的功能这个是整个系统里最能提升体验感的部分。会员打开小程序或H5页面后最常用的功能是场地实时查看按场馆类型查看某个日期、某个时段的空闲状态我用颜色来区分空闲、已订、维护三种状态。在线预约下单选好场次后系统自动计算价格会员可选择余额支付或绑定在线支付。订单记录查看已预约场地的订单详情支持开场前4小时免费取消弥补因临时有事导致的损失。余额与课时管理显示当前余额、次卡剩余次数、年卡到期日期每次消费都有流水明细。2.2 前台运营端收银快订单改起来要顺手前台操作页面我设计得比较朴素核心诉求就一个减少操作步骤。前台需要处理办卡、充值、退款、预约改签、散客登记还要能快速查到某位会员的资料。比如会员报出手机号输入框直接模糊搜索匹配到后自动带出会员等级和余额然后直接选业务操作整个过程不超过三秒。前台还有一个高频操作是场地状态的人工修正。比如顾客订了晚上七点到九点的羽毛球场结果下大雨没来也没提前取消前台需要一键把订单标记为爽约同时释放场地。2.3 教练端看课表管学员教练端的权限比较窄只需要看三样东西我的课表、我的学员、课时消耗记录。私教课的时长扣减逻辑要注意如果学员购买的是12节私教课包每上完一节系统自动扣减一次还有剩余次数提醒。2.4 管理端对外是数据驾驶舱对内是规则配置中心管理端是权限最高的一侧包含系统全部数据模块。除了常规的场地、会员、订单维护有两个设计比较容易被忽略我在这里重点提一下计费规则配置不同时段的价格策略不能写死在代码里要支持管理员可视化配置。例如工作日的18:00-22:00是黄金时段价格上浮50%周末全天按节假日价格计算这些都要能随时调整。操作日志关键操作必须留痕谁在什么时间修改了价格、给哪位会员退了款都要有记录。这个需求在验收时经常被提到所以设计表结构时我就预留了操作日志表。角色和权限的整体设计我采用RBAC模型用五张表实现用户表、角色表、菜单表、用户角色关联表、角色菜单关联表。后端在拦截器里校验角色编码前端根据角色动态渲染菜单这样无论加多少种新角色都不会改动核心代码。3. 数据库设计会员、场地、订单三张核心表这样设计才不打架数据库设计是整个系统最重要的地基。表关系理不顺后面写SQL和做统计的时候会非常痛苦。我先画整体表结构再逐个讲解核心表的设计理由。3.1 核心表清单与设计意图数据表表名核心作用场地信息表venue记录所有场馆和场地区分类型、位置、容量、状态场地时段价格表venue_period_price按星期、按时段、按场地类型存价格策略会员表member存储会员资料、卡类型、余额、累计消费会员卡类型表member_card_type次卡、年卡、VIP等卡种的规则定义订单表orders场地预约订单主表订单明细表order_item订单按场次拆分明细课程表course私教和团课的基本信息教练表coach教练基本信息和带课能力充值流水表recharge_record每笔充值和退款的资金流水消费流水表consume_record每次余额扣款或课时扣减的流水操作日志表sys_operation_log关键操作留痕用户权限表sys_user, sys_role, sys_menu 等后台登录用户和权限3.2 会员表的设计细节会员表除了基础联系方式和卡信息之外有四个字段很容易忽略但很关键member_no会员编号用年月日加序号生成例如HY202507160001既是业务标识也方便前台快速查找。card_type_id关联卡类型表而不是直接存字符串。因为卡类型可能会改名称直接存ID改名称只动子表。balance余额字段用DECIMAL(10,2)不要用FLOAT。浮点数在精度上会出问题金额相关的字段一律用定点数。status状态字段我用0正常、1冻结、2注销而不是直接删记录。会员的充值记录和订单记录需要永久保留物理删除会造成审计链断裂。我踩过一个教训最初会员表的手机号直接建了普通索引后来数据量到十万级之后查询变慢改成唯一索引并配合前端格式校验问题就解决了。3.3 订单和场地时段的锁冲突处理场地预约最核心的逻辑是防止同一个场地、同一个时间段被两个人同时下单。我用两种策略共同保证数据库唯一约束场地明细表中对(venue_id, booking_date, time_slot)建唯一索引。这是最后一道防线不管代码怎么写都不会超卖。下单前置检查在事务内执行SELECT ... FOR UPDATE锁住订单明细中的场地行然后再判断是否可订。注意FOR UPDATE一定要放在事务里并且查询条件最好带上索引字段否则会锁全表造成严重的并发性能问题。3.4 时间字段的设计经验时间字段我用MySQL的DATETIME顺便踩过时区相关的坑。如果应用服务器和数据库服务器不在同一时区Java的LocalDateTime和MySQL的DATETIME之间转换会出现小时偏移。建议连接串强制带serverTimezoneAsia/Shanghai并且实体类统一使用LocalDateTime不要混用java.util.Date和LocalDateTime否则序列化格式会乱成一团。数据库初始化脚本我建议手写不要全靠ORM的自动建表。建表语句要写明ENGINEInnoDB DEFAULT CHARSETutf8mb4 COLLATEutf8mb4_general_ci尤其是utf8mb4这个字符集不能为了省事用默认的utf8否则会员昵称里带个生僻字或表情符号直接给你报Incorrect string value错误。4. 后端核心实现SpringBoot项目的骨架与关键模块开发后端是整套系统的中枢重点不是写CRUD而是把工程结构理清楚把几个容易出问题的公共机制配置好。4.1 工程结构与统一响应体我习惯按功能模块分包而不是按技术层次分包。按技术分层会导致一个业务改动要跨好几个包按功能模块分包则天然形成了内聚com.gym.admin ├── common // 统一返回体、异常处理、工具类 ├── config // 配置类如拦截器、跨域、MyBatis ├── modules │ ├── venue // 场地模块 │ ├── member // 会员模块 │ ├── booking // 订单预约模块 │ ├── course // 课程教练模块 │ └── report // 统计报表模块 ├── security // 登录认证与权限拦截统一响应体我封装了一个ResultT包含code、message和data三个字段业务接口全部返回这个结构。配合全局异常处理器业务逻辑里只需要丢出BizException前端就能收到带错误码和提示信息的JSONclean。4.2 MyBatis配置与Mapper开发要点MyBatis的Mapper接口和XML映射文件写起来有讲究。我做了三个约定第一所有动态SQL都用XML写不在接口上写注解SQL。注解适合简单查询但一旦涉及动态条件、批量更新、多表联查注解读起来非常痛苦XML的where、if、foreach标签表达力强得多。第二配置驼峰映射map-underscore-to-camel-case: true。数据库字段member_no自动映射到Java属性memberNo省去一大堆resultMap手写映射。第三自定义拦截器不要和分页拦截器混着乱写。MyBatis的插件机制用Intercepts注解我自己写过一个操作日志插件差点跟分页插件在Executor层面冲突后来学乖了日志记录直接放在Service层用AOP处理不碰MyBatis的Executor。4.3 分页插件配置和使用分页是后台管理系统的高频场景。我使用的分页插件是PageHelper配置和用法如下dependency groupIdcom.github.pagehelper/groupId artifactIdpagehelper-spring-boot-starter/artifactId version1.4.7/version /dependency配置一行就够pagehelper: helper-dialect: mysql reasonable: true support-methods-arguments: truereasonable: true的作用是当页码大于总页数时自动回退到最后一页不至于前端翻页翻出白屏。使用上有一个非常重要的约定PageHelper.startPage()后面必须紧跟第一条SQL查询语句中间不能查其他东西。我曾经在startPage之后先查了一波会员对象再查订单结果分页失效数据全量返回排查了很久才发现是页面上一个多余的赋值操作插在了中间。4.4 场地预定的并发事务实现发布场地预订的核心Service方法时我用注解式事务Transactional(rollbackFor Exception.class)包住整个操作。关键逻辑如下查询场地时段记录使用FOR UPDATE锁定判断该时段是否已被占用被占用则抛异常扣减会员余额先查余额再更新余额这一步如果用乐观锁需要在会员表加版本号字段生成主订单和订单明细写入消费流水注意Transactional默认只在RuntimeException下回滚如果业务代码里捕获了异常又没有重新抛出事务不会回滚就会出现钱扣了订单没生成的情况。所以异常处理要么不做catch让Spring统一管理要么catch之后重新抛出BizException。4.5 登录认证与权限拦截登录认证在这个项目里用的是JWT方案流程是用户登录成功后后端生成一个带有用户ID和角色编码的Token返回给前端前端每次请求在Header里带Authorization: Bearer xxx后端写一个拦截器解析Token并放行。关键点是拦截器只校验Token有效性角色的功能权限在具体的Service或注解里校验。我写了一个RequireRole(admin)注解配合AOP被注解标记的接口只允许指定角色访问。这样做的好处是权限规则跟着方法走看代码时一目了然。5. 前端Vue实现从项目初始化到核心页面开发前端这部分是很多人容易卡住的地方尤其是没接触过Vue的后端同学。我尽量把搭建过程和页面实现逻辑讲细一点。5.1 工程初始化与依赖安装我用Vue CLI创建项目命令如下vue create gym-admin-frontend在配置选择时选了Router和Vuex脚手架自己会装好依赖并生成基础结构。然后手动安装Element UI和Axiosnpm install element-ui axios echarts这里有个新手容易踩的坑国内网络环境下npm install的速度不稳定经常超时失败。建议在项目根目录创建.npmrc配一下国内镜像速度能快一个量级。安装完依赖之后在main.js里注册Element UIimport Vue from vue import ElementUI from element-ui import element-ui/lib/theme-chalk/index.css import App from ./App.vue import router from ./router import store from ./store Vue.use(ElementUI) new Vue({ router, store, render: h h(App) }).$mount(#app)注册Element UI之后基础的表格、表单、弹窗、日期选择器这些组件就都可以直接用了开发效率提升非常明显。如果觉得Element UI太重只按需引入需要的组件也行不过后台管理系统我个人建议全量引入省心。5.2 路由设计与登录守卫路由设计上我把页面分成两类一类是公共页面比如登录页和场地展示页另一类是登录后才能访问的管理页面。管理页面统一挂在名为Layout的父路由下面这样左侧菜单栏和顶部导航只需要写一次子页面自动继承布局。登录守卫用Vue Router的beforeEach实现起来很简洁router.beforeEach((to, from, next) { const token localStorage.getItem(token) if (to.path ! /login !token) { next(/login) } else { next() } })页面内的按钮级权限我通过自定义指令v-permission实现。管理员和普通员工的菜单本来就不一样前端初始化时根据后端返回的菜单列表动态注册路由这是一种比较优雅的做法避免把一份完整的菜单塞给所有人再看一遍删掉。5.3 Axios封装与拦截器Axios的封装质量直接决定前后端联调的效率。我的封装思路是新建一个request.js实例化Axios对象设置baseURL和请求超时时间然后在请求拦截器里加Token在响应拦截器里统一处理错误码。import axios from axios import { Message } from element-ui const service axios.create({ baseURL: /api, timeout: 10000 }) service.interceptors.request.use(config { const token localStorage.getItem(token) if (token) { config.headers[Authorization] Bearer token } return config }) service.interceptors.response.use( response { const res response.data if (res.code ! 200) { Message.error(res.message || 请求失败) return Promise.reject(new Error(res.message)) } return res }, error { if (error.response error.response.status 401) { router.push(/login) } else { Message.error(网络异常请稍后重试) } return Promise.reject(error) } ) export default service注意baseURL设置成/api这只是给开发环境的Proxy代理用的标识生产环境会用Nginx把/api前缀转发到后端服务这样前后端部署时就不需要改前端代码了。5.4 场地预约页面的核心逻辑场地预约页是整个系统前端最复杂的页面核心是场地状态日历和时段选择。我用ECharts先渲染一个场地状态总览然后用Element UI的日历组件展示每天各时段的预订热度。时段选择区是一个动态生成的表格行是场地编号列是时间片每个格子有三种状态绿色空闲可订灰色已经被别人订走黄色当前选中待提交用户点击黄色格子后组件把选中的场地和时间片集合传给后端后端统一做可用性校验。选完触发价格计算单价从venue_period_price读取会员折扣从卡类型表读取前端直接展示折后价格。这块用到Vue的双向绑定和计算属性比较多建议把选中集合维护在Vuex里避免跨组件传参传得晕头转向。5.5 数据大屏和图表展示管理端的首页我放了一个今日概览用ECharts画了三个图表主屏是一个24小时场地利用率曲线图下方两个辅助区域分别是各场馆营收占比的饼图和本周客流量的柱状图。数据获取走一个聚合接口/report/overview后端一次返回所有图表所需的数据结构。前端图表在mounted里请求数据后初始化ECharts实例窗口大小变化时调用chart.resize()。注意组件销毁时要执行chart.dispose()否则多标签页切换会导致内存泄漏。6. 联调与部署阶段必踩的坑我一个个给你排掉前后端独立开发完联调阶段才是真正检验系统质量的时刻。这个阶段暴露的问题五花八门我挑几个出现频率最高、最容易让人卡半天的分享出来。6.1 跨域配置的正确姿势前端跑在http://localhost:8080后端跑在http://localhost:8081直接请求必然有跨域问题。开发环境我推荐用Vue CLI的Proxy代理解决配置在vue.config.jsmodule.exports { devServer: { port: 8080, proxy: { /api: { target: http://localhost:8081, changeOrigin: true, pathRewrite: { ^/api: } } } } }这样前端请求/api/venue/list时开发服务器会把请求转发到http://localhost:8081/venue/list前端代码里不存在跨域问题。生产环境则由Nginx统一处理location /api/ { proxy_pass http://127.0.0.1:8081/; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; }后端也可以配置CORS但要注意一个隐蔽问题如果允许携带CookieallowedOrigins不能写*必须写确切的前端地址。我之前图省事写了*结果前端每次登录都拿不到Set-Cookie排查半天才发现是CORS规范对凭据的限制。6.2 前后端时间格式不统一后端序列化LocalDateTime返回给前端默认格式是类似2025-07-16T14:30:00带T的ISO格式前端控件直接显示这一串字符非常不友好。我在后端加了一个统一的Jackson配置spring: jackson: date-format: yyyy-MM-dd HH:mm:ss time-zone: GMT8时间从后端到前端的问题解决之后反向还有一个坑前端日期选择器默认给的是yyyy-MM-dd而后端实体类的LocalDateTime要求带时分秒直接传字符串会被Spring的格式转换器拒绝。给实体类的日期字段统一加上JsonFormat(pattern yyyy-MM-dd HH:mm:ss)并且前端在传参时拼上00:00:00作为默认时间整个链路就顺畅了。6.3 上传文件时的URL编码上传问题系统里有一个功能是上传场地实拍图片我最初直接在Controller里写MultipartFile接收参数并保存到本地磁盘通过http://ip:port/static/...访问。但后来发现上传文件名带中文或空格时前端展示图片会404原因是URL没有做编码。解决方式是自定义一个静态资源映射配置同时规定上传文件重命名为UUID文件名彻底避免编码问题。6.4 MyBatis分页插件与统计SQL的冲突统计报表模块有一个按月营收分组的SQL里面用到了临时表和嵌套子查询。分页插件对这类复杂SQL的判断有时会不准导致统计结果被拦腰截断。我的做法是统计类SQL不走PageHelper.startPage()直接封装成List返回前端拿到全部数据后再自行分页展示。统计报表的查询条件本来就少数据量也不会爆炸后端全量返回是合理选择。6.5 逻辑删除字段与唯一索引的冲突会员手机号我建了唯一索引同时会员表还有逻辑删除字段。问题来了如果一个会员注销了手机号还在表里下次同手机号注册新会员时唯一索引会阻止插入。解决办法是唯一索引改成组合索引(phone, deleted)这样同一个手机号只要deleted不同就可以共存。物理删除和逻辑删除混用会产生很多类似的边界问题设计索引时一定要先想清楚删除策略。6.6 后端接口的并发校验不能省场地预约是高并发场景前端做了场地状态展示之后后端下单接口在保存订单前必须重新校验一次场地可用性。不能完全信任前端传过来的可预订状态。我在Service层用分布式锁的思路做了限制以场地加时间段生成Redis锁key加锁成功才继续处理订单加锁失败直接拒绝并提示该时段刚被其他用户预订。数据库唯一索引是最底层的兜底Redis锁是性能和体验的优化两者不冲突。7. 系统的扩展方向与我的最终感受标准的版本做到这一步该有的功能都有了但上线运营一段时间后总会有新的需求冒出来。我把自己经历过的一些扩展点列出来给大家做个参考。第一个是对接微信小程序。目前前台会员是通过H5页面访问的体验尚可但如果想让用户更便捷地预订场地小程序是必然方向。后端接口设计时我把返回体结构做成统一的数据格式小程序端实际上可以直接复用这套接口只需要把Axios换成小程序的wx.request即可。第二个是短信通知与消息推送。用户预约成功、课程开始前、余额变动时都应该有通知机制。最初没有接短信网关预约成功只是页面弹窗很多用户过了几天就忘约了。后来接了一个短信服务开场前两小时自动发提醒短信爽约率明显下降。第三个是自动退款和优惠券系统。目前退款需要前台人工操作逻辑复杂的地方在于退款金额和余额回滚要放在同一个事务里。下一步可以做成超时自动退款、优惠券系统配合拉新活动这些都是在现有表结构上增加关联表就能实现的功能。最后再分享一个小技巧。这套系统的后端接口我统一用Postman维护了一套完整的接口文档每一步开发联调都按照文档来。等整个项目交付之后这套文档的价值甚至超过了源码本身因为后面所有新增功能和对接第三方系统都得先回去翻接口文档。我建议不管项目多紧接口文档不要省哪怕只是把请求和响应示例贴进去都能省下后面几倍的时间。
返回列表