ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

SpringBoot+Vue3+DeepSeek健身房管理系统:秋招简历项目

SpringBoot+Vue3+DeepSeek健身房管理系统:秋招简历项目 秋招季有个很扎心的现象很多同学简历上的项目栏清一色写着“图书管理系统”或“学生管理系统”。这类项目不是不能写而是同质化太严重面试官一天能看到十几份相似描述很难留下记忆点。如果你正在准备秋招、毕设或者课设又想要一个“既容易跑通、又能讲出亮点”的项目本文要聊的 SpringBoot Vue3 DeepSeek 健身房管理系统是一个很合适的选择。这个项目的核心判断是它的价值不在于 CRUD 本身而在于“业务闭环 AI 能力融合”。健身房管理系统本身具备会员、课程、教练、器材、预约、统计这些完整业务模块足够撑起一份简历项目而接入 DeepSeek 后又能展示你对“大模型 API 工程化接入”的理解。这两点叠加就能和大多数学生管理系统拉开差距。我写这篇文章的目标很简单先帮你把项目整体想清楚再带你把前后端工程从零跑通最后整理一份排查清单和简历表达建议。整个流程我尽量控制在 2 小时左右代码和配置都给出可直接复制的最小版本。如果你正准备答辩或者面试建议先收藏再对照着一步步做。1. 为什么说这是秋招简历里的“差异化项目”先抛一个问题面试官看简历项目时到底在看什么他大概率不会指望一个应届生做出多复杂的系统他更想看的是三件事第一你完整做完了一个项目而不是只跑通了某个 Demo第二你知道项目里每个模块为什么存在能讲清楚业务逻辑第三你遇到问题时能定位、能解决具备基本工程素养。图书管理系统输在哪里输在业务太薄。无非是增删改查再加个模糊查询面试官问两句就到底了。健身房管理系统不一样它天然包含会员生命周期管理、课程预约、私教排班、器材维护、营收统计等场景。这些业务背后牵扯到表关系设计、状态流转、权限控制任何一个点都能展开深聊。接入 DeepSeek 是另一个加分项。2026 年前后端分离项目里AI 能力已经不再是什么炫技内容而是很多企业内部系统尝试集成的能力。你在项目里做一个“智能体测分析”或者“AI 课程推荐”哪怕实现并不复杂也能说明你对大模型 API、Prompt 设计、流式输出这些概念有过真实接触。这个项目尤其适合三类人一是秋招需要项目背书的应届生二是需要做毕设或课设、追求“能运行且结构完整”的同学三是在校期间只写过单体系统、想体验一次前后端分离完整流程的开发者。如果你已经有分布式项目经验那这个项目偏基础不建议再花时间。2. 项目概览与技术选型2.1 核心功能模块一套完整的健身房管理系统可以按照用户角色拆成三个端管理员端处理会员管理、教练管理、课程管理、器材管理、统计报表教练端负责排班、查看预约、填写体测数据会员端则关注课程预约、体测记录和缴费记录。如果是课设或简历项目不必三个端全部做完但至少要把管理员端做完整教练端和会员端各取一部分核心功能即可。我的建议是功能列表这样划分模块功能点说明会员管理新增会员、会员卡类型、续费、状态变更核心模块必须有教练管理教练信息、排班、课时数统计展现多表关联课程管理团课、私教课、课程预约预约状态是关键器材管理器材登记、维修状态、报废简单 CRUD用来凑模块体测管理身高体重 BMI、体测报告可接入 DeepSeek 生成建议统计报表会员增长、营收趋势使用聚合查询系统管理用户登录、角色权限、操作日志JWT 拦截器2.2 技术栈与选型说明后端采用 SpringBoot 2.7.x MyBatis-Plus MySQL 8.0这是目前高校项目中最稳的组合。不推荐一上来就用 SpringBoot 3.x因为 SpringBoot 3 强制要求 JDK 17很多学校机器的 JDK 还是 1.8版本冲突会浪费大量调试时间。如果你的 JDK 是 1.8选择 2.7.x 是安全的。前端采用 Vue3 Vite Element Plus Pinia Axios。Vue3 的 Composition API 配合script setup语法比 Vue2 的 Options API 写起来更简洁也符合企业项目现状。Vite 作为构建工具启动速度快对新手非常友好。AI 能力采用 DeepSeek API。先说明这个系统的核心业务不依赖 AIAI 是锦上添花的能力。我们用 DeepSeek 来实现“体测报告生成”和“训练计划推荐”两个场景。这样设计的好处是即使 DeepSeek API 调用失败系统主体仍然可用不影响演示。技术栈表格如下层次技术版本建议后端框架SpringBoot2.7.xORMMyBatis-Plus3.5.x数据库MySQL8.0鉴权JWTjjwt 0.9.1 或 0.11.5前端框架Vue33.4.x构建工具Vite5.xUI 组件库Element Plus2.x状态管理Pinia2.xHTTP 客户端Axios1.xAI 能力DeepSeek API官方接口3. 搭建开发环境与初始化工程3.1 环境准备清单先检查本机环境缺什么装什么。需要 JDK 1.8 或更高版本、Maven 3.6、Node.js 16建议 18 或 20、MySQL 8.0、一个顺手的 IDE。后端推荐 IntelliJ IDEA前端用 VS Code 即可当然你用 IDEA 同时开前后端也可以。版本方面有一个容易踩坑的地方Maven 仓库中 SpringBoot 2.7.18 是 2.x 系列最后一个版本非常稳定。JDK 不要用太高版本JDK 8 和 JDK 11 都能跑 2.7.x。前端方面 Vite 5 需要 Node 18如果你的 Node 是 16就改用 Vite 4否则会有兼容问题。java -version mvn -version node -v npm -v以上命令都能正常输出版本号环境就没有大问题。MySQL 启动后可以通过命令行mysql -uroot -p验证连接。3.2 创建后端工程建议不要用 Spring Initializr 网页生成直接在 IDEA 里新建 Spring Boot 工程或者用 Maven 命令创建。因为我们需要手动控制依赖版本网页生成后反而要删改一堆东西。后端工程名建议叫gym-backend包结构如下com.example.gym ├── common # 公共类返回结果、异常处理、JWT工具 ├── config # 配置类CORS、MyBatis-Plus配置 ├── controller # 控制器 ├── entity # 数据库实体 ├── mapper # MyBatis-Plus Mapper接口 ├── service # 业务层接口与实现 ├── dto # 数据传输对象 └── utils # 工具类3.3 创建前端工程前端工程用 Vite 脚手架创建即可npm create vitelatest gym-frontend -- --template vue cd gym-frontend npm install npm install element-plus axios pinia vue-router npm run dev如果执行npm create vite时卡住先检查 Node 版本再考虑配置国内 npm 镜像npm config set registry https://registry.npmmirror.com4. 数据库设计与后端核心实现4.1 数据库建表脚本先设计核心表。为了演示方便这里给出会员、课程、预约三张核心表其余表结构可以按同样风格扩展。项目根目录新建sql/init.sql内容如下CREATE DATABASE IF NOT EXISTS gym_system DEFAULT CHARACTER SET utf8mb4; USE gym_system; CREATE TABLE member ( id BIGINT PRIMARY KEY AUTO_INCREMENT COMMENT 主键, member_no VARCHAR(32) NOT NULL COMMENT 会员编号, name VARCHAR(64) NOT NULL COMMENT 姓名, phone VARCHAR(20) NOT NULL COMMENT 手机号, card_type TINYINT DEFAULT 1 COMMENT 会员卡类型 1月卡 2季卡 3年卡, card_status TINYINT DEFAULT 1 COMMENT 状态 1正常 2冻结 3过期, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, UNIQUE KEY uk_member_no (member_no), UNIQUE KEY uk_phone (phone) ) ENGINEInnoDB COMMENT 会员表; CREATE TABLE course ( id BIGINT PRIMARY KEY AUTO_INCREMENT COMMENT 主键, course_name VARCHAR(64) NOT NULL COMMENT 课程名称, course_type TINYINT DEFAULT 1 COMMENT 1团课 2私教课, coach_id BIGINT DEFAULT NULL COMMENT 教练ID, capacity INT DEFAULT 20 COMMENT 上课人数上限, price DECIMAL(10,2) DEFAULT 0 COMMENT 价格, course_time DATETIME DEFAULT NULL COMMENT 开课时间, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP ) ENGINEInnoDB COMMENT 课程表; CREATE TABLE appointment ( id BIGINT PRIMARY KEY AUTO_INCREMENT COMMENT 主键, member_id BIGINT NOT NULL COMMENT 会员ID, course_id BIGINT NOT NULL COMMENT 课程ID, status TINYINT DEFAULT 1 COMMENT 1已预约 2已取消 3已完成, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, UNIQUE KEY uk_member_course (member_id, course_id) ) ENGINEInnoDB COMMENT 预约表;4.2 后端 pom.xml 依赖配置gym-backend/pom.xml最核心的依赖如下注意 SpringBoot 版本用 2.7.18parent groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-parent/artifactId version2.7.18/version relativePath/ /parent dependencies dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId /dependency dependency groupIdcom.baomidou/groupId artifactIdmybatis-plus-boot-starter/artifactId version3.5.3.1/version /dependency dependency groupIdmysql/groupId artifactIdmysql-connector-java/artifactId scoperuntime/scope /dependency dependency groupIdio.jsonwebtoken/groupId artifactIdjjwt/artifactId version0.9.1/version /dependency dependency groupIdorg.projectlombok/groupId artifactIdlombok/artifactId optionaltrue/optional /dependency /dependencies这里真正容易踩坑的地方是mysql-connector-java的版本。SpringBoot 2.7.x 默认管理的是 MySQL 驱动 8.0.x如果你本地装的是 MySQL 5.7需要显式改成 5.1.49。如果连接时报Public Key Retrieval is not allowed多半是 MySQL 8 的驱动和连接字符串配置问题后面排查表里会提到。4.3 application.yml 配置src/main/resources/application.yml配置如下server: port: 8080 spring: datasource: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3306/gym_system?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/ShanghaiuseSSLfalseallowPublicKeyRetrievaltrue username: root password: root jackson: date-format: yyyy-MM-dd HH:mm:ss time-zone: Asia/Shanghai mybatis-plus: configuration: log-impl: org.apache.ibatis.logging.stdout.StdOutImpl map-underscore-to-camel-case: true global-config: db-config: id-type: auto jwt: secret: gym-secret-key-please-change expire: 604800allowPublicKeyRetrievaltrue这个配置是为了解决 MySQL 8 驱动下连接报错问题。serverTimezoneAsia/Shanghai指定时区避免日期显示相差 8 小时。map-underscore-to-camel-case开启后数据库字段member_no可以自动映射到实体属性memberNo。4.4 会员管理核心代码实现创建一个会员实体Member.javapackage com.example.gym.entity; import com.baomidou.mybatisplus.annotation.IdType; import com.baomidou.mybatisplus.annotation.TableId; import com.baomidou.mybatisplus.annotation.TableName; import lombok.Data; import java.time.LocalDateTime; Data TableName(member) public class Member { TableId(type IdType.AUTO) private Long id; private String memberNo; private String name; private String phone; private Integer cardType; private Integer cardStatus; private LocalDateTime createTime; private LocalDateTime updateTime; }创建对应的 Mapper 接口package com.example.gym.mapper; import com.baomidou.mybatisplus.core.mapper.BaseMapper; import com.example.gym.entity.Member; import org.apache.ibatis.annotations.Mapper; Mapper public interface MemberMapper extends BaseMapperMember { }创建控制器MemberController.java这里演示分页查询和新增会员两个接口package com.example.gym.controller; import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; import com.baomidou.mybatisplus.extension.plugins.pagination.Page; import com.example.gym.common.Result; import com.example.gym.entity.Member; import com.example.gym.service.MemberService; import org.springframework.web.bind.annotation.*; import javax.annotation.Resource; RestController RequestMapping(/api/member) public class MemberController { Resource private MemberService memberService; GetMapping(/page) public Result page(RequestParam(defaultValue 1) Integer page, RequestParam(defaultValue 10) Integer size, RequestParam(required false) String name) { PageMember pageParam new Page(page, size); QueryWrapperMember wrapper new QueryWrapper(); if (name ! null !name.isEmpty()) { wrapper.like(name, name); } wrapper.orderByDesc(create_time); return Result.success(memberService.page(pageParam, wrapper)); } PostMapping public Result add(RequestBody Member member) { memberService.save(member); return Result.success(); } }Service 层就不贴完整代码了核心是继承 MyBatis-Plus 的IService和ServiceImpl这部分属于固定写法。Result 是一个统一返回类包含code、message、data三个字段成功时 code 为 200。统一返回结构的意义在于前端 Axios 拦截器只需要判断一个 code就能决定是否弹出错误提示。4.5 JWT 登录认证前后端分离项目登录状态不能依赖 Session最常用的方案是 JWT。我们写一个简单的 JWT 工具类package com.example.gym.utils; import io.jsonwebtoken.Claims; import io.jsonwebtoken.Jwts; import io.jsonwebtoken.SignatureAlgorithm; import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Component; import java.util.Date; Component public class JwtUtil { Value(${jwt.secret}) private String secret; Value(${jwt.expire}) private Long expire; public String generateToken(Long userId, String username) { Date now new Date(); Date expireDate new Date(now.getTime() expire * 1000); return Jwts.builder() .setHeaderParam(typ, JWT) .setSubject(username) .claim(userId, userId) .setIssuedAt(now) .setExpiration(expireDate) .signWith(SignatureAlgorithm.HS256, secret) .compact(); } public Claims parseToken(String token) { return Jwts.parser() .setSigningKey(secret) .parseClaimsJws(token) .getBody(); } }登录接口的逻辑很简单根据用户名和密码查询用户如果匹配成功生成 token 返回给前端。前端拿到 token 后存储在 Pinia 和 localStorage 中之后每次请求在 Axios 拦截器里加上Authorization: Bearer token。后端再通过一个拦截器统一校验 token放行/api/login拦截其他/api/**请求。这里有一个新手常见的误区JWT 并不是放在请求头里就安全了它本质上是一段可解签的数据不能存放密码等敏感信息。如果前端把 token 存到 localStorage还要考虑 XSS 攻击风险。作为课设项目存在 localStorage 可以接受但简历面试时要能说出这个权衡。5. 集成 DeepSeek 实现智能教练能力5.1 为什么要在项目中接入 AI坦白说健身房管理系统的 CRUD 部分并不复杂真正能让面试官眼前一亮的是你把业务数据和 AI 能力结合起来了。举例来说会员录入身高体重后系统根据体测数据生成训练建议会员想预约私教课系统根据他的锻炼目标推荐课程。这些功能用 DeepSeek API 都可以实现而且代码量不大。这里的实现思路是后端调用 DeepSeek 的对话补全接口把会员的体测数据拼进 Prompt让模型输出结构化的建议文本然后保存在体测记录中。简单说就是用一个大模型 API 客户端把系统数据转成自然语言请求再解析响应。5.2 DeepSeek API 调用封装先把 DeepSeek 的 API Key 配置到application.yml中deepseek: api-key: your-deepseek-api-key base-url: https://api.deepseek.com model: deepseek-chat然后创建一个配置类读取配置package com.example.gym.config; import lombok.Data; import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.stereotype.Component; Data Component ConfigurationProperties(prefix deepseek) public class DeepSeekConfig { private String apiKey; private String baseUrl; private String model; }再写一个调用 DeepSeek 的服务类。为了避免引入额外的 SDK这里直接使用 SpringBoot 推荐的RestTemplate或 JDK 11 后的HttpClient都可以。为了兼容 JDK 8用RestTemplate更稳妥package com.example.gym.service.impl; import com.example.gym.config.DeepSeekConfig; import com.example.gym.dto.BodyMeasureDTO; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; import org.springframework.http.*; import org.springframework.stereotype.Service; import org.springframework.web.client.RestTemplate; import javax.annotation.Resource; import java.util.*; Service public class DeepSeekService { Resource private DeepSeekConfig deepSeekConfig; Resource private RestTemplate restTemplate; public String generateFitnessAdvice(BodyMeasureDTO measure) { String prompt buildPrompt(measure); MapString, Object requestBody new HashMap(); requestBody.put(model, deepSeekConfig.getModel()); requestBody.put(messages, Arrays.asList( Collections.singletonMap(role, system), Collections.singletonMap(role, user) )); HttpHeaders headers new HttpHeaders(); headers.setContentType(MediaType.APPLICATION_JSON); headers.setBearerAuth(deepSeekConfig.getApiKey()); HttpEntityMapString, Object entity new HttpEntity(requestBody, headers); ResponseEntityString response restTemplate.exchange( deepSeekConfig.getBaseUrl() /chat/completions, HttpMethod.POST, entity, String.class ); try { ObjectMapper mapper new ObjectMapper(); JsonNode root mapper.readTree(response.getBody()); return root.path(choices).get(0).path(message).path(content).asText(); } catch (Exception e) { return AI 建议生成失败请稍后重试; } } private String buildPrompt(BodyMeasureDTO measure) { return 你是一名专业健身教练。根据以下体测数据给出100字以内的训练建议。 姓名 measure.getName() 身高 measure.getHeight() cm 体重 measure.getWeight() kg 体脂率 measure.getBodyFatRate() % 目标 measure.getGoal(); } }注意上面的代码是简化示意实际运行时requestBody中的messages结构需要补全为两个独立对象。完整逻辑应该是{ model: deepseek-chat, messages: [ {role: system, content: 你是一名专业健身教练}, {role: user, content: 根据以下体测数据给出建议...} ] }面试时如果你能讲清楚这个请求结构说明你真的调过接口而不是只贴了一段网上的代码。关键设计点是AI 请求要放在 Service 层由 Controller 调用通过 DTO 接收参数。不要在前台直接暴露 API Key否则任何人都能通过浏览器控制台看到你的 Key。此外调用要设置超时时间避免 DeepSeek 服务响应慢导致接口超时。6. 前端 Vue3 实现登录与会员管理页面6.1 前端工程结构Vue3 项目建议按下面结构组织不要所有代码都放在一个组件里gym-frontend/src ├── api/ # 接口定义 ├── assets/ # 静态资源 ├── components/ # 公共组件 ├── router/ # 路由配置 ├── store/ # Pinia状态 ├── views/ # 页面 │ ├── Login.vue │ ├── Dashboard.vue │ ├── member/MemberList.vue │ ├── course/CourseList.vue │ └── report/BodyReport.vue ├── utils/request.js # Axios封装 ├── App.vue └── main.js6.2 Axios 封装src/utils/request.js是前端所有请求的入口必须封装好三个能力请求头携带 token、统一处理响应错误、401 时跳转登录页。import axios from axios import { ElMessage } from element-plus import router from ../router const request axios.create({ baseURL: http://localhost:8080, timeout: 15000 }) // 请求拦截器携带 token request.interceptors.request.use(config { const token localStorage.getItem(token) if (token) { config.headers.Authorization Bearer ${token} } return config }) // 响应拦截器处理错误 request.interceptors.response.use( response { const res response.data if (res.code ! 200) { ElMessage.error(res.message || 请求失败) return Promise.reject(new Error(res.message)) } return res }, error { if (error.response error.response.status 401) { localStorage.removeItem(token) router.push(/login) ElMessage.warning(登录已过期请重新登录) } else { ElMessage.error(error.message || 网络错误) } return Promise.reject(error) } ) export default request6.3 登录页面登录页的核心逻辑就是调用登录接口、保存 token、跳转首页。这里给出 Vue3 Element Plus 的实现方式template div classlogin-container el-card classlogin-card h2健身房管理系统/h2 el-form :modelloginForm label-width60px el-form-item label账号 el-input v-modelloginForm.username placeholder请输入账号 / /el-form-item el-form-item label密码 el-input v-modelloginForm.password typepassword show-password placeholder请输入密码 keyup.enterhandleLogin / /el-form-item el-form-item el-button typeprimary stylewidth: 100% :loadingloading clickhandleLogin 登录 /el-button /el-form-item /el-form /el-card /div /template script setup import { reactive, ref } from vue import { useRouter } from vue-router import { ElMessage } from element-plus import request from ../utils/request const router useRouter() const loading ref(false) const loginForm reactive({ username: , password: }) const handleLogin async () { if (!loginForm.username || !loginForm.password) { ElMessage.warning(请输入账号和密码) return } loading.value true try { const res await request.post(/api/login, loginForm) localStorage.setItem(token, res.data.token) localStorage.setItem(username, loginForm.username) ElMessage.success(登录成功) router.push(/dashboard) } finally { loading.value false } } /script style scoped .login-container { height: 100vh; display: flex; align-items: center; justify-content: center; background: linear-gradient(135deg, #1f2937, #374151); } .login-card { width: 400px; padding: 20px; } /style注意登录按钮的loading状态这看起来是小事但实际项目非常常见。如果请求慢用户会连续点登录按钮导致重复提交。加上loading是个好习惯面试时可以主动提。6.4 会员列表页面会员列表是后台管理系统最典型的页面包含搜索、分页表格、新增弹窗三个部分。这里我给出核心的表格和分页部分帮助你理解完整页面结构template div el-card div classtoolbar el-input v-modelqueryParams.name placeholder会员姓名 clearable stylewidth: 200px clearloadData / el-button typeprimary clickhandleSearch搜索/el-button el-button typesuccess clickshowDialog true新增会员/el-button /div el-table :datatableData border stripe el-table-column propmemberNo label会员编号 width120 / el-table-column propname label姓名 width100 / el-table-column propphone label手机号 width140 / el-table-column propcardType label卡类型 width100 template #default{ row } el-tag v-ifrow.cardType 1 typeprimary月卡/el-tag el-tag v-else-ifrow.cardType 2 typewarning季卡/el-tag el-tag v-else typedanger年卡/el-tag /template /el-table-column el-table-column propcardStatus label状态 width100 template #default{ row } el-tag v-ifrow.cardStatus 1 typesuccess正常/el-tag el-tag v-else-ifrow.cardStatus 2 typeinfo冻结/el-tag el-tag v-else typedanger过期/el-tag /template /el-table-column el-table-column propcreateTime label创建时间 / /el-table el-pagination v-model:current-pagequeryParams.page v-model:page-sizequeryParams.size :totaltotal layouttotal, prev, pager, next current-changeloadData / /el-card /div /template script setup import { onMounted, reactive, ref } from vue import request from ../../utils/request const tableData ref([]) const total ref(0) const queryParams reactive({ page: 1, size: 10, name: }) const loadData async () { const res await request.get(/api/member/page, { params: queryParams }) tableData.value res.data.records total.value res.data.total } const handleSearch () { queryParams.page 1 loadData() } onMounted(loadData) /scriptElement Plus 的el-tag在这里有两个作用一是把数字状态变成用户可读的文字二是让界面视觉层次更清晰。这类细节在答辩时值得强调因为展示出你考虑过“用户体验”而不是单纯数据展示。6.5 路由配置与登录守卫路由守卫是前后端分离项目里必须做的一环。它的作用是未登录用户访问系统页面时自动跳转到登录页已登录用户访问登录页时自动跳转到首页。在src/router/index.js中配置import { createRouter, createWebHistory } from vue-router const router createRouter({ history: createWebHistory(), routes: [ { path: /login, component: () import(../views/Login.vue) }, { path: /, component: () import(../layout/Layout.vue), redirect: /dashboard, children: [ { path: dashboard, component: () import(../views/Dashboard.vue) }, { path: member, component: () import(../views/member/MemberList.vue) }, { path: course, component: () import(../views/course/CourseList.vue) }, { path: report, component: () import(../views/report/BodyReport.vue) } ] } ] }) router.beforeEach((to, from, next) { const token localStorage.getItem(token) if (to.path ! /login !token) { next(/login) } else { next() } }) export default router7. 前后端联调与运行验证7.1 启动后端服务在 IDEA 中打开gym-backend工程等待 Maven 下载依赖完成后找到启动类GymApplication.java右键运行。看到类似下面的日志就说明启动成功Tomcat started on port(s): 8080 (http) with context path Started GymApplication in 3.42 seconds如果启动失败优先看最底部几行报错。常见的是端口占用、数据库连接失败、缺少依赖。端口占用可以改application.yml中的端口或者查看占用情况# Windows netstat -ano | findstr 8080 # macOS/Linux lsof -i :80807.2 启动前端服务在gym-frontend目录下运行npm run dev看到Local: http://localhost:5173/说明启动成功。浏览器打开后先用管理员账号登录然后测试会员列表加载。如果页面能显示表格数据前后端联调已经通了。这里有一个需要特别注意的问题后端端口是 8080前端页面是 5173两者端口不同属于跨域请求。如果前端请求报 CORS 错误需要在后端配置跨域。最简单的方式是加一个配置类package com.example.gym.config; import org.springframework.context.annotation.Configuration; import org.springframework.web.servlet.config.annotation.CorsRegistry; import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; Configuration public class CorsConfig implements WebMvcConfigurer { Override public void addCorsMappings(CorsRegistry registry) { registry.addMapping(/api/**) .allowedOrigins(http://localhost:5173) .allowedMethods(GET, POST, PUT, DELETE) .allowCredentials(true) .maxAge(3600); } }7.3 接口测试建议推荐下载 Postman 或者直接用 IDEA 自带的 HTTP Client 测试接口。先测试登录拿到 token再测试会员分页接口请求头加上Authorization: Bearer token。如果返回 JSON 中包含records字段说明分页接口正常。如果没有 Postman也可以直接使用浏览器访问http://localhost:8080/api/member/page?page1size10但浏览器无法方便地添加请求头。最推荐的测试路径Postman 登录 - 保存 token - 测试业务接口。7.4 预期运行效果跑通后的完整流程应该是输入管理员账号密码登录成功跳转 Dashboard。Dashboard 展示会员总数、今日预约数、本月营收等统计卡片。进入会员管理页面可以搜索、分页查看会员列表。新增会员填写姓名、手机号、卡类型保存后列表刷新。进入体测管理页面录入身高体重点击“生成 AI 建议”页面展示 DeepSeek 返回的训练建议文本。8. 常见问题与排查思路开发过程中遇到问题先不要慌绝大多数问题都能通过看日志定位。我整理了一份高频问题排查表问题现象可能原因排查方式解决方案后端启动失败报数据库连接错误MySQL 未启动或账号密码错检查 MySQL 服务状态执行mysql -uroot -p修改 application.yml 中的账号密码连接 MySQL 报 Public Key Retrieval is not allowedMySQL 8 驱动认证策略问题查看完整异常堆栈连接串添加allowPublicKeyRetrievaltrue前端启动报错Node版本不兼容Vite 5 需要 Node 18查看 npm 启动日志升级 Node 或换 Vite 4前端请求后端接口 404接口路径不对或后端未启动浏览器 F12 看 Network 面板核对 Controller 的 RequestMapping 路径前端请求后端接口 401token 缺失或已过期打开 localStorage 查看 token重新登录检查拦截器是否放行登录接口跨域报错 CORS前后端端口不同后端未配置看浏览器控制台具体报错后端配置 CorsConfig接口能通但表格不显示数据后端返回结构不是{code,message,data}格式浏览器直接访问接口看 JSON 结构调整 Axios 拦截器兼容实际返回结构时间是数组格式 有 8 小时偏差数据库时区和应用时区不一致查看数据库时间和系统时间连接串加serverTimezoneAsia/Shanghai实体字段改为 LocalDateTimeDeepSeek 接口报 401API Key 错误检查 yml 配置是否正确重新生成 API KeyDeepSeek 接口响应很慢网络原因或 Prompt 太长查看接口耗时增加超时时间简化 Prompt这里我想强调两个排查原则。第一遇到报错先读最后 20 行日志而不是从第一行开始看大多数异常信息在堆栈底部。第二前端页面有问题先开 F12看到底是请求没发出去、响应异常还是渲染逻辑错误。这个排查思路比记住具体解决方案更重要。9. 从“能运行”到“好项目”简历表达与工程化建议9.1 简历上的项目描述建议很多同学项目做完了简历却写不好。常见的写法是“使用 SpringBoot Vue3 开发了一套健身房管理系统”这句话实在太单薄了。我建议按照“项目背景 你的职责 技术难点 最终成果”四段式来写。参考话术项目描述面向健身房场景的前后端分离管理系统包含会员管理、课程预约、体测分析等核心模块并集成 DeepSeek 大模型实现智能训练建议生成。个人职责负责系统架构设计、数据库表设计、后端核心接口开发、前端页面开发及部署。基于 JWT 实现无状态登录鉴权通过 Axios 拦截器统一处理 token 和异常响应设计预约状态机处理课程预约的冲突与取消逻辑集成 DeepSeek API实现体测数据到训练建议的自动生成。技术难点预约模块并发场景下的数据一致性大模型 API 调用的超时与异常兜底前后端跨域配置与接口规范统一。面试官看到这个描述至少有三四个点可以追问。相比“实现了增删改查”这种写法展示了你的业务理解和工程意识。9.2 代码层面的工程化建议如果你想让这个项目从“课设水平”提升到“实习水平”下面几件事值得做第一统一返回结果和全局异常处理。文章里已经用了 Result 类再补充一个GlobalExceptionHandler用RestControllerAdvice捕获业务异常和参数校验异常避免异常堆栈直接暴露给前端。第二参数校验不能只在后端手动判断。可以引入spring-boot-starter-validation在 DTO 字段上使用NotBlank、NotNull注解Controller 参数使用Valid RequestBody。第三日志不能只靠 System.out。在 Service 层使用 Slf4j 输出关键操作日志例如新增会员、预约成功、AI 调用失败。生产环境的排障全靠日志。第四配置文件要区分环境。准备application-dev.yml和application-prod.yml启动时用spring.profiles.active指定环境。这不算复杂操作但能直观体现工程素养。9.3 关于 DeepSeek 接入的注意事项DeepSeek API 的调用在面试中是一个加分点也是一把双刃剑。如果被问到安全性和稳定性你要能回答出来。我这里给出几个要点一是 API Key 绝不能暴露在前端代码中一定要由后端发起调用。前端把体测数据提交给后端后端再带 Key 请求 DeepSeek。二是对 AI 响应要做缓存。同样的体测数据不应该每次打开页面都重新请求一次可以把生成结果存到数据库下次直接读取。三是 AI 接口可能超时或限流必须有兜底方案。如果 DeepSeek 返回失败页面要展示“AI 建议生成失败”的提示而不是白屏。这些点看起来是细节但面试官往往就是从这些细节判断你是在“做项目”还是在“跑 demo”。9.4 后续可以扩展的方向如果时间和精力允许建议在这个基础上做两个扩展一是引入 Redis 缓解热点数据查询压力把统计接口的查询结果缓存 5 分钟二是把登录注册改成验证码 多次失败锁定这在面试中能引出安全话题。每多一个功能点简历上就多一个可以深聊的话题。另外还可以考虑写一个简单的部署脚本把这个项目通过 Docker 容器跑起来。秋招岗位描述里经常要求“熟悉 Linux 和 Docker”你不需要部署得很复杂只需要有一个Dockerfile和一段部署说明就能证明你具备交付能力。10. 写在最后回到最开始的问题为什么推荐你花 2 小时做这个健身房管理系统因为它不是在重复图书管理系统的老路而是在一个真实业务场景中加入了你自己的思考。会员、教练、课程、预约、体测、AI 建议这条业务链路从前端页面到后端接口再到模型调用每一层都有可以表达的内容。如果你正在准备秋招项目做完以后给自己留一个下午时间做“模拟面试”。找个朋友或者自己对镜子把项目从登录到 AI 建议完整讲一遍重点讲清楚为什么这样设计表结构为什么选择 JWTDeepSeek 请求失败怎么办。能把这些讲明白你在面试中的项目环节就已经赢过了大多数人。希望这篇文章能帮你把项目顺利跑通。代码可以精简但思路不要精简。先跑起来再逐步优化这本来就是每个开发者熟悉新项目的最佳路径。
返回列表