大家好,我是专注于分享实战开发经验的博主。在高校信息化建设中,课程管理是教务工作的核心,传统的手工或单机系统已难以应对复杂的排课、选课和成绩管理需求。本文将手把手带你从零搭建一个基于 Spring Boot 3.x 和 Vue 3 的前后端分离课程管理系统,涵盖从环境搭建、数据库设计、核心接口开发到前端页面联调的完整闭环。无论你是想学习前后端分离架构的在校学生,还是需要快速构建此类业务系统的开发者,都能从本文获得可直接复用的代码和清晰的工程化思路。

1. 背景与核心概念

1.1 什么是前后端分离架构?

在传统的单体应用(如 JSP、Thymeleaf)中,前端页面和后端逻辑耦合在同一个项目中,由后端服务器渲染页面并返回。而前后端分离架构将应用清晰地拆分为两个独立的部分:

  • 后端(Backend) :专注于业务逻辑、数据持久化和 API 接口的提供。通常使用 Spring Boot 等框架构建 RESTful API,以 JSON 格式进行数据交互。
  • 前端(Frontend) :专注于用户界面和交互逻辑。使用 Vue、React 等框架构建单页面应用(SPA),通过 HTTP 请求调用后端 API 获取数据并动态渲染页面。

这种架构的优势在于职责清晰、并行开发、易于维护和扩展,并且前端可以独立部署,是目前企业级应用开发的主流选择。

1.2 学校课程管理系统的核心业务

本系统旨在模拟一个简化的高校课程管理流程,主要包含以下核心模块:

  • 用户管理 :区分管理员、教师、学生三种角色,实现登录认证与权限控制。
  • 课程管理 :管理员或教师对课程信息(课程号、名称、学分、学时等)进行增删改查。
  • 排课管理 :为课程分配上课时间、地点和授课教师。
  • 选课管理 :学生根据培养方案和个人兴趣选择课程,系统需处理选课冲突、容量限制等逻辑。
  • 成绩管理 :授课教师录入和修改学生成绩,学生查询个人成绩。

我们将使用 Spring Boot 构建稳定、高效的后端服务,使用 Vue 3 构建现代化、响应式的前端界面,两者通过清晰的 API 契约进行通信。

2. 环境准备与版本说明

一个可复现的环境是项目成功的第一步。请确保你的开发环境满足以下要求。

2.1 后端开发环境

  • 操作系统 :Windows 10/11, macOS 或 Linux(本文演示以 Windows 为例)。
  • JDK :版本 17 或 21(Spring Boot 3.x 要求至少 JDK 17)。推荐使用 Amazon Corretto 17。
  • IDE :IntelliJ IDEA Ultimate 或 Community 版,或 VS Code 配合相应插件。
  • 构建工具 :Apache Maven 3.6+ 或 Gradle。
  • 数据库 :MySQL 8.0+。我们将使用 MySQL 作为持久化存储。
  • 其他工具 :Postman 或 Apifox,用于测试 API 接口。

2.2 前端开发环境

  • Node.js :版本 18.x 或 20.x LTS。这是运行 Vue 和包管理工具 npm 的基础。
  • 包管理器 :npm 或 yarn(本文使用 npm)。
  • IDE :VS Code,并安装 Volar 扩展(Vue 3 官方推荐)。

2.3 项目依赖版本说明

版本依赖是项目稳定的关键。以下是本文示例采用的核心依赖版本,你可以在实际项目中根据需要进行调整。

后端(Spring Boot)关键依赖

<!-- pom.xml 中的部分依赖 -->
<parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <version>3.2.5</version> <!-- 使用稳定的 3.2.x 系列 -->
</parent>

<dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-data-jpa</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.security</groupId>
        <artifactId>spring-security-crypto</artifactId>
    </dependency>
</dependencies>

前端(Vue 3)项目创建 : 我们将使用 Vue 官方脚手架 create-vue 来创建项目,它会自动配置好 Vite、TypeScript 等现代工具链。

# 在命令行中执行
npm create vue@latest

在创建过程中,可以选择添加 TypeScript Router Pinia (状态管理)等。本文示例将包含这些选项。

3. 数据库设计与核心实体建模

在编写代码之前,合理的数据库设计是系统的基石。我们采用 JPA 的实体类来映射数据库表。

3.1 核心实体关系分析

系统主要包含以下几个实体:

  1. User(用户) :存储所有用户的登录信息和基本资料。通过 role 字段区分身份。
  2. Course(课程) :存储课程的基本信息。
  3. Schedule(排课) :关联课程、教师和上课时间地点。一门课程在一个学期内可以有多个排课记录(如每周一、三上课)。
  4. StudentCourse(学生选课) :关联学生和具体的排课记录,并记录最终成绩。这是学生选课和成绩管理的核心表。

3.2 实体类代码实现

以下是核心实体类的 Java 代码,使用了 Lombok 简化 Getter/Setter。

User 实体

// 文件路径:src/main/java/com/example/coursemanagement/entity/User.java
package com.example.coursemanagement.entity;

import jakarta.persistence.*;
import lombok.Data;
import java.time.LocalDateTime;

@Entity
@Table(name = "sys_user")
@Data
public class User {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @Column(unique = true, nullable = false)
    private String username; // 学号/工号

    private String password;
    private String name;
    private String email;

    @Enumerated(EnumType.STRING)
    private UserRole role; // 枚举:ADMIN, TEACHER, STUDENT

    @Column(updatable = false)
    private LocalDateTime createTime;

    @PrePersist
    protected void onCreate() {
        createTime = LocalDateTime.now();
    }
}

Course 实体

// 文件路径:src/main/java/com/example/coursemanagement/entity/Course.java
package com.example.coursemanagement.entity;

import jakarta.persistence.*;
import lombok.Data;

@Entity
@Table(name = "course")
@Data
public class Course {
    @Id
    private String courseCode; // 课程代码,如 CS101

    @Column(nullable = false)
    private String courseName;

    private Integer credit; // 学分
    private Integer totalHours; // 总学时
    private String description;
}

Schedule 与 StudentCourse 实体 : 由于篇幅限制,这里给出关键字段。 Schedule 实体包含 courseCode (外键)、 teacherId (外键关联 User)、 semester weekDay startSection (开始节次)等字段。 StudentCourse 实体包含 studentId scheduleId score 字段。

3.3 数据库初始化脚本

建议在 src/main/resources 下创建 schema.sql data.sql ,用于在应用启动时自动建表和插入初始数据(如管理员账号)。

-- schema.sql
CREATE TABLE IF NOT EXISTS `sys_user` (
  `id` bigint NOT NULL AUTO_INCREMENT,
  `username` varchar(50) UNIQUE NOT NULL,
  `password` varchar(255) NOT NULL,
  `name` varchar(50) NOT NULL,
  `role` varchar(20) NOT NULL,
  `create_time` datetime DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

-- 其他表创建语句...

application.properties 中配置:

spring.sql.init.mode=always
spring.sql.init.schema-locations=classpath:schema.sql
spring.sql.init.data-locations=classpath:data.sql

4. 后端核心功能开发

我们将采用经典的三层架构:Controller(控制层)、Service(业务层)、Repository(数据访问层)。

4.1 用户认证与登录接口

首先实现一个简单的登录接口,暂不集成 Spring Security,以理解基本原理。

1. 创建数据传输对象(DTO)

// 文件路径:src/main/java/com/example/coursemanagement/dto/LoginDTO.java
package com.example.coursemanagement.dto;
import lombok.Data;

@Data
public class LoginDTO {
    private String username;
    private String password;
}

2. 创建统一响应结果封装

// 文件路径:src/main/java/com/example/coursemanagement/common/Result.java
package com.example.coursemanagement.common;
import lombok.Data;

@Data
public class Result<T> {
    private Integer code;
    private String message;
    private T data;

    public static <T> Result<T> success(T data) {
        Result<T> result = new Result<>();
        result.setCode(200);
        result.setMessage("success");
        result.setData(data);
        return result;
    }
    // 可以补充 error 方法
}

3. 实现 UserService 和 LoginController

// 文件路径:src/main/java/com/example/coursemanagement/service/UserService.java
package com.example.coursemanagement.service;
// ... import
@Service
public class UserService {
    @Autowired
    private UserRepository userRepository;
    @Autowired
    private PasswordEncoder passwordEncoder; // Spring Security 的加密组件

    public User login(String username, String rawPassword) {
        User user = userRepository.findByUsername(username);
        if (user != null && passwordEncoder.matches(rawPassword, user.getPassword())) {
            // 登录成功,注意不要返回密码
            user.setPassword(null);
            return user;
        }
        return null;
    }
}
// 文件路径:src/main/java/com/example/coursemanagement/controller/LoginController.java
package com.example.coursemanagement.controller;
// ... import
@RestController
@RequestMapping("/api/auth")
public class LoginController {
    @Autowired
    private UserService userService;

    @PostMapping("/login")
    public Result<User> login(@RequestBody LoginDTO loginDTO) {
        User user = userService.login(loginDTO.getUsername(), loginDTO.getPassword());
        if (user == null) {
            // 返回错误结果,这里简化处理
            Result<User> result = new Result<>();
            result.setCode(401);
            result.setMessage("用户名或密码错误");
            return result;
        }
        return Result.success(user);
    }
}

4. 密码加密配置 : 在配置类或主应用类中配置一个 PasswordEncoder Bean。

// 文件路径:src/main/java/com/example/coursemanagement/config/SecurityConfig.java
package com.example.coursemanagement.config;
// ... import
@Configuration
public class SecurityConfig {
    @Bean
    public PasswordEncoder passwordEncoder() {
        // 使用 BCrypt 强哈希算法
        return new BCryptPasswordEncoder();
    }
}

data.sql 中插入用户时,密码需要是加密后的字符串。可以使用以下代码生成:

String encodedPwd = new BCryptPasswordEncoder().encode("123456");
System.out.println(encodedPwd); // 将输出结果放入sql

4.2 课程管理CRUD接口

这是典型的 RESTful API 实现。

1. CourseController

// 文件路径:src/main/java/com/example/coursemanagement/controller/CourseController.java
package com.example.coursemanagement.controller;
// ... import
@RestController
@RequestMapping("/api/courses")
public class CourseController {
    @Autowired
    private CourseService courseService;

    @GetMapping
    public Result<List<Course>> getAllCourses() {
        return Result.success(courseService.findAll());
    }

    @GetMapping("/{code}")
    public Result<Course> getCourseByCode(@PathVariable String code) {
        return Result.success(courseService.findByCode(code));
    }

    @PostMapping
    public Result<Course> createCourse(@RequestBody Course course) {
        // 实际项目需要做数据校验,如课程代码是否已存在
        return Result.success(courseService.save(course));
    }

    @PutMapping("/{code}")
    public Result<Course> updateCourse(@PathVariable String code, @RequestBody Course course) {
        course.setCourseCode(code); // 确保路径参数和对象ID一致
        return Result.success(courseService.save(course));
    }

    @DeleteMapping("/{code}")
    public Result<Void> deleteCourse(@PathVariable String code) {
        courseService.deleteByCode(code);
        return Result.success(null);
    }
}

2. CourseService 与 CourseRepository : Service 层处理业务逻辑,Repository 层由 Spring Data JPA 自动实现。

// 文件路径:src/main/java/com/example/coursemanagement/service/CourseService.java
package com.example.coursemanagement.service;
// ... import
@Service
public class CourseService {
    @Autowired
    private CourseRepository courseRepository;

    public List<Course> findAll() {
        return courseRepository.findAll();
    }
    public Course findByCode(String code) {
        return courseRepository.findById(code).orElse(null);
    }
    public Course save(Course course) {
        return courseRepository.save(course);
    }
    public void deleteByCode(String code) {
        courseRepository.deleteById(code);
    }
}
// 文件路径:src/main/java/com/example/coursemanagement/repository/CourseRepository.java
package com.example.coursemanagement.repository;
// ... import
@Repository
public interface CourseRepository extends JpaRepository<Course, String> {
    // Spring Data JPA 会根据方法名自动生成查询,如 findById 已继承
}

4.3 跨域配置

由于前端运行在独立的端口(如 http://localhost:5173 ),需要后端允许跨域请求。

// 文件路径:src/main/java/com/example/coursemanagement/config/CorsConfig.java
package com.example.coursemanagement.config;
// ... import
@Configuration
public class CorsConfig {
    @Bean
    public WebMvcConfigurer corsConfigurer() {
        return new WebMvcConfigurer() {
            @Override
            public void addCorsMappings(CorsRegistry registry) {
                registry.addMapping("/api/**") // 针对所有/api开头的接口
                        .allowedOrigins("http://localhost:5173") // 允许前端地址
                        .allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS")
                        .allowedHeaders("*")
                        .allowCredentials(true);
            }
        };
    }
}

5. 前端Vue 3项目开发

我们使用 Vue 3 的 Composition API 配合 <script setup> 语法,并采用 Pinia 进行状态管理,Vue Router 进行路由导航。

5.1 项目初始化与依赖安装

使用 create-vue 创建项目后,安装必要的依赖。

# 进入项目目录
cd course-management-frontend
# 安装 Axios 用于HTTP请求,Element Plus作为UI组件库
npm install axios element-plus @element-plus/icons-vue
# 安装 Pinia 和 Vue Router (如果创建时未选择)
npm install pinia vue-router@4

5.2 配置Axios与API调用

创建统一的请求工具,便于管理基地址、拦截器等。

1. 创建 src/utils/request.js

import axios from 'axios';
import { ElMessage } from 'element-plus';
import router from '@/router';

// 创建axios实例
const service = axios.create({
  baseURL: 'http://localhost:8080/api', // 后端API基础地址
  timeout: 10000 // 请求超时时间
});

// 请求拦截器
service.interceptors.request.use(
  config => {
    // 在发送请求之前做些什么,例如添加token
    const token = localStorage.getItem('token');
    if (token) {
      config.headers['Authorization'] = `Bearer ${token}`;
    }
    return config;
  },
  error => {
    // 对请求错误做些什么
    console.error('Request Error:', error);
    return Promise.reject(error);
  }
);

// 响应拦截器
service.interceptors.response.use(
  response => {
    // 对响应数据做点什么
    const res = response.data;
    // 假设后端统一返回 { code, message, data } 结构
    if (res.code !== 200) {
      ElMessage.error(res.message || 'Error');
      // 如果是401未授权,跳转到登录页
      if (res.code === 401) {
        router.push('/login');
      }
      return Promise.reject(new Error(res.message || 'Error'));
    } else {
      return res.data; // 直接返回业务数据
    }
  },
  error => {
    // 对响应错误做点什么
    console.error('Response Error:', error);
    ElMessage.error(error.message || 'Network Error');
    return Promise.reject(error);
  }
);

export default service;

2. 创建课程相关的API模块 src/api/course.js

import request from '@/utils/request';

export function getCourseList() {
  return request({
    url: '/courses',
    method: 'get'
  });
}

export function createCourse(data) {
  return request({
    url: '/courses',
    method: 'post',
    data
  });
}

export function updateCourse(code, data) {
  return request({
    url: `/courses/${code}`,
    method: 'put',
    data
  });
}

export function deleteCourse(code) {
  return request({
    url: `/courses/${code}`,
    method: 'delete'
  });
}

5.3 实现课程列表与表单页面

使用 Element Plus 的表格和表单组件快速构建页面。

1. 课程列表页面 src/views/course/CourseList.vue

<template>
  <div class="course-container">
    <div class="header">
      <h2>课程管理</h2>
      <el-button type="primary" @click="handleCreate">新增课程</el-button>
    </div>
    <el-table :data="courseList" border style="width: 100%">
      <el-table-column prop="courseCode" label="课程代码" width="120" />
      <el-table-column prop="courseName" label="课程名称" />
      <el-table-column prop="credit" label="学分" width="80" />
      <el-table-column prop="totalHours" label="学时" width="80" />
      <el-table-column label="操作" width="180">
        <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>

    <!-- 新增/编辑课程对话框 -->
    <el-dialog v-model="dialogVisible" :title="dialogTitle" width="500px">
      <el-form :model="form" :rules="rules" ref="formRef" label-width="80px">
        <el-form-item label="课程代码" prop="courseCode">
          <el-input v-model="form.courseCode" :disabled="isEdit" />
        </el-form-item>
        <el-form-item label="课程名称" prop="courseName">
          <el-input v-model="form.courseName" />
        </el-form-item>
        <el-form-item label="学分" prop="credit">
          <el-input-number v-model="form.credit" :min="1" :max="10" />
        </el-form-item>
        <el-form-item label="学时" prop="totalHours">
          <el-input-number v-model="form.totalHours" :min="16" :max="128" />
        </el-form-item>
        <el-form-item label="描述">
          <el-input v-model="form.description" type="textarea" :rows="3" />
        </el-form-item>
      </el-form>
      <template #footer>
        <span class="dialog-footer">
          <el-button @click="dialogVisible = false">取消</el-button>
          <el-button type="primary" @click="submitForm">确认</el-button>
        </span>
      </template>
    </el-dialog>
  </div>
</template>

<script setup>
import { ref, reactive, onMounted } from 'vue';
import { ElMessage, ElMessageBox } from 'element-plus';
import { getCourseList, createCourse, updateCourse, deleteCourse } from '@/api/course';

const courseList = ref([]);
const dialogVisible = ref(false);
const dialogTitle = ref('');
const isEdit = ref(false);
const formRef = ref();

const form = reactive({
  courseCode: '',
  courseName: '',
  credit: 2,
  totalHours: 32,
  description: ''
});

const rules = {
  courseCode: [{ required: true, message: '请输入课程代码', trigger: 'blur' }],
  courseName: [{ required: true, message: '请输入课程名称', trigger: 'blur' }]
};

// 加载课程列表
const loadCourses = async () => {
  try {
    const data = await getCourseList();
    courseList.value = data;
  } catch (error) {
    console.error('加载课程列表失败:', error);
  }
};

// 打开新增对话框
const handleCreate = () => {
  dialogTitle.value = '新增课程';
  isEdit.value = false;
  Object.keys(form).forEach(key => form[key] = ''); // 清空表单
  form.credit = 2;
  form.totalHours = 32;
  dialogVisible.value = true;
};

// 打开编辑对话框
const handleEdit = (row) => {
  dialogTitle.value = '编辑课程';
  isEdit.value = true;
  Object.assign(form, row); // 将行数据赋值给表单
  dialogVisible.value = true;
};

// 提交表单
const submitForm = async () => {
  await formRef.value.validate();
  try {
    if (isEdit.value) {
      await updateCourse(form.courseCode, form);
      ElMessage.success('更新成功');
    } else {
      await createCourse(form);
      ElMessage.success('创建成功');
    }
    dialogVisible.value = false;
    loadCourses(); // 刷新列表
  } catch (error) {
    ElMessage.error('操作失败');
  }
};

// 删除课程
const handleDelete = (row) => {
  ElMessageBox.confirm(`确认删除课程 "${row.courseName}" 吗?`, '提示', {
    confirmButtonText: '确定',
    cancelButtonText: '取消',
    type: 'warning'
  }).then(async () => {
    try {
      await deleteCourse(row.courseCode);
      ElMessage.success('删除成功');
      loadCourses();
    } catch (error) {
      ElMessage.error('删除失败');
    }
  }).catch(() => {});
};

onMounted(() => {
  loadCourses();
});
</script>

<style scoped>
.course-container {
  padding: 20px;
}
.header {
  display: flex;
  justify-content: space-between;
  align-items: center;
  margin-bottom: 20px;
}
</style>

5.4 配置路由与状态管理

配置 Vue Router 将页面关联起来,并使用 Pinia 管理用户登录状态。

1. 路由配置 src/router/index.js

import { createRouter, createWebHistory } from 'vue-router';
import Login from '@/views/Login.vue';
import CourseList from '@/views/course/CourseList.vue';
// 导入其他组件...

const routes = [
  {
    path: '/',
    redirect: '/courses'
  },
  {
    path: '/login',
    name: 'Login',
    component: Login
  },
  {
    path: '/courses',
    name: 'CourseList',
    component: CourseList,
    meta: { requiresAuth: true } // 需要登录才能访问
  },
  // ... 其他路由
];

const router = createRouter({
  history: createWebHistory(),
  routes
});

// 路由守卫,检查登录状态
router.beforeEach((to, from, next) => {
  const isAuthenticated = localStorage.getItem('token'); // 简单判断,实际应更复杂
  if (to.meta.requiresAuth && !isAuthenticated) {
    next('/login');
  } else {
    next();
  }
});

export default router;

2. Pinia Store 管理用户状态 src/stores/user.js

import { defineStore } from 'pinia';
import { login as apiLogin } from '@/api/auth';

export const useUserStore = defineStore('user', {
  state: () => ({
    token: localStorage.getItem('token') || '',
    userInfo: JSON.parse(localStorage.getItem('userInfo') || '{}')
  }),
  actions: {
    async login(loginForm) {
      try {
        const user = await apiLogin(loginForm);
        // 假设登录成功返回用户信息和token(这里简化,实际token可能在响应头)
        this.token = `mock-token-${user.id}`;
        this.userInfo = user;
        // 存储到本地
        localStorage.setItem('token', this.token);
        localStorage.setItem('userInfo', JSON.stringify(user));
        return Promise.resolve(user);
      } catch (error) {
        return Promise.reject(error);
      }
    },
    logout() {
      this.token = '';
      this.userInfo = {};
      localStorage.removeItem('token');
      localStorage.removeItem('userInfo');
    }
  }
});

6. 前后端联调与运行

6.1 启动后端服务

  1. 确保 MySQL 服务已启动,并创建好数据库(如 course_management )。
  2. application.properties 中配置数据库连接。
spring.datasource.url=jdbc:mysql://localhost:3306/course_management?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
spring.jpa.hibernate.ddl-auto=update # 与sql.init配合使用,首次启动后可改为validate
spring.jpa.show-sql=true
  1. 在 IDEA 中运行 CourseManagementApplication 主类,或使用命令行 mvn spring-boot:run
  2. 看到 Started CourseManagementApplication 日志且无报错,说明后端启动成功,默认端口 8080。

6.2 启动前端服务

  1. course-management-frontend 目录下,执行 npm run dev
  2. 控制台会输出本地访问地址,通常是 http://localhost:5173
  3. 浏览器打开该地址,即可看到前端页面。

6.3 测试完整流程

  1. 访问 http://localhost:5173 ,由于未登录,会被重定向到登录页。
  2. 使用 data.sql 中插入的初始管理员账号登录(例如,用户名 admin ,密码 123456 )。
  3. 登录成功后,进入课程管理页面,尝试进行新增、编辑、删除课程的操作。
  4. 打开浏览器开发者工具的“网络(Network)”选项卡,观察前端发起的 API 请求和后端的响应数据,确保通信正常。

7. 常见问题与排查思路

在开发过程中,你可能会遇到以下典型问题。

问题现象 可能原因 排查步骤与解决方案
前端访问后端 API 出现 CORS 错误 后端未正确配置跨域,或配置的源(Origin)不匹配 1. 检查后端 CorsConfig allowedOrigins 是否包含前端地址(如 http://localhost:5173 )。
2. 检查前端请求的 URL 是否正确,是否以 /api 开头。
后端启动时报数据库连接失败 数据库服务未启动、连接信息错误、驱动问题 1. 确认 MySQL 服务正在运行。
2. 检查 application.properties 中的 url username password
3. 确认 MySQL 版本与 mysql-connector-j 依赖版本兼容。
前端页面空白,控制台报 JS 错误 依赖未安装、组件导入错误、路由配置问题 1. 运行 npm install 确保所有依赖已安装。
2. 检查浏览器控制台错误信息,定位到具体文件和行号。
3. 检查 Vue 组件中的 import 路径是否正确。
登录成功但后续请求 401 未授权 Token 未正确存储或未随请求发送 1. 检查前端 request.js 的请求拦截器,是否成功从 localStorage 读取 token 并设置到请求头。
2. 检查后端接口是否需要 @CrossOrigin 注解或全局配置。
插入或更新数据时,字段为 null 前端提交的数据字段名与后端实体类字段名不匹配 1. 使用浏览器开发者工具查看网络请求的 Payload ,确认字段名和格式。
2. 检查后端实体类的 @Column 注解名称是否与数据库字段对应。
页面刷新后登录状态丢失 前端仅用内存状态管理,未持久化 1. 确保登录成功后,将 token 和用户信息存入 localStorage sessionStorage
2. 在应用初始化时(如 main.js 或根组件),从存储中读取状态并恢复到 Pinia Store。

8. 最佳实践与工程建议

将项目运行起来只是第一步,要投入生产环境,还需要考虑更多工程化因素。

8.1 后端工程化建议

  • API 版本管理 :在 URL 路径中加入版本号,如 /api/v1/courses ,为后续不兼容的升级留有余地。
  • 统一异常处理 :使用 @ControllerAdvice @ExceptionHandler 全局捕获异常,并返回结构统一的错误信息,避免将堆栈信息暴露给前端。
  • 参数校验 :在 DTO 上使用 @Valid 注解配合 javax.validation 约束(如 @NotBlank @Size )进行入参校验。
  • 日志规范 :使用 SLF4J 接口配合 Logback,对不同级别(INFO, WARN, ERROR)的日志进行合理输出,便于问题追踪。
  • 配置分离 :使用 application-dev.properties application-prod.properties spring.profiles.active 来管理不同环境的配置(数据库、Redis、文件路径等)。

8.2 前端工程化建议

  • 环境变量 :使用 .env.development .env.production 文件管理不同环境的后端 API 基地址,避免硬编码。
  • 组件拆分 :将大型页面拆分为可复用的子组件(如 CourseTable.vue CourseForm.vue ),提高代码可维护性。
  • 路由懒加载 :使用 () => import(‘…’) 语法实现路由组件的懒加载,优化首屏加载速度。
  • 状态管理 :对于跨多个组件的复杂状态(如用户信息、全局配置),使用 Pinia 集中管理,避免 prop 层层传递。
  • API 模块化 :将不同业务模块的 API 请求函数分别放在 src/api/ 目录下对应的文件中,便于维护。

8.3 安全注意事项

  • 密码存储 :务必使用 BCryptPasswordEncoder 等强哈希算法加密密码, 绝对禁止 明文存储。
  • SQL 注入 :使用 Spring Data JPA 或 MyBatis 等持久层框架的参数化查询,可有效防止 SQL 注入。
  • 接口权限 :本示例仅做了简单的路由守卫,实际项目中,后端每个接口都应根据用户角色进行权限校验(可使用 Spring Security 或 Sa-Token)。
  • XSS 防护 :前端在渲染从后端获取的数据时,对于富文本内容,要进行转义或使用安全的渲染方式。
  • 敏感信息 :不要在日志、前端代码或响应体中打印或返回敏感信息,如密码、数据库连接字符串、密钥等。

8.4 部署考量

  • 后端打包 :使用 mvn clean package 生成可执行的 JAR 文件,通过 java -jar 命令运行。考虑使用 Docker 容器化部署,保证环境一致性。
  • 前端构建 :运行 npm run build 生成静态资源(位于 dist 目录)。可以将这些文件放到 Nginx 或 Apache 等 Web 服务器下,或上传到 CDN。
  • 分离部署 :前后端独立部署是常见模式。前端构建后,通过 Nginx 配置反向代理,将 /api 路径的请求转发到后端服务器。
  • 数据库 :生产环境务必使用独立的数据库服务器,并做好定期备份。

通过以上步骤,你已经完成了一个具备基础功能的 Spring Boot + Vue 3 前后端分离课程管理系统。这个项目麻雀虽小,但五脏俱全,涵盖了从数据库设计、后端 API 开发、前端页面交互到工程化实践的完整链路。你可以在此基础上,继续实现排课、选课、成绩管理等更复杂的业务模块,并引入更完善的安全框架和部署方案,将其打磨成一个真正可用的系统。

Logo

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

更多推荐