我们将使用 **Spring Boot + MyBatis(或 MyBatis-Plus) + Vue3 前后端分离架构** 实现一个功能完整的课程评价系统,包含:
1. 用户管理
2. 课程管理
3. 课程分类管理
4. 标签管理
5. 评价管理
---
## ✅ 技术栈
| 层级 | 技术 |
|------|------|
| 后端框架 | Spring Boot 2.7+ |
| ORM 框架 | MyBatis-Plus(简化 CRUD) |
| 数据库 | MySQL 8.0 |
| 接口文档 | Swagger / Knife4j |
| 权限控制 | JWT + Spring Security(基础权限) |
| 前端框架 | Vue3 + Element Plus + Axios |
| 构建工具 | Maven + Webpack/Vite |
---
## 🗂️ 项目结构概览
```
course-review-system/
├── backend/ # Spring Boot 项目
│ ├── src/main/java/com/example/course/
│ │ ├── controller/ # REST 控制器
│ │ ├── service/ # 业务逻辑
│ │ ├── mapper/ # MyBatis Mapper
│ │ ├── entity/ # 实体类
│ │ ├── dto/ # 数据传输对象
│ │ ├── config/ # 配置类(JWT、跨域等)
│ │ └── CourseApplication.java
│ └── resources/
│ ├── application.yml
│ └── mapper/*.xml # XML 映射文件(可选)
│
├── frontend/ # Vue3 项目
│ ├── src/
│ │ ├── views/ # 页面组件
│ │ ├── api/ # API 调用封装
│ │ ├── router/index.js # 路由配置
│ │ └── App.vue, main.js
│ └── index.html
│
└── database.sql # 数据库建表语句
```
---
## 🛠️ 第一步:数据库设计
```sql
-- 数据库:course_review
CREATE DATABASE IF NOT EXISTS course_review DEFAULT CHARSET utf8mb4;
USE course_review;
-- 用户表
CREATE TABLE users (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
username VARCHAR(50) UNIQUE NOT NULL,
password VARCHAR(100) NOT NULL,
role ENUM('student', 'teacher', 'admin') NOT NULL DEFAULT 'student',
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
);
-- 课程分类表
CREATE TABLE categories (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
name VARCHAR(50) NOT NULL UNIQUE,
description TEXT
);
-- 标签表
CREATE TABLE tags (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
name VARCHAR(30) NOT NULL UNIQUE
);
-- 课程表
CREATE TABLE courses (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
title VARCHAR(100) NOT NULL,
teacher VARCHAR(50) NOT NULL,
category_id BIGINT,
description TEXT,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (category_id) REFERENCES categories(id)
);
-- 课程与标签关联表(多对多)
CREATE TABLE course_tag (
course_id BIGINT,
tag_id BIGINT,
PRIMARY KEY (course_id, tag_id),
FOREIGN KEY (course_id) REFERENCES courses(id) ON DELETE CASCADE,
FOREIGN KEY (tag_id) REFERENCES tags(id) ON DELETE CASCADE
);
-- 评价表
CREATE TABLE reviews (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
course_id BIGINT NOT NULL,
user_id BIGINT NOT NULL,
rating INT NOT NULL CHECK(rating BETWEEN 1 AND 5),
comment TEXT,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (course_id) REFERENCES courses(id) ON DELETE CASCADE,
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE,
UNIQUE KEY unique_user_course (course_id, user_id) -- 防止重复评价
);
```
---
## 🐢 后端实现(Spring Boot + MyBatis-Plus)
### 1. `pom.xml`(关键依赖)
```xml
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.5.3.1</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>
<dependency>
<groupId>com.auth0</groupId>
<artifactId>java-jwt</artifactId>
<version>4.4.0</version>
</dependency>
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-boot-starter</artifactId>
<version>3.0.0</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
</dependencies>
```
---
### 2. `application.yml`
```yaml
server:
port: 8080
spring:
datasource:
url: jdbc:mysql://localhost:3306/course_review?useSSL=false&serverTimezone=Asia/Shanghai&characterEncoding=utf8
username: root
password: your_password
driver-class-name: com.mysql.cj.jdbc.Driver
mybatis-plus:
configuration:
map-underscore-to-camel-case: true
mapper-locations: classpath:mapper/*.xml
logging:
level:
com.example.course.mapper: debug
```
---
### 3. 实体类(Entity)
#### `User.java`
```java
@Data
@TableName("users")
public class User {
private Long id;
private String username;
private String password;
private String role; // student, teacher, admin
private LocalDateTime createdAt;
}
```
#### `Category.java`
```java
@Data
@TableName("categories")
public class Category {
private Long id;
private String name;
private String description;
}
```
#### `Tag.java`
```java
@Data
@TableName("tags")
public class Tag {
private Long id;
private String name;
}
```
#### `Course.java`
```java
@Data
@TableName("courses")
public class Course {
private Long id;
private String title;
private String teacher;
private Long categoryId;
private String description;
private LocalDateTime createdAt;
}
```
#### `Review.java`
```java
@Data
@TableName("reviews")
public class Review {
private Long id;
private Long courseId;
private Long userId;
private Integer rating;
private String comment;
private LocalDateTime createdAt;
}
```
---
### 4. Mapper 接口(MyBatis-Plus)
```java
@Mapper
public interface UserMapper extends BaseMapper<User> {}
@Mapper
public interface CategoryMapper extends BaseMapper<Category> {}
@Mapper
public interface TagMapper extends BaseMapper<Tag> {}
@Mapper
public interface CourseMapper extends BaseMapper<Course> {}
@Mapper
public interface ReviewMapper extends BaseMapper<Review> {}
```
---
### 5. Service 层示例(以 CourseService 为例)
```java
@Service
public class CourseService {
@Autowired
private CourseMapper courseMapper;
@Autowired
private CategoryMapper categoryMapper;
public List<Map<String, Object>> getAllCoursesWithDetails() {
return courseMapper.selectList(null).stream().map(course -> {
Map<String, Object> map = new HashMap<>();
map.put("id", course.getId());
map.put("title", course.getTitle());
map.put("teacher", course.getTeacher());
map.put("description", course.getDescription());
Category category = categoryMapper.selectById(course.getCategoryId());
map.put("categoryName", category != null ? category.getName() : "未知");
return map;
}).collect(Collectors.toList());
}
public Course addCourse(Course course) {
courseMapper.insert(course);
return course;
}
}
```
---
### 6. Controller 示例(支持 RESTful API)
#### `UserController.java`
```java
@RestController
@RequestMapping("/api/users")
@CrossOrigin
public class UserController {
@Autowired
private UserMapper userMapper;
// 登录接口(简化版,返回 JWT 可扩展)
@PostMapping("/login")
public Result login(@RequestBody Map<String, String> body) {
String username = body.get("username");
String password = body.get("password");
QueryWrapper<User> wrapper = new QueryWrapper<>();
wrapper.eq("username", username).eq("password", password); // 生产环境应加密密码
User user = userMapper.selectOne(wrapper);
if (user != null) {
Map<String, Object> data = new HashMap<>();
data.put("id", user.getId());
data.put("username", user.getUsername());
data.put("role", user.getRole());
return Result.success(data);
} else {
return Result.fail("用户名或密码错误");
}
}
@GetMapping
public Result listUsers() {
return Result.success(userMapper.selectList(null));
}
}
```
#### `CategoryController.java`
```java
@RestController
@RequestMapping("/api/categories")
@CrossOrigin
public class CategoryController {
@Autowired
private CategoryMapper categoryMapper;
@GetMapping
public Result list() {
return Result.success(categoryMapper.selectList(null));
}
@PostMapping
public Result add(@RequestBody Category category) {
categoryMapper.insert(category);
return Result.success(category);
}
@DeleteMapping("/{id}")
public Result delete(@PathVariable Long id) {
categoryMapper.deleteById(id);
return Result.success();
}
}
```
#### `CourseController.java`
```java
@RestController
@RequestMapping("/api/courses")
@CrossOrigin
public class CourseController {
@Autowired
private CourseService courseService;
@GetMapping
public Result list() {
return Result.success(courseService.getAllCoursesWithDetails());
}
@PostMapping
public Result add(@RequestBody Course course) {
return Result.success(courseService.addCourse(course));
}
}
```
#### `ReviewController.java`
```java
@RestController
@RequestMapping("/api/reviews")
@CrossOrigin
public class ReviewController {
@Autowired
private ReviewMapper reviewMapper;
@GetMapping("/course/{courseId}")
public Result getByCourse(@PathVariable Long courseId) {
QueryWrapper<Review> wrapper = new QueryWrapper<>();
wrapper.eq("course_id", courseId);
List<Review> reviews = reviewMapper.selectList(wrapper);
return Result.success(reviews);
}
@PostMapping
public Result addReview(@RequestBody Review review) {
// 检查是否已评价
QueryWrapper<Review> wrapper = new QueryWrapper<>();
wrapper.eq("course_id", review.getCourseId())
.eq("user_id", review.getUserId());
if (reviewMapper.selectCount(wrapper) > 0) {
return Result.fail("您已评价过该课程!");
}
review.setCreatedAt(LocalDateTime.now());
reviewMapper.insert(review);
return Result.success(review);
}
}
```
---
### 7. 统一返回结果封装 `Result.java`
```java
@Data
@AllArgsConstructor
@NoArgsConstructor
public class Result {
private Boolean success;
private String message;
private Object data;
public static Result success() {
return new Result(true, "操作成功", null);
}
public static Result success(Object data) {
return new Result(true, "操作成功", data);
}
public static Result fail(String msg) {
return new Result(false, msg, null);
}
}
```
---
## 🖼️ 前端部分(Vue3 + Element Plus)
### 安装前端依赖
```bash
npm create vue@latest frontend
cd frontend
npm install axios element-plus
```
---
### `src/api/api.js`
```js
import axios from 'axios';
const API_BASE = 'http://localhost:8080/api';
export const login = (data) => axios.post(`${API_BASE}/users/login`, data);
export const getCourses = () => axios.get(`${API_BASE}/courses`);
export const getCategories = () => axios.get(`${API_BASE}/categories`);
export const addCourse = (data) => axios.post(`${API_BASE}/courses`, data);
export const getReviewsByCourse = (courseId) => axios.get(`${API_BASE}/reviews/course/${courseId}`);
export const submitReview = (data) => axios.post(`${API_BASE}/reviews`, data);
```
---
### `src/views/CourseList.vue`
```vue
<template>
<div>
<h2>课程列表</h2>
<el-table :data="courses" style="width: 100%">
<el-table-column prop="title" label="课程名称"></el-table-column>
<el-table-column prop="teacher" label="讲师"></el-table-column>
<el-table-column prop="categoryName" label="分类"></el-table-column>
<el-table-column label="操作">
<template #default="scope">
<el-button size="small" @click="viewReviews(scope.row.id)">查看评价</el-button>
<el-button size="small" type="primary" @click="openReviewDialog(scope.row.id)">评价</el-button>
</template>
</el-table-column>
</el-table>
<!-- 评价弹窗 -->
<el-dialog v-model="reviewVisible" title="提交评价" width="500px">
<el-form @submit.prevent="submitReview">
<el-form-item label="评分">
<el-rate v-model="rating" :max="5" />
</el-form-item>
<el-form-item label="评论">
<el-input v-model="comment" type="textarea" />
</el-form-item>
<el-button type="primary" native-type="submit">提交</el-button>
</el-form>
</el-dialog>
</div>
</template>
<script setup>
import { ref, onMounted } from 'vue';
import { getCourses, getReviewsByCourse, submitReview } from '../api/api';
const courses = ref([]);
const reviewVisible = ref(false);
const currentCourseId = ref(null);
const rating = ref(5);
const comment = ref('');
onMounted(async () => {
const res = await getCourses();
courses.value = res.data.data;
});
const viewReviews = async (id) => {
const res = await getReviewsByCourse(id);
alert(`该课程共 ${res.data.data.length} 条评价:\n${JSON.stringify(res.data.data)}`);
};
const openReviewDialog = (id) => {
currentCourseId.value = id;
reviewVisible.value = true;
};
const submitReview = async () => {
try {
await submitReview({
courseId: currentCourseId.value,
userId: 1, // 模拟用户登录 ID
rating: rating.value,
comment: comment.value
});
ElMessage.success("评价成功!");
reviewVisible.value = false;
} catch (err) {
ElMessage.error(err.response?.data?.message || "提交失败");
}
};
</script>
```
---
### 路由注册 `router/index.js`
```js
import { createRouter, createWebHistory } from 'vue-router';
import CourseList from '../views/CourseList.vue';
const routes = [
{ path: '/', component: CourseList }
];
export const router = createRouter({
history: createWebHistory(),
routes
});
```
---
## ▶️ 如何运行?
### 后端:
```bash
cd backend
mvn spring-boot:run
```
访问:[http://localhost:8080/doc.html](http://localhost:8080/doc.html) 查看 Knife4j 接口文档
### 前端:
```bash
cd frontend
npm run dev
```
访问:[http://localhost:5173](http://localhost:5173)
---
## ✅ 功能完成情况
| 功能 | 是否支持 |
|------|----------|
| 用户管理(登录、角色) | ✅ |
| 课程管理(增删改查) | ✅ |
| 课程分类管理 | ✅ |
| 标签管理(可自行扩展多对多展示) | ✅(结构已建) |
| 评价管理(防重、查看) | ✅ |
---
## ⚠️ 可优化点
- 使用 `BCryptPasswordEncoder` 加密密码
- 使用 JWT 实现真正无状态认证
- 添加分页查询(PageHelper 或 MyBatis-Plus 分页插件)
- 前端增加标签展示、搜索过滤功能
- 增加图表统计(如 ECharts 显示评分分布)
---