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

资讯详情

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

SpringBoot健身房管理系统实战:从数据建模到JWT鉴权与事务控制

SpringBoot健身房管理系统实战:从数据建模到JWT鉴权与事务控制 1. 项目整体设计与技术选型解析1.1 为什么说健身房管理系统是SpringBoot练手的“黄金项目”先聊点实在的。我见过不少准备做毕设或者刚入行想练手的朋友上来就问“什么项目比较合适”。我的回答一直很明确管理系统类项目尤其是健身房、图书馆、酒店这种带真实业务规则的是性价比最高的选择。原因很简单。健身房管理系统表面上是个CRUD项目但它实际上把企业级开发里最常碰到的那些东西都串起来了多角色权限管理员、前台、教练、会员、复杂状态流转会员卡从办卡到续费到过期、事务一致性问题预约私教课要同时扣课时、锁库存、定时任务会员卡到期提醒、教练排班状态更新、报表统计营收、出勤率等等。做完这一个项目SpringBoot的自动装配、MyBatis的持久层操作、Spring事务的传播机制、JWT鉴权这些核心技能基本都能过一遍而且业务场景贴生活讲起来也容易懂。再说回SpringBoot本身。它解决的痛点用过SSM的都懂——大量XML配置、jar包依赖冲突、外部容器部署麻烦。SpringBoot把内嵌Tomcat、自动配置、starter依赖整合这些事全包了真正做到了“写业务代码比配环境多”。配合上像Spring Initializr这种官方脚手架从零起一个可运行的项目只需要两分钟。这也是为什么现在高校的毕设题和中小型公司的内部系统几乎都是SpringBoot的天下。1.2 技术选型SpringBoot版本、持久层框架和前端方案怎么定先给出一份我这套系统实际采用的完整技术清单后面所有讲解都基于这份选型技术栈选型选择理由基础框架SpringBoot 2.7.18稳定、资料多、兼容JDK8避免过高版本带来的配置变更持久层MyBatis-Plus 3.5.3单表CRUD不用写SQL复杂查询用Wrapper效率高数据库MySQL 8.0主流、便宜、团队熟悉8.0以上对JSON、窗口函数支持更好鉴权方案JWT 拦截器无状态、前后端分离友好比Session更适合现代Web应用前端Vue 3 Element Plus和SpringBoot配合成熟管理后台组件齐全上手极快构建工具Maven生态最成熟IDEA内置支持依赖管理直观缓存Redis可选用于验证码存储、热点数据缓存、接口防刷这里重点聊两个选型上的“坑”。第一个是SpringBoot版本的选择。现在搜索框里SpringBoot已经出到3.x了很多新手直接选了最新版然后JDK用的还是8结果项目直接跑不起来。SpringBoot 3.0开始强制要求JDK17同时javax包全部迁移成了jakarta老教程里的import全得换。我做这个项目时选的是2.7.18这是2.x的最终版本既有2.x的大量现成资料可以查又修复了不少老版本的bug对毕设和企业里存量系统升级都是最稳的选择。如果你的环境已经是JDK17那直接上3.x没问题但要注意MyBatis-Plus、一些第三方工具是否已做好适配。第二个是持久层框架选MyBatis还是MyBatis-Plus。我建议无脑选后者。MyBatis-Plus不是取代MyBatis它是在MyBatis基础上做了增强而且不改变原有用法。它有现成的BaseMapper休眠的单表增删改查连SQL都不用写了写复杂查询还能用LambdaQueryWrapper避免字符串硬编码。别的不说就冲着分页插件这一个小功能就能省掉你一大段手写PageHelper配置的心力。至于JPA写简单项目确实快但到多表关联、复杂SQL聚合统计时你就会怀念SQL的灵活性了尤其时面试环节对这块比较敏感用MyBatis体系更能体现你懂SQL。1.3 项目目录结构与分层职责项目结构这块我直接给出实际用的包结构每个包干什么用、为什么这么分都给大家标清楚com.gym.management ├── GymApplication.java // 启动类 ├── controller/ // 接口层只负责参数接收和结果返回 ├── service/ // 业务层核心业务逻辑都在这层 │ └── impl/ ├── mapper/ // 数据访问层继承BaseMapper ├── entity/ // 数据库实体映射 ├── dto/ // 前端交互的数据传输对象 ├── vo/ // 视图对象用于响应前端 ├── config/ // 配置类拦截器、跨域、MyBatis-Plus分页等 ├── common/ // 公共模块统一返回体、异常、常量 ├── utils/ // 工具类JWT、日期处理、手机号校验等 └── job/ // 定时任务我见过不少新手把Controller写得巨肥几百行的业务代码全堆在接口方法里。这样做的后果是测试没法写、复用没门路、出bug了定位要翻半天。所以即便项目不大我也坚持标准的Controller-Service-Mapper三层。这张图的划分逻辑很直白——Controller层只干活“接客”的Service层是“核心大脑”Mapper层专注SQL交互。每层之间通过DTO/VO传递数据而不是直接拿实体类往外抛这样前端字段变了不会牵一发动全身。分层之外还有两个公共组件值得一开始就铺好一个是统一的返回体Result规定了code、message、data三段结构。所有接口都返回这个结构前端axios拦截器里只需要判断一次code是所有前后端联调体验能一致的基础。另一个是全局异常处理器RestControllerAdvice配合自定义异常BusinessException业务校验失败时直接在service层throw由统一处理器转换成果断的HTTP响应。这两个组件应该在写第一个接口前就搭好不然后面每个接口都在用自己的方式返回错误前端得崩溃。2. 核心业务数据建模与后端设计方案2.1 核心数据表结构设计健身房项目的数据模型是整个系统最见功力的地方。行业里常说“CRUD谁都会写表设计才是分水岭”。这个项目我拆出了八张核心业务表它们之间的关系和设计要点如下。会员表和会员卡表是整个系统的地基。会员表存基础信息姓名、手机号、性别、身高体重、体脂率等身体指标、入会时间、来源渠道会员卡表则记录卡号、卡类型月卡/季卡/年卡/次卡、开卡时间、到期时间、赠送时长、剩余次数、卡状态有效/过期/挂失/退卡。这里有两个设计细节容易被新手忽略第一过期时间不要只存一个字段要存开通时间、到期时间、赠送时长三个字段这样续费时才能精确计算剩余价值第二卡状态一定要有“挂失”和“退卡”两种不只是“有效/过期”真实运营场景里这两个状态出现频率极高。课程模块分两张表课程表和排期表。课程表维护课程基础信息名称、类型如瑜伽/动感单车/器械、适合人群、课程介绍、标准课时长排期表记录具体某一天某一时段在哪间教室由哪个教练带的课包含最大人数和已约人数。这里“已约人数”这个字段虽然看似冗余理论上可以用预约记录count出来但实际开发中我会直接存在排期表里因为每次查排期列表时显示“3/15”这种需要count聚合数据量上来后性能很差宁可牺牲一点冗余换取查询性能。私教预约表是业务最复杂的表字段包含预约ID、会员ID、教练ID、预约时间、课时扣减数、状态待上课/已完成/已取消/爽约、取消时间、完成时间。设计时一定记得加唯一索引member_id, appointment_time这是从数据库层面保证同一个会员不能在同一时间段约两节课。教练表则包含教练的擅长领域、资质证书、排班状态和时间段占用信息。订单表用来记录所有涉及钱的操作办卡缴费、续费、私教课购买、商品购买。字段包含订单号、会员ID、订单类型、金额、支付方式、支付状态、支付时间。订单号建议不要用数据库自增主键而是用“时间戳随机数”生成业务订单号因为微信/支付宝对账时要的是全局唯一的业务单号。操作日志表记录所有关键操作字段包含操作人、操作类型、操作内容、IP地址、操作时间。别小看这张表后面排查线上问题、应对客户投诉时它就是你的保命符。2.2 后端核心接口与关键API设计接口设计遵循RESTful风格资源名用复数动作由HTTP方法表达。所有接口统一挂在/api前缀下鉴权接口除外。实际项目中我会把接口划分成三组认证授权接口、核心业务接口、报表统计接口。核心业务接口的URL和职责如下表。接口方法作用权限/api/memberPOST新增会员管理员/前台/api/member/{id}GET查询会员详情管理员/前台/教练/api/member/{id}/cardGET查询会员的卡信息管理员/前台/api/cardPOST办理会员卡管理员/前台/api/card/{id}/renewPOST会员卡续费管理员/前台/api/course/scheduleGET查询课程排期管理员/前台/教练/会员/api/appointmentPOST预约私教课会员/api/appointment/{id}/cancelPOST取消预约会员/前台/api/stats/revenueGET营收统计管理员/api/stats/member-countGET会员增长统计管理员接口设计时有一个原则查询接口尽量支持分页和条件筛选。比如会员列表查询我一般接受pageNum、pageSize、keyword匹配姓名/手机号、cardType、cardStatus这五个参数。为什么这么设计因为前端列表页几乎必然有搜索框、筛选项、分页器你接口不支持前端就得把全量数据拉下来自己过滤数据一多页面直接卡死。别问问就是踩过坑。2.3 核心业务场景的设计思路接下来讲三个贯穿系统始终的关键设计思想。会员卡状态流转。我从这类项目里悟到的核心要领是状态字段不要靠随手改而是定义一个状态机。WAIT_ACTIVE待激活、ACTIVE有效、EXPIRED过期、FROZEN挂失、REFUNDED退卡五个状态之间只能按规则流转。比如挂失的卡只能解挂恢复为有效不能直接退卡退卡必须先解决名下未完成的预约。状态机看起来代码繁琐但它能把业务异常情况挡在源头——用户在前台操作时永远走不到“无效”分支因为你代码里就没给路径。预约冲突与事务一致性。私教预约的经典场景是会员发起预约要同时完成三件事——检查该时段是否冲突、扣减会员剩余课时数、排期已约人数加一。这三件事必须在一个事务里任何一步失败都要整体回滚。实现时我在service方法上加Transactional(rollbackFor Exception.class)同时配合乐观锁更新排期已约人数时用UPDATE ... SET booked_count booked_count 1 WHERE id ? AND booked_count max_count数据库行锁保证并发安全。这里千万别用“先查再改”的方式两个请求同时查到已约人数为14上限15都以为可以预约最后就超卖了。到期检测与定时任务。会员卡到期不能光靠前台查询时判断还要有一个每天凌晨扫描的定时任务把所有即将到期7天内的会员推送短信提醒把所有已过期的卡状态自动改成EXPIRED顺便把今天该上课但没来的预约标记为“爽约”。这些用SpringBoot的Scheduled注解就能搞定配上cron表达式“0 0 2 * * ?”每天凌晨两点执行。定时任务最怕重复执行生产环境如果有多个实例部署两台机器同一时间都跑任务就会重复发短信。解决办法有两种一种是用ShedLock分布式锁包一层另一种是我实际用的——限制任务只能由指定实例执行。3. 实操过程与核心功能实现3.1 从零搭建项目初始化、依赖配置与执行验证这部分我按实操顺序来跟着做就能跑起来。先在IDEA里用Spring Initializr创建项目注意Group填com.gymArtifact填managementJava版本选8或11对应SpringBoot 2.7.x。依赖先勾这几个Web、MySQL Driver、Lombok。后面pom.xml里再手动补充其他依赖。pom.xml里最终的核心依赖清单如下。我给每个依赖都标了作用方便理解为什么要引它dependencies !-- Web场景启动器包含内嵌Tomcat和SpringMVC -- dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId /dependency !-- MyBatis-Plus 的 SpringBoot 启动器 -- dependency groupIdcom.baomidou/groupId artifactIdmybatis-plus-boot-starter/artifactId version3.5.3/version /dependency !-- MySQL 数据库驱动 -- dependency groupIdcom.mysql/groupId artifactIdmysql-connector-java/artifactId scoperuntime/scope /dependency !-- Lombok减少实体类的getter/setter模板代码 -- dependency groupIdorg.projectlombok/groupId artifactIdlombok/artifactId optionaltrue/optional /dependency !-- JWT 鉴权 -- dependency groupIdio.jsonwebtoken/groupId artifactIdjjwt-api/artifactId version0.11.5/version /dependency dependency groupIdio.jsonwebtoken/groupId artifactIdjjwt-impl/artifactId version0.11.5/version scoperuntime/scope /dependency !-- 参数校验 -- dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-validation/artifactId /dependency /dependencies然后写application.yml配置。这些配置项每一个都有讲究写错了启动直接报错server: port: 8080 spring: datasource: url: jdbc:mysql://localhost:3306/gym_db?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/ShanghaiuseSSLfalse username: root password: 你的密码 driver-class-name: com.mysql.cj.jdbc.Driver jackson: date-format: yyyy-MM-dd HH:mm:ss time-zone: Asia/Shanghai mybatis-plus: configuration: map-underscore-to-camel-case: true global-config: db-config: logic-delete-field: deleted logic-delete-value: 1 logic-not-delete-value: 0这里有几个配置细节值得展开。第一数据库连接URL里必须带serverTimezoneAsia/Shanghai不然MySQL 8的驱动跟JVM默认时区不一致查时间字段会有8小时偏差。第二map-underscore-to-camel-case要设置为true这样数据库的create_time字段能自动映射到Java的createTime属性不用手动写TableField注解。第三logic-delete是MyBatis-Plus的逻辑删除功能设置之后删除操作变成UPDATE虽然实际开发中有争议但对管理系统来说“保留痕迹”的价值远大于“物理删除省空间”。配置完成后在启动类里加一个简单的接口验证整个链路是否通。跑起来后访问localhost:8080/api/ping返回OK说明项目骨架已经正常工作。接下来再做MyBatis-Plus的配置类把分页插件注册进去——这一步经常有人忘记做结果分页功能总是返回全量数据Configuration MapperScan(com.gym.management.mapper) public class MybatisPlusConfig { Bean public MybatisPlusInterceptor mybatisPlusInterceptor() { MybatisPlusInterceptor interceptor new MybatisPlusInterceptor(); interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL)); return interceptor; } }3.2 用户登录与JWT鉴权的落地实现登录这块我用的是JWTJSON Web Token方案。它的核心设计是把用户身份信息加密成一段token字符串前端每次请求时在请求头带上后端验证签名后就能确认身份。为什么选JWT而不是传统Session主要是两个原因第一前端是Vue独立部署的登录状态要跨域保存JWT天然支持第二项目以后可能要拆多个服务JWT是无状态的不用考虑Session共享问题。具体实现分成三步。第一是登录接口接收手机号和密码校验通过后生成token返回给前端。这里有一个细节密码存储绝不使用明文我用的是BCrypt加密这是Spring Security家族的标准密码算法每次加密的结果都不一样自带随机盐比MD5那种可破解的散列安全得多。注册时加密存入数据库登录时用BCrypt.matches校验。第二是拦截器配置写一个JwtInterceptor实现HandlerInterceptor在preHandle方法里解析请求头中的token校验签名和过期时间。拦截器注册时要注意登录接口、注册接口要放行静态资源也要放行其余接口全部拦截。public class JwtInterceptor implements HandlerInterceptor { Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { // 放行跨域预检请求 if (OPTIONS.equalsIgnoreCase(request.getMethod())) { return true; } String token request.getHeader(Authorization); if (StringUtils.hasText(token) token.startsWith(Bearer )) { token token.substring(7); try { Long userId JwtUtil.parseToken(token); request.setAttribute(userId, userId); return true; } catch (Exception e) { // token无效或过期统一返回401 } } response.setStatus(HttpStatus.UNAUTHORIZED.value()); response.setContentType(application/json;charsetUTF-8); response.getWriter().write({\code\:401,\message\:\登录已过期请重新登录\}); return false; } }第三是角色鉴权属于拦截器之上的第二层控制。我用自定义注解RequireRole标注在Controller方法上指定访问所需角色。在拦截器里解析注解再把当前用户的角色从token里取出来比对不满足就返回403。这比在每个接口方法里手写if判断要优雅得多新加接口时写好注解就行不会漏配权限。有一个我实际踩过的坑要提醒大家JWT的secret密钥一定要放进配置文件通过环境变量注入这个密钥的长度要够至少32字节否则JJWT会直接报错。我之前偷懒写了个短密钥结果运行时报错“The signing keys algorithm does not match its length”后来改成64位的随机字符串才解决。3.3 会员卡办理与状态流转的实现会员卡办理的流程看似简单——选卡型、付款、开卡但实现时要注意两个细节。第一个是到期时间的计算年卡不是简单的“当前时间365天”而是要加上赠送天数。我的实现是开卡日期加上卡型天数再加上赠送天数同时还要考虑“系统日期走到自然日边界时跨年月份天数不同”的情况。用Java的LocalDate处理这个最方便它会自动处理闰年和平年public LocalDateTime calculateExpireTime(LocalDateTime startTime, Integer durationDays, Integer giftDays) { return startTime.plusDays(durationDays giftDays); }第二个是办理时的事务办卡要写会员卡表、订单表、操作日志表三处任何一处失败都不能让用户“钱付了没卡”。我直接用Transactional(rollbackFor Exception.class)包起来rollbackFor指定Exception.class是因为Spring默认只在RuntimeException下回滚而自定义业务异常通常继承RuntimeException不写这个参数容易被检查异常悄悄吞掉。状态流转这块我的实现方式是在更新状态的方法中业务规则禁止的转换直接抛BusinessException。比如退卡操作必须校验该会员没有未完成的预约否则直接拒绝public void refundCard(Long cardId) { MemberCard card getById(cardId); // 已退卡的卡不能再退 if (card.getStatus() CardStatus.REFUNDED) { throw new BusinessException(该会员卡已退卡不能重复操作); } // 名下有未完成预约的卡不能退 long unfinishedCount appointmentMapper.countUnfinishedByMember(card.getMemberId()); if (unfinishedCount 0) { throw new BusinessException(该会员名下有未完成的私教预约请先处理); } // 退卡时计算未使用天数的退款金额 BigDecimal refundAmount calculateRefund(card); // 更新状态、写退款订单、记录日志 // ... }同样道理挂失卡解挂、过期卡续费都应该有对应的校验逻辑。状态机思维说白了就是在每个状态变更入口“把门”让非法流转没有路可走。这套东西看起来笨但它最大的好处是换一个开发人员来看代码读一遍状态流转规则就能理解全部业务边界不用靠猜。3.4 私教预约功能的事务控制与防超卖预约功能是整个系统并发压力最大、最容易出bug的业务环节。场景是这样的热门教练的一节私教课在放课瞬间可能被多个会员同时抢约必须保证不超卖、不重复。我用的方案是数据库层面加唯一索引业务层面加事务控制更新库存时使用条件更新防超卖。这套组合拳打下来实测并发一百个请求也没有出现数据错乱。具体的实现方式先查排期确认没有满员再插入预约记录时如果数据库有唯一索引member_id appointment_time重复插入会抛DuplicateKeyException捕获后转成友好提示。然后扣课时数时用条件更新Transactional(rollbackFor Exception.class) public void bookAppointment(AppointmentDTO dto) { // 1. 查询排期信息验证未满员 CourseSchedule schedule scheduleMapper.selectById(dto.getScheduleId()); if (schedule.getBookedCount() schedule.getMaxCount()) { throw new BusinessException(该课程已约满); } // 2. 插入预约记录触发唯一索引防重复 try { appointmentMapper.insert(appointment); } catch (DuplicateKeyException e) { throw new BusinessException(您在该时间段已有预约); } // 3. 条件更新排期已约人数防止超卖 int rows scheduleMapper.incrementBookedCount(dto.getScheduleId(), schedule.getMaxCount()); if (rows 0) { throw new BusinessException(该课程刚刚已被约满请选择其他时段); } // 4. 扣减会员卡的剩余课时数 memberCardMapper.deductRemainCount(dto.getCardId()); }取消预约也是同样的处理逻辑而且要考虑的时序更多扣的课时要还回来已约人数要减回去预约状态改成已取消。如果取消的是已经上课的课程那就不叫取消了而是“爽约”课时不返还。这里我提醒一句事务里别写“先查再改”的思维能合并成一个UPDATE语句就别拆成两步这是高并发场景的铁律。我最初实现这个接口时没加唯一索引结果前端双击提交按钮同一秒产生了两条预约记录前台小姑娘被会员质问了两轮才查出来是代码问题。后来老老实实加了唯一索引又在service入口做了“同一会员60秒内不能重复提交同一排期预约”的防抖校验世界从此清净了。3.5 定时任务实现会员卡到期状态自动更新健身房的会员卡背后天然存在“时间管理”需求不能用人工每天去查哪些卡到期了。我在项目里用Scheduled写了一个每天凌晨执行的定时任务完成三件事到期提醒、状态变更、爽约标记。Component Slf4j public class CardExpireTask { Scheduled(cron 0 0 2 * * ?) public void processExpiringCards() { // 1. 查出7天内即将到期的有效卡发送提醒短信 ListMemberCard expiringCards cardMapper.selectExpiringWithin(7); // 遍历发送短信、记录通知日志 // 2. 查出已过期的有效卡状态更新为EXPIRED ListMemberCard expiredCards cardMapper.selectExpired(); for (MemberCard card : expiredCards) { card.setStatus(CardStatus.EXPIRED); cardMapper.updateById(card); // 同步把该会员名下未完成的预约取消并释放教练时段 appointmentMapper.cancelByMember(card.getMemberId()); } } }定时任务的关键细节方法上没参数、没返回值、不能有循环依赖这些是Spring管理定时任务的硬性约束。我写定时任务时会把“查询待处理数据”和“处理逻辑”分开宁可多写两层循环也要保证单一职责。另外任务执行时间要注意避开业务高峰凌晨2点是我从运营那边问出来的低峰期——他们十点半打烊会员不会在这个时间点约课。还有一点很重要定时任务里的异常一定要catch住不能让它冒出方法甚至中断整个任务链。我见过一个定时任务因为一条脏数据抛异常后续所有待处理的卡都不更新了线上会员卡过期了还能继续进店吓出一身冷汗。从那之后我的定时任务统一加了大try-catch每条数据处理失败单独记录到日志表和告警表不让一条脏数据拖垮全量任务。3.6 前端联调与部署细节前端我用的是Vue 3 Element Plus开发时跑在Vite的5173端口SpringBoot跑在8080两边的接口交互必然要处理跨域问题。跨域的本质是浏览器的同源策略解决方式有两种后端CORS配置或者前端通过Vite代理转发。我在开发环境用Vite代理配置在vite.config.js里export default defineConfig({ server: { proxy: { /api: { target: http://localhost:8080, changeOrigin: true } } } })生产部署时我直接把前端build之后的dist目录拷进SpringBoot的static目录让前后端部署在同一个服务中。这样不仅没有跨域问题部署也简单——一个jar包搞定一切。具体的做法是将前端打包后dist里所有文件复制到src/main/resources/static下重新打包就行。联调时有一个极其常见的问题后端返回的LocalDateTime序列化成“2024-01-15T10:30:00”前端却要“2024-01-15 10:30:00”。我在application.yml里全局配置了jackson的date-format和time-zone但如果某个字段用了LocalDateTime而配置没生效可以给字段加JsonFormat(pattern yyyy-MM-dd HH:mm:ss)注解临时指定。这类时间交互的格式问题前端报错时往往显示的是“Invalid Date”排查思路是先看接口返回的原始JSON格式。4. 常见问题与排查技巧实录4.1 项目启动阶段的常见异常把我在实际搭建和帮别人排障过程中遇到的启动问题整理成了一张速查表按照出现频率排序。异常现象根本原因解决方案端口被占用 Port 8080 was already in use上次运行未停止或别的进程占用改端口或查占用进程Windows用netstat -anoAccess denied for user root数据库账号密码错误或权限不足核对application.yml数据源配置检查MySQL用户授权Unknown database gym_db数据库还没创建先执行CREATE DATABASE gym_db再连The server time zone value is unrecognized缺时区配置URL加serverTimezoneAsia/ShanghaiFailed to configure a DataSource数据源配置缺失或依赖不完整检查spring-boot-starter-jdbc是否引入ClassNotFoundException: jakarta.*SpringBoot版本过新代码用了javax包3.x要用jakarta前缀或降级到2.7.x启动报错是新手最慌的阶段我自己的经验是启动日志永远从最后十行往前看Caused by那一段才是根因。SpringBoot启动失败时日志里经常有一大坨堆栈信息真正的元凶在Caused by嵌套的最后几行前面那些全是“被牵连”的信息。还有一个隐蔽的坑隐藏在同名类上如果你新建了跟框架类同名的类项目启动时会出现奇怪的代理异常或类型转换错误。排查方法很简单——检查自己的包路径里有没有System、Properties这种跟JDK类撞名的类。4.2 MyBatis-Plus使用中的高频问题MyBatis-Plus用着爽但第一次上手也会碰到几个奇怪的现象。最常见的一个是逻辑删除配置后自己手写的SQL全失效了——因为你在XML里写的语句没有自动带上deleted 0过滤条件需要手动加。另一个常见问题是分页失效查出来的数据永远是全量这基本就是分页插件没注册成功检查MybatisPlusInterceptor有没有正确放进配置类里。还有个我印象深刻的问题用LambdaQueryWrapper时如果实体属性名跟数据库字段不对应比如实体是剩余次数remainCount数据库是remain_count没开map-underscore-to-camel-case的话查询出来的字段全是null。所以配置里那句map-underscore-to-camel-case: true千万别省。字段自动填充也是MyBatis-Plus的实用功能但配置起来有两个易错点第一创建时间createTime和更新时间updateTime需要实现MetaObjectHandler接口重写insertFill和updateFill两个方法第二实体字段上必须加TableField(fill FieldFill.INSERT)注解否则填充逻辑不会触发。我见过有人配置了MetaObjectHandler但实体上没加注解结果时间字段永远是null找了一下午才发现是漏了注解。4.3 前后端联调的典型问题联调阶段的问题通常不像启动错误那么直接往往表现为“页面数据不对”或“接口报了看不懂的状态码”。我按实战经验整理几类高频问题。JWT鉴权拦截后前端拿到401但页面还在执行后续代码。这个问题本质是axios拦截器没根据状态码做跳转。正确做法是在axios响应拦截器里遇到401就清空本地存储的token并跳转登录页。同时后端拦截器放行OPTIONS预检请求这一点也特别重要忘了放行的话浏览器控制台会一直报CORS错误其实是预检请求被401拦截了。跨域问题表象很迷惑GET请求正常POST请求报跨域。这通常是对预检请求的处理不完整后端CORS配置里除了加Header以外还要明确允许请求方法。在SpringBoot里最简单的方式是直接在拦截器注册处配置CorsConfiguration允许所有来源、所有方法、所有请求头但这只适合开发阶段真正上线时建议允许的来源列表写白名单。日期时间格式不一致是最折磨人的联调问题后端返回“2024-01-15T10:30:00”前端Element Plus的日期组件显示error。这里除了后端全局格式化以外前端axios的transformResponse里再做一层兜底格式化也行但最好的方案还是后端统一成字符串格式再输出前端拿到就是规规整整的展示值别再让前端做二次解析。还有一个业务层面的典型问题会员卡到期后前端还能正常发起预约请求。这是后端没做状态校验导致的——预约接口只查了排期和课时数没检查会员卡状态。正确做法是在预约前校验卡片必须是ACTIVE状态这个校验放在service层而不是controller层因为任何入口进来的请求最终都要走service只校验controller等于给系统留后门。4.4 性能排查的实用思路管理系统通常并发量不算高但也不排除做课表抢课、促销活动时短时间流量猛增的情况。我一般从三个层面做排查。先看SQL。我用MyBatis-Plus自带日志输出SQL或者配置p6spy打印完整执行语句和执行时长。发现页面响应慢时第一个怀疑对象就是SQL有没有多查了不需要的字段、有没有没命中索引、有没有N1查询问题。最常见的N1是查会员列表后循环查每个会员的卡信息本来一条SQL能搞定的事变成了1N条SQL。解决方式很简单列表接口里一次性查出游标下的所有卡信息在内存里做组装。再看事务粒度。事务范围越大锁持有时间越长并发能力越差。我见过同事把文件上传、短信发送这种耗时操作也写进了事务方法里导致高峰期数据库连接池被打满。事务里只放对数据一致性有要求的操作外部调用全挪到事务外面。最后看缓存策略。会员详情、课程排期这类读多写少的数据用Redis缓存起来能明显减负。我的做法是热门排期列表缓存5分钟会员详情缓存30秒修改操作主动更新缓存而不是等它被动过期。Redis在SpringBoot里的接入方式很成熟用spring-boot-starter-data-redis配一下连接信息通过StringRedisTemplate操作JSON序列化后的数据业务上用起来就是简单读写。5. 项目扩展与个人经验体会这类管理系统做完以后很多同学会问我还能加点什么。我根据自己的扩展经验给出几个方向。对接小程序端是最常见也最贴合健身房场景的扩展。现在大部分健身房都有微信小程序会员在小程序上约课查卡后台用SpringBoot对接微信登录、微信支付。技术上新增一套小程序后端的鉴权逻辑和管理端JWT鉴权共存接口复用度很高主要是新增一个用户端视图适配微信生态。引入工作流引擎做审批流也是一个不错的进阶方向。健身房有请假、退款审批这些流程用手写状态标记也能做但用Flowable这类轻量级工作流引擎可以把审批节点配置化。这个选题放在简历上是妥妥的加分项面试官看到“工作流”关键词一般都会来劲。报表可视化可以再升级一个层次。我现在这个项目只做了简单的ECharts柱状图和折线图其实还可以做会员画像分析、课程受欢迎度排行、教练业绩排名后端提供聚合查询接口前端用数据透视表组件展示。“数据驱动运营”这个话术在你的项目答辩PPT里一出现评委的关注点马上就从“这个项目很基础”跳到“这人有点产品思维”。我个人做完整个项目的体会是这种管理系统真正的难点从来不是SpringBoot框架功能本身而是业务规则的完整性和健壮性。很多刚学框架的人以为CRUD写完就算完事实际上边界情况才是拉开差距的地方——卡过期了还能不能约课、教练被调走了已有预约怎么处理、会员退款后剩余课时怎么结算。这些问题想清楚了代码自然就清晰了。最后分享一个小技巧。我给这个系统的每个写操作接口都加了操作日志注解通过AOP在方法执行完成后自动记录操作人、操作内容、操作时间和IP地址。一开始我只是为了排查问题方便结果到了项目答辩环节老师问“你这个系统有哪些亮点”时我直接演示了后台的审计日志页面从登录到办卡到退卡每一步都清清楚楚那种“你考虑到了真实生产环境才有的需求”的印象分远比堆砌再多CRUD接口都有用。这个项目看似只是“健身房管理系统”但它像一面很小的镜子把SpringBoot生态里的核心组件、真实业务里的复杂设计、前后端协作的工程问题全映射出来了。把一个项目吃透比囫囵吞枣做十个项目要值钱得多。你现在拿到的代码量和踩坑经验已经足够撑起一份漂亮的简历和一个有底气的答辩现场。
返回列表