1. 项目概述
这个基于SpringBoot+Vue3+MyBatis的个性化电影推荐系统,是我在研究生期间参与开发的一个实战项目。系统采用前后端分离架构,后端使用Java SpringBoot框架,前端采用Vue3组合式API开发,数据库选用MySQL,通过MyBatis实现数据持久化。核心功能是通过分析用户历史行为和电影特征,实现个性化的电影推荐。
提示:在实际开发中发现,采用前后端分离架构时,接口文档的规范性和完整性对开发效率影响很大。建议使用Swagger或YAPI进行接口管理。
系统主要包含以下几个核心模块:
- 用户管理模块:处理用户注册、登录和个人信息维护
- 电影管理模块:管理电影信息和分类标签
- 评分记录模块:记录用户对电影的评分和评论
- 推荐引擎模块:基于协同过滤算法生成个性化推荐
- 后台管理模块:提供数据统计和系统配置功能
2. 技术选型与架构设计
2.1 后端技术栈
后端采用SpringBoot 2.7.x版本,这是目前企业级Java开发的主流框架。选择SpringBoot主要基于以下考虑:
- 自动配置特性大幅减少了XML配置
- 内嵌Tomcat服务器简化了部署流程
- 丰富的starter依赖可以快速集成常用组件
- 完善的生态和社区支持
数据访问层使用MyBatis-Plus 3.5.x,相比原生MyBatis,它提供了更多开箱即用的功能:
- 通用Mapper减少重复CRUD代码
- 强大的条件构造器简化复杂查询
- 分页插件支持多种数据库
- 性能分析插件帮助优化SQL
2.2 前端技术栈
前端选用Vue3 + TypeScript的组合,主要优势包括:
- Composition API使代码组织更灵活
- 更好的TypeScript支持
- 更小的打包体积和更高的运行效率
- 更灵活的逻辑复用方式
UI组件库使用Element Plus,这是对Vue3支持最好的企业级组件库之一。同时引入ECharts实现数据可视化,展示用户行为和推荐效果统计。
2.3 数据库设计
MySQL 8.0作为关系型数据库,存储结构化数据。三个核心表的设计如下:
2.3.1 用户表(user)
sql复制CREATE TABLE `user` (
`user_id` bigint NOT NULL AUTO_INCREMENT,
`username` varchar(50) COLLATE utf8mb4_general_ci NOT NULL,
`password_hash` varchar(100) COLLATE utf8mb4_general_ci NOT NULL,
`email` varchar(100) COLLATE utf8mb4_general_ci DEFAULT NULL,
`register_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
`last_login` datetime DEFAULT NULL,
`interest_tags` text COLLATE utf8mb4_general_ci,
PRIMARY KEY (`user_id`),
UNIQUE KEY `username` (`username`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
2.3.2 电影表(movie)
sql复制CREATE TABLE `movie` (
`movie_id` bigint NOT NULL AUTO_INCREMENT,
`title` varchar(100) COLLATE utf8mb4_general_ci NOT NULL,
`director` varchar(50) COLLATE utf8mb4_general_ci DEFAULT NULL,
`release_year` int DEFAULT NULL,
`genre` varchar(50) COLLATE utf8mb4_general_ci DEFAULT NULL,
`description` text COLLATE utf8mb4_general_ci,
`avg_rating` float DEFAULT '0',
`poster_url` varchar(200) COLLATE utf8mb4_general_ci DEFAULT NULL,
PRIMARY KEY (`movie_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
2.3.3 评分表(rating)
sql复制CREATE TABLE `rating` (
`rating_id` bigint NOT NULL AUTO_INCREMENT,
`user_id` bigint NOT NULL,
`movie_id` bigint NOT NULL,
`rating_score` float NOT NULL,
`rating_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
`feedback_comment` text COLLATE utf8mb4_general_ci,
PRIMARY KEY (`rating_id`),
KEY `user_id` (`user_id`),
KEY `movie_id` (`movie_id`),
CONSTRAINT `rating_ibfk_1` FOREIGN KEY (`user_id`) REFERENCES `user` (`user_id`),
CONSTRAINT `rating_ibfk_2` FOREIGN KEY (`movie_id`) REFERENCES `movie` (`movie_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
注意:在实际部署时,建议为评分表添加联合索引(user_id, movie_id),可以显著提升查询性能。
3. 核心功能实现
3.1 用户认证与授权
系统采用JWT(JSON Web Token)实现无状态认证,核心流程如下:
- 用户登录时,服务端验证凭证
- 生成包含用户ID和权限的JWT令牌
- 令牌通过HTTP Header返回给客户端
- 客户端后续请求携带该令牌
- 服务端验证令牌有效性并处理请求
Spring Security配置示例:
java复制@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http.csrf().disable()
.authorizeRequests()
.antMatchers("/api/auth/**").permitAll()
.antMatchers("/api/admin/**").hasRole("ADMIN")
.anyRequest().authenticated()
.and()
.addFilter(new JwtAuthenticationFilter(authenticationManager()))
.addFilter(new JwtAuthorizationFilter(authenticationManager()))
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
}
}
3.2 推荐算法实现
系统采用混合推荐策略,结合基于用户的协同过滤和基于内容的推荐:
3.2.1 基于用户的协同过滤
- 计算用户相似度(余弦相似度):
java复制public double userSimilarity(long userId1, long userId2) {
List<Rating> ratings1 = ratingMapper.findByUserId(userId1);
List<Rating> ratings2 = ratingMapper.findByUserId(userId2);
// 构建评分向量
Map<Long, Double> vector1 = ratings1.stream()
.collect(Collectors.toMap(Rating::getMovieId, Rating::getRatingScore));
Map<Long, Double> vector2 = ratings2.stream()
.collect(Collectors.toMap(Rating::getMovieId, Rating::getRatingScore));
// 计算共同评分项
Set<Long> commonMovies = new HashSet<>(vector1.keySet());
commonMovies.retainAll(vector2.keySet());
if (commonMovies.isEmpty()) return 0.0;
// 计算余弦相似度
double dotProduct = 0.0;
double norm1 = 0.0;
double norm2 = 0.0;
for (Long movieId : commonMovies) {
double score1 = vector1.get(movieId);
double score2 = vector2.get(movieId);
dotProduct += score1 * score2;
norm1 += Math.pow(score1, 2);
norm2 += Math.pow(score2, 2);
}
return dotProduct / (Math.sqrt(norm1) * Math.sqrt(norm2));
}
- 生成推荐列表:
java复制public List<Movie> recommendByUserCF(long userId, int topN) {
// 获取目标用户评分过的电影
List<Rating> userRatings = ratingMapper.findByUserId(userId);
Set<Long> ratedMovies = userRatings.stream()
.map(Rating::getMovieId)
.collect(Collectors.toSet());
// 找到相似用户
List<User> allUsers = userMapper.findAll();
Map<User, Double> similarUsers = new HashMap<>();
for (User otherUser : allUsers) {
if (otherUser.getUserId() == userId) continue;
double similarity = userSimilarity(userId, otherUser.getUserId());
if (similarity > 0) {
similarUsers.put(otherUser, similarity);
}
}
// 计算推荐得分
Map<Long, Double> recommendationScores = new HashMap<>();
for (User similarUser : similarUsers.keySet()) {
double similarity = similarUsers.get(similarUser);
List<Rating> similarUserRatings = ratingMapper.findByUserId(similarUser.getUserId());
for (Rating rating : similarUserRatings) {
if (!ratedMovies.contains(rating.getMovieId())) {
double weightedScore = rating.getRatingScore() * similarity;
recommendationScores.merge(rating.getMovieId(), weightedScore, Double::sum);
}
}
}
// 排序并返回TopN推荐
return recommendationScores.entrySet().stream()
.sorted(Map.Entry.<Long, Double>comparingByValue().reversed())
.limit(topN)
.map(entry -> movieMapper.findById(entry.getKey()))
.filter(Optional::isPresent)
.map(Optional::get)
.collect(Collectors.toList());
}
3.2.2 基于内容的推荐
- 电影特征向量化:
java复制public Map<String, Double> extractMovieFeatures(Movie movie) {
Map<String, Double> features = new HashMap<>();
// 导演特征
if (movie.getDirector() != null) {
features.put("director:" + movie.getDirector(), 1.0);
}
// 类型特征
if (movie.getGenre() != null) {
Arrays.stream(movie.getGenre().split(","))
.forEach(genre -> features.put("genre:" + genre.trim(), 1.0));
}
// 关键词特征(从描述中提取)
if (movie.getDescription() != null) {
List<String> keywords = extractKeywords(movie.getDescription());
keywords.forEach(keyword ->
features.merge("keyword:" + keyword, 1.0, Double::sum));
}
return features;
}
- 用户画像构建:
java复制public Map<String, Double> buildUserProfile(long userId) {
List<Rating> userRatings = ratingMapper.findByUserId(userId);
Map<String, Double> profile = new HashMap<>();
for (Rating rating : userRatings) {
Optional<Movie> movieOpt = movieMapper.findById(rating.getMovieId());
if (movieOpt.isPresent()) {
Map<String, Double> features = extractMovieFeatures(movieOpt.get());
double weight = rating.getRatingScore() / 5.0; // 归一化
for (Map.Entry<String, Double> entry : features.entrySet()) {
profile.merge(entry.getKey(), entry.getValue() * weight, Double::sum);
}
}
}
return profile;
}
- 内容推荐生成:
java复制public List<Movie> recommendByContent(long userId, int topN) {
// 获取用户画像
Map<String, Double> userProfile = buildUserProfile(userId);
// 获取用户已评分的电影
Set<Long> ratedMovies = ratingMapper.findByUserId(userId).stream()
.map(Rating::getMovieId)
.collect(Collectors.toSet());
// 计算所有未评分电影与用户画像的相似度
List<Movie> allMovies = movieMapper.findAll();
Map<Movie, Double> movieScores = new HashMap<>();
for (Movie movie : allMovies) {
if (!ratedMovies.contains(movie.getMovieId())) {
Map<String, Double> movieFeatures = extractMovieFeatures(movie);
double similarity = cosineSimilarity(userProfile, movieFeatures);
movieScores.put(movie, similarity);
}
}
// 返回TopN推荐
return movieScores.entrySet().stream()
.sorted(Map.Entry.<Movie, Double>comparingByValue().reversed())
.limit(topN)
.map(Map.Entry::getKey)
.collect(Collectors.toList());
}
3.3 前后端交互实现
前端通过Axios与后端API交互,主要接口设计如下:
| 接口路径 | 方法 | 描述 | 参数 |
|---|---|---|---|
| /api/auth/login | POST | 用户登录 | username, password |
| /api/auth/register | POST | 用户注册 | username, password, email |
| /api/movies | GET | 获取电影列表 | page, size, genre, year |
| /api/movies/ | GET | 获取电影详情 | - |
| /api/ratings | POST | 提交评分 | movieId, score, comment |
| /api/recommendations | GET | 获取推荐列表 | type(user/content/hybrid), limit |
| /api/users/{id}/ratings | GET | 获取用户评分记录 | - |
前端API调用示例:
javascript复制import axios from 'axios';
const api = axios.create({
baseURL: process.env.VUE_APP_API_BASE_URL,
timeout: 10000,
});
// 请求拦截器
api.interceptors.request.use(config => {
const token = localStorage.getItem('token');
if (token) {
config.headers.Authorization = `Bearer ${token}`;
}
return config;
});
// 获取推荐电影
export const getRecommendations = (type = 'hybrid', limit = 10) => {
return api.get('/api/recommendations', {
params: { type, limit }
});
};
// 提交评分
export const submitRating = (movieId, score, comment = '') => {
return api.post('/api/ratings', {
movieId, score, comment
});
};
4. 系统部署与优化
4.1 后端部署配置
SpringBoot应用推荐使用Docker容器化部署,Dockerfile示例:
dockerfile复制FROM openjdk:11-jre-slim
WORKDIR /app
COPY target/movie-recommendation-system-*.jar app.jar
EXPOSE 8080
ENTRYPOINT ["java", "-jar", "app.jar"]
生产环境建议添加JVM参数优化:
bash复制java -jar -Xms512m -Xmx1024m -XX:MaxMetaspaceSize=256m \
-XX:+UseG1GC -XX:MaxGCPauseMillis=200 \
-Dspring.profiles.active=prod app.jar
4.2 前端部署配置
Vue3项目构建和Nginx配置:
nginx复制server {
listen 80;
server_name yourdomain.com;
location / {
root /usr/share/nginx/html;
index index.html;
try_files $uri $uri/ /index.html;
}
location /api {
proxy_pass http://backend:8080;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
}
}
4.3 性能优化实践
- 数据库优化:
- 为常用查询字段添加索引
- 使用连接池控制数据库连接数
- 对大表考虑分库分表策略
- 缓存策略:
java复制@Configuration
@EnableCaching
public class CacheConfig {
@Bean
public RedisCacheManager cacheManager(RedisConnectionFactory factory) {
RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig()
.entryTtl(Duration.ofMinutes(30))
.disableCachingNullValues()
.serializeValuesWith(SerializationPair.fromSerializer(
new GenericJackson2JsonRedisSerializer()));
return RedisCacheManager.builder(factory)
.cacheDefaults(config)
.transactionAware()
.build();
}
}
@Service
@CacheConfig(cacheNames = "movies")
public class MovieService {
@Cacheable(key = "#id")
public Movie findById(Long id) {
return movieMapper.findById(id).orElse(null);
}
@CacheEvict(allEntries = true)
public void clearCache() {
// 清空缓存
}
}
- 推荐算法优化:
- 使用离线计算+实时更新的混合模式
- 对相似度计算采用近似算法降低复杂度
- 引入时间衰减因子,更重视近期行为
5. 常见问题与解决方案
5.1 冷启动问题
新用户或新物品缺乏足够数据时,推荐质量下降。我们采用以下解决方案:
- 新用户:结合热门电影和内容标签进行推荐
- 新电影:基于内容相似度推荐给可能感兴趣的用户
- 引入混合推荐策略,平衡协同过滤和内容推荐
5.2 数据稀疏性问题
用户-物品评分矩阵通常非常稀疏。我们通过以下方法缓解:
- 矩阵填充技术:使用平均值或基于内容的预测值填充缺失项
- 降维技术:如SVD分解降低特征维度
- 引入社交网络信息丰富用户特征
5.3 实时性挑战
用户行为需要快速反映到推荐结果中。实现方案:
- 分层架构:离线计算+近线更新+实时响应
- 流处理:使用Kafka处理实时用户行为事件
- 增量更新:定期更新用户相似度和物品特征
5.4 系统监控与评估
建立完整的监控和评估体系:
- 技术指标监控:API响应时间、错误率、吞吐量
- 业务指标跟踪:点击率、转化率、推荐覆盖率
- A/B测试框架:对比不同算法的实际效果
6. 项目扩展方向
在实际开发过程中,我发现这个系统还有多个可以深入优化的方向:
- 多模态推荐:结合海报图像、预告片视频等多媒体内容
- 上下文感知推荐:考虑时间、地点、设备等上下文因素
- 强化学习:使用Bandit算法实现探索-利用平衡
- 可解释性推荐:提供推荐理由增强用户信任
- 联邦学习:在保护用户隐私的前提下实现跨平台推荐
经验分享:在实现推荐系统时,不要一味追求算法复杂度,而应该注重业务场景的适配性。简单的算法配合良好的工程实现,往往比复杂的算法更能产生实际价值。