Spring Boot 3.x与Vue 3实战:从零构建高校课程管理系统
大家好,我是专注于分享实战开发经验的博主。在高校信息化建设中,课程管理是教务工作的核心,传统的手工或单机系统已难以应对复杂的排课、选课和成绩管理需求。本文将手把手带你从零搭建一个基于 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 核心实体关系分析
系统主要包含以下几个实体:
- User(用户) :存储所有用户的登录信息和基本资料。通过
role字段区分身份。 - Course(课程) :存储课程的基本信息。
- Schedule(排课) :关联课程、教师和上课时间地点。一门课程在一个学期内可以有多个排课记录(如每周一、三上课)。
- 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 启动后端服务
- 确保 MySQL 服务已启动,并创建好数据库(如
course_management)。 - 在
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
- 在 IDEA 中运行
CourseManagementApplication主类,或使用命令行mvn spring-boot:run。 - 看到
Started CourseManagementApplication日志且无报错,说明后端启动成功,默认端口 8080。
6.2 启动前端服务
- 在
course-management-frontend目录下,执行npm run dev。 - 控制台会输出本地访问地址,通常是
http://localhost:5173。 - 浏览器打开该地址,即可看到前端页面。
6.3 测试完整流程
- 访问
http://localhost:5173,由于未登录,会被重定向到登录页。 - 使用
data.sql中插入的初始管理员账号登录(例如,用户名admin,密码123456)。 - 登录成功后,进入课程管理页面,尝试进行新增、编辑、删除课程的操作。
- 打开浏览器开发者工具的“网络(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 开发、前端页面交互到工程化实践的完整链路。你可以在此基础上,继续实现排课、选课、成绩管理等更复杂的业务模块,并引入更完善的安全框架和部署方案,将其打磨成一个真正可用的系统。
更多推荐


所有评论(0)