ARTICLE DETAIL

资讯详情

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

SpringBoot健身管理系统实战:会员、课程与预约核心架构拆解

SpringBoot健身管理系统实战:会员、课程与预约核心架构拆解 健身场馆用上管理系统多数人第一反应是这不就一套会员登记软件吗。真正做过健身服务管理系统之后你会发现麻烦全在业务规则里会员卡有次卡、月卡、年卡、私教课包每种卡的计费逻辑完全不同团操课要排期、要限人数、还要防止同一时段教练冲突私教预约更是牵扯到教练的课时费结算。项目表面是CRUD实际是把健身房的线下运营规则完整搬到线上。我这次基于SpringBoot做了一套健身服务管理系统从需求梳理、表结构设计到核心功能开发、前后端联调再到部署优化完整走了一遍。这篇文章不写虚的直接把设计思路、关键实现和踩坑经验拆开讲适合正在做毕设、或者刚入门想拿真实业务练手的同学参考。1. 先从痛点出发健身场馆到底缺一套什么样的系统1.1 管理现状会员、课程、私教三本账很多中小型健身房的日常管理还是Excel 微信群 纸质签到表三件套。会员开卡靠前台手工登记课程预约靠会员群里接龙私教课约课靠教练自己记在小本子上。表面看没啥大问题一旦场馆会员超过几百人、团课一天排五六节问题就全暴露了会员卡到期时间记不清经常出现卡已经过期还在上课的情况团操课预约没有人数限制热门课程超员了才发现私教课时费结算靠月底人工对账教练和场馆之间经常为课时数扯皮会员的体测记录、锻炼记录散落在各个教练手里换一个教练就跟失忆一样。这套系统要解决的就是这三本账的数字化把会员资产、课程资源、教练产能全部纳入统一管理让每一笔开卡、每一次预约、每一节消耗都有据可查。1.2 系统定位与建设目标明确了痛点系统边界就清楚了。我把它定位成一套场馆运营后台 会员自助端的双端系统覆盖健身房最核心的日常运营场景。模块解决的问题核心对象会员管理会员档案、开卡续卡、卡状态跟踪会员、会员卡课程管理团操课排期、约课、签到、人数控制课程、课程排期私教管理私教课包、约课、教练课时统计私教课包、预约记录场地设备管理场地时段占用、设备报修状态场地、设备运营统计营收、到店、课程热度、教练业绩各类统计报表技术侧的要求也很明确后端用SpringBoot前端用Vue做前后端分离数据库用MySQL缓存和分布式session交给Redis文件上传走MinIO——这套组合是目前做单体应用最成熟稳妥的方案社区资料多、遇到问题好查后续接毕业设计答辩也说得清楚。2. 需求分析与功能模块拆分把业务拆成可开发的边界2.1 前台用户端与后台运营端的功能清单需求分析阶段最容易犯的错就是想到哪写到哪。我的做法是先按用户角色画功能清单再逐条确认业务规则。用户端小程序或H5注册登录、个人信息维护浏览团操课程表在线预约/取消预约查看自己的会员卡信息、剩余次数、到期时间私教课约课、查看教练介绍与可约时段体测数据记录与历史对比。运营后台会员管理新增会员、开卡、续费、冻结、退卡课程管理课程分类、排期、教练安排、约课人数设置私教管理私教课包配置、预约审核、课时核销订单管理线上购卡订单、续费订单、退卡退款统计报表营收日报/月报、会员增长趋势、课程预约率、教练课时排行系统管理员工账号、角色权限、操作日志。2.2 核心业务流程预约→上课→扣卡→对账梳理业务流程时我画了一条主线会员买卡 → 约课 → 到店签到 → 上课 → 扣减次数/时长 → 生成消耗记录 → 后台对账。这套流程里有三个关键状态需要全程跟踪会员卡状态正常 → 预约扣锁 → 上课核销 → 次数变化课程排期状态可约 → 预约中 → 已满/已截止 → 已结束 → 已核销教练状态空闲 → 已被预约 → 上课中 → 休息。这些状态不是在数据库里存个字段就行而是要通过代码逻辑保证状态流转只有一个方向。比如一张次卡会员预约了团课在已预约状态下卡里的次数不能被其他预约重复锁定否则就会出现超卖。2.3 一个关键取舍会员卡类型与计费规则建模健身房的会员卡花样非常多如果你尝试把所有卡型做成一张表字段会被撑爆。我采用了卡项模板 会员卡实例的建模方式这也是实际项目里最常用的做法。卡项模板定义这个卡是什么——卡名称、类型次卡/期限卡、有效时长、总次数、价格、适用课程范围会员卡实例会员购买后生成的卡有卡号、开始时间、结束时间、剩余次数、状态。举个例子一张季卡就是期限卡只记录开始和结束时间不限制次数一张30次私教课包就是次卡保留了剩余次数同时绑定了指定教练。这套模型的好处是新增卡型不需要改表结构只在卡项模板里加一条配置就行。后面接线上支付、做营销活动比如拼团卡、体验卡也都是往卡项模板里扩展。3. 技术选型与工程结构SpringBoot生态里怎么搭配最稳妥3.1 后端技术底座SpringBoot MyBatis MySQL技术栈的选择我坚持一个原则不求新只求稳。毕业设计和中小型项目最怕的是技术选型太冷门出了问题连踩坑记录都搜不到。SpringBoot提供自动装配、内嵌Tomcat、Starter机制几分钟就能起一个Web服务。版本上我用的是SpringBoot 2.7.x这个版本成熟稳定和MyBatis、Spring Security的兼容性都验证过很多次。MyBatis MyBatis-PlusMyBatis负责SQL控制权复杂多表查询自己写SQLMyBatis-Plus提供通用Mapper和条件构造器单表CRUD不需要手写SQL开发效率能提升一大截。MySQL 8.x业务数据存储事务支持、行级锁都是刚需。数据库连接池用Druid既做连接管理又能监控慢SQL。Redis课程预约、验证码、热点数据缓存。之所以引入Redis是因为课程预约这类高频读操作如果直接打MySQL到了晚上热门课程开放预约的时间点数据库压力会非常大。3.2 中间件选型Redis缓存、MinIO文件存储的引入理由Redis在这个项目里干了三件事都不复杂但很有代表性预约防超卖用Redis的原子递增操作扣减课程剩余名额验证码存储短信/邮件验证码设置过期时间存Redis最合适数据缓存课程表、首页轮播图、热门课程列表这些不常变的数据缓存后接口响应时间从几十毫秒降到几毫秒。MinIO是后面加上去的。系统里需要处理会员头像、教练形象照、课程宣传图一开始存服务器本地目录部署到容器环境后就傻了——容器一重建文件就没了。换MinIO之后文件走对象存储API和数据库分离一个私有部署的对象存储服务Java SDK接入简单Nginx反代一下就能外网访问。热搜词里有人专门搜minio加入到springboot可见这个需求确实普遍。3.3 工程目录结构与分层设计工程结构直接决定后期维护体验。我用的标准分层结构gym-system ├── src/main/java/com/gym │ ├── controller # 接口层只做参数接收和结果返回 │ ├── service # 业务层核心业务逻辑 │ ├── mapper # 数据访问层MyBatis接口 │ ├── entity # 实体类 │ ├── dto # 参数传输对象避免实体直接暴露给前端 │ ├── vo # 视图对象聚合多表查询结果 │ ├── config # 配置类Security、Redis、MinIO等 │ ├── common # 统一结果封装、异常处理、工具类 │ └── GymApplication.java ├── src/main/resources │ ├── mapper # MyBatis XML文件 │ ├── application.yml │ └── application-dev.yml # 环境差异化配置这里特别想强调一点实体类entity不要直接返回给前端。很多新手图省事把数据库实体直接序列化返回结果要么把密码字段暴露出去要么字段名和前端需要的不一致。我习惯加一层VOController层只负责组装VO这样做还有个好处——多表查询的时候可以聚合多个实体的字段到一个对象接口设计更干净。4. 数据库设计表怎么建才能扛住健身房的真实业务4.1 核心表梳理会员、员工、课程、订单、卡项数据库设计是这套系统的地基。我前后调整了三次表结构第一次只考虑功能能不能跑通第二次考虑了并发预约第三次才把财务对账表加进来。最终核心表一共有12张这里挑几张最有代表性的说。会员主表member字段类型说明idbigint主键phonevarchar(20)手机号登录账号唯一索引passwordvarchar(100)BCrypt加密密码namevarchar(50)姓名gendertinyint性别birthdaydate生日height / weightdecimal身高体重体测用member_typetinyint区分普通会员/私教会员statustinyint状态正常/冻结/注销create_timedatetime注册时间会员卡表member_card记录了卡项快照、卡号、有效期、剩余次数、状态。特别注意要冗余一个card_snapshot字段存购买时的卡项信息JSON格式因为卡项后续可能调价或改规则会员的卡权益应该以购买时的快照为准。课程排期表course_schedule是课程模块的核心。字段包括课程ID、教练ID、开始时间、结束时间、预约截止时间、总名额、已约名额、状态。已约名额这个字段在并发场景下是热点更新字段也是防超卖的主战场。预约记录表appointment记录哪个会员通过什么渠道预约了哪节课程同时关联会员卡ID因为扣次是从这张卡扣的。订单表orders所有涉及钱的交易都走订单表。开卡、续费、退卡各生成一条订单订单状态包含待支付/已支付/已退款/已关闭。4.2 几个容易踩坑的字段设计与索引策略数据库设计里最容易踩坑的不是表不够多而是字段类型选错和索引建错。第一个坑金额字段用float还是decimal。答案是必须用decimal。浮点数在MySQL里是近似存储一分钱误差在财务场景是不可接受的。我统一用decimal(10,2)如果涉及更精确的分账则考虑decimal(18,4)。第二个坑手机号当登录名要不要加唯一索引。废话当然要。但要注意手机号长度国际区号加手机号最长可能到20位varchar(20)是底线。第三个坑日期字段用datetime还是timestamp。业务系统需要处理时区计算我统一用datetime避免timestamp在2038年问题虽然还早但设计习惯要养成。排期类功能要精确到分钟级别日期格式不要省掉时分秒。索引方面我给所有外键字段都建了普通索引——member_card表的member_id、appointment表的member_id和schedule_id、schedule表的course_id和coach_id。另外给订单表建了order_no唯一索引业务上要求订单号不允许重复。预约记录表建了(schedule_id, member_id, status)联合索引这是最频繁的查询组合——查某个会员对某节课的预约状态。4.3 预约与扣次的事务一致性设计预约课程这个操作涉及四张表的状态变化校验会员卡、校验排期名额、写预约记录、扣卡剩余次数。这四步必须在一个事务里完成任何一步失败都要全部回滚。但是光靠数据库事务还不够并发请求下会出现都查到了剩余1个名额同时都写入预约的问题。这里有两条路乐观锁在schedule表的已约名额字段上做版本控制UPDATE course_schedule SET booked_count booked_count 1 WHERE id ? AND booked_count total_count受影响行数为0说明预约失败悲观锁SELECT ... FOR UPDATE锁定排期行直到事务结束才释放。我最终选的是乐观锁理由很简单课程预约的冲突窗口很短乐观锁的冲突重试最多发生一次而悲观锁会阻塞后续所有预约请求在高并发时段会拖慢响应速度。配合Redis预扣名额 数据库乐观锁兜底既保证了响应速度又保证了最终一致性。这段逻辑后面细说。5. 核心功能模块开发实现先跑通会员注册与课程预约5.1 会员注册与登录JWT认证的落地代码用户认证这块我用的方案是Spring Security JWT。会员端和运营后台共用一套认证框架通过不同角色区分权限范围。注册流程很简单前端传手机号 验证码后端校验验证码Redis里取再校验手机号是否已注册然后BCrypt加密密码入库。注意密码绝对不能明文存。登录成功后后端生成JWT返回前端。JWT里放了userId和role后续请求通过拦截器解析token把用户信息放进ThreadLocal方便业务层随时取当前用户。// 配置类中放行白名单其余请求全部需要认证 Override protected void configure(HttpSecurity http) throws Exception { http.csrf().disable() .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS) .and() .authorizeRequests() .antMatchers(/api/auth/register, /api/auth/login, /api/course/list).permitAll() .antMatchers(/api/admin/**).hasRole(ADMIN) .anyRequest().authenticated() .and() .addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class); }这里有个容易被忽略的问题JWT是无状态的一旦签发无法主动失效。如果用户想退出登录只能等token自然过期。解决办法是把token的jti唯一标识存一份到Redis设置与token相同的过期时间退出时删除这个key拦截器里校验这个key是否存在。提示JWT的过期时间不要太长前后端分离场景一般2小时合适。项目里遇到过有人把过期时间设成7天结果会员手机丢了别人拿着token还能继续约课处理起来非常被动。5.2 课程排期与预约防止超卖的业务层控制课程预约是这套系统里并发压力最大的接口。每周日晚八点开放下周课程预约一瞬间可能上千个请求进来。我当时写第一版直接操作数据库结果压测到500并发的时候预约成功率只有七成——不是接口挂了是大量的行锁等待导致请求超时。第二版改成了Redis预扣 数据库兜底的双层方案public AppointmentResult bookCourse(Long scheduleId, Long memberId, Long memberCardId) { String lockKey course:book: scheduleId; String quotaKey course:quota: scheduleId; // 1. Redis原子操作预扣名额 Long remain redisTemplate.opsForValue().decrement(quotaKey); if (remain null || remain 0) { // 名额不足回补后返回 redisTemplate.opsForValue().increment(quotaKey); return AppointmentResult.fail(课程名额已满); } try { // 2. 数据库事务校验卡状态、插入预约记录、扣减次数 return transactionTemplate.execute(status - { MemberCard card memberCardMapper.selectById(memberCardId); if (card null || card.getStatus() ! 1) { status.setRollbackOnly(); return AppointmentResult.fail(会员卡无效); } // 乐观锁更新已约名额 int updated courseScheduleMapper.batchBook(scheduleId); if (updated 0) { status.setRollbackOnly(); return AppointmentResult.fail(课程名额已满); } appointmentMapper.insert(...); memberCardMapper.deductTimes(memberCardId, 1); return AppointmentResult.success(); }); } catch (Exception e) { // 3. 失败回补Redis名额 redisTemplate.opsForValue().increment(quotaKey); throw e; } }这段逻辑的核心是Redis扣减成功不代表预约成功数据库事务失败后必须回补名额否则会出现Redis显示有位置、数据库里却没记录的情况。很多新手写到这里只做了Redis扣减没做失败回补线上就会出严重的数据不一致。5.3 私教课预约与教练排班私教预约和团课预约的差异点在于团课是课程维度锁名额私教是教练维度锁时间。一个教练同时段不能被两个会员约走所以私教预约的表设计多了coach_id start_time end_time的联合唯一索引从数据库层面保证时间不冲突。教练排班这块我加了一个教练可约时段表coach_available_slot教练可以提前维护自己下周哪些时间段可以带课。会员约课时只能看到这个教练已开放的时段而不是任意时间段都能约。这样做的好处是业务规则清晰教练的休息时间能得到保障系统也不用在校验教练是否在休息上做过多复杂判断。私教课包的设计和团课卡也做了区分。团课扣的是会员卡里的次数私教扣的是课包次数。两者在数据库里是不同的表但都统一定义了一个扣次接口用策略模式根据卡类型分发到不同的扣减逻辑Component public class DeductStrategyFactory { private final MapString, DeductStrategy strategyMap; public DeductStrategy getStrategy(String cardType) { return strategyMap.get(cardType); } }这个模式在系统里很好用后面如果新增了周卡畅玩卡这些新卡型只需要新增一个实现类注册进去不需要改动原有代码。6. 后台管理与统计报表运营人员真正关心的那些界面6.1 会员卡续费、转卡、退卡的处理逻辑运营后台最复杂的不是预约是会员卡生命周期管理。续费、转卡、退卡这三件事每件都牵扯财务数据做的时候要格外小心。续费我处理的方案是在原卡上延长有效期或增加次数同时生成一条续费订单。续费不走新建卡流程因为会员卡ID要保持不变卡号也不变否则会员端的卡历史记录就断了。转卡A会员把卡转给B会员需要同步修改会员卡绑定的member_id并且生成一条转卡记录。转卡时要注意如果卡绑定了私教课包课包是否一起转我最后定的规则是——全部资产一起转但是双方的教练关系不转B会员可以重新选择教练。退卡退卡要和结算打通。按剩余时长/剩余次数折算金额同时标记原卡状态为已退卡生成负向订单。这里最值得注意的问题是退卡之后该会员所有未上课的预约必须批量取消并且释放课程名额。我第一版漏了这一步结果退卡的会员名下还有预约记录开场前系统发现他卡无效但没有自动取消导致课程人数满员但实际有人来不了。6.2 课程签到与上课次数核销课程签到做的是二维码核销方案。会员到店后出示小程序里的上课二维码前台或教练用管理端扫码枪扫一下后端核销预约记录并更新会员卡剩余次数。这里有个业务上要仔细想的地方预约时已经扣次签到时还要不要扣我见过的两种方案都各有道理方案A预约时不扣次签到时扣次。问题会员约了不来白占一个名额方案B预约时扣次签到只是确认到场。问题会员临时取消预约要原路退回扣次。我采用方案B因为健身房的思路是名额资源比次数更稀缺宁可先锁定次数不来可以退到库存里。但要注意取消预约的退次必须和预约是同一张卡如果期间会员换过卡需要做卡次迁移逻辑。签到核销的代码需要注意幂等性防止用户连续刷新二维码导致重复核销。我在appointment记录上加了checkin_status字段核销时用了带条件的更新UPDATE appointment SET checkin_status 1 WHERE id ? AND checkin_status 0更新行数为0说明已经核销过直接返回已签到避免重复扣次。6.3 运营统计营收、到店率、课程热度统计报表是后台里看起来简单做起来难的部分。难不在SQL难在统计口径。举个例子营收到底是按订单支付时间算还是按上课时间算如果是跨月报表数据会差很多。我的做法是两类报表分开财务口径报表按订单支付时间统计每日营收、月营收、充值金额、退款金额对账用运营口径报表按实际上课时间统计到店人次、课程预约率、教练课时数做运营分析用。SQL层面直接使用聚合查询例如课程热度报表SELECT c.category_name, COUNT(DISTINCT a.member_id) AS member_cnt, COUNT(a.id) AS book_cnt, ROUND(COUNT(DISTINCT a.member_id) / COUNT(DISTINCT s.id), 2) AS avg_per_class FROM course_schedule s LEFT JOIN course c ON s.course_id c.id LEFT JOIN appointment a ON a.schedule_id s.id WHERE s.start_time BETWEEN #{startTime} AND #{endTime} GROUP BY c.id ORDER BY book_cnt DESC统计类报表要注意控制查询时间范围千万别允许前端一次性查一年的明细再聚合。我在后台做了强制限制明细查询最多查三个月月报表最多查一年。这个限制在文档里写清楚上线后运营没用出问题反而觉得查询更快了。7. 性能、安全与部署上线之前必须处理的几件大事7.1 接口安全与权限控制细化前后端分离项目接口安全不能只靠前端隐藏按钮来解决。运营后台的每个接口都要做细粒度的权限控制比如普通员工只能操作会员查询、课程预约、签到核销教练只能查看自己的排期和自己的学员店长/管理员全部权限包括财务、报表、员工管理。Spring Security里我用PreAuthorize注解做方法级权限控制和维护硬编码在代码里的if (user.role ADMIN)相比声明式权限让权限逻辑一目了然也不会散落在业务代码里。另一个安全重点是接口参数校验。用Validated注解配合DTO上的NotNull、DecimalMin这些约束把参数错误挡在进入业务层之前。我见过太多项目把参数校验散落在Controller里写一堆if-else代码又长又容易漏。7.2 缓存与热数据优化系统稳定运行之后性能优化的重点集中在两个热点课程表接口会员端高频访问一周的课程数据基本不变用Redis缓存整个周课程列表定时任务每10分钟刷新一次。课程列表接口响应时间从80ms降到了8ms以下。会员卡信息查询每次会员进入个人中心都要查卡信息多张表关联查询。我的处理是会员卡主信息直接存Redis用memberId做Key卡片变更操作时主动更新缓存而不是等过期。缓存更新策略选的是Cache Aside Pattern旁路缓存读的时候先读缓存读不到再读数据库并回填写的时候先更新数据库再删除缓存。这个模式最通用也最容易理解。要注意的是先更新数据库再删缓存的顺序不能反否则并发下会出现旧值回填缓存的问题。7.3 打包部署与配置管理部署环节我踩过一次版本坑。项目最开始用SpringBoot 3.0因为JDK要求必须是17而服务器上装的是JDK 8导致部署起来很被动。后来整个项目降到SpringBoot 2.7 JDK 8一切顺畅。这里给个建议做毕业设计或者中小型项目不要盲目追SpringBoot新版本先确认目标环境支持什么JDK。部署方式是打包成jar后用Docker部署FROM openjdk:8-jdk-alpine COPY gym-system.jar /app/gym-system.jar ENTRYPOINT [java, -jar, /app/gym-system.jar]配置管理上我把不同环境的配置拆分到application-dev.yml、application-prod.yml启动时通过--spring.profiles.activeprod指定环境。密码、密钥这些敏感信息不要写在配置文件里提交到代码仓库用环境变量注入spring: datasource: url: ${DB_URL} username: ${DB_USERNAME} password: ${DB_PASSWORD}前端打完包之后我直接放到Nginx里做静态资源托管再用Nginx反向代理/api路径到后端服务顺带解决跨域问题。部署拓扑很简单Nginx 一个SpringBoot容器 MySQL Redis。数据库服务器单独放在内网不直接暴露公网端口。8. 实际开发中遇到的坑与解决经验8.1 SpringBoot版本兼容性别被新版本带偏之前提到SpringBoot版本问题再展开说说。网上很多教程用的SpringBoot 3.x配合Jakarta命名空间当你搜到一段代码是javax.persistence开头和本地jakarta.persistence对不上编译就是不通过。对于新手我强烈建议SpringBoot 2.7.x JDK 8的组合能查到的资料最多、遇到的坑最少。热搜词里一堆人在搜springboot版本太高的问题说明这确实是普遍现象。8.2 事务失效的几个隐蔽场景事务注解Transactional用起来简单但有几个坑真的是不踩不知道同类内部调用一个类里的A方法调用B方法两个方法都加了TransactionalB的事务是不生效的。Spring的事务是基于代理实现的内部调用走的是this引用而不是代理对象。解决办法是拆到不同Service类或者注入自身代理。异常被吞事务回滚只会发生在抛出RuntimeException或Error的时候如果代码里catch住了异常不往外抛事务不会回滚。一定记得在catch块里手动TransactionAspectSupport.currentTransactionStatus().setRollbackOnly()或者把异常继续往外抛。非Spring管理的连接Transactional只对Spring容器管理的bean生效自己new出来的对象没有代理加注解也没用。8.3 前后端联调中的常见摩擦前后端分离开发联调阶段最容易出问题。几个我的经验总结统一返回结构后端定义统一的ResultT结构包含code、message、data三个字段。成功和失败都走这个结构前端就不用针对不同接口做各种奇怪的判断。时间格式约定Java的LocalDateTime和JavaScript的Date默认序列化格式不一致会在前端显示成2025-06-12T14:30:00这种带T的字符串。我在SpringBoot里全局配置了时间格式化统一返回yyyy-MM-dd HH:mm:ss。空值处理Java返回null字段前端取到是正常操作但小程序里有些组件对undefined比较敏感。我和前端约定分页列表里的字段必须给默认值不能返回null。8.4 Redis缓存预热与穿透问题课程预约场景下Redis的Key如果加载时机不对会出现缓存穿透。比如会员端访问课程详情接口第一次有人请求的时候Redis还没有数据所有请求同时落到MySQL数据库直接被打满。我的解决方法是缓存预热定时任务每天凌晨把第二天的课程数据预加载到Redis同时设置合理的过期时间。另外加了一个简单的防穿透方案——查询结果为空时也缓存一个空值占位设置较短过期时间避免恶意请求反复穿透。缓存雪崩问题我在设计时也有意识地规避不同课程详情Key的过期时间加了一个随机偏移量防止同一时刻大规模缓存同时失效。8.5 文件上传与MinIO的集成细节项目里需要处理课程图片上传前后端分离的方案里文件上传一般有两种做法前端直接把文件POST到MinIO后端签发临时上传凭证前端先把文件传到后端后端再转存到MinIO。我选了方案2原因是后端转存可以做统一的格式校验和文件类型限制并且用户上传记录能写到数据库里后续排查问题有据可查。MinIO集成本身不难引入minio依赖后配置endpoint、accessKey、secretKey、bucketName即可。有一个细节容易忽略MinIO的endpoint地址要注意区分容器内部访问地址和外部访问地址。Docker部署时MinIO容器内用http://minio:9000但浏览器访问文件URL要用http://服务器IP:9000。如果混着用就会出现文件上传成功了但前端加载不出来的情况。8.6 分布式会话与登录态换一种更轻的认证方案项目最开始考虑过用Spring Session Redis实现分布式会话共享毕竟前后端分离部署后如果以后要扩多个实例session共享是个问题。但这套系统是单体应用部署一个实例就够了引入Spring Session反而增加复杂度。最终认证方案是JWT无状态化Redis里只存token的jti做主动失效控制这样既保证了扩展性又没增加额外的Session管理负担。如果你的系统以后确实要上多实例JWT本身是无状态的特性反而更方便——每个实例只需要共享同一个Redis就能完成token失效校验。9. 系统测试与上线后的数据验证系统开发完之后我做了几轮针对核心流程的专项测试。这里想聊两句测试的思路很多个人项目往往忽略了这一环结果上线后到处都是小毛病。第一轮测的是业务流程完整性用真实场景数据跑了一遍——新人注册、购买年卡、预约团课、取消预约、签到期满、续费、退卡。这一步主要发现问题其实在业务规则的前后一致性比如预约取消后次数是否回填、退卡后课程名额是否释放、签到后报表数据是否实时更新。第二轮测的是并发场景模拟100个用户同时抢预约同一节热门团课。用JMeter压测重点关注预约接口的失败率、响应时间和数据库死锁情况。第一次压测就发现了乐观锁冲突导致部分请求返回请重试时前端没有做重试引导后来在接口返回码里增加了冲突可重试的明确标识。第三轮是权限边界测试普通会员能不能通过手动拼URL访问到管理后台的接口普通员工能不能越权查询财务数据通过给角色配置不同的接口权限并逐一验证确保越权访问被Spring Security拦截。上线后我也保持观察日志和慢SQL记录。前两周重点看预约高峰期的接口响应情况确认Redis预扣确实缓解了数据库压力。慢SQL日志帮我发现了一个常用查询没走索引就是后台会员列表按手机号模糊搜索的那条语句。用LIKE %xxx%无法命中索引后来改成允许用户按手机号前几位查询用LIKE xxx%查询速度从秒级提升到了毫秒级。最后分享一个我个人的体会做这类管理系统真正考验人的不是某个技术点有多深而是你能不能把几十个业务规则拼在一起还让它们不打架。项目做完之后我把每张表、每个核心接口都整理成了文档答辩的时候对着业务流程图讲比堆技术名词要有效得多。如果你也在做类似的项目建议把预约防超卖退卡释放名额统计口径区分这几个细节写在你的项目亮点里这些才是面试官和答辩老师真正感兴趣的东西。
返回列表