ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Spring Boot + Vue健身房管理系统实战:从数据库设计到部署上线

Spring Boot + Vue健身房管理系统实战:从数据库设计到部署上线 开门见山说个事前段时间帮朋友健身房做的那套管理系统前后大概折腾了一个多月从数据库设计到前后端联调再到部署上线踩的坑比想象中多。这套系统的技术栈就是标题里写的Spring Boot Vue功能上覆盖了会员管理、课程预约、教练排班、签到统计这些健身房日常运营的核心场景。写这篇东西不是为了秀技术就是想把这个项目从设计到落地的完整思路和实操过程记录下来给准备做类似管理系统的朋友一个可以抄作业的参考。这篇文章适合正在做毕业设计、课程设计或者想自己搞一套前后端分离项目练手的人读不管你是后端为主还是前端为主都能找到自己能复现的部分。1. 项目概述与需求拆解1.1 系统定位与核心功能健身管理系统这名字听着大但落到实际业务上核心就是解决健身房日常运营里这些事谁来上课、谁买了卡、卡什么时候到期、教练的时间怎么排、会员约了课没来怎么处理。说白了这套系统的本质是业务管理系统不是电商系统也不是社交平台所有功能都围绕人、卡、课、预约四个核心对象展开。我做的这套系统最终定了四个角色管理员、前台、教练、会员。管理员负责全局配置和统计报表前台负责会员办卡和签到操作教练管理自己的课程和学员会员则是通过前端页面完成注册、选课、预约、查看记录这些自助操作。功能模块上我把系统拆成了这样几块会员管理会员信息的增删改查、会员卡类型月卡、季卡、年卡管理、卡到期提醒。课程管理团操课程的发布、课程时间表、每节课的可预约人数、课程状态可预约/已满/已结束。教练管理教练基本信息、教练负责的课程列表、教练排班。预约管理会员预约课程、取消预约、签到记录、爽约标记。统计报表每日/每周的预约量、签到率、热门课程排行。公告通知运营公告的发布与展示。这套系统做下来最核心的业务闭环是会员注册 → 查看课程表 → 预约课程 → 到店签到 → 教练核销。把这条链路跑通系统的主干就算立住了。1.2 为什么选Spring Boot Vue这套组合技术选型这个话题每次都能吵半天。我直接说我的结论Spring Boot Vue是目前做前后端分离项目最稳的组合没有之一。这不是因为它最先进而是因为它最适合这类中小型业务系统的开发节奏。后端用Spring Boot理由很实在。第一生态成熟MyBatis Plus、Spring Security、Redis这些组件都有非常完善的中文文档和社区解决方案踩了坑基本都能搜到答案。第二开发效率高Spring Boot的自动配置和starter机制把大量繁琐的配置工作省掉了一个main方法就能把服务跑起来对中小项目和单人开发尤其友好。第三Java本身的类型系统和Spring的依赖注入让后期维护相对容易一个项目写完之后半年再回来看代码还是能读懂的。前端用Vue理由同样直接。Vue的学习曲线比React平缓模板语法直观组件化开发模式适合这种以表单和表格为主的管理系统页面。配合Element Plus组件库后台管理类页面的开发速度非常快。Vue Router负责路由跳转Pinia管状态Axios发请求这几件套组合起来前端开发的工作量控制得很低。不过这里要提醒一点选Vue3还是Vue2这得想清楚。如果是从零开始的新项目直接上Vue3 Vite Element Plus。Vue2已经停止维护了新项目没必要再用组合式API都别扭的老版本。我这套系统就是用Vue3写的后面所有代码示例都是Vue3的写法。2. 数据库设计与后端核心实现2.1 数据库表结构设计思路数据库是这个系统的地基表设计的好坏直接决定后面写业务代码的体验。我设计表的时候遵循了一个原则按业务对象拆表用业务ID关联不做冗余字段。核心表一共有六张。会员表member的字段包括id主键、username、passwordBCrypt加密后存储、real_name、phone、gender、card_type月卡/季卡/年卡、card_start_date、card_end_date、status正常/冻结、create_time。这里有个很重要的设计细节会员状态和卡到期时间必须独立存字段不要通过计算去判断因为定时任务和查询条件都要用到这两个字段冗余一点换来查询方便是值得的。课程表course的字段包括id、course_name、coach_id关联教练表、start_time、end_time、max_people最大预约人数、booked_count已预约人数、status可预约/已满/已结束、intro课程简介。预约人数的控制是这个表的关键逻辑我后面在预约模块会详细讲到并发控制的问题。预约表booking的字段包括id、member_id、course_id、booking_time、status已预约/已签到/已取消/爽约、check_in_time。这张表是业务发生最频繁的表所以索引要建好member_id和course_id都要加索引。教练表coach、公告表announcement、签到表check_in相对简单不展开写了。另外提醒一句如果你用的是MySQL所有表的引擎都用InnoDB字符集用utf8mb4这样能避免很多中文乱码和事务支持的坑。2.2 Spring Boot项目分层结构项目结构上我采用了最标准的四层架构controller接口层→ service业务层→ mapper数据访问层→ entity实体类。这是Spring Boot项目最经典的分层方式每一层只做自己该做的事职责边界清晰后面维护的时候找代码非常快。一个值得参考的包结构是这样的com.example.fitness ├── controller/ # 接口层 ├── service/ # 业务层 │ ├── impl/ ├── mapper/ # MyBatis数据访问接口 ├── entity/ # 数据库实体 ├── dto/ # 请求/响应对象 ├── config/ # 配置类跨域、拦截器、WebMvc等 ├── common/ # 统一返回结果、异常处理、工具类 ├── security/ # JWT鉴权相关 └── FitnessApplication.java分层设计有个很容易被忽略的坑很多人在service层直接返回entity实体这是不对的。实体类是数据库的映射不该直接把数据库字段暴露给前端。我习惯单独建一个dto包接口的入参和出参都用dto对象比如返回会员列表的时候用MemberDTO把必要的字段拼装好再返回。这样做的最大好处是数据库表结构变化时接口的返回结构可以保持稳定前端不会因为后端改了表结构就崩。再说说统一返回结果。我建了一个Result类固定结构是{code: 200, message: success, data: {}}所有接口都返回这个格式。这样前端拦截器只需要判断code就知道请求是否成功不用每个接口单独去解析。这个习惯看着简单但实际用起来非常爽尤其是前后端联调的时候不会出现这个接口为什么返回的字段名不一样这种问题。2.3 JWT登录鉴权的实现方式管理系统里登录鉴权必须做不做的话任何人都能调用接口把会员数据删了。我用的方案是JWTJSON Web Token。这里说下整体设计流程。用户登录成功后后端生成一个有效期为2小时的token返回给前端。前端把token存在localStorage里每次请求Axios拦截器把它加到请求头。后端写了一个拦截器拦下所有非登录接口的请求解析token并把用户信息放到ThreadLocal里这样后面每次业务操作都能拿到当前登录人的信息。生成token的核心代码大概是这样的String token Jwts.builder() .setSubject(userId.toString()) .claim(role, role) .setExpiration(new Date(System.currentTimeMillis() 7200000)) .signWith(SignatureAlgorithm.HS256, secretKey) .compact();拦截器里解析token的过程就不贴完整代码了有一个经验值得分享解析token异常要分类型处理。token过期和token被篡改返回的错误信息不应该一样前者提示登录已过期请重新登录并跳转登录页后者提示登录状态异常并清理本地存储。这个细节直接影响用户体验我见过很多项目在这块做得比较粗糙用户被卡在一个没有明确提示的报错里。权限控制方面我用的是角色判断。接口注解上加一个自定义的RequireRole(admin)拦截器里判断当前登录人的角色是否有权限访问。不需要引入Spring Security这种重框架因为系统角色只有四种简单的角色判断足够用引入重框架反而增加学习和调试成本。3. 前端Vue页面设计与API对接3.1 前端技术栈与目录结构前端这边用的组合是Vue3 Vite Vue Router Pinia Axios Element Plus。没有用TypeScript原因很实际团队和后续接手的人都更熟JavaScript而且这套系统的类型复杂度不高TS的收益有限。不过如果你打算长期维护并且团队水平可以上TS是加分项。目录结构按功能划分推荐这样组织src/ ├── api/ # 接口请求定义 ├── assets/ # 静态资源 ├── components/ # 公共组件 ├── router/ # 路由配置 ├── store/ # 全局状态Pinia ├── utils/ # 工具函数request封装等 ├── views/ # 页面组件 │ ├── member/ # 会员管理 │ ├── course/ # 课程管理 │ ├── booking/ # 预约管理 │ ├── dashboard/ # 统计报表 │ └── login.vue # 登录页 └── App.vueapi目录下每个模块一个文件比如member.js、course.js、booking.js每个文件导出对应模块的接口函数。这样做的好处是页面组件里不直接写请求URL所有接口统一定义后端接口路径变了只需要改一个文件。3.2 Axios封装的关键点Axios封装是前端项目的重中之重代码量不大但设计得好不好直接决定开发效率和联调体验。我的request工具类做了三件事。第一请求拦截器从localStorage里取token有就加到请求头的Authorization字段没有就放行。第二响应拦截器解包统一返回结果里的codecode为200就返回data部分业务代码里直接拿到业务数据不用每个页面再解一层。code为401就清空本地存储、跳转登录页。第三错误统一弹提示网络错误、服务器错误统一用Element Plus的Message组件提示页面里不做重复的错误处理逻辑。// utils/request.js 核心结构 import axios from axios import { ElMessage } from element-plus 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) { return res.data } if (res.code 401) { localStorage.clear() window.location.href /login } ElMessage.error(res.message) return Promise.reject(new Error(res.message)) }, error { ElMessage.error(网络请求失败请稍后重试) return Promise.reject(error) } ) export default service这样封装完后页面里调接口就是非常简洁的写法比如会员列表import { listMember } from /api/member const getList async () { const data await listMember({ page: currentPage.value, size: 10 }) tableData.value data.records total.value data.total }3.3 路由守卫与权限控制路由层面我用Vue Router的全局前置守卫做登录拦截。逻辑很简单访问任何一个页面之前检查有没有token没有就重定向到登录页。登录之后就放行。管理员和会员看到的页面不同这件事我用了动态路由的思路处理路由表分成两部分公共路由直接注册权限路由在登录后根据角色动态添加。动态路由这个功能在管理系统里很实用但实现起来有个坑路由动态添加后退出登录再切换账号之前添加的路由不会自动清除。解决方法是退出登录时调用router.options.routes重置路由或者直接用重定向刷新页面让路由表重建。我踩过这个坑用后者的方式解决了简单可靠。Pinia在这里主要存两类全局信息当前登录用户的基本信息用户名、角色、头像和菜单的展开收起状态。用户信息在登录接口返回后存入store退出登录时清空。整个项目用到的全局状态不多Pinia的模块拆成user.js和app.js两个就够了。4. 核心模块实操课程预约全流程实现4.1 业务规则与流程分析课程预约是这套系统里业务逻辑最复杂的模块也是并发压力最大的模块。如果预约功能做不好整个系统在真实场景下是扛不住的。预约的业务规则我梳理出了四条会员必须登录且会员卡未到期才能预约。同一节课一个会员只能预约一次。课程状态为可预约才能被预约已满或已结束不能预约。取消预约后该会员可以重新预约同一节课如果还有名额。整个流程文字描述是这样会员进入课程列表页看到所有发布日期在当天及之后的课程每门课程卡片上显示剩余名额。点击预约按钮前端弹出确认框确认后请求后端预约接口。后端校验通过后写入预约记录同时课程表的已预约人数加一。页面刷新后课程的剩余名额减少。4.2 后端接口实现与并发控制预约接口的URL是POST /api/booking请求体参数是{courseId: 1, memberId: 5}。Service层的bookCourse方法逻辑分五步查会员卡状态、查课程当前状态、查是否已预约、判断是否约满、插入预约记录并更新已约人数。这里最大的坑是并发问题两个会员同时点预约恰好都查到了剩余名额1个然后都执行插入最后一节课超员了。解决办法我用了数据库层面的事务加悲观锁。查询课程记录时加上SELECT ... FOR UPDATE行级锁锁住课程行这样同一时间只有一个请求能执行后面的预约逻辑。Transactional public void bookCourse(Long memberId, Long courseId) { Member member memberMapper.selectById(memberId); if (member null || member.getStatus() ! 1 || member.getCardEndDate().before(new Date())) { throw new BizException(会员卡状态异常无法预约); } Course course courseMapper.selectByIdForUpdate(courseId); if (course null || course.getStatus() ! 0) { throw new BizException(该课程当前不可预约); } int count bookingMapper.countByMemberAndCourse(memberId, courseId, 已预约); if (count 0) { throw new BizException(您已预约该课程请勿重复预约); } if (course.getBookedCount() course.getMaxPeople()) { throw new BizException(该课程名额已满); } Booking booking new Booking(); booking.setMemberId(memberId); booking.setCourseId(courseId); booking.setStatus(已预约); booking.setBookingTime(new Date()); bookingMapper.insert(booking); course.setBookedCount(course.getBookedCount() 1); courseMapper.updateById(course); }这段代码有三个要点。一是Transactional必须加事务保证插入预约记录和更新课程人数是原子操作任何一个失败都会回滚。二是selectByIdForUpdate这个方法是自己在Mapper里写的加了FOR UPDATE关键字不是MyBatis Plus自动生成的。三是业务异常要主动抛出来让全局异常处理器统一包装成Result返回不要在Service里自己try-catch吞掉。4.3 前端页面交互实现前端课程列表页我用了Element Plus的Card组件展示课程卡片每张卡片显示课程名、教练、时间、剩余名额底部是预约按钮。剩余名额为0时按钮禁用已经预约过的课程按钮文案变成已预约且不可再点。预约按钮的点击处理逻辑是const handleBook async (courseId) { try { await createBooking({ memberId: userStore.userInfo.id, courseId }) ElMessage.success(预约成功) await getCourseList() // 刷新列表 } catch (e) { // 错误提示已经在拦截器里统一处理了 } }这里有个易踩的坑预约成功后一定要刷新课程列表但不要整页刷新调用getCourseList重新拉数据就好。因为课程卡片上的剩余名额是列表数据里的字段不重新拉数据的话前端显示的剩余名额还是之前的用户连续预约两节不同的课会看到名额不对。曾经为了省事直接用location.reload()结果弹窗消失、页面闪烁体验非常差。4.4 联调阶段几个容易踩的坑前后端联调是整个过程里最容易出问题的时候我发现的问题多数集中在三个地方。第一是日期格式。后端LocalDateTime默认序列化出来的格式是2024-11-20T14:30:00带一个T前端如果想显示成2024-11-20 14:30要么后端统一配置Jackson的日期格式要么前端做格式化处理。我建议后端直接配置全局生效省得前端每处都处理。第二是字段命名风格。后端Java习惯用驼峰命名createTime数据库习惯用下划线create_timeMyBatis Plus开了驼峰映射之后没问题。但前端联调时看返回的JSON字段是驼峰还是下划线必须前后端统一确认好不然就会出现页面拿到的是undefined这种经典问题。第三是跨域。前后端分离开发模式下前端跑在5173端口后端跑在8080端口必然触发跨域。我在后端写了一个CorsConfig配置类允许指定的前端源地址跨域。上线部署后跨域配置还不能删因为Nginx反向代理的配置方式不同这步我在后面部署章节专门讲。5. 部署上线与常见问题排查5.1 前后端打包与部署方案系统开发完成后部署上线这里分享一套我验证过很多次的方案。前端用Nginx做静态文件服务器和反向代理后端的Spring Boot应用打成jar包直接跑。前端打包执行npm run build打包产物在dist目录。把dist目录上传到服务器任意位置然后配置Nginx。后端打包执行mvn clean package -DskipTests生成jar包后用nohup java -jar fitness-system.jar logs/fitness.log 21 启动。生产环境建议用systemd管理进程这样重启方便还能开机自启。Nginx配置有两个关键点一是前端history路由模式必须配置try_files不然刷新页面会404二是/api路径要反向代理到后端端口。server { listen 80; server_name your-domain.com; root /www/fitness-front; index index.html; location / { try_files $uri $uri/ /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; } }try_files $uri $uri/ /index.html这行是必须的。Vue Router默认使用history模式URL是/member/list这种真实路径刷新时浏览器会向Nginx发起这个路径的请求Nginx找不到对应的静态文件就会404。try_files把所有未知路径回退到index.html再由前端路由接管页面就能正常显示了。5.2 常见问题速查表做这套系统的过程中我整理了一张问题排查表都是实际遇到过的按照从高频到低频排列。问题现象可能原因解决方法前端请求接口报403/跨域Nginx没配代理或后端跨域配置丢失检查Nginx的location /api配置确认proxy_pass指向后端刷新页面404路由history模式缺少try_files配置在Nginx location / 里加try_files $uri $uri/ /index.html预约接口数据不一致并发场景下没有用悲观锁或事务确认查询课程用了FOR UPDATE锁方法加了Transactional中文乱码数据库字符集不是utf8mb4ALTER TABLE xxx CONVERT TO CHARACTER SET utf8mb4登录后马上失效token过期时间太短或时区问题检查JWT的expiration时间和服务器时间是否一致打包后接口地址不对前端写死了localhost用环境变量配置接口baseURL打包时注入正确的API地址上传的图片显示不了静态资源路径配置问题把上传目录配置到Nginx静态资源路径或用OSS存储排查问题的思路也很重要。我习惯先把问题分成三类前端问题、后端问题、部署环境问题。前端问题先看浏览器开发者工具的Network面板接口请求没发出去就是前端问题发出去没响应就是后端或代理问题。后端问题看日志Spring Boot的日志会打印完整的异常栈和SQL语句95%的问题看日志就能定位。部署环境问题基本集中在端口没开、路径不对、Nginx配置错误这三类。5.3 安全加固的几个必做项刚部署上线时系统安全性是没法用的暴露在外网必须做几件事。密码存储必须用BCrypt就算数据库被拖走明文密码也不能泄露。接口层面要防SQL注入MyBatis Plus的#{}预编译机制能挡住大部分注入攻击但还是不要用拼接SQL的方式写复杂动态查询。管理后台登录接口要加验证码。最初做系统时没加验证码结果上线的第二天就被扫到登录接口开始密码暴力尝试。后来我引入了Hutool的验证码工具库用后端生成验证码图片登录时校验验证码。这步虽然增加了一点用户体验成本但在公网环境下是必须的。运维层面也有经验Spring Boot的actuator接口默认暴露了健康检查、环境信息等端点如果你引入了这个依赖却没配置权限等于把服务器的一部分信息免费送给别人看。生产环境配置里要显式关闭这些端点只保留health一个就够了。6. 写在最后的实操体会整个健身管理系统做下来我最深的体会是这类管理系统的核心难点不在技术,而在业务逻辑的严谨性。预约模块的并发控制、会员卡到期时间的比较、状态流转的边界条件这些看似不起眼的地方才是真正考验工程师功力的地方。技术栈Spring Boot和Vue只是工具把业务想清楚才是根本。如果现在重新让我做一遍我会先花更多时间画清楚状态流转图而不是急着写代码。另外想提一个后续可以扩展的方向这套系统目前只做了Web端如果健身房需要完全可以把前端换成微信小程序后端接口只要稍微调整鉴权方式就能复用基本不需要重写业务逻辑。这个项目本身的价值不在于代码量有多大而在于把一套真实业务场景完整落地了从需求到上线整个链路都走通了这个经验比代码本身值钱。
返回列表