Django RestFramework

news2024/11/24 7:26:51

安装restframework

pip install djangorestframework
pip install markdown       # Markdown support for the browsable API.
pip install django-filter  # Filtering support

安装其他模块

pip install pillow
pip install django-cors-headers

建模和迁移数据

drf包含四个部分,models,views,urls,和serializers,其中serializers是比django多出的内容。
serializers实现了数据的序列化和反序列化,即数据orm转json,和json转orm。
user数据已经建好。
models还是原来的models

serializers.py



class UserSerializer(serializers.HyperlinkedModelSerializer):

    class Meta:
        model = User
        fields = ('id', 'url', 'username', 'email', )


urls指定访问api的路径

urls

from django.conf.urls import url, include
from django.contrib import admin

from rest_framework import routers
from quickstartapp import views

router = routers.DefaultRouter()

router.register(r'users', views.UserViewSet)
# router.register(r'groups', views.GroupViewSet)

urlpatterns = [
    url(r'^admin/', admin.site.urls),
    url(r'^', include(router.urls)),

]

views指定展示的形式,可以在这里定制

views

from django.contrib.auth.models import User

from rest_framework import viewsets

from quickstartapp.serializers import UserSerializer

class UserViewSet(viewsets.ModelViewSet):
    """
    查看、编辑用户数据的API接口。
    """
    queryset = User.objects.all().order_by('-date_joined')     # 排序方法
    serializer_class = UserSerializer

Filtering against query parameters(根据查询参数进行过滤)

过滤初始查询集的最后一个示例是基于url中的查询参数确定初始查询集。

我们可以通过重写.get_queryset()方法来处理像http://example.com/api/purchases?username=denvercoder9这样的网址,并且只有在URL中包含username参数时,才过滤queryset:
比如下例子:
访问链接如下。

http://localhost:8000/blogapp/BlogPageCate/?cxcategory=3

数据库表如下
在这里插入图片描述

views中需要这样设置


class BlogPageCatetView(generics.ListAPIView):
    '''分类导航图标列表'''
    serializer_class = BlogPageSerializer
    permissin_classes = (permissions.AllowAny,)
    pagination_class = LimitOffsetPagination  # 分页 请求加 ?limit = xx

    def get_queryset(self):
        # user = self.request.user
        queryset = BlogPage.objects.all()
        # queryset = BlogPage.objects.all()
        cxcategory = self.request.query_params.get('cxcategory', None)
        if cxcategory is not None:
            queryset = queryset.filter(cxcategory_id=cxcategory)
        return queryset

setting

在这里插入图片描述

增加分组功能

serializers
在这里插入图片描述

views
在这里插入图片描述
urls
在这里插入图片描述

增加分页功能

setting
在这里插入图片描述

增加登陆功能

urls
在这里插入图片描述

限制权限

在这里插入图片描述

DRF示例-snippets

snippets\settings.py

"""
Django settings for snippets project.

Generated by 'django-admin startproject' using Django 3.1.7.

For more information on this file, see
https://docs.djangoproject.com/en/3.1/topics/settings/

For the full list of settings and their values, see
https://docs.djangoproject.com/en/3.1/ref/settings/
"""

from pathlib import Path

# Build paths inside the project like this: BASE_DIR / 'subdir'.
BASE_DIR = Path(__file__).resolve().parent.parent


# Quick-start development settings - unsuitable for production
# See https://docs.djangoproject.com/en/3.1/howto/deployment/checklist/

# SECURITY WARNING: keep the secret key used in production secret!
SECRET_KEY = 'q3s1m3_sg6$p8yf9hmz%#tu)&tv(r@%mqbbfezkd1#993vysrw'

# SECURITY WARNING: don't run with debug turned on in production!
DEBUG = True

ALLOWED_HOSTS = []


# Application definition

INSTALLED_APPS = [
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
    'rest_framework',
    'snippetsapp.apps.SnippetsappConfig',

]

MIDDLEWARE = [
    'django.middleware.security.SecurityMiddleware',
    '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 = 'snippets.urls'

TEMPLATES = [
    {
        'BACKEND': 'django.template.backends.django.DjangoTemplates',
        'DIRS': [],
        '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',
            ],
        },
    },
]

WSGI_APPLICATION = 'snippets.wsgi.application'


# Database
# https://docs.djangoproject.com/en/3.1/ref/settings/#databases

DATABASES = {
    'default': {
        'ENGINE': 'django.db.backends.mysql',
        'NAME': 'snippets',
        'HOST': '127.0.0.1',
        'PORT': 3306,
        'USER': 'root',
        'PASSWORD': '123',
    }
}


# Password validation
# https://docs.djangoproject.com/en/3.1/ref/settings/#auth-password-validators

AUTH_PASSWORD_VALIDATORS = [
    {
        'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator',
    },
    {
        'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator',
    },
    {
        'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator',
    },
    {
        'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator',
    },
]


# Internationalization
# https://docs.djangoproject.com/en/3.1/topics/i18n/

LANGUAGE_CODE = 'en-us'

TIME_ZONE = 'UTC'

USE_I18N = True

USE_L10N = True

USE_TZ = True


# Static files (CSS, JavaScript, Images)
# https://docs.djangoproject.com/en/3.1/howto/static-files/

STATIC_URL = '/static/'

snippetsapp\models.py

from django.db import models
from pygments import highlight
from pygments.formatters.html import HtmlFormatter

from pygments.lexers import get_all_lexers, get_lexer_by_name
from pygments.styles import get_all_styles

LEXERS = [item for item in get_all_lexers() if item[1]]
LANGUAGE_CHOICES = sorted([(item[1][0], item[0]) for item in LEXERS])
STYLE_CHOICES = sorted((item, item) for item in get_all_styles())


class Snippet(models.Model):
    created = models.DateTimeField(auto_now_add=True)
    title = models.CharField(max_length=100, blank=True, default='')
    code = models.TextField()
    linenos = models.BooleanField(default=False)
    language = models.CharField(choices=LANGUAGE_CHOICES, default='python', max_length=100)
    style = models.CharField(choices=STYLE_CHOICES, default='friendly', max_length=100)
    owner = models.ForeignKey('auth.User', related_name='snippets', on_delete=models.CASCADE, null= True , blank= True)
    highlighted = models.TextField( null=True,blank=True)

    class Meta:
        ordering = ('created',)

    def save(self, *args, **kwargs):
        """
        Use the `pygments` library to create a highlighted HTML
        representation of the code snippet.
        高亮显示相关
        """
        lexer = get_lexer_by_name(self.language)
        linenos = 'table' if self.linenos else False
        options = {'title': self.title} if self.title else {}
        formatter = HtmlFormatter(style=self.style, linenos=linenos,
                                  full=True, **options)
        self.highlighted = highlight(self.code, lexer, formatter)
        super(Snippet, self).save(*args, **kwargs)

snippetsapp\serializers.py

from rest_framework import serializers
from snippetsapp.models import Snippet, LANGUAGE_CHOICES, STYLE_CHOICES

class SnippetSerializer(serializers.ModelSerializer):
    class Meta:
        model = Snippet
        fields = ('id', 'title', 'code', 'linenos', 'language', 'style','owner')


#
# class SnippetSerializer(serializers.Serializer):
#     id = serializers.IntegerField(read_only=True)
#     title = serializers.CharField(required=False, allow_blank=True, max_length=100)
#     code = serializers.CharField(style={'base_template': 'textarea.html'})
#     linenos = serializers.BooleanField(required=False)
#     language = serializers.ChoiceField(choices=LANGUAGE_CHOICES, default='python')
#     style = serializers.ChoiceField(choices=STYLE_CHOICES, default='friendly')
#
#     def create(self, validated_data):
#         """
#         Create and return a new `Snippet` instance, given the validated data.
#         """
#         return Snippet.objects.create(**validated_data)
#
#     def update(self, instance, validated_data):
#         """
#         Update and return an existing `Snippet` instance, given the validated data.
#         """
#         instance.title = validated_data.get('title', instance.title)
#         instance.code = validated_data.get('code', instance.code)
#         instance.linenos = validated_data.get('linenos', instance.linenos)
#         instance.language = validated_data.get('language', instance.language)
#         instance.style = validated_data.get('style', instance.style)
#         instance.save()
#         return instance





snippetsapp\views.py

from snippetsapp.models import Snippet
from snippetsapp.serializers import SnippetSerializer
from rest_framework import generics
from rest_framework import permissions

class SnippetList(generics.ListCreateAPIView):
    queryset = Snippet.objects.all()
    serializer_class = SnippetSerializer
    permission_classes = (permissions.IsAuthenticatedOrReadOnly,)


class SnippetDetail(generics.RetrieveUpdateDestroyAPIView):
    queryset = Snippet.objects.all()
    serializer_class = SnippetSerializer


# #######################################
# from snippetsapp.models import Snippet
# from snippetsapp.serializers import SnippetSerializer
# from rest_framework import mixins
# from rest_framework import generics
#
# class SnippetList(mixins.ListModelMixin,
#                   mixins.CreateModelMixin,
#                   generics.GenericAPIView):
#     queryset = Snippet.objects.all()
#     serializer_class = SnippetSerializer
#
#     def get(self, request, *args, **kwargs):
#         return self.list(request, *args, **kwargs)
#
#     def post(self, request, *args, **kwargs):
#         return self.create(request, *args, **kwargs)
#
#
# class SnippetDetail(mixins.RetrieveModelMixin,
#                     mixins.UpdateModelMixin,
#                     mixins.DestroyModelMixin,
#                     generics.GenericAPIView):
#     queryset = Snippet.objects.all()
#     serializer_class = SnippetSerializer
#
#     def get(self, request, *args, **kwargs):
#         return self.retrieve(request, *args, **kwargs)
#
#     def put(self, request, *args, **kwargs):
#         return self.update(request, *args, **kwargs)
#
#     def delete(self, request, *args, **kwargs):
#         return self.destroy(request, *args, **kwargs)



####################################3

# from snippetsapp.models import Snippet
# from snippetsapp.serializers import SnippetSerializer
# from django.http import Http404
# from rest_framework.views import APIView
# from rest_framework.response import Response
# from rest_framework import status
#
# class SnippetList(APIView):
#     """
#     List all snippets, or create a new snippet.
#     """
#     def get(self, request, format=None):
#         snippets = Snippet.objects.all()
#         serializer = SnippetSerializer(snippets, many=True)
#         return Response(serializer.data)
#
#     def post(self, request, format=None):
#         serializer = SnippetSerializer(data=request.data)
#         if serializer.is_valid():
#             serializer.save()
#             return Response(serializer.data, status=status.HTTP_201_CREATED)
#         return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
#
#
# class SnippetDetail(APIView):
#     """
#     Retrieve, update or delete a snippet instance.
#     """
#     def get_object(self, pk):
#         try:
#             return Snippet.objects.get(pk=pk)
#         except Snippet.DoesNotExist:
#             raise Http404
#
#     def get(self, request, pk, format=None):
#         snippet = self.get_object(pk)
#         serializer = SnippetSerializer(snippet)
#         return Response(serializer.data)
#
#     def put(self, request, pk, format=None):
#         snippet = self.get_object(pk)
#         serializer = SnippetSerializer(snippet, data=request.data)
#         if serializer.is_valid():
#             serializer.save()
#             return Response(serializer.data)
#         return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
#
#     def delete(self, request, pk, format=None):
#         snippet = self.get_object(pk)
#         snippet.delete()
#         return Response(status=status.HTTP_204_NO_CONTENT)



###############################################

# from rest_framework import status
# from rest_framework.decorators import api_view
# from rest_framework.response import Response
# from snippetsapp.models import Snippet
# from snippetsapp.serializers import SnippetSerializer
#
#
# @api_view(['GET', 'POST'])
# def snippet_list(request,format=None):
#     """
#     List all code snippets, or create a new snippet.
#     """
#     if request.method == 'GET':
#         snippets = Snippet.objects.all()
#         serializer = SnippetSerializer(snippets, many=True)
#         return Response(serializer.data)
#
#     elif request.method == 'POST':
#         serializer = SnippetSerializer(data=request.data)
#         if serializer.is_valid():
#             serializer.save()
#             return Response(serializer.data, status=status.HTTP_201_CREATED)
#         return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
#
# @api_view(['GET', 'PUT', 'DELETE'])
# def snippet_detail(request, pk):
#     """
#     Retrieve, update or delete a code snippet.
#     """
#     try:
#         snippet = Snippet.objects.get(pk=pk)
#     except Snippet.DoesNotExist:
#         return Response(status=status.HTTP_404_NOT_FOUND)
#
#     if request.method == 'GET':
#         serializer = SnippetSerializer(snippet)
#         return Response(serializer.data)
#
#     elif request.method == 'PUT':
#         serializer = SnippetSerializer(snippet, data=request.data)
#         if serializer.is_valid():
#             serializer.save()
#             return Response(serializer.data)
#         return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
#
#     elif request.method == 'DELETE':
#         snippet.delete()
#         return Response(status=status.HTTP_204_NO_CONTENT)

###########################################################
#
#
# from django.http import HttpResponse, JsonResponse
# from django.views.decorators.csrf import csrf_exempt
# from rest_framework.renderers import JSONRenderer
# from rest_framework.parsers import JSONParser
# from snippetsapp.models import Snippet
# from snippetsapp.serializers import SnippetSerializer
#
#
# @csrf_exempt
# def snippet_list(request):
#     """
#     List all code snippets, or create a new snippet.
#     """
#     if request.method == 'GET':
#         snippets = Snippet.objects.all()
#         serializer = SnippetSerializer(snippets, many=True)
#         return JsonResponse(serializer.data, safe=False)
#
#     elif request.method == 'POST':
#         data = JSONParser().parse(request)
#         serializer = SnippetSerializer(data=data)
#         if serializer.is_valid():
#             serializer.save()
#             return JsonResponse(serializer.data, status=201)
#         return JsonResponse(serializer.errors, status=400)
#
#
# @csrf_exempt
# def snippet_detail(request, pk):
#     """
#     Retrieve, update or delete a code snippet.
#     """
#     try:
#         snippet = Snippet.objects.get(pk=pk)
#     except Snippet.DoesNotExist:
#         return HttpResponse(status=404)
#
#     if request.method == 'GET':
#         serializer = SnippetSerializer(snippet)
#         return JsonResponse(serializer.data)
#
#     elif request.method == 'PUT':
#         data = JSONParser().parse(request)
#         serializer = SnippetSerializer(snippet, data=data)
#         if serializer.is_valid():
#             serializer.save()
#             return JsonResponse(serializer.data)
#         return JsonResponse(serializer.errors, status=400)
#
#     elif request.method == 'DELETE':
#         snippet.delete()
#         return HttpResponse(status=204)

重新迁移数据库

When that’s all done we’ll need to update our database tables. Normally we’d create a database migration in order to do that, but for the purposes of this tutorial, let’s just delete the database and start again.
当修改过models后,需要更新数据库表。通常是建立一个数据库迁移,在这个课程中,我们只是删除这个数据库并且重新开始。

rm -f db.sqlite3
rm -r snippetsapp/migrations
python manage.py makemigrations snippetsapp
python manage.py migrate

实际中的操作

C:\djproject\snippets>python manage.py makemigrations
Migrations for 'snippetsapp':
  snippetsapp\migrations\0002_auto_20210402_0756.py
    - Add field highlighted to snippet
    - Add field owner to snippet

C:\djproject\snippets>python manage.py migrate
Operations to perform:
  Apply all migrations: admin, auth, contenttypes, sessions, snippetsapp
Running migrations:
  Applying snippetsapp.0002_auto_20210402_0756... OK

DRF接口增加流程

setting配置

时区设置

项目setting文件

LANGUAGE_CODE = 'zh-hans'

TIME_ZONE = 'Asia/Shanghai'

USE_I18N = True

USE_L10N = True

USE_TZ = True

资源文件路径

STATIC_URL = '/static/'

MEDIA_URL='/media/'
MEDIA_ROOT=os.path.join(os.path.dirname(BASE_DIR), 'eshop/media')

restframework设置

REST_FRAMEWORK = {
    'DEFAULT_PAGINATION_CLASS': 'rest_framework.pagination.PageNumberPagination',
    'PAGE_SIZE': 6,   # 分页设置
    'DEFAULT_AUTHENTICATION_CLASSES': (    # 认证设置
        'rest_framework.authentication.BasicAuthentication',
        'rest_framework.authentication.SessionAuthentication',
        'rest_framework.authentication.TokenAuthentication',
    ),
}

数据库设定


# DATABASES = {
#     'default': {
#         'ENGINE': 'django.db.backends.mysql',
#         'NAME': 'eshop',
#         'HOST': '127.0.0.1',
#         'PORT': 3306,
#         'USER': 'root',
#         'PASSWORD': '123',
#     }
# }

DATABASES = {
    'default': {
        'ENGINE': 'django.db.backends.sqlite3',
        'NAME': BASE_DIR / 'db.sqlite3',
    }
}

项目初始化文件mysql支持设定

项目根_init_.py

import pymysql
pymysql.install_as_MySQLdb()

中间件设定

MIDDLEWARE = [
    'corsheaders.middleware.CorsMiddleware',
    'django.middleware.security.SecurityMiddleware',
    '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',

]

应用设定

# Application definition

INSTALLED_APPS = [
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
    'rest_framework',    # restframework支持
    'rest_framework.authtoken',   # 增加身份认证
    # 'django_filters',
    'computerapp.apps.ComputerappConfig',   # 你的应用
    'corsheaders',   跨站支持

]

创建数据模型

数据模型是整个应用的基础,应事先规划好,避免后期有大的改动

models.py

from django.db import models

# Create your models here.
from django.db import models
# from django.utils.six import python_2_unicode_compatible

from django.conf import settings


# @python_2_unicode_compatible
class Category(models.Model):
    """
    商品类别:笔记本、平板电脑、一体机、台式机、服务器
    """
    name = models.CharField(max_length=200, verbose_name='名称')
    created = models.DateTimeField(auto_now_add=True)
    updated = models.DateTimeField(auto_now=True)

    class Meta:
        verbose_name = '商品类别'
        verbose_name_plural = verbose_name

    def __str__(self):
        return self.name


# @python_2_unicode_compatible
class Manufacturer(models.Model):
    """
    生产厂商
    """
    name = models.CharField(max_length=200)
    description = models.TextField()
    logo = models.ImageField(blank=True, null=True, max_length=200, upload_to='manufacturer/uploads/%Y/%m/%d/')
    created = models.DateTimeField(auto_now_add=True)
    updated = models.DateTimeField(auto_now=True)

    def __str__(self):
        return self.name


# @python_2_unicode_compatible
class Product(models.Model):
    """
    产品
    """
    model = models.CharField(max_length=200,verbose_name='型号')
    description = models.TextField()
    image = models.ImageField(max_length=200, upload_to='product/uploads/%Y/%m/%d/')
    price = models.DecimalField(max_digits=12, decimal_places=2, verbose_name='价格')
    sold = models.PositiveIntegerField(default=0)
    category = models.ForeignKey(Category, related_name='product_in', on_delete=models.CASCADE, verbose_name='类别')
    manufacturer = models.ForeignKey(Manufacturer, related_name='product_of', on_delete=models.CASCADE)
    created = models.DateTimeField(auto_now_add=True)
    updated = models.DateTimeField(auto_now=True)

    class Meta:
        verbose_name = '产品'
        verbose_name_plural = verbose_name

    def __str__(self):
        return self.model


# @python_2_unicode_compatible
class DeliveryAddress(models.Model):
    """
    收货地址
    """
    user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE, related_name='delivery_address_of', )
    contact_person = models.CharField(max_length=200)
    contact_mobile_phone = models.CharField(max_length=200)
    delivery_address = models.TextField()
    created = models.DateTimeField(auto_now_add=True)
    updated = models.DateTimeField(auto_now=True)

    def __str__(self):
        return self.delivery_address


# @python_2_unicode_compatible
class UserProfile(models.Model):
    """
    用户档案
    """
    user = models.OneToOneField(settings.AUTH_USER_MODEL, on_delete=models.CASCADE, related_name='profile_of', )
    mobile_phone = models.CharField(blank=True, null=True, max_length=200)
    nickname = models.CharField(blank=True, null=True, max_length=200)
    description = models.TextField(blank=True, null=True)
    icon = models.ImageField(blank=True, null=True, max_length=200, upload_to='user/uploads/%Y/%m/%d/')
    created = models.DateTimeField(auto_now_add=True)
    updated = models.DateTimeField(auto_now=True)
    delivery_address = models.ForeignKey(DeliveryAddress, related_name='user_delivery_address',
                                         on_delete=models.CASCADE, blank=True, null=True, )


# @python_2_unicode_compatible
class Order(models.Model):
    """
    订单
    """
    STATUS_CHOICES = (
        ('0', 'new'),
        ('1', 'not paid'),
        ('2', 'paid'),
        ('3', 'transport'),
        ('4', 'closed'),
    )
    status = models.CharField(choices=STATUS_CHOICES, default='0', max_length=2)
    user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE, related_name='order_of', )
    remark = models.TextField(blank=True, null=True)
    product = models.ForeignKey(Product, related_name='order_product', on_delete=models.CASCADE)
    price = models.DecimalField(max_digits=12, decimal_places=2)
    quantity = models.PositiveIntegerField(default=1)
    address = models.ForeignKey(DeliveryAddress, related_name='order_address', on_delete=models.CASCADE)
    created = models.DateTimeField(auto_now_add=True)
    updated = models.DateTimeField(auto_now=True)

    def __str__(self):
        return 'order of %d' % (self.user.id)

创建序列化器

用Django开发RESTful风格的API存在着很多重复的步骤。过程往往都是,
(1)把到前端请求的过来的json字符串,然后通过json.loads转换为字典,字典在转换为对象,保存在数据库。
(2)返回的时候呢,都是先把对象查询出来,然后转换为字典,再通过JsonResponse转换为json字符串并且返回给前端。
接口的开发,基本就是在重复这两个动作,而且这两个动作语句特别多,序列化就是来解决这个问题。

  1. 序列化,序列化器会把模型对象转换成字典,经过response以后变成json字符串
  2. 反序列化,把客户端发送过来的数据,经过request以后变成字典,序列化器可以把字典转成模型
  3. 反序列化,完成数据校验功能

serializers.py

from django.contrib.auth.models import User
from rest_framework import serializers

from computerapp.models import Product, Manufacturer, UserProfile, DeliveryAddress, Order


class ProductListSerializer(serializers.ModelSerializer):
    class Meta:
        model = Product
        fields = ('id', 'model', 'image', 'price', 'sold', 'category', 'manufacturer',)


class ManufacturerSerializer(serializers.ModelSerializer):
    class Meta:
        model = Manufacturer
        fields = ('id', 'name',)


class CategorySerializer(serializers.ModelSerializer):
    class Meta:
        model = Manufacturer
        fields = ('id', 'name',)


class ProductRetrieveSerializer(serializers.ModelSerializer):
    manufacturer = ManufacturerSerializer()  # 获取厂商对象,替换下面的字段
    category = CategorySerializer()  # 获取类别对象,替换下面的字段

    class Meta:
        model = Product
        fields = (
            'id', 'model', 'image', 'price', 'sold', 'category', 'manufacturer', 'description', 'created', 'updated',)


class UserProfileSerializer(serializers.ModelSerializer):
    class Meta:
        model = UserProfile
        fields = ('id', 'user', 'mobile_phone', 'nickname', 'description', 'icon', 'created', 'updated',)
        read_only_fields = ('user',)


class UserInfoSerializer(serializers.ModelSerializer):
    profile_of = UserProfileSerializer()

    class Meta:
        model = User
        fields = ('id', 'username', 'email', 'first_name', 'last_name', 'date_joined', 'profile_of',)


class UserSerializer(serializers.ModelSerializer):
    class Meta:
        model = User
        fields = ('id', 'username', 'password', 'email', 'first_name', 'last_name',)
        extra_kwargs = {'password': {'write_only': True}}

    def create(self, validated_data):
        user = User(**validated_data)  # 接受前端传过来的用户名和密码,关键字参数
        user.set_password(validated_data['password'])  # 通过字典方式调用
        user.save()  # 保存到内存中
        user_profile = UserProfile(user=user)
        user_profile.save()
        return user


class DeliveryAddressSerilizer(serializers.ModelSerializer):
    '''收货地址'''

    class Meta:
        model = DeliveryAddress
        fields = ('id', 'user', 'contact_person', 'contact_mobile_phone', 'delivery_address', 'created', 'updated',)
        read_only_fields = ('user',)  # 设置为只读类型,不能在前端修改


class OrderListSerializer(serializers.ModelSerializer):
    product = ProductListSerializer()
    address = DeliveryAddressSerilizer()

    class Meta:
        model = Order
        fields = ('id', 'status', 'user', 'product', 'price', 'quantity', 'remark', 'address', 'created', 'updated',)


class OrderCreateSerializer(serializers.ModelSerializer):
    class Meta:
        model = Order
        fields = ('id', 'status', 'user', 'product', 'price', 'quantity', 'remark', 'address', 'created', 'updated',)
        read_only_fields = ('user', 'price', 'address', 'status',)


class OrderRUDSerializer(serializers.ModelSerializer):
    class Meta:
        model = Order
        fields = ('id',)

创建视图类

views.py

from rest_framework import generics
from rest_framework import permissions
from rest_framework.exceptions import NotFound
from rest_framework.filters import OrderingFilter, SearchFilter
from rest_framework.pagination import LimitOffsetPagination

from rest_framework.response import Response
from rest_framework.views import APIView

from computerapp.models import Product, DeliveryAddress, UserProfile, Order
from computerapp.serializers import ProductListSerializer, ProductRetrieveSerializer, UserInfoSerializer, \
    UserSerializer, DeliveryAddressSerilizer, UserProfileSerializer, OrderListSerializer, OrderCreateSerializer, \
    OrderRUDSerializer

import logging
import datetime
import json

LOG_FILENAME = 'shop.log'

# logging.basicConfig(filename=LOG_FILENAME,level = logging.DEBUG)
logging.basicConfig(filename=LOG_FILENAME, level=logging.INFO)


# Create your views here.
class ProductListView(generics.ListAPIView):
    '''产品列表'''
    queryset = Product.objects.all()
    serializer_class = ProductListSerializer
    permissin_classes = (permissions.AllowAny,)
    filter_backends = (OrderingFilter, SearchFilter)  # 过滤选项
    ordering_fields = ('category', 'manufacturer', 'created', 'sold',)  # 排序字段
    search_fields = ('description', 'model')  # 搜索字段
    ordering = ('id',)
    pagination_class = LimitOffsetPagination  # 分页 请求加 ?limit = xx


class ProductListByCategoryView(generics.ListAPIView):
    '''产品类别列表'''
    serializer_class = ProductListSerializer
    permissin_classes = (permissions.AllowAny,)
    filter_backends = (OrderingFilter, SearchFilter)
    ordering_fields = ('category', 'manufacturer', 'created', 'sold', 'stock', 'price',)
    search_fields = ('description',)
    ordering = ('id',)
    '''查询集根据条件获取'''

    def get_queryset(self):
        category = self.request.query_params.get('category', None)

        if category is not None:
            queryset = Product.objects.filter(category=category)
        else:
            queryset = Product.objects.all()

        return queryset


class ProductListByCategoryManufacturerView(generics.ListAPIView):
    '''产品按类别品牌列表'''
    serializer_class = ProductListSerializer
    permissin_classes = (permissions.AllowAny,)
    filter_backends = (OrderingFilter, SearchFilter)
    ordering_fields = ('category', 'manufacturer', 'created', 'sold', 'stock', 'price',)
    search_fields = ('description',)
    ordering = ('id',)

    def get_queryset(self):
        category = self.request.query_params.get('category', None)
        manufacturer = self.request.query_params.get('manufacturer', None)

        if category is not None:
            queryset = Product.objects.filter(category=category, manufacturer=manufacturer)
        else:
            queryset = Product.objects.all()

        return queryset


class ProductRetrieveView(generics.RetrieveAPIView):
    queryset = Product.objects.all()
    serializer_class = ProductRetrieveSerializer
    permission_classes = (permissions.AllowAny,)


class UserInfoView(APIView):
    '''用户基本信息'''
    permission_classes = (permissions.IsAuthenticated,)

    def get(self, request, format=None):
        user = self.request.user
        serializer = UserInfoSerializer(user)
        return Response(serializer.data)


class UserProfileRUView(generics.RetrieveUpdateAPIView):
    '''用户其他信息'''
    serializer_class = UserProfileSerializer
    permission_classes = (permissions.IsAuthenticated,)

    def get_object(self):
        user = self.request.user
        obj = UserProfile.objects.get(user=user)
        return obj


class UserCreateView(generics.CreateAPIView):
    '''用户创建'''
    serializer_class = UserSerializer


class DeliveryAddressLCView(generics.ListCreateAPIView):
    '''收货地址LC'''
    serializer_class = DeliveryAddressSerilizer  # 没有加LC,因为较简单
    permission_classes = (permissions.IsAuthenticated,)  # 需要登陆认证

    def get_queryset(self):
        user = self.request.user
        queryset = DeliveryAddress.objects.filter(user=user)  # 模型来的对象
        return queryset

    def perform_create(self, serializer):
        user = self.request.user
        s = serializer.save(user=user)
        profile = user.profile_of  # profile_of 反向关系,从models来
        profile.delivery_address = s  # 把新创建的收货地址设为默认地址存入用户扩展信息中
        profile.save()


class DeliveryAddressRUDView(generics.RetrieveUpdateDestroyAPIView):
    '''收货地址RUD'''
    serializer_class = DeliveryAddressSerilizer
    permission_classes = (permissions.IsAuthenticated,)

    def get_object(self):
        user = self.request.user
        # obj =DeliveryAddress.objects.get(user=user)
        try:
            obj = DeliveryAddress.objects.get(id=self.kwargs['pk'], user=user)  # id由前端传来,一个用户有多个id
        except Exception as e:
            raise NotFound('no found')
        return obj


class CartListView(generics.ListAPIView):
    '''购物车列表'''
    serializer_class = OrderListSerializer  # 可以和订单公用一个序列器
    permissin_classes = (permissions.IsAuthenticated,)

    def get_queryset(self):
        user = self.request.user
        queryset = Order.objects.filter(user=user, status='0')  # 我的状态为零的,表示在购物车
        return queryset


class OrderListView(generics.ListAPIView):
    '''订单列表'''
    serializer_class = OrderListSerializer  # 和cart公用一个序列器
    permissin_classes = (permissions.IsAuthenticated,)

    def get_queryset(self):
        user = self.request.user
        queryset = Order.objects.filter(user=user, status__in=['1', '2', '3', '4'])  # status__in 是django用法,表示值在。。范围
        return queryset


class OrderCreateView(generics.CreateAPIView):
    '''创建订单'''
    queryset = Order.objects.all()
    serializer_class = OrderCreateSerializer
    permission_classes = (permissions.IsAuthenticated,)

    def perform_create(self, serializer):
        user = self.request.user
        product = serializer.validated_data.get('product')
        serializer.save(user=user, price=product.price, address=user.profile_of.delivery_address, )

        logging.info('user %d cart changed,product %d related.Time is %s.', user.id, product.id,
                     str(datetime.datetime.now()))


class OrderRUDView(generics.RetrieveUpdateDestroyAPIView):
    '''OrderRUD'''
    serializer_class = OrderRUDSerializer
    permission_classes = (permissions.IsAuthenticated,)

    def get_object(self):
        user = self.request.user
        obj = Order.objects.get(user=user, id=self.kwargs['pk'])
        return obj

    def perform_update(self, serializer):
        user = self.request.user
        serializer.save(user=user, status='1')

过滤组件与分页组件的使用简介

# views.py

# drf提供两种过滤组件类:全文搜索、排序
from rest_framework.filters import SearchFilter, OrderingFilter
from django_filters.rest_framework import DjangoFilterBackend

# 通过安装第三方库django-filter,即可使用第三种过滤组件:分类
from django_filters.rest_framework import DjangoFilterBackend

# drf提供三种分页类
from rest_framework.pagination import PageNumberPagination, LimitOffsetPagination, CursorPagination


class CourseViewSet(GenericViewSet, ListModelMixin):
    # 过滤组件
    filter_backends = [SearchFilter, OrderingFilter, DjangoFilterBackend]

    # 过滤:全文搜索
    search_fields = ['name']  # 按数据库非外键字段,包含了目标参数即可匹配

    # 过滤:分类搜索
    fields = []  # 按数据库字段,包括外键字段,完全相等才匹配到结果
    filter_class = CourseFilterSet  # 按设置的分类规则进行分类,优先级大于按字段分类

    # 过滤:排序
    ordering_fields = []  # 按数据库字段,包括外键字段
    
    # 分页组件
    pagination_class = PageNumberPagination
    # pagination_class = LimitOffsetPagination
    
    # 分页参数配置:PageNumberPagination
    page_size = 100  # 每页显示最多的数目
    page_query_param = 'page'  # 前端发送的分页的页数参数的关键字名,默认位"page"
    page_size_query_param = 'page_size'  # 前端发送的每页数目参数的关键字名,默认位None
    max_page_size = None  # 前端最多能设置的每页数量
    
    # 分页参数配置:LimitOffsetPagination
    '''
    default_limit = 100  # 每页显示最多的数目,默认配置与page_size相同,api_settings.PAGE_SIZE
    limit_query_param = 'limit'
    offset_query_param = 'offset'  # 偏移多少数目的参数的关键字名
    max_limit = None
    '''

过滤组件和分页组件的入口函数

过滤与分页均只对群查接口有意义,因此只在群查中使用,即list()方法中调用组件的入口函数

但是组件的入口函数,是定义在GenericAPIView中的

class GenericAPIView(views.APIView):
    def filter_queryset(self, queryset):
        pass
    
    def paginate_queryset(self, queryset):
        pass
    

过滤组件:分类filter_class

from . import models
from django_filters.filterset import FilterSet
from django_filters import filters
class CourseFilterSet(FilterSet):
    # 通过NumberFilter类自定义分类字段
    # 如下定义两个区间分类字段:teacher_id大于min_price和teacher_id小于max_price
    min_price = filters.NumberFilter(field_name='teacher_id', lookup_expr='gte')
    max_price = filters.NumberFilter(field_name='teacher_id', lookup_expr='lte')
    
    class Meta:
        model = models.Course
        fields = ['参与分类的数据库字段']  # 自定义的min_price、max_price字段不需要写在fields里面

创建路由

urls.py

"""eshop URL Configuration
项目urls
"""
from django.conf import settings
from django.conf.urls.static import static
from django.contrib import admin
from django.urls import path, include
from rest_framework.authtoken import views

urlpatterns = [
    path('admin/', admin.site.urls),
    path('computer/', include('computerapp.urls')),
    path('api-auth/', include('rest_framework.urls', namespace='rest_framework')),   # 增加api认证
    path('api-token-auth/', views.obtain_auth_token),    # 获得token,利用rest框架自身功能实现

]


# 如果开启debug模式,使用media文件需要加入以下设置
if settings.DEBUG:
    urlpatterns += static(settings.MEDIA_URL,document_root=settings.MEDIA_ROOT)

应用urls

from django.urls import path
from rest_framework.urlpatterns import format_suffix_patterns
from computerapp import views


urlpatterns = [
    path('user_info/', views.UserInfoView.as_view()),
    path('user_profile_ru/<int:pk>/', views.UserProfileRUView.as_view()),
    path('product_list/', views.ProductListView.as_view()),
    path('product_list_by_category/', views.ProductListByCategoryView.as_view()),
    path('product_list_by_category_manufacturer/', views.ProductListByCategoryManufacturerView.as_view()),
    path('product_retrieve/<int:pk>/', views.ProductRetrieveView.as_view()),
    path('user_create/', views.UserCreateView.as_view()),
    path('delivery_address_lc/', views.DeliveryAddressLCView.as_view()),
    path('delivery_address_rud/<int:pk>/', views.DeliveryAddressRUDView.as_view()),
    path('cart_list/', views.CartListView.as_view()),
    path('order_list/', views.OrderListView.as_view()),
    path('order_create/', views.OrderCreateView.as_view()),
    path('order_rud/<int:pk>/', views.OrderRUDView.as_view()),

]

urlpatterns = format_suffix_patterns(urlpatterns)

增加用户信息

model使用时系统自带的用户user

views增加用户基本信息

在这里插入图片描述

serializers增加

在这里插入图片描述

urls增加

在这里插入图片描述

models增加用户扩展信息

在这里插入图片描述

增加认证功能

认证方法

流程
前端通过用户名密码访问token获取接口,获取token,存储在本地,以后再访问需要认证的资源,可以带着这个token访问接口,后端即认为是此用户名密码用户访问。
目前常用的为客户端webstorage,服务端token;cookie和session方法不再常用
在这里插入图片描述

使用token方法

要使用 TokenAuthentication 方案 ,需要在setting包含rest_framework.authtoken应用项;并且配置认证类包含TokenAuthentication。

INSTALLED_APPS = [
    ...
    'rest_framework.authtoken'
]

增加认证类
在这里插入图片描述
当使用 TokenAuthentication, 你可能需要通过提供用户名和密码而获得token这样一个机制。REST framework提供了一个内置的 view 来实现这个功能。只需要添加 obtain_auth_token view 到你的 URLconf就可以使用它。

from rest_framework.authtoken import views
urlpatterns += [
    path('api-token-auth/', views.obtain_auth_token)
]

用户前端访问这个接口,只需要这一行代码即完成token的生成并返回。
Note that the URL part of the pattern can be whatever you want to use.

The obtain_auth_token view will return a JSON response when valid username and password fields are POSTed to the view using form data or JSON:

{ 'token' : '9944b09199c62bcf9418ad846dd0e4bbdfc6ee4b' }

配置完成后的数据库迁移

manage.py makemigrations 
manage.py migrate


rest_framework.authtoken
应用提供了数据库迁移。

C:\djproject\eshop>python manage.py makemigrations
Migrations for 'computerapp':
  computerapp\migrations\0002_auto_20210404_1048.py
    - Change Meta options on category
    - Change Meta options on product
    - Alter field name on category
    - Alter field category on product
    - Alter field model on product
    - Alter field price on product

C:\djproject\eshop>python manage.py migrate
Operations to perform:
  Apply all migrations: admin, auth, authtoken, computerapp, contenttypes, sessions
Running migrations:
  Applying computerapp.0002_auto_20210404_1048... OK

以上第一条记录auth, authtoken,数据迁移。
以下是先做,报了错误,修改后然后做的上面步骤。

C:\djproject\eshop>python manage.py migrate
System check identified some issues:

WARNINGS:
?: (rest_framework.W001) You have specified a default PAGE_SIZE pagination rest_framework setting, without specifying also a DEFAULT_PAGINATION_CLASS.
        HINT: The default for DEFAULT_PAGINATION_CLASS is None. In previous versions this was PageNumberPagination. If you wish to define PAGE_SIZE globally whilst defining pagination_
class on a per-view basis you may silence this check.
Operations to perform:
  Apply all migrations: admin, auth, authtoken, computerapp, contenttypes, sessions
Running migrations:
  Applying admin.0003_logentry_add_action_flag_choices... OK
  Applying auth.0009_alter_user_last_name_max_length... OK
  Applying auth.0010_alter_group_name_max_length... OK
  Applying auth.0011_update_proxy_permissions... OK
  Applying auth.0012_alter_user_first_name_max_length... OK
  Applying authtoken.0003_tokenproxy... OK

在根urls增加

在这里插入图片描述
第一行为引入views
第二行为增加认证功能
第三行为获得token功能,利用了rest自带功能

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1265691.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

局域网协议:ICMP (Internet Control Message Protocol,互联网控制消息协议)

ICMP&#xff08;Internet Control Message Protocol&#xff0c;互联网控制消息协议&#xff09;是用于在IP网络中传递控制消息的协议。它通常被用于网络设备之间交换状态信息和错误报告&#xff0c;以及执行网络诊断和故障排除。 文章目录 ICMP主要功能ICMP的工作原理ICMP消…

Spring Boot 3 + Spring Security 6 最新版本修改 Json 登录后 RememberMe 功能问题失效的解决方案

当 Spring Boot 版本更新到 3 之后&#xff0c;最低要求的 JDK 版本变为 17&#xff0c;相应的 最新版本的 Spring Security 的配置也发生了变化&#xff0c;一下主要讲解一些新的 Spring Security 的配置方法 1. 配置由继承WebSeucrityConfigurerAdapter变成只需添加一个Secur…

查看当前目录下文件数量

查看当前目录下文件数量 查看文件夹数量查看文件数查看所有文件&#xff08;包括子文件&#xff09;数量查看所有目录&#xff08;包括子目录&#xff09;数量查看图片数量 查看文件夹数量 ls -l | grep ^d | wc -l查看文件数 不包含文件夹 ls -l | grep ^- | wc -l查看所有…

Vue 定义只读数据 readonly 与 shallowReadonly

readonly 让一个响应式数据变为 **深层次的只读数据**。 shallowReadonly 让一个响应式数据变为 **浅层次的只读数据**&#xff0c;只读第一层。 isReadonly 判断一个数据是不是只读数据。 应用场景&#xff1a;不希望数据被修改时使用。 readonly深层次只读&#xff1a; …

Matplotlib直方图的创建_Python数据分析与可视化

Matplotlib直方图的创建 概念区分绘制直方图 概念区分 什么是直方图&#xff1f; 直方图&#xff08;Histogram&#xff09;又称质量分布图&#xff0c;是统计报告图的一种&#xff0c;由一系列高度不等的纵向条纹或线段表示数据分布的情况&#xff0c;一般用横轴表示数据所属…

16.spirng源码解析-registerBeanPostProcessors

注册拦截bean创建的bean处理器 此部分实质上是在BeanDefinitions中寻找BeanPostProcessor&#xff0c;之后调用BeanFactory.addBeanPostProcessor方法保存在一个List中&#xff0c;注意添加时仍然有优先级的概念&#xff0c;优先级高的在前面。

ESP-Mesh-Lite 用户指南

ESP-MESH-LITE 本指南提供有关 Mesh-Lite 协议的介绍。 概述 ESP-MESH-LITE 是一套建立在 Wi-Fi 协议之上的网络协议。ESP-MESH-LITE 允许分布在大范围区域内&#xff08;室内和室外&#xff09;的大量设备&#xff08;下文称节点&#xff09;在同一个 WLAN&#xff08;无线…

聚观早报 |红魔9 Pro开卖;真我GT5 Pro定档

【聚观365】11月29日消息 红魔9 Pro开卖 真我GT5 Pro定档 一加12镜头细节公布 Redmi K70 Pro将搭载夜枭算法 苹果Vision Pro头显下月量产 红魔9 Pro开卖 红魔电竞旗舰最新力作——红魔9 Pro系列正式发布。作为一款全能电竞旗舰&#xff0c;该机搭载了第三代骁龙8移动平台…

如何恢复已删除的照片 ?适用于 Windows 的Android 数据恢复软件值得尝试

“我丢失了 Android 手机上的照片&#xff0c;有人告诉我使用恢复程序来找回所有手机数据。我使用的是 Windows 10 和华为 手机&#xff0c;对于 Windows最有效的 Android 数据恢复是什么&#xff1f;” Android 恢复程序用于检索丢失或删除的文件&#xff0c;如照片、联系人、…

pytorch读取tiny-imagenet-200的验证集(val)

ori_train torchvision.datasets.ImageFolder(root args.datadir /tiny-imagenet-200/train/, transformtransform)#可以获取class_idx的映射class_idx ori_train.class_to_idx val_annotations.txt中存储着每个图片对应的类别 获取验证集的标签 test_target []#读取val_…

uniapp微信小程序中阻止事件冒泡

开发场景&#xff1a;列表中展示地块的数据信息&#xff0c;用户可以点击列表进入地块的详情界面&#xff0c;也可以点击列表中的星星按钮进行收藏 遇到的问题&#xff1a;每次点击星星的时候&#xff0c;都会触发父级的点击事件&#xff0c;从而进入到详情界面 原本的代码&am…

红队攻防之hash登录RDP

没什么好害怕&#xff0c;孩子放心去飞吧&#xff0c;在你的身后有个等你的家 Restricted Admin Mode 受限管理模式是一项 Windows 功能&#xff0c;可防止将 RDP 用户的凭据存储在建立 RDP 连接的计算机的内存中。 这是用来防止用户&#xff08;管理员&#xff09;在 RDP 进…

VT-VSPA1-1X比例压力阀控制板

替代力士乐同型号,可以完全互换使用&#xff1b;适用于力士乐系列所有无电位置反馈的直动式和先导式比例压力阀的控制&#xff1b;外置欧板式连接&#xff0c;VT-VSPA1-1X型放大器配套支架型号&#xff1a;VT-3002-1X/32D或VT-3002-2X/32D&#xff1b; VT-VSPA2-1-1X/T1、VT-V…

面试必须要知道的MySQL知识--索引

10 索引 10.1 数据页存储结构 10.1.1 数据页的各个部分 在讲索引之前&#xff0c;让我们看看一个单独的数据页是什么样子的 去除掉一些我们不太需要那么关注的部分后&#xff0c;简化如下&#xff1a; 也就是说平时我们在一个表里插入的一行一行的数据会存储在数据页里&#…

DDR-MIG 学习记录

MIG调试总结&#xff1a; 对vivado软件中用于控制DDR2 / DDR3 的 控制器MIG(Memory Interface Generator)IP核进行了仿真测试&#xff0c;以学习如何用IP核来控制FPGA板载SDRAM的读写。我们只需要学会MIG的接口控制就可以。 ①配置IP核 Xilinx 的 DDR 控制器的名称简写为 MIG&…

解密Long型数据传递:Spring Boot后台如何避免精度丢失问题

前端和后端之间的数据传递至关重要。然而&#xff0c;当涉及到Long类型数据时&#xff0c;可能会出现精度丢失问题&#xff0c;这会影响数据的准确性。本文将为你介绍两种解决方案&#xff0c;帮助你确保Long类型数据在前端和后端之间的精确传递。 精度丢失测试 访问:http://l…

深度学习可解释性Python库

本文整理了10个常用于可解释AI的Python库&#xff0c;方便我们更好的理解AI模型的决策。 原文阅读 什么是XAI&#xff1f; XAI&#xff08;Explainable AI&#xff09;的目标是为模型的行为和决策提供合理的解释&#xff0c;这有助于增加信任、提供问责制和模型决策的透明度…

SAP FB01 更新采购凭证历史EKBE

参考链接BAPI_ACC_DOCUMENT_POST – Vendor Down payment: Update Purchase order info and PO history 不需要链接里的替代过程&#xff0c;可以直接写在函数BAPI_ACC_DOCUMENT_POST的增强结构EXTENSION2里 需要复制BTE增强1050 在其中调用函数ME_CREATE_HISTORY_FINANCE 即…

工具 | docker删除不使用的容器

工具 | docker删除不使用的容器 Docker 清理命令

ESP32-Web-Server 实战编程-通过网页控制设备的 GPIO

ESP32-Web-Server 实战编程-通过网页控制设备的 GPIO 概述 前述博客讲解了 Web 编程的基本知识&#xff0c;包括 HTML、CSS、JavaScript 三个部分&#xff0c;从这节开始&#xff0c;我们进入实战部分&#xff0c;在实际项目中进一步学习 ESP32-Web 编程。 GPIO &#xff08…