尧图网络科技YAOTU DIGITAL 获取报价
获取报价
首页 / 资讯中心 / 文章详情

Flask从零搭建在线电影网站:数据建模、用户认证与部署实战

发布时间:2026/9/14 3:19:58

资讯中心
01
ARTICLE

Flask从零搭建在线电影网站:数据建模、用户认证与部署实战

Flask从零搭建在线电影网站:数据建模、用户认证与部署实战
简介这是一份基于Python Flask框架开发的在线电影网站项目适合Python Web初学者、毕业设计及全栈项目实训人群可帮助理解Flask路由、模板渲染与前后端数据交互流程。压缩包共2000个文件约20.67MB主要涵盖html/css/js等前台页面、py后端逻辑、png/svg/gif等静态素材以及bootstrap/AdminLTE等前端框架资源内容预览中可见完整的模板样式体系整体结构清晰便于按模块阅读与二次开发。已有890人学习下载适合作为课程设计或项目实战参考。通过该资源可获得整套电影网站源码、前端界面与后端接口设计能直接运行体验也可对照源码梳理Web开发常用技术栈快速搭建功能型在线电影平台。1. 在线电影网站用 Flask 搭起来到底要解决什么问题做在线电影网站最常被卡住的不是播放器也不是服务器带宽而是项目结构路由、模板、数据库、登录状态混在一个文件里功能一多就改不动。Flask 对这种规模的项目恰好合适——它不替你决定目录结构也不把 ORM 强塞给你但周边生态足够成熟把 SQLite、SQLAlchemy、Flask-Login 拼起来就能搭出一个带用户系统、能搜索、能记录播放进度的站点。下面以 Flask 搭建在线电影网站为主线从最小骨架开始逐步加入数据建模、注册登录、播放记录、搜索与部署细节。新手可以照着操作把项目跑起来能写 CRUD 的熟手重点看第 3 章的查询优化和第 5 章部署前的配置项。2. 先建最小可运行的 Flask 电影站骨架2.1 用虚拟环境和依赖清单开局为什么不用全局安装我一般不会把 Flask 装到系统 Python 里。原因很简单项目依赖是组合出来的Flask 2.x 和 3.x 的配置方式有差别SQLAlchemy 的版本又影响查询语法全装到全局换一个项目就要升级一次环境带来不必要的冲突。在线电影网站这类项目依赖不多但 Flask、SQLAlchemy、Login 三个库之间仍然存在版本联动用虚拟环境隔离是最省事的做法。无论你是在 Windows 上照着 python 下载安装教程配好的环境还是跟着 vscode python 环境配置把解释器指到了项目目录后面的命令都一致。在项目根目录执行mkdir movie-site cd movie-site python -m venv venv source venv/bin/activate # Windows 下用 venv\Scripts\activate pip install Flask3.0.* Flask-SQLAlchemy3.1.* Flask-Login0.6.* pip freeze requirements.txtvenv 是 Python 3.3 自带模块不依赖额外工具source venv/bin/activate进入虚拟环境Windows 下换成venv\Scripts\activate。pip freeze requirements.txt生成依赖清单后换机器时一条pip install -r requirements.txt就能恢复环境。下表列出了三件事各自的用途操作作用说明python -m venv venv创建独立解释器目录项目级隔离不污染全局pip install ...安装运行依赖版本用3.0.*锁定主版本pip freeze requirements.txt固化依赖快照部署时按此还原版本策略上我习惯用3.0.*锁住主版本给小版本留补丁空间如果团队协作或要上线打包再换成pip freeze输出的完整版本号。用 PyCharm 时记得在 Settings 里把项目解释器指到 venv 目录否则终端能运行、IDE 里却报ModuleNotFoundError。2.2 用应用工厂把模型、路由和模板串起来常见的错误写法是把 db 和 login_manager 定义在单个 app.py 里然后用一个全局 app 变量到处引用。项目一旦拆成多个模块循环导入就会变成日常。应用工厂的用意是把创建 app 的过程封装成一个函数扩展先实例化、后绑定目录结构也清晰# app/__init__.py from flask import Flask from flask_sqlalchemy import SQLAlchemy from flask_login import LoginManager db SQLAlchemy() login_manager LoginManager() def create_app(config_pathNone): app Flask(__name__) app.config.from_pyfile(config_path or config.py) db.init_app(app) login_manager.init_app(app) login_manager.login_view auth.login from .routes import main, auth, movie app.register_blueprint(main) app.register_blueprint(auth) app.register_blueprint(movie) with app.app_context(): from . import models db.create_all() return app注意看延迟导入routes和models都在 create_app 内部导入目的是避免模块还在加载时互相引用。login_manager.login_view auth.login指定未登录用户跳转到登录页Flask-Login 会把目标地址放在next参数里登录后再跳回去。db.create_all()很适合开发阶段因为它不会修改已存在的表所以改动字段之后要自己处理迁移正规项目后期要上 Flask-Migrate这里先用它保持最小依赖。提示config.py里至少要写SECRET_KEY和SQLALCHEMY_DATABASE_URI。本地开发一般用sqlite:///movie.dbFlask 会按相对路径在实例目录下生成文件。2.3 把电影列表和详情页的路由模板先跑通应用工厂搭好后先写两个最简单、也最能验证项目结构的 Blueprint首页返回电影列表详情页返回单部电影。# app/routes/movie.py from flask import Blueprint, render_template, abort from app import db from app.models import Movie movie_bp Blueprint(movie, __name__) movie_bp.route(/) def index(): movies Movie.query.order_by(Movie.created_at.desc()).limit(12).all() return render_template(movie/index.html, moviesmovies) movie_bp.route(/movie/int:movie_id) def detail(movie_id): movie db.session.get(Movie, movie_id) if movie is None: abort(404) return render_template(movie/detail.html, moviemovie)模板继承在这里就该用上否则每个页面都要重复写 header 和 footer!-- templates/base.html -- !doctype html html head meta charsetutf-8 title{% block title %}在线电影{% endblock %}/title /head body nav a href{{ url_for(movie.index) }}首页/a a href{{ url_for(movie.index) }}全部电影/a /nav {% block content %}{% endblock %} /body /html详情页模板里的播放器是核心。preloadmetadata是视频站点的常用设置页面加载时只取视频头部元数据不提前缓冲正片首屏速度会明显更快!-- templates/movie/detail.html -- {% extends base.html %} {% block content %} video controls poster{{ movie.cover }} preloadmetadata source src{{ movie.video_url }} typevideo/mp4 你的浏览器不支持 video 标签。 /video h1{{ movie.title }}/h1 p分类{{ movie.category.name }} · 年份{{ movie.year }}/p {% endblock %}到这里浏览器输入http://127.0.0.1:5000/就能看到页面。如果出现模板找不到先检查 templates 目录层级Flask 默认从项目根目录下的 templates 查找应用工厂没有额外指定template_folder时模板不能塞进包内部的子目录。3. 用 SQLite SQLAlchemy 管理电影数据别把数据写死在内存里3.1 电影、分类、用户三张表怎么设计SQLite 文件型数据库适合电影站开发阶段和中小型站点省去安装数据库服务的成本。但要按正规方式建表不然后期换库会很痛苦。三张表对应三个业务对象分类、电影、用户。# app/models.py from datetime import datetime from flask_login import UserMixin from werkzeug.security import generate_password_hash from . import db, login_manager class Category(db.Model): __tablename__ categories id db.Column(db.Integer, primary_keyTrue) name db.Column(db.String(32), uniqueTrue) movies db.relationship(Movie, back_populatescategory, lazydynamic) class Movie(db.Model): __tablename__ movies id db.Column(db.Integer, primary_keyTrue) title db.Column(db.String(128), indexTrue) category_id db.Column(db.Integer, db.ForeignKey(categories.id)) cover db.Column(db.String(255)) video_url db.Column(db.String(255)) rating db.Column(db.Float, default0) play_count db.Column(db.Integer, default0) year db.Column(db.Integer) created_at db.Column(db.DateTime, defaultdatetime.utcnow) category db.relationship(Category, back_populatesmovies) class User(UserMixin, db.Model): __tablename__ users id db.Column(db.Integer, primary_keyTrue) username db.Column(db.String(32), uniqueTrue) password_hash db.Column(db.String(128)) login_manager.user_loader def load_user(user_id): return db.session.get(User, int(user_id))这里有几个选型上的考虑。category_id外键没有写ondeleteCASCADE这是有意的误删分类时不希望把整个分类下的电影也清掉真需要清理时改成软删除更安全。play_count用来做热门排序rating用来做推荐排序两个字段都是冗余但值得因为每次推荐都去统计播放表开销太大。User表先建在这里后面章节才用到它但密码字段必须和登录逻辑分开后续详述。3.2 用种子数据脚本填充初始数据在线电影网站的运营流程里数据要么来自后台录入要么来自采集脚本开发阶段直接写一组种子数据最方便。网上很多免费 Python 源码里都能看到类似结构但大部分只给了建表和插入没有处理重复运行的问题# scripts/seed.py from app import create_app, db from app.models import Category, Movie app create_app() with app.app_context(): cate Category.query.filter_by(name动作).first() if cate is None: cate Category(name动作) db.session.add(cate) db.session.flush() # 提前拿到分类 id db.session.add(Movie( title示例影片, categorycate, cover/static/covers/demo.jpg, video_url/static/videos/demo.mp4, rating8.5, year2024, )) db.session.commit() print(seed done)flush()的作用是把 INSERT 发送到数据库但暂不提交这样cate.id马上可用如果直接把cate传给relationship其实也可以ORM 会在提交时补上外键但后续要拼接字符串时就必须提前 flush。种子脚本重复运行时先检查分类是否存在否则每跑一次就多一条重复数据。封面图处理上开发阶段用项目内静态文件路径没问题video_url也可以先指到本地测试视频。生产环境视频体量大不建议塞进项目目录常见做法是放到对象存储或独立文件服务数据库里只存 URL。3.3 列表页分页与避免 N1 查询等电影数量超过几十条列表页最容易踩到的是 N1 查询模板里访问movie.category.name每部电影都会触发一条 SQL。加joinedload是最直接的解法from sqlalchemy.orm import joinedload page request.args.get(page, 1, typeint) pagination Movie.query \ .options(joinedload(Movie.category)) \ .order_by(Movie.created_at.desc()) \ .paginate(pagepage, per_page12) movies pagination.itemsjoinedload会把查询改写成 LEFT OUTER JOIN 一次取出分类数据。request.args.get(page, 1, typeint)里typeint就是 Python 类型转换的典型用法用户传?pageabc时自动回落成默认值 1不会抛 400。模板翻页条对应结构如下{% for movie in movies %} a href{{ url_for(movie.detail, movie_idmovie.id) }} img src{{ movie.cover }} loadinglazy p{{ movie.title }}/p /a {% endfor %} {% if pagination.has_prev %} a href{{ url_for(movie.index, pagepagination.prev_num) }}上一页/a {% endif %}paginate的几个参数可以记一下参数作用常见值page当前页码从request.args取per_page每页条数12 或 20error_out越界是否报 404设为 False 可自定义空页需要注意joinedload适合加载数据不适合做过滤。如果后面要按分类名过滤电影应该改用join而不是joinedload否则关联表的过滤条件会让结果集出错这是 SQLAlchemy 新手最容易踩的边界。4. 用户注册、登录与播放记录Flask-Login 的接入方式4.1 密码哈希、session 与登录状态的分离不要把密码明文存数据库也不要自己设计session[is_login] True这种方案。常见的做法是用 Werkzeug 的密码哈希函数登录状态交给 Flask-Login 管理它只把user_id写进 session用户是否登录由服务端根据 session 解密结果判断不依赖任何可被前端伪造的标记。# config.py SECRET_KEY dev-secret-change-me SQLALCHEMY_DATABASE_URI sqlite:///movie.db SQLALCHEMY_TRACK_MODIFICATIONS FalseUser 模型里有个password_hash字段注册时用generate_password_hash(password)生成哈希再入库登录时用check_password_hash(user.password_hash, password)比对。真实项目里 SECRET_KEY 要用随机数生成的长字符串否则 session 可以被反解伪造。load_user 已经在 models.py 里写好了它是 Flask-Login 从 session 恢复用户对象的入口UserMixin提供is_authenticated、is_anonymous等属性模板里就能直接判断current_user。4.2 注册与登录路由实现注册和登录路由是标准的 POST/GET 双方法处理重点在参数校验和重定向安全# app/routes/auth.py from flask import (Blueprint, render_template, redirect, url_for, flash, request) from flask_login import login_user, logout_user, login_required, current_user from werkzeug.security import generate_password_hash, check_password_hash from app import db from app.models import User auth_bp Blueprint(auth, __name__) auth_bp.route(/register, methods[GET, POST]) def register(): if request.method POST: username request.form.get(username, ).strip() password request.form.get(password, ) if len(username) 3 or len(password) 6: flash(用户名至少 3 位密码至少 6 位, danger) elif User.query.filter_by(usernameusername).first(): flash(用户名已存在, danger) else: user User(usernameusername, password_hashgenerate_password_hash(password)) db.session.add(user) db.session.commit() login_user(user) return redirect(url_for(movie.index)) return render_template(auth/register.html) auth_bp.route(/login, methods[GET, POST]) def login(): if request.method POST: username request.form.get(username, ).strip() password request.form.get(password, ) user User.query.filter_by(usernameusername).first() if user and check_password_hash(user.password_hash, password): login_user(user, rememberremember in request.form) next_url request.args.get(next) if next_url and next_url.startswith(/) and not next_url.startswith(//): return redirect(next_url) return redirect(url_for(movie.index)) flash(用户名或密码错误, danger) return render_template(auth/login.html) auth_bp.route(/logout) login_required def logout(): logout_user() return redirect(url_for(movie.index))这里有两个容易被忽略的细节。第一rememberremember in request.form会把 remember cookie 的有效期拉长用户勾选“记住我”后关闭浏览器再打开仍然保持登录不勾选时 session 随浏览器会话结束而失效。第二next参数如果直接重定向可能被构造钓鱼跳转常见做法是只允许站内相对路径也就是上面代码里的startswith(/)且不以//开头。模板里配合get_flashed_messages显示提示在 base.html 加一段即可{% with messages get_flashed_messages(with_categoriestrue) %} {% for category, message in messages %} div classalert alert-{{ category }}{{ message }}/div {% endfor %} {% endwith %}4.3 把播放进度记到库里断点续播在线电影网站常见的体验是看到一半刷新后接着播。实现逻辑不复杂前端 video 触发 timeupdate 时向后端上报播放秒数播放页初始化时读取记录设置 currentTime。先加一张进度表# app/models.py 追加 class PlayHistory(db.Model): __tablename__ play_history id db.Column(db.Integer, primary_keyTrue) user_id db.Column(db.Integer, db.ForeignKey(users.id), indexTrue) movie_id db.Column(db.Integer, db.ForeignKey(movies.id), indexTrue) position db.Column(db.Integer, default0) updated_at db.Column(db.DateTime, defaultdatetime.utcnow, onupdatedatetime.utcnow)读写接口各一个读取用 GET保存用 POSTmovie_bp.route(/movie/int:movie_id/resume) login_required def resume_position(movie_id): h PlayHistory.query.filter_by( user_idcurrent_user.id, movie_idmovie_id ).first() return {position: h.position if h else 0} movie_bp.route(/api/progress, methods[POST]) login_required def save_progress(): data request.get_json(forceTrue) movie_id int(data.get(movie_id, 0)) position int(data.get(position, 0)) h PlayHistory.query.filter_by( user_idcurrent_user.id, movie_idmovie_id ).first() if h is None: h PlayHistory(user_idcurrent_user.id, movie_idmovie_id) db.session.add(h) h.position position db.session.commit() return {ok: True}前端配合video的两个事件注意加防抖const video document.getElementById(player); video.addEventListener(loadedmetadata, async () { const res await fetch(/movie/${video.dataset.movieId}/resume); const data await res.json(); if (data.position 30) { video.currentTime data.position; } }); let savedFlag 0; video.addEventListener(timeupdate, () { if (video.currentTime - savedFlag 5) { savedFlag video.currentTime; fetch(/api/progress, { method: POST, headers: { Content-Type: application/json }, body: JSON.stringify({ movie_id: video.dataset.movieId, position: Math.floor(video.currentTime) }) }); } });timeupdate 事件每秒钟会触发多次每次都发请求是浪费5 秒一次的判断足够体感连续。position 30才续播是为了避免刚打开页面就自动跳走比如用户只是想看片头结果被拉到中间。request.get_json(forceTrue)开发阶段方便生产环境下建议去掉 force只接受正确的 Content-Type。5. 搜索、推荐与部署前必查的三个细节5.1 搜索用 LIKE 还是全文检索小项目用contains就够它生成的 SQL 是LIKE %keyword%千级数据量没有任何问题。如果硬要上 Elasticsearch反而要先解决同步、分词、部署成本三个问题这对小型电影站是负收益q request.args.get(q, ).strip() movies [] if q: movies Movie.query \ .filter(Movie.title.contains(q)) \ .order_by(Movie.play_count.desc()) \ .limit(20).all()contains会自动处理百分号和通配符转义避免用户输入%时把整个表扫出来。排序用play_count而不是created_at因为搜索结果里用户更在意热门内容。等数据量上万并且要把导演、简介一起搜时SQLite 可以换 FTS5否则维持 LIKE 就好。5.2 相似电影推荐同分类按评分取前几条详情页底部的“猜你喜欢”不用做得很玄同分类按评分和播放量取前几条是常见做法recommend Movie.query.filter( Movie.category_id movie.category_id, Movie.id ! movie.id ).order_by(Movie.rating.desc(), Movie.play_count.desc()).limit(6).all()评分相同时用 play_count 打破平局模板里循环输出即可。这个方案足够在起步阶段撑住推荐位等用户行为数据积累起来再考虑基于协同过滤的算法。5.3 部署前必须改的三个配置项上线时先改配置再换运行方式。开发时flask run带的服务器不适合对外提供服务常见做法是用 Gunicorn 起多进程。先确认 Linux 系统安装 Python 后能正常执行python3 -m venv再把配置补齐SECRET_KEY 使用随机数生成的长字符串 SQLALCHEMY_TRACK_MODIFICATIONS False SESSION_COOKIE_HTTPONLY True REMEMBER_COOKIE_HTTPONLY True启动命令gunicorn -w 2 -b 0.0.0.0:8000 app:create_app()-w 2表示两个 worker 进程参考值是 CPU 核数不是越多越好因为 SQLite 对并发写入有锁worker 再多也会在写库时排队。SESSION_COOKIE_HTTPONLY True防止前端 JS 读取 session cookie减少被脚本窃取的可能。上线前我会在服务器上把这几项写进 config.py再跑一遍注册、登录和断点续播流程确认 session 和进度接口没有因配置变动而失效。本文还有配套的精品资源点击获取
02
RELATED NEWS

相关资讯

更多网站建设与数字化升级内容

03
WHY YAOTU

想打造同款高转化官网?

懂行业、懂生意,从建站到增长一站式陪跑

场景化定制

不做模板站,围绕你的业务场景量身设计,小众不撞款。

营销型架构

以转化目标组织内容与路径,让官网真正带来询盘。

全周期服务

设计、开发、运营、运维一体,上线只是开始。

免费获取你的建站方案

留下需求,专属顾问 24 小时内为你输出方案建议。