做毕设选来选去不少人最后都会落到“管理系统”这类题目上。今天要聊的这个项目——基于SpringBoot的健身房管理系统属于计算机毕业设计里非常典型的“业务应用型”题目。它不像算法类课题那样烧脑也不像纯前端项目那样单薄而是把后端框架、数据库设计、权限控制、业务逻辑、前端交互全都串起来了一套流程走下来基本涵盖了企业级Java开发的核心环节。这个系统能做什么一句话概括给健身房提供一个线上管理后台让管理员管会员、管课程、管教练、管设备、管续费提醒让会员能约课、查卡、看记录。从毕业设计角度讲它既有完整的CRUD基础操作又有“预约冲突检测”“会籍到期自动标记”这类带点业务深度的功能做亮点非常适合拿来作为SpringBoot学习的综合练手项目。谁适合参考这个项目如果你正在准备Java方向的毕业设计或者想通过一个完整体验SpringBoot MyBatis Plus Vue的技术栈组合又或者你想在简历上写一个有“完整业务闭环”的个人项目这篇拆解都能直接帮你把路铺好。我会从需求拆解、表结构设计、核心代码实现、部署测速到答辩话术一条线讲完尽量把那些文档里不写的“坑”也一并排掉。1. 项目整体设计与需求拆解1.1 健身房管理的核心业务场景先想清楚一个问题健身房日常运营到底需要管什么把这个想透了系统功能就不愁凑不够数。我去过不少健身房也跟做私教系统的朋友聊过一个典型的健身房日常运转大致是这样会员办卡月卡、季卡、年卡会员拿着卡去上课团操课、私教课、器械区自由训练教练按课时开课、带会员训练前台要处理会员咨询、办卡、续费、停卡老板要看的是一段时间内卖了多少卡、哪些课程最受欢迎、哪些教练课时量最高。说到底业务核心就三个词会员、课程、营收。所以这个管理系统的功能模块基本可以划分为六块会员管理会员档案、办卡记录、卡状态正常/过期/停用、续费操作课程管理团操课与私教课排课、课程预约、预约冲突处理教练管理教练信息、带课 schedule、课时统计设备管理器械档案、报修记录、保养计划订单与收入统计办卡订单、续费订单、按月/按年营收汇总系统管理管理员账号、角色权限、操作日志1.2 为什么选SpringBoot作为基础框架说实话现在做Java后端毕业设计SpringBoot基本是“标准答案”不是因为它最炫而是它把以前Spring SpringMVC那一套繁琐的XML配置全都收了开发者只需要关注业务本身。从毕设答辩的角度讲选SpringBoot有三个非常实际的好处第一快速启动、快速演示。一个SpringApplication.run()就能把整个后端拉起来不用像老项目那样配一堆web.xml和applicationContext.xml。现场演示时少一步配置就少一次翻车概率。第二生态整合方便。MyBatis Plus、Spring Security、Redis、MinIO这些常用组件都能通过starter一键引入。毕设要用到的技术栈绝大多数都有现成的starter不用自己造轮子。第三好解释、好答辩。SpringBoot的自动配置、starter机制、内嵌Tomcat、Actuator监控都是答辩时能展开讲的点。老师问“你为什么用SpringBoot”你可以非常清晰地回答“它简化了配置、提高了开发效率、便于快速集成第三方组件”有理有据。1.3 单体架构还是前后端分离这个题目下我推荐前后端分离的单体架构而不是把服务拆成微服务。原因很简单毕设的体量根本不需要微服务拆了反而给自己挖坑。前后端分离体现在后端只提供RESTful API前端用Vue Element Plus单独跑在另一个端口上。这样做的好处是边界清晰你写后端时不用管页面长什么样只需要保证接口返回的数据结构稳定联调时比前后端混在一起写要省心很多。单体部署的意思是后端打包成一个jar包前端打包成静态资源。部署时可以前后端分开部署后端跑8080前端跑80端口也可以把前端打包后的dist目录直接扔进SpringBoot的static目录做成一个独立jar包。我更推荐前者因为分离部署更接近真实公司的做法简历上写“前后端分离项目”也站得住脚。技术栈建议这样选层级选型说明后端框架SpringBoot 2.7.x稳定且兼容性好不推荐直接用3.x很多教程和依赖还没跟上ORMMyBatis Plus单表CRUD零SQL分页插件好用适合快速开发权限认证Spring Security JWT无状态认证前后端分离标配数据库MySQL 8.0免费、通用性强缓存Redis可选用于缓存课程预约状态不加也能跑前端Vue 3 Element Plus Axios组件化开发后台管理界面成熟对象存储MinIO可选存会员头像、课程图片接口文档Knife4jSwagger增强版自动生成API文档答辩加分项2. 数据库设计与表结构详解2.1 核心数据表梳理数据库设计是毕设项目的底盘也是最容易被答辩老师追问的地方。我这个项目的表结构是经过实际业务推敲的一共设计了11张核心表这里把关键的表拎出来讲。会员表member存会员基本信息正则校验手机号、身份证号。核心字段是卡类型、开卡时间、到期时间、卡状态。有个细节不要只存“状态”字段一定要把到期时间存下来因为“是否过期”可以通过到期时间 当前时间动态算出来而不是每次人工去改状态这个设计在答辩时能体现你的数据库思维。会员卡表member_card会员和卡分开存的好处是一个会员支持多次续卡每次续卡生成一条新记录卡状态的变更全都以流水形式记录查续费历史时直接查这张表就行。很多毕设把卡信息直接写在会员表里导致续费时只能覆盖旧数据这是不规范的做法。课程表course区分团操课和私教课用course_type字段标记。字段包括课程名、教练ID、上课时间、上课地点、容量上限、已选人数。预约表reservation这是整个系统业务上最关键的桥接表。一个会员预约一节课程这里落一条记录。预约前要校验两个东西一是这门课是否还有余位二是这个会员在同一时间段是否已预约其他课程。这两条校验逻辑就是项目里可以重点写的“亮点代码”。其他表还有教练表coach、设备表equipment、报修记录表repair_record、订单表orders、系统用户表sys_user、角色表sys_role、操作日志表operation_log。2.2 关键表字段设计细节以member_card为例字段设计要特别注意CREATE TABLE member_card ( id bigint(20) NOT NULL AUTO_INCREMENT, member_id bigint(20) NOT NULL COMMENT 会员ID, card_type tinyint(4) NOT NULL COMMENT 卡类型1月卡 2季卡 3年卡, start_date datetime NOT NULL COMMENT 开卡时间, expire_date datetime NOT NULL COMMENT 到期时间, status tinyint(4) NOT NULL DEFAULT 1 COMMENT 状态1正常 0过期 2停用, create_time datetime DEFAULT CURRENT_TIMESTAMP, update_time datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (id), KEY idx_member_id (member_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT会员卡记录表;从这张表能看出三个设计原则member_id建了索引因为查会员的卡记录是最频繁的操作时间字段用了datetime别用timestamptimestamp只能存到2038年而且受时区影响状态字段用tinyint存数字枚举不要用varchar直接存“正常”“过期”省空间、查询快、代码里用常量做语义化另外我特意加了update_time并设置自动更新这样MyBatis Plus执行更新操作时不用手动维护审计记录查起来也方便。表结构设计好不好答辩老师扫一眼就能看出你的水平。2.3 MyBatis Plus为什么适合毕设用MyBatis Plus而不是用原生MyBatis是我个人非常推荐的选择。理由特别实际单表操作基本不用写SQLBaseMapper里直接提供selectById、selectPage、insert、updateById这些现成方法。你只需要在Service层写业务逻辑结构比反复写xxMapper.xml清爽得多。对毕设来说能省下大量调试SQL的时间把精力放到业务逻辑和页面效果上。多表关联的场景MyBatis Plus也扛得住比如“查询会员及其最新卡信息”可以先用TableField标注关联字段再写一个联合查询的VO类。这里有个建议多表查询不要惯着MyBatis Plus让你写嵌套子查询的毛病直接手写SQL放在Mapper.xml里可读性更高性能也更好。3. 核心功能实现与关键技术解析3.1 JWT权限认证流程完整实现管理系统的后台不是公开页面必须要做登录认证。前后端分离场景下主流方案就是JWTJSON Web Token。JWT的整体思路是用户登录成功后后端生成一个带签名、带过期时间的token返回给前端前端每次请求都把这个token放到请求头Authorization里后端写一个过滤器拦截请求校验token的合法性和有效期。因为token本身携带了用户信息比如用户ID、用户名后端不需要在服务端存session这就实现了无状态认证。关键代码逻辑如下// 登录接口核心逻辑 public Result login(String username, String password) { SysUser user userService.getByUsername(username); if (user null) { return Result.error(用户不存在); } // BCrypt加密校验密码不存明文 if (!BCrypt.checkpw(password, user.getPassword())) { return Result.error(密码错误); } // 生成JWT过期时间设为24小时 String token JwtUtil.generateToken(user.getId(), user.getUsername()); return Result.success(token); }JwtUtil里做签名和解析用的是io.jsonwebtoken这个库核心代码public static String generateToken(Long userId, String username) { return Jwts.builder() .setSubject(username) .claim(userId, userId) .setIssuedAt(new Date()) .setExpiration(new Date(System.currentTimeMillis() EXPIRE_TIME)) .signWith(SignatureAlgorithm.HS256, SECRET_KEY) .compact(); }这里有个毕设踩坑高发点JWT的SECRET_KEY千万别写在代码里的硬编码字符串至少放到配置文件里答辩时被问“安全吗”你就不至于哑口。我试过把密钥写死在常量类里被老师一眼看穿后来改成了application.yml里的自定义配置属性并主动加了句“生产环境应注入环境变量”观感完全不一样。Spring Security集成时核心是写一个OncePerRequestFilter在每次请求进来后先校验token通过则把用户信息放入SecurityContextHolderpublic class JwtAuthenticationFilter extends OncePerRequestFilter { Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { String token request.getHeader(Authorization); if (StringUtils.hasText(token) token.startsWith(Bearer )) { token token.substring(7); // 去掉前缀 Claims claims JwtUtil.parseToken(token); if (claims ! null) { // 构建UserDetails放入Spring Security上下文 LoginUser loginUser new LoginUser(claims); UsernamePasswordAuthenticationToken auth new UsernamePasswordAuthenticationToken( loginUser, null, loginUser.getAuthorities()); SecurityContextHolder.getContext().setAuthentication(auth); } } filterChain.doFilter(request, response); } }3.2 课程预约与冲突检测预约功能是这个系统业务上最有技术含量的部分值得重点展开。需求是这样的会员登录后可以预约某节团操课预约成功则课程的“已选人数”加1。两个约束条件课程余位不能为0同一会员在同一时间段不能同时约两节课。实现思路Transactional(rollbackFor Exception.class) public Result reserve(Long courseId, Long memberId) { // 校验课程存在 Course course courseMapper.selectById(courseId); if (course null) { return Result.error(课程不存在); } // 校验余位 if (course.getSelectedCount() course.getCapacity()) { return Result.error(该课程已约满); } // 校验同一时间段是否冲突 // 查询会员在该课程时间段的所有预约记录 LambdaQueryWrapperReservation wrapper new LambdaQueryWrapper(); wrapper.eq(Reservation::getMemberId, memberId) .eq(Reservation::getStatus, 1) .eq(Reservation::getCourseTime, course.getCourseTime()); Long count reservationMapper.selectCount(wrapper); if (count 0) { return Result.error(您在该时间段已预约其他课程); } // 扣减余位并保存预约记录 course.setSelectedCount(course.getSelectedCount() 1); courseMapper.updateById(course); Reservation reservation new Reservation(); reservation.setCourseId(courseId); reservation.setMemberId(memberId); reservationMapper.insert(reservation); return Result.success(预约成功); }细看这段代码有三处值得注意的细节一是加了Transactional事务注解扣余位和插预约记录必须同时成功或同时失败防止数据不一致。二是用LambdaQueryWrapper做条件查询MyBatis Plus的写法胜在链式条件清晰比拼SQL字符串安全得多。三是“查询该会员该时段预约”这一步锁定了courseTime这个时间字段。如果课程表里的时间是从2025-05-20 09:00到2025-05-20 10:00那么冲突检测就得判断“当前课程开始时间在新预约课程的开始和结束时间之间、或者结束时间在新课时间范围内”。等宽时间段的冲突检测比较简单但如果课程时长不一样比如私教课1小时、团操课45分钟就必须做区间重叠判断private boolean isTimeConflict(LocalDateTime start1, LocalDateTime end1, LocalDateTime start2, LocalDateTime end2) { return start1.isBefore(end2) start2.isBefore(end1); }这个区间重叠判断写成独立方法既便于单元测试答辩时也能自信地讲“我考虑了长度不同的课程时间段之间的重叠问题”。3.3 会员到期自动标记“会员卡到期”是健身房系统里很自然的业务需求但实现方式有讲究。最直接的想法是写个定时任务每天跑一遍扫描所有到期时间小于当天的会员卡把状态置为“过期”。我一开始也是这么干的后来发现纯属多余。其实改造成一个动态计算代码更干净查询会员列表时直接用SQL条件判断expire_date NOW() AND status 1把这些会员展示成“已过期”状态即可。这样省去了定时任务模块也避免了定时任务执行失败导致状态没更新到的问题。当然如果你想让系统显得更“高级”可以用SpringBoot自带的Scheduled注解写一个定时任务每天凌晨扫描过期卡并发送站内通知。这就是一个可选项用来做“技术亮点”展示给老师看挺合适Component public class CardExpireTask { Scheduled(cron 0 0 1 * * ?) // 每天凌晨1点执行 public void checkExpireCard() { ListMemberCard expiredCards cardMapper.selectExpiredCards(); for (MemberCard card : expiredCards) { card.setStatus(0); cardMapper.updateById(card); // 可以在这里插入一条站内消息或者写操作日志 log.info(会员卡到期{}, card.getId()); } } }单独一个类就可实现不用额外依赖。不过要注意如果你用了动态计算方案再叠加定时任务可能出现状态已经被代码改成0、但动态计算又把它算成“该过期”的重复逻辑问题。最简单的方式是只用动态计算方案不要加定时任务避免逻辑重复。如果你一定要展示定时任务那么在SQL动态判断时直接把status 1作为条件加进去两个方案共存也不冲突。3.4 图片上传与MinIO对象的整合思路系统里有会员头像、课程封面图就涉及到图片上传功能。最粗糙的做法是把图片存到本地磁盘然后后端接口返回一个本地访问路径。但这样做有两个问题一是重启项目后临时文件可能丢失二是部署到服务器上文件路径到处飘不好维护。更合适的做法是引入MinIO作为对象存储服务。MinIO是兼容S3协议的开源对象存储社区版完全免费而且上手极快对毕设来说是最匹配的选型。使用步骤大致如下第一步用Docker快速拉起一个MinIO服务docker run -d \ --name minio \ -p 9000:9000 \ -p 9001:9001 \ -e MINIO_ROOT_USERminioadmin \ -e MINIO_ROOT_PASSWORDminioadmin \ minio/minio server /data --console-address :9001第二步在SpringBoot里引入依赖dependency groupIdio.minio/groupId artifactIdminio/artifactId version8.5.7/version /dependency第三步封装一个MinioService提供上传、下载、删除三个方法核心上传逻辑public String upload(MultipartFile file, String bucketName) throws Exception { // 校验文件非空 if (file.isEmpty()) { throw new RuntimeException(文件不能为空); } // 生成文件名用UUID 原扩展名避免文件名冲突 String originalFilename file.getOriginalFilename(); String ext originalFilename.substring(originalFilename.lastIndexOf(.)); String objectName UUID.randomUUID() ext; // 检查bucket是否存在不存在则创建 boolean exists minioClient.bucketExists(BucketExistsArgs.builder() .bucket(bucketName).build()); if (!exists) { minioClient.makeBucket(MakeBucketArgs.builder().bucket(bucketName).build()); } // 上传文件 minioClient.putObject(PutObjectArgs.builder() .bucket(bucketName) .object(objectName) .stream(file.getInputStream(), file.getSize(), -1) .contentType(file.getContentType()) .build()); // 拼接访问路径 return http://你的服务器IP:9000/ bucketName / objectName; }实际操作中直接把访问路径的前半段配置到配置项里比如minio.endpoint不要硬编码到代码里。MinIO加入项目后需要在Linux服务器上也安装一个演示时把图片传上去能看到效果答辩体验拉满。而且MinIO本身的部署、容器化操作也是比较好的加分点。不过要注意如果只在本机演示不部署服务器用本地文件存储也完全可以没必要为了“用而用”给自己增加环境负担。4. 前端页面设计与联调经验4.1 页面框架选型后端做好了前端如果太丑也不行毕设答辩现场第一印象全靠页面撑。这里建议直接使用Vue 3 Element Plus Vite的组合再用一个开源的Vue管理后台模板做基础比如 vue-element-plus-admin 这类项目它已经把登录页、布局菜单、动态路由、权限控制全搭好了你只需要往里面填自己的业务页面。选模板的好处特别明显省掉搭框架的时间页面风格统一组件交互成熟不需要从零写分页组件、弹窗表单、状态标签这些“体力活”。4.2 页面与接口的对接这里要强调一个非常重要的联调实践接口返回的数据结构必须前后端约定好。我个人推荐统一用以下格式{ code: 200, message: 操作成功, data: { records: [], total: 100 } }所有接口一律返回Result对象前端Axios统一拦截状态码200走成功逻辑非200弹错误提示。这样写页面前端时不用每个接口单独处理异常分支代码重复率很低。前端调用接口的标准写法// 会员列表查询 export function getMemberList(params) { return request({ url: /api/member/page, method: get, params }) }页面里接收数据后交给el-table渲染数据用el-pagination做分页。这套流程是后台管理系统开发的通用范式项目里每一模块都遵循同一个套路写起来很快。4.3 前后端联调中的跨域问题前后端分离项目第一次联调十有八九会遇到跨域错误。前端跑在http://localhost:5173后端跑在http://localhost:8080两边端口不同浏览器会拦截跨域请求。解决方案有两种后端统一加CORS配置或者前端用Vite代理。后端加CORS配置更简单实用Configuration public class CorsConfig implements WebMvcConfigurer { Override public void addCorsMappings(CorsRegistry registry) { registry.addMapping(/**) .allowedOriginPatterns(*) .allowedMethods(GET, POST, PUT, DELETE, OPTIONS) .allowedHeaders(*) .allowCredentials(true) .maxAge(3600); } }等部署到服务器上前后端用Nginx做反向代理时跨域问题就不存在了因为浏览器看到的只有一个域名。这也是为什么部署上线后“跨域”这么个让新手头疼的问题自动消失的原因。5. 部署上线与答辩准备5.1 本地打包与配置调整项目开发完下一步就是打包部署。后端打包前要把application.yml里的数据库连接从本地改成服务器地址Redis地址、MinIO地址也一并改掉。建议用SpringBoot的多环境配置application-dev.yml和application-prod.yml分开spring: profiles: active: prod打包命令mvn clean package -DskipTests打包后的target目录下会生成一个可执行的jar包直接用java -jar启动java -jar gym-system.jar --spring.profiles.activeprod前端打包是npm run build生成的dist目录里就是纯静态文件把它上传到服务器交给Nginx托管即可。5.2 Nginx配置要点Nginx同时托管前端静态资源和反向代理后端接口配置示例server { listen 80; server_name your_server_ip; # 前端静态资源 location / { root /usr/share/nginx/html/dist; index index.html; try_files $uri $uri/ /index.html; } # 后端接口代理 location /api/ { proxy_pass http://127.0.0.1:8080; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; } }这里有两个细节很多人会漏掉。第一前端路由用了history模式刷新某个子路由时Nginx要回退到index.html否则刷新页面白屏就是上面配置里try_files的作用。第二后端接口需要统一前缀/api前后端约定接口都走这个前缀Nginx做路径转发时一条规则就搞定非常干净。5.3 答辩常见问题与话术准备答辩是整个毕设的临门一脚技术做得再好讲不清楚都白搭。这里列几个该项目下老师大概率会问的问题以及你可以如何组织思路。问题一系统架构是怎样的回答思路这是一个基于SpringBoot Vue的前后端分离单体系统后端采用分层架构Controller层负责接收前端请求Service层处理业务逻辑Mapper层DAO层通过MyBatis Plus操作数据库各层之间通过接口交互。整体属于单体应用部署时前后端分离部署后端打包成jar包前端由Nginx托管。问题二权限是怎么控制的回答思路用了Spring Security JWT实现无状态认证。用户登录后后端生成签名token前端请求时携带token后端通过过滤器校验token合法性并基于用户角色控制接口访问权限。管理员角色可以访问全部模块普通员工只能访问指定模块权限配置集中在Spring Security的过滤链里。问题三课程预约并发时怎么办回答思路首先在业务逻辑层做了超卖防护通过数据库的行锁机制控制并发扣减余位其次预约前会对会员同一时间段的课程做冲突检测另外如果采用Redis缓存课程余位可以利用Redis的原子性操作进一步保障并发安全。目前项目中以数据库机制为主Redis方案是扩展方向。问题四数据库中哪些表是你自己设计的回答思路所有表都是基于业务需求自己设计的核心表包括会员表、会员卡表、课程表、预约表、订单表。以会员卡表为例之所以单独建表而不是直接挂在会员表上是因为一个会员可能多次续卡续卡是流水操作每次都要留痕单独建表可支持一个会员对应多条卡记录这样续费历史和到期管理都清晰可查。准备答辩时可以对着这几类问题把所有模块的代码过一遍确保每一个业务页面都能讲清“为什么这样做”“数据从哪里来”“用到哪些表”。能做到这一点答辩基本稳了。5.4 项目后续可以扩展的方向这个项目做完了不意味着就到此为止。如果时间充裕想体现更深的技术思考可以考虑下面几个扩展点第一引入Redis缓存热点数据。课程信息和会员信息是查询最频繁的数据用Redis做缓存可以有效降低数据库压力前端的访问速度也能明显提升。第二增加数据可视化大屏。在管理后台增加一个首页仪表盘用ECharts画图展示会员增长趋势、课程预约热度、营收变化曲线。这个扩展对答辩的视觉冲击力非常大而且实现难度不高很适合做加分项。第三引入消息通知机制。比如会员课程预约成功时在站内发送消息提醒会员卡即将到期时系统自动生成续费提醒通知。用SpringBoot的事件机制就能实现不一定要上消息队列体验会完整很多。这些扩展选一个做就够撑起“系统亮点介绍”环节了不要贪多。贪多的结果往往是每个点都没做精反而让老师觉得你东一榔头西一棒槌。6. 常见报错与排查思路6.1 接口返回404或者路径不对前后端联调最容易犯的错前端请求的URL是/api/member/page后端的Controller映射是/member/page结果打不通。排查时先在浏览器Network面板看真实请求地址再去后端的RequestMapping里对照一下。统一给所有后端接口加上/api前缀是省事的做法——在配置类里设置server.servlet.context-path: /api这样Controller里只需写/member/page请求地址依然是/api/member/page两边都对得上。6.2 JWT拦截器放行登录接口配置了Spring Security之后登录接口要是被拦截器挡住整个项目都进不去。需要把/api/auth/login这个地址加入白名单http.authorizeRequests() .antMatchers(/api/auth/login, /doc.html, /webjars/**, /v3/api-docs/**).permitAll() .anyRequest().authenticated()最容易被忽略的是放行Swagger相关的路径。集成Knife4j后如果不放行/doc.html、/webjars/**、/v3/api-docs/**这些路径接口文档页面就无法访问。调试接口时会非常不方便所以这部分白名单务必加全。6.3 上传图片后URL打不开这个问题的原因通常集中在三处一是MinIO的MINIO_ROOT_USER和MINIO_ROOT_PASSWORD配置有问题造成鉴权失败二是bucket权限是私有的图片访问路径没有带签名参数所以访问时返回403三是服务器安全组没有放行9000端口。排查步骤很简单先在服务器上用curl直接访问MinIO对外提供的图片URL看HTTP状态码。如果是403去MinIO控制台把bucket的访问策略改成public如果是连接超时基本判定是端口未放行。我实操时遇到最多的情况其实是忘记在Nginx里代理9000端口导致外部访问不了解决方法是加一个/minio/路径的location到Nginx。6.4 打包后前端页面白屏前端npm run build后部署到Nginx发现访问首页白屏打开控制台报错说找不到JS文件路径。这通常是因为Vite打包默认基路径是绝对路径/部署在子目录时资源全部404。解决方法是修改vite.config.js里的base配置export default defineConfig({ base: ./, // 使用相对路径 plugins: [vue()], })改完重新打包部署就正常了。这个问题在部署到子目录时会遇到如果你用根路径部署一般不会触发但配一下更稳妥。6.5 前端调接口提示CORS错误即使后端配置了CORS前端可能仍然报跨域错大概率是前端Axios没有走代理或者后端CORS配置被Spring Security拦截器拦截了。检查顺序后端的CORS配置类是否正确生效、Spring Security的过滤链是否把CORS配置放在最前面。我的做法是在Spring Security的配置里额外启用corshttp.cors().and()同时确保CorsConfig类被Spring扫描到。两边都配置好后跨域问题基本都能解决。等部署到线上统一走Nginx代理之后这个问题以后都不会再遇到了。7. 写在最后的实操心得这个项目我从零搭到完成前后花了大约三周时间。回顾整个过程有几个感受想特别分享给正在做毕设的同学。第一别再纠结选题“新不新”了。管理系统的赛道虽然很拥挤但它能稳稳体现你的工程能力而工程能力正是企业招聘时最看重的。相比之下一味追求“AI健身房”这种花哨方向如果没有真实数据支撑很容易做到一半做不下去。大道至简扎实的技术和完整的功能比唬人的标题有说服力得多。第二做毕设的核心是一张表结构设计图。接到题目先别急着敲代码拿一张纸把实体关系画清楚——会员和卡什么关系、课程和教练什么关系、预约和课程什么关系、订单和会员什么关系。这些理清了后面写代码就是按图索骥根本不迷茫。我见过太多同学对着代码发呆问题就出在跳过设计直接上手。第三Git提交要养成习惯。项目过程中几乎必然会对代码做大改比如临时调整表结构、改动接口返回值。这时候如果没有Git做版本回退一个晚上就能把自己搞崩溃。建议从项目第一天就使用Git做本地版本管理每完成一个功能模块就提交一次提交信息写清楚“完成了什么”这样万一某一步改坏了随时可以回退到上一个稳定版本。最后说一个实际遇到过的小问题。有次我在课程时间字段上吃了大亏——数据库里存的是datetime前端传过来的是字符串2025-05-20T09:00两者格式不一致导致前端时间回显为空。排查发现是没在后端加JsonFormat(pattern yyyy-MM-dd HH:mm:ss)注解。所以前后端交互的时间格式一定要在一开始就约定好别等联调两天后再来处理这种低级问题。做毕设本身就是在模拟从业者的工作模式先设计再编码最后交付和讲解。顺着这条链路把一个系统吃透你收获的不只是一个题目分数而是一整套以后进公司就能直接上手的工程方法。希望这篇拆解对你有实际帮助也欢迎做完之后回来交流你踩到的新坑。
阅读完成 · 觉得有帮助?