1. 项目概述
这个基于SpringBoot+Vue的协同过滤电影推荐系统是一个典型的互联网应用项目,采用了前后端分离的架构设计。后端使用SpringBoot框架搭建RESTful API服务,前端采用Vue.js构建用户界面,数据库选用MySQL存储数据,通过MyBatis实现数据持久化操作。
系统核心功能是通过分析用户的历史观影行为和评分数据,运用协同过滤算法为用户推荐可能感兴趣的电影。这种推荐方式不依赖于电影本身的属性信息,而是基于"相似用户喜欢相似物品"的基本假设,能够发现用户潜在的兴趣偏好。
在实际开发中发现,协同过滤算法虽然原理简单,但在实现过程中需要考虑数据稀疏性、冷启动等实际问题,这些都会直接影响推荐效果。
2. 系统架构设计
2.1 技术栈选型
后端技术栈:
- SpringBoot 2.7.x:简化Spring应用的初始搭建和开发过程
- MyBatis 3.5.x:轻量级ORM框架,简化数据库操作
- MySQL 8.0:关系型数据库,存储用户和电影数据
- Redis 6.x:缓存热门电影和推荐结果,提高系统响应速度
前端技术栈:
- Vue 3.x:渐进式JavaScript框架
- Element Plus:基于Vue 3的UI组件库
- Axios:处理HTTP请求
- ECharts:数据可视化展示
2.2 系统模块划分
系统主要分为以下几个功能模块:
- 用户管理模块:处理用户注册、登录、个人信息维护
- 电影管理模块:电影信息的CRUD操作
- 推荐引擎模块:实现协同过滤算法
- 评分管理模块:记录用户对电影的评分
- 系统管理模块:权限控制、日志记录等
3. 数据库设计
3.1 核心数据表结构
sql复制-- 用户表
CREATE TABLE `user` (
`id` bigint NOT NULL AUTO_INCREMENT,
`username` varchar(50) NOT NULL,
`password` varchar(100) NOT NULL,
`email` varchar(100) NOT NULL,
`avatar` varchar(255) DEFAULT NULL,
`create_time` datetime NOT NULL,
`update_time` datetime NOT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `idx_username` (`username`),
UNIQUE KEY `idx_email` (`email`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
-- 电影表
CREATE TABLE `movie` (
`id` bigint NOT NULL AUTO_INCREMENT,
`title` varchar(100) NOT NULL,
`director` varchar(50) DEFAULT NULL,
`actors` varchar(255) DEFAULT NULL,
`genre` varchar(50) DEFAULT NULL,
`release_year` int DEFAULT NULL,
`rating` decimal(3,1) DEFAULT NULL,
`poster` varchar(255) DEFAULT NULL,
`description` text,
`create_time` datetime NOT NULL,
`update_time` datetime NOT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
-- 用户评分表
CREATE TABLE `user_rating` (
`id` bigint NOT NULL AUTO_INCREMENT,
`user_id` bigint NOT NULL,
`movie_id` bigint NOT NULL,
`rating` decimal(2,1) NOT NULL,
`create_time` datetime NOT NULL,
`update_time` datetime NOT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `idx_user_movie` (`user_id`,`movie_id`),
KEY `idx_movie` (`movie_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
3.2 数据关系设计
系统采用关系型数据库设计,主要实体关系如下:
- 用户与评分:一对多关系
- 电影与评分:一对多关系
- 用户与推荐结果:一对多关系
数据库设计时特别需要注意索引的合理设置,特别是在用户评分表上建立了(user_id, movie_id)的联合唯一索引,防止重复评分,同时为movie_id建立了普通索引,优化基于电影的查询性能。
4. 协同过滤算法实现
4.1 算法原理
协同过滤算法主要分为两种类型:
- 基于用户的协同过滤(UserCF):找出与目标用户兴趣相似的用户群体,推荐这个群体喜欢的电影
- 基于物品的协同过滤(ItemCF):找出与目标用户已评分电影相似的其他电影,推荐这些相似电影
本系统实现了这两种算法,并提供了算法切换功能。
4.2 核心代码实现
java复制// 基于用户的协同过滤实现
public List<Movie> userBasedCF(Long userId, int size) {
// 1. 获取目标用户的评分数据
Map<Long, Double> userRatings = ratingService.getUserRatings(userId);
// 2. 计算用户相似度矩阵
Map<Long, Double> similarityScores = new HashMap<>();
for (Long otherUserId : userService.getAllUserIds()) {
if (otherUserId.equals(userId)) continue;
Map<Long, Double> otherRatings = ratingService.getUserRatings(otherUserId);
double similarity = calculateSimilarity(userRatings, otherRatings);
if (similarity > 0) {
similarityScores.put(otherUserId, similarity);
}
}
// 3. 获取相似用户的偏好电影
Map<Long, Double> candidateMovies = new HashMap<>();
for (Map.Entry<Long, Double> entry : similarityScores.entrySet()) {
Long similarUserId = entry.getKey();
double similarity = entry.getValue();
Map<Long, Double> ratings = ratingService.getUserRatings(similarUserId);
for (Map.Entry<Long, Double> ratingEntry : ratings.entrySet()) {
Long movieId = ratingEntry.getKey();
double rating = ratingEntry.getValue();
// 跳过用户已经评分的电影
if (userRatings.containsKey(movieId)) continue;
candidateMovies.merge(movieId, similarity * rating, Double::sum);
}
}
// 4. 排序并返回推荐结果
return candidateMovies.entrySet().stream()
.sorted(Map.Entry.<Long, Double>comparingByValue().reversed())
.limit(size)
.map(entry -> movieService.getMovieById(entry.getKey()))
.collect(Collectors.toList());
}
// 计算余弦相似度
private double calculateSimilarity(Map<Long, Double> ratings1, Map<Long, Double> ratings2) {
Set<Long> commonMovies = new HashSet<>(ratings1.keySet());
commonMovies.retainAll(ratings2.keySet());
if (commonMovies.isEmpty()) return 0;
double dotProduct = 0;
double norm1 = 0;
double norm2 = 0;
for (Long movieId : commonMovies) {
double r1 = ratings1.get(movieId);
double r2 = ratings2.get(movieId);
dotProduct += r1 * r2;
norm1 += r1 * r1;
norm2 += r2 * r2;
}
return dotProduct / (Math.sqrt(norm1) * Math.sqrt(norm2));
}
4.3 性能优化
在实际应用中,我们发现直接实现上述算法会有性能问题,特别是当用户和电影数量较大时。我们采取了以下优化措施:
- 数据采样:计算相似度时,只考虑有共同评分记录的用户对
- 相似度预计算:定期离线计算用户/电影相似度矩阵,存储在Redis中
- 分区处理:将用户按活跃度分组,只计算活跃用户间的相似度
- 并行计算:使用Java 8的并行流加速矩阵计算
算法优化过程中,我们发现相似度矩阵的存储和更新是个难点。最终采用Redis的Sorted Set来存储用户相似度,既保证了查询效率,又便于更新维护。
5. 前后端交互实现
5.1 RESTful API设计
系统后端提供了以下主要API端点:
code复制GET /api/movies/recommend - 获取推荐电影列表
POST /api/ratings - 提交电影评分
GET /api/movies/{id} - 获取电影详情
GET /api/users/{id}/ratings - 获取用户评分历史
5.2 前端组件实现
前端使用Vue 3的Composition API实现推荐页面:
vue复制<template>
<div class="recommend-container">
<h2>为您推荐</h2>
<el-radio-group v-model="algorithmType" @change="loadRecommendations">
<el-radio-button label="user">基于用户</el-radio-button>
<el-radio-button label="item">基于电影</el-radio-button>
</el-radio-group>
<div v-loading="loading">
<el-row :gutter="20">
<el-col :span="6" v-for="movie in movies" :key="movie.id">
<movie-card :movie="movie" @rate="handleRate"/>
</el-col>
</el-row>
</div>
</div>
</template>
<script setup>
import { ref, onMounted } from 'vue'
import { getRecommendations } from '@/api/recommend'
import MovieCard from '@/components/MovieCard.vue'
const algorithmType = ref('user')
const movies = ref([])
const loading = ref(false)
const loadRecommendations = async () => {
try {
loading.value = true
const { data } = await getRecommendations(algorithmType.value)
movies.value = data
} catch (error) {
console.error('获取推荐失败:', error)
} finally {
loading.value = false
}
}
const handleRate = (movieId, rating) => {
// 处理评分逻辑
}
onMounted(() => {
loadRecommendations()
})
</script>
5.3 跨域问题解决
由于采用前后端分离架构,需要处理跨域问题。后端通过配置CORS解决:
java复制@Configuration
public class WebConfig implements WebMvcConfigurer {
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/**")
.allowedOrigins("*")
.allowedMethods("GET", "POST", "PUT", "DELETE")
.allowedHeaders("*")
.maxAge(3600);
}
}
6. 系统部署与性能调优
6.1 部署方案
系统采用Docker容器化部署,主要包含以下服务:
- 前端服务:Nginx + Vue静态资源
- 后端服务:SpringBoot应用
- 数据库服务:MySQL
- 缓存服务:Redis
使用docker-compose编排服务:
yaml复制version: '3'
services:
frontend:
image: nginx:alpine
ports:
- "80:80"
volumes:
- ./frontend/dist:/usr/share/nginx/html
depends_on:
- backend
backend:
build: ./backend
ports:
- "8080:8080"
environment:
- SPRING_DATASOURCE_URL=jdbc:mysql://mysql:3306/movie_recommend
- SPRING_DATASOURCE_USERNAME=root
- SPRING_DATASOURCE_PASSWORD=123456
- SPRING_REDIS_HOST=redis
depends_on:
- mysql
- redis
mysql:
image: mysql:8.0
ports:
- "3306:3306"
environment:
- MYSQL_ROOT_PASSWORD=123456
- MYSQL_DATABASE=movie_recommend
volumes:
- mysql_data:/var/lib/mysql
redis:
image: redis:alpine
ports:
- "6379:6379"
volumes:
- redis_data:/data
volumes:
mysql_data:
redis_data:
6.2 性能优化措施
-
数据库优化:
- 合理设计索引
- 使用连接池(HikariCP)
- 读写分离(主从复制)
-
缓存策略:
- 热门电影数据缓存
- 推荐结果缓存
- 使用Spring Cache抽象层
-
异步处理:
- 使用@Async处理耗时操作
- 评分提交后异步更新推荐模型
-
JVM调优:
- 合理设置堆内存大小
- 选择合适的GC算法
在压力测试中发现,推荐算法的计算是系统瓶颈。最终采用定时任务夜间计算相似度矩阵,白天直接使用预计算结果的方案,大幅提高了系统响应速度。
7. 常见问题与解决方案
7.1 冷启动问题
问题描述:新用户或新电影缺乏足够的历史数据,难以产生有效推荐。
解决方案:
- 对于新用户:采用热门电影推荐、基于内容的推荐作为补充
- 对于新电影:利用电影元数据(类型、导演等)计算相似度
- 引入混合推荐策略,结合协同过滤和基于内容的方法
7.2 数据稀疏性问题
问题描述:用户只对少量电影评分,导致相似度计算不准确。
解决方案:
- 使用降维技术(SVD)处理评分矩阵
- 引入隐式反馈数据(浏览时长、点击行为等)
- 采用基于模型的协同过滤(矩阵分解)
7.3 系统扩展性问题
问题描述:随着用户和电影数量增长,系统性能下降。
解决方案:
- 采用分布式计算框架(Spark)处理大规模数据
- 实现增量更新算法,避免全量计算
- 引入分片策略,按用户分组处理
8. 项目扩展方向
-
推荐算法增强:
- 引入深度学习模型(如Neural CF)
- 实现实时推荐系统
- 增加上下文感知推荐(时间、地点等)
-
系统功能扩展:
- 增加社交功能(好友关系、影评分享)
- 实现多端同步(Web、移动端)
- 加入电影订阅和提醒功能
-
技术架构升级:
- 引入消息队列(Kafka)处理用户行为数据
- 使用微服务架构拆分系统功能
- 采用云原生技术部署
在实际开发这类推荐系统时,我发现算法效果与数据质量密切相关。建议在项目初期就建立完善的数据收集和清洗机制,同时设计灵活的策略切换机制,便于比较不同算法的实际效果。系统上线后,还需要建立A/B测试框架,持续优化推荐效果。
