CodeRoadMap
首页路线题库资源库文章社区
CodeRoadMap

程序员的学习成长路线图。内容免费开放,登录后可同步路线图与课时进度。

由 程序员柏风 独立维护 · 关于作者

学习

  • 路线图
  • 课程
  • 面试题
  • 文章
  • 我

更多

  • 资源
  • 社区
  • 关于作者
  • 内容说明

© 2026 CodeRoadMap

津ICP备2026012044号-1·津公网安备12011002026155号·coderoadmap@126.com
Django 实战入门/课时/综合练习
RawBlame
课时目录12 节

Django 实战入门

  1. 01Django 概览
  2. 02项目结构
  3. 03URL 路由
  4. 04视图层
  5. 05模型与 ORM
  6. 06查询与优化
  7. 07模板 DTL
  8. 08Admin 与认证
  9. 09表单与安全
  10. 10DRF 与异步任务
  11. 11测试与部署
  12. 12综合练习

综合练习

Blog + API 小项目。

30 分钟

综合练习

这一课你将能做到

  1. 从零搭建一个完整的 Django Blog + API 项目,覆盖用户、文章、评论、标签
  2. 同时提供 HTML 渲染页面(全栈)和 REST API(前后端分离)两种访问方式
  3. 用 Docker 打包项目并部署到生产环境,附带完整的监控和备份方案

为什么要学

前面 11 课把 Django 的各个模块拆开讲,这一课把它们串起来做一个完整项目。真正的工程能力不是「会写每个模块」,而是「知道什么时候用什么模块、怎么把它们组装起来」。

这个练习会用到:项目结构、URL 路由、视图、模型、查询、模板、Admin、表单、DRF、测试、部署——全部 12 课的知识。做完它,你对 Django 的理解会从「会用」升级到「能设计」。

和本站(codeforge)对比:本站是 Spring Boot 全栈分离(Java 后端 + React 前端),这个 Django 项目同时提供两种模式:全栈渲染(DTL)和纯 API(DRF)。你会直观感受到两种架构的取舍。

先建立图像

                    ┌─────────────────────────┐
                    │      Nginx / CDN        │
                    └───────────┬─────────────┘
                                │
                    ┌───────────┴─────────────┐
                    │   Gunicorn + Django     │
                    │                         │
   浏览器 ──HTML──> │  Template Views (DTL)   │
   React ──JSON──>  │  DRF API Views          │
                    │                         │
                    │  ┌───────────────────┐  │
                    │  │  Admin 后台管理   │  │
                    │  └───────────────────┘  │
                    │                         │
                    │  ┌───────────────────┐  │
                    │  │  Celery Worker    │  │ <── Redis
                    │  │  (发邮件/报表)  │  │
                    │  └───────────────────┘  │
                    └───────────┬─────────────┘
                                │
                    ┌───────────┴─────────────┐
                    │   PostgreSQL 数据库     │
                    └─────────────────────────┘

跟着做

步骤 1:项目初始化

mkdir django-blog && cd django-blog
python -m venv .venv
.\.venv\Scripts\Activate.ps1  # Windows
pip install django djangorestframework django-filter celery redis psycopg2-binary gunicorn whitenoise python-dotenv
django-admin startproject config .
python manage.py startapp accounts
python manage.py startapp blog
python manage.py startapp api

步骤 2:配置 settings.py

# config/settings.py
import os
from pathlib import Path
from dotenv import load_dotenv

load_dotenv()

BASE_DIR = Path(__file__).resolve().parent.parent

SECRET_KEY = os.getenv("DJANGO_SECRET_KEY", "dev-secret-key-change-in-production")
DEBUG = os.getenv("DJANGO_DEBUG", "True") == "True"
ALLOWED_HOSTS = os.getenv("DJANGO_ALLOWED_HOSTS", "localhost,127.0.0.1").split(",")

INSTALLED_APPS = [
    "django.contrib.admin",
    "django.contrib.auth",
    "django.contrib.contenttypes",
    "django.contrib.sessions",
    "django.contrib.messages",
    "django.contrib.staticfiles",
    # 第三方
    "rest_framework",
    "rest_framework.authtoken",
    "django_filters",
    "whitenoise.runserver_nostatic",  # 开发环境用 WhiteNoise
    # 本地
    "accounts",
    "blog",
    "api",
]

MIDDLEWARE = [
    "django.middleware.security.SecurityMiddleware",
    "whitenoise.middleware.WhiteNoiseMiddleware",
    "django.contrib.sessions.middleware.SessionMiddleware",
    "django.middleware.common.CommonMiddleware",
    "django.middleware.csrf.CsrfViewMiddleware",
    "django.contrib.auth.middleware.AuthenticationMiddleware",
    "django.contrib.messages.middleware.MessageMiddleware",
    "django.middleware.clickjacking.XFrameOptionsMiddleware",
]

ROOT_URLCONF = "config.urls"
WSGI_APPLICATION = "config.wsgi.application"

TEMPLATES = [{
    "BACKEND": "django.template.backends.django.DjangoTemplates",
    "DIRS": [BASE_DIR / "templates"],
    "APP_DIRS": True,
    "OPTIONS": {
        "context_processors": [
            "django.template.context_processors.debug",
            "django.template.context_processors.request",
            "django.contrib.auth.context_processors.auth",
            "django.contrib.messages.context_processors.messages",
        ],
    },
}]

# 数据库
DATABASES = {
    "default": {
        "ENGINE": "django.db.backends.sqlite3",
        "NAME": BASE_DIR / "db.sqlite3",
    }
}

# DRF 配置
REST_FRAMEWORK = {
    "DEFAULT_PAGINATION_CLASS": "rest_framework.pagination.PageNumberPagination",
    "PAGE_SIZE": 10,
    "DEFAULT_FILTER_BACKENDS": [
        "django_filters.rest_framework.DjangoFilterBackend",
        "rest_framework.filters.SearchFilter",
        "rest_framework.filters.OrderingFilter",
    ],
    "DEFAULT_AUTHENTICATION_CLASSES": [
        "rest_framework.authentication.SessionAuthentication",
        "rest_framework.authentication.TokenAuthentication",
    ],
    "DEFAULT_PERMISSION_CLASSES": [
        "rest_framework.permissions.IsAuthenticatedOrReadOnly",
    ],
}

# Celery 配置
CELERY_BROKER_URL = os.getenv("CELERY_BROKER_URL", "redis://localhost:6379/0")
CELERY_RESULT_BACKEND = os.getenv("CELERY_RESULT_BACKEND", "redis://localhost:6379/1")

# 静态文件
STATIC_URL = "/static/"
STATIC_ROOT = BASE_DIR / "staticfiles"
STATICFILES_DIRS = [BASE_DIR / "static"]

# 媒体文件
MEDIA_URL = "/media/"
MEDIA_ROOT = BASE_DIR / "media"

# 自定义用户模型
AUTH_USER_MODEL = "accounts.User"

步骤 3:自定义用户模型

# accounts/models.py
from django.contrib.auth.models import AbstractUser
from django.db import models


class User(AbstractUser):
    avatar = models.ImageField(upload_to="avatars/", blank=True)
    bio = models.TextField(blank=True, max_length=500)
    github = models.URLField(blank=True)

    class Meta:
        verbose_name = "用户"
        verbose_name_plural = "用户"

    def __str__(self):
        return self.username
# accounts/admin.py
from django.contrib import admin
from django.contrib.auth.admin import UserAdmin
from .models import User


@admin.register(User)
class CustomUserAdmin(UserAdmin):
    fieldsets = UserAdmin.fieldsets + (
        ("扩展信息", {"fields": ["avatar", "bio", "github"]}),
    )
    list_display = ["username", "email", "bio", "is_staff", "date_joined"]

步骤 4:博客模型

# blog/models.py
from django.db import models
from django.conf import settings


class Category(models.Model):
    name = models.CharField(max_length=100, unique=True)
    slug = models.SlugField(max_length=100, unique=True)
    created_at = models.DateTimeField(auto_now_add=True)

    class Meta:
        verbose_name = "分类"
        verbose_name_plural = "分类"

    def __str__(self):
        return self.name


class Tag(models.Model):
    name = models.CharField(max_length=50, unique=True)

    def __str__(self):
        return self.name


class Article(models.Model):
    STATUS_CHOICES = [
        ("draft", "草稿"),
        ("published", "已发布"),
    ]

    title = models.CharField(max_length=200)
    slug = models.SlugField(max_length=200, unique=True)
    content = models.TextField()
    author = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE, related_name="articles")
    category = models.ForeignKey(Category, on_delete=models.SET_NULL, null=True, blank=True, related_name="articles")
    tags = models.ManyToManyField(Tag, blank=True, related_name="articles")
    status = models.CharField(max_length=20, choices=STATUS_CHOICES, default="draft")
    views = models.PositiveIntegerField(default=0)
    created_at = models.DateTimeField(auto_now_add=True)
    updated_at = models.DateTimeField(auto_now=True)

    class Meta:
        verbose_name = "文章"
        verbose_name_plural = "文章"
        ordering = ["-created_at"]
        indexes = [
            models.Index(fields=["status", "-created_at"]),
        ]

    def __str__(self):
        return self.title


class Comment(models.Model):
    article = models.ForeignKey(Article, on_delete=models.CASCADE, related_name="comments")
    author = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE, related_name="comments")
    content = models.TextField(max_length=1000)
    created_at = models.DateTimeField(auto_now_add=True)

    class Meta:
        ordering = ["-created_at"]

    def __str__(self):
        return f"{self.author.username} 评论了 {self.article.title}"

步骤 5:Admin 配置

# blog/admin.py
from django.contrib import admin
from .models import Article, Category, Tag, Comment


@admin.register(Category)
class CategoryAdmin(admin.ModelAdmin):
    list_display = ["name", "slug", "created_at"]
    prepopulated_fields = {"slug": ("name",)}


@admin.register(Tag)
class TagAdmin(admin.ModelAdmin):
    list_display = ["name"]


@admin.register(Article)
class ArticleAdmin(admin.ModelAdmin):
    list_display = ["title", "author", "category", "status", "views", "created_at"]
    list_filter = ["status", "category", "created_at"]
    search_fields = ["title", "content"]
    list_editable = ["status"]
    autocomplete_fields = ["author", "category"]
    filter_horizontal = ["tags"]
    date_hierarchy = "created_at"
    actions = ["make_published"]

    @admin.action(description="设为已发布")
    def make_published(self, request, queryset):
        queryset.update(status="published")


@admin.register(Comment)
class CommentAdmin(admin.ModelAdmin):
    list_display = ["article", "author", "content", "created_at"]
    list_filter = ["created_at"]
    search_fields = ["content"]

步骤 6:HTML 视图(全栈模式)

# blog/views.py
from django.views.generic import ListView, DetailView, CreateView
from django.contrib.auth.mixins import LoginRequiredMixin
from django.shortcuts import get_object_or_404
from django.contrib import messages
from .models import Article, Comment
from .forms import CommentForm


class ArticleListView(ListView):
    model = Article
    template_name = "blog/article_list.html"
    context_object_name = "articles"
    paginate_by = 10
    queryset = Article.objects.filter(status="published").select_related("author", "category")


class ArticleDetailView(DetailView):
    model = Article
    template_name = "blog/article_detail.html"
    context_object_name = "article"
    slug_url_kwarg = "slug"

    def get_queryset(self):
        return super().get_queryset().filter(status="published").select_related("author", "category").prefetch_related("tags", "comments__author")

    def get_context_data(self, **kwargs):
        ctx = super().get_context_data(**kwargs)
        ctx["comment_form"] = CommentForm()
        # 增加阅读数
        Article.objects.filter(pk=self.object.pk).update(views=models.F("views") + 1)
        return ctx


class CommentCreateView(LoginRequiredMixin, CreateView):
    model = Comment
    form_class = CommentForm

    def form_valid(self, form):
        form.instance.author = self.request.user
        form.instance.article = get_object_or_404(Article, slug=self.kwargs["slug"])
        messages.success(self.request, "评论发表成功!")
        return super().form_valid(form)

    def get_success_url(self):
        return self.object.article.get_absolute_url()

步骤 7:DRF API(前后端分离模式)

# api/serializers.py
from rest_framework import serializers
from blog.models import Article, Category, Tag, Comment
from accounts.models import User


class UserSerializer(serializers.ModelSerializer):
    class Meta:
        model = User
        fields = ["id", "username", "avatar", "bio"]


class CategorySerializer(serializers.ModelSerializer):
    article_count = serializers.IntegerField(read_only=True)

    class Meta:
        model = Category
        fields = ["id", "name", "slug", "article_count"]


class TagSerializer(serializers.ModelSerializer):
    class Meta:
        model = Tag
        fields = ["id", "name"]


class CommentSerializer(serializers.ModelSerializer):
    author = UserSerializer(read_only=True)

    class Meta:
        model = Comment
        fields = ["id", "author", "content", "created_at"]


class ArticleListSerializer(serializers.ModelSerializer):
    author = UserSerializer(read_only=True)
    category = CategorySerializer(read_only=True)
    tags = TagSerializer(many=True, read_only=True)
    comment_count = serializers.IntegerField(read_only=True)

    class Meta:
        model = Article
        fields = ["id", "title", "slug", "author", "category", "tags",
                  "views", "comment_count", "created_at"]


class ArticleDetailSerializer(serializers.ModelSerializer):
    author = UserSerializer(read_only=True)
    category = CategorySerializer(read_only=True)
    tags = TagSerializer(many=True, read_only=True)
    comments = CommentSerializer(many=True, read_only=True)

    class Meta:
        model = Article
        fields = ["id", "title", "slug", "content", "author", "category",
                  "tags", "comments", "views", "status", "created_at", "updated_at"]
# api/views.py
from rest_framework import viewsets, filters, status
from rest_framework.decorators import action
from rest_framework.response import Response
from rest_framework.permissions import IsAuthenticated, IsAuthenticatedOrReadOnly
from django_filters.rest_framework import DjangoFilterBackend
from blog.models import Article, Category, Tag, Comment
from .serializers import (
    ArticleListSerializer, ArticleDetailSerializer,
    CategorySerializer, TagSerializer, CommentSerializer
)


class CategoryViewSet(viewsets.ReadOnlyModelViewSet):
    queryset = Category.objects.annotate(article_count=models.Count("articles"))
    serializer_class = CategorySerializer
    lookup_field = "slug"


class TagViewSet(viewsets.ReadOnlyModelViewSet):
    queryset = Tag.objects.all()
    serializer_class = TagSerializer


class ArticleViewSet(viewsets.ModelViewSet):
    permission_classes = [IsAuthenticatedOrReadOnly]
    filter_backends = [DjangoFilterBackend, filters.SearchFilter, filters.OrderingFilter]
    filterset_fields = ["status", "category__slug", "author__username", "tags__name"]
    search_fields = ["title", "content"]
    ordering_fields = ["created_at", "views"]
    ordering = ["-created_at"]
    lookup_field = "slug"

    def get_queryset(self):
        return Article.objects.select_related("author", "category").prefetch_related("tags", "comments__author").annotate(
            comment_count=models.Count("comments")
        )

    def get_serializer_class(self):
        if self.action == "list":
            return ArticleListSerializer
        return ArticleDetailSerializer

    def perform_create(self, serializer):
        serializer.save(author=self.request.user)

    @action(detail=True, methods=["post"], permission_classes=[IsAuthenticated])
    def like(self, request, slug=None):
        article = self.get_object()
        article.views = models.F("views") + 1
        article.save()
        article.refresh_from_db()
        return Response({"views": article.views})

    @action(detail=True, methods=["post"], permission_classes=[IsAuthenticated])
    def comment(self, request, slug=None):
        article = self.get_object()
        serializer = CommentSerializer(data=request.data)
        if serializer.is_valid():
            serializer.save(author=request.user, article=article)
            return Response(serializer.data, status=status.HTTP_201_CREATED)
        return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)


class CommentViewSet(viewsets.ModelViewSet):
    serializer_class = CommentSerializer
    permission_classes = [IsAuthenticatedOrReadOnly]

    def get_queryset(self):
        return Comment.objects.select_related("author", "article").all()

    def perform_create(self, serializer):
        serializer.save(author=self.request.user)
# config/urls.py
from django.contrib import admin
from django.urls import path, include
from rest_framework.routers import DefaultRouter
from api.views import ArticleViewSet, CategoryViewSet, TagViewSet, CommentViewSet

router = DefaultRouter()
router.register(r"articles", ArticleViewSet, basename="article")
router.register(r"categories", CategoryViewSet, basename="category")
router.register(r"tags", TagViewSet, basename="tag")
router.register(r"comments", CommentViewSet, basename="comment")

urlpatterns = [
    path("admin/", admin.site.urls),
    path("api/", include(router.urls)),
    path("api-auth/", include("rest_framework.urls")),
    path("", include("blog.urls")),  # HTML 页面
]

步骤 8:测试

# blog/tests.py
import pytest
from django.urls import reverse
from blog.models import Article
from blog.factories import ArticleFactory, UserFactory


@pytest.mark.django_db
class TestArticleViews:
    def test_list(self, client):
        ArticleFactory.create_batch(15, status="published")
        response = client.get(reverse("blog:index"))
        assert response.status_code == 200
        assert len(response.context["articles"]) == 10  # 分页

    def test_detail(self, client):
        article = ArticleFactory(status="published")
        response = client.get(article.get_absolute_url())
        assert response.status_code == 200

    def test_draft_not_visible(self, client):
        article = ArticleFactory(status="draft")
        response = client.get(article.get_absolute_url())
        assert response.status_code == 404


@pytest.mark.django_db
class TestArticleAPI:
    def test_list(self, api_client):
        ArticleFactory.create_batch(5, status="published")
        response = api_client.get("/api/articles/")
        assert response.status_code == 200
        assert response.json()["count"] == 5

    def test_create_authenticated(self, api_client):
        user = UserFactory()
        api_client.force_authenticate(user=user)
        response = api_client.post("/api/articles/", {
            "title": "新文章",
            "slug": "new-article",
            "content": "...",
        })
        assert response.status_code == 201

步骤 9:Docker 部署

# Dockerfile
FROM python:3.12-slim

WORKDIR /app

# 安装系统依赖
RUN apt-get update && apt-get install -y --no-install-recommends \
    postgresql-client \
    && rm -rf /var/lib/apt/lists/*

# 安装 Python 依赖
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt

# 复制代码
COPY . .

# 收集静态文件
RUN python manage.py collectstatic --noinput

EXPOSE 8000

CMD ["gunicorn", "config.wsgi:application", "--bind", "0.0.0.0:8000", "--workers", "4"]
# docker-compose.yml
version: "3.8"

services:
  web:
    build: .
    ports:
      - "8000:8000"
    environment:
      - DJANGO_SECRET_KEY=${DJANGO_SECRET_KEY}
      - DJANGO_DEBUG=False
      - DB_NAME=blogdb
      - DB_USER=bloguser
      - DB_PASSWORD=${DB_PASSWORD}
      - DB_HOST=db
      - DB_PORT=5432
      - CELERY_BROKER_URL=redis://redis:6379/0
      - CELERY_RESULT_BACKEND=redis://redis:6379/1
    depends_on:
      - db
      - redis
    volumes:
      - static_volume:/app/staticfiles
      - media_volume:/app/media

  db:
    image: postgres:16
    environment:
      - POSTGRES_DB=blogdb
      - POSTGRES_USER=bloguser
      - POSTGRES_PASSWORD=${DB_PASSWORD}
    volumes:
      - postgres_data:/var/lib/postgresql/data

  redis:
    image: redis:7-alpine

  celery:
    build: .
    command: celery -A config worker --loglevel=info
    environment:
      - DJANGO_SECRET_KEY=${DJANGO_SECRET_KEY}
      - DJANGO_DEBUG=False
      - DB_NAME=blogdb
      - DB_USER=bloguser
      - DB_PASSWORD=${DB_PASSWORD}
      - DB_HOST=db
      - CELERY_BROKER_URL=redis://redis:6379/0
    depends_on:
      - db
      - redis

volumes:
  postgres_data:
  static_volume:
  media_volume:

启动:

docker-compose up --build -d
docker-compose exec web python manage.py migrate
docker-compose exec web python manage.py createsuperuser

讲清楚要点

  1. 项目结构决定可维护性:config/ 放全局配置,accounts/ 放用户,blog/ 放业务,api/ 放接口。每个 app 职责单一,可插拔。
  2. 同时支持 HTML 和 API 是 Django 的特色:同一套模型,Template Views 渲染 HTML,DRF ViewSet 提供 JSON。不是二选一,而是互补。
  3. N+1 问题在实战中最常见:列表页一定要 select_related("author", "category") + prefetch_related("tags", "comments"),否则 10 篇文章可能产生 30+ 次查询。
  4. Docker Compose 一键起全栈:web + db + redis + celery 四个服务一条命令,本地开发和生产环境完全一致。
  5. 测试要覆盖关键路径:列表、详情、创建、权限,至少这四个场景要有自动化测试。factory-boy 让造测试数据不再痛苦。
  6. 环境变量是配置的单一事实来源:开发用 .env,生产用 Docker env 或 K8s ConfigMap,代码里零硬编码。

你可能会懵的地方

  • 为什么要同时做 HTML 和 API? 真实项目经常需要:SEO 页面用 HTML 渲染(搜索引擎爬虫),App/小程序用 API。一套模型两种输出,Django 的 MTV 架构天然支持。

  • Admin 和 API 的数据会同步吗? 会。它们都操作同一个数据库,Admin 改的数据 API 立即能查到。Admin 适合内部运营,API 适合终端用户。

  • Celery 在这个项目里干什么? 发新文章通知邮件、每日统计报表、图片压缩。这些耗时操作不阻塞 HTTP 响应,用户无感知。

  • docker-compose 里的 depends_on 能保证数据库 ready 吗? 不能。depends_on 只保证启动顺序,不保证服务就绪。生产环境用 healthcheck 或启动脚本里加 wait-for-it.sh。

  • 如何扩展成多租户 SaaS? 加 Tenant 模型,所有业务表加 tenant 外键,中间件根据域名/请求头解析当前租户,查询时自动过滤。Django 有 django-tenants 等第三方包支持 schema 级隔离。

错法对照

错法问题正解
所有代码堆在一个 app无法复用,职责混乱按业务拆 accounts/blog/api
视图里直接写 SQL失去 ORM 保护,SQL 注入风险用 ORM 或 extra() 谨慎使用
忘记 select_related列表页 30+ 次 SQL列表/详情都加关联查询
测试不写上线后才发现 bug关键路径必须有自动化测试
密钥硬编码在代码里泄露后全部 session 失效环境变量 + .env 文件

卡点急救

  1. Docker 启动后数据库连不上:db 服务还没 ready。加 restart: unless-stopped 或启动脚本里循环检测 pg_isready。
  2. Admin 里 ForeignKey 下拉卡死:数据量太大。改用 autocomplete_fields 或 raw_id_fields。
  3. Celery 任务不执行:Redis 没启动或 broker URL 配错。celery -A config inspect active 查看 worker 状态。

面试常问

  • Django 项目如何做水平扩展? Web 层:Gunicorn 多 worker + Nginx 负载均衡 + 多实例。数据库层:PostgreSQL 读写分离 + 连接池。缓存层:Redis 集群。任务层:Celery 多 worker + 队列优先级。静态文件:CDN。每层独立扩展,无状态设计。

  • 从 Spring Boot 迁移到 Django 的主要挑战是什么? 思维转换:Spring 是「配置驱动」,Django 是「约定优于配置」。Spring 依赖注入显式,Django 用全局单例(如 settings)。ORM 差异:JPA 更严格,Django ORM 更灵活。最大挑战是团队习惯,不是技术。

  • 如何设计一个支持百万级文章的博客系统? 数据库:PostgreSQL + 分区表 + 读写分离。缓存:Redis 缓存文章列表/详情,ES 做全文搜索。队列:Celery 处理统计/通知。存储:图片/附件走对象存储(S3/OSS)。监控:Prometheus + Grafana。Django 只是 Web 层,架构设计才是关键。

自测三问

  1. 这个项目同时提供了 HTML 页面和 REST API,它们共享哪些组件?各自独立的部分是什么?
  2. 如果要把这个博客部署到 Kubernetes,需要改哪些配置?
  3. 如何给这个项目加一个「文章草稿自动保存」功能?需要改哪些层?

小结一句话

综合练习把 12 课知识串成完整项目:Django 全栈渲染 + DRF API + Celery 异步 + Docker 部署,一套代码两种输出,这就是现代 Web 开发的完整图景。

延伸

  • Django 官方文档 - 部署
  • DRF 官方文档
  • Celery 官方文档
  • 进阶方向:Django Channels(WebSocket)、Django Ninja(FastAPI 风格 API)、Wagtail(CMS)
加载本节练习…
← 上一课测试与部署