这个电影推荐平台采用前后端分离架构,前端使用Python的Flask框架实现用户交互界面,后端采用Java的SSM(Spring+SpringMVC+MyBatis)框架处理业务逻辑。系统主要功能包括:电影信息管理、用户评分收集、个性化推荐算法实现、新闻公告管理等。平台通过分析用户历史行为数据,为用户推荐可能感兴趣的高分经典电影。
技术选型思考:选择Flask作为前端框架是因为其轻量级和灵活性,特别适合快速构建展示型页面;而SSM框架在后端开发中具有成熟的生态体系,能够很好地处理复杂的业务逻辑和数据持久化需求。这种混合架构充分发挥了两种技术栈各自的优势。
系统采用典型的三层架构设计:
code复制客户端 → Flask前端 → REST API → Spring MVC → MyBatis → MySQL
↑
推荐算法模块
核心数据表包括:
sql复制CREATE TABLE `movie` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`title` varchar(255) NOT NULL,
`director` varchar(100) DEFAULT NULL,
`actors` varchar(255) DEFAULT NULL,
`release_year` int(11) DEFAULT NULL,
`genre` varchar(50) DEFAULT NULL,
`rating` decimal(3,1) DEFAULT NULL,
`description` text,
`cover_url` varchar(255) DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
前后端通过RESTful API交互,主要接口包括:
系统采用基于用户的协同过滤算法实现推荐功能:
java复制public List<Movie> recommendMovies(int userId, int topN) {
// 1. 获取目标用户的评分记录
List<Rating> userRatings = ratingMapper.selectByUserId(userId);
// 2. 计算与其他用户的相似度
Map<Integer, Double> userSimilarities = new HashMap<>();
for (User otherUser : userMapper.selectAll()) {
if (otherUser.getId() == userId) continue;
double similarity = calculateSimilarity(userId, otherUser.getId());
userSimilarities.put(otherUser.getId(), similarity);
}
// 3. 获取相似用户喜欢的电影
Map<Movie, Double> movieScores = new HashMap<>();
for (Map.Entry<Integer, Double> entry : userSimilarities.entrySet()) {
int similarUserId = entry.getKey();
double similarity = entry.getValue();
for (Rating rating : ratingMapper.selectByUserId(similarUserId)) {
if (!userHasRated(userId, rating.getMovieId())) {
Movie movie = movieMapper.selectById(rating.getMovieId());
movieScores.merge(movie, rating.getScore() * similarity, Double::sum);
}
}
}
// 4. 返回评分最高的N部电影
return movieScores.entrySet().stream()
.sorted(Map.Entry.<Movie, Double>comparingByValue().reversed())
.limit(topN)
.map(Map.Entry::getKey)
.collect(Collectors.toList());
}
后端采用Spring MVC实现电影管理功能:
java复制@RestController
@RequestMapping("/api/movies")
public class MovieController {
@Autowired
private MovieService movieService;
@GetMapping
public ResponseEntity<List<Movie>> listMovies(
@RequestParam(required = false) String genre,
@RequestParam(required = false) Integer year,
@RequestParam(required = false) String keyword) {
MovieQuery query = new MovieQuery();
query.setGenre(genre);
query.setYear(year);
query.setKeyword(keyword);
List<Movie> movies = movieService.queryMovies(query);
return ResponseEntity.ok(movies);
}
@PostMapping
public ResponseEntity<Movie> addMovie(@RequestBody Movie movie) {
movieService.addMovie(movie);
return ResponseEntity.status(HttpStatus.CREATED).body(movie);
}
@PutMapping("/{id}")
public ResponseEntity<Movie> updateMovie(
@PathVariable Integer id, @RequestBody Movie movie) {
movie.setId(id);
movieService.updateMovie(movie);
return ResponseEntity.ok(movie);
}
}
使用Spring Security实现基于JWT的用户认证:
java复制@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private UserDetailsService userDetailsService;
@Autowired
private JwtAuthenticationFilter jwtAuthenticationFilter;
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userDetailsService)
.passwordEncoder(passwordEncoder());
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http.csrf().disable()
.authorizeRequests()
.antMatchers("/api/auth/**").permitAll()
.antMatchers("/api/news/**").permitAll()
.anyRequest().authenticated()
.and()
.sessionManagement()
.sessionCreationPolicy(SessionCreationPolicy.STATELESS);
http.addFilterBefore(jwtAuthenticationFilter,
UsernamePasswordAuthenticationFilter.class);
}
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
}
前端采用Flask+Jinja2模板引擎实现,主要模块包括:
python复制from flask import Flask, render_template, request
import requests
app = Flask(__name__)
API_BASE_URL = "http://localhost:8080/api"
@app.route('/')
def index():
# 获取热门电影
response = requests.get(f"{API_BASE_URL}/movies?sort=rating&order=desc&limit=10")
popular_movies = response.json()
# 获取新闻公告
news_response = requests.get(f"{API_BASE_URL}/news")
news_list = news_response.json().get('data', [])
return render_template('index.html',
popular_movies=popular_movies,
news_list=news_list)
@app.route('/recommendations')
def recommendations():
# 需要用户登录后才能查看推荐
user_id = session.get('user_id')
if not user_id:
return redirect('/login')
response = requests.get(
f"{API_BASE_URL}/recommendations?userId={user_id}",
headers={'Authorization': f'Bearer {session.get("token")}'}
)
recommended_movies = response.json()
return render_template('recommendations.html',
movies=recommended_movies)
电影详情页展示电影基本信息、评分和相似推荐:
html复制{% extends "base.html" %}
{% block content %}
<div class="movie-detail">
<div class="row">
<div class="col-md-4">
<img src="{{ movie.cover_url }}" class="img-fluid" alt="{{ movie.title }}">
</div>
<div class="col-md-8">
<h1>{{ movie.title }} ({{ movie.release_year }})</h1>
<p><strong>导演:</strong> {{ movie.director }}</p>
<p><strong>主演:</strong> {{ movie.actors }}</p>
<p><strong>类型:</strong> {{ movie.genre }}</p>
<p><strong>评分:</strong> {{ movie.rating }}</p>
<p>{{ movie.description }}</p>
{% if current_user.is_authenticated %}
<div class="rating-form">
<h3>我的评分</h3>
<form action="/rate/{{ movie.id }}" method="post">
<select name="score" class="form-control">
<option value="1">1星</option>
<option value="2">2星</option>
<option value="3">3星</option>
<option value="4">4星</option>
<option value="5">5星</option>
</select>
<button type="submit" class="btn btn-primary">提交评分</button>
</form>
</div>
{% endif %}
</div>
</div>
<div class="similar-movies mt-5">
<h3>相似电影推荐</h3>
<div class="row">
{% for movie in similar_movies %}
<div class="col-md-3">
<div class="card">
<img src="{{ movie.cover_url }}" class="card-img-top" alt="{{ movie.title }}">
<div class="card-body">
<h5 class="card-title">{{ movie.title }}</h5>
<p class="card-text">{{ movie.rating }}分</p>
<a href="/movie/{{ movie.id }}" class="btn btn-sm btn-outline-primary">查看详情</a>
</div>
</div>
</div>
{% endfor %}
</div>
</div>
</div>
{% endblock %}
后端使用Maven管理依赖,关键依赖包括:
xml复制<dependencies>
<!-- Spring Boot Starter -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- MyBatis -->
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>2.2.0</version>
</dependency>
<!-- MySQL Connector -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
<!-- JWT -->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.1</version>
</dependency>
<!-- Guava -->
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>30.1.1-jre</version>
</dependency>
</dependencies>
java复制@Service
public class MovieService {
@Autowired
private MovieMapper movieMapper;
@Autowired
private RedisTemplate<String, Object> redisTemplate;
private final Cache<Integer, Movie> localCache = Caffeine.newBuilder()
.maximumSize(1000)
.expireAfterWrite(10, TimeUnit.MINUTES)
.build();
public Movie getMovieById(Integer id) {
// 先查本地缓存
Movie movie = localCache.getIfPresent(id);
if (movie != null) {
return movie;
}
// 再查Redis
String cacheKey = "movie:" + id;
movie = (Movie) redisTemplate.opsForValue().get(cacheKey);
if (movie != null) {
localCache.put(id, movie);
return movie;
}
// 最后查数据库
movie = movieMapper.selectById(id);
if (movie != null) {
redisTemplate.opsForValue().set(cacheKey, movie, 1, TimeUnit.HOURS);
localCache.put(id, movie);
}
return movie;
}
}
数据库优化:
推荐算法优化:
由于前后端分离部署,需要处理跨域问题。后端配置CORS支持:
java复制@Configuration
public class CorsConfig implements WebMvcConfigurer {
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/**")
.allowedOrigins("http://localhost:5000") // Flask前端地址
.allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS")
.allowedHeaders("*")
.allowCredentials(true)
.maxAge(3600);
}
}
对于新用户或新电影,采用以下策略解决冷启动问题:
java复制public List<Movie> handleColdStart(Integer userId) {
// 如果是新用户
if (userId == null || ratingMapper.countByUserId(userId) == 0) {
// 返回热门电影
return movieMapper.selectPopularMovies(10);
}
// 如果是新电影(评分不足)
List<Movie> newMovies = movieMapper.selectNewMoviesWithFewRatings();
if (!newMovies.isEmpty()) {
// 基于内容相似度推荐
Movie randomNewMovie = newMovies.get(random.nextInt(newMovies.size()));
return movieMapper.selectSimilarByContent(
randomNewMovie.getGenre(),
randomNewMovie.getDirector(),
5);
}
// 默认返回空列表
return Collections.emptyList();
}
集成Spring Boot Actuator和Prometheus实现系统监控:
yaml复制# application.yml
management:
endpoints:
web:
exposure:
include: health,info,metrics,prometheus
metrics:
tags:
application: movie-recommendation
配置日志系统,使用Logback记录关键操作:
xml复制<!-- logback-spring.xml -->
<configuration>
<appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
<file>logs/application.log</file>
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<fileNamePattern>logs/application.%d{yyyy-MM-dd}.log</fileNamePattern>
<maxHistory>30</maxHistory>
</rollingPolicy>
<encoder>
<pattern>%d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level %logger{36} - %msg%n</pattern>
</encoder>
</appender>
<logger name="com.example.movie" level="DEBUG" additivity="false">
<appender-ref ref="FILE"/>
</logger>
<root level="INFO">
<appender-ref ref="FILE"/>
</root>
</configuration>
多算法融合推荐:
用户行为分析:
微服务化改造:
移动端适配:
实际开发中发现,推荐算法的效果高度依赖数据质量。在项目初期,我们遇到了用户评分数据稀疏的问题,通过引入电影内容相似度作为辅助指标,显著提升了推荐的相关性。另外,对于高并发场景,将推荐结果预计算并缓存是必不可少的优化手段。