简介:一套基于Flask框架的电影管理系统源码,面向有一定Python基础的Web开发人员与Flask入门者,帮助理解后台管理系统从路由分发、数据模型、权限认证到服务部署的完整实现路径。压缩包共55个文件,以47个Python源文件为主,涵盖用户、电影、影院、订单、后台管理等核心业务模块,并包含settings、config、constants等配置类文件,以及gunicorn、supervisord、Dockerfile、entrypoint.sh等部署与进程守护配置,整体仅102KB,目录分层明确。项目采用蓝图加中间件的组织方式,在common中贡献了JWT工具、输出封装、类型转换、数据库连接等可复用组件,便于学习Flask工程化写法与接口鉴权逻辑。同时,admin.py与task模块体现了后台任务和权限控制的典型场景,可作为课程设计、毕业设计或小型管理系统的参考蓝本。目前已有293人学习下载,适合用来快速验证Flask项目架构并上手实战。
1. 为什么“基于Flask框架的电影管理系统源码”值得自己动手拆一遍
在开源社区、毕设仓库和接单市场上,“基于Flask框架的电影管理系统源码”是出现频率极高的一类项目。十份里八份是同一个套路:Flask + SQLite + Jinja2 模板,前台做列表和详情,后台做增删改查,最后套一个 Bootstrap 页面。问题是,这类源码大多能跑起来,却经不起追问:用户表没有唯一约束,评论被人用DELETE请求直接删,评分统计数据每次请求都全表扫描。把这些源码当成学习材料去拆,比从零写一遍收获更大,因为你能看到别人在真实开发里怎么设计表、怎么拆蓝图、怎么处理授权边界。
这套系统适合三类人:刚把 Python 语法过完、想用一个完整项目把 Flask 开发串起来的初学者;要做毕业设计或课程设计、需要一个可扩展骨架的学生;以及想快速搭建内部演示系统、但又不想碰 Django 重框架的工程师。我这个标题聊的“源码”不是让你一键复制部署,而是把一套标准电影管理系统的表结构、权限模型、查询优化和部署方式拆开,给你一份可以直接照着改的工程化答案。
2. 从零跑通一套最小可用的Flask电影管理系统:目录、数据表与模型设计
2.1 项目结构先定下来:Web 开发决定后期改代码的心情
拿到一套 Flask 电影管理系统源码,第一件事不是看代码,是看目录。常见做法是两种:单文件app.py全塞进去,或者用包结构拆开。单文件适合 200 行以内的演示脚本,但电影管理系统至少要包含用户、电影、评分、评论、管理员后台这五块,全塞一个文件里后期加功能就是灾难。我一般会推荐下面的最小包结构:
flask_movie/ ├── run.py # 启动入口,设置环境变量并调用 create_app() ├── config.py # 配置:SECRET_KEY、SQLALCHEMY_DATABASE_URI ├── requirements.txt # 依赖清单 ├── app/ │ ├── __init__.py # create_app() 工厂函数,注册所有蓝图 │ ├── models.py # SQLAlchemy 模型定义 │ ├── auth.py # 登录、注册、登出蓝图 │ ├── main.py # 首页、电影列表、搜索蓝图 │ ├── admin.py # 管理后台蓝图 │ └── templates/ │ ├── base.html │ ├── index.html │ ├── login.html │ └── admin/ └── instance/ └── movie.db # SQLite 文件,运行后自动生成run.py不要直接实例化Flask,而是调用工厂函数,这样测试和迁移工具都能拿到同一个应用实例。配置单独放config.py,数据库 URI 写sqlite:///movie.db时注意路径相对的是instance目录,这是 Flask 3.x 的默认行为,很多旧教程没提这一点,导致新手会问“数据库文件到底生成在哪”。requirements.txt锁住 Flask、Flask-SQLAlchemy、Flask-Login 这三个核心包即可,迁移工具可以后补。
2.2 数据表设计:电影、分类、评分、评论之间的关联关系
电影管理系统的核心表一般是四张:用户表、分类表、电影表、评论表。如果把评分也拆出来,就是五张。我见过很多源码把评分字段直接设计成电影表里的一个fload列,然后每次有人打分就 UPDATE 一次,最后连评分人数都没有,排行榜只能按豆瓣爬来的分数排,这是典型的表设计没想清楚。正确做法是评分独立成表,一个用户对一部电影只能评分一次,用联合唯一约束保证。
| 表名 | 关键字段 | 索引与约束 |
|---|---|---|
| users | id, username, password_hash, is_admin, created_at | username 唯一索引 |
| categories | id, name, sort_order | name 唯一 |
| movies | id, title, category_id, release_date, rating_avg, rating_count, description, cover_url | category_id 外键索引,rating_avg 普通索引 |
| ratings | id, user_id, movie_id, score, created_at | (user_id, movie_id) 联合唯一 |
| comments | id, movie_id, user_id, content, created_at | movie_id 外键索引 |
这里的关键点在于:rating_avg和rating_count虽然是冗余字段,但在读多写少的业务里是值得的。每次查询首页电影列表时,如果都要去 ratings 表做实时聚合,数据量到十万条级别就会有明显延迟。更稳妥的做法是:评分写入时同步更新 movies 表里的这两个字段,查询时直接取,这样排行榜排序也能直接走索引。后文会给出这部分的 SQL 写法。
2.3 用 Flask-SQLAlchemy 定义模型,避免表名自动生成的坑
定义模型时最容易被忽略的是__tablename__。Flask-SQLAlchemy 默认会把类名转成蛇形命名,比如MovieCategory会变成movie_category,但如果你从别的系统搬表结构过来,或者改了类名没改表名,迁移时就会直接建新表。显式写上表名,既是自文档化,也避免意外。
from flask_sqlalchemy import SQLAlchemy from werkzeug.security import generate_password_hash, check_password_hash from datetime import datetime db = SQLAlchemy() class User(db.Model): __tablename__ = "users" id = db.Column(db.Integer, primary_key=True) username = db.Column(db.String(80), unique=True, nullable=False) password_hash = db.Column(db.String(256), nullable=False) is_admin = db.Column(db.Boolean, default=False) created_at = db.Column(db.DateTime, default=datetime.now) def set_password(self, password): self.password_hash = generate_password_hash(password) def check_password(self, password): return check_password_hash(self.password_hash, password) # 注意:这里不创建 comments 的 relationship # 电影系统里用户和评论的关系用 query 时再 join 即可set_password 放在模型里而不是视图函数里,能保证所有创建用户的入口都走同一套哈希逻辑。Werkzeug 的generate_password_hash默认使用scrypt算法,这比很多源码里直接存的 MD5 安全得多。Flask 老版本里SQLAlchemy的初始化方式是把db = SQLAlchemy(app)写在主文件里,工厂模式要求先创建db对象,在create_app()里调用db.init_app(app),这个区别决定了你能否把模型拆到独立文件。
电影表的模型定义里,容易出问题的不是字段,而是relationship的写法。常见源码里会写movies = db.relationship("Movie", backref="category"),看起来方便,但在删除分类时会把电影一并置空或报错。所以删除策略,我放在第 4 章后台部分讲。
写模型还要注意时间字段的默认值。default=datetime.now是接收函数本身,default=datetime.now()是在导入模块时就把时间固定住了,这个细节会让同一批创建的数据时间几乎一致,排查问题时会误导人。表结构定完,下一步就是把它跑起来。
3. 登录、权限与蓝图拆分:让源码能经得住二次开发的关键
3.1 登录方案选型:Flask-Login、Session 还是自写 JWT?
电影管理系统源码里最常见的登录实现是把用户 id 存进 session,然后每个视图函数开头写一句if "user_id" not in session:,看起来简单,三个问题立刻出现:没有记住登录状态的有效期控制;登出时要手动清 session,容易漏;装饰器是重复代码,不同权限的函数要粘贴两遍逻辑。
常见且可靠的做法是上 Flask-Login,它只解决“登录状态怎么记录”的问题,不规定密码怎么存、session 存哪里,侵入性很小。JWT 方案不是不行,但这个系统是服务端渲染页面为主,不是前后端分离的 API 服务,JWT 要额外管理刷新和吊销,得不偿失。所以选型顺序:服务端渲染用 Flask-Login,纯 API 用 JWT,电影管理系统属于前者。
from flask_login import LoginManager, UserMixin, login_user, logout_user, login_required, current_user login_manager = LoginManager() login_manager.login_view = "auth.login" # 未登录时跳转的端点 login_manager.login_message = "请先登录后再访问" login_manager.login_message_category = "warning" class User(UserMixin, db.Model): # 字段定义同第 2 章,继承 UserMixin 后自动获得 is_authenticated 等属性和方法 pass @login_manager.user_loader def load_user(user_id): return db.session.get(User, int(user_id))参数说明:login_view指向auth.login,表示未登录用户访问受保护页面时会被重定向到这个端点;login_message是闪现消息的内容,分类为warning会在模板里对应到 Bootstrap 的警告框样式。user_loader必须返回用户对象或 None,Flask-Login 每次请求都会调用它把 user 塞进current_user,这里用db.session.get而不是User.query.get,后者在 SQLAlchemy 2.0 里已标记为废弃。
登录视图里有一个安全细节:登录成功后不要直接跳回首页,而是读request.args.get("next")然后做白名单校验。很多源码跳转是直接redirect(next),如果next被篡改成外部地址,就变成开放重定向漏洞。校验方法很简单:urlparse(next).netloc必须为空字符串。
3.2 用蓝图拆分 auth、main、admin,避免 include 地狱
不拆蓝图的 Flask 项目,一般在 500 行之后开始出现循环导入。视图函数要引用模型,模型又定义了关系,随时会因为 import 顺序报错。真正的工程化拆分是用蓝图,它让每个功能模块拥有独立的 URL 前缀,也能独立挂模板和静态文件目录。
# app/__init__.py from flask import Flask from flask_migrate import Migrate from config import Config def create_app(config_class=Config): app = Flask(__name__) app.config.from_object(config_class) db.init_app(app) migrate = Migrate(app, db) from app.auth import bp as auth_bp from app.main import bp as main_bp from app.admin import bp as admin_bp app.register_blueprint(auth_bp, url_prefix="/auth") app.register_blueprint(main_bp) # 首页和电影列表放根路径 app.register_blueprint(admin_bp, url_prefix="/admin") return app这段代码的逻辑是:所有扩展先在工厂函数里初始化,蓝图模块在函数内部 import,避免模块加载时的循环依赖。url_prefix="/auth"后,蓝图里定义的路由自动加上前缀。main蓝图不设前缀,根路径/就归它管。要注意的是视图函数命名不能全局重复,比如main.movie_detail和admin.movie_detail如果重名,url_for必须写全限定名,否则 Flask 会报构建错误。
蓝图文件里要单独建自己的路由。admin 蓝图的模板目录会默认继承全局templates,这没问题,但 admin 的页面和前端页面样式差异大时,可以在蓝图构造时指定template_folder="templates/admin",这样 html 文件不用再套一层 admin 子目录。
3.3 基于 before_request 的后台权限校验,三个分支一个不能少
有了登录和蓝图,接下来就是权限控制的最后一块拼图:管理员接口怎么校验。常见做法是给每个 admin 路由都加@login_required装饰器,但这样每个视图函数都得写一行if not current_user.is_admin:,容易漏。正确姿势是放在蓝图里统一处理。
from flask import Blueprint, abort from flask_login import current_user, login_required bp = Blueprint("admin", __name__, url_prefix="/admin") def admin_required(): if not current_user.is_authenticated: abort(401) # 不应到此分支,但防御性写上是好习惯 if not current_user.is_admin: abort(403) bp.before_request(login_required) bp.before_request(admin_required)这段代码里两层 before_request 的书写顺序是有讲究的。Flask 会按注册顺序依次执行,先执行login_required,它能拦截未登录用户并重定向到登录页;再执行自定义的admin_required,判断已登录但非管理员的用户,返回 403。反过来的话,未登录用户会先收到 403 而不是跳转登录,交互体验就错了。abort(401)的分支正常情况下不会走到,因为login_required已经把未登录的过滤掉了,但保留它可以在未来有人调整装饰器顺序时兜底。
这套三件套写完之后,你的源码就具备了最基础的纵深:视图层、蓝图层、请求生命周期钩子。很多号称完整的电影管理源码连第三层都没有,这是拆源码时要重点对比的部分。下一章把功能写厚,看前台展示和管理后台怎么做优化。
4. 电影管理系统的核心功能:前台展示、后台CRUD、评分聚合与搜索优化
4.1 首页列表查询:用 joinedload 消除 N+1 问题
前台首页要展示电影列表,每部电影带分类名和评分。新手源码通常这样写:
movies = Movie.query.all()然后在模板里循环访问movie.category.name。这会产生 N+1 次查询:先查一次 movies 表拿到全部数据,再为每部电影查一次 categories 表。电影列表 30 条就是 31 次查询,页面响应时间随列表长度线性增长。换成显式 join 一次性取回:
from sqlalchemy.orm import joinedload movies = (Movie.query .options(joinedload(Movie.category)) .order_by(Movie.release_date.desc()) .limit(24) .all())逻辑说明:joinedload(Movie.category)会生成一条 LEFT OUTER JOIN,把 movie 和 category 的数据在同一批查询里带回内存。options()的作用是给这个查询单独指定加载策略,不修改全局模型的关系配置,其他查询不受影响。排序字段用release_date而不是rating_avg,因为要优先保证“新上映的在前”这个业务语义,评分排序放到专门的 Top10 接口。
分页参数limit(24)要和模板里的分页组件配套。Flask-SQLAlchemy 的paginate(page=1, per_page=24)返回一个 Pagination 对象,包含items、has_prev、has_next、iter_pages等属性。比手动计算 offset 更省事,而且错误传入负页码时会自动修正为第一页,这一点比手写page = max(request.args.get("page", 1), 1)更稳妥,少写一行边界代码。
4.2 后台 CRUD 的操作细节:删除必须走 POST 且有 CSRF 保护
管理后台的四个操作里,删除是最容易被做错的。很多源码把删除写成<a href="/admin/movie/delete/5">删除</a>,这是个典型的 GET 请求副作用问题。爬虫、预加载、用户误刷新,都会触发删除操作,一次误点就丢数据。正确做法是把删除按钮放进表单,用 POST 提交,并加上 CSRF 令牌。
# admin.py from flask_wtf.csrf import generate_csrf @app_bp.route("/movie/<int:movie_id>/delete", methods=["POST"]) @login_required @admin_required def delete_movie(movie_id): movie = db.get_or_404(Movie, movie_id) db.session.delete(movie) db.session.commit() flash("电影已删除") return redirect(url_for("admin.movie_list"))模板里对应的按钮写法:
<form method="post" action="{{ url_for('admin.delete_movie', movie_id=movie.id) }}" style="display:inline;"> <input type="hidden" name="csrf_token" value="{{ csrf_token() }}"> <button type="submit" class="btn btn-sm btn-danger" onclick="return confirm('确定删除这部电影吗?');">删除</button> </form>说明:db.get_or_404是 SQLAlchemy 2.0 的写法,等价于Movie.query.get_or_404,记录不存在时直接返回 404 页面,省去手写 if 判断。模板里csrf_token()来自 Flask-WTF,需要在表单中显式渲染隐藏字段,否则 POST 会被拒绝。onclick的 confirm 只是前端提醒,真正的安全边界在 POST + CSRF + 管理员校验这三层上。
编辑/新增的套路一样:GET 渲染表单,POST 校验并保存。校验不要只依赖前端 HTML5 的required属性,后端要再调一次。最简单的做法是flask_wtf.FlaskForm定义字段,但很多老源码用的是手写request.form取值,这种写法在字段多时很容易漏掉类型转换,比如release_date从字符串转日期对象失败直接 500。折中方案是写一个字典做字段名和转换函数的映射,准入门槛比用 WTForms 低。
4.3 评分聚合与 Top10 榜单:用 SQL 函数代替 Python 循环
评分功能如果独立成 ratings 表,统计 Top10 就能用一条 SQL 完成,而不是“把所有电影读进 Python,算完再排序”。SQLAlchemy 提供func聚合函数,能生成高效的数据库端计算。
from sqlalchemy import func from app.models import Movie, Rating top_movies = (db.session.query( Movie.id, Movie.title, func.avg(Rating.score).label("avg_score"), func.count(Rating.id).label("rating_count")) .join(Rating, Rating.movie_id == Movie.id) .group_by(Movie.id) .order_by(func.avg(Rating.score).desc()) .limit(10) .all())参数说明:func.avg对应 SQL 的AVG,func.count对应COUNT,label给结果起别名方便 Python 端取值。join默认是 INNER JOIN,这意味着没有评分的电影不会出现在 Top10 里,符合业务逻辑。order_by使用与 select 相同的聚合表达式,注意这里不能写label("avg_score")的字符串名,某些数据库方言不认,直接写表达式最稳。
如果业务要求“评分人数不足 5 人的电影不参与排行”,加一个having(func.count(Rating.id) >= 5)即可。排行缓存是另一件事:Top10 没必要每次请求都重新聚合,可以用 Redis 存 5 分钟,或者简单点,在内存里用@cache.cached(timeout=300)。没有缓存框架时,先在前端加一个 5 分钟的 HTTP 缓存头,也能把压力挡在数据库外面层。
4.4 搜索接口的两个必调参数:模糊匹配与结果分页
电影搜索是最容易被低估的功能。实现上,Movie.title.like("%关键词%")是标配,但两个细节决定了性能上限。第一个是大小写问题:SQLite 的 LIKE 默认对 ASCII 字符不区分大小写,MySQL 的默认排序规则取决于建表时的 collation,同一个关键词在不同环境结果不一致。第二个是关键词为空时行为:空关键词应该返回空列表,而不是全表,否则等于一次无索引全扫。
from flask import request, abort keyword = request.args.get("q", "").strip() if len(keyword) < 2: flash("关键词最少输入两个字符") return redirect(url_for("main.index")) movies = (Movie.query .filter(Movie.title.ilike(f"%{keyword}%")) .paginate(page=page, per_page=12) .items)这里用ilike而不是like,SQLAlchemy 会把ilike翻译成对大小写不敏感的匹配,SQLite 和 PostgreSQL 都原生支持,MySQL 下两者行为一致。len(keyword) < 2限制最短关键词长度,既可以过滤无意义的单字搜索,又能避免搜索“一”这种匹配全表的结果。paginate返回 items 之前,它内部会执行两个查询:一个 count 统计总数,一页查询当前页数据,所以不需要手动再写 COUNT。
搜索没有命中时,页面要区分两种情况:“没有结果”和“关键词太短被拦截”。前者展示“没有找到相关内容”,后者直接重定向回首页带 flash 消息,不要让用户觉得是自己的问题。
5. 把“源码”变成自己的工程:部署前的检查清单与重构顺序
5.1 从 SQLite 切到 MySQL 时最容易踩的三个坑
开发和演示用 SQLite 没问题,但真要给别人用,数据库得换 MySQL 或 PostgreSQL。切换时三个坑命中率极高。第一个是自动递增主键:SQLite 的rowid逻辑迁移到 MySQL 的AUTO_INCREMENT没问题,但 PostgreSQL 用的SERIAL,如果模型里写了primary_key=True而没有指定自增策略,迁移工具可能生成错误。第二个是布尔字段:SQLite 把True存成 1,False存成 0,MySQL 的TINYINT(1)行为一致,但如果你看到源码里用了db.Boolean,在 PostgreSQL 下是真正的布尔类型,查询时where is_admin = 1会直接报错,必须写is_admin = true。第三个是日期时间默认值:MySQL 的DEFAULT CURRENT_TIMESTAMP和 SQLAlchemy 的default=datetime.now语义不同,前者是数据库层设置,后者是应用层写入,迁移后历史记录的created_at如果为空,程序就会炸。所以切库时不要只改连接串,要用迁移工具重新生成一遍库结构。
使用 Flask-Migrate 生成迁移脚本时,建议先备份 SQLite 数据,再flask db upgrade到 MySQL。直接用工具导数据但结构不对,后面修数据的成本比重新迁移高得多。
5.2 用 Waitress 跑生产环境,静态文件交给反向代理
Flask 自带的开发服务器会打印那句经典的警告,告诉你不要在生产环境使用。Linux 服务器上常见做法是用 gunicorn 启动,Windows 环境用 waitress 更省心。都通过命令行指定监听地址和端口,启动命令里同时指定几个 worker 就能扛住演示级别的并发。要注意静态文件,比如图片封面、CSS、JS,默认都由 Flask 路由分发,带宽占用不低。生产部署时把这些文件的 URL 前缀指向反向代理,反向代理直接读磁盘,应用服务器完全不参与 静态文件 响应,内存和 I/O 压力都能明显降下来。
配置好之后的验证方式不是打开首页看一眼,而是用curl -I看响应头。能正常响应的接口应该返回 200 和正确的Content-Type,静态文件的响应头里应该能看到代理添加的缓存字段。
5.3 验证源码完整性的 4 条命令
拿到随便一套源码,在没有文档的情况下,用下面四条命令按顺序过一遍,能不能用一目了然。先检查 Python 语法错误,再去重依赖装上,然后启动前看路由表是否完整,最后写一个最小测试确认数据库能读写。这四条命令全过,源码基本可以进入二次开发阶段。对路由地址不熟悉的,优先看flask routes的输出,那里面列出了所有端点和允许的 HTTP 方法,对照源码的模板能找到系统入口。
本文还有配套的精品资源,点击获取