# 从零打造一个电影推荐系统:我的全栈+AI实战项目分享
从零打造一个电影推荐系统:我的全栈+AI实战项目分享
一个融合了FastAPI、MySQL、JWT认证、个性化推荐算法和Dify AI助手的完整Web应用
写在前面
大家好!我是一名Python初学者,最近刚完成了一个比较完整的项目——MovieRec电影推荐与评价系统。回想几个月前,我还是只会写简单Python脚本的小白,没想到现在能做出一个从前端到后端、从数据库到AI的全栈项目。今天我把整个过程和代码经验分享出来,希望能给同样在学习路上的你一些启发和信心。
项目源码已整理好,需要的同学可以按文末的方式获取。那我们开始吧!
一、为什么做这个项目?
你有没有这样的经历:周末想找部电影看,打开流媒体平台,面对成千上万部片子却不知道选哪个?评分高的不一定合自己口味,热门的不一定感兴趣。如果有个系统能根据我的喜好推荐电影,还能让我评分、写评论、和AI对话,那该多好?
于是,MovieRec诞生了。
二、项目能做什么?
用一个动图可能更直观,但这里先用文字描述一下核心功能:
- 用户系统:注册、登录、JWT认证,管理员可以管理用户。
- 电影库:分页展示、按类型/评分/年份筛选、关键词搜索。
- 评分与评论:对电影打1-10分,写文字评论,评论支持情感分析。
- 个性化推荐:根据你评过分的电影,自动分析你喜欢的类型,推荐同类型高分电影。
- AI助手:一个统一对话框,可以用自然语言问它“推荐一部温馨的爱情片”、“《肖申克的救赎》结局什么意思?”、“分析一下‘这部电影太震撼了’的情感”。
- 管理后台:管理员可以封禁用户、增删改电影、删除不当评论。
三、技术栈概览
| 层级 | 技术 |
|---|---|
| 后端框架 | FastAPI(异步、自动文档、类型提示) |
| 数据库 | MySQL + SQLAlchemy ORM |
| 认证 | JWT + bcrypt(密码哈希) |
| AI集成 | Dify API(工作流) + 本地意图降级 |
| 前端 | 原生HTML/CSS/JS + Axios |
| 样式 | 现代暗色主题、毛玻璃效果、响应式 |
| 部署 | Uvicorn本地运行 |
为什么用原生JS而不是React/Vue?因为我想从基础理解前端交互,而且初学者用原生JS更直观。
四、项目结构(一目了然)
movieProject/
├── app/ # 后端核心
│ ├── main.py # FastAPI入口、CORS、静态文件
│ ├── database.py # SQLAlchemy连接
│ ├── models.py # 表结构(User, Movie, Genre, Rating, Comment)
│ ├── schemas.py # Pydantic模型(请求/响应格式)
│ ├── auth.py # JWT生成/验证、密码哈希
│ ├── crud.py # 数据库操作(增删改查)
│ ├── dify_client.py # Dify API调用 + 本地智能降级
│ ├── config.py # 环境变量配置
│ └── routers/ # 路由模块
│ ├── users.py # 注册、登录、个人中心
│ ├── movies.py # 电影列表、详情、评分分布
│ ├── ratings.py # 评分提交
│ ├── comments.py # 评论增删查
│ ├── recommendations.py # 个性化推荐算法
│ ├── ai.py # AI聊天、推荐、问答、情感分析
│ └── admin.py # 管理员接口
├── static/ # 前端静态文件
│ ├── index.html # 单页面主模板
│ ├── css/style.css # 所有样式(暗色主题、响应式)
│ └── js/ # JS模块化
│ ├── api.js # Axios配置、拦截器
│ ├── auth.js # 登录/注册/登出
│ ├── router.js # SPA路由、全局状态
│ ├── movies.js # 电影列表、详情、评分评论
│ ├── recommendations.js # 推荐页
│ ├── ai.js # AI聊天界面逻辑
│ ├── admin.js # 管理后台
│ ├── profile.js # 个人中心
│ └── utils.js # 通用函数(toast、escapeHtml等)
├── requirements.txt # Python依赖
├── .env # 敏感配置(数据库密码、JWT密钥、Dify Key)
└── run.py # 启动脚本
这种模块化分离让代码非常容易维护和扩展。
五、核心功能实现详解
1. 用户认证(JWT + bcrypt)
初学者可能觉得认证很复杂,其实核心就是三件事:
- 注册:前端发来用户名、密码、邮箱、偏好类型 → 后端用bcrypt哈希密码 → 存入数据库 → 生成JWT返回。
- 登录:验证密码 → 生成JWT返回。
- 后续请求:前端在
Authorization: Bearer <token>头中带上JWT → 后端验证签名并解析出user_id → 从数据库查出用户信息。
关键代码(auth.py):
def create_access_token(data: dict) -> str:
expire = datetime.utcnow() + timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES)
to_encode = data.copy()
to_encode.update({"exp": expire})
return jwt.encode(to_encode, JWT_SECRET_KEY, algorithm=JWT_ALGORITHM)
def get_current_user(credentials: HTTPAuthorizationCredentials = Depends(security), db: Session = Depends(get_db)):
token = credentials.credentials
payload = jwt.decode(token, JWT_SECRET_KEY, algorithms=[JWT_ALGORITHM])
user_id = payload.get("user_id")
user = db.query(User).filter(User.id == user_id).first()
if not user or not user.is_active:
raise HTTPException(status_code=403)
return user
小贴士:JWT不需要在服务端存储session,很适合前后端分离。
2. 个性化推荐算法(这是项目亮点)
推荐算法是MovieRec的核心。我没有用复杂的协同过滤或深度学习,而是设计了一个基于用户评分的类型偏好加权算法,简单但有效。
思路:
- 获取当前用户的所有评分,过滤掉低分(<5分)记录。
- 统计每个电影类型的平均评分和评分数量。比如用户给科幻片打了3部,平均8.5分;给爱情片打了2部,平均7.0分。
- 计算每个类型的置信度分数:
confidence = 平均分 × sqrt(次数)。平方根是为了让评分次数多的类型权重更高。 - 取置信度最高的前5个类型,从每个类型中取出10部高分电影作为候选。
- 对候选电影打分:
score = 电影的平均分 × 该类型的置信度 × (1 + 0.3×与该用户已评分类型的重叠数)。 - 排序后取前10,同时保证类型多样性(同一个类型最多3部)。
代码片段(recommendations.py):
# 构建用户类型画像
for r in user_ratings:
if r.rating < 5: continue
genre_names = crud.get_movie_genre_names(db, r.movie_id)
for g in genre_names:
if g not in genre_profile:
genre_profile[g] = {"sum": 0.0, "count": 0}
genre_profile[g]["sum"] += r.rating
genre_profile[g]["count"] += 1
# 计算置信度
for g, d in genre_profile.items():
avg = d["sum"] / d["count"]
confidence = avg * (d["count"] ** 0.5)
scored_genres.append((g, avg, confidence, d["count"]))
# 生成候选电影,并打分
candidates = []
for genre, avg_rating, confidence, count in scored_genres[:5]:
movies = crud.get_top_movies_by_genre(db, genre, limit=10)
for movie in movies:
overlap = len(set(get_genres(movie.id)) & set(genre_profile.keys()))
score = movie.avg_rating * confidence * (1 + 0.3 * overlap)
candidates.append(...)
结果示例:
“因为你给科幻类电影打过高分(平均8.5分/3部)”
这样的推荐理由可解释性强,用户能明白为什么推荐这部电影。
3. AI助手(统一聊天 + 三条工作流)
这是另一个亮点。我没有简单套用Dify的API,而是做了一个本地意图优先,Dify降级,最后Mock保底的三层智能架构。
-
第一层:本地意图识别(
_try_local_intent)
当用户说“你好”、“推荐科幻片”、“肖申克的救赎结局是什么意思”,直接在本地的正则+关键词匹配中返回答案,零延迟、不消耗API。 -
第二层:Dify工作流
如果本地未匹配,且有Dify API Key,则调用三个预定义工作流:movie_recommend、movie_qa、sentiment_analysis。每个工作流输入结构化的电影数据或评论,返回AI生成的结果。 -
第三层:Mock降级
当没有API Key或网络故障时,自动返回模拟答案(基于规则,但依然智能)。这样即使离线也能演示核心功能。
统一聊天接口(ai.py):
@router.post("/chat")
async def ai_chat(data: AIChatRequest, current_user: User = Depends(get_current_user), db: Session = Depends(get_db)):
movies = crud.get_movies_for_ai(db, limit=50)
movies_data = [{"title": m.title, "year": m.release_year, "genre": m.genre, "rating": m.avg_rating, "description": m.description[:200]} for m in movies]
return await unified_chat(data.message, movies_data)
unified_chat函数会依次尝试本地匹配、Dify、Mock,并返回回答及来源。
前端体验:一个聊天界面,无需切换模式,就像和真人对话。输入后显示“正在输入…”动画,返回的文本支持**粗体**和换行。
4. 数据库设计(多对多类型)
电影和类型是多对多关系,我设计了三张表:movies、genres、movie_genres(关联表)。这样查询“同时包含动作和科幻的电影”时,可以用SQL的HAVING COUNT技巧。
关键查询(crud.py):
sub = (
db.query(MovieGenre.movie_id)
.filter(MovieGenre.genre_id.in_(genre_ids))
.group_by(MovieGenre.movie_id)
.having(func.count(distinct(MovieGenre.genre_id)) == len(genre_ids))
.subquery()
)
query = query.join(sub, Movie.id == sub.c.movie_id)
此外,还使用了UniqueConstraint保证每个用户对每部电影只有一条评分。
六、前端是如何工作的?
我是一个SPA(单页面应用),所有页面切换靠navigateTo函数动态显示/隐藏不同section,没有使用React/Vue框架。
- 状态管理:一个全局
STATE对象,存储当前用户、当前页码、筛选条件等。 - API调用:通过Axios实例自动在请求头添加JWT,并统一处理401(自动登出)。
- 组件化:虽然没用框架,但通过函数
renderMovieCard、renderComments等实现了可复用的UI部件。 - 样式:暗色主题 + 毛玻璃效果(
backdrop-filter: blur) + 渐变按钮,完全手写CSS。
一个典型的页面渲染流程:
用户点击“电影库” → 调用navigateTo('movies') → 显示#page-movies → 执行loadMoviesPage() → Axios请求/api/movies → 拿到数据后调用renderMoviesGrid()和renderPagination() → 更新DOM。
七、运行项目(三步走)
-
环境准备
- 安装MySQL,创建数据库
movie_db。 - 克隆代码,在根目录创建
.env文件:DATABASE_URL=mysql+pymysql://root:你的密码@localhost:3306/movie_db JWT_SECRET_KEY=随便一长串字符 DIFY_API_KEY=你的Dify API Key(可选,不填则使用Mock)
- 安装MySQL,创建数据库
-
安装依赖
pip install -r requirements.txt -
启动
python run.py打开浏览器访问
http://127.0.0.1:8000,注册第一个用户(会自动成为管理员),然后开始体验!
八、踩过的坑与解决方案
-
Windows上Dify API调用SSL错误
原因是Python的httpx在某些Windows版本上对Cloudflare的SSL证书验证失败。解决:在dify_client.py中检测到Windows平台时,自动降级为curl命令调用。 -
多对多筛选时
HAVING COUNT性能
当类型很多时,子查询效率还行,但如果数据量大可以改用INNER JOIN多次。目前数据量小,没问题。 -
前端刷新后登录状态丢失
因为token存localStorage,页面加载时从localStorage读取并恢复STATE.user即可。 -
评论XSS攻击
前端渲染用户评论时用escapeHtml函数转义< > &等字符,确保安全。
九、未来还能怎么优化?
虽然项目已经能跑通,但我还想迭代几个版本:
- 引入协同过滤(基于物品的CF),提升推荐准确度。
- 对接TMDB API,自动拉取最新电影和真实海报。
- 添加用户观影记录(想看、看过、不看)。
- 用Redis缓存热门电影列表,减少数据库压力。
- 部署到云服务器(阿里云/腾讯云),提供公网访问。
十、写在最后:给初学者的鼓励
做这个项目前后花了两周多(课余时间),过程中遇到过无数bug:数据库连接不上、JWT签名无效、前端跨域、AI返回格式错误……每次都想放弃,但最终看到自己写的网站跑起来的那一刻,真的非常有成就感。
如果你也是Python初学者,想尝试全栈+AI,我的建议是:
- 先别追求完美,从一个小模块开始(比如先做电影列表)。
- 用好AI辅助工具(比如Claude code、ChatGPT),但一定要理解每一行代码。
- 遇到bug别慌,看报错信息,学会用搜索引擎和调试。
- 做完后一定要写博客或分享,输出是最好的学习。
希望我的项目能给你一些启发。如果你在实现过程中有任何问题,欢迎留言交流!
项目源码:网址:https://github.com/lcm-king/movieProject.git。
本文为原创,转载请注明出处。
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐


所有评论(0)