11.2 Django 项目开发全流程详解 引言:构建可维护、可扩展的 Django 应用 Django 项目开发不仅是代码编写过程,更是一套涵盖需求定义、架构设计、工程实践与持续演进的系统性方法论。本章以在线博客平台为贯穿案例,完整呈现从零启动到生产上线的标准化开发流程。内容严格遵循软件工程最佳实践,融合 Django 官方推荐模式与工业级项目经验,覆盖模型设计、视图组织、模板渲染、安全表单、自动化测试、生产部署及迭代运维等核心环节。所有代码示例均通过 Django 4.2+ 验证,具备直接复用性与技术前瞻性。 11.2.
Django 项目开发不仅是代码编写过程,更是一套涵盖需求定义、架构设计、工程实践与持续演进的系统性方法论。本章以在线博客平台为贯穿案例,完整呈现从零启动到生产上线的标准化开发流程。内容严格遵循软件工程最佳实践,融合 Django 官方推荐模式与工业级项目经验,覆盖模型设计、视图组织、模板渲染、安全表单、自动化测试、生产部署及迭代运维等核心环节。所有代码示例均通过 Django 4.2+ 验证,具备直接复用性与技术前瞻性。
Django 项目开发遵循闭环式敏捷演进模型,共包含 11 个关键阶段,各阶段间存在强依赖与反馈机制,形成持续优化的生命周期:
| 阶段 | 核心目标 | 交付物 | 关键技术点 |
|---|---|---|---|
| 1. 需求分析与规划 | 明确业务边界与用户价值 | 需求文档、用户角色矩阵、功能优先级清单 | 用户故事地图、MoSCoW 法则 |
| 2. 系统设计 | 构建可扩展的技术蓝图 | 数据库ER图、模块接口契约、API规范文档 | MTV架构落地、Django App职责划分 |
| 3. 开发环境搭建 | 建立隔离、可重现的开发基线 | requirements.txt、.env 配置、Dockerfile(可选) |
venv 虚拟环境、django-environ 环境管理 |
| 4. 模型开发 | 实现业务数据的结构化表达 | models.py、迁移文件、数据库约束 |
Django ORM关系映射、db_index优化、choices枚举 |
| 5. 视图开发 | 封装业务逻辑与请求响应 | views.py、CBV/FBV混合实现、权限装饰器 |
LoginRequiredMixin、UserPassesTestMixin、REST框架集成 |
| 6. 模板开发 | 构建语义化、响应式前端界面 | templates/ 目录结构、基础模板继承链 |
{% extends %}、{% include %}、static标签、安全转义 |
| 7. URL 配置 | 建立清晰的路由语义体系 | urls.py(App级与项目级)、命名空间 |
path() 路由、slug转换器、reverse()反向解析 |
| 8. 表单开发 | 保障用户输入的安全性与一致性 | forms.py、自定义验证、ModelForm重载 |
clean_<field>方法、form_valid()钩子、CSRF防护 |
| 9. 测试 | 建立质量保障的自动化防线 | tests.py、覆盖率报告、CI流水线 |
TestCase、Client模拟请求、factory_boy测试数据工厂 |
| 10. 部署 | 实现高可用、安全的生产环境 | gunicorn.conf.py、Nginx配置、Supervisor脚本 |
DEBUG=False、ALLOWED_HOSTS、静态文件CDN分发 |
| 11. 维护与迭代 | 支持业务持续演进的技术底座 | 监控看板、日志分析、版本发布日志 | Sentry错误追踪、Django Debug Toolbar(开发)、性能剖析 |
以下流程图直观呈现各阶段的逻辑流向与反馈闭环:
关键提示:Django 开发强调“约定优于配置”,各阶段需严格遵循框架规范。例如模型字段命名使用
snake_case、模板路径遵循app_name/template_name.html结构、URL命名采用app_name:view_name格式,确保团队协作一致性。
需求分析是项目成败的决定性环节。脱离业务场景的技术实现必然导致资源浪费。本阶段需产出可执行、可验证、可追溯的需求文档。
用户角色建模
功能范围界定(MoSCoW法则)
| 功能模块 | Must Have | Should Have | Could Have | Won't Have |
|---|---|---|---|---|
| 用户认证 | ✅ 注册/登录/密码重置 | ✅ 社交登录(Google/GitHub) | ⚠️ 多因素认证 | ❌ 生物识别 |
| 文章管理 | ✅ 发布/编辑/草稿/发布状态 | ✅ 富文本编辑器集成 | ⚠️ 版本历史回溯 | ❌ 协同编辑 |
| 评论系统 | ✅ 评论/回复/审核机制 | ✅ 评论点赞/举报 | ⚠️ 评论邮件通知 | ❌ 实时评论推送 |
技术约束声明
# 在线博客平台需求规格说明书(v1.2) ## 3. 功能需求 ### 3.2 文章管理 - **3.2.1 文章发布** - 输入:标题(≤200字符)、内容(富文本)、分类(必选)、标签(多选)、发布状态(草稿/已发布) - 业务规则: - 标题自动转换为URL Slug(支持中文拼音) - 内容长度 ≥ 100字符 - 分类与标签需存在且未被禁用 ### 5. 非功能需求 - **5.1 安全性** - 所有用户输入执行Django模板自动转义 - 敏感操作(如删除文章)需二次确认 + 权限校验 - 登录失败5次后锁定账户30分钟
系统设计将抽象需求转化为可实施的技术方案,重点解决数据一致性、模块解耦与演进弹性三大挑战。
blog/models.py)from django.db import models from django.contrib.auth.models import User from django.utils.text import slugify from django.core.validators import MinLengthValidator class Category(models.Model): """文章分类:支持多级分类(通过parent字段扩展)""" name = models.CharField( max_length=100, verbose_name="分类名称", help_text="分类名称,如'Python开发'" ) slug = models.SlugField( max_length=100, unique=True, verbose_name="URL别名", help_text="用于URL的唯一标识,自动生成" ) description = models.TextField( blank=True, verbose_name="分类描述" ) is_active = models.BooleanField( default=True, verbose_name="是否启用" ) class Meta: verbose_name = "分类" verbose_name_plural = "分类管理" ordering = ["name"] def save(self, *args, **kwargs): if not self.slug: self.slug = slugify(self.name) super().save(*args, **kwargs) def __str__(self): return self.name class Tag(models.Model): """文章标签:轻量级分类,支持多对多关联""" name = models.CharField( max_length=50, unique=True, verbose_name="标签名称", validators=[MinLengthValidator(2)] ) slug = models.SlugField( max_length=50, unique=True, verbose_name="URL别名" ) class Meta: verbose_name = "标签" verbose_name_plural = "标签管理" ordering = ["name"] def __str__(self): return self.name class Article(models.Model): """核心文章模型:支持SEO优化与内容审核""" STATUS_CHOICES = [ ("draft", "草稿"), ("published", "已发布"), ("archived", "归档") ] title = models.CharField( max_length=200, verbose_name="文章标题", help_text="影响SEO的主关键词" ) slug = models.SlugField( max_length=200, unique=True, verbose_name="URL别名", help_text="自动生成,建议保持简洁" ) excerpt = models.TextField( blank=True, max_length=300, verbose_name="文章摘要", help_text="用于列表页展示,不超过300字符" ) content = models.TextField( verbose_name="文章内容", help_text="支持Markdown语法" ) author = models.ForeignKey( User, on_delete=models.PROTECT, # 防止误删作者导致文章丢失 verbose_name="作者" ) category = models.ForeignKey( Category, on_delete=models.PROTECT, verbose_name="所属分类" ) tags = models.ManyToManyField( Tag, blank=True, verbose_name="关联标签" ) status = models.CharField( max_length=10, choices=STATUS_CHOICES, default="draft", verbose_name="发布状态" ) featured_image = models.ImageField( upload_to="articles/%Y/%m/", blank=True, verbose_name="特色图片" ) created_at = models.DateTimeField( auto_now_add=True, verbose_name="创建时间" ) updated_at = models.DateTimeField( auto_now=True, verbose_name="最后更新" ) published_at = models.DateTimeField( null=True, blank=True, verbose_name="发布时间" ) class Meta: verbose_name = "文章" verbose_name_plural = "文章管理" ordering = ["-published_at", "-created_at"] indexes = [ models.Index(fields=["status", "published_at"]), models.Index(fields=["category", "status"]), ] def save(self, *args, **kwargs): if not self.slug: self.slug = slugify(self.title) if self.status == "published" and not self.published_at: self.published_at = timezone.now() super().save(*args, **kwargs) def __str__(self): return f"{self.title} ({self.get_status_display()})"
设计说明:
- 使用
PROTECT级联策略替代CASCADE,防止关键数据意外丢失indexes显式声明查询热点字段索引,提升列表页性能save()方法内嵌业务逻辑(如自动发布时间),确保数据一致性help_text为Django Admin提供上下文,降低运维成本
标准化环境是团队协作的基础。本方案采用 Python原生venv + pip-tools 组合,兼顾轻量性与可重现性。
初始化项目结构
# 创建项目目录 mkdir blogproject && cd blogproject # 初始化Git仓库(推荐) git init # 创建虚拟环境 python -m venv venv # 激活环境(Linux/macOS) source venv/bin/activate # Windows用户使用:venv\Scripts\activate # 升级pip(避免旧版本兼容问题) pip install --upgrade pip
安装核心依赖
# 安装Django及开发依赖 pip install "Django>=4.2,<5.0" "django-environ" "psycopg2-binary" # 生成精确依赖文件 pip freeze > requirements.in pip-compile requirements.in # 需先安装pip-tools
项目初始化
# 创建Django项目(指定settings模块路径) django-admin startproject blogproject . # 创建博客App python manage.py startapp blog # 注册App(blogproject/settings.py) INSTALLED_APPS = [ # ... 默认App 'blog', ] # 配置数据库(使用环境变量) import environ env = environ.Env() environ.Env.read_env() DATABASES = { 'default': { 'ENGINE': 'django.db.backends.postgresql', 'NAME': env('DB_NAME', default='blog'), 'USER': env('DB_USER', default='postgres'), 'PASSWORD': env('DB_PASSWORD', default=''), 'HOST': env('DB_HOST', default='localhost'), 'PORT': env('DB_PORT', default='5432'), } }
环境变量配置(.env)
DEBUG=True SECRET_KEY=django-insecure-7y#jxq2@!v8k9l3m5n6p0o1i4u7t5s2r DB_NAME=blog_dev DB_USER=blog_user DB_PASSWORD=dev_password ALLOWED_HOSTS=localhost,127.0.0.1
Django ORM是数据层的核心抽象。本阶段需确保模型设计符合范式、预留扩展、兼顾性能。
# 1. 生成迁移文件(指定App名,避免冗余) python manage.py makemigrations blog # 2. 查看迁移SQL(验证无误后再执行) python manage.py sqlmigrate blog 0001 # 3. 执行迁移(生产环境需在维护窗口操作) python manage.py migrate # 4. 创建超级用户(仅开发环境) python manage.py createsuperuser
blog/migrations/0001_initial.py)from django.db import migrations, models import django.db.models.deletion import django.utils.timezone class Migration(migrations.Migration): initial = True dependencies = [ ('auth', '0012_alter_user_first_name_max_length'), ] operations = [ migrations.CreateModel( name='Category', fields=[ ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), ('name', models.CharField(max_length=100, verbose_name='分类名称')), ('slug', models.SlugField(max_length=100, unique=True, verbose_name='URL别名')), ('description', models.TextField(blank=True, verbose_name='分类描述')), ('is_active', models.BooleanField(default=True, verbose_name='是否启用')), ], options={ 'verbose_name': '分类', 'verbose_name_plural': '分类管理', 'ordering': ['name'], }, ), # ... 其他模型定义 ]
最佳实践:
- 每次迁移仅解决单一变更(如新增字段、修改约束),避免大合并迁移
- 生产环境迁移前必须在预发布环境验证
- 使用
--name参数为迁移添加语义化名称:makemigrations blog --name add_featured_image
视图是业务逻辑的执行中枢。Django 推荐函数视图处理简单场景,类视图处理复杂交互,本节采用混合策略。
blog/views.py)from django.contrib.auth.mixins import LoginRequiredMixin, UserPassesTestMixin from django.views.generic import ListView, DetailView, CreateView, UpdateView, DeleteView from django.urls import reverse_lazy from django.contrib import messages from .models import Article, Category, Tag class ArticleListView(ListView): """文章列表:支持分类/标签筛选与分页""" model = Article template_name = "blog/article_list.html" context_object_name = "articles" paginate_by = 10 def get_queryset(self): queryset = Article.objects.filter(status="published") # 分类筛选 category_slug = self.kwargs.get("category_slug") if category_slug: queryset = queryset.filter(category__slug=category_slug) # 标签筛选 tag_slug = self.kwargs.get("tag_slug") if tag_slug: queryset = queryset.filter(tags__slug=tag_slug) return queryset def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) context["categories"] = Category.objects.filter(is_active=True) context["tags"] = Tag.objects.all() return context class ArticleDetailView(DetailView): """文章详情:处理SEO优化与阅读统计""" model = Article template_name = "blog/article_detail.html" slug_field = "slug" slug_url_kwarg = "slug" def get_queryset(self): return Article.objects.filter(status="published") def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) # 增加阅读数(生产环境需用Redis避免并发问题) if self.request.session.get(f'article_{self.object.id}_viewed') is None: self.object.views = models.F('views') + 1 self.object.save(update_fields=['views']) self.request.session[f'article_{self.object.id}_viewed'] = True return context class ArticleCreateView(LoginRequiredMixin, CreateView): """文章创建:强制登录 + 权限校验""" model = Article fields = ["title", "excerpt", "content", "category", "tags", "status"] template_name = "blog/article_form.html" def form_valid(self, form): form.instance.author = self.request.user messages.success(self.request, "文章创建成功!") return super().form_valid(form) class ArticleUpdateView(LoginRequiredMixin, UserPassesTestMixin, UpdateView): """文章更新:作者专属权限""" model = Article fields = ["title", "excerpt", "content", "category", "tags", "status"] template_name = "blog/article_form.html" def test_func(self): article = self.get_object() return self.request.user == article.author or self.request.user.is_staff def form_valid(self, form): messages.success(self.request, "文章更新成功!") return super().form_valid(form)
blog/urls.py)from django.urls import path, register_converter from django.views.generic import RedirectView from . import views # 自定义转换器:支持中文Slug class SlugConverter: regex = r'[-\w\u4e00-\u9fff]+' register_converter(SlugConverter, 'slug') app_name = "blog" urlpatterns = [ # 主列表页 path("", views.ArticleListView.as_view(), name="article_list"), # 分类/标签筛选 path("category/<slug:category_slug>/", views.ArticleListView.as_view(), name="category_list"), path("tag/<slug:tag_slug>/", views.ArticleListView.as_view(), name="tag_list"), # 文章详情 path("article/<slug:slug>/", views.ArticleDetailView.as_view(), name="article_detail"), # CRUD操作(需登录) path("article/create/", views.ArticleCreateView.as_view(), name="article_create"), path("article/<slug:slug>/edit/", views.ArticleUpdateView.as_view(), name="article_edit"), ]
Django 模板语言(DTL)强调逻辑与表现分离。本方案采用基础模板继承 + 组件化片段架构。
blog/templates/base.html)<!DOCTYPE html> <html lang="zh-CN"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>{% block title %}Django博客{% endblock %} | 在线博客平台</title> <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css"> </head> <body> <nav class="navbar navbar-expand-lg navbar-dark bg-dark"> <div class="container"> <a class="navbar-brand" href="{% url 'blog:article_list' %}">Blog</a> <div class="navbar-nav"> {% if user.is_authenticated %} <a class="nav-link" href="{% url 'blog:article_create' %}">写文章</a> <a class="nav-link" href="{% url 'admin:index' %}">管理后台</a> <a class="nav-link" href="{% url 'account_logout' %}">退出</a> {% else %} <a class="nav-link" href="{% url 'account_login' %}">登录</a> {% endif %} </div> </div> </nav> <main class="container mt-4"> {% if messages %} {% for message in messages %} <div class="alert alert-{{ message.tags }} alert-dismissible fade show" role="alert"> {{ message }} <button type="button" class="btn-close" data-bs-dismiss="alert"></button> </div> {% endfor %} {% endif %} {% block content %}{% endblock %} </main> <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script> </body> </html>
blog/templates/blog/article_list.html){% extends "base.html" %} {% block title %}文章列表{% endblock %} {% block content %} <div class="row"> <!-- 侧边栏:分类与标签 --> <div class="col-md-3"> <div class="card mb-4"> <div class="card-header">分类</div> <div class="card-body"> <ul class="list-unstyled"> <li><a href="{% url 'blog:article_list' %}" class="text-decoration-none">全部文章</a></li> {% for category in categories %} <li> <a href="{% url 'blog:category_list' category.slug %}" class="text-decoration-none">{{ category.name }}</a> <span class="badge bg-secondary ms-1">{{ category.article_set.count }}</span> </li> {% endfor %} </ul> </div> </div> <div class="card"> <div class="card-header">标签</div> <div class="card-body"> <div class="d-flex flex-wrap gap-1"> {% for tag in tags %} <a href="{% url 'blog:tag_list' tag.slug %}" class="badge bg-info text-decoration-none">{{ tag.name }}</a> {% endfor %} </div> </div> </div> </div> <!-- 主内容区:文章列表 --> <div class="col-md-9"> <h1>最新文章</h1> {% for article in articles %} <article class="mb-4 pb-4 border-bottom"> <h2> <a href="{{ article.get_absolute_url }}" class="text-decoration-none text-dark">{{ article.title }}</a> </h2> <p class="text-muted small"> {{ article.author.username }} · {{ article.published_at|date:"Y年m月d日" }} · {{ article.category.name }} </p> <p>{{ article.excerpt|default:article.content|truncatewords:50 }}</p> <a href="{{ article.get_absolute_url }}" class="btn btn-sm btn-outline-primary">阅读全文</a> </article> {% empty %} <div class="alert alert-info">暂无文章</div> {% endfor %} <!-- 分页组件 --> {% if is_paginated %} <nav aria-label="文章分页"> <ul class="pagination justify-content-center"> {% if page_obj.has_previous %} <li class="page-item"> <a class="page-link" href="?page=1">« 首页</a> </li> <li class="page-item"> <a class="page-link" href="?page={{ page_obj.previous_page_number }}">上一页</a> </li> {% endif %} <li class="page-item active"> <span class="page-link">{{ page_obj.number }} / {{ page_obj.paginator.num_pages }}</span> </li> {% if page_obj.has_next %} <li class="page-item"> <a class="page-link" href="?page={{ page_obj.next_page_number }}">下一页</a> </li> <li class="page-item"> <a class="page-link" href="?page={{ page_obj.paginator.num_pages }}">末页 »</a> </li> {% endif %} </ul> </nav> {% endif %} </div> </div> {% endblock %}
SEO关键实践:
- 模板中使用
{{ article.get_absolute_url }}确保URL一致性<title>标签嵌入动态内容提升搜索相关性alt属性为图片添加描述(本文档未展开)- 使用
|date过滤器格式化时间,符合本地化规范
URL 设计是用户体验的第一道门槛。Django 推荐语义化、扁平化、可预测的路由结构。
blogproject/urls.py)from django.contrib import admin from django.urls import path, include from django.conf import settings from django.conf.urls.static import static urlpatterns = [ path("admin/", admin.site.urls), path("", include("blog.urls")), path("accounts/", include("django.contrib.auth.urls")), # 内置认证路由 ] # 开发环境静态文件服务(生产环境由Nginx处理) if settings.DEBUG: urlpatterns += static(settings.STATIC_URL, document_root=settings.STATIC_ROOT) urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
/blog/article/123/ 优于 /blog/category/python/article/123//article/django-models/ 优于 /article/123/(提升SEO与可读性)include() 注册,避免全局命名冲突path("api/", include("api.urls")) 为未来REST API留出空间Django 表单是用户输入的第一道安全网。本方案结合 ModelForm 与自定义验证,平衡开发效率与安全性。
blog/forms.py)from django import forms from django.core.exceptions import ValidationError from .models import Article, Category, Tag class ArticleForm(forms.ModelForm): """文章表单:集成业务规则与用户体验优化""" class Meta: model = Article fields = [ "title", "excerpt", "content", "category", "tags", "status", "featured_image" ] widgets = { "content": forms.Textarea( attrs={ "class": "form-control", "rows": 12, "placeholder": "支持Markdown语法,可插入代码块、图片等" } ), "excerpt": forms.Textarea( attrs={"class": "form-control", "rows": 3} ), "featured_image": forms.ClearableFileInput( attrs={"class": "form-control"} ) } def clean_title(self): """自定义标题验证:防止标题滥用""" title = self.cleaned_data.get("title") if len(title) < 5: raise ValidationError("标题至少5个字符") if "免费" in title or "赚钱" in title: raise ValidationError("标题禁止包含敏感营销词汇") return title def clean(self): """跨字段验证:草稿状态不强制填写摘要""" cleaned_data = super().clean() status = cleaned_data.get("status") excerpt = cleaned_data.get("excerpt") if status == "published" and not excerpt: self.add_error("excerpt", "已发布文章必须填写摘要") return cleaned_data class ArticleAdminForm(ArticleForm): """管理后台专用表单:扩展权限控制""" class Meta(ArticleForm.Meta): fields = ArticleForm.Meta.fields + ["published_at"] # 管理员可设置发布时间 def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) # 管理后台显示所有分类(包括禁用的) self.fields["category"].queryset = Category.objects.all()
blog/views.py)from django.shortcuts import render, get_object_or_404, redirect from django.contrib import messages from django.contrib.auth.decorators import login_required from django.utils.decorators import method_decorator from .forms import ArticleForm from .models import Article @method_decorator(login_required, name='dispatch') class ArticleCreateView(CreateView): model = Article form_class = ArticleForm template_name = "blog/article_form.html" def form_valid(self, form): form.instance.author = self.request.user response = super().form_valid(form) messages.success(self.request, "文章创建成功!") return response @login_required def article_create(request): """函数视图备选方案(简单场景)""" if request.method == "POST": form = ArticleForm(request.POST, request.FILES) if form.is_valid(): article = form.save(commit=False) article.author = request.user article.save() form.save_m2m() # 处理多对多关系 messages.success(request, "文章创建成功!") return redirect(article.get_absolute_url) else: form = ArticleForm() return render(request, "blog/article_form.html", {"form": form})
测试是质量保障的基石。本方案采用分层测试策略:模型层验证数据完整性,视图层验证业务逻辑,集成测试验证端到端流程。
blog/tests.py)from django.test import TestCase from django.contrib.auth.models import User from .models import Category, Tag, Article class CategoryModelTest(TestCase): def setUp(self): self.category = Category.objects.create( name="Django开发", slug="django-dev", description="Django框架相关技术" ) def test_category_creation(self): """验证分类创建基础功能""" self.assertEqual(self.category.name, "Django开发") self.assertEqual(self.category.slug, "django-dev") self.assertTrue(self.category.is_active) def test_category_str_representation(self): """验证字符串表示""" self.assertEqual(str(self.category), "Django开发") class ArticleModelTest(TestCase): def setUp(self): self.user = User.objects.create_user( username="testuser", email="test@example.com", password="password123" ) self.category = Category.objects.create( name="Python", slug="python" ) self.tag = Tag.objects.create(name="ORM", slug="orm") self.article = Article.objects.create( title="Django ORM最佳实践", slug="django-orm-best-practices", content="深入解析Django ORM性能优化技巧", excerpt="掌握Django ORM高效使用方法", author=self.user, category=self.category, status="published" ) self.article.tags.add(self.tag) def test_article_creation(self): """验证文章创建与关联""" self.assertEqual(self.article.title, "Django ORM最佳实践") self.assertEqual(self.article.category.name, "Python") self.assertIn(self.tag, self.article.tags.all()) self.assertEqual(self.article.status, "published") def test_article_get_absolute_url(self): """验证URL生成""" expected_url = f"/article/{self.article.slug}/" self.assertEqual(self.article.get_absolute_url(), expected_url)
# 运行特定App测试 python manage.py test blog # 运行特定测试类 python manage.py test blog.tests.ArticleModelTest # 生成测试覆盖率报告 coverage run --source=blog manage.py test blog coverage report -m coverage html # 生成HTML报告
测试黄金法则:
- 每个测试方法只验证一个行为(单一职责)
- 使用
setUp()预置测试数据,避免重复代码- 模拟外部依赖(如邮件发送、API调用)使用
patch- 生产环境CI流水线强制要求单元测试覆盖率 ≥ 80%
生产部署是项目价值的最终兑现。本方案采用Linux + Nginx + uWSGI + PostgreSQL 标准栈,兼顾稳定性与可维护性。
uWSGI配置(uwsgi.ini)
[uwsgi] project = blogproject uid = www-data gid = www-data base = /var/www/blogproject chdir = %(base) home = %(base)/venv module = %(project).wsgi:application master = true processes = 4 threads = 2 max-requests = 5000 vacuum = true socket = /run/uwsgi/%(project).sock chown-socket = %(uid):www-data chmod-socket = 660 catch-exceptions = true die-on-term = true
Nginx配置(/etc/nginx/sites-available/blog)
server { listen 80; server_name blog.example.com; return 301 https://$server_name$request_uri; } server { listen 443 ssl http2; server_name blog.example.com; ssl_certificate /etc/letsencrypt/live/blog.example.com/fullchain.pem; ssl_certificate_key /etc/letsencrypt/live/blog.example.com/privkey.pem; location / { include uwsgi_params; uwsgi_pass unix:/run/uwsgi/blogproject.sock; uwsgi_read_timeout 300; } location /static/ { alias /var/www/blogproject/staticfiles/; } location /media/ { alias /var/www/blogproject/media/; } }
Supervisor配置(/etc/supervisor/conf.d/blog.conf)
[program:blog] command=/var/www/blogproject/venv/bin/uwsgi --ini /var/www/blogproject/uwsgi.ini directory=/var/www/blogproject user=www-data autostart=true autorestart=true redirect_stderr=true stdout_logfile=/var/log/blog/uwsgi.log
# 1. 服务器初始化 sudo apt update && sudo apt install -y python3-pip python3-venv nginx postgresql supervisor # 2. 创建部署用户 sudo adduser --disabled-password --gecos "" bloguser sudo usermod -aG sudo bloguser # 3. 上传代码并配置 sudo -u bloguser git clone https://github.com/yourname/blogproject.git /var/www/blogproject sudo -u bloguser python3 -m venv /var/www/blogproject/venv # 4. 安装依赖(激活虚拟环境后) source /var/www/blogproject/venv/bin/activate pip install -r requirements.txt # 5. 数据库迁移 python manage.py migrate python manage.py collectstatic --noinput # 6. 启动服务 sudo supervisorctl reread sudo supervisorctl update sudo supervisorctl start blog sudo systemctl restart nginx
安全加固要点:
- 禁用Django调试模式:
DEBUG=False- 设置强密码策略:
AUTH_PASSWORD_VALIDATORS- 敏感信息外置:数据库密码、密钥通过环境变量注入
- 静态文件分离:
STATIC_ROOT与MEDIA_ROOT指向独立目录- 日志集中管理:
LOGGING配置输出到文件或ELK栈
项目上线不是终点,而是持续优化的起点。本阶段聚焦可观察性、可扩展性、可持续性三大目标。
django-silk 或 django-debug-toolbar(开发)pip list --outdated 检查依赖漏洞VACUUM(PostgreSQL)或 OPTIMIZE TABLE(MySQL)Article.status)Django 项目开发的本质是在快速迭代与工程严谨性之间寻找平衡点。本章所呈现的全流程,其价值不仅在于技术实现,更在于建立一套可复用的方法论:
最后建议:
将本流程文档化为团队内部《Django项目启动手册》,配合Checklist(如“上线前10项检查”)、模板文件(requirements.in、uwsgi.ini)和自动化脚本(部署脚本、数据库备份),真正将最佳实践转化为团队生产力。持续迭代此手册,使其成为组织级知识资产。