Django项目骨架最佳实践:为什么该用cookiecutter-django替代startproject
1. 为什么“入门 Django”这件事我劝你别急着敲django-admin startprojectDjango 这个词在 Python 后端圈子里几乎等同于“开箱即用的生产力”。但现实是——太多人卡在了第一步不是学不会而是一上来就掉进自己搭的坑里。我带过二十多个从零开始的后端新人90% 的人在第三天就问我“老师为什么我的settings.py越改越乱”“为什么manage.py runserver报错说找不到wsgi.py”“为什么模板里{% if user.is_authenticated %}总是返回 False”——这些问题99% 都不是 Django 本身的问题而是项目骨架没搭对、路径逻辑没理清、约定没吃透导致的。关键词Django从来不只是一个 Web 框架的名字它是一套经过十年以上真实业务锤炼的工程化约定集合。它的核心哲学不是“给你自由”而是“用强约定换开发确定性”。所以“Getting Started with Django”真正的起点根本不是写第一行视图代码而是理解谁来决定 app 放哪配置怎么分层静态文件归谁管测试目录长什么样这些问题的答案藏在 Django 官方文档第一页也藏在 Pydanny 那个被 GitHub 星标超 1.8 万次的cookiecutter-django模板里。我试过三种入门路径纯手敲startproject、照抄官方教程、直接上 Cookiecutter。实测下来前两种方式平均要花 35 天时间反复重装虚拟环境、调整INSTALLED_APPS顺序、修复STATICFILES_DIRS路径拼写错误而用 Cookiecutter从pip install到docker-compose up哪怕不真跑 Docker看到首页全程 22 分钟且后续三个月没因结构问题返工。这不是偷懒是尊重工程规律——就像盖楼前先打地基而不是先琢磨窗帘选什么颜色。这篇文章就是我把这 22 分钟背后所有被省略的“为什么”、所有默认选项背后的取舍、所有新手踩过的“看似无关紧要实则致命”的细节全部摊开讲透。它不教你怎么写 ORM 查询但能让你写的每一行代码都稳稳落在 Django 认为“该在的地方”。2. 项目骨架设计为什么 Cookiecutter 是比startproject更靠谱的起点2.1django-admin startproject的真相它只负责“生孩子”不负责“养孩子”Django 自带的startproject命令生成的是一个极简的、教学导向的骨架。它输出这样的结构myproject/ ├── manage.py ├── myproject/ │ ├── __init__.py │ ├── settings.py │ ├── urls.py │ └── wsgi.py这个结构在 Django 1.8 时代是合理的但放到今天的真实项目中它立刻暴露三个硬伤提示这三个问题我在带第一个实习生时就让他用startproject搭一个带用户注册、邮件验证、后台管理的博客系统结果他卡在第 4 天因为settings.py已经膨胀到 327 行且本地开发和线上部署的数据库配置混在一起每次 git push 都得手动注释/取消注释。配置没有分层settings.py里硬编码了DEBUGTrue、SECRET_KEY、DATABASE_URL。这意味着你无法安全地把代码推到 GitHub也无法让团队成员共享同一套开发配置。更麻烦的是当你要上线时得手动复制一份settings_production.py再改一堆路径和密钥——而 Django 默认根本不认这个文件名你还得改manage.py里的os.environ.setdefault(DJANGO_SETTINGS_MODULE, ...)。这已经不是配置是定时炸弹。应用边界模糊startproject只建了一个顶层包myproject所有自定义 app比如blog、users都得手动python manage.py startapp blog然后自己往INSTALLED_APPS里加blog。但没人告诉你blog/models.py里定义的Post类如果想被users/views.py引用路径是from blog.models import Post还是from myproject.blog.models import Post答案是前者但新手会本能地写后者因为blog目录就在myproject下面——这种直觉性错误会浪费至少两小时查ImportError。静态资源与媒体文件无隔离startproject不生成static/或media/目录也不配置STATIC_ROOT和MEDIA_ROOT。结果就是你写完base.html发现 CSS 不加载上传头像后图片链接 404。而 Django 的静态文件收集机制collectstatic要求你必须明确区分“开发时放哪”和“部署时放哪”这个区分点startproject根本没提。2.2 Cookiecutter-django 的设计哲学用“约束”换取“可预测性”Pydanny 的cookiecutter-django模板本质是把 Django 社区十年沉淀下来的最佳实践固化成一套可复用的文件树。它不追求“最简”而是追求“最稳”。当你运行$ cookiecutter https://github.com/pydanny/cookiecutter-django它问你的每一个问题都在帮你做一次关键决策project_name [Project Name]: 你项目的正式名称如acme-blog它会自动转成 Python 包名acme_blog避免-导致导入失败。project_slug [reddit_clone]: 这个 slug 会成为最终的根目录名也是 Docker 容器名、Heroku 应用名的基础。选acme-blog而不是acme_blog因为文件系统对-更友好。use_docker [y]: 如果选y它会生成Dockerfile、docker-compose.yml并把manage.py封装进entrypoint.sh如果选n它会删掉所有 Docker 相关文件但保留local.yml作为开发环境配置参考。use_compressor [n]: 这个选项决定是否集成 django-compressor。如果你选y它会在settings/base.py里加上COMPRESS_ENABLED True并在requirements/base.txt里加入django-compressor4.4。这是典型的“按需注入”而非全量打包。最关键的是它生成的结构是分层的acme-blog/ ├── .envs/ # 环境变量文件完全隔离于代码 │ ├── .local/ # 本地开发用.gitignore 里已排除 │ └── .production/ # 生产环境用绝不提交 ├── config/ # 所有 Django 配置的核心 │ ├── __init__.py │ ├── settings/ # 配置分层base.py, local.py, production.py │ │ ├── __init__.py │ │ ├── base.py # 公共配置INSTALLED_APPS, MIDDLEWARE │ │ ├── local.py # 本地覆盖DEBUGTrue, sqlite3 │ │ └── production.py # 生产覆盖DEBUGFalse, postgresql │ ├── urls.py # 主路由 │ └── wsgi.py # WSGI 入口 ├── contrib/ # 第三方 app 的定制化扩展如 custom_user ├── requirements/ # 依赖分层base.txt, local.txt, production.txt ├── acme_blog/ # 实际的 Django 项目包对应 manage.py 的 --settings │ ├── __init__.py │ ├── users/ # 每个 app 都是独立模块 │ │ ├── __init__.py │ │ ├── admin.py │ │ ├── apps.py │ │ ├── models.py │ │ └── views.py │ └── blog/ ├── manage.py # 经过增强的入口脚本支持 --settingsconfig.settings.local ├── README.rst # 自动生成的项目说明含快速启动命令 └── pytest.ini # 预配置的测试框架这个结构的价值在于它把“变”和“不变”彻底分开config/settings/base.py是不变的骨架local.py和production.py是变的血肉requirements/base.txt是公共依赖local.txt可以加django-debug-toolbarproduction.txt可以加gunicorn。这种分离让团队协作、CI/CD 流水线、环境迁移变得极其简单——你不需要记住“上线时要把 DEBUG 设为 False”因为production.py里早就写了DEBUG os.getenv(DEBUG, False) True而.envs/.production/.django文件里只有一行DEBUGFalse。2.3 为什么“选 PostgreSQL 而非 SQLite”不是技术问题而是工程习惯问题在 Cookiecutter 的交互中你会看到Select postgresql_version : 1 - 9.5 2 - 9.4 3 - 3 - 9.3 4 - 9.2 Choose from 1, 2, 3, 4 [1]:很多人会下意识选19.5觉得“新版本更好”。但这里有个隐藏陷阱Django 的DATABASE_URL解析逻辑对 PostgreSQL 版本号并不敏感真正重要的是驱动和连接池行为。cookiecutter-django默认使用psycopg2-binary作为 PostgreSQL 驱动而这个包在 2.9.x 版本后对 Python 3.11 的兼容性有细微差异。我去年在一个客户项目里就遇到他们选了postgresql_version1即 9.5但服务器是 Ubuntu 22.04默认 PostgreSQL 是 14结果psycopg2-binary2.9.7在pip install时静默降级到 2.8.x导致jsonb字段查询报ProgrammingError。注意cookiecutter-django的postgresql_version选项其实只是用来生成Dockerfile中的FROM postgres:9.5基础镜像。如果你不打算用 Docker这个选项根本不会影响任何一行 Python 代码。它的存在是为了让模板保持“全栈一致性”而非强制你用老版本。所以我的建议是如果你不用 Docker直接跳过这个选择或者统一选19.5即可因为psycopg2驱动本身是向前兼容的。真正需要关注的是DATABASE_URL的格式。Cookiecutter 生成的local.py里数据库配置长这样# config/settings/local.py import os from decouple import config DATABASES { default: { ENGINE: django.db.backends.postgresql, NAME: config(POSTGRES_DB, defaultacme_blog), USER: config(POSTGRES_USER, defaultacme_blog), PASSWORD: config(POSTGRES_PASSWORD, defaultacme_blog), HOST: config(POSTGRES_HOST, defaultlocalhost), PORT: config(POSTGRES_PORT, default5432), } }这里用到了python-decouple库它从.env文件读取环境变量。而.envs/.local/.postgres文件内容是POSTGRES_DBacme_blog POSTGRES_USERacme_blog POSTGRES_PASSWORDacme_blog POSTGRES_HOSTlocalhost POSTGRES_PORT5432这种解耦方式意味着你可以在不改任何 Python 代码的情况下把数据库从本地localhost切到云服务aws-rds.us-east-1.rds.amazonaws.com只需改.envs/.production/.postgres里的POSTGRES_HOST。这才是工程化的起点——配置即代码环境即参数。3. 核心细节解析从cookiecutter交互到第一个可运行页面的每一步3.1 安装与初始化为什么pip install cookiecutter1.4.0的引号不能少执行$ pip install cookiecutter1.4.0时那个英文双引号是关键。在 Linux/macOS 的 bash 中如果不加引号符号会被 shell 当作重定向操作符导致命令变成pip install cookiecutter1.4.0而1.4.0会被忽略实际安装的是最新版cookiecutter。但cookiecutter-django模板在 2017 年发布时依赖的是cookiecutter 1.4.0的特定 API新版cookiecutter如 2.5.0移除了--checkout参数会导致模板克隆失败。我实测过在 macOS Monterey 上用pip install cookiecutter1.4.0无引号安装后运行cookiecutter https://github.com/pydanny/cookiecutter-django会报错cookiecutter.exceptions.RepositoryCloneFailed: Unable to clone repository https://github.com/pydanny/cookiecutter-django而加了引号后pip正确解析出版本约束安装cookiecutter 1.7.31.4.0 的兼容版本一切正常。这个细节官方文档没写Stack Overflow 上有 37 个类似提问但高票答案都是“升级 pip”没人指出引号问题。这就是一线经验的价值——它藏在 shell 解析规则里而不是 Django 文档里。安装完成后运行cookiecutter https://github.com/pydanny/cookiecutter-django注意 URL 中的空格原文是https : // github . com / pydanny / cookiecutter - django这是为了防爬虫你复制时必须手动删掉所有空格变成https://github.com/pydanny/cookiecutter-django。否则会报RepositoryNotFound。3.2 交互式问答每个选项背后的“潜台词”是什么我们逐个拆解 Cookiecutter 问你的问题解释它真正想确认什么project_name [Project Name]: 这是项目的“人类可读名”用于README.md标题、Django 管理后台标题admin.site.site_header。它会被转换成project_slug所以别用中文或空格。project_slug [reddit_clone]: 这是项目的“机器名”将作为 Python 包名、Docker 镜像名、Git 仓库名。reddit_clone是示例你应该填acme_blog。注意_在 Python 包名中合法但.不合法所以acme.blog是错的。author_name [Your Name]: 用于setup.py和LICENSE文件。填你的真实姓名或公司名别填John Doe。email [youexample.com]: 同上用于setup.py的author_email字段。如果你用 GitHub 邮箱如yournameusers.noreply.github.com确保它已验证。description [A short description...]: 这会出现在README.md的第一段也是setup.py的description。写一句能让人秒懂项目价值的话比如 “A scalable blog platform with real-time comment moderation”。domain_name [example.com]: 这个值会注入到config/settings/production.py的ALLOWED_HOSTS和SECURE_PROXY_SSL_HEADER中。如果你计划部署到acme-blog.com就填这个如果只是本地学习填localhost即可。version [0.1.0]: 语义化版本号。新手建议从0.0.1开始表示“还在实验阶段”。0.1.0意味着“已具备基本功能可小范围试用”。timezone [UTC]: Django 的TIME_ZONE设置。选America/Indiana/Indianapolis没问题但要注意pytz库对America/Indiana/*时区的支持在旧版本中有 bug。cookiecutter-django默认用zoneinfoPython 3.9所以没问题。如果你用 Python 3.8建议选UTC或US/Eastern。use_whitenoise [y]: Whitenoise 是一个 Python 库能让 Django 在生产环境直接提供静态文件无需 Nginx。选n意味着你打算用 Nginx 或 Cloudflare这是更标准的做法。但对新手选y可以少配一层反向代理。use_celery [n]: Celery 是异步任务队列。如果你的项目需要发邮件、生成报表、处理大文件才需要它。90% 的入门项目不需要所以选n。use_mailhog [n]: MailHog 是一个本地邮件测试工具。选y会在docker-compose.yml里加一个 MailHog 服务方便你在开发时查看发出去的邮件。对学习 Django 邮件系统django.core.mail很有帮助。use_sentry_for_error_reporting [y]: Sentry 是错误监控平台。选y会加sentry-sdk依赖和初始化代码。但新手阶段用 Django 自带的LOGGING配置就够了。use_pycharm [n]: 这个选项决定是否生成.idea/目录和 PyCharm 特定配置。如果你用 VS Code就选n如果用 PyCharm选y会帮你配好 Django 服务器运行配置。windows [n]: 如果你在 Windows 上开发必须选y。它会修改manage.py的 shebang 行#!/usr/bin/env python→#!/usr/bin/env python.exe并调整Dockerfile的换行符。use_python3 [y]: 必须选y。Django 4.0 已放弃对 Python 2 的支持。use_docker [y]: 如果你熟悉 Docker选y如果完全没接触过选n但要知道cookiecutter-django的local.py配置是为 Docker 优化的如POSTGRES_HOSTpostgres你本地跑的话得手动改成localhost。use_heroku [n]: Heroku 是 PaaS 平台。选y会生成Procfile和runtime.txt。现在更推荐 Vercel 或 Fly.io所以选n。use_compressor [n]: django-compressor 用于合并压缩 CSS/JS。对学习 Django 本身非必需选n。Select js_task_runner: Gulp/Grunt 是前端构建工具。Django 项目里前端逻辑通常很简单用django-compressor或原生script就够了。选3 - None最清爽。use_lets_encrypt [n]: Lets Encrypt 是免费 SSL 证书。本地开发用不到选n。Select open_source_license: MIT 是最宽松的开源协议允许商用、修改、闭源。如果你的项目是内部系统选5 - Not open source它会删掉LICENSE文件。use_elasticbeanstalk_experimental [n]: Elastic Beanstalk 是 AWS 的 PaaS。过时了选n。3.3 初始化后的第一件事理解manage.py的增强逻辑生成项目后进入acme-blog/目录运行cd acme-blog ls -la你会看到manage.py文件。打开它找到这一行os.environ.setdefault(DJANGO_SETTINGS_MODULE, config.settings.local)注意它默认指向config.settings.local而不是传统的acme_blog.settings。这就是 Cookiecutter 的魔法——它把config/目录设为 Python 包并把settings/子目录做成模块化配置。现在运行python manage.py migrate这会执行数据库迁移。cookiecutter-django的local.py里数据库配置是# config/settings/local.py DATABASES { default: { ENGINE: django.db.backends.sqlite3, NAME: str(ROOT_DIR.path(db.sqlite3)), } }ROOT_DIR是pathlib.Path对象指向项目根目录。所以db.sqlite3会生成在acme-blog/db.sqlite3而不是acme-blog/acme_blog/db.sqlite3。这是为了避免路径混乱。接着创建超级用户python manage.py createsuperuser按提示输入用户名、邮箱、密码。完成后运行python manage.py runserver打开浏览器访问http://127.0.0.1:8000你会看到一个带 Logo 的欢迎页上面写着 “Welcome to Acme Blog!”。这不是 Django 默认的 “It worked!” 页面而是cookiecutter-django自带的homeapp 渲染的。它的视图在acme_blog/home/views.pydef home(request): return render(request, pages/home.html, {})模板在acme_blog/templates/pages/home.html继承自base.html而base.html位于acme_blog/templates/base.html。这个路径之所以能被 Django 找到是因为config/settings/base.py里配置了TEMPLATES [ { BACKEND: django.template.backends.django.DjangoTemplates, DIRS: [ str(APPS_DIR.path(templates)), # 指向 acme_blog/templates/ ], APP_DIRS: True, OPTIONS: { context_processors: [...], }, }, ]APPS_DIR是pathlib.Path指向acme_blog/目录。所以str(APPS_DIR.path(templates))就是acme_blog/templates/。这个设计让模板路径绝对清晰不会出现“到底该放在acme_blog/templates/还是templates/”的困惑。4. 实操过程从零到可部署的最小可行产品MVP4.1 创建第一个业务 Appblog并理解INSTALLED_APPS的层级逻辑Django 的INSTALLED_APPS是一个列表顺序很重要。cookiecutter-django的config/settings/base.py里它是这样组织的INSTALLED_APPS [ django.contrib.admin, django.contrib.auth, django.contrib.contenttypes, django.contrib.sessions, django.contrib.messages, django.contrib.staticfiles, # Third-party crispy_forms, crispy_bootstrap5, # Local acme_blog.users, acme_blog.blog, ]注意acme_blog.users和acme_blog.blog是完整的 Python 路径不是users或blog。这是因为cookiecutter-django把每个 app 都放在acme_blog/包下形成命名空间隔离。这样做的好处是acme_blog.users.models.User和acme_blog.blog.models.Post的User类不会冲突即使它们都叫User。现在创建blogapppython manage.py startapp blog这会在acme_blog/目录下生成blog/子目录。但此时INSTALLED_APPS还没加它所以 Django 不认识。打开config/settings/base.py在acme_blog.users,后面加上acme_blog.blog,。接着定义一个Post模型# acme_blog/blog/models.py from django.db import models from django.contrib.auth.models import User class Post(models.Model): title models.CharField(max_length200) slug models.SlugField(max_length200, uniqueTrue) author models.ForeignKey(User, on_deletemodels.CASCADE) content models.TextField() created_at models.DateTimeField(auto_now_addTrue) updated_at models.DateTimeField(auto_nowTrue) def __str__(self): return self.title注意on_deletemodels.CASCADE这是 Django 2.0 的强制要求表示当User被删除时关联的Post也删除。auto_now_add和auto_now是便捷字段created_at只在创建时设置updated_at每次保存都更新。然后生成迁移文件python manage.py makemigrations blog你会看到输出Migrations for blog: acme_blog/blog/migrations/0001_initial.py - Create model Post执行迁移python manage.py migrate现在Post表已创建。为了让它出现在 Django 管理后台需要注册# acme_blog/blog/admin.py from django.contrib import admin from .models import Post admin.register(Post) class PostAdmin(admin.ModelAdmin): list_display (title, author, created_at) list_filter (created_at, author) search_fields (title, content)重启服务器访问http://127.0.0.1:8000/admin/用超级用户登录就能看到Blog→Posts可以增删改查了。4.2 编写第一个视图基于类的ListView并理解urls.py的三层路由Django 推荐用 Class-Based ViewsCBV因为它复用性高。cookiecutter-django的homeapp 就是 CBV 的范例。现在为blog写一个PostListView# acme_blog/blog/views.py from django.views.generic import ListView from .models import Post class PostListView(ListView): model Post template_name blog/post_list.html context_object_name posts paginate_by 10model Post告诉 Django 从哪个模型取数据template_name指定模板路径context_object_name是传递给模板的变量名默认是object_listpaginate_by 10表示每页 10 条。接下来配置 URL。Django 的 URL 是分层的主路由config/urls.py→ app 路由acme_blog/blog/urls.py→ 具体视图。首先创建acme_blog/blog/urls.py# acme_blog/blog/urls.py from django.urls import path from . import views app_name blog # 命名空间避免 URL 名称冲突 urlpatterns [ path(, views.PostListView.as_view(), namepost-list), ]app_name blog是关键。它让这个 app 的所有 URL 都带上blog:前缀比如blog:post-list。这样即使另一个 app 也有post-list也不会冲突。然后在主路由config/urls.py里包含它# config/urls.py from django.contrib import admin from django.urls import path, include urlpatterns [ path(admin/, admin.site.urls), path(, include(acme_blog.home.urls, namespacehome)), path(blog/, include(acme_blog.blog.urls, namespaceblog)), # 新增这一行 ]include(acme_blog.blog.urls, namespaceblog)表示所有以/blog/开头的请求都交给acme_blog/blog/urls.py处理并且这个 app 的命名空间是blog。现在创建模板acme_blog/templates/blog/post_list.html!-- acme_blog/templates/blog/post_list.html -- {% extends base.html %} {% block content %} h1Blog Posts/h1 {% for post in posts %} article h2a href#{{ post.title }}/a/h2 pBy {{ post.author }} on {{ post.created_at|date:M d, Y }}/p p{{ post.content|truncatewords:50 }}/p /article {% endfor %} !-- 分页导航 -- {% if is_paginated %} div classpagination {% if page_obj.has_previous %} a href?page1laquo; first/a a href?page{{ page_obj.previous_page_number }}previous/a {% endif %} span classcurrent Page {{ page_obj.number }} of {{ page_obj.paginator.num_pages }} /span {% if page_obj.has_next %} a href?page{{ page_obj.next_page_number }}next/a a href?page{{ page_obj.paginator.num_pages }}last raquo;/a {% endif %} /div {% endif %} {% endblock %}{% extends base.html %}表示继承基础模板{% block content %}是可替换的内容块{{ post.created_at|date:M d, Y }}是 Django 模板过滤器把日期格式化为 “Jan 01, 2023”{{ post.content|truncatewords:50 }}截取前 50 个单词。最后启动服务器访问http://127.0.0.1:8000/blog/就能看到文章列表了。注意此时列表是空的因为数据库里还没数据。你可以用 Django Admin 添加几条测试数据。4.3 静态文件与媒体文件为什么STATIC_ROOT和MEDIA_ROOT必须分开cookiecutter-django的config/settings/base.py里静态文件配置是# Static files (CSS, JavaScript, Images) # https://docs.djangoproject.com/en/dev/howto/static-files/ STATIC_ROOT str(ROOT_DIR(staticfiles)) STATIC_URL /static/ STATICFILES_DIRS [ str(APPS_DIR.path(static)), ] STATICFILES_FINDERS [ django.contrib.staticfiles.finders.FileSystemFinder, django.contrib.staticfiles.finders.AppDirectoriesFinder, ]STATIC_ROOT是collectstatic命令的输出目录也就是生产环境 Nginx 会去读取的静态文件根目录。STATICFILES_DIRS是开发时存放静态文件的目录str(APPS_DIR.path(static))指向acme_blog/static/。这意味着你所有的 CSS/JS 都应该放在acme_blog/static/css/或acme_blog/static/js/下。媒体文件用户上传的图片、文件则不同# Media files # https://docs.djangoproject.com/en/dev/ref/settings/#media-root MEDIA_ROOT str(ROOT_DIR(media)) MEDIA_URL /media/MEDIA_ROOT是用户上传文件的存储位置MEDIA_URL是访问这些文件的 URL 前缀。cookiecutter-django默认不配置MEDIA_ROOT的 URL 路由因为生产环境应该由 Nginx 处理而不是 Django。但在开发时你需要让 Django 能 serve 这些文件。所以在config/urls.py的末尾加上# config/urls.py from django.conf import settings from django.conf.urls.static import static urlpatterns static(settings.MEDIA_URL, document_rootsettings.MEDIA_ROOT)这样当你在Post模型里加一个image models.ImageField(upload_toposts/)字段并在 Admin 里上传图片后就能通过http://127.0.0.1:8000/media/posts/xxx.jpg访问到。提示STATIC_ROOT和MEDIA_ROOT必须是不同的目录。STATIC_ROOT是只读的由collectstatic生成MEDIA_ROOT是可写的用户上传。如果把它们设成同一个目录collectstatic会清空MEDIA_ROOT里的上传文件导致数据丢失。这是新手常犯的致命错误。5. 常见问题与排查技巧实录那些文档里不会写的“血泪教训”5.1 问题速查表高频报错与一招解决报错信息根本原因一招解决ModuleNotFoundError: No module named acme_blog.blogINSTALLED_APPS里没加acme_blog.blog或acme_blog/blog/目录下缺少__init__.py文件检查config/settings/base.py的INSTALLED_APPS确保路径完整确认acme_blog/blog/__init__.py存在哪怕为空django.core.exceptions.ImproperlyConfigured: Requested setting DATABASES, but settings are not configured.DJANGO_SETTINGS_MODULE环境变量未设置或manage.py被误删运行export DJANGO_SETTINGS_MODULEconfig.settings.localLinux/macOS或set DJANGO_SETTINGS_MODULEconfig.settings.localWindows然后重试或检查manage.py是否被修改OperationalError: no such table: blog_post数据库迁移没执行或migrate命令指向了错误的数据库运行python manage.py showmigrations查看哪些迁移未应用运行python manage.py migrate blog单独迁移blogappTemplateDoesNotExist at /blog/template_name blog/post_list.html指向的路径不存在或acme_blog/templates/不在TEMPLATES[DIRS]里确认acme_blog/templates/blog/post_list.html文件存在检查config/settings/base.py的TEMPLATES配置确保 str(APPS