最近在帮学弟学妹们准备期末课设和毕业设计,发现很多同学对“前后端分离”项目望而却步,觉得从零搭建一个完整的系统太复杂。其实,只要掌握核心流程,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)

  1. JDK :版本 17 或以上(Spring Boot 3.x 要求至少 JDK 17)。在终端输入 java -version 检查。
  2. Maven :版本 3.6+ ,用于管理项目依赖和构建。在终端输入 mvn -v 检查。
  3. IDE IntelliJ IDEA Ultimate (推荐) 或 Eclipse 。社区版也可用。
  4. MySQL :版本 8.0+ 。安装并启动MySQL服务,记住你的root密码。

2.2 前端环境 (Vue 3)

  1. Node.js :版本 18+ 。这是运行Vue和包管理工具npm的基础。在终端输入 node -v npm -v 检查。
  2. 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 创建项目是最快的方式。

  1. 选择 Spring Boot 3.x (本文使用 3.2.x)。
  2. 打包方式: Jar
  3. Java版本: 17
  4. 依赖选择:
    • 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 启动并测试后端

  1. 在MySQL中创建数据库: CREATE DATABASE fitness_db;
  2. 在IDEA中运行 FitnessApplication 主类。
  3. 看到控制台输出 Tomcat started on port(s): 8080 即表示启动成功。
  4. 使用 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 完整启动流程

  1. 启动后端 :在IDEA中运行Spring Boot应用,确保控制台无报错,端口 8080
  2. 启动前端 :在 fitness-frontend 目录下执行 npm run dev ,访问 http://localhost:5173
  3. 操作验证
    • 点击“新增课程”按钮,填写信息并提交。
    • 在表格中点击“预约”/“取消”按钮,观察“已预约”人数的变化。
    • 点击“编辑”修改课程信息。
    • 点击“删除”移除课程(会有确认提示)。

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 功能扩展方向

  1. 会员管理模块 :参照课程管理,实现会员的增删改查页面,并与课程预约关联(记录哪个会员预约了哪节课)。
  2. 用户登录与权限 :引入Spring Security或JWT,区分管理员(管理课程、会员)和普通会员(仅预约课程)。
  3. 课程分类与筛选 :为课程增加类型字段(如:有氧、力量、柔韧),并实现前端按类型、时间筛选。
  4. 数据可视化 :使用ECharts等库,展示课程预约热度、会员增长趋势等图表。
  5. 文件上传 :允许为课程或教练上传图片。

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 部署上线(简易版)

  1. 后端打包 :在项目根目录执行 mvn clean package ,会在 target 目录生成 fitness-0.0.1-SNAPSHOT.jar
  2. 前端构建 :在 fitness-frontend 目录执行 npm run build ,生成静态文件在 dist 文件夹。
  3. 运行后端 :将jar包和前端 dist 文件夹放到服务器。运行 java -jar fitness-0.0.1-SNAPSHOT.jar
  4. 配置后端服务静态资源 :在Spring Boot中可以通过配置,将 dist 目录的内容作为静态资源服务,这样访问 http://服务器IP:8080 就能看到前端页面。或者使用Nginx代理前后端。

按照本文的步骤,你不仅能在1小时内搭建出一个可运行的系统,更能理解前后端分离开发的核心流程。这套技术栈和架构模式是当前企业开发的标配,掌握它对你未来的学习和求职都大有裨益。项目源码和相关的配置课件,可以方便地进行二次开发,添加更多业务模块。如果在实现过程中遇到其他问题,多查阅官方文档和社区,大部分坑都有现成的解决方案。动手做一遍,远比看十遍教程更有收获。

Logo

汇聚全球AI编程工具,助力开发者即刻编程。

更多推荐