Vue+Spring Boot健身房管理系统实战:前后端分离架构与CRUD实现
在实际开发中,很多开发者学习了 Vue 和 Spring Boot 的基础知识后,却不知道如何将它们组合起来,构建一个功能完整、结构清晰的前后端分离项目。健身房管理系统是一个典型的业务场景,它涵盖了会员管理、课程预约、设备维护、员工排班等多个模块,非常适合用来练习前后端分离架构下的 CRUD 操作、权限控制、数据交互和项目部署。本文将带你从零开始,理解并实践如何构建一个基于 Vue 和 Spring Boot 的健身房管理系统,并深入探讨在整合过程中那些容易被忽略但至关重要的细节。
本文的目标读者是已经掌握 Vue 和 Spring Boot 基础,希望进行项目实战的开发者。你将了解到如何规划项目结构、设计数据库、实现前后端数据交互、处理跨域问题,以及如何将项目打包部署。我们不会止步于“跑通”,而是会深入到配置参数的含义、常见错误的排查路径,以及从学习环境到生产环境需要考虑的额外因素。
1. 理解前后端分离架构与项目技术选型
在开始编码之前,必须清晰理解我们选择的技术栈各自扮演什么角色,以及它们如何协同工作。这决定了后续的目录结构、开发流程和问题排查方向。
1.1 为什么选择 Vue + Spring Boot
Vue 作为前端框架,负责用户界面的构建和用户交互逻辑。它通过组件化开发,使得页面结构清晰、易于维护。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`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 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 ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 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?useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai&useSSL=false username: your_username password: your_password jackson: # 统一JSON序列化格式 date-format: yyyy-MM-dd HH:mm:ss time-zone: GMT+8 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中的serverTimezone=Asia/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 BaseMapper<Member> { // 可以在此定义自定义的复杂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 IService<Member> { Page<Member> 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 ServiceImpl<MemberMapper, Member> implements MemberService { @Override public Page<Member> pageQuery(MemberQueryDTO queryDTO) { // 构建分页对象 Page<Member> page = new Page<>(queryDTO.getPageNum(), queryDTO.getPageSize()); // 构建查询条件 LambdaQueryWrapper<Member> 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 R<Long> addMember(@Valid @RequestBody Member member) { // 业务逻辑:检查会员号是否重复等... memberService.save(member); return R.success(member.getId()); } @DeleteMapping("/{id}") public R<Boolean> deleteMember(@PathVariable Long id) { return R.success(memberService.removeById(id)); } @PutMapping public R<Boolean> updateMember(@Valid @RequestBody Member member) { return R.success(memberService.updateById(member)); } @GetMapping("/{id}") public R<Member> getMemberById(@PathVariable Long id) { return R.success(memberService.getById(id)); } @GetMapping("/page") public R<Page<Member>> 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 R<T> implements Serializable { private Integer code; // 状态码,如 200成功,500失败 private String msg; // 提示信息 private T data; // 返回数据 public static <T> R<T> success(T data) { R<T> r = new R<>(); r.setCode(200); r.setMsg("操作成功"); r.setData(data); return r; } public static <T> R<T> error(String msg) { R<T> 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 vite@latest 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 class="member-list"> <el-card> <template #header> <div class="card-header"> <span>会员管理</span> <el-button type="primary" @click="handleAdd">新增会员</el-button> </div> </template> <!-- 搜索表单 --> <el-form :inline="true" :model="queryParams" class="demo-form-inline"> <el-form-item label="姓名"> <el-input v-model="queryParams.name" placeholder="请输入姓名" clearable /> </el-form-item> <el-form-item label="手机号"> <el-input v-model="queryParams.phone" placeholder="请输入手机号" clearable /> </el-form-item> <el-form-item label="状态"> <el-select v-model="queryParams.status" placeholder="请选择" clearable> <el-option label="正常" :value="1" /> <el-option label="冻结" :value="0" /> </el-select> </el-form-item> <el-form-item> <el-button type="primary" @click="handleQuery">查询</el-button> <el-button @click="resetQuery">重置</el-button> </el-form-item> </el-form> <!-- 数据表格 --> <el-table :data="tableData" border style="width: 100%"> <el-table-column prop="memberNumber" label="会员卡号" width="180" /> <el-table-column prop="name" label="姓名" width="120" /> <el-table-column prop="phone" label="手机号" width="150" /> <el-table-column prop="gender" label="性别" width="80"> <template #default="scope"> {{ scope.row.gender === 1 ? '男' : scope.row.gender === 2 ? '女' : '未知' }} </template> </el-table-column> <el-table-column prop="cardType" label="卡类型" width="120" /> <el-table-column prop="validUntil" label="有效期至" width="180"> <template #default="scope"> {{ formatDate(scope.row.validUntil) }} </template> </el-table-column> <el-table-column prop="status" label="状态" width="100"> <template #default="scope"> <el-tag :type="scope.row.status === 1 ? 'success' : 'danger'"> {{ scope.row.status === 1 ? '正常' : '冻结' }} </el-tag> </template> </el-table-column> <el-table-column label="操作" width="200" fixed="right"> <template #default="scope"> <el-button size="small" @click="handleEdit(scope.row)">编辑</el-button> <el-button size="small" type="danger" @click="handleDelete(scope.row)">删除</el-button> </template> </el-table-column> </el-table> <!-- 分页 --> <div class="pagination-container"> <el-pagination v-model:current-page="queryParams.pageNum" v-model:page-size="queryParams.pageSize" :page-sizes="[10, 20, 50, 100]" :total="total" layout="total, sizes, prev, pager, next, jumper" @size-change="handleSizeChange" @current-change="handleCurrentChange" /> </div> </el-card> <!-- 新增/编辑对话框 --> <MemberDialog v-model="dialogVisible" :form-data="currentRow" @success="handleDialogSuccess" /> </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; } </style>3.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的信息。
- 确保 MySQL 服务已启动,并创建了
启动前端:
- 进入
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。 |
| 前端请求后端接口报 404 | 1. 代理配置错误。 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.active=prod指定激活生产配置。
- 使用
安全与优化:
- API 安全:实现完整的认证(如 JWT)和授权(如基于角色的访问控制)。
- 输入校验:在后端 Controller 和 Service 层加强参数校验,防止 SQL 注入和 XSS 攻击。
- 日志:配置完整的日志框架(如 Logback),记录请求、错误和业务日志,便于排查问题。
- 数据库连接池:调整连接池参数(如 HikariCP),优化数据库连接性能。
- 前端优化:配置路由懒加载、组件异步加载,减少首屏加载时间。
通过以上步骤,你不仅能够搭建并运行一个健身房管理系统,更能理解前后端分离项目从开发到部署的完整流程和其中的关键细节。在实际项目中,可以根据业务需求,在此基础上扩展课程管理、预约系统、数据统计等功能模块。