尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

Spring Boot健身房操课预约系统设计与实现

Spring Boot健身房操课预约系统设计与实现 1. 为什么健身操课预约系统是“看着简单、做好难”的一类题目我是在帮朋友做毕业设计预审时第一次认真看这个题目的。当时列表里有一堆“XX管理系统”健身房操课预约系统放在里面并不显眼。但细看之后我反而觉得这是个非常值得做的题目它既有登录鉴权、又有列表查询还有会员端和后台管理端核心业务是排课和预约规则清晰但又不至于简单到没东西可写。很多同学一看到“预约系统”四个字就觉得无非是做几个增删改查页面。真上手以后才发现教练排课要不要冲突检测满员之后能不能预约用户取消后名额怎么释放一个人一天能不能重复预约这些规则全都要理清楚。理得清楚答辩时就有话说理不清楚代码写到最后全是补丁。1.1 题目背后的典型需求场景健身房的操课预约和普通商品预约有几个明显的不同点。第一操课是有“物理容量”的。一个操房最多装下二十个人瑜伽垫、动感单车都是固定数量所以名额必须有上限。第二操课是“按时间点发生”的一次性活动不是长期库存。课程一旦过了开始时间这堂课的预约记录就只能走“已上课”或者“未出席”的状态不能无限期挂在那。第三操课经常有排课周期比如每周一晚上七点是动感单车课同一门课会重复出现。这意味着数据模型要能把“课程模板”和“上课实例”分开否则后台排课就会非常痛苦。这套需求放到毕业设计里正好能覆盖最常考的几个技术点。比如Spring Boot的定时任务可以用来处理过期未签到记录Redis乐观锁或数据库行锁可以用来防止超员JWT做用户身份识别后台用Vue或者模板引擎做管理界面。业务规模不大不小难度刚好适合一个学期的开发周期。1.2 什么样的毕设才算“完整”我看到不少同学习惯打开IDE就开始建表建了五六张表后开始写Controller写到哪算哪。这种方式做管理系统可能会歪打正着但做预约系统绝对会踩坑因为预约系统里存在最多的其实是“状态限制”。完整的健身房操课预约系统至少要包含三类角色视角会员用户能浏览课程、按日期筛选、查看某节课剩余名额、提交预约、取消未开始的预约、查看自己的约课记录。教练用户能查看分配给自己的课表能处理学员名单能标记上课情况。管理员用户管理会员和教练账号、维护课程模板、编排具体上课时间、指定教室和教练、查看整体约课数据。每个角色背后都需要独立的权限控制哪怕后端只做“一个User表加一个role字段”前端菜单也要根据角色动态隐藏这在答辩演示的时候非常加分因为功能演示流程会显得非常完整。2. 技术选型与Spring Boot项目骨架设计技术选型这个环节很多人低估了它的重要性。同学习惯于直接搜“Spring Boot毕设项目”看到别人用哪个版本自己就跟着用但版本决定了很多后续开发体验。我这里的建议比较务实直接用Java 8加Spring Boot 2.7.x配合MyBatis-Plus和MySQL前端用Vue。这套组合的稳定性很高社区资料多遇到报错几乎都能搜到结果。2.1 为什么建议用Spring Boot 2.7而不是3.xSpring Boot 3.x出来以后确实有同学想尝鲜。但我个人认为做毕业设计这个场景下稳定和查找资料方便是第一位的。Spring Boot 3.x强制要求JDK 17以上而Java 8环境在不少学校机房和云服务器上仍然是主流。另外一个很隐蔽的坑是包名变化Spring Boot 3把javax开头的包全部换成了jakarta很多老代码复制过来没法直接用你得知道哪里要改。如果你用的还是常见的视频教程、开源工具类它们大概率都是基于2.x写的。万一碰到跑不起来的情况改包的功夫都够你把一个模块写完。当然如果你平时已经在用JDK 17也熟悉jakarta前缀那用3.x也没问题。但为了防止答辩现场环境出岔子2.7.x真的是最稳妥的方案。2.2 后端依赖清单和原因我通常在pom.xml里只需要引入下面几个核心依赖不额外堆砌没用的包spring-boot-starter-web提供Spring MVC和内嵌Tomcat。spring-boot-starter-validation用于参数校验比如手机号格式、姓名非空。mybatis-plus-boot-starterMyBatis-Plus简化单表CRUD。mysql-connector-javaMySQL驱动注意版本要和数据库对应。jjwt或java-jwt生成和解析JWT Token。lombok减少实体类里的getter/setter样板代码。knife4j或springdoc在浏览器里生成Swagger接口文档。有人会问Spring Data JPA不也能做吗确实能但毕设开发周期短JPA的懒加载和级联操作反而容易把人绕晕。MyBatis-Plus会主动帮我们生成单表增删改查方法多表联查就自己写SQL心智负担小很多。答辩过程中如果被问到为什么选它也能说得很清楚单表操作靠通用Mapper复杂查询靠XML边界很清晰。2.3 项目目录结构和分层思路后端代码我习惯使用一种不至于过度的分层方式controller、service、mapper、entity、dto、common分开。每一层只干一件事这样到了写说明书和答辩演示的时候每一层都能对应上一个“设计模式”或者“架构原则”。src/main/java/com/example/gym ├── common # 通用返回结果、异常处理器、常量 ├── config # Spring配置类比如WebMvc配置、Knife4j配置 ├── controller # 接收HTTP请求返回给前端 ├── service # 业务服务层写预约核心逻辑 ├── mapper # MyBatis-Plus或手写SQL的Mapper接口 ├── entity # 数据库实体类 ├── dto # 接口出入参对象避免直接把实体暴露出去 └── utils # JWT工具类、日期工具类等很多同学喜欢把前端传过来的参数直接用实体类接收图省事。但预约系统里前端传来的数据往往不是完整的数据库字段比如“预约操作”只需要scheduleId和userId没必要和整张预约记录表绑定。用DTO专门接收一层一层转换代码也更安全。3. 数据库表设计决定成败的一个环节健身房操课预约系统的表不算特别多但每张表之间的关系需要花心思。我用了一段时间重新设计了一遍发现如果把表设计成下面这样后面的开发会非常顺。3.1 用户侧的表怎么建我这里把会员和教练都放在sys_user表里通过role字段区分。再加一张member_profile表存会员等级、剩余私教次数等扩展信息加一张coach_profile表存教练介绍、擅长课程类型等扩展信息。这样做的原因是会员和教练后续扩展的属性差异较大混在一张表里会导致大量字段为空看起来又乱又难维护。如果只做基础功能也可以把角色直接设计成字段role_codeadmin、coach、member。但登录、注册、权限判断这几个逻辑要提前想好。不能用一种用户实体通吃所有角色之后才发现教练也要管理班级成员名单而教练和会员根本没有关联字段。3.2 课程模板和排课实例最容易被写成“一张课表”的就是课程部分比如只建一张course表包括上课时间、地点、教练ID。我建议把课程拆成course和course_schedule两张表。course表存的是“什么样的一门课”比如课程名称是“动感单车”、课程封面图、课程介绍、默认人数上限。 course_schedule表存的是“某一次具体排课”比如“1月10日晚上7点单车房王教练上限20人”。为什么要分开因为同一门课要开很多次。如果把上课时间直接冗余在course表里下周五想加一节课你不得不给course表加一个时段字段查询某个时间段的课程时就会很别扭。分开后管理员可以先建好课程模板再一句接一句往课程表里插入排课记录。前端展示时按日期条件去查schedule表即可非常清楚。类似地教室也可以单独建一张room表。如果健身房有两间操房那么同一时间就不能排两节课在同一间教室。排课时需要检查教室和教练是否被占用。如果觉得毕设范围太大可以把教室做成schedule表里的字符串字段roomName然后在排课逻辑里手动判断冲突。多数毕业答辩能接受后一种做法但如果你能在说明文档里写清楚为什么简化反而显得你懂取舍。3.3 预约记录表与唯一约束预约记录是系统的核心设计时要提前考虑唯一性。一个用户同一时间段不能预约两节课一个用户对同一节课只能预约一次不能重复预约。为了在代码之外加一道保险我会在booking表里加唯一索引。CREATE TABLE booking ( id bigint NOT NULL AUTO_INCREMENT, member_id bigint NOT NULL COMMENT 会员用户ID, schedule_id bigint NOT NULL COMMENT 排课ID, status tinyint NOT NULL DEFAULT 0 COMMENT 0已预约,1已取消,2已签到,3缺席, create_time datetime DEFAULT NULL, update_time datetime DEFAULT NULL, PRIMARY KEY (id), UNIQUE KEY uk_member_schedule (member_id,schedule_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;unique键是防止“同一个会员对同一节课按了两次预约按钮”的底线。很多时候前端的按钮防重复点击不一定靠得住网络慢的时候用户连续点三下后端没处理好就会插入两条记录。有了这个唯一键数据库层面直接报错你写代码时更容易发现并发问题。4. 核心预约业务并发名额如何控制预约系统的技术难点集中在这里一个排课名额只有20个现在已经预约了20个人第21个人同时提交预约怎么办如果代码逻辑是先select count判断小于20再insert那么在两个用户同时操作的时候两个请求都可能读到“当前19人”然后都插入成功最后结果是21个人约上了。这种错误叫做超卖很多同学在做秒杀和电商项目时会遇到但在操课预约里几乎一模一样。毕设答辩时老师特别喜欢问这个问题。如果你回答不上来会被认为“这个系统只能做演示不能用”。4.1 实现方案一数据库行锁形式的原子更新我推荐用一条update语句来完成对剩余名额的扣减。假设我们已经在course_schedule表中增加了一个字段booked_count表示当前已预约人数。每次新增预约时后端不要先做select再调insert而是直接执行// 这里假设前端传来的参数里只有scheduleId和memberId boolean success scheduleMapper.reduceStock(scheduleId); service层检查affected rows如果等于0说明已经没有名额了。对应的SQL写法是UPDATE course_schedule SET booked_count booked_count 1 WHERE id #{scheduleId} AND booked_count max_count这样当两个请求同时到达时数据库的行锁会让它们排队执行。第一个请求让booked_count从19变成20第二个请求执行时发现条件booked_count max_count已经不成立受影响行数为0于是业务层直接抛出“课程已满员”的异常。这个方法不需要引入Redis对毕设项目来说足够优雅还能把原理讲明了。4.2 实现方案二利用Redis做预减库存如果学有余力想体现自己了解分布式场景可以在系统里加入Redis来预扣库存。比如key是schedule:stock:1001value是初始剩余数先用Redis的DECR命令原子减1如果返回值小于0说明名额不足直接拒绝预约如果成功再把预约记录写入MySQL。这个方案确实更接近真实互联网项目的做法性能和并发能力都更强。但要注意Redis和MySQL之间的一致性Redis扣减成功MySQL插入失败要去补偿回滚Redis这种来回补偿的代码才是最考验人的。如果不做很复杂的补偿机制我建议毕业设计以MySQL原子更新为主Redis可以作为加分项在文档里简单设计。4.3 预约冲突检测除了人数校验还有时间冲突检测。比如一个会员一天之内不能约太多课或者同一时间段不能约两节课。这块不能用简单的唯一索引解决因为判断条件是“重叠时间段”。最简单的办法是在service层处理预约前先去数据库查询该会员在目标时间段范围内是否已经存在状态为“已预约”的排课记录。这里用SQL会比较方便SELECT COUNT(*) FROM booking b JOIN course_schedule cs ON b.schedule_id cs.id WHERE b.member_id #{memberId} AND b.status 0 AND cs.start_time #{newEndTime} AND cs.end_time #{newStartTime}只要数量大于0就提示用户“该时间段已有课程安排”。这种SQL在数据量小的时候不会有性能问题也完全符合毕业设计的需求。4.4 取消预约与名额释放对应的取消逻辑也要保证原子性。用户点击取消后先把booking表中这条记录的状态从“已预约”改成“已取消”然后执行UPDATE course_schedule SET booked_count booked_count - 1 WHERE id #{scheduleId}这里建议两个操作放到同一个事务中避免出现“预约状态取消成功了名额没有释放”或者反过来的情况。可以先取消记录再减库存也可以反过来在service方法上加上Transactional注解这一步毕业设计里经常被忽视务必好好写。如果用redo还是cancel实际上如果有非正常状态的处理比如教练临时取消整节课那么所有已经预约这节课的人都要批量取消。这种操作需要在事务里循环处理最好把课程状态改成“课程取消”而不是把课程记录物理删除。否则历史数据全部丢失后续后台统计会对不上。5. 后端权限设计与接口安全健身操课预约系统的接口不能允许任何人随意调用。比如普通会员不能调用“创建课程”接口教练不能修改其他教练的课程。权限设计的常见做法是用JWT生成Token在后端通过拦截器统一校验。5.1 登录与Token生成用户登录成功后后端查出用户ID、角色等信息通过JWT工具类生成一个带过期时间的Token返回给前端。前端之后每次请求都在请求头里带上Authorization字段。我用Java写JWT时会比较关注下面两点不要把密码、手机号这类敏感字段放到Token里。Token只是临时凭证如果泄露至少不能把敏感信息一起泄露。Token要有过期时间。毕竟健身房操课预约是一个长期使用的项目不能让用户登录一次就永久有效。具体代码可以直接参考jjwt官方的例子。核心就是把用户主键和角色作为claim放进去签名密钥放在application.yml中。5.2 不同角色的操作权限权限的落地方式我习惯先做一个自定义注解比如RequireRole(value admin)然后写一个HandlerInterceptor拦截器在preHandle方法里取出当前登录用户角色判断是否匹配。这样需要在管理员接口上标注一下就好了。例如RequireRole(admin) PostMapping(/course) public Result createCourse(RequestBody CourseCreateDTO dto) { return service.create(dto); }这种设计代码简洁答辩时也容易讲清楚能说明自己理解基于角色访问控制。如果想把权限做细可以做成动态权限表把操作权限和角色关联角色和用户关联形成RBAC模型。但它的开发量会明显增加对操课预约这种业务来说前端做好菜单控制、后端做好角色拦截已经足够。5.3 参数校验与统一异常处理预约系统参数校验也是高发细节。比如创建排课时开始时间不能晚于结束时间排课的课程人数要大于0约课的scheduleId不能为空。使用spring-boot-starter-validation直接在DTO字段上加上注解NotNull(message 排课ID不能为空) private Long scheduleId;然后在Controller参数上写Validated框架就能自动校验。校验不通过时通过RestControllerAdvice写一个全局异常捕获统一转成Result对象返回给前端。全局异常处理还有一个好处业务层抛出各种带业务提示的异常比如“该课程已满员”“您已预约过该课程”前端都能收到结构一致的JSON消息。代码整洁度会高很多。6. 前端页面设计与后端接口的配合思路很多同学做毕设把大量时间花在页面外观上最后反而没时间调核心功能。我建议前端只追求清晰、能完整走通业务流程不要过度追求酷炫。选型方面如果是Web端管理系统直接使用Vue加Element Plus如果还想在手机上演示可以考虑加一个移动端H5页面但一般毕业设计做一个Web版本就足够完整。6.1 会员端页面最核心的部分会员端的主页面我认为应该是“课程日历加课程卡片”。用户进入系统后顶部是一个星期选择器可以选择查看某一天有哪些操课下方是课程卡片列表每张卡片展示课程名称、开始时间、教练、教室、剩余名额、预约按钮。预约按钮的状态可以根据数据变化该课程已经满员按钮置灰显示“已满”。该用户已经预约过按钮变成“已预约”点击可能变成“取消预约”。课程已经开始或已结束按钮不可点。课程被管理员取消整张卡片置灰。这些状态判断可以放到前端但最终一定要由后端再次校验。因为前端只是展示后端才是真正做规则判断的地方。比如倒计时结束之后前端按钮没有自动刷新用户还是点击了预约按钮后端也必须拒绝。6.2 后台管理端开发的小建议管理端的核心功能是排课也就是维护course_schedule表的数据。页面看起来会是一个表单选择一个课程模板、选择教室、选择教练、选择开始时间、结束时间填上人数上限点“发布排课”。如果要从专业角度提升一下可以在发布前给教练和教室各做一个时间轴排课时显示当前教练在哪个时间段有课、哪个教室被占用这样能直接从界面上防止手动排课冲突。虽然这个功能会多花几天时间但它对“健身房操课预约”的业务特色体现得最强答辩效果和演示效果都会比常规的几张管理表格好很多。7. 常见问题速查与避坑技巧最后一部分写点实操心得。我在实际调试这类系统时遇到过很多问题其中有不少问题很固定写在这里给你参考。7.1 状态不同步导致预约和显示不一致如果你只是在前端简单地计算剩余名额比如拿课程总人数减已预约人数然后再显示很可能出现不同步问题。正确做法是后端接口返回course_schedule表里的booked_count前端只负责展示。不要把业务计算分散在前端尤其要在接口文档里写清楚剩余名额是后端返回的还是前端算的。7.2 时区导致的排课时间错乱Spring Boot默认以UTC来处理日期时间而我们的MySQL连接参数里如果设置了serverTimezoneAsia/Shanghai前后端如果直接传字符串时间容易差8个小时。解决方法是统一用LocalDateTime接收和保存在数据库连接串中明确指定时区并且前端传时间时使用ISO格式。最稳妥的做法是后端做排课时把开始时间和结束时间都当作UTC时间的字符串格式传入并转换避免本地浏览器时区和服务器时区之间的差异。7.3 循环依赖和事务自调用当你在Service层拆分太多互相调用的类时可能出现循环依赖比如ScheduleService依赖BookingServiceBookingService又依赖ScheduleService。这时Spring虽然有些情况能处理但最好还是重新拆分职责。事务自调用也容易踩坑常见写法是同类里的一个方法调用另一个标有Transactional的方法事务会失效因为Spring是通过代理对象调用事务的。解决办法是把事务方法放到另一个Service中或者通过注入自身代理对象来调用。7.4 超过最大预约数时的回滚如果你采用的是“先select查数量再insert”的写法到最后还可能遇到一种情况第21个人插入成功了因为你查询的时候是19人但在他查询后和插入前已经有人并发插入成第20人。这时候务必使用我前面章节提到的原子更新SQL不要只用Java的synchronized。synchronized只能保住单机单进程内的互斥一旦未来部署多实例就会失效而且它是把整个请求串行化性能会很差。7.5 课程被删除导致预约记录变孤儿一定要谨慎使用物理删除。会员端用户查看历史数据时“已取消的课程”会变成一个不存在的课程记录前端展示时出现null。比较优雅的做法是给排课表加一个status字段0正常1取消。用户取消整节课时课程记录保留但status变成“已取消”。后台展示历史数据时可以查到前台看到预约记录时可以提示“该课程已被取消”。7.6 Lombok在实体类里的坑如果用了Lombok的Data多个实体之间互相产生toString、equals和hashCode时可能会有深层递归。比如entity里包含List SysUser里又引用Booking列表在打印日志时可能会触发递归导致栈溢出。建议在涉及关联对象的实体里重写toString时只输出id和名称不要输出整个关联对象列表。8. 讲解“预约系统”时最有说服力的测试场景毕业设计要现场演示我建议多测下面几个关键场景并在演示时主动展现当某一课程只剩下一个名额时开两个浏览器账号同时预约同一个排课最终只有一个成功。预约成功后取消再去预约其他同时段的课程能正常插入。会员重复点击预约按钮时界面不会生成多条预约记录。教练排课时如果和已有课程冲突后端给出清晰错误提示。管理员取消一节课后所有已预约的会员都能在“我的预约”中看到取消状态。这些动作可能看起来并不复杂反而是整个系统里最能证明工程判断力的地方。演示时不要只是把页面点一遍而是先把前置条件说明白比如“我已经在数据库里把人数预置成19人现在还有1个名额”。然后把两个浏览器同时预约的操作过程展示出来最后把数据库里的记录show出来效果会很直观。我个人在实际开发这类系统时最深刻的体会是不要把并发控制留到最后才做。很多同学先把所有页面写完再回头看“满员冲突”结果发现要么在业务代码里到处塞判断要么数据已经产生脏数据。正确顺序应该是先写一个最简单的会员预约接口把名额判断、唯一索引、状态变更这一套逻辑跑通再扩展课程管理、教练管理、前端页面。核心问题绕不开越早解决后面越省时间。
返回列表