基于SpringBoot+Vue的健身房会员管理系统:从需求到部署实战
发布时间:2026/10/11 14:01:06 锦皓数字建站

做健身房管理系统这个项目之前我其实已经帮朋友的小型健身工作室手工处理过会员台账用Excel记会员卡片信息、课程消耗、到期时间说实话每天都在跟“这卡到底哪天到期”“这节私教课用了没有”这类问题搏斗。后来有一个某健身房老板找到我说要上一套能管住会员、私教、团课、教练排班和卡项续费的管理系统我综合评估之后选了SpringBootVue这个组合数据库用MySQL持久层用MyBatis。这套方案在网上被叫“Java全栈毕设”已经很多年了但它被做成毕业设计不代表它不实用。我恰恰认为它能长期成为教学最常选的项目是因为它覆盖了绝大多数业务系统都会碰到的完整闭环要登录鉴权、要多角色权限、要处理业务状态流转、要画数据图表、要做到前后端分离部署。这篇内容就是围绕这个系统从需求拆解、数据库建模、后端接口设计、前端页面开发到运行部署踩坑的全过程展开给正准备复刻这个项目、或者想拿它做二次开发的读者一条能直接照着走的路。我尽量把每一步“为什么这么做”也讲清楚而不只是贴一堆代码让你自己猜。1. 健身房的“管理混乱”到底乱在哪从会员卡到私教课1.1 传统手工记录无法回答的三个问题我调研那家工作室时前台小姐姐给我看了一抽屉会员卡登记表其中有做的比较好的也有完全空白的、记录涂改到看不清日期。我总结了这类场景下最典型的三个问题第一个问题是“会员卡到期判断靠人工算”。月卡、季卡、年卡、次卡混在一起不同会员还有不同的赠送天数、请假冻结规则。前台每天都要翻本子今天谁到期、谁该提醒续费靠人工抽空翻一遍漏掉是必然的。第二个问题是“私教课次数到底还有几次”。很多工作室的私教课是打包卖的比如买20节送2节每上一次就要划掉一节。一旦教练和前台记录不同步会员说“我明明还剩3节”前台说“系统显示你只有1节”纠纷就这么来了。第三个问题是“教练排班和约课冲突”。团课约满了、私教时间重叠了都靠微信群里吼效率极低还会得罪会员。这三点放在系统里其实都是很标准的功能点会员卡状态管理、私教课余量管理、约课排课和冲突检测。你与其去网上搜那些包装得很玄的“行业解决方案”不如先把这三个场景吃透后面开发起来思路会非常顺。1.2 从需求到功能模块的拆分技术开发不能一上来就建库写接口先把角色和用例理清。这套系统我打成四个角色超级管理员、前台/运营、教练、会员。四个角色对应四类不同查看和操作权限角色核心权限对应前端界面超级管理员员工账号管理、全部门店数据、系统配置、续费审核管理后台前台/运营会员开卡/续费/换卡、课程排期、约课记录查询、收银记录运营工作台教练我的排班、我的私教课列表、上课确认扣次教练端会员个人卡信息、剩余次数、课程表、在线预约/取消会员端模块拆分上我按业务域划分成会员管理、卡种管理、私教课管理、团课排课、教练管理、预约管理、收银记录、数据统计、系统登录与权限管理。听起来很多但实际上数据库表只有十几张后端接口几十个对一个中小型工作室来说完全够用。这里要提醒一句做系统最怕一开始就想做“大而全”健身房管理系统的核心永远是“卡、课、人、钱”四个字其他功能都可以往这四个字上靠。2. 为什么是SpringBootVue选型背后的真实逻辑2.1 后端框架对比与MyBatis的取舍后端框架我在SpringBoot之间几乎没犹豫过因为它的自动配置和生态对单人开发太友好了。真正纠结的是持久层框架MyBatis还是MyBatis-Plus或者直接用Spring Data JPA。很多教程喜欢用JPA因为它不需要写SQL定义一个实体类就能自动建表、自动CRUD。但我最终选了MyBatis原因很实在第一健身管理系统里有大量报表统计、多表联查比如“本月私教课消耗排名”“团课预约人数汇总”这类复杂查询用JPA的Specification写起来极其难受而MyBatis直接写XML里的SQL一眼就能看懂。第二当系统后期因为业务调整比如卡种新增了冻结天数字段要修改SQL时你定位和修改的成本会低很多。不过我也会在Mapper层配合MyBatis-Plus自带的基础方法用于普通的单表增删改查。这个组合相当于“简单操作不手写SQL复杂操作全手工SQL”在开发效率上是一种折中方案也是很多企业项目的真实做法。2.2 前端Vue生态配合与API风格统一前端我用的是Vue Element UI Axios ECharts这组常见搭档。Vue作为渐进式框架上手成本低数据驱动的思路对需要频繁刷新会员卡状态、课表余量的页面非常合适。Element UI则是后台管理的“粮食”表格、弹窗、表单、日期选择器都是现成的不用自己折腾CSS。接口设计方面我全部采用RESTful风格资源用名词操作用HTTP动词。例如POST /api/auth/login登录GET /api/member/page分页查询会员POST /api/member新增会员PUT /api/member/{id}/renew会员续费GET /api/course/timetable获取课程表POST /api/course/booking预约课程GET /api/dashboard/stats首页统计数据统一了接口风格之后前端可以封装一个通用的Request工具把所有响应包在约定好的结构里。我的约定是{ code: 200, message: success, data: { } }前端Axios拦截器统一处理code非200直接弹错误提示200就返回data。这样一来后续新增接口的时候前后端配合成本被压到最低。2.3 MySQL选型与单表自增ID的理由数据库就是MySQL没有太多悬念。MySQL在中小型业务量下性能足够且大家最熟悉出了问题也最容易查资料和请人帮忙。字符集统一用utf8mb4因为如果以后要存用户的特殊表情utf8是不够用的。这个细节在开发阶段测不出来但等真上线跑了一段时间再想改字符集就非常痛苦。ID生成策略我全部用了数据库自增而不是雪花ID。原因也很简单单机部署、数据量不大、不需要跨库分表自增ID对前端调试更友好。雪花ID适合高并发分布式场景但不是所有系统都配得上这套复杂度别为了所谓“规范”给自己增加无意义的代码量。顺带说一句时间字段我统一用datetime存储凡是涉及到到期时间的业务字段比如卡到期时间一定不要用字符串。字符串比较“2026-01-01”比“2025-12-30”要大这个字典序碰巧正确但一旦时间格式不统一比如出现2026-1-1整个逻辑就会出问题。时间就用数据库时间类型代码里用Java的LocalDateTime处理这是教训换来的经验。3. 数据库建模的核心细节把会员、卡种、私教课连成一张网3.1 主力表结构与字段设计思路整套系统的表我按“人、卡、课、钱”四条线拆分最核心的几张表设计大概是下面这种思路。会员表memberCREATE TABLE member ( id bigint NOT NULL AUTO_INCREMENT COMMENT 主键, member_no varchar(32) NOT NULL COMMENT 会员编号业务展示用, name varchar(50) NOT NULL COMMENT 姓名, phone varchar(20) NOT NULL COMMENT 手机号, gender tinyint DEFAULT 0 COMMENT 0未知 1男 2女, card_type_id bigint DEFAULT NULL COMMENT 当前卡种ID, card_start_time datetime DEFAULT NULL COMMENT 当前卡片开始时间, card_end_time datetime DEFAULT NULL COMMENT 当前卡片到期时间, remain_count int DEFAULT 0 COMMENT 剩余次卡次数, status tinyint DEFAULT 0 COMMENT 0正常 1冻结 2过期, remark varchar(255) DEFAULT NULL, create_time datetime DEFAULT NULL, update_time datetime DEFAULT NULL, PRIMARY KEY (id), UNIQUE KEY uk_phone (phone) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT会员表;这里要特别强调设计上的一个思路会员当前卡信息是冗余在会员表上的而不是只靠会员卡记录表去联查。因为前端列表页、首页统计、到期提醒都频繁需要“这个会员现在是什么状态”如果每次都去子表里取出当前有效那张卡查询会变复杂。冗余带来的问题只需要用“更新时保证一致性”来解决也就是说所有开卡、续费、换卡操作都走同一个后端Service避免多处绕开逻辑直接改库。卡种表card_type核心字段就简单了卡名称、卡类型时长卡还是次卡、有效时长天数、总次数、价格、续费是否继承剩余天数。私教课记录表personal_course_record我建议这样设计idmember_id 会员IDcoach_id 教练IDcourse_name 课程名称total_count 总购买节数used_count 已上节数surplus_count 剩余节数buy_time 购买时间status 0进行中 1已完结 2已退款私教课次数我不会设计成“每次上课生成一条消费记录”而是用“总量已用”两个字段维护每次扣次时更新used_count。这样前台查询剩余次数就是一条SQL的事而上课明细可以用另一张course_consume_record表记录历史。预约表booking_record则用于存放团课和私教的预约记录关键字段有member_id、schedule_id、booking_type1团课 2私教、status1已预约 2已取消 3已完成 4爽约。每次预约都要做冲突检测比如同一会员同一时间段不能同时预约两节课。3.2 卡到期判断与状态计算逻辑会员表里的status我存的是冗余计算结果但真正判断是否到期的逻辑要写在Java代码里而不是靠定时任务去批量更新。原因是健身房的卡状态可能是“动态的”一个会员今天到期了明天老板给他做了续费状态就要立刻恢复。我的做法是写一个统一的状态计算器查询会员的card_end_time和remain_count再结合当前时间得出该会员最真实的状态。列表页查询时虽然读的是member.status但在保存之前所有写操作都会调用这个计算器重新计算一遍。这样既有查询性能又保证了业务准确性。判断逻辑核心代码大概是public MemberStatus calculateStatus(MemberDO member, LocalDateTime now) { // 冻结优先判断 if (member.getStatus().equals(MemberStatus.FROZEN.getCode())) { return MemberStatus.FROZEN; } LocalDateTime endTime member.getCardEndTime(); if (endTime ! null endTime.isBefore(now)) { return MemberStatus.EXPIRED; } // 次卡判定卡未过期但次数已经用完业务上算“待续费”不归为过期 if (member.getRemainCount() ! null member.getRemainCount() 0) { return MemberStatus.COUNT_USED_UP; } return MemberStatus.NORMAL; }这里我特意把“次数用完”和“时间到期”做了区分很多小系统在这点上偷懒直接把次卡用完也算过期结果会员用次卡续费时还要先解冻、再续费操作上非常绕。4. 后端接口设计的几个关键点从登录鉴权到并发扣次4.1 登录鉴权与多角色权限控制登录这块直接用JWT简单又不失体面。用户表存管理员、前台、教练三类账号会员虽然也能登录但我不让会员和员工共用一张表。原因很简单员工有后台权限会员只有自己的H5/普通页面权限二者字段差异很大。把会员登录单独用member表做员工的登录用sys_user表做。JWT令牌我放在请求头Authorization里后端用一个拦截器统一解析。拦截器从Token里读出userId、userType、roleType放进ThreadLocal后续Controller直接用CurrentUserHolder.getUser()就能拿到当前登录人的信息避免每个接口都重复解析Token。角色的权限控制我用了一个简单的内存权限表每种角色对应一组允许访问的接口前缀。比如教练角色的权限列表是/api/course/coach/**、/api/member/coach/**这样我在拦截器里做一次URL前缀匹配就能完成大部分权限控制。不用上Spring Security那套复杂配置对单人开发、中小项目来说够用且好维护。4.2 会员续费的业务规则是个“最容易写错”的地方续费是这个系统里业务规则最密集的接口也是我认为最需要仔细讲清楚的部分。它不能简单做成“往卡种表对应的到期时间上加一段天数”。我遇到的真实场景有老会员还剩10天到期现在续费一年要不要给他累加这10天大多数情况下应该累加是你的权益不能坑老会员。老会员已经过期30天现在来续费一年的卡是否要从今天开始算并补齐过期期间的福利一般不应该过期期间本来就不能享受权益。次卡续费是不是直接增加剩余次数通常是但如果你同时允许“升级卡种”旧卡的剩余次数怎么折算我把续费逻辑收敛成下面几步查询会员当前卡状态判断当前是否过期。未过期则新到期时间当前到期时间新卡天数已过期则新到期时间当前时间新卡天数如果是次卡续费新次数当前剩余次数新卡次数计算完新到期时间和新次数后更新member表和生成一条recharge_record收银记录如果业务要求开新发票或打印小票把收银记录ID带出去。整个更新过程放在一个Transactional事务里避免出现会员表更新成功、收银记录插入失败这种不一致情况。实测里最常出现的bug就是忘记加事务开发时单接口测试看不出问题并发或者断电情况下一旦失败就是脏数据。4.3 私教课约课与并发扣次的处理约课模块在后端接口里看起来只是“插入一条预约记录”但有一个很隐蔽的坑同一个教练的同一个时间段两个会员同时提交预约两边都查询“该时段无预约”然后都插入成功导致排班冲突。这是典型的并发覆盖问题。解法上最稳妥的不是Java代码加synchronized而是利用数据库的唯一约束。我在schedule表字段上加唯一索引比如uk_coach_time(coach_id, start_time, end_time)当两个人同时插入相同时间段时数据库会直接报“Duplicate entry”错误后端捕获这个异常并转成友好的提示“该时段已被预约”。这个方案比代码层的锁靠谱得多因为即使以后你部署多个后端实例数据库约束依然有效。如果暂时不想动表结构也可以在约课记录表中添加coach_id booking_time的唯一索引效果类似。我把这个经验写成了一段备注放在项目README里后续接手的人不会踩同样的坑。5. Vue前端从零搭建的经验权限路由、动态菜单与图表展示5.1 登录状态管理与路由权限拦截Vue前端最基础的工程结构我就不多说了重点是前端也要有“权限意识”。我把菜单和路由权限做成“按角色动态生成”的方式登录成功后后端返回当前用户的角色标识和允许访问的菜单列表。前端将菜单列表存到Vuex里用router.addRoute动态加入对应路由。比如教练登录后菜单只显示“我的排班”“我的会员”“上课记录”超级管理员登录后菜单才有“员工管理”“系统配置”。这么做有一个额外好处前端菜单不用在不同角色之间用v-if去堆权限判断代码看起来非常干净。路由守卫里还要判断Token是否存在以及当前路由是否需要权限。我封装了一个whiteList登录页、忘记密码等无需登录的页面不在白名单里的路由统一走校验。很简单的代码但能避免“随便改URL就进入后台页面”这种低级安全问题。5.2 会员看板卡片状态、续费提醒和图表数据绑定前端首页我做了三个卡片区域今日到期人数、本月新增会员数、本月营业额下方放了一张近30天会员增长与营业额折线图。数据都来自/api/dashboard/stats接口后端一次性聚合返回前端只需要把ECharts配置好。这里有个小经验ECharts的option最好用methods里的函数生成而不是直接写在data里。因为图表数据是从接口异步加载的如果你把series写死到data里等数据回来再setOption会需要做额外的深拷贝处理用函数生成则每一次setOption都是全新对象不会出现图表内存残留、option数据叠加的问题。续费提醒列表则直接对接会员分页接口的status字段。前端拿到“即将到期”“次数不足”“已过期”三类会员后用Tabs标签页分别展示操作按钮分别是拨打电话、一键续费、去编辑。续费按钮点击后弹出续费对话框选择卡种后台自动计算新到期时间前端只需要实时展示会员当前的剩余天数作为参考价格由卡种决定。整个过程既符合业务也不会让前端代码写得太重。5.3 Axios封装与前后端联调时的细节Axios封装这块我想重点说一个容易忽略的点URL的baseURL要区分开发环境和打包环境。我用的方式是const service axios.create({ baseURL: process.env.VUE_APP_BASE_API, timeout: 10000 })在开发环境的.env.development里把VUE_APP_BASE_API配成http://localhost:8080/api同时给Vue CLI配置devServer的proxy代理把前端3000端口转发到后端8080端口从而解决开发时的跨域问题。部署到生产环境后Nginx作为反向代理把/api开头的请求转发到后端服务前端本身不感知环境变化。这个方案被团队用了很多次属于稳定可复用的联调模板。上传接口的个人经验是别放到业务请求的同一份Axios拦截逻辑里处理因为上传文件通常需要单独的进度回调而且后端往往要求multipart格式和JSON请求的处理逻辑不一样。我单独封装了uploadRequest用原生的XMLHttpRequest再配进度条。6. 完整运行部署指南与常见坑6.1 本地环境运行步骤一个SpringBootVue前后端分离项目第一次跑起来的步骤其实很固定我把清单整理出来免得每次都要回忆安装JDK 1.8或更高版本、Maven 3.6、MySQL 5.7、Node.js 14。创建数据库编码选utf8mb4然后执行项目里提供的init.sql初始化脚本。修改后端配置文件application.yml把数据库连接地址、账号密码改成自己的。在后端项目根目录执行mvn spring-boot:run或直接运行main启动类确认控制台出现“Started Application”字样。前端项目目录执行npm install安装依赖然后执行npm run serve。浏览器访问Vue的本地端口默认账号密码看初始化脚本里的sys_user表。这里有个非常容易卡住的点npm install装了特别久或者报错。多数情况是因为网络原因或者Node版本和依赖不兼容。我的做法是优先使用Node的LTS版本然后在项目里锁package.json版本不要一个“^”符号让依赖随机浮动。如果装到某个包老报错去清掉node_modules重装一次比反复分析错误日志更省时间。6.2 部署到Linux服务器的注意点部署环节我会把前后端分开放。后端打成jar包用nohup java -jar gym-admin.jar --spring.profiles.activeprod启动前端npm run build后生成dist目录丢给Nginx托管。Nginx的关键配置片段大概是server { listen 80; server_name gym.example.com; root /www/gym-front/dist; index index.html; location /api/ { proxy_pass http://127.0.0.1:8080/api/; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; } location / { try_files $uri $uri/ /index.html; } }try_files那行一定要有因为Vue是SPA单页应用前端路由切换时如果刷新页面Nginx如果不把请求重写到index.html就会出现“刷新后404”的问题。这个问题在本地开发时不存在因为Vue CLI的devServer已经处理了history回退所以很多人要到部署阶段才遇到。另外一个坑是后端服务启动后过一会儿自己退出了多半是因为启动命令没有加日志重定向进程收到后台运行信号后直接把输出写到挂掉的终端。正确做法nohup java -jar gym-admin.jar gym.log 21 systemd管理方式适合更正式的部署我实际项目里就用了systemd的service文件管理jar包方便开机自启和看日志。7. 我开发完这个项目之后的三点心得做完这个健身房管理系统回头看最有价值的不是某段代码能跑而是对整个业务系统的开发节奏有了更实在的体感。第一是数据库设计阶段多花两小时后面能省两天。我第一版私教课表就只设计了“剩余次数”一个字段没有购买总节数结果每次想看会员历史买过多少课都要去翻收银表后来还是老老实实补了total_count。建模时把“业务查询的最小数据单元”想清楚比想着怎么套更先进的设计模式重要得多。第二是不要把前端当傻瓜但一定要把前端当不可信的人。后端必须自己做所有权限校验和业务状态校验前端隐藏按钮只能算体验优化不是安全措施。我刚做完系统时让朋友做了一次“绕过前端”的入侵测试他能直接通过浏览器控制台调用后端接口给会员延期这就是典型的后端接口没有重复校验身份的后果。第三是这个系统最值得扩展的方向不是加更多花哨功能而是把预约提醒做到位。比如会员预约成功后发送通知模板、到期前三天自动发送续费提醒这种“系统主动触达”的能力比做一个看起来很漂亮的数据大屏更能让健身房老板感知到系统的价值。我目前这个版本用的是简单的短信网关配置接口如果你接手这个项目我建议从消息通知这个方向继续迭代投入产出比最高。项目做完了源码和文档也整理得差不多了。如果你想把它跑起来或者打算在此基础上改成自己门店的流程建议先别急着删表把会员卡状态计算逻辑和约课冲突处理这两块读透其他的都只是增删改查。
锦
锦皓数字建站
深耕本土企业品牌数字化升级,专注原创端正雅致商务官网,从视觉设计到稳定运维全程保驾护航。