Spring Boot 3 + Vue 3 一小时快速构建健身管理系统
最近在帮学弟学妹们准备期末课设和毕业设计,发现很多同学对“前后端分离”项目望而却步,觉得从零搭建一个完整的系统太复杂。其实,只要掌握核心流程,1小时快速搭建一个具备增删改查功能的可运行项目是完全可行的。
本文将以一个 健身管理系统 为例,手把手带你从零开始,使用 Spring Boot 3 作为后端, Vue 3 + Element Plus 作为前端,快速构建一个功能完整、界面美观的Web应用。整个过程我会拆解成清晰的步骤,并提供完整的源码和关键配置,确保你不仅能跑起来,还能理解每一步背后的逻辑。无论是应付期末作业、课程设计,还是作为个人练手项目,这套方案都能让你事半功倍。
1. 项目背景与核心技术栈
在开始敲代码之前,我们先明确这个项目要做什么,以及为什么选择这些技术。
1.1 项目需求:健身管理系统
想象一下,一个健身房需要管理它的会员、教练、课程和预约信息。我们的系统需要提供以下核心功能:
- 会员管理 :注册新会员,查看、编辑、删除会员信息。
- 教练管理 :管理教练的基本信息和授课方向。
- 课程管理 :发布健身课程(如瑜伽、动感单车),并设置课程时间、教练和容纳人数。
- 预约管理 :会员可以预约课程,系统需要处理预约、取消预约,并防止超额预约。
这是一个典型的 CRUD(增删改查) 应用,也是绝大多数管理类系统的雏形。
1.2 为什么选择这些技术?
- 后端:Spring Boot 3
- 快速开发 :约定大于配置,内嵌Tomcat,无需繁琐的XML配置。
- 生态强大 :Spring Data JPA 能极大简化数据库操作,Spring MVC 提供稳健的Web层支持。
- 适合新手 :社区资料丰富,遇到问题容易找到解决方案。
- 前端:Vue 3 + Element Plus
- 渐进式框架 :易于上手,从简单的页面开始,逐步增加复杂度。
- 组合式API :Vue 3 的
setup语法让逻辑组织更清晰。 - UI组件库 :Element Plus 提供了丰富、美观的现成组件(表格、表单、弹窗等),让我们能快速搭建出专业的后台界面。
- 数据库:MySQL
- 最流行的开源关系型数据库,学习成本低,与Spring Boot集成简单。
- 架构:前后端分离
- 前端 独立运行在Node.js服务上,负责页面渲染和用户交互。
- 后端 提供纯粹的RESTful API接口,返回JSON数据。
- 优势 :前后端开发可以并行,技术栈选择更灵活,更符合现代Web开发趋势。
2. 开发环境准备
工欲善其事,必先利其器。请确保你的电脑上已经安装了以下软件,版本尽量与下文保持一致,以避免不必要的兼容性问题。
2.1 后端环境 (Spring Boot)
- JDK :版本 17 或以上(Spring Boot 3.x 要求至少 JDK 17)。在终端输入
java -version检查。 - Maven :版本 3.6+ ,用于管理项目依赖和构建。在终端输入
mvn -v检查。 - IDE : IntelliJ IDEA Ultimate (推荐) 或 Eclipse 。社区版也可用。
- MySQL :版本 8.0+ 。安装并启动MySQL服务,记住你的root密码。
2.2 前端环境 (Vue 3)
- Node.js :版本 18+ 。这是运行Vue和包管理工具npm的基础。在终端输入
node -v和npm -v检查。 - Vue CLI 或 Vite :本文使用更轻快的 Vite 作为构建工具。可以通过npm全局安装:
npm install -g @vue/cli或直接使用Vite。
2.3 辅助工具
- Postman 或 Apifox :用于测试后端API接口。
- Git :用于版本控制和获取本文源码。
3. 后端开发:Spring Boot 3 实现 RESTful API
我们从后端开始,构建提供数据服务的API。
3.1 创建Spring Boot项目
使用 IntelliJ IDEA 的 Spring Initializr 创建项目是最快的方式。
- 选择 Spring Boot 3.x (本文使用 3.2.x)。
- 打包方式:
Jar。 - Java版本:
17。 - 依赖选择:
- Spring Web :构建Web应用,包含RESTful支持。
- Spring Data JPA :简化数据库操作。
- MySQL Driver :连接MySQL数据库。
- Lombok :通过注解自动生成Getter/Setter等方法,让代码更简洁。
项目创建完成后, pom.xml 文件应包含类似依赖:
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>com.mysql</groupId>
<artifactId>mysql-connector-j</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
3.2 配置数据库连接
在 src/main/resources/application.properties 文件中配置数据库信息。请将 your_password 和 your_database 替换为你自己的。
# 应用服务端口,避免与前端冲突
server.port=8080
# 数据库连接配置
spring.datasource.url=jdbc:mysql://localhost:3306/fitness_db?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai
spring.datasource.username=root
spring.datasource.password=your_password
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
# JPA 配置
spring.jpa.database-platform=org.hibernate.dialect.MySQL8Dialect
spring.jpa.hibernate.ddl-auto=update # 启动时根据实体类更新表结构,生产环境请改为 validate 或 none
spring.jpa.show-sql=true # 在控制台显示SQL语句,方便调试
spring.jpa.properties.hibernate.format_sql=true # 格式化输出的SQL
注意 : ddl-auto=update 在开发时很方便,但生产环境务必改为 validate ,并通过SQL脚本管理表结构,以防数据丢失。
3.3 创建实体类 (Entity)
实体类对应数据库中的表。我们创建会员(Member)和课程(Course)两个核心实体。
文件路径: src/main/java/com/fitness/entity/Member.java
package com.fitness.entity;
import jakarta.persistence.*;
import lombok.Data;
import java.time.LocalDateTime;
@Entity
@Table(name = "t_member")
@Data // Lombok 注解,自动生成 getter, setter, toString 等
public class Member {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(nullable = false, unique = true)
private String memberId; // 会员卡号
@Column(nullable = false)
private String name;
private String phone;
private String email;
@Enumerated(EnumType.STRING)
private Gender gender; // 枚举类型:MALE, FEMALE
private LocalDateTime registerDate;
@PrePersist
protected void onCreate() {
registerDate = LocalDateTime.now(); // 自动设置注册时间
}
public enum Gender {
MALE, FEMALE
}
}
文件路径: src/main/java/com/fitness/entity/Course.java
package com.fitness.entity;
import jakarta.persistence.*;
import lombok.Data;
import java.time.LocalDateTime;
@Entity
@Table(name = "t_course")
@Data
public class Course {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(nullable = false)
private String name; // 课程名,如“高温瑜伽”
@Column(nullable = false)
private String coachName; // 教练姓名
@Column(nullable = false)
private LocalDateTime startTime; // 课程开始时间
@Column(nullable = false)
private Integer duration; // 课程时长(分钟)
@Column(nullable = false)
private Integer capacity; // 课程容量
@Column(nullable = false)
private Integer booked; // 已预约人数,初始为0
// 判断课程是否已满
public boolean isFull() {
return booked >= capacity;
}
// 预约课程
public boolean book() {
if (!isFull()) {
booked++;
return true;
}
return false;
}
// 取消预约
public boolean cancel() {
if (booked > 0) {
booked--;
return true;
}
return false;
}
}
3.4 创建数据访问层 (Repository)
Spring Data JPA 的神奇之处在于,我们只需定义一个接口,就能获得基本的CRUD方法。
文件路径: src/main/java/com/fitness/repository/MemberRepository.java
package com.fitness.repository;
import com.fitness.entity.Member;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
@Repository
public interface MemberRepository extends JpaRepository<Member, Long> {
// 可以根据会员卡号查询
Member findByMemberId(String memberId);
// 可以自定义更多查询方法,JPA会自动实现
// List<Member> findByNameContaining(String keyword);
}
文件路径: src/main/java/com/fitness/repository/CourseRepository.java
package com.fitness.repository;
import com.fitness.entity.Course;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
import java.time.LocalDateTime;
import java.util.List;
@Repository
public interface CourseRepository extends JpaRepository<Course, Long> {
// 查询指定时间之后的课程
List<Course> findByStartTimeAfter(LocalDateTime time);
}
3.5 创建业务逻辑层 (Service)
Service层处理业务规则,例如预约课程时的名额检查。
文件路径: src/main/java/com/fitness/service/CourseService.java
package com.fitness.service;
import com.fitness.entity.Course;
import com.fitness.repository.CourseRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.List;
import java.util.Optional;
@Service
public class CourseService {
@Autowired
private CourseRepository courseRepository;
public List<Course> findAll() {
return courseRepository.findAll();
}
public Optional<Course> findById(Long id) {
return courseRepository.findById(id);
}
public Course save(Course course) {
return courseRepository.save(course);
}
public void deleteById(Long id) {
courseRepository.deleteById(id);
}
@Transactional // 重要:预约操作需要事务保证数据一致性
public boolean bookCourse(Long courseId) {
Optional<Course> optionalCourse = courseRepository.findById(courseId);
if (optionalCourse.isPresent()) {
Course course = optionalCourse.get();
boolean success = course.book(); // 调用实体类的book方法
if (success) {
courseRepository.save(course); // 更新已预约人数
return true;
}
}
return false; // 课程不存在或已满
}
@Transactional
public boolean cancelCourse(Long courseId) {
Optional<Course> optionalCourse = courseRepository.findById(courseId);
if (optionalCourse.isPresent()) {
Course course = optionalCourse.get();
boolean success = course.cancel();
if (success) {
courseRepository.save(course);
return true;
}
}
return false;
}
}
3.6 创建控制器层 (Controller)
Controller接收前端请求,调用Service,并返回JSON数据。这里需要处理 跨域请求(CORS) ,因为前端运行在另一个端口。
文件路径: src/main/java/com/fitness/controller/CourseController.java
package com.fitness.controller;
import com.fitness.entity.Course;
import com.fitness.service.CourseService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
@RestController
@RequestMapping("/api/courses")
@CrossOrigin(origins = "http://localhost:5173") // 允许Vue前端(默认端口5173)跨域访问
public class CourseController {
@Autowired
private CourseService courseService;
@GetMapping
public List<Course> getAllCourses() {
return courseService.findAll();
}
@GetMapping("/{id}")
public ResponseEntity<Course> getCourseById(@PathVariable Long id) {
return courseService.findById(id)
.map(ResponseEntity::ok)
.orElse(ResponseEntity.notFound().build());
}
@PostMapping
public Course createCourse(@RequestBody Course course) {
// 设置初始已预约人数为0
course.setBooked(0);
return courseService.save(course);
}
@PutMapping("/{id}")
public ResponseEntity<Course> updateCourse(@PathVariable Long id, @RequestBody Course courseDetails) {
return courseService.findById(id)
.map(course -> {
course.setName(courseDetails.getName());
course.setCoachName(courseDetails.getCoachName());
course.setStartTime(courseDetails.getStartTime());
course.setDuration(courseDetails.getDuration());
course.setCapacity(courseDetails.getCapacity());
// booked 人数通常不通过更新接口直接修改
Course updatedCourse = courseService.save(course);
return ResponseEntity.ok(updatedCourse);
})
.orElse(ResponseEntity.notFound().build());
}
@DeleteMapping("/{id}")
public ResponseEntity<?> deleteCourse(@PathVariable Long id) {
courseService.deleteById(id);
return ResponseEntity.ok().build();
}
@PostMapping("/{id}/book")
public ResponseEntity<?> bookCourse(@PathVariable Long id) {
boolean success = courseService.bookCourse(id);
Map<String, Object> response = new HashMap<>();
if (success) {
response.put("success", true);
response.put("message", "课程预约成功");
return ResponseEntity.ok(response);
} else {
response.put("success", false);
response.put("message", "课程预约失败,可能课程已满或不存在");
return ResponseEntity.badRequest().body(response);
}
}
@PostMapping("/{id}/cancel")
public ResponseEntity<?> cancelCourse(@PathVariable Long id) {
boolean success = courseService.cancelCourse(id);
Map<String, Object> response = new HashMap<>();
if (success) {
response.put("success", true);
response.put("message", "取消预约成功");
return ResponseEntity.ok(response);
} else {
response.put("success", false);
response.put("message", "取消预约失败");
return ResponseEntity.badRequest().body(response);
}
}
}
注意 : MemberController 的编写逻辑类似,主要是对 Member 实体进行CRUD,这里不再赘述以节省篇幅。
3.7 启动并测试后端
- 在MySQL中创建数据库:
CREATE DATABASE fitness_db; - 在IDEA中运行
FitnessApplication主类。 - 看到控制台输出
Tomcat started on port(s): 8080即表示启动成功。 - 使用 Postman 测试API:
GET http://localhost:8080/api/courses(获取所有课程)POST http://localhost:8080/api/courses(创建课程,Body选raw JSON)
{ "name": "动感单车", "coachName": "王教练", "startTime": "2024-06-15T19:00:00", "duration": 60, "capacity": 20 }POST http://localhost:8080/api/courses/1/book(预约ID为1的课程)
确保所有接口都能正常返回数据。
4. 前端开发:Vue 3 + Element Plus 构建管理界面
后端API就绪后,我们开始构建用户界面。
4.1 创建Vue 3项目
打开终端,执行以下命令:
# 使用Vite创建Vue项目,项目名 fitness-frontend
npm create vue@latest fitness-frontend
创建过程中,根据提示选择:
- Vue Router: No (为简化,本项目暂不使用路由)
- Pinia: No (为简化,使用组件状态)
- ESLint: Yes (推荐,保持代码规范)
创建完成后,进入项目并安装Element Plus和Axios(用于HTTP请求):
cd fitness-frontend
npm install element-plus axios
npm install # 安装其他初始依赖
4.2 配置Element Plus和Axios
修改 src/main.js 文件,全局引入Element Plus和Axios。
文件路径: src/main.js
import { createApp } from 'vue'
import './style.css'
import App from './App.vue'
// 引入Element Plus
import ElementPlus from 'element-plus'
import 'element-plus/dist/index.css'
// 引入Axios并配置基础URL
import axios from 'axios'
axios.defaults.baseURL = 'http://localhost:8080' // 指向后端API地址
const app = createApp(App)
app.use(ElementPlus)
app.mount('#app')
4.3 实现课程管理页面
我们将主要功能集成在 App.vue 中。实际项目中建议拆分组件。
文件路径: src/App.vue
<template>
<div id="app" class="app-container">
<el-container>
<el-header style="text-align: center; background-color: #409EFF; color: white; line-height: 60px;">
<h1>🏋️ 健身课程管理系统</h1>
</el-header>
<el-main>
<!-- 操作栏:新增课程按钮和搜索框 -->
<div style="margin-bottom: 20px; display: flex; justify-content: space-between;">
<el-button type="primary" @click="dialogFormVisible = true">➕ 新增课程</el-button>
<el-input v-model="searchKeyword" placeholder="搜索课程名称或教练" style="width: 300px;" clearable @clear="loadCourses">
<template #append>
<el-button :icon="Search" @click="handleSearch" />
</template>
</el-input>
</div>
<!-- 课程数据表格 -->
<el-table :data="filteredCourses" border stripe style="width: 100%">
<el-table-column prop="id" label="ID" width="80" />
<el-table-column prop="name" label="课程名称" />
<el-table-column prop="coachName" label="教练" />
<el-table-column prop="startTime" label="开始时间">
<template #default="scope">
{{ formatDate(scope.row.startTime) }}
</template>
</el-table-column>
<el-table-column prop="duration" label="时长(分钟)" />
<el-table-column prop="capacity" label="容量" />
<el-table-column prop="booked" label="已预约">
<template #default="scope">
<el-tag :type="scope.row.booked >= scope.row.capacity ? 'danger' : 'success'">
{{ scope.row.booked }} / {{ scope.row.capacity }}
</el-tag>
</template>
</el-table-column>
<el-table-column label="操作" width="280">
<template #default="scope">
<el-button size="small" type="success" :disabled="scope.row.booked >= scope.row.capacity" @click="handleBook(scope.row.id)">
预约
</el-button>
<el-button size="small" type="warning" :disabled="scope.row.booked <= 0" @click="handleCancel(scope.row.id)">
取消
</el-button>
<el-button size="small" @click="handleEdit(scope.row)">编辑</el-button>
<el-button size="small" type="danger" @click="handleDelete(scope.row.id)">删除</el-button>
</template>
</el-table-column>
</el-table>
</el-main>
</el-container>
<!-- 新增/编辑课程的对话框 -->
<el-dialog v-model="dialogFormVisible" :title="formTitle" width="500px">
<el-form :model="form" :rules="rules" ref="formRef" label-width="100px">
<el-form-item label="课程名称" prop="name">
<el-input v-model="form.name" autocomplete="off" />
</el-form-item>
<el-form-item label="教练姓名" prop="coachName">
<el-input v-model="form.coachName" autocomplete="off" />
</el-form-item>
<el-form-item label="开始时间" prop="startTime">
<el-date-picker
v-model="form.startTime"
type="datetime"
placeholder="选择日期时间"
value-format="YYYY-MM-DDTHH:mm:ss"
style="width: 100%;"
/>
</el-form-item>
<el-form-item label="时长(分钟)" prop="duration">
<el-input-number v-model="form.duration" :min="30" :max="180" :step="10" />
</el-form-item>
<el-form-item label="课程容量" prop="capacity">
<el-input-number v-model="form.capacity" :min="1" :max="50" />
</el-form-item>
</el-form>
<template #footer>
<span class="dialog-footer">
<el-button @click="dialogFormVisible = false">取消</el-button>
<el-button type="primary" @click="submitForm">确认</el-button>
</span>
</template>
</el-dialog>
</div>
</template>
<script setup>
import { ref, reactive, computed, onMounted } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import { Search } from '@element-plus/icons-vue'
import axios from 'axios'
// 课程列表数据
const courses = ref([])
const searchKeyword = ref('')
// 对话框控制
const dialogFormVisible = ref(false)
const formTitle = ref('新增课程')
const isEditMode = ref(false)
const currentEditId = ref(null)
// 表单数据
const form = reactive({
name: '',
coachName: '',
startTime: '',
duration: 60,
capacity: 20
})
// 表单验证规则
const rules = {
name: [{ required: true, message: '请输入课程名称', trigger: 'blur' }],
coachName: [{ required: true, message: '请输入教练姓名', trigger: 'blur' }],
startTime: [{ required: true, message: '请选择开始时间', trigger: 'change' }],
duration: [{ required: true, message: '请输入时长', trigger: 'blur' }],
capacity: [{ required: true, message: '请输入课程容量', trigger: 'blur' }]
}
const formRef = ref()
// 计算属性:根据搜索关键词过滤课程
const filteredCourses = computed(() => {
if (!searchKeyword.value) return courses.value
const keyword = searchKeyword.value.toLowerCase()
return courses.value.filter(course =>
course.name.toLowerCase().includes(keyword) ||
course.coachName.toLowerCase().includes(keyword)
)
})
// 加载课程列表
const loadCourses = async () => {
try {
const response = await axios.get('/api/courses')
courses.value = response.data
} catch (error) {
console.error('加载课程失败:', error)
ElMessage.error('加载课程列表失败')
}
}
// 搜索课程
const handleSearch = () => {
// 搜索逻辑已通过 computed 属性 filteredCourses 实现
ElMessage.info(`搜索关键词: ${searchKeyword.value}`)
}
// 预约课程
const handleBook = async (id) => {
try {
const response = await axios.post(`/api/courses/${id}/book`)
if (response.data.success) {
ElMessage.success(response.data.message)
loadCourses() // 刷新列表
} else {
ElMessage.warning(response.data.message)
}
} catch (error) {
ElMessage.error('预约请求失败')
}
}
// 取消预约
const handleCancel = async (id) => {
try {
const response = await axios.post(`/api/courses/${id}/cancel`)
if (response.data.success) {
ElMessage.success(response.data.message)
loadCourses()
} else {
ElMessage.warning(response.data.message)
}
} catch (error) {
ElMessage.error('取消预约请求失败')
}
}
// 编辑课程 - 打开对话框并填充数据
const handleEdit = (row) => {
isEditMode.value = true
formTitle.value = '编辑课程'
currentEditId.value = row.id
Object.assign(form, {
name: row.name,
coachName: row.coachName,
startTime: row.startTime,
duration: row.duration,
capacity: row.capacity
})
dialogFormVisible.value = true
}
// 删除课程
const handleDelete = (id) => {
ElMessageBox.confirm('确定要删除这条课程记录吗?', '警告', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(async () => {
try {
await axios.delete(`/api/courses/${id}`)
ElMessage.success('删除成功')
loadCourses()
} catch (error) {
ElMessage.error('删除失败')
}
}).catch(() => {
// 用户点击了取消
})
}
// 提交表单(新增或编辑)
const submitForm = async () => {
// 表单验证
await formRef.value.validate(async (valid) => {
if (!valid) {
ElMessage.warning('请检查表单填写是否正确')
return
}
try {
if (isEditMode.value) {
// 编辑模式
await axios.put(`/api/courses/${currentEditId.value}`, form)
ElMessage.success('课程更新成功')
} else {
// 新增模式
await axios.post('/api/courses', form)
ElMessage.success('课程新增成功')
}
dialogFormVisible.value = false
resetForm()
loadCourses() // 刷新列表
} catch (error) {
console.error('保存课程失败:', error)
ElMessage.error('保存失败,请检查网络或数据')
}
})
}
// 重置表单
const resetForm = () => {
formRef.value?.resetFields()
isEditMode.value = false
currentEditId.value = null
formTitle.value = '新增课程'
Object.assign(form, {
name: '',
coachName: '',
startTime: '',
duration: 60,
capacity: 20
})
}
// 格式化日期时间显示
const formatDate = (dateTimeStr) => {
if (!dateTimeStr) return ''
const date = new Date(dateTimeStr)
return date.toLocaleString('zh-CN')
}
// 组件挂载时加载数据
onMounted(() => {
loadCourses()
})
</script>
<style scoped>
.app-container {
padding: 20px;
}
</style>
4.4 启动前端项目
在 fitness-frontend 目录下运行:
npm run dev
Vite 会启动开发服务器,通常在 http://localhost:5173 。打开浏览器访问该地址,你应该能看到一个功能完整的健身课程管理界面。
现在,前后端分离的健身管理系统就搭建完成了! 你可以在前端页面进行课程的增删改查、预约和取消操作,所有数据都会通过API保存到MySQL数据库中。
5. 项目运行与联调
5.1 完整启动流程
- 启动后端 :在IDEA中运行Spring Boot应用,确保控制台无报错,端口
8080。 - 启动前端 :在
fitness-frontend目录下执行npm run dev,访问http://localhost:5173。 - 操作验证 :
- 点击“新增课程”按钮,填写信息并提交。
- 在表格中点击“预约”/“取消”按钮,观察“已预约”人数的变化。
- 点击“编辑”修改课程信息。
- 点击“删除”移除课程(会有确认提示)。
5.2 关键联调检查点
- 跨域问题 :如果前端请求后端API失败,并出现CORS错误,请检查后端
CourseController上的@CrossOrigin注解端口是否与前端的axios.defaults.baseURL端口一致。 - 数据库连接 :确保MySQL服务已启动,且
application.properties中的数据库名、用户名、密码正确。 - 日期格式 :前端提交的日期时间格式(
YYYY-MM-DDTHH:mm:ss)需要与后端LocalDateTime类型匹配。
6. 常见问题与解决方案
在搭建和运行过程中,你可能会遇到以下问题:
| 问题现象 | 可能原因 | 解决方案 |
|---|---|---|
后端启动失败,提示 java.lang.IllegalArgumentException |
JDK版本不兼容。Spring Boot 3.x 需要 JDK 17+ 。 | 检查并升级JDK版本。在IDEA的 File -> Project Structure -> Project 中设置正确的SDK。 |
前端 npm install 失败或很慢 |
网络问题或Node.js版本过低。 | 1. 使用淘宝镜像: npm config set registry https://registry.npmmirror.com 2. 升级Node.js到18+版本。 |
前端页面能打开,但列表为空,控制台报错 Failed to fetch |
1. 后端服务未启动。 2. 跨域(CORS)配置错误。 3. 前端axios配置的baseURL不对。 |
1. 检查后端是否运行在 localhost:8080 。 2. 检查后端Controller的 @CrossOrigin 注解。 3. 检查 src/main.js 中 axios.defaults.baseURL 的配置。 |
| 新增课程成功,但页面不刷新 | 前端 loadCourses() 函数未在操作成功后调用。 |
确保在 submitForm 、 handleBook 、 handleCancel 、 handleDelete 等函数操作API成功后,都调用 loadCourses() 重新获取数据。 |
日期时间显示为 [object Object] 或格式不对 |
前端未对后端返回的日期字符串进行格式化。 | 使用 formatDate 函数(示例中已提供)或引入 dayjs / moment 库进行格式化。 |
| 点击按钮无反应,控制台无错误 | 按钮的 @click 事件绑定的函数名错误或函数未定义。 |
检查Vue组件中 methods 或 <script setup> 中是否正确定义了函数,以及模板中绑定是否正确。 |
7. 项目扩展与最佳实践建议
这个基础版本已经实现了核心功能,但一个完整的课设或毕业设计还可以从以下方面进行扩展和优化:
7.1 功能扩展方向
- 会员管理模块 :参照课程管理,实现会员的增删改查页面,并与课程预约关联(记录哪个会员预约了哪节课)。
- 用户登录与权限 :引入Spring Security或JWT,区分管理员(管理课程、会员)和普通会员(仅预约课程)。
- 课程分类与筛选 :为课程增加类型字段(如:有氧、力量、柔韧),并实现前端按类型、时间筛选。
- 数据可视化 :使用ECharts等库,展示课程预约热度、会员增长趋势等图表。
- 文件上传 :允许为课程或教练上传图片。
7.2 后端工程化建议
- 统一响应封装 :创建一个
Result类,统一所有API的返回格式(code,message,data),便于前端处理。 - 全局异常处理 :使用
@ControllerAdvice和@ExceptionHandler捕获并处理各类异常,返回友好的错误信息。 - 数据验证 :在Controller的入参上使用
@Valid注解,并结合实体类字段的@NotBlank、@Size等注解进行数据校验。 - 日志记录 :使用SLF4J记录重要的业务操作和异常信息,便于排查问题。
- 配置文件分离 :将
application.properties拆分为application-dev.properties(开发) 和application-prod.properties(生产),通过spring.profiles.active指定环境。
7.3 前端工程化建议
- 组件拆分 :将课程表格、课程表单对话框等拆分为独立的
.vue组件,提高代码复用性和可维护性。 - 状态管理 :当组件间通信复杂时,引入Pinia进行集中式状态管理。
- 路由管理 :使用Vue Router实现多页面跳转(如:/courses, /members, /login)。
- API模块化 :将所有的axios请求封装到单独的
api目录下的文件中(如course.js,member.js),避免在组件中直接写URL。 - 环境变量 :使用
.env.development和.env.production文件管理不同环境的后端API地址。
7.4 部署上线(简易版)
- 后端打包 :在项目根目录执行
mvn clean package,会在target目录生成fitness-0.0.1-SNAPSHOT.jar。 - 前端构建 :在
fitness-frontend目录执行npm run build,生成静态文件在dist文件夹。 - 运行后端 :将jar包和前端
dist文件夹放到服务器。运行java -jar fitness-0.0.1-SNAPSHOT.jar。 - 配置后端服务静态资源 :在Spring Boot中可以通过配置,将
dist目录的内容作为静态资源服务,这样访问http://服务器IP:8080就能看到前端页面。或者使用Nginx代理前后端。
按照本文的步骤,你不仅能在1小时内搭建出一个可运行的系统,更能理解前后端分离开发的核心流程。这套技术栈和架构模式是当前企业开发的标配,掌握它对你未来的学习和求职都大有裨益。项目源码和相关的配置课件,可以方便地进行二次开发,添加更多业务模块。如果在实现过程中遇到其他问题,多查阅官方文档和社区,大部分坑都有现成的解决方案。动手做一遍,远比看十遍教程更有收获。
更多推荐




所有评论(0)