ARTICLE DETAIL

资讯详情

深耕编程入门与网站建设的一线实战洞察。

Spring Boot实战:健身房管理系统中的业务闭环与并发控制

Spring Boot实战:健身房管理系统中的业务闭环与并发控制 如果单看名字“健身房管理系统”很容易被当成一个普通到不能再普通的增删改查作业。可当你真正把业务理一遍就会发现会员、教练、课程这三件事扯在一起远比想象中复杂卡到期了谁提醒、教练排课撞了怎么办、课程约满之后怎么锁住余量、取消预约要不要释放名额。基于 Java 和 Spring Boot 实现一个健身房综合管理平台正好能把这些问题全部落到代码里做成一个能真正演示、能讲出业务逻辑的完整项目。这篇内容我按自己做这个系统的过程来写从选题理由、数据库设计、功能拆解、后端实现到常见坑位都过一遍。适合正在纠结选题的计算机专业学生也适合想通过一个真实业务场景把 Spring Boot、MyBatis、事务、定时任务、权限等知识点串起来的自学者。别把它当成一个“用框架写玩具 CRUD”的练习它其实是练习需求分析和并发处理的一个好载体。1. 题目看起来简单但真正难的是把业务做成闭环1.1 从“增删改查”到“闭环业务”先搞清楚系统在模拟什么很多同学做管理系统一上来就建四个菜单会员管理、教练管理、课程管理、系统管理每个菜单底下都是列表加新增编辑删除。做完之后发现页面确实不少但答辩老师一句话就问住了“你的会员和课程有什么关系教练和课程怎么联动”健身房这个场景和普通商品管理系统不一样它天然有很强的流程感。真实门店的运营逻辑大概是这样的会员到前台登记资料购买年卡或者次卡然后根据课表预约课程上课前跟教练确认签到教练有自己的授课安排今天几点在哪个教室带团课几点有一节私教不能同时出现在两个教室课程要有排期排期里有教练、时间、教室和人数上限会员卡到期之后不能继续预约系统还要提前提醒续费。如果能看到这一层项目就不再是三个互相独立的 CRUD而是一套有状态流转、有校验规则、有数据关联的业务系统。我在设计时把它们串成了一条主线会员办卡得到一张有有效期的会员卡会员预约某一节已排好的课程预约成功后课程排期的已约人数加一课程约满后不能继续预约会员如有事可以取消预约取消后释放名额教练只看到自己所授课程的预约名单会员卡到期后系统自动禁用约课功能并提前通过站内信提醒续费。还有一条管理员视角的支线查看会员增长趋势、某节课的预约率、教练课程量、续费到期情况也就是题目里说的“智能”体现。智能不是玄学。在这个系统里我把它落实成三件事自动计算会员卡有效期的续费规则、预约课程时的余量和重复校验、以及管理员首页那套能实时反映经营状态的数据看板。1.2 技术选型Spring Boot 为什么是主框架前端怎么搭配技术选型是答辩时最容易被追问的一块所以不能只写“用了 Spring Boot”要能讲清楚每一项是干什么的。后端我选了 Spring Boot 2.7.x。为什么不直接上 3.x因为很多线上教程、历年毕设代码都基于 2.xSpring Boot 3 换了jakarta包名如果跟着老教程敲代码会出现大量 import 报错。2.7 既兼容旧资料功能上做毕设完全够用等基础扎实了再换 3.x 也不难。数据库用 MySQL 8持久层选 MyBatis-Plus。MyBatis-Plus 不用手写大量单表 CRUD 的 XML分页插件也很好用能省下大量凑页面的时间把精力放在业务本身。如果你的班级要求必须用主流的 Spring Data JPA也可以但我个人更推荐 MyBatis-Plus因为它的排查链路更直接SQL 出问题也好定位。权限这块我没有直接上一整套 Spring Security。不是说它不好而是作为个人项目服务端权限本质就三类角色管理员、教练、会员完全可以通过 JWT 加 HandlerInterceptor 完成。用拦截器实现角色的校验代码体量小又能把 token 解析、白名单配置这些核心机制讲透。前端可选 Vue 3 加 Element Plus。如果后端能力还没那么熟也可以做成纯接口项目搭配一个超简单的管理后台静态页面。市面上流行的前后端分离写法是 Vue 3 Vite Axios后端只返回 JSON这样前后端能分开调试也让项目看起来更完整。我把项目整体技术栈列在下面需要的话可以直接照抄层次选型说明后端框架Spring Boot 2.7.x生态成熟资料多持久层MyBatis-Plus MySQL 8分页好做SQL 可控安全方式JWT 拦截器角色权限可控且容易解释定时任务Spring Task处理到期会员提醒前端Vue 3 Element Plus ECharts管理端页面和数据报表接口文档knife4j 或 springdoc写报告时能截图用这里说一下为什么我没有引入 Redis。很多项目喜欢在课表查询上加 Redis 缓存或者用 Redis 做分布式锁。但毕设项目通常是单体部署并发量并不高引入 Redis 会增加环境安装和配置成本。我更推荐先把项目跑通把 Redis 之类的内容留在“项目扩展亮点”里答辩时作为“如果系统要上生产环境可以怎么优化”来谈这样既不会显得卖弄又能体现你有思考。1.3 核心流程的边界条件比页面数量更值钱一个管理系统页面很多但真正能在答辩时展示出设计水平的往往是一些边界条件的处理。以下是我在做需求梳理时总结出的核心流程和边界条件办卡时如果会员已经有一张未过期的卡续费后的到期日应该怎么算正确逻辑是从原到期日后一天开始累加而不是从当前日期重新算。会员预约课程时如果预约已满是继续让他排队还是提示失败这里采用简单策略约满直接失败。同一个会员同一节课能不能重复预约要在业务层拦住不能让数据库里出现两条重复记录。会员卡过期后在约课查询里应该直接过滤掉不能让过期卡继续约课。教练编辑排课时如果该排期已经有会员预约能不能直接删除不能。只能把排期状态改成“停用”或先沟通改期。取消预约后排期的已约人数需要做减一操作不能只删掉预约记录而不更新统计数。这些边界条件如果能在报告中画成流程图在代码中有明确判断系统给人的观感就完全不一样。它们也是后续写“系统亮点”“核心功能描述”时的素材。如果你现在还没开始设计我建议先按上面这些流程走一遍再动手建表。需求越细后期返工越少。2. 数据库怎么设计才能支撑“智能系统”而不只是存数据2.1 核心实体与关联关系健身房系统里最常见的错误是把“会员”和“会员卡”设计成一张表。一张会员表上直接挂cardType、expireTime看起来很省事但碰到续卡记录、历史卡查询、换卡类型时就非常痛苦。因为在真实业务里一个会员是可能办过多次卡的续费、换卡、补卡都要有记录可查。我实际拆成了以下几张核心表表名含义核心字段/说明member会员档案姓名、手机号、性别、生日、身高体重等card_type卡类型卡名、价格、时长天、剩余次数、类型member_card会员卡实例会员 ID、卡类型 ID、开始日期、结束日期、剩余次数、状态coach教练档案姓名、手机、专长、等级、入职日期、底薪方案course课程基础信息课程名称、分类、时长、难度、课程简介course_schedule课程排期对应课程、教练、开始时间和结束时间、容量、已约人数、教室course_booking预约记录会员、排期、预约状态、签到时间sys_user系统登录账号后台管理员登录的账号可关联角色message站内消息给会员发送到期提醒、课程提醒这些表之间的关系用一句话就能解释member和card_type决定member_cardcourse和coach组成course_schedulemember_card预约course_schedule后产生course_booking。这个结构已经覆盖了系统的主要业务流程。2.2 会员档案、会员卡与卡类型分开设表的理由member表只管会员的基本属性它不关心这个会员当前有没有卡、是什么卡。card_type表是卡种模板比如“年卡 3000 元/365 天”“30 次卡 1800 元/次数”。member_card表才是会员实际购买的那张卡它记录每一张卡的开始日期、结束日期和剩余次数。这样做有什么好处第一会员卡到期后会员记录还在不会因为卡到期而把会员数据一起“禁掉”第二会员第二次续费时可以保留历史 member_card 记录方便统计“这个会员累计消费了多少”第三查询会员当前持有的有效卡只需要查member_card里状态为正常且结束日期大于今天的记录逻辑非常清晰。在设计表时我给member_card和member都预留了一个独立的业务编号字段比如member_no和card_no。不要直接用自增主键给会员看会员编号用时间戳加随机数生成会更专业。如果项目时间紧张也可以直接用自增 ID但业务编号字段保留下来对后续扩展有帮助。course_booking里我会记录member_card_id而不只是member_id原因是预约业务必须校验“会员持有一张有效的会员卡”。如果会员手上有多张卡某些卡可能只适用于团体课某些卡是私教专享记录使用时用的是哪一张卡会让报表统计更准确。2.3 课程表与排期表拆开才能处理重复排课和容量控制课程基础信息和课程排期必须分开。一个课程比如“动感单车”可能每周一、周三、周五晚上各有一节排期是具体的“2025 年某月某日 19:00-20:00 单车房教练张三”。如果你只设计一张课程表里面放上课时间就无法表达同一门课的多个开课时间。排期表里的关键字段包括对应课程 course_id决定这节课是什么内容对应教练 coach_id教练能看自己的授课题表start_time 和 end_time用来做时间冲突检测capacity 和 booked_count一个上限一个当前已约约满自动拒绝room 字段比如“单车房”“瑜伽房”status排期状态正常、已结束、停用等。重点关注capacity和booked_count。约课成功后给booked_count加 1取消预约后减 1。判断约满不能只看一次查询结果写代码时还要防止并发时两个请求同时读到未满数据导致超卖。这个我在第 4 章会专门讲。2.4 给表设计加索引的实用建议做毕设时数据量不大但索引写得好不好答辩老师看一眼建表语句就知道你水平如何。我在时间字段上加了普通索引。比如course_schedule的start_time因为查询“今天有哪些课”会高频按时间过滤member_card的end_date也建议加因为定时任务要找“即将到期”的会员卡没有索引就是全表扫描。业务唯一性约束也要建比如course_booking中同一会员同一排期不能重复预约我会在程序里先判断同时也给member_id schedule_id建一个唯一索引作为兜底。表与表之间的外键我没有在数据库层面强制建。不是外键不好而是这个项目逻辑外键更灵活。比如删除教练时如果已经存在历史排期记录数据库外键会限制删除但业务上我们可能只希望把教练状态改成离职历史数据保留。用逻辑外键删除操作可控不报错不过代码里要注意不能产生孤儿数据。这些设计不复杂却能在写代码时省掉大量重复判断也是将来写“数据库设计说明书”时最能拿得出手的部分。3. 功能模块怎么拆每个模块里有哪些隐藏业务点3.1 会员模块办卡、续费、到期提醒一条链会员模块看起来就是“会员列表 添加会员”但实际包含的操作要复杂得多。会员注册时后台录入姓名、手机号、性别等基础信息。注册成功后管理员可以直接帮他选择卡类型比如购买一张“年卡”系统自动创建一条member_card记录开始日期是今天结束日期是今天加 365 天。如果选择的是次卡则开始日期今天、结束日期可以是空存一个total_times每次上课扣减剩余次数。续费是个很容易写错的地方。假设会员原来就有一张年卡2024 年 12 月 31 日到期他在 2024 年 10 月就提前续了一年。如果代码把开始日期设成当前日期那等于他白送了系统两个多月会员期对会员不公平。正确做法是如果已有卡未到期新卡开始日期 原卡结束日期 1天结束日期再往后加一年。我的 service 里有这么一段简单逻辑LocalDate startDate LocalDate.now(); MemberCard currentCard memberCardMapper.findActiveCard(memberId); if (currentCard ! null currentCard.getEndDate().isAfter(startDate)) { startDate currentCard.getEndDate().plusDays(1); } LocalDate endDate startDate.plusDays(cardType.getDurationDays());这只是个很小的细节但很多项目都没处理。你把这段代码写到报告里就成了一个业务严谨性的证明。到期提醒用定时任务实现每天早上 9 点扫描结束日期在“未来 7 天以内”的会员卡给对应会员在message表里插入站内消息文案可以写“您的会员卡将在 x 天后到期请及时续费”。会员登录后能查到未读消息管理员也能在会员详情中看到历史提醒。3.2 教练模块排课、查看预约名单、课时记录教练模块不要只做“教练信息的增删改查”。教练的排课与上课签到才更有意义。管理员进入排课页面选中一名教练、一门课程设置开始和结束时间、教室、容量生成一条排期。教练登录后可以按周查看自己的排课情况周一晚上有动感单车周三下午有一节私教。课程开始后教练能看到这节课的预约名单上课前可以勾选“签到”确认该会员已来上课。这里需要考虑排课冲突。新增排期的时候要先查一下这名教练在同一时间段是否已经存在其他排期。判断区间重叠的条件可以写成新排期开始时间小于已有排期的结束时间且新排期结束时间大于已有排期的开始时间两个条件同时成立就说明有重叠。SQL 可以写成select count(*) from course_schedule where coach_id #{coachId} and status 1 and start_time #{endTime} and end_time #{startTime}返回大于 0 就拒绝新增并提示“该教练当前时间段已有课程”。教练薪资这块也可以做轻量实现。每个课程设置一个提成金额课结束后自动给教练生成一条课时记录。如果时间不够这个功能可以放在“扩展计划”里不作为必须项。3.3 课程与排期管理基础数据加开课实例课程基础表里放的是静态信息课程名称、课程分类、课程时长、难度、简介、适用于私教还是团课。比如“动感单车”分类为团课“产后恢复”分类为私教。排期表再关联具体课程和教练。团课的容量一般较大比如单车课可以 20 人私教课容量直接设为 1代表这节课只允许一个人约。这个设计看起来很直观但很多第一次做的人会把“课程”和“排期”混在一起直接在课程表里维护开课时间导致同一门课每天要新增一条课程数据时间一长数据非常混乱。把“课程”理解为菜谱“排期”按照某道菜的菜单号和一个具体厨师、具体开桌时间对应两个层永远不会混淆。管理员端还有一个删除保护如果一节排期已经有人预约哪怕只约了一个人也不能允许物理删除排期只能把状态置为“停用”。如果有学员已预约应该先发送通知再停用否则学员看到的课程突然消失数据也不一致。会员端查课的逻辑很简单查状态正常的排期过滤已预约满的过滤和会员已有预约冲突的按开始时间排序返回。这样展示出来的课程都是“我现在能约的”。3.4 “智能看板”和管理端首页项目标题里有“智能”两个字我建议一定要做一个管理端首页数据看板。这样页面一进来就有直观的视觉效果也方便答辩演示时快速讲清项目价值。看板核心数据包括总会员数、本月新增会员数当前有效会员卡数量、未来 30 天到期卡数量今日待上课表数量热门课程预约率排行近半年每月办卡、续费人数趋势折线图用 ECharts 画图即可后端提供查询接口。比如统计每个月的会员办卡数量SQL 可以这样写select date_format(create_time, %Y-%m) as month, count(*) as total from member where create_time #{startTime} group by month order by month这里注意create_time是datetime先用date_format转成yyyy-MM格式再分组。如果数据里有 NULL 要先用ifnull处理不然前端 chart 很容易显示异常。统计接口响应数据可以封装成ListMapString, Object虽然不够规范但作为报表接口够用简单直接也方便前端直接循环渲染。4. 后端核心实现与关键代码细节4.1 项目分层结构和包规划Spring Boot 项目分层不要搞得太花哨。我用的是常见结构com.gym ├── GymApplication.java ├── config // 拦截器、跨域、分页插件等配置 ├── common // 统一返回结果、异常处理、常量 ├── controller // 接口层 ├── service // 业务层接口 │ └── impl // 业务实现 ├── mapper // MyBatis-Plus 的 Mapper 接口 ├── entity // 数据表实体 ├── dto // 接收参数对象 ├── vo // 接口返回对象 └── utils // JWT、日期处理等工具不要把所有逻辑都写在 Controller 里。Controller 只负责接收参数和返回结果Service 处理业务规则Mapper 负责数据库操作。在答辩时这也是一个很好的“软件工程规范”讨论点。如果代码全堆在 Controller老师一眼就能看出来。统一返回结果我封装了一个简单的ResultT包含code、msg、data三个字段。这样所有接口返回结构统一前端处理也省心。异常用全局RestControllerAdvice捕获业务异常统一抛出RuntimeException子类再由全局处理器转成友好提示。这是代码可维护性的基本盘。4.2 JWT 登录与拦截器权限控制登录流程我用的是 JWT 生成 token然后通过 HandlerInterceptor 做登录校验和角色鉴权。登录接口校验完用户名密码后把用户 ID 和角色放进去生成 token。工具类大致如下String token Jwts.builder() .setSubject(String.valueOf(admin.getId())) .claim(role, admin.getRole()) .setExpiration(new Date(System.currentTimeMillis() 7 * 24 * 3600 * 1000L)) .signWith(SignatureAlgorithm.HS256, secretKey) .compact();后端写一个拦截器在preHandle里解析 token。如果 token 不存在或解析失败直接返回 401解析成功就放行。需要区分接口角色的地方用自定义注解完成比如RequireRole(admin)。这里不建议把用户信息直接存在 ThreadLocal 后又忘记清理。在拦截器的afterCompletion里移除一次不然线程复用会串数据。做项目时坑不大但在高并发环境下就是一个隐患。Swagger 或 knife4j 接口文档建议保留。生成接口文档之后在写毕业论文时直接截图篇幅就有了同时接口调试也方便。4.3 预约课程事务、超卖和重复预约的处理预约是系统里最容易出错的地方我把完整逻辑梳理了一遍。不推荐在 Controller 里写如下操作先查剩余人数判断是否已满再 insert 预约记录最后 update 排期人数。因为在高并发场景下两个请求可以几乎同时查到剩余人数为 1随后都执行 insert导致预约人数比容量多 1出现“超卖”。课堂容量实际上就像火车票。最简单的可靠方式是用数据库的条件更新让数据库帮我们做原子性判断。核心 SQLupdate course_schedule set booked_count booked_count 1 where id #{scheduleId} and capacity booked_count执行这个 update受影响行数是 1说明更新成功可以插入预约记录受影响行数是 0说明已经约满直接返回“课程已满”。因为update对同一行记录有锁保护即使并发请求同时进来MySQL 也会让它们串行执行不会产生超卖。在 Service 中包上TransactionalTransactional(rollbackFor Exception.class) public void bookCourse(Integer memberCardId, Integer scheduleId) { // 校验会员卡状态 // 校验排期是否正常 // 校验是否已经预约过同一排期 int rows courseScheduleMapper.increaseBookedCount(scheduleId); if (rows 0) { throw new BusinessException(该课程已约满); } // 插入预约记录 bookingMapper.insert(...); }这样的设计已经足够兼顾性能和正确性。不少教程还提到用悲观锁select ... for update在 MySQL 中也能解决问题还能先查询再判断。考虑到毕设场景条件更新更简洁也更好解释。取消预约的流程与之相反先更新预约记录状态为“已取消”再把排期的booked_count减一。这两个操作必须写在同一个事务中否则会出现预约状态取消成功而人数没有释放的情况。4.4 定时任务扫描到期会员卡在启动类加上EnableScheduling再写一个定时任务类Component public class CardExpireTask { Scheduled(cron 0 0 9 * * ?) public void remindExpiringCard() { LocalDate targetDate LocalDate.now().plusDays(7); ListMemberCard expiringCards memberCardMapper.findExpiringCards(targetDate); // 逐个给会员插入站内消息 } }这里 cron 表达式表示每天 9 点执行。“未来 7 天到期”的条件在 SQL 里可以写成end_date between today and today7同时卡片状态要是正常状态避免重复提醒已经过期或停用的卡。如果项目在本地调试时希望看到效果可以把 cron 改成每分钟执行一次填数据时把到期日设置成明天。演示完再改回正常频率或者直接手动调用接口触发执行。补充一点单机部署下 Spring Task 没有问题。如果以后做集群部署同一个定时任务会在每个节点各执行一次这时候就要引入 XXL-JOB 之类的分布式调度平台或者利用数据库分布式锁。这个可以放进论文的“展望”部分我不建议在毕设项目里做大而全的实现。4.5 报表统计接口的一个细节报表查询经常涉及按日期分组。如果只想统计会员注册数量注意时区问题。使用date_format(create_time, %Y-%m)时间是 MySQL 服务器本地时间一般没问题。但在画折线图时缺失的月份也要补 0否则图表会是断线的前端还要自己做补点比较麻烦。我采用的做法是在后端先构造近 12 个月的月份列表然后查询结果转成 Map最后遍历列表把缺失月份补 0。这样返回给前端的数据永远是一条连续曲线。代码量不大但用户体验好很多。同理统计课程预约率时booked_count / capacity要做除法。别忘了两个字段都是整型直接除会得到整数结果。要么先乘 1.0要么用cast转成小数再保留一位百分比。这是个很小但常见的 SQL 细节。5. 开发过程中必须跨过的坑我帮你列一份避坑清单5.1 Spring Boot 版本太高教程全部对不上怎么办开始我用的是 Spring Boot 3.2。本来觉得版本新挺好结果跟着老教程写代码时javax.servlet全变成了jakarta.servlet很多第三方依赖的兼容也很麻烦。如果本地只需要跑学习项目没有特别的需求建议先选 Spring Boot 2.7.x。如果已经用了 3.x 也没关系你在导入下面这些包时换掉前缀就行javax.servlet - jakarta.servlet javax.annotation - jakarta.annotation javax.persistence - jakarta.persistence但这个替换过程很烦人尤其对新手不友好。我更推荐刚开始学习就锁定一个版本不要跟着最新版最新走。做毕设求的是稳不是新。5.2 “源发行版 17 需要目标发行版 17”这类编译报错这个报错十有八九是本地 JDK、Maven 编译级别和项目设置不一致导致的。例如电脑里装了 JDK 17而 Maven 配置或 IDE 的 project structure 还停留在其他版本编译时就会提示 source/target 版本不匹配。解决办法是确认三处保持一致pom.xml 里的java.version设置IntelliJ IDEA 的 Project SDKMaven 的settings.xml或者 IDE 的 Java Compiler 设置如果项目想要稳定建议统一使用 JDK 8 或 JDK 17然后在 pom.xml 中显式声明properties java.version8/java.version maven.compiler.source8/maven.compiler.source maven.compiler.target8/maven.compiler.target /properties我之前踩过这个坑后每次创建新项目第一件事就是检查 IDE 右下角 SDK 和 pom 中的版本是否一致。如果你也使用 JDK 17那么把 java.version 改为 17 同样没问题关键是不能互相矛盾。5.3 JSON 序列化时无限递归一开始我把实体之间的关系全部用对象直接关联比如排期实体里有课程对象课程对象里又有排期列表。这样 MyBatis-Plus 查询时会尝试把对象序列化给前端结果形成 A 包含 B、B 包含 A 的循环直接导致接口响应时栈溢出。这类问题推荐两种解决方式在实体字段上加JsonIgnore打断关联关系的序列化或者用 VO/DTO 把实体转换成扁平结构返回不让实体直接给前端。我更推荐第二种方案。因为它把内部数据结构和外部接口隔离开以后改表结构不用影响前端。虽然写代码时多一点转换量但对于管理系统来说值得。毕竟不管使用哪种方式都不该把数据库实体原样返回给前端。5.4 事务没生效回滚没反应代码经常出问题明明加了Transactional但方法中执行完两步操作后第二步报错第一步却仍然提交了数据。最常见的原因是事务方法被同类内部调用。Spring 的事务基于代理如果在一个类的方法 A 里直接调用同类的方法 BB 上写Transactional是无效的因为调用并没有经过 Spring 生成的代理对象。public void cancelBooking(Integer bookingId) { // 这里直接调用同类方法事务不会生效 updateBookingStatus(bookingId); } Transactional public void updateBookingStatus(Integer bookingId) { ... }要解决就要保证事务方法是从类外部调用的或者把updateBookingStatus放到另一个 Service 里通过注入去调用。还有一点Spring 默认只对运行时异常回滚如果方法中捕获了Exception而不重新抛出事务也会失效。所以在事务方法中不要随意吞掉异常。最后数据库本身也必须是支持事务的引擎。在 MySQL 8 中默认 InnoDB 没问题但如果用了 MyISAM 表事务不生效需要把表引擎切换成 InnoDB。这个坑很隐蔽检查起来可以先查一下表 engine。5.5 Mapper XML 没被扫描到启动就报找不到使用 MyBatis-Plus 时如果 XML 文件放在src/main/java下面Maven 默认不会把*.xml打包到 target 中运行时会提示 Invalid bound statement。最简单的处理是把 XML 放在src/main/resources/mapper/目录下并在 application.yml 里配置mybatis-plus: mapper-locations: classpath*:/mapper/**/*.xml如果你习惯把 XML 放在 Java 目录下也可以在 pom.xml 的build标签中加上resources配置把 XML 也纳入编译资源。但对于新手我更推荐使用 resource 目录干净又不容易配错。5.6 不要把所有列表查询都 Load 到内存里一开始我图省事课程列表先查出所有排期再在内存中判断有没有约满。这种写法在小数据量下能跑但等我把演示数据填充到上千条之后页面明显变慢。正确的处理是把过滤条件放到 SQL 中让数据库先过滤。例如查询我现在能预约的课可以写条件排期状态正常、结束时间大于当前时间、已预约人数小于容量。MyBatis-Plus 的QueryWrapper支持这种条件。LambdaQueryWrapperCourseSchedule wrapper new LambdaQueryWrapper(); wrapper.eq(CourseSchedule::getStatus, 1) .gt(CourseSchedule::getStartTime, LocalDateTime.now()) .apply(capacity booked_count) .orderByAsc(CourseSchedule::getStartTime);这个apply把自定义 SQL 拼进去非常方便。但注意字段名之间不要带空格或者用反引号否则容易拼出语法错误。遇到这种列表页接口尽量让 SQL 把分页和过滤一起完成后端只负责把查询结果给前端。所有列表查询都无脑查全表再过滤数据量一大就爆炸。6. 演示与答辩准备的实用建议6.1 演示流程提前设计别一上来点菜单答辩演示时最忌讳一上来就展示“会员管理新增”、“教练管理删除”看得人头昏脑涨。你应该按业务故事走先登录管理员后台看数据看板总览再点进课程排期新增一节课程然后切换会员视角模拟一个会员进来约这节课最后回排期后台查看预约名单。演示数据非常关键。我建议提前写一个数据初始化 SQL放 10 个会员、5 个教练、8 门课程、未来一周的排期以及几张即将到期的会员卡。这样到演示当天定时任务可以看到提醒被触发预约容量也能立刻看到效果。6.2 答辩时可能会被问到哪些问题结合我做这个项目的经验老师大概率会问下面几个问题建议提前准备好如果两个人同时预约最后一个名额你怎么保证不超卖为什么在逻辑上删除排期而不直接物理删除事务加了注解为什么有时候不生效会员到期后系统是怎么做到不能继续约课的课程列表查询如果数据量很大你怎么优化你的权限校验是怎么实现的每个问题都不用讲很深但要讲清楚自己的实现方案和理由。比如超卖问题你可以说我在 SQL 里用条件更新capacity booked_count时才能把人数加一这个 update 本身就是原子操作所以不会超卖。老师听到这个回答通常就满意了。6.3 项目后续可以继续扩展什么如果想给项目加分可以在代码已稳定后提三个合理的扩展点会员端增加微信小程序后端把现在的接口按小程序风格适配进入课室时通过会员卡号或二维码扫码签到替代人工点名使用 Redis 缓存课表和热门课程并引入消息队列处理预约高峰。这些内容就算不写代码写进论文的“后续展望”也有说服力。不过核心主体功能已经完成这些扩展不要做成项目的主线否则容易把工期拉爆。另外再分享一个小技巧整个项目过程中养成写开发日志的习惯不需要长篇大论每天记几条“今天做了什么、踩了什么坑、怎么解决”。到写论文的时候这些记录就是你最好的素材很多细节当初不记等过一个月再回忆真的想不起来。我个人每次做完这类项目最值钱的部分往往不是代码本身而是那份踩坑记录。健身房管理系统这个题目看起来很传统但只要把业务跑通、把边界条件写清楚、把异常情况处理明白它给人的感觉就是一套真正的系统而不是一张表接一张表的页面展示。
返回列表