ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

Django生产部署实战:Docker+PostgreSQL+allauth权限与静态文件全链路

Django生产部署实战:Docker+PostgreSQL+allauth权限与静态文件全链路

简介:本资源是William S. Vincent所著《Django for Professionals》中文版PDF电子书,面向具备Python基础、正从Django入门迈向生产级开发的中高级Web开发者。全书系统讲解如何构建可部署、高安全、易维护的专业网站,覆盖Docker容器化部署、PostgreSQL生产数据库集成、自定义用户模型与邮箱验证认证流程、单元测试与集成测试策略、环境变量管理、性能调优及安全加固等核心实践。资源为1个25.89MB的PDF文件,内容结构清晰,含7大章节:从Docker与PostgreSQL环境搭建,到书店项目实战、用户注册增强(django-allauth)、静态资源管理及高级测试方法,每章均配代码示例与Git实践要点。目前已有61人学习下载,适合希望掌握Django社区主流生产规范、填补开发与上线之间关键能力断层的工程师系统精读与实操参考。

1. Django生产级网站不是“跑起来就行”:这本书把Django从玩具项目拉回真实战场

你写过Django博客、待办清单、投票系统——它们都能python manage.py runserver跑通,但上线前一小时崩在ALLOWED_HOSTS没配、静态文件404、PostgreSQL连接超时、用户注册邮件发不出、DEBUG=True被扫出敏感信息……这些不是“小问题”,是生产环境里会直接导致服务不可用、数据泄露或支付失败的硬伤。《Django for Professionals》不讲怎么搭第一个Hello World,它默认你已会CRUD,转头就带你拆解一个真实书店系统的全链路:用Docker隔离开发与部署环境、用PostgreSQL替代SQLite扛住并发写入、用django-allauth实现邮箱登录+社交认证+密码重置闭环、用collectstatic和CDN-ready配置管理前端资源、用PermissionRequiredMixin和UserPassesTestMixin做细粒度权限控制、用Stripe集成真实支付流并绑定用户权限。它不教Django是什么,而是教你在客户投诉电话打来前5分钟,如何快速定位是数据库连接池耗尽、还是EMAIL_BACKEND配置漏了EMAIL_USE_TLS。适合那些正卡在“本地能跑,上线就翻车”临界点的中级Django开发者——你不需要从零学Django,你需要的是把已知能力焊进生产流水线的那套螺丝刀、扭矩扳手和故障诊断仪。


2. Docker不是可选项,是生产级Django的呼吸面罩:容器化开发环境落地实操

2.1 为什么Docker是Django生产化的第一道防火墙?

很多团队还在用virtualenv+手动pip install管理依赖,结果开发机装了psycopg2 2.9,测试机装了2.8,生产服务器因为缺少libpq-dev编译失败;或者本地用SQLite调试快,上线切PostgreSQL后才发现auto_now_add在事务中行为不一致。Docker强制你把“Python版本+Django版本+数据库驱动+系统库依赖”全部打包成不可变镜像。书中第1章开篇就强调:容器不是为部署而生,是为消除“在我机器上能跑”的玄学而生。它让requirements.txt从文本清单变成可执行契约——镜像构建失败,说明依赖声明本身就有缺陷;容器启动失败,说明环境变量或端口配置存在硬冲突。这不是炫技,是把“环境一致性”从口头承诺变成CI/CD流水线里的自动校验点。

2.2 从零构建Django+PostgreSQL双容器开发环境

书中第1章和第2章给出的docker-compose.yml是经过生产验证的最小可行组合。我们按实际复现步骤拆解(注意:所有路径基于Linux/macOS,Windows需用WSL2):

# 创建项目根目录 mkdir bookstore && cd bookstore # 初始化Django项目(在宿主机执行,避免容器内权限问题) django-admin startproject config . # 创建Dockerfile(注意:Dockerfile必须放在项目根目录,与manage.py同级) cat > Dockerfile << 'EOF' FROM python:3.9-slim # 设置工作目录 WORKDIR /app # 复制依赖文件(分层缓存关键:先copy requirements再copy代码) COPY requirements.txt . RUN pip install --no-cache-dir -r requirements.txt # 复制项目代码(最后copy,避免每次改代码都重装依赖) COPY . . # 暴露端口(Django默认8000) EXPOSE 8000 # 启动命令(使用gunicorn而非runserver,体现生产意识) CMD ["gunicorn", "--bind", "0.0.0.0:8000", "config.wsgi:application"] EOF

提示:requirements.txt需显式包含gunicorn==21.2.0(稳定版)、psycopg2-binary==2.9.7(避免编译依赖),不能只写Django>=4.2——生产环境必须锁定次要版本号。

接着创建docker-compose.yml:

version: '3.8' services: web: build: . command: python manage.py runserver 0.0.0.0:8000 volumes: - .:/app ports: - "8000:8000" environment: - DEBUG=1 - SECRET_KEY=dev-secret-key-change-in-prod - DATABASE_URL=postgresql://postgres:postgres@db:5432/bookstore depends_on: - db db: image: postgres:15-alpine volumes: - postgres_data:/var/lib/postgresql/data/ environment: - POSTGRES_DB=bookstore - POSTGRES_USER=postgres - POSTGRES_PASSWORD=postgres healthcheck: test: ["CMD-SHELL", "pg_isready -U postgres -d bookstore"] interval: 30s timeout: 10s retries: 5 volumes: postgres_data:

逻辑说明:

  • web服务用build: .指向当前目录Dockerfile,volumes: .:/app实现代码热重载(开发阶段必需);
  • db服务用postgres:15-alpine精简镜像,volumes持久化数据卷避免容器重启丢库;
  • healthcheck是关键:pg_isready检测PostgreSQL是否真正就绪,避免web服务因DB未启动而报Connection refused后崩溃退出;
  • DATABASE_URL使用django-environ解析(需在settings.py中配置),统一管理数据库连接字符串,为后续切换生产环境铺路。

启动后验证:

docker-compose up -d # 等待db健康检查通过(约15秒) docker-compose ps # 查看web日志确认Django启动成功 docker-compose logs -f web # 访问 http://localhost:8000 应看到Django欢迎页

2.3 避坑:Docker环境下Django开发的五个血泪经验

  1. 现象:docker-compose up后web容器反复重启,日志显示django.core.exceptions.ImproperlyConfigured: Requested setting DATABASES, but settings are not configured.
    原因:settings.py中os.environ.get('DEBUG')返回None,导致if DEBUG:分支未执行,数据库配置未加载;根本原因是.env文件未被Docker环境读取(Docker默认不加载宿主机.env)。
    解决:在docker-compose.yml的web服务下添加env_file: .env,并在.env中写DEBUG=1;或改用environment: - DEBUG=1硬编码(开发阶段可接受)。

  2. 现象:浏览器访问http://localhost:8000返回DisallowedHost错误。
    原因:Django默认ALLOWED_HOSTS = [],而Docker容器内request.get_host()返回localhost:8000,不在白名单中。
    解决:在settings.py中将ALLOWED_HOSTS设为['localhost', '127.0.0.1', '[::1]'],或更安全地用os.environ.get('ALLOWED_HOSTS', 'localhost').split(',')动态解析。

  3. 现象:修改Python代码后,runserver未自动重载,必须docker-compose restart web。
    原因:Docker容器内runserver的文件监视器无法监听挂载卷(volume)的文件系统事件(尤其macOS/Windows)。
    解决:在docker-compose.yml的web服务下添加command: python manage.py runserver 0.0.0.0:8000 --noreload,并安装watchmedo:RUN pip install watchdog,再用command: watchmedo auto-restart --directory=./ --pattern=*.py --recursive -- python manage.py runserver 0.0.0.0:8000。

  4. 现象:PostgreSQL容器首次启动后,python manage.py migrate报错relation "auth_user" does not exist。
    原因:web服务启动太快,在db服务完成初始化(创建database/user)前就尝试连接。
    解决:在docker-compose.yml中为web服务添加depends_on: db仅表示启动顺序,不保证服务就绪;必须配合healthcheck和restart: on-failure,或在web的command中加入等待脚本:sh -c "until pg_isready -U postgres -d bookstore; do sleep 2; done && python manage.py migrate && python manage.py runserver 0.0.0.0:8000"。

  5. 现象:docker-compose down后重新up,PostgreSQL数据丢失。
    原因:volumes定义了postgres_data,但docker-compose down默认不删除命名卷(named volume),而docker-compose down -v会删——但若忘记加-v,下次up时Docker会创建新卷,旧数据仍在但不可见。
    解决:始终用docker-compose down -v清理,或在docker-compose.yml中明确volumes路径:./postgres_data:/var/lib/postgresql/data/(绑定挂载),虽牺牲一点性能但数据绝对可见可控。


3. PostgreSQL不是“换数据库而已”:从SQLite到生产级关系型数据库的三重跃迁

3.1 为什么Django项目必须放弃SQLite走向PostgreSQL?

新手常认为“SQLite轻量、免配置、单文件”,但生产环境里它有三座不可逾越的大山:

  • 并发写入瓶颈:SQLite使用文件锁,同一时间只能有一个写操作。当用户同时提交订单、管理员后台更新库存、定时任务同步数据时,请求会排队阻塞,响应时间从毫秒级飙升至秒级;
  • 数据类型贫瘠:没有JSONField原生支持,处理API返回的嵌套结构需手动序列化;没有ArrayField,标签系统(如图书分类)只能用多对多表,查询复杂度指数上升;
  • 运维黑洞:无用户权限体系(所有连接拥有文件系统级读写权),无连接池(每个HTTP请求新建连接,100并发即100个连接),无慢查询日志(无法定位SELECT * FROM books WHERE title LIKE '%python%'这类全表扫描)。

书中第2章用psycopg2替换sqlite3,表面是改一行DATABASES配置,实质是把Django从“单机玩具”推入“分布式服务”思维——你开始思考连接数限制、事务隔离级别、索引优化、备份策略。

3.2 在Docker中配置PostgreSQL并迁移Django模型

首先确保requirements.txt包含psycopg2-binary==2.9.7(二进制包免编译,开发阶段够用;生产环境建议源码编译psycopg2==2.9.7以获最佳性能)。然后修改config/settings.py:

# config/settings.py import environ env = environ.Env() environ.Env.read_env() # 读取.env文件 # 数据库配置(使用django-environ解析DATABASE_URL) import dj_database_url DATABASES = { 'default': dj_database_url.config( default=env('DATABASE_URL', default='sqlite:///db.sqlite3') ) }

.env文件内容:

# .env DATABASE_URL=postgresql://postgres:postgres@db:5432/bookstore DEBUG=1 SECRET_KEY=dev-secret-key-change-in-prod

参数说明:dj_database_url.config()自动将DATABASE_URL字符串解析为Django标准DATABASES字典;default=参数提供降级方案(本地无Docker时回退SQLite);env('DATABASE_URL', ...)确保环境变量缺失时不报错。

执行迁移:

# 进入web容器执行(确保db已启动) docker-compose exec web bash # 容器内执行 python manage.py makemigrations python manage.py migrate # 创建超级用户(用于admin后台) python manage.py createsuperuser

验证PostgreSQL连接:

# 进入db容器 docker-compose exec db psql -U postgres -d bookstore # 在psql中执行 bookstore=# \dt # 查看所有表,应有auth_*、django_*、books_*等 bookstore=# SELECT COUNT(*) FROM auth_user; -- 应返回1(刚创建的superuser)

3.3 避坑:PostgreSQL与Django协同的四个隐形陷阱

  1. 现象:python manage.py migrate报错django.db.utils.OperationalError: FATAL: database "bookstore" does not exist。
    原因:docker-compose.yml中db服务的POSTGRES_DB=bookstore环境变量生效,但web服务启动时DATABASE_URL指向bookstore库,而PostgreSQL初始化脚本需10秒以上创建库,此时连接已发起。
    解决:在web服务command中加入等待逻辑(见2.3节第4条),或使用docker-compose run --rm web python manage.py migrate(手动触发,确保db已就绪)。

  2. 现象:Django admin后台创建用户时,密码字段明文显示,且保存后密码未哈希。
    原因:Custom User Model未继承AbstractBaseUser或未正确实现set_password()方法;或AUTH_USER_MODEL未在settings.py顶部声明(必须在任何model import前)。
    解决:检查settings.py第一行是否有AUTH_USER_MODEL = 'users.CustomUser';检查users/models.py中CustomUser类是否包含objects = CustomUserManager()且CustomUserManager继承BaseUserManager并实现create_user()。

  3. 现象:python manage.py dumpdata auth.user导出JSON含password字段明文,但loaddata后用户无法登录。
    原因:dumpdata导出的是数据库存储的哈希值(如pbkdf2_sha256$...),但若导出时DEBUG=True且SECRET_KEY不同,哈希值在新环境无法验证。
    解决:生产环境dumpdata后,用python -c "from django.contrib.auth.hashers import make_password; print(make_password('raw_password'))"生成新哈希,或直接重置密码:docker-compose exec web python manage.py changepassword username。

  4. 现象:SELECT * FROM books_book ORDER BY created_at DESC LIMIT 10在10万数据时响应超2秒。
    原因:created_at字段未建索引,PostgreSQL执行全表扫描。
    解决:在books/models.py的Book模型中添加class Meta: ordering = ['-created_at'],Django会自动为ordering字段建索引;或手动建索引:python manage.py dbshell进入后执行CREATE INDEX CONCURRENTLY books_book_created_at_idx ON books_book (created_at DESC);(CONCURRENTLY避免锁表)。


4. 用户认证不是“login.html就行”:从Django内置Auth到django-allauth的生产级演进

4.1 为什么Django内置Auth模块在生产中必须被替换?

Django自带django.contrib.auth足够教学,但生产场景暴露三大短板:

  • 邮箱非必填:User.email字段可为空,导致密码重置、通知邮件功能失效;
  • 无邮箱验证闭环:用户注册后立即可登录,无法防止恶意邮箱占位或拼写错误;
  • 无社交登录:现代应用需微信、GitHub一键登录,内置Auth需手动对接OAuth2,代码量爆炸且易出安全漏洞。

书中第7章引入django-allauth,不是因为它“功能多”,而是它用标准化协议+生产验证流程解决了上述问题:注册时发送带token的确认邮件、登录时校验邮箱有效性、密码重置走独立时效token、社交登录封装Google/Facebook/微信(需额外适配)的OAuth2握手。它把“用户身份生命周期管理”从DIY工程变成可配置组件。

4.2 集成django-allauth:四步完成邮箱验证认证体系

第一步:安装与基础配置

# 添加到requirements.txt django-allauth==0.54.0 # 安装 pip install -r requirements.txt

第二步:settings.py配置(关键!顺序和参数决定成败)

# config/settings.py INSTALLED_APPS += [ 'django.contrib.sites', 'allauth', 'allauth.account', 'allauth.socialaccount', # 如需社交登录 ] # 必须设置SITE_ID(allauth依赖sites框架) SITE_ID = 1 # 认证后端(覆盖Django默认,启用allauth) AUTHENTICATION_BACKENDS = ( 'django.contrib.auth.backends.ModelBackend', 'allauth.account.auth_backends.AuthenticationBackend', ) # 邮箱配置(开发阶段用console backend,生产换smtp) EMAIL_BACKEND = 'django.core.mail.backends.console.EmailBackend' # 生产环境示例(需配置SMTP服务) # EMAIL_BACKEND = 'django.core.mail.backends.smtp.EmailBackend' # EMAIL_HOST = 'smtp.gmail.com' # EMAIL_PORT = 587 # EMAIL_USE_TLS = True # EMAIL_HOST_USER = env('EMAIL_HOST_USER') # EMAIL_HOST_PASSWORD = env('EMAIL_HOST_PASSWORD') # allauth特有配置 ACCOUNT_EMAIL_REQUIRED = True # 注册必须填邮箱 ACCOUNT_EMAIL_VERIFICATION = 'mandatory' # 强制邮箱验证 ACCOUNT_LOGIN_ON_EMAIL_CONFIRMATION = True # 验证后自动登录 ACCOUNT_LOGOUT_REDIRECT_URL = '/' # 登出后跳首页 LOGIN_REDIRECT_URL = '/' # 登录后跳首页 LOGOUT_REDIRECT_URL = '/' # 登出后跳首页

第三步:URL路由

# config/urls.py from django.contrib import admin from django.urls import path, include urlpatterns = [ path('admin/', admin.site.urls), path('accounts/', include('allauth.urls')), # 关键:allauth提供完整URL集 path('', include('pages.urls')), # 主页 ]

第四步:模板覆盖(templates/account/下创建)

  • signup.html:继承account/base.html,自定义注册表单;
  • email_confirm.html:邮箱验证成功页;
  • verification_sent.html:发送验证邮件后提示页;
  • login.html:登录页,含“忘记密码”链接。

逻辑说明:allauth.urls自动映射/accounts/login/、/accounts/logout/、/accounts/signup/、/accounts/password/reset/等20+URL;ACCOUNT_EMAIL_VERIFICATION = 'mandatory'使signup视图在创建用户后不直接登录,而是发送邮件并跳转verification_sent.html;用户点击邮件中链接,confirm_email视图校验token并激活账户。

4.3 避坑:django-allauth集成中的五个高频翻车点

  1. 现象:注册后收不到验证邮件,EMAIL_BACKEND = 'console'也无输出。
    原因:settings.py中EMAIL_BACKEND配置位置错误——必须在INSTALLED_APPS之后、AUTHENTICATION_BACKENDS之前;或DEBUG=False时Django默认禁用console backend。
    解决:将EMAIL_BACKEND配置移到settings.py末尾,并添加LOGGING配置确保邮件日志输出:LOGGING = {'version': 1, 'handlers': {'console': {'class': 'logging.StreamHandler'}}, 'loggers': {'django.core.mail': {'handlers': ['console'], 'level': 'INFO'}}}。

  2. 现象:点击邮箱验证链接报Page not found (404),URL类似/accounts/confirm-email/abc123.../。
    原因:SITE_ID = 1未设置,或django.contrib.sites未加入INSTALLED_APPS;allauth依赖Site模型获取当前域名生成验证链接。
    解决:检查INSTALLED_APPS是否含'django.contrib.sites';运行python manage.py migrate确保sites表创建;在Django admin中确认Sites对象的domain为localhost:8000(开发)或yourdomain.com(生产)。

  3. 现象:ACCOUNT_EMAIL_REQUIRED = True但注册表单仍允许跳过邮箱。
    原因:Custom User Model未将email字段设为unique=True且blank=False,allauth的表单验证未生效。
    解决:在users/models.py中CustomUser类定义email = models.EmailField(_('email address'), unique=True),并确保CustomUserManager.create_user()方法强制传入email参数。

  4. 现象:密码重置邮件发送成功,但点击链接报The password reset link was invalid。
    原因:PASSWORD_RESET_TIMEOUT默认为259200秒(3天),但若服务器时间不同步(如Docker容器时区为UTC,宿主机为CST),token生成与校验时间戳偏差导致失效。
    解决:在settings.py中统一时区:TIME_ZONE = 'Asia/Shanghai',并确保Docker容器时区同步:在Dockerfile中添加ENV TZ=Asia/Shanghai和RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ > /etc/timezone。

  5. 现象:python manage.py test时allauth相关测试报NoReverseMatch,找不到account_login等URL。
    原因:测试用TestCase未加载allauth的URL配置,或ROOT_URLCONF指向错误模块。
    解决:在测试类中显式指定urls:class AccountTests(TestCase): urls = 'config.urls';或在test_settings.py中确保INSTALLED_APPS包含allauth且ROOT_URLCONF = 'config.urls'。


5. 静态文件不是“扔进static文件夹就完事”:Django staticfiles的生产级资产管线

5.1 为什么Django的staticfiles机制在生产中必须重构?

新手常把CSS/JS图片全放static/目录,DEBUG=True时runserver自动服务,但上线后DEBUG=False时Django完全不处理静态文件——它期望你用Nginx/Apache反向代理静态请求。若未配置,用户访问/static/css/main.css直接404。书中第6章用collectstatic+STATIC_ROOT+STATICFILES_DIRS构建三层资产管线:

  • STATICFILES_DIRS:开发阶段存放第三方库(Bootstrap、jQuery)和自定义静态文件的源目录;
  • STATIC_ROOT:生产阶段collectstatic命令将所有静态文件(含STATICFILES_DIRS和各app的static/)合并到的单一目录,供Web服务器托管;
  • STATIC_URL:浏览器请求静态文件的URL前缀(如/static/),Django模板中{% static 'css/main.css' %}生成/static/css/main.css。

这三层分离,让“开发时分散管理、生产时集中交付”成为可能。

5.2 构建可扩展的静态文件架构:从Bootstrap到Crispy Forms

第一步:目录结构规划(遵循Django最佳实践)

bookstore/ ├── static/ # STATICFILES_DIRS之一:存放全局静态资源 │ ├── css/ │ │ └── main.css │ ├── js/ │ │ └── main.js │ └── images/ │ └── logo.png ├── books/ │ └── static/books/ # app专属静态资源,会被collectstatic收集 │ └── css/book.css ├── config/ │ └── settings.py

第二步:settings.py配置(关键参数含义)

# config/settings.py import os from pathlib import Path BASE_DIR = Path(__file__).resolve().parent.parent.parent # 开发阶段:STATICFILES_DIRS指向多个源目录 STATICFILES_DIRS = [ BASE_DIR / "static", # 全局static目录 ] # 生产阶段:collectstatic输出目录(绝对路径!) STATIC_ROOT = BASE_DIR / "staticfiles" # 注意:不是'static/' # 浏览器请求URL前缀 STATIC_URL = '/static/' # 查找静态文件的查找器(默认已包含FileSystemFinder和AppDirectoriesFinder) # STATICFILES_FINDERS = [ # 'django.contrib.staticfiles.finders.FileSystemFinder', # 'django.contrib.staticfiles.finders.AppDirectoriesFinder', # ]

第三步:集成Bootstrap与Django Crispy Forms

# 安装crispy-forms pip install django-crispy-forms==2.0 # 添加到INSTALLED_APPS INSTALLED_APPS += ['crispy_forms', 'crispy_bootstrap5'] # 设置模板包 CRISPY_ALLOWED_TEMPLATE_PACKS = "bootstrap5" CRISPY_TEMPLATE_PACK = "bootstrap5"

在templates/base.html中引入Bootstrap CSS/JS:

<!-- templates/base.html --> <!DOCTYPE html> <html> <head> <meta charset="utf-8"> <title>{% block title %}Bookstore{% endblock %}</title> <!-- Bootstrap CSS --> <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet"> </head> <body> <nav class="navbar navbar-expand-lg navbar-light bg-light"> <div class="container"> <a class="navbar-brand" href="{% url 'home' %}">Bookstore</a> </div> </nav> <div class="container mt-4"> {% block content %}{% endblock %} </div> <!-- Bootstrap JS --> <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script> </body> </html>

在pages/views.py中使用Crispy Forms渲染登录表单:

# pages/views.py from django.shortcuts import render from django.contrib.auth.forms import AuthenticationForm from crispy_forms.helper import FormHelper from crispy_forms.layout import Submit def login_view(request): form = AuthenticationForm() form.helper = FormHelper() form.helper.form_method = 'post' form.helper.add_input(Submit('submit', 'Login', css_class='btn btn-primary')) return render(request, 'pages/login.html', {'form': form})

templates/pages/login.html:

<!-- templates/pages/login.html --> {% extends 'base.html' %} {% load crispy_forms_tags %} {% block content %} <div class="row justify-content-center"> <div class="col-md-6"> <h2>Login</h2> <form method="post"> {% csrf_token %} {{ form | crispy }} </form> </div> </div> {% endblock %}

5.3 避坑:staticfiles在Docker与生产环境的六个致命细节

  1. 现象:DEBUG=False时/static/css/main.css返回404,但DEBUG=True正常。
    原因:Django在DEBUG=False时不提供静态文件服务,必须由Web服务器(Nginx)处理;collectstatic未执行或STATIC_ROOT路径错误。
    解决:在Docker中web服务启动前执行collectstatic:修改Dockerfile的CMD为sh -c "python manage.py collectstatic --noinput && gunicorn --bind 0.0.0.0:8000 config.wsgi:application";确保STATIC_ROOT是绝对路径且容器内可写。

  2. 现象:collectstatic报错OSError: [Errno 13] Permission denied: '/app/staticfiles'。
    原因:Docker容器以非root用户运行(安全最佳实践),但staticfiles目录由root创建,普通用户无写权限。
    解决:在Dockerfile中添加用户创建和目录授权:

    RUN addgroup -g 101 -f appgroup && adduser -S appuser -u 101 USER appuser RUN mkdir -p /app/staticfiles RUN chmod -R 755 /app/staticfiles
  3. 现象:Bootstrap CSS生效,但Crispy Forms的按钮无样式,<button>元素无btn btn-primary类。
    原因:CRISPY_TEMPLATE_PACK = "bootstrap5"未设置,或INSTALLED_APPS中'crispy_bootstrap5'未添加(Django 4.2+需此包)。
    解决:确认INSTALLED_APPS含'crispy_bootstrap5',且CRISPY_TEMPLATE_PACK值为"bootstrap5"(非"bootstrap4")。

  4. 现象:{% static 'images/logo.png' %}在模板中生成/static/images/logo.png,但Nginx配置location /static/指向/app/staticfiles/,而logo.png实际在/app/static/images/。
    原因:collectstatic未将static/目录下的文件复制到staticfiles/。
    解决:运行docker-compose exec web python manage.py collectstatic --noinput,检查staticfiles/目录下是否有images/logo.png;若无,确认STATICFILES_DIRS包含BASE_DIR / "static"且路径正确。

  5. 现象:生产环境Nginx返回502 Bad Gateway,日志显示connect() failed (111: Connection refused) while connecting to upstream。
    原因:Nginx配置的upstream指向http://web:8000,但Docker网络中web服务名解析失败,或web容器未监听0.0.0.0:8000(只监听127.0.0.1:8000)。
    解决:在Dockerfile的CMD中确保gunicorn绑定0.0.0.0:8000(非127.0.0.1:8000);在docker-compose.yml中为web服务添加network_mode: "host"(开发阶段)或确保nginx服务与web在同一网络(networks: [default])。

  6. 现象:collectstatic后staticfiles/目录巨大(>100MB),含大量未使用的Bootstrap源码。
    原因:STATICFILES_DIRS错误包含了node_modules/或bootstrap/src/等开发目录。
    解决:STATICFILES_DIRS只应包含dist/编译后的文件,如static/css/bootstrap.min.css;用npm run build生成压缩版,勿直接拷贝源码。


6. 权限控制不是“login_required装饰器”:Django细粒度权限的实战落地

6.1 为什么@login_required只是权限系统的起点,而非终点?

@login_required只能拦截未登录用户,但生产系统需要更精细的控制:

  • 图书编辑页面只允许作者或管理员访问;
  • 订单列表页只显示当前用户自己的订单;
  • 后台管理页禁止普通用户访问;
  • 评论删除按钮只对评论作者或版主显示。

书中第13章用PermissionRequiredMixin、UserPassesTestMixin、Groups构建三层防御:

  • 模型级权限:Django自动为每个Model生成add_、change_、delete_、view_权限,可分配给Group;
  • 视图级权限:PermissionRequiredMixin要求用户拥有指定权限才能访问视图;
  • 对象级权限:UserPassesTestMixin在test_func()中编写自定义逻辑(如return obj.author == self.request.user)。

这三层不是叠加,而是按需组合——简单场景用PermissionRequiredMixin,复杂业务逻辑用UserPassesTestMixin。

6.2 实现图书编辑权限:从Group分配到对象级校验

第一步:定义图书模型及权限

# books/models.py from django.db import models from django.contrib.auth.models import User class Book(models.Model): title = models.CharField(max_length=200) author = models.ForeignKey(User, on_delete=models.CASCADE, related_name='books') content = models.TextField() created_at = models.DateTimeField(auto_now_add=True) class Meta: # 自动为Book模型生成view_book, add_book, change_book, delete_book权限 permissions = [ ("can_edit_others_books", "Can edit books authored by others"), ]

第二步:创建Group并分配权限

# 进入Django shell docker-compose exec web python manage.py shell
# shell中执行 from django.contrib.auth.models import Group, Permission from django.contrib.contenttypes.models import ContentType from books.models import Book # 创建Editor组 editors_group = Group.objects.create(name='Editors') # 获取Book模型的content_type content_type = ContentType.objects.get_for_model(Book) # 获取自定义权限 permission = Permission.objects.get( codename='can_edit_others_books', content_type=content_type, ) # 将权限分配给Editor组 editors_group.permissions.add(permission) # 将用户加入组(例如用户ID=1) from django.contrib.auth import get_user_model User = get_user_model() user = User.objects.get(id=1) user.groups.add(editors_group)

第三步:图书编辑视图(对象级权限)

# books/views.py from django.contrib.auth.mixins import LoginRequiredMixin, UserPassesTestMixin from django.views.generic import UpdateView from .models import Book class BookUpdateView(LoginRequiredMixin, UserPassesTestMixin, UpdateView): model = Book fields = ['title', 'content <p> <a href="https://download.csdn.net/download/weixin_31569663/90830803" style="color:#ec7500;font-size:14px;"> 本文还有配套的精品资源,点击获取 </a> <img alt="menu-r.4af5f7ec.gif" src="https://csdnimg.cn/release/wenkucmsfe/public/img/menu-r.4af5f7ec.gif" style="width:16px;margin-left:4px;vertical-align:text-bottom;cursor:text;"> </p>
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进