Vue+Spring Boot健身房管理系统实战:前后端分离架构与CRUD实现
在实际开发中很多开发者学习了 Vue 和 Spring Boot 的基础知识后却不知道如何将它们组合起来构建一个功能完整、结构清晰的前后端分离项目。健身房管理系统是一个典型的业务场景它涵盖了会员管理、课程预约、设备维护、员工排班等多个模块非常适合用来练习前后端分离架构下的 CRUD 操作、权限控制、数据交互和项目部署。本文将带你从零开始理解并实践如何构建一个基于 Vue 和 Spring Boot 的健身房管理系统并深入探讨在整合过程中那些容易被忽略但至关重要的细节。本文的目标读者是已经掌握 Vue 和 Spring Boot 基础希望进行项目实战的开发者。你将了解到如何规划项目结构、设计数据库、实现前后端数据交互、处理跨域问题以及如何将项目打包部署。我们不会止步于“跑通”而是会深入到配置参数的含义、常见错误的排查路径以及从学习环境到生产环境需要考虑的额外因素。1. 理解前后端分离架构与项目技术选型在开始编码之前必须清晰理解我们选择的技术栈各自扮演什么角色以及它们如何协同工作。这决定了后续的目录结构、开发流程和问题排查方向。1.1 为什么选择 Vue Spring BootVue 作为前端框架负责用户界面的构建和用户交互逻辑。它通过组件化开发使得页面结构清晰、易于维护。Spring Boot 作为后端框架负责处理业务逻辑、数据持久化和提供 RESTful API 接口。这种前后端分离的架构使得前端和后端可以独立开发、测试和部署提高了开发效率和系统的可扩展性。对于健身房管理系统这类内部管理系统界面交互复杂度中等但业务规则和数据处理要求较高。Vue 的响应式数据和组件库能快速搭建管理后台界面而 Spring Boot 的自动配置和丰富的 Starter 依赖能让后端开发专注于业务无需过多纠缠于框架整合。1.2 核心依赖与版本管理版本兼容性是项目启动的第一道坎。一个常见的错误是随意选择最新版本导致依赖冲突。以下是一个经过验证的、稳定的依赖版本组合适用于本项目的学习与开发。后端 (Spring Boot) 核心依赖Spring Boot:2.7.18 (这是一个长期支持版本稳定且社区资源丰富)Spring Web:用于构建 RESTful API。MyBatis-Plus:3.5.3 (极大简化了单表 CRUD 操作需与 Spring Boot 2.7.x 兼容)MySQL Driver:8.0.x (连接 MySQL 8.0 数据库)Lombok:减少样板代码如 Getter/Setter。Spring Boot DevTools:开发时热重启。Hibernate Validator:进行接口参数校验。前端 (Vue) 核心依赖Vue:3.x (本文使用 Composition API)Vue Router:4.x (路由管理)Pinia:2.x (状态管理替代 Vuex)Axios:1.x (HTTP 客户端用于调用后端 API)Element Plus:2.x (UI 组件库快速构建管理后台界面)Vite:作为构建工具启动和热更新速度远超 Webpack。注意在pom.xml或package.json中锁定主要依赖的版本号避免因自动升级到不兼容版本导致项目无法运行。1.3 项目结构规划清晰的项目结构是团队协作和后期维护的基础。前后端分离项目通常分为两个独立的工程目录。gym-management-system/ ├── backend/ # Spring Boot 后端项目 │ ├── src/main/java/com/example/gym/ │ │ ├── config/ # 配置类跨域、MyBatis-Plus等 │ │ ├── controller/ # 控制器接收请求返回响应 │ │ ├── entity/ # 实体类对应数据库表 │ │ ├── mapper/ # MyBatis Mapper 接口 │ │ ├── service/ # 业务逻辑层接口 │ │ │ └── impl/ # 业务逻辑层实现 │ │ ├── dto/ # 数据传输对象请求/响应封装 │ │ ├── common/ # 通用类统一返回结果、异常等 │ │ └── GymApplication.java # 主启动类 │ └── src/main/resources/ │ ├── application.yml # 主配置文件 │ └── mapper/ # MyBatis XML 文件如使用 ├── frontend/ # Vue 前端项目 │ ├── public/ │ ├── src/ │ │ ├── api/ # 封装所有后端 API 请求 │ │ ├── assets/ # 静态资源 │ │ ├── components/ # 可复用组件 │ │ ├── router/ # 路由配置 │ │ ├── stores/ # Pinia 状态管理 │ │ ├── views/ # 页面视图组件 │ │ ├── utils/ # 工具函数 │ │ └── App.vue │ ├── index.html │ ├── package.json │ └── vite.config.js # Vite 配置 └── README.md2. 后端工程搭建与核心配置后端是整个系统的大脑负责数据安全和业务规则。我们先从数据库设计开始然后搭建 Spring Boot 工程。2.1 数据库设计与建表健身房管理系统的核心实体包括会员、员工、课程、设备和预约记录。这里以会员表和课程预约表为例展示设计思路。-- 会员表 CREATE TABLE member ( id bigint NOT NULL AUTO_INCREMENT COMMENT 主键ID, member_number varchar(20) NOT NULL COMMENT 会员卡号, name varchar(50) NOT NULL COMMENT 姓名, phone varchar(20) DEFAULT NULL COMMENT 手机号, gender tinyint DEFAULT 0 COMMENT 性别 (0:未知1:男2:女), birthday date DEFAULT NULL COMMENT 生日, card_type varchar(20) DEFAULT NULL COMMENT 卡类型 (次卡、月卡、年卡), valid_until datetime DEFAULT NULL COMMENT 有效期至, status tinyint DEFAULT 1 COMMENT 状态 (1:正常0:冻结), create_time datetime DEFAULT CURRENT_TIMESTAMP COMMENT 创建时间, update_time datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT 更新时间, PRIMARY KEY (id), UNIQUE KEY uk_member_number (member_number), KEY idx_phone (phone) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT会员表; -- 课程预约表 CREATE TABLE course_booking ( id bigint NOT NULL AUTO_INCREMENT, member_id bigint NOT NULL COMMENT 会员ID, course_id bigint NOT NULL COMMENT 课程ID, booking_time datetime NOT NULL COMMENT 预约时间, status varchar(20) DEFAULT BOOKED COMMENT 状态 (BOOKED:已预约CANCELLED:已取消COMPLETED:已完成), notes varchar(255) DEFAULT NULL COMMENT 备注, create_time datetime DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), KEY idx_member_id (member_id), KEY idx_course_id (course_id), CONSTRAINT fk_booking_member FOREIGN KEY (member_id) REFERENCES member (id) ON DELETE RESTRICT, CONSTRAINT fk_booking_course FOREIGN KEY (course_id) REFERENCES course (id) ON DELETE RESTRICT ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT课程预约表;设计要点唯一约束会员卡号必须唯一防止重复。索引在手机号、会员ID、课程ID等查询频繁的字段上建立索引提升查询效率。外键约束像course_booking表通过外键确保数据一致性。在生产环境中需根据实际访问量和分库分表策略谨慎使用外键。字段注释每个字段都添加 COMMENT便于后期维护。2.2 Spring Boot 项目初始化与关键配置使用 Spring Initializr 或 IDE 创建项目选择 Web, MySQL, MyBatis-Plus, Lombok 等依赖。关键的配置在application.yml中。# application.yml server: port: 8080 servlet: context-path: /api # 统一API前缀 spring: datasource: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3306/gym_db?useUnicodetruecharacterEncodingUTF-8serverTimezoneAsia/ShanghaiuseSSLfalse username: your_username password: your_password jackson: # 统一JSON序列化格式 date-format: yyyy-MM-dd HH:mm:ss time-zone: GMT8 mybatis-plus: configuration: log-impl: org.apache.ibatis.logging.stdout.StdOutImpl # 开发时开启SQL日志 global-config: db-config: id-type: auto # 主键自增 logic-delete-field: deleted # 全局逻辑删除字段名如果表中有此字段 logic-delete-value: 1 # 逻辑已删除值 logic-not-delete-value: 0 # 逻辑未删除值 mapper-locations: classpath*:/mapper/**/*.xml # XML映射文件位置配置解释server.servlet.context-path: 为所有后端接口添加/api前缀便于前端代理和 Nginx 配置。spring.datasource.url中的serverTimezoneAsia/Shanghai非常重要避免数据库时间与系统时间不一致。mybatis-plus.configuration.log-impl在开发阶段有助于调试 SQL生产环境应关闭或改为文件日志。2.3 实现会员管理模块的 CRUD我们以会员管理为例展示后端如何从实体层到控制层进行开发。1. 实体类 (Entity)使用 Lombok 和 MyBatis-Plus 注解。// src/main/java/com/example/gym/entity/Member.java package com.example.gym.entity; import com.baomidou.mybatisplus.annotation.*; import lombok.Data; import java.time.LocalDateTime; import java.util.Date; Data TableName(member) // 指定表名 public class Member { TableId(type IdType.AUTO) // 主键自增 private Long id; private String memberNumber; private String name; private String phone; private Integer gender; // 使用包装类型允许为null private Date birthday; private String cardType; private LocalDateTime validUntil; private Integer status; TableField(fill FieldFill.INSERT) // 插入时自动填充 private LocalDateTime createTime; TableField(fill FieldFill.INSERT_UPDATE) // 插入和更新时自动填充 private LocalDateTime updateTime; }2. Mapper 接口继承 MyBatis-Plus 的BaseMapper即刻拥有基础 CRUD 方法。// src/main/java/com/example/gym/mapper/MemberMapper.java 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 // 重要让 Spring 管理此 Mapper public interface MemberMapper extends BaseMapperMember { // 可以在此定义自定义的复杂SQL方法 }3. Service 层定义接口和实现封装业务逻辑。// src/main/java/com/example/gym/service/MemberService.java package com.example.gym.service; import com.baomidou.mybatisplus.extension.plugins.pagination.Page; import com.baomidou.mybatisplus.extension.service.IService; import com.example.gym.entity.Member; import com.example.gym.dto.MemberQueryDTO; public interface MemberService extends IServiceMember { PageMember pageQuery(MemberQueryDTO queryDTO); } // src/main/java/com/example/gym/service/impl/MemberServiceImpl.java package com.example.gym.service.impl; import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import com.baomidou.mybatisplus.extension.plugins.pagination.Page; import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; import com.example.gym.dto.MemberQueryDTO; import com.example.gym.entity.Member; import com.example.gym.mapper.MemberMapper; import com.example.gym.service.MemberService; import org.apache.commons.lang3.StringUtils; import org.springframework.stereotype.Service; Service public class MemberServiceImpl extends ServiceImplMemberMapper, Member implements MemberService { Override public PageMember pageQuery(MemberQueryDTO queryDTO) { // 构建分页对象 PageMember page new Page(queryDTO.getPageNum(), queryDTO.getPageSize()); // 构建查询条件 LambdaQueryWrapperMember wrapper new LambdaQueryWrapper(); wrapper.like(StringUtils.isNotBlank(queryDTO.getName()), Member::getName, queryDTO.getName()) .eq(StringUtils.isNotBlank(queryDTO.getPhone()), Member::getPhone, queryDTO.getPhone()) .eq(queryDTO.getStatus() ! null, Member::getStatus, queryDTO.getStatus()) .orderByDesc(Member::getCreateTime); // 按创建时间倒序 // 执行分页查询 return this.page(page, wrapper); } }4. Controller 层提供 RESTful API并处理跨域问题。// src/main/java/com/example/gym/controller/MemberController.java package com.example.gym.controller; import com.baomidou.mybatisplus.extension.plugins.pagination.Page; import com.example.gym.common.R; import com.example.gym.dto.MemberQueryDTO; import com.example.gym.entity.Member; import com.example.gym.service.MemberService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.*; import javax.validation.Valid; RestController RequestMapping(/member) CrossOrigin // 简单处理跨域生产环境建议使用配置类或网关处理 public class MemberController { Autowired private MemberService memberService; PostMapping public RLong addMember(Valid RequestBody Member member) { // 业务逻辑检查会员号是否重复等... memberService.save(member); return R.success(member.getId()); } DeleteMapping(/{id}) public RBoolean deleteMember(PathVariable Long id) { return R.success(memberService.removeById(id)); } PutMapping public RBoolean updateMember(Valid RequestBody Member member) { return R.success(memberService.updateById(member)); } GetMapping(/{id}) public RMember getMemberById(PathVariable Long id) { return R.success(memberService.getById(id)); } GetMapping(/page) public RPageMember pageMember(MemberQueryDTO queryDTO) { // 参数校验可以放在DTO里用Validated return R.success(memberService.pageQuery(queryDTO)); } }5. 统一返回结果封装定义一个通用的返回类R使前端接口调用格式统一。// src/main/java/com/example/gym/common/R.java package com.example.gym.common; import lombok.Data; import java.io.Serializable; Data public class RT implements Serializable { private Integer code; // 状态码如 200成功500失败 private String msg; // 提示信息 private T data; // 返回数据 public static T RT success(T data) { RT r new R(); r.setCode(200); r.setMsg(操作成功); r.setData(data); return r; } public static T RT error(String msg) { RT r new R(); r.setCode(500); r.setMsg(msg); return r; } // 可以定义更多状态码如 400参数错误401未授权403禁止访问等 }3. 前端工程搭建与 API 集成前端负责数据的展示和收集并通过 Axios 与后端通信。3.1 使用 Vite 初始化 Vue 3 项目使用以下命令快速创建项目并选择 Vue 和 TypeScript/JavaScript。npm create vitelatest frontend -- --template vue cd frontend npm install安装必要的依赖npm install axios pinia element-plus vue-router npm install -D iconify-json/ep # Element Plus 图标3.2 配置路由与状态管理路由配置 (src/router/index.js):import { createRouter, createWebHistory } from vue-router import Login from ../views/Login.vue import Layout from ../layouts/MainLayout.vue import MemberList from ../views/member/MemberList.vue const routes [ { path: /login, name: Login, component: Login, meta: { requiresAuth: false } }, { path: /, component: Layout, meta: { requiresAuth: true }, // 需要登录 children: [ { path: /members, name: MemberList, component: MemberList }, // ... 其他路由如课程管理、设备管理等 ] } ] const router createRouter({ history: createWebHistory(), routes }) // 可在此添加全局路由守卫进行权限校验 // router.beforeEach((to, from, next) { ... }) export default router状态管理 (src/stores/user.js):使用 Pinia 管理用户登录状态、token 等信息。// src/stores/user.js import { defineStore } from pinia import { ref } from vue import { login as apiLogin } from /api/auth export const useUserStore defineStore(user, () { const token ref(localStorage.getItem(token) || ) const userInfo ref(JSON.parse(localStorage.getItem(userInfo) || {})) const login async (loginForm) { const res await apiLogin(loginForm) token.value res.data.token userInfo.value res.data.userInfo // 存储到本地 localStorage.setItem(token, token.value) localStorage.setItem(userInfo, JSON.stringify(userInfo.value)) } const logout () { token.value userInfo.value {} localStorage.removeItem(token) localStorage.removeItem(userInfo) router.push(/login) } return { token, userInfo, login, logout } })3.3 封装 Axios 并实现会员列表页1. 封装 Axios 实例 (src/utils/request.js):统一处理请求拦截添加 token、响应拦截处理错误和基础 URL。import axios from axios import { ElMessage } from element-plus import router from /router const service axios.create({ baseURL: import.meta.env.VITE_APP_BASE_API, // 从 .env 文件读取 timeout: 10000 }) // 请求拦截器 service.interceptors.request.use( (config) { const token localStorage.getItem(token) if (token) { config.headers[Authorization] Bearer ${token} } return config }, (error) { return Promise.reject(error) } ) // 响应拦截器 service.interceptors.response.use( (response) { const res response.data // 假设后端统一返回 { code, msg, data } 结构 if (res.code 200) { return res.data // 直接返回业务数据 } else { ElMessage.error(res.msg || 请求失败) // 如果是 401 未授权跳转到登录页 if (res.code 401) { router.push(/login) } return Promise.reject(new Error(res.msg || Error)) } }, (error) { console.error(请求错误:, error) ElMessage.error(error.message || 网络错误) return Promise.reject(error) } ) export default service2. 封装 API 模块 (src/api/member.js):import request from /utils/request export function getMemberList(params) { return request({ url: /member/page, method: get, params // 对应 RequestParam参数会拼接到 URL 上 }) } export function addMember(data) { return request({ url: /member, method: post, data // 对应 RequestBody参数在请求体中 }) } export function updateMember(data) { return request({ url: /member, method: put, data }) } export function deleteMember(id) { return request({ url: /member/${id}, method: delete }) }3. 实现会员列表页面 (src/views/member/MemberList.vue):template div classmember-list el-card template #header div classcard-header span会员管理/span el-button typeprimary clickhandleAdd新增会员/el-button /div /template !-- 搜索表单 -- el-form :inlinetrue :modelqueryParams classdemo-form-inline el-form-item label姓名 el-input v-modelqueryParams.name placeholder请输入姓名 clearable / /el-form-item el-form-item label手机号 el-input v-modelqueryParams.phone placeholder请输入手机号 clearable / /el-form-item el-form-item label状态 el-select v-modelqueryParams.status placeholder请选择 clearable el-option label正常 :value1 / el-option label冻结 :value0 / /el-select /el-form-item el-form-item el-button typeprimary clickhandleQuery查询/el-button el-button clickresetQuery重置/el-button /el-form-item /el-form !-- 数据表格 -- el-table :datatableData border stylewidth: 100% el-table-column propmemberNumber label会员卡号 width180 / el-table-column propname label姓名 width120 / el-table-column propphone label手机号 width150 / el-table-column propgender label性别 width80 template #defaultscope {{ scope.row.gender 1 ? 男 : scope.row.gender 2 ? 女 : 未知 }} /template /el-table-column el-table-column propcardType label卡类型 width120 / el-table-column propvalidUntil label有效期至 width180 template #defaultscope {{ formatDate(scope.row.validUntil) }} /template /el-table-column el-table-column propstatus label状态 width100 template #defaultscope el-tag :typescope.row.status 1 ? success : danger {{ scope.row.status 1 ? 正常 : 冻结 }} /el-tag /template /el-table-column el-table-column label操作 width200 fixedright template #defaultscope el-button sizesmall clickhandleEdit(scope.row)编辑/el-button el-button sizesmall typedanger clickhandleDelete(scope.row)删除/el-button /template /el-table-column /el-table !-- 分页 -- div classpagination-container el-pagination v-model:current-pagequeryParams.pageNum v-model:page-sizequeryParams.pageSize :page-sizes[10, 20, 50, 100] :totaltotal layouttotal, sizes, prev, pager, next, jumper size-changehandleSizeChange current-changehandleCurrentChange / /div /el-card !-- 新增/编辑对话框 -- MemberDialog v-modeldialogVisible :form-datacurrentRow successhandleDialogSuccess / /div /template script setup import { ref, reactive, onMounted } from vue import { ElMessage, ElMessageBox } from element-plus import { getMemberList, deleteMember } from /api/member import MemberDialog from ./components/MemberDialog.vue import { formatDate } from /utils/date const tableData ref([]) const total ref(0) const dialogVisible ref(false) const currentRow ref({}) const queryParams reactive({ pageNum: 1, pageSize: 10, name: , phone: , status: null }) // 加载数据 const loadData async () { try { const res await getMemberList(queryParams) tableData.value res.records total.value res.total } catch (error) { console.error(加载会员列表失败:, error) } } // 查询 const handleQuery () { queryParams.pageNum 1 loadData() } // 重置查询 const resetQuery () { Object.assign(queryParams, { pageNum: 1, pageSize: 10, name: , phone: , status: null }) loadData() } // 分页事件 const handleSizeChange (val) { queryParams.pageSize val loadData() } const handleCurrentChange (val) { queryParams.pageNum val loadData() } // 新增 const handleAdd () { currentRow.value {} dialogVisible.value true } // 编辑 const handleEdit (row) { currentRow.value { ...row } // 浅拷贝避免直接修改表格数据 dialogVisible.value true } // 删除 const handleDelete (row) { ElMessageBox.confirm(确定要删除会员【${row.name}】吗, 提示, { confirmButtonText: 确定, cancelButtonText: 取消, type: warning }).then(async () { await deleteMember(row.id) ElMessage.success(删除成功) loadData() }).catch(() {}) } // 对话框操作成功回调 const handleDialogSuccess () { dialogVisible.value false loadData() } onMounted(() { loadData() }) /script style scoped .card-header { display: flex; justify-content: space-between; align-items: center; } .pagination-container { margin-top: 20px; display: flex; justify-content: flex-end; } /style3.4 配置开发环境代理解决跨域问题在开发阶段前端运行在localhost:5173后端运行在localhost:8080浏览器会因同源策略阻止请求。Vite 提供了便捷的代理配置。// vite.config.js import { defineConfig } from vite import vue from vitejs/plugin-vue export default defineConfig({ plugins: [vue()], server: { port: 5173, proxy: { /api: { // 匹配所有以 /api 开头的请求 target: http://localhost:8080, // 后端地址 changeOrigin: true, // 改变请求头中的 Origin rewrite: (path) path.replace(/^\/api/, ) // 重写路径去掉 /api 前缀 } } } })配置后前端请求/api/member/page会被代理到http://localhost:8080/member/page从而绕过浏览器的跨域限制。4. 项目运行、验证与常见问题排查将前后端项目都启动起来进行功能验证并熟悉常见的错误排查路径。4.1 启动与验证步骤启动后端确保 MySQL 服务已启动并创建了gym_db数据库。在 IDE 中运行GymApplication主类或使用命令行mvn spring-boot:run。观察控制台日志确认无报错并看到类似Tomcat started on port(s): 8080的信息。启动前端进入frontend目录运行npm run dev。控制台会输出本地访问地址通常是http://localhost:5173。功能验证在浏览器打开http://localhost:5173应能看到登录页或主界面。打开浏览器开发者工具F12进入 Network 标签页。在页面中进行操作如查询会员列表。在 Network 中应能看到发往后端localhost:8080的请求并且状态码为 200。检查后端控制台应能看到对应的 SQL 执行日志。4.2 常见问题与排查路径在整合过程中90% 的问题集中在网络通信、配置和依赖版本上。问题现象可能原因检查方式处理建议前端页面无法访问 (localhost:5173)1. 前端服务未启动。2. 端口被占用。1. 检查终端是否成功运行npm run dev。2. 运行netstat -ano | findstr :5173(Windows) 或lsof -i:5173(Mac/Linux) 查看端口占用。1. 重新启动前端服务。2. 杀死占用端口的进程或修改vite.config.js中的port。前端请求后端接口报 4041. 代理配置错误。2. 后端接口路径错误。3. 后端服务未启动。1. 检查浏览器 Network 中请求的完整 URL 是否正确是否带/api前缀。2. 检查vite.config.js中的proxy配置。3. 检查后端控制台是否启动成功。1. 核对前端request.js中的baseURL和代理规则。2. 直接使用 Postman 或 curl 测试后端接口确认接口本身可用。前端请求后端接口报跨域错误 (CORS)1. 开发阶段代理未生效。2. 生产环境未配置 CORS。1. 检查请求 URL 的 Origin 是否还是前端地址。2. 检查后端是否添加了CrossOrigin注解或全局 CORS 配置。1. 确保vite.config.js代理配置正确且前端请求路径以/api开头。2. 生产环境建议在后端使用配置类统一处理 CORS而不是在每个 Controller 上加注解。后端连接数据库失败1. 数据库服务未启动。2.application.yml中数据库连接信息错误。3. MySQL 驱动版本不匹配。1. 检查 MySQL 服务状态。2. 检查url,username,password。3. 查看启动日志中的具体错误信息。1. 启动 MySQL。2. 使用数据库客户端工具测试连接。3. 确认 MySQL 版本5.x 或 8.x与驱动版本匹配。8.x 需要com.mysql.cj.jdbc.Driver和serverTimezone参数。页面显示正常但表格无数据1. 前端 API 请求未成功。2. 后端查询逻辑有误。3. 数据库无数据。1. 打开浏览器开发者工具 Network 标签查看 API 请求的响应状态和数据。2. 查看后端控制台 SQL 日志看是否执行了查询。3. 直接查询数据库确认。1. 根据 Network 中的响应状态码和消息体定位问题。2. 在后端 Service 层方法中打日志或调试检查查询条件。MyBatis-Plus 插入/更新失败1. 实体类字段与数据库列名未正确映射。2. 主键策略配置错误。1. 检查TableField注解的value属性是否与数据库列名一致驼峰转下划线默认开启。2. 检查TableId注解的type属性。1. 开启 MyBatis-Plus SQL 日志查看最终执行的 SQL 语句。2. 确认数据库表主键是否为自增AUTO_INCREMENT。4.3 生产环境部署考量学习环境能跑通只是第一步生产环境需要考虑更多。前端部署运行npm run build生成静态文件位于dist目录。将dist目录下的文件部署到 Nginx 或 Apache 等 Web 服务器。配置 Nginx将 API 请求反向代理到后端服务。# nginx.conf 示例片段 server { listen 80; server_name your-domain.com; # 前端静态资源 location / { root /path/to/frontend/dist; index index.html; try_files $uri $uri/ /index.html; # 支持 Vue Router 的 history 模式 } # 后端 API 代理 location /api/ { proxy_pass http://localhost:8080/; # 后端服务地址 proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; } }后端部署使用mvn clean package打包生成可执行的 JAR 文件。在服务器上安装 Java 运行环境JRE。使用nohup java -jar your-app.jar 启动或使用 systemd、Docker 等更优的方式管理进程。关键配置外置将数据库密码等敏感信息从application.yml移到外部配置文件如application-prod.yml或环境变量中并通过--spring.profiles.activeprod指定激活生产配置。安全与优化API 安全实现完整的认证如 JWT和授权如基于角色的访问控制。输入校验在后端 Controller 和 Service 层加强参数校验防止 SQL 注入和 XSS 攻击。日志配置完整的日志框架如 Logback记录请求、错误和业务日志便于排查问题。数据库连接池调整连接池参数如 HikariCP优化数据库连接性能。前端优化配置路由懒加载、组件异步加载减少首屏加载时间。通过以上步骤你不仅能够搭建并运行一个健身房管理系统更能理解前后端分离项目从开发到部署的完整流程和其中的关键细节。在实际项目中可以根据业务需求在此基础上扩展课程管理、预约系统、数据统计等功能模块。