简介:Python电影系统源码是一套基于Django框架的完整Web应用,面向希望系统学习Python Web开发的初中级开发者,覆盖电影信息展示、用户购票、在线评论等典型业务场景。压缩包共79个文件,大小约905KB,其中43个Python源码对应Django项目的模型、视图、URL配置、表单与认证等核心模块,5个HTML模板负责页面渲染,6个JavaScript和5个CSS文件实现前端交互样式,10张PNG图片用于界面展示和流程说明,整体目录结构清晰,便于按模块阅读。项目还附带部署相关配置,可对接Gunicorn/uWSGI等WSGI服务器。目前已有224人浏览学习过该资源。源码充分展示了Django的模型-视图-模板分层设计、ORM数据库操作、URL路由、用户认证与权限、表单处理、CSRF/XSS安全防护等关键知识点,并覆盖从开发到上线的常见环节,能帮助开发者快速理解实际项目的完整结构与运行流程,是提升Django开发能力的高价值参考资料。
1. 为什么这份 Python 电影系统源码值得拆开看
拿到Python电影系统源码.zip时,很多人的第一反应是直接解压,然后python manage.py runserver。这个 djangoFilmSystem 项目真正值钱的地方在于,它不是单文件 Demo,而是把用户注册登录、图片验证码、在线帮助、后台电影管理拆到了不同的 Django app 里,目录结构接近公司里真实维护的工程。对刚学完 Python 语法的人来说,可以从里面看到 WSGI 应用从入口到数据库的完整链路;对写过几年后端的开发者,也能从settings.py、中间件和模板布局里对比自己的项目组织方式。这篇文章直接从解压和启动开始,把目录结构、模型层、视图层、路由与模板串起来讲,最后落到验证码和生产环境里最容易踩的坑上。
2. 先从 manage.py 入手:djangoFilmSystem 的目录结构与启动参数
2.1 工程目录:settings 与 apps 怎么分层
一个标准的 Django 工程由一个 project 和多个 app 组成。根目录下的manage.py是命令行入口,所有启动、迁移、创建管理员的操作都靠它分发;djangoFilmSystem/是项目配置包,存放settings.py、urls.py、wsgi.py和asgi.py;apps/把业务模块收拢成子应用,templates/放页面模板,static/放 CSS、JS 和图片,utils/里是验证码生成和统一返回码这类被多个 app 复用的公共代码。dbs/目录则是 SQLite 数据库文件的常见存放位置。
我一般建议拿到 zip 后不要急着启动,先按下面的目录树对照一遍,确认每个目录的职责:
djangoFilmSystem-main/ ├── manage.py # 命令行入口:启动、迁移、创建管理员 ├── djangoFilmSystem/ # 项目配置包 │ ├── settings.py │ ├── urls.py │ ├── asgi.py │ └── wsgi.py ├── apps/ # 业务子应用 │ ├── users/ # 用户注册、登录、信息维护 │ ├── verifications/ # 图片/短信验证码 │ ├── online_help/ # 在线帮助、公告 │ └── administer/ # 后台管理:电影、排片、票务 ├── templates/ │ ├── index.html │ └── register.html ├── static/ │ ├── css/ js/ images/ │ └── auth.js message.js photo.js ├── utils/ │ ├── captcha/ # 验证码生成工具 │ └── res_code.py # 统一返回码 ├── dbs/ # 可选的 SQLite 数据库 ├── 数据格式demo.md └── README.md这个结构里最容易忽略的是djangoFilmSystem同时是包名,也是 WSGI application 的导入路径。gunicorn djangoFilmSystem.wsgi:application能启动,就是因为djangoFilmSystem/wsgi.py中定义了application = get_wsgi_application()。如果你把整个项目文件夹改名,只要包名也跟着变,wsgi.py和settings.py里所有djangoFilmSystem.settings的引用都要同步修改,否则启动时会报ModuleNotFoundError。
2.2 初始化虚拟环境:Python 版本、Django 版本和依赖安装
准备环境的第一步是先建虚拟环境,不要让这个项目的依赖和系统 Python 混在一起。常规操作如下:
cd djangoFilmSystem-main python -m venv venv source venv/bin/activate # Windows 用 venv\Scripts\activate pip install django pip install pillow python manage.py checkpython -m venv venv会在当前目录生成一个独立的 Python 环境,激活之后安装的包只作用于这个项目。pip install django安装框架本体,pillow是验证码模块绘制图片时最常见的依赖;如果utils/captcha用的是纯算法生成,那么pillow可以去掉。python manage.py check只检查配置和依赖,不启动服务,拿到项目第一次跑时报错时,可以先用它来确认是否是依赖缺失。
依赖装完,还需要初始化数据库再启动。执行顺序一般是:
python manage.py makemigrations python manage.py migrate python manage.py runserver 0.0.0.0:8000makemigrations从models.py中读取模型变更,生成迁移文件;migrate把迁移同步到数据库。第一次跑如果没执行这两步,页面会报no such table。runserver 0.0.0.0:8000的关键是0.0.0.0,它让服务监听所有网卡,虚拟机和局域网内其他机器都能访问;只想本机调试时直接runserver即可。默认端口是 8000,如果被占用,改成runserver 8001。
2.3 settings.py 中第一次跑必须改的参数
启动后如果看到DisallowedHost报错,需要先改ALLOWED_HOSTS。这个项目的settings.py里有几个参数是本地开发阶段必须确认的:
| 配置项 | 作用 | 本地建议 | 说明 |
|---|---|---|---|
ALLOWED_HOSTS | 允许访问的域名/主机 | ['*'] | 空数组时只能处理 localhost,局域网 IP 会被拒绝 |
INSTALLED_APPS | 注册的应用列表 | 保留apps.users等 | 漏注册会出现模板找不到或迁移表不生成 |
DATABASES | 数据库连接 | 默认 sqlite3 | 确认dbs目录存在且可写 |
TIME_ZONE | 时区 | Asia/Shanghai | 不设置的话排片时间会偏差 8 小时 |
LANGUAGE_CODE | 界面语言 | zh-hans | 配合USE_TZ=True使用时注意时间格式化 |
INSTALLED_APPS里如果漏掉了自定义 app,makemigrations不会生成对应表,模板里的{% url %}也会因为路由没加载而失效。如果migrate时报table already exists,不要直接删源文件里的数据库,先把dbs/*.sqlite3备份后移除,再重新执行迁移。数据格式demo.md里通常记录了接口的返回格式,可以按它检查代码里的字段命名是否一致。
3. 模型层与 ORM:打开电影系统的数据表结构和关联查询
3.1 业务模型:用户、电影、场次与订单是怎么关联的
Django 的模型是数据库表的抽象层,每个模型类对应一张表,模型字段对应表字段。这个电影系统虽然把业务拆成了四个 app,但电影、场次、订单这几张核心表最终还是通过外键关联。简单梳理一下:Film是电影主表,Screening记录某部电影在什么时间、哪个影厅放映,Order记录哪个用户购买了哪个座位。
下面这段代码是常见的模型实现参考,和源码中apps/administer/models.py的写法基本一致:
# apps/administer/models.py from django.db import models from django.contrib.auth.models import User class Film(models.Model): name = models.CharField('电影名', max_length=100) director = models.CharField('导演', max_length=50) release_date = models.DateField('上映日期') status = models.CharField('状态', max_length=10, choices=[('coming', '未上映'), ('on', '上映中'), ('off', '已下映')], default='on') created_at = models.DateTimeField('创建时间', auto_now_add=True) class Meta: db_table = 'film' ordering = ['-release_date'] class Screening(models.Model): film = models.ForeignKey(Film, on_delete=models.CASCADE, related_name='screenings') hall = models.CharField('影厅', max_length=20) start_time = models.DateTimeField('开场时间') class Order(models.Model): user = models.ForeignKey(User, on_delete=models.CASCADE) screening = models.ForeignKey(Screening, on_delete=models.CASCADE) seat = models.CharField('座位', max_length=10) created_at = models.DateTimeField('下单时间', auto_now_add=True)每个模型对应数据库一张表,db_table显式指定表名,不写时默认是app名_小写模型名。on_delete=models.CASCADE表示主记录删除时关联数据也删除;订单这类需要留痕的场景,更推荐on_delete=PROTECT,防止误删历史数据。related_name='screenings'定义了反向查询名,可以用film.screenings.all()取某部电影的全部场次,而不需要自己去Screening表里再 filter 一次。
3.2 常用 ORM 查询与聚合统计
想知道数据库里有哪些正在上映的电影,不需要打开dbs下的 SQLite 文件,直接在 Django shell 里操作更直观:
python manage.py shellfrom datetime import date from django.db.models import Count from apps.administer.models import Film, Screening now = date.today() on_films = Film.objects.filter(status='on', release_date__lte=now) for film in on_films: print(film.id, film.name, film.director) future = Screening.objects.filter(start_time__gt='2025-01-01 00:00:00') print(list(future.values('film__name').annotate(total=Count('id')).order_by('-total')))filter(status='on', release_date__lte=now)表示状态是上映中,且上映日期小于等于今天,这是一个典型的范围过滤。values('film__name')通过双下划线跨外键取出电影名字段,等价于 SQL 里的GROUP BY film.name;annotate(total=Count('id'))给每个分组统计数量,order_by('-total')按数量倒序。Screening.objects.filter(start_time__gt=...)里的__gt是大于条件,类似的还有__lt、__gte、__lte。如果只要单条记录,用get_object_or_404而不是filter(...).first(),这样记录不存在时能直接返回 404 页面。
3.3 迁移顺序与模型字段修改的常见坑
模型改字段之后必须重新生成迁移,不然数据库不知道新结构。开发阶段最常撞到的几个报错,处理思路如下:
| 报错信息 | 原因 | 解决方法 |
|---|---|---|
no such table: film | 没有执行 migrate,或迁移到了另一个数据库 | 先python manage.py makemigrations再migrate |
table film already exists | 数据库文件残留旧表,与迁移记录不一致 | 备份后删除dbs/*.sqlite3重新 migrate |
Column '' cannot be null | 新增字段未给null或默认值 | 迁移时选择提供 default,或修改模型字段加default=... |
relation does not exist | PostgreSQL 下 schema 名不匹配 | 检查数据库用户是否有对应 schema 权限 |
这里要提醒一句:不要轻易删掉dbs目录里的 SQLite 文件。如果里面有历史测试数据,删完所有迁移记录要从头跑,createsuperuser创建的账号也会丢失。稳妥的办法是先把数据格式demo.md里的表结构说明读完,确认没有可用数据后再重置。
4. URL 路由、视图和模板渲染:电影列表如何从数据库渲染成 HTML 页面
4.1 路由分发:项目 URLs 和 app 内 URLs 的两层设计
Django 的 URL 配置不是只能堆在djangoFilmSystem/urls.py里。项目里常见的做法是把子应用的路由拆到各自的urls.py,再用include拼到主路由上。主路由会像下面这样:
# djangoFilmSystem/urls.py from django.contrib import admin from django.urls import path, include urlpatterns = [ path('admin/', admin.site.urls), path('', include('apps.administer.urls')), path('user/', include('apps.users.urls')), path('verify/', include('apps.verifications.urls')), ]子应用里再定义自己的路由:
# apps/administer/urls.py from django.urls import path from . import views urlpatterns = [ path('', views.index, name='index'), path('movie/<int:film_id>/', views.movie_detail, name='movie_detail'), path('order/', views.create_order, name='create_order'), ]path('movie/<int:film_id>/')是 Django 2.0 以后的路由语法,尖括号里写转换器int和变量名film_id,URL 里匹配到的数字会作为film_id参数传给视图函数。name是 URL 的别名,模板里可以用{% url 'movie_detail' film.pk %}生成真实地址,避免把 URL 硬编码到页面里。include的路径apps.administer.urls对应根目录apps/administer/urls.py。如果多个 app 里出现同名路由别名,反向解析会拿到第一个匹配,所以建议在urls.py里声明app_name = 'administer',再写{% url 'administer:movie_detail' film.id %}。
4.2 视图:使用函数视图处理请求的典型写法
视图承担的是“接收请求、查数据库、渲染模板”这三件事,常见实现有函数视图与类视图两种。源码里如果用的是函数视图,大概会长这样:
# apps/administer/views.py from django.shortcuts import render, get_object_or_404 from django.contrib.auth.decorators import login_required from .models import Film def index(request): film_list = Film.objects.filter(status='on') return render(request, 'index.html', {'films': film_list}) @login_required def movie_detail(request, film_id): film = get_object_or_404(Film, pk=film_id) return render(request, 'detail.html', {'film': film}) @login_required def create_order(request): if request.method == 'POST': # 按表单提交的 screening_id 和 seat 创建订单 pass return render(request, 'order.html')render(request, template, context)先把模板和 context 合并成 HTML,再返回HttpResponse;context 是字典,模板里用{{ films }}访问。pk是主键的通用别名,get_object_or_404(Film, pk=film_id)在找不到时抛 404,不需要自己写 try/except。@login_required是内置装饰器,未登录用户会被重定向到settings.LOGIN_URL,默认是/accounts/login/。如果这个项目没配置登录页,可以在settings.py里显式改成LOGIN_URL = '/user/login',让它指向apps/users下面的登录视图。
4.3 模板语法与静态资源:让 index.html 正常显示电影列表
templates/index.html放在项目根目录,是因为settings.TEMPLATES的DIRS里注册了BASE_DIR / 'templates'。页面里展示的电影数据来自视图传过来的films,写法是 Django 模板语言,不是原生 Python:
{% load static %} <!DOCTYPE html> <html lang="zh-hans"> <head> <meta charset="utf-8"> <link rel="stylesheet" href="{% static 'css/style.css' %}"> </head> <body> <ul> {% for film in films %} <li> <a href="{% url 'movie_detail' film.id %}">{{ film.name }}</a> <span>{{ film.director }}</span> </li> {% empty %} <li>当前没有上映中的电影</li> {% endfor %} </ul> <form method="post" action="{% url 'create_order' %}"> {% csrf_token %} <input type="text" name="screening_id"> <input type="text" name="seat"> <button type="submit">购买</button> </form> <script src="{% static 'js/auth.js' %}"></script> </body> </html>{% load static %}写在文件顶部,{% static 'css/style.css' %}会拼接STATIC_URL输出最终 URL。{% for %}配合{% empty %}能处理空列表,比在视图里做判断更直观。{% csrf_token %}会输出一个隐藏的csrfmiddlewaretoken输入框,Django 的CsrfViewMiddleware在接收 POST 时会校验这个 token;模板里漏写的话,表单提交会直接返回 403 Forbidden,这是新手最容易撞到的错误之一。
4.4 模板表单与用户注册:从 register.html 到 Django 用户认证
注册页register.html的表单提交后会走到apps/users这个应用。源码为了保持演示简洁,常采用下面的处理方式:
from django.contrib.auth.models import User from django.shortcuts import render, redirect def register(request): if request.method == 'POST': username = request.POST.get('username') password = request.POST.get('password') if not username or not password: return render(request, 'register.html', {'error': '用户名和密码不能为空'}) if User.objects.filter(username=username).exists(): return render(request, 'register.html', {'error': '用户已存在'}) User.objects.create_user(username=username, password=password) return redirect('/') return render(request, 'register.html', {})User.objects.create_user()是 Django 创建用户的正确方式,它会把明文密码做哈希再写入数据库,而不是把密码存成明文。如果换成User.objects.create(username=..., password=...),密码就是明文字符串,之后@login_required和authenticate()永远都验不过。request.POST.get()返回表单字段的值,没填时返回None,所以要先判空。实际项目中更推荐用 Django Form 或ModelForm,它能自动完成字段绑定、类型校验和错误回显,代码量不会比手写多。
模板中经常出现的标签语法,按使用频率整理如下:
| 模板语法 | 作用 | 典型场景 |
|---|---|---|
{{ variable }} | 输出变量 | 输出用户名、电影名 |
{% for %}/{% empty %} | 循环列表,空值兜底 | 渲染电影列表 |
{% url 'name' arg %} | 反向解析 URL | 生成详情页链接 |
{% csrf_token %} | 输出防跨站令牌 | 所有 POST 表单 |
{% static 'path' %} | 拼接静态文件地址 | 加载 CSS、JS |
模板里不支持写复杂表达式和函数调用,需要提前计算的数据一律放到视图 context 中;模板只负责展示和简单分支,这样后期维护时定位问题更快。
5. 验证码闭环与生产部署避坑:把 DEBUG=False 之前的准备做好
5.1 verifications 与 utils/captcha 的协作方式
验证码在这个项目里不是一个只画图的独立工具,它分成两层:utils/captcha负责生成图片和答案,apps/verifications负责把答案写入 session,并把图片作为 HTTP 响应返回。生成部分的常见实现如下:
# utils/captcha/captcha.py import random from PIL import Image, ImageDraw def generate_captcha(): text = ''.join(random.sample('abcdefghjkmnpqrstuvwxyz23456789', 4)) image = Image.new('RGB', (120, 40), (240, 240, 240)) draw = ImageDraw.Draw(image) for i, ch in enumerate(text): draw.text((10 + i * 25, 8), ch, fill=(40, 40, 40)) return image, text字符集里故意没放i、l、o、0、1,能显著降低人工读错的概率,同时减少验证码被自动识别成功的机会。draw.text的坐标用索引乘以固定步长,图片宽度改变时,这个坐标步长也要同步调整。
校验时最容易被忽略的是大小写和一次性校验。正确逻辑是在生成时把text存进request.session,校验时取出并立刻删除:
def captcha_check(request): user_input = request.POST.get('captcha', '').lower() saved = request.session.get('captcha_text', '') if not saved or user_input != saved: return render(request, 'register.html', {'error': '验证码错误'}) request.session.pop('captcha_text') # 验证通过后继续执行用户名密码校验request.session.get从 session 里取出生成时保存的答案,取出来后立刻pop,让同一个验证码只能用一次。如果验证码校验后没有失效,攻击者可以反复重放这个请求。建议在 session 里再存一个生成时间,超过 60 秒就强制失效。
5.2 本地和生产环境静态文件的差异
开发阶段runserver会自动处理静态文件,一旦上线把DEBUG设为False,Django 默认就不再提供静态资源。所以需要执行collectstatic把 app 和项目里的静态文件统一复制到STATIC_ROOT:
python manage.py collectstatic --noinput pip install gunicorn gunicorn djangoFilmSystem.wsgi:application -b 0.0.0.0:8000collectstatic之后不会自动配上 URL,还需要 Nginx 的location /static/ { alias /static目录/; }或whitenoise中间件。gunicorn的导入路径djangoFilmSystem.wsgi:application要和项目包名保持一致,如果目录改名,这里也要同步改。线上环境样式全丢时,先看STATIC_URL和STATIC_ROOT是否指向同一个目录,再检查 Nginx alias 里有没有写错路径。
本文还有配套的精品资源,点击获取