多彩编程 多彩编程MZPH · CODE BLOG
ARTICLE DETAIL

文章详情

深耕前端与后端开发技术的一线实战笔记与踩坑复盘。

SpringBoot+Vue+MySQL健身房管理系统:从数据库设计到部署实战

SpringBoot+Vue+MySQL健身房管理系统:从数据库设计到部署实战 不少做管理系统的朋友看到可直接运行四个字就兴奋但实际拿到项目后光是环境配置和初始化数据就能耗掉大半天。我做这套健身房管理系统出发点是给中小型健身房一个开箱即用的信息化工具后端用SpringBoot前端用Vue数据库用MySQL前后端分离代码到位后能立刻跑起来。这篇文章不打算写成一堆功能列表的说明书而是把我在选型、设计表结构、写接口、调前端、启动部署过程中踩过的坑和想明白的事从头到尾理一遍。无论你是准备拿它做毕业设计还是在帮健身房做小型管理工具或者单纯想学一个完整的SpringBootVue实战项目这套思路应该都能帮上忙。1. 一套可直接运行的健身房管理系统到底在解决什么问题1.1 健身房日常管理最让人头疼的几个场景很多人觉得健身房管理系统不就是给会员办个卡嘛但真正接触过门店运营就会发现麻烦事远比想象的琐碎。前台的日常工作包括登记新会员、办理续费、处理私教课预约、记录每天进店锻炼的人员有时还要盯着哪些会员快到期了然后打电话续费。没系统的时候这些操作要么靠纸质表格要么靠Excel要么直接凭记忆。纸质登记的典型问题是会员卡号可能重复、办卡日期写错、续费时找不到历史记录。Excel稍微好一点但多人同时编辑经常出现覆盖问题尤其是前台和店长同时在改同一份文件。更麻烦的是到期提醒靠人肉去翻表格找即将过期的会员漏掉一两个人太正常了。我见过某健身房因为漏提醒一个月内流失了七八个原本准备续费的老会员。这就是信息管理系统存在的意义把登记、查询、提醒、统计这些重复劳动固化下来让人工操作降到最低。1.2 功能边界哪些功能该做哪些不该做做这类系统最容易犯的毛病是想把所有能想到的功能都塞进去。我在设计这套系统时给自己定了一条规矩只服务前台和店长这两个角色不做任何超出门店日常管理范围的功能。最终确定的核心模块如下会员管理新增会员、编辑资料、会员卡状态变更、到期时间查看。卡种管理次卡、月卡、年卡、私教课包不同卡种对应不同有效期和次数逻辑。课程与教练管理维护团课和私教课信息绑定教练和上课时间。预约与入场会员预约私教课或团课前台记录入场信息。订单与收银记录办卡、续费、购买商品等财务流水。经营统计查看会员增长趋势、续费收入、课程预约热度。人脸识别、自动扣款、微信小程序预约这些方向不是不好但它们要么依赖额外硬件设备要么需要开放支付平台接口做进来会显著提高部署复杂度和可直接运行的目标冲突。第一版宁可做得小而稳也不要做成一个启动需要配置一堆外部依赖的大杂烩。2. 技术选型背后SpringBoot Vue MySQL的组合为什么这么稳2.1 后端框架选择后端我选的是SpringBoot 2.7.x配合JDK 1.8。很多人纠结要不要直接用SpringBoot 3.x我的建议是如果是追求拿到就能跑的项目2.7.x反而更稳妥。原因很简单3.x要求JDK 17部分老版本依赖可能不兼容而大多数人的本地环境还是JDK 8或11。既然目标是减少启动阻力那就选兼容性最广的组合。持久层用了MyBatis-Plus。单表CRUD不需要手写SQL它内置的BaseMapper已经提供了增删改查和分页查询。以前用原生MyBatis写一个简单的条件查询都要配一堆XML现在代码量能省一半。比如会员列表的分页查询核心代码只需几行PageMember page new Page(current, size); LambdaQueryWrapperMember wrapper new LambdaQueryWrapper(); wrapper.like(StringUtils.isNotBlank(keyword), Member::getName, keyword) .orderByDesc(Member::getCreateTime); return memberMapper.selectPage(page, wrapper);没有复杂到需要手写SQL的地方大部分业务逻辑都靠MyBatis-Plus的Wrapper搞定维护成本很低。2.2 前端框架选择前端用的是Vue 3 Element Plus。Vue 3的组合式API写起来更灵活Element Plus的组件库对于后台管理系统来说堪称生产力工具表格、表单、弹窗、日期选择器全都现成。我最初也考虑过Vue 2 Element UI毕竟老项目生态成熟。但Element Plus的样式和交互比Element UI更现代而且Vue 3已经是主流方向新项目没必要用旧技术。组件库省下的时间非常可观比如会员列表页el-table配合el-pagination一个列表加搜索加分页功能半小时就能写好换做手写HTML和原生JS一天都未必搞定。2.3 数据库选型数据库选了MySQL 8.0。相比5.78.0的窗口函数、公共表达式、更好的字符集支持都是实打实的优势。对于这个项目数据量级别完全不用担心单表几十万条记录对于MySQL没有任何压力。这套技术栈之所以稳在于每一层都有非常成熟的使用范式网上资料多、踩坑记录丰富。用它们组合出来的项目遇到问题基本都能快速搜索到解决方案不会卡在某个冷门技术点上。3. 数据库设计把会员、课程、订单这几张核心表的关系理顺3.1 核心表结构设计数据库是这类系统的地基表结构如果设计得不对后面写接口、做统计都会非常别扭。这套系统的核心表有以下几张member会员基本信息包括姓名、手机号、性别、出生日期、备注。card_type卡种定义包括卡种名称、类型次卡/月卡/年卡、有效天数或次数。member_card会员持有的卡关联会员和卡种记录开卡时间、到期时间、剩余次数。coach教练信息。course课程信息关联教练包含课程名称、上课时间、人数上限。appointment预约记录关联会员、课程。order订单表记录办卡、续费产生的流水。check_in入场记录记录会员每次到店信息。以member_card表为例关键字段设计大致是CREATE TABLE member_card ( id int NOT NULL AUTO_INCREMENT, member_id int NOT NULL COMMENT 会员ID, card_type_id int NOT NULL COMMENT 卡种ID, start_date date DEFAULT NULL COMMENT 开卡日期, end_date date DEFAULT NULL COMMENT 到期日期, remain_count int DEFAULT NULL COMMENT 剩余次数次卡用, status tinyint DEFAULT 1 COMMENT 状态1有效 0失效, PRIMARY KEY (id), KEY idx_member_id (member_id), KEY idx_end_date (end_date) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;注意idx_end_date这个索引它是到期提醒的核心支撑。系统每天扫描一次end_date落在未来7天内的会员卡记录然后提醒前台联系续费。没有这个索引表数据一多扫描效率就会下降。3.2 次卡和期卡怎么统一处理健身房卡种表面上五花八门但本质只有两类按期计费的月卡、季卡、年卡和按次计费的次卡、私教课包。我处理这两类卡的思路是在card_type表里加一个type字段值为period表示期卡值为count表示次卡。期卡在开卡时根据有效天数计算end_date次卡则维护remain_count。续费操作时期卡直接延长end_date次卡累加remain_count。这里有一个小细节同一个会员可能同时持有期卡和私教课包此时member_card表里会存在多条记录。查询会员当前状态时要判断是否存在status1且end_date大于当天的期卡记录如果没有再判断是否有剩余次数的次卡记录。这个逻辑我写成接口时反复测试过确保不会出现会员明明还有次卡却被判断为过期的情况。3.3 预约冲突如何处理课程预约比较常见的问题是同一时段被约满或者同一个会员重复预约同一节课。我用一个联合唯一索引来兜底ALTER TABLE appointment ADD UNIQUE KEY uk_member_course (member_id, course_id, appointment_date);同一个会员同一天同一节课只能预约一次。再加上预约前查询课程当前人数是否小于上限双重校验之后基本不会出现超卖。4. 后端从登录到续卡核心接口设计与权限控制4.1 统一返回格式与异常处理前后端分离的项目最怕接口返回格式不统一。前端写死了读取code和data字段结果某个接口突然返回了不同的结构前端就得做一堆兼容判断。这套系统定义了一个全局返回体public class ResultT { private Integer code; private String message; private T data; public static T ResultT ok(T data) { ResultT r new Result(); r.setCode(200); r.setMessage(success); r.setData(data); return r; } public static T ResultT error(String message) { ResultT r new Result(); r.setCode(500); r.setMessage(message); return r; } }再配一个全局异常处理器把业务异常、参数校验异常、未知异常全部拦下来统一包装成Result.error()返回。这样前端拿到的永远是{ code, message, data }结构错误提示也能直接展示给前台操作人员。4.2 权限控制用一个轻量级方案系统只有两个角色管理员和前台工作人员。管理员能访问所有功能前台工作人员只能操作会员、预约、入场记录这些业务模块不能访问统计报表和系统设置。我使用的是基于JWT的轻量级鉴权方案没有引入Spring Security。对于这种小型管理系统Spring Security的配置复杂度有点高而拦截器加JWT的方案足够用。流程是登录成功后后端签发一个JWT里面带上用户ID和角色。前端每次请求把Token放在请求头里。后端写一个拦截器解析Token并校验角色权限。核心的拦截器逻辑示意public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { String token request.getHeader(Authorization); if (token null || !jwtUtil.validateToken(token)) { response.setStatus(401); return false; } String role jwtUtil.getRoleFromToken(token); if (STAFF.equals(role) request.getRequestURI().startsWith(/admin)) { response.setStatus(403); return false; } return true; }这样既保证了接口不被匿名访问又实现了角色隔离。比起引入一整套安全框架代码量小得多逻辑也更直观。4.3 续费接口的事务处理续费是健身房最频繁的操作也是最容易出问题的操作。一次续费涉及的操作包括计算新到期时间、更新会员卡记录、生成订单流水。任何一个步骤失败都会导致数据不一致。比如会员续了一年卡结果到期时间更新了订单流水却因为网络问题没生成财务对账时就会对不上。所以续费接口必须加事务Transactional(rollbackFor Exception.class) public void renewCard(Integer cardId, Integer cardTypeId) { MemberCard card memberCardMapper.selectById(cardId); CardType type cardTypeMapper.selectById(cardTypeId); // 计算新的到期日期或累加剩余次数 card.setEndDate(calculateNewEndDate(card, type)); memberCardMapper.updateById(card); // 生成订单记录 Order order buildOrder(card, type); orderMapper.insert(order); }Transactional保证上面三个操作要么全部成功要么全部回滚。这里有个经验之谈事务一定要加rollbackFor Exception.class否则遇到某些运行时异常外的错误类型时Spring默认不会回滚数据就悄悄坏了。4.4 基础数据和统计报表接口除了日常业务接口统计报表是店长比较关注的部分。我设计了几个聚合查询比如统计每月新增会员数、每月续费收入、各课程预约人数排行。MySQL的DATE_FORMAT和GROUP BY就能搞定不需要额外引入分析数据库SELECT DATE_FORMAT(create_time, %Y-%m) AS month, COUNT(*) AS cnt FROM member WHERE create_time DATE_SUB(CURDATE(), INTERVAL 12 MONTH) GROUP BY DATE_FORMAT(create_time, %Y-%m) ORDER BY month;这类查询虽然不复杂但对于续卡率新增会员趋势这些店长关心的问题是最直接的数据支撑。5. Vue前端设计让店面前台真正愿意上手用5.1 页面结构与路由规划前端页面的规划遵循一个原则把前台操作频率最高的功能放在最容易触达的位置。页面结构如下登录页账号密码登录。主布局左侧菜单栏、顶部用户信息、右侧内容区。会员管理页会员列表、搜索、新增、编辑、续费、卡状态查看。课程管理页课程列表、排课信息、预约情况。预约管理页今日预约列表、到场确认。订单流水页订单列表、支付方式筛选、金额统计。统计报表页会员增长、经营收入图表。路由守卫的逻辑非常简单没有Token就跳转登录页有Token但用户角色不是管理员时拦截对统计报表页的访问。router.beforeEach((to, from, next) { const token localStorage.getItem(token); const role localStorage.getItem(role); if (!token to.path ! /login) { next(/login); } else if (to.path.startsWith(/admin) role ! ADMIN) { next(/dashboard); } else { next(); } });5.2 API请求封装与状态管理前端所有请求统一走封装好的request.js。这个文件负责三件事在请求头自动携带Token、统一处理接口返回的code和message、遇到401时自动清除登录状态并跳转登录页。service.interceptors.response.use( response { const res response.data; if (res.code ! 200) { ElMessage.error(res.message); return Promise.reject(new Error(res.message)); } return res.data; }, error { if (error.response error.response.status 401) { localStorage.removeItem(token); window.location.href /login; } return Promise.reject(error); } );状态管理用了Pinia。对于这套系统全局状态并不复杂只需要共享当前登录用户信息和系统菜单配置所以Pinia比Vuex更轻写法也简洁很多。5.3 表单校验与交互细节做管理系统前端最容易忽略的是表单校验。前台工作人员不一定懂技术误操作是常态。新增会员时手机号格式校验、续费时选择卡种必填校验、预约时选择日期不能早于今天这些校验都能在Element Plus的rules里配置const rules { name: [{ required: true, message: 请输入会员姓名, trigger: blur }], phone: [ { required: true, message: 请输入手机号, trigger: blur }, { pattern: /^1[3-9]\d{9}$/, message: 手机号格式不正确, trigger: blur } ] };还有一个细节是金额显示。订单金额如果不格式化看到的就是一长串数字容易看错。我在前端做了统一的金额格式化函数所有涉及金额的地方都显示为保留两位小数的格式这个细节能让前台每天看数据时舒服很多。6. 项目启动全流程从下载代码到页面正常显示6.1 本地环境准备清单很多人拿到项目第一反应是直接打开IDEA点运行结果报一堆错。环境没准备好项目永远不可能可直接运行。我整理了一份环境清单照着准备就不会卡住依赖项推荐版本备注JDK1.8不要用17以上部分依赖不兼容Maven3.6用于后端依赖下载和打包MySQL8.05.7也可以但建议8.0Node.js16.xVue 3项目构建要求npm/yarn随Node附带用于前端依赖安装IDEA / VSCode任意后端推荐IDEA前端VSCode也行6.2 数据库初始化与环境配置先创建数据库然后导入项目中的sql脚本脚本会创建所有表并插入基础卡种数据和测试账号。我用的是以下命令mysql -u root -p gym_database.sql然后修改后端application.yml中的数据库连接信息spring: datasource: url: jdbc:mysql://localhost:3306/gym_db?useUnicodetruecharacterEncodingutf8useSSLfalseserverTimezoneAsia/Shanghai username: root password: your_password有两个配置项很容易被忽略但缺了就会出问题serverTimezoneAsia/Shanghai不设置的话MySQL与Java的时间对象可能出现8小时时差日期记录全错。characterEncodingutf8不设置的话中文可能出现乱码尤其是会员姓名和地址这类字段。6.3 后端和前端启动步骤后端起服务很简单在项目根目录执行mvn spring-boot:run如果用的是IDEA直接运行主启动类即可。看到Tomcat started on port(s): 8080就说明后端启动成功。前端需要先安装依赖npm install npm run dev默认地址是http://localhost:5173Vite的默认端口。如果后端端口改过需要同步修改前端request.js里的baseURL。6.4 实际部署中踩过的坑第一坑npm安装卡死。原因大多是网络波动或使用了旧的依赖缓存。解决办法是先清除缓存再装npm cache clean --force rm -rf node_modules package-lock.json npm install第二坑MySQL 5.7和8.0的驱动类名差异。老教程里经常用com.mysql.jdbc.Driver而MySQL 8.0要使用com.mysql.cj.jdbc.Driver。如果是按我推荐的8.0版本pom.xml里放的是dependency groupIdmysql/groupId artifactIdmysql-connector-java/artifactId version8.0.33/version /dependency第三坑跨域问题。前端在5173端口后端在8080端口浏览器默认会拦截跨域请求。后端写一个CORS配置类是最省事的方案Configuration public class CorsConfig implements WebMvcConfigurer { Override public void addCorsMappings(CorsRegistry registry) { registry.addMapping(/**) .allowedOriginPatterns(*) .allowedMethods(GET, POST, PUT, DELETE, OPTIONS) .allowedHeaders(*) .allowCredentials(true); } }这三个坑几乎是每个接触前后端分离项目的人都会遇到的处理完之后系统基本就能正常跑起来了。7. 项目验证与实际使用中的优化心得7.1 完整流程自测从办卡到入场系统跑起来之后我做的第一件事不是看代码覆盖率而是模拟一条完整的业务链路来验证系统是否真的能用。我从登录开始先后做了这些操作新增一个会员给会员办一张年卡修改会员联系方式给会员预约一节私教课记录一次入场进行续费操作最后在订单流水中查看对应的两条记录在统计报表中确认数据出现。每一步都通过界面操作完成确认没有报错后才算验收通过。这种全链路验证非常有必要。单元测试只能保证单个方法正确但页面跳转、字段传递、状态更新这些环节只有在真实操作中才能暴露出问题。我就在自测中发现过续费后会员卡状态没有刷新的Bug原因是前端操作成功后没有重新拉取最新数据。7.2 根据实际反馈做的小优化项目上线试运营一段时间后收到最多反馈来自前台基本都是关于操作效率和误导性提示的。有两个优化我觉得值得分享。第一个是搜索功能。会员列表最开始的搜索只有姓名但实际场景中前台往往只记得手机尾号或者记不清名字只记得会员卡号。后来我把搜索条件扩展成同时匹配姓名、手机号、卡号查询效率提升很明显。第二个是到期提醒的展示位置。原来到期提醒是隐藏在一个不起眼的角落里前台很少翻到。后来我在首页加了一个醒目的到期会员列表直接显示未来七天内到期的会员并附上一键拨号按钮。这个改动让续费率有了肉眼可见的提升因为前台愿意主动去看提醒了。7.3 源码里的一些设计习惯说明对于想拿这套系统做二次开发的人有几个设计习惯先说清楚能省去你不少翻代码的时间。所有数据库操作都通过Service层调用Mapper接口Controller层只负责参数接收和结果返回不在Controller里写任何业务逻辑。这样做的好处是续费、退卡这类涉及多表操作的方法逻辑集中在一个地方改起来不用东翻西找。日期统一用java.time.LocalDate和LocalDateTime没有用老旧的Date。前端展示时Element Plus的列格式化工具能直接处理ISO格式的日期不需要额外转换。7.4 后续可以继续扩展的方向这套系统的定位是轻量、可运行但如果实际业务需要有几个扩展方向是很自然的。第一个是短信提醒。到期会员提醒现在只停留在站内展示结合短信服务商API后可以实现快到期的自动短信问候。这是投入产出比很高的功能很多健身房愿意为此付费。第二个是微信小程序端让会员自己在手机上查看卡剩余次数、预约课程。这就需要在后端增开一套小程序专用接口并引入微信登录流程。第三个是数据报表增强。目前统计报表只是基础的趋势图可以继续增加续卡率会员流失预警课程坪效这些经营分析维度。这些指标对店长做决策很有参考价值但需要更复杂的SQL和图表交互。写在最后的体会做完这套系统我最深的体会是项目能跑和好用之间隔着很长的距离。技术选型只是基础真正的功夫花在业务逻辑的理解和数据关系的梳理上。对于没有接触过前后端分离项目的人来说拿一套结构清晰的代码去读、去改、去扩展比从零开始要快得多。如果你打算拿它做学习参考建议先完整走一遍我前面说的全链路自测然后挑一个模块自己动手改一改比如给会员表加一个紧急联系人字段从数据库到后端接口再到前端页面完整走一遍这一套下来你对SpringBoot和Vue的协作模式会有非常直观的理解。希望这篇文章对正在做管理系统开发的朋友有点实际帮助。
返回列表