从零打造一个电影推荐系统:我的全栈+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的核心。我没有用复杂的协同过滤或深度学习,而是设计了一个基于用户评分的类型偏好加权算法,简单但有效。

思路

  1. 获取当前用户的所有评分,过滤掉低分(<5分)记录。
  2. 统计每个电影类型的平均评分和评分数量。比如用户给科幻片打了3部,平均8.5分;给爱情片打了2部,平均7.0分。
  3. 计算每个类型的置信度分数confidence = 平均分 × sqrt(次数)。平方根是为了让评分次数多的类型权重更高。
  4. 取置信度最高的前5个类型,从每个类型中取出10部高分电影作为候选。
  5. 对候选电影打分:score = 电影的平均分 × 该类型的置信度 × (1 + 0.3×与该用户已评分类型的重叠数)
  6. 排序后取前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_recommendmovie_qasentiment_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. 数据库设计(多对多类型)

电影和类型是多对多关系,我设计了三张表:moviesgenresmovie_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(自动登出)。
  • 组件化:虽然没用框架,但通过函数renderMovieCardrenderComments等实现了可复用的UI部件。
  • 样式:暗色主题 + 毛玻璃效果(backdrop-filter: blur) + 渐变按钮,完全手写CSS。

一个典型的页面渲染流程
用户点击“电影库” → 调用navigateTo('movies') → 显示#page-movies → 执行loadMoviesPage() → Axios请求/api/movies → 拿到数据后调用renderMoviesGrid()renderPagination() → 更新DOM。

七、运行项目(三步走)

  1. 环境准备

    • 安装MySQL,创建数据库movie_db
    • 克隆代码,在根目录创建.env文件:
      DATABASE_URL=mysql+pymysql://root:你的密码@localhost:3306/movie_db
      JWT_SECRET_KEY=随便一长串字符
      DIFY_API_KEY=你的Dify API Key(可选,不填则使用Mock)
      
  2. 安装依赖

    pip install -r requirements.txt
    
  3. 启动

    python run.py
    

    打开浏览器访问 http://127.0.0.1:8000,注册第一个用户(会自动成为管理员),然后开始体验!

八、踩过的坑与解决方案

  1. Windows上Dify API调用SSL错误
    原因是Python的httpx在某些Windows版本上对Cloudflare的SSL证书验证失败。解决:在dify_client.py中检测到Windows平台时,自动降级为curl命令调用。

  2. 多对多筛选时HAVING COUNT性能
    当类型很多时,子查询效率还行,但如果数据量大可以改用INNER JOIN多次。目前数据量小,没问题。

  3. 前端刷新后登录状态丢失
    因为token存localStorage,页面加载时从localStorage读取并恢复STATE.user即可。

  4. 评论XSS攻击
    前端渲染用户评论时用escapeHtml函数转义 < > & 等字符,确保安全。

九、未来还能怎么优化?

虽然项目已经能跑通,但我还想迭代几个版本:

  • 引入协同过滤(基于物品的CF),提升推荐准确度。
  • 对接TMDB API,自动拉取最新电影和真实海报。
  • 添加用户观影记录(想看、看过、不看)。
  • Redis缓存热门电影列表,减少数据库压力。
  • 部署到云服务器(阿里云/腾讯云),提供公网访问。

十、写在最后:给初学者的鼓励

做这个项目前后花了两周多(课余时间),过程中遇到过无数bug:数据库连接不上、JWT签名无效、前端跨域、AI返回格式错误……每次都想放弃,但最终看到自己写的网站跑起来的那一刻,真的非常有成就感。

如果你也是Python初学者,想尝试全栈+AI,我的建议是

  1. 先别追求完美,从一个小模块开始(比如先做电影列表)。
  2. 用好AI辅助工具(比如Claude code、ChatGPT),但一定要理解每一行代码。
  3. 遇到bug别慌,看报错信息,学会用搜索引擎和调试。
  4. 做完后一定要写博客或分享,输出是最好的学习。

希望我的项目能给你一些启发。如果你在实现过程中有任何问题,欢迎留言交流!

项目源码:网址:https://github.com/lcm-king/movieProject.git。


本文为原创,转载请注明出处。

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐