十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

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

基于SpringBoot的健身房管理系统实战:预约防冲突与Flowable审批流

基于SpringBoot的健身房管理系统实战:预约防冲突与Flowable审批流 很多人拿到《基于SpringBoot的健身房管理系统的设计与实现》这个题目第一反应是“又一个CRUD练手项目”。说实话我当初也是这么想的。但真正动手之后才发现健身房这个场景比想象中要复杂得多会员卡种的状态流转、私教课的预约冲突、场地时段编排、过期提醒每一个环节都牵涉到真实业务逻辑绝不是简单堆几张表就能糊弄过去的。这篇博文我把自己从选题、设计、编码到部署的完整思路和踩坑记录整理出来。如果你正在准备Java方向的毕业设计或者想找一个能把SpringBoot全家桶、工作流引擎、定时任务都串起来的实战项目这篇文章应该能帮你省下不少瞎琢磨的时间。1. 内容整体设计与思路拆解1.1 为什么选“健身房管理”这个场景我见过太多人选“XXX管理系统”当毕设无外乎图书、超市、仓库、医院挂号说实话这些选题的区分度已经很低了。健身房管理系统之所以值得做是因为它的业务模型层次感很强能自然引入一些非CRUD的技术点而不是生硬地“为了用而用”。卡种管理涉及会员卡状态机未激活、正常、过期、挂起、退卡不同卡种对应不同的场地权限和课程折扣这是典型的状态流转场景适合引入状态机设计思路。私教课预约教练有时间段、会员有课程需求、每个时段还有人数上限本质上是一个“多对多资源冲突检测”的问题。这块如果用数据库裸查去写代码会很臃肿但用乐观锁或者唯一索引约束来做干净利落。场地时段编排健身房就那么大操房、器械区、游泳池都有容量限制需要做时段维度的排课和人数统计这牵扯到时间片的数据建模。这套模型下来既覆盖了传统管理系统的增删改查又能在预约防冲突、卡状态自动提醒、报表统计这些点上去做比较有深度的设计。答辩的时候老师问“你这个项目的难点在哪”你就有话可说了而不是干巴巴地讲“JPA的save方法”。1.2 技术选型背后的权衡技术栈选择了SpringBoot MyBatis-Plus MySQL Redis Vue这套组合在Java毕设里属于“高性价比方案”。每个组件都是经过实际测试的没有一个是充门面的。组件选型理由实际使用场景SpringBoot 2.7.18稳定版本不追求新集成生态成熟整个后端基础框架MyBatis-Plus单表CRUD不用手写SQL节省大量代码量会员、课程、订单等基础表操作MySQL 8.0免费InnoDB引擎支持事务所有结构化业务数据Redis缓存热点数据解决并发预约下的性能问题场地时段查询、验证码、令牌存储Flowable 7开箱即用的轻量工作流引擎解决审批流程私教课请假审批、退款审批Vue 3 Element Plus前端快速开发组件较完整界面效果尚可管理后台所有页面这里多说一句Flowable。很多人一听工作流就觉得复杂、没必要但在实际业务里退款审核、请假审批、续卡审核这些流程天然就是多级审批如果自己在代码里写if-else去判断状态流转后续加一个审批节点就要改一遍核心代码。而Flowable把流程定义和业务代码解耦前端提交申请后端根据流程图自动推进节点还会生成待办任务。这个点放在设计文档里是实实在在的需求驱动技术选型而不是技术堆砌。对了Flowable 7开源版对JDK8兼容性还不错不用非得升级。1.3 功能模块划分整个系统按角色划分为三类用户每类用户的功能边界必须清楚这是答辩时评审老师一定会问的点。管理员端会员卡办理、续费、挂起/解挂操作卡种增减设置教练信息审核场地排课管理所有订单和预约记录的查询、统计系统的收入报表和用户活跃统计。教练端查看自己的课程表和会员预约记录给会员发布训练计划提交请假申请走Flowable审批会员评价的查看与回复。会员端登录注册、卡种浏览、在线购卡/续费模拟支付、预约私教课、预约场地时段、查看训练日历、课表提醒、个人资料管理。2. 核心细节解析与实操要点2.1 数据库设计36张表的结构骨架数据库是整个系统最核心的部分设计得不好后面写代码就是在填坑中煎熬到凌晨。我把表大致分为用户与权限、卡种与订单、课程与预约、公告与消息、日志与统计五个部分。重点说几个容易出问题的表会员卡表member_card状态字段不能随便命名为status建议用一个枚举字符串UNACTIVATED、ACTIVE、EXPIRED、SUSPENDED、RESIGNED这样代码里读起来语义清晰也不容易把0、1、2的映射关系搞混。每次卡片状态更新都要在card_status_log表中留痕谁操作的、什么原因、从哪变到哪这个审计字段在答辩时很容易被问到。时段表time_slot核心建模思路是“一个时段某天某场地某时间段容量上限”。不要用date和time单独存直接一个start_time和end_time再加上total_capacity和booked_count。每次预约要么在事务里锁行要么用MyBatis-Plus的update ... where booked_count total_capacity做原子判断避免超卖。订单表orders订单状态用PENDING_PAY待支付、PAID已支付、REFUNDING退款中、REFUNDED已退款、CLOSED已关闭。退款中的状态与Flowable审批流程挂钩流程走完才能流转到REFUNDED。这一点在实现部分再展开。我建库的另一个心得是所有时间统一用datetime且后端在存储层统一使用LocalDateTime不依赖数据库的CURRENT_TIMESTAMP自动填充。原因很简单项目里涉及不同时区和定时任务的时间计算如果数据库和Java代码各算各的排查线上问题会非常痛苦。2.2 后端环境搭建与工程初始化环境这块我踩过一个大坑刚开始用的JDK 17加上最新版SpringBoot 3.x结果发现MyBatis-Plus的很多老派注解还不兼容Flowable的集成包也还没有完全适配折腾了两个晚上果断退回JDK 8 SpringBoot 2.7.18一把梭把所有依赖全部拉通。这里建议工程初始化直接一把梭# application.yml 核心配置摘录 server: port: 8080 spring: datasource: url: jdbc:mysql://localhost:3306/gym_system?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/Shanghai username: root password: xxxxx driver-class-name: com.mysql.cj.jdbc.Driver redis: host: localhost port: 6379 database: 0 servlet: multipart: max-file-size: 100MB max-request-size: 100MB mybatis-plus: configuration: map-underscore-to-camel-case: true log-impl: org.apache.ibatis.logging.stdout.StdOutImpl global-config: db-config: logic-delete-field: deleted logic-delete-value: 1 logic-not-delete-value: 0这里解释一下为什么配置文件里要写map-underscore-to-camel-case true。如果你的表字段叫start_timeJava属性叫startTime不开这个配置你会在代码里到处写TableField(start_time)繁琐且容易遗漏。打开之后自动映射省心不少。还有一点逻辑删除字段全局配置所有表统一加一个deleted字段查询时MyBatis-Plus自动拼接where deleted 0。虽然多占用一点存储空间但能保住所有历史数据的“案发现场”排查问题的时候非常有用。2.3 Redis的合理引入和业务落点Redis在这个项目里我用了三个真实场景每一个都是实际业务反复测试过的场地时段高并发查询缓存会员端首页展示一周的课程表、场地忙碌度数据来源是数据库的预约表。这种跨表聚合查询如果把所有数据都实时查库数据库压力会飙升。我的做法是当天课程列表缓存到Rediskey设计为gym:schedule:{date}有效期10分钟过期后异步刷新。实测下来QPS提高了将近5倍。验证码与登录令牌短信验证码存Redis有效期5分钟key为sms:code:{phone}登录成功后生成的JWT token加上Redis做主动失效控制实现“修改密码后所有会话失效”。防重复提交同一个用户对同一节私教课的一键预约请求在Redis里用setNx gym:appointment:{suitKey} 1加一个3秒的分布式锁避免前端连点两下导致重复下单。3. 实操过程与核心环节实现3.1 用Flowable实现退款审批流Flowable的核心思路是把业务流程定义成BPMN文件我们需要做的就是设计好流程图然后在关键节点上写监听器把任务表里的流程数据与业务表关联起来。我的退款审批流程图非常简明会员发起退款请求 - 管理员初审 - 财务确认 - 原路返还模拟 - 流程结束。如果管理员初审不通过流程直接走到结束节点订单状态变为REJECTED。具体实现上SpringBoot启动时Flowable会自动部署resources/processes目录下的.bpmn20.xml文件。启动流程如下public void startRefundProcess(Long orderId, String memberName) { // 先更新订单状态为REFUNDING orderMapper.updateStatus(orderId, OrderStatus.REFUNDING); // 启动工作流把orderId放到businessKey里 MapString, Object variables new HashMap(); variables.put(orderId, orderId); variables.put(applicant, memberName); runtimeService.startProcessInstanceByKey(refund_approval, orderId.toString(), variables); }这里的关键是businessKey流程实例与我们的订单表如何关联答案是业务流程ID就存businessKey后续在待办列表中直接通过orderId去查询流程不需要在Flowable的ACT_RU_EXECUTION表里干瞪眼。审批节点执行时需要注入TaskListenerComponent(refundManagerHandler) public class RefundManagerHandler implements TaskListener { Override public void notify(DelegateTask delegateTask) { if (EVENTNAME_CREATE.equals(delegateTask.getEventName())) { // 设置审批人比如从数据库里查管理员id delegateTask.setAssignee(admin_user_id); // 同时可以往业务表里写入一条待办通知 } } }审批通过时再在Service中调用taskService.complete(taskId, Collections.singletonMap(approved, true));如果approved为false会走到排除分支${approved false}直接结束流程并更新订单状态。整套逻辑搞定后无论加多少层审批节点都不需要改主流程代码。3.2 私教课预约防冲突方案这个功能是我花了最多时间打磨的。规则很简单一个会员同一时间段不能约两门课一门课同一时段不能超过容量上限。但实现上需要处理好并发预约。数据库层面预约表加唯一索引(member_id, time_period)。只要同一会员在同一周期的预约撞车数据库直接报唯一键冲突这正是我们想要的效果。代码层面先查该会员是否已有冲突课程如果有直接返回友好提示然后用前文提到的原子更新控制容量// 容量扣减的原子操作 int updated courseMapper.reduceStock(courseId, timeSlotId); if (updated 0) { throw new BizException(该时段名额已满); }对应的SQL要写成UPDATE course_schedule SET booked_count booked_count 1 WHERE id #{scheduleId} AND booked_count total_capacity这条SQL能保证“扣减的数量不会超过容量”在并发请求下数据库行锁会排队执行不会出现超卖的情况。前端在预约按钮上再配合Redis分布式锁基本能挡住99%的重复预约请求。3.3 定时任务与卡状态自动判断因为会员卡有过期时间、有休眠时间单纯靠用户登录时判断状态不够系统必须有一个兜底的定时任务在每天凌晨执行校准。SpringBoot自带的Scheduled能轻松实现但正式项目中要注意线程池配置不然多个定时任务可能会互相阻塞。Configuration public class ScheduledConfig implements SchedulingConfigurer { Override public void configureTasks(ScheduledTaskRegistrar taskRegistrar) { taskRegistrar.setScheduler(Executors.newScheduledThreadPool(5)); } }核心任务逻辑Component public class CardStatusTask { Scheduled(cron 0 10 0 * * ?) public void expireCards() { ListMemberCard cards cardMapper.selectExpiredTomorrow(); for (MemberCard card : cards) { if (card.getExpireDate().isBefore(LocalDate.now())) { cardMapper.updateStatus(card.getId(), CardStatus.EXPIRED); // 同时给会员推送一条站内信 messageService.push(card.getMemberId(), 您的会员卡已过期请及时续费); } } } }这里有个注意点不要只查expire_date today因为可能会有部分会员卡在当天续费成功你凌晨跑任务时它已经续费到明年了这时候必须连表查询订单表确保“当前卡到期日之后有没有新的成功订单”。如果只看卡表的到期日续费当天就可能被误标为过期。我当时第一次上线就踩了这个坑第二天被报销系统报错才发现。3.4 前端页面与文件上传下载前端我用的Vue 3 Element Plus与后端交互全部走RESTful API。对于管理后台来说重点是表格页、表单页、审批页的组件复用。文件名上传下载这一块我单独封装了uploadService上传前端el-upload组件通过action属性直接调后端/api/file/upload接口后端将文件存储到本地磁盘同时生成UUID文件名防止重名。大文件上传毕设场景其实不需要做分片上传100MB以内的文件用MultipartFile直接接收就行了。我一直用SpringBoot默认的spring.servlet.multipart.max-file-size100MB限制相关参数。下载通过ResponseEntitybyte[]返回二进制流同时设置Content-Disposition: attachment; filenamexxx前端会触发浏览器下载。这里建议将文件路径存到数据库而不是直接存Base64不然查询列表数据时会非常庞大。4. 常见问题与排查技巧实录4.1 问题速查表问题现象根因解决方案前端请求跨域后端未配置CORS全局配置类实现WebMvcConfigurer并设置allowedOriginPatterns(*)注意不要用allowedOrigins(*)否则带token请求会被浏览器拦截上传文件大小超出限制默认1MB上限application.yml里配置spring.servlet.multipart.max-file-size100MB和max-request-size100MB定时任务不执行主类忘记加EnableScheduling在启动类上加上该注解Flowable表自动创建失败数据库账号权限不足Flowable默认会在运行库中自动建表检查spring.datasource.url和账号是否有DDL权限日期返回时少8小时Jackson和MySQL的时区不一致数据库连接serverTimezoneAsia/Shanghai并统一Java侧的时间类型为LocalDateTimeMyBatis-Plus 逻辑删除失效实体字段名和全局配置不一致确保所有实体有TableLogic注解或全局配置开启logic-delete-field并发预约超卖没有做容量原子扣减使用UPDATE ... SET booked_count booked_count 1 WHERE booked_count total_capacityJWT过期后仍在请求前端未处理401状态封装axios拦截器遇到401清空本地token并跳转登录4.2 事务边界一个看似正确却翻车的案例在一次退款接口中我最初写了这样的逻辑Transactional public void refundOrder(Long orderId) { orderMapper.updateStatus(orderId, REFUNDING); refundService.startFlowableProcess(orderId); deductCoupon(orderId); // 回退优惠券 }看起来事务没问题但实际运行时发现Flowable流程已经启动了订单状态却回滚了。排查后才知道runtimeService.startProcessInstanceByKey内部走的是独立事务它提交后外层事务回滚不会影响Flowable。这样的结果就是流程表里有一条流程但订单表里没有对应的退款记录。解决方案是解耦先更新订单状态再启动流程或者用PROPAGATION_REQUIRES_NEW把启动流程的操作拆出去。同样的道理发送短信通知、调用外部接口这类操作也不应该放在核心事务里否则会造成事务时间过长数据库锁竞争加剧。4.3 环境版本兼容的教训网上很多教程在教“最新版本”但现实很骨感。SpringBoot 3.x虽然前景很好但很多第三方的starter还是针对SpringBoot 2.x适配的特别是工作流和代码生成工具。这里分享一个笨办法去Maven中央仓库看某个starter的发布时间如果它的Release版本时间早于你所用的SpringBoot版本发布时间大概率兼容性没问题反之你就得做大量踩坑测试。我自己在一个实验项目里试过SpringBoot 3.2 Flowable发现它的自动配置类路径变化明显反正我是没有耐心去挨个Debug。4.4 个人心得从“会写”到“写得通”最后分享一点我的体会。很多人在做这种系统的时候习惯把全部精力放在后端CRUD接口上结果到了答辩发现前端页面一塌糊涂、业务流程说不清楚。实际上评审老师更看重的是你的业务闭环会员从注册 - 购卡 - 预约 - 核销 - 到期这一整条链路是否能跑通中间的数据状态是否一致。我当时是先画了一张业务流程时序图把每个节点需要的数据、接口、页面都列清楚再开始编码。这样做的好处是写代码的时候心里永远有张地图哪里是断头路哪里是收口逻辑一目了然。如果你也准备拿这个项目作为毕设或课设建议在“预约防冲突”和“Flowable审核流”这两个点上多花心思打磨出一条完整的操作路径。这两个模块是你和普通增删改查项目拉开差距的两块压舱石。踩坑多没关系关键是每个坑你都能说清楚原因和解决方案这在答辩的时候反而会成为加分项。
返回列表