Zhong__Django framework笔记

时间:2020.09.15

环境:Python3.7 Django==3.0.8  djangorestframework==3.11.0

目的:快速开发web应用啊

说明:笔记

作者:Zhong QQ交流群:121160124 欢迎加入!


目录

DRF简介

安装Django环境

安装DRF

DRF认证

JWT

DRF权限管理

DRF视图

APIView

GenericAPIView

提供的方法

五个扩展类

几个可用子类视图

ViewSet

常用视图集父类

 视图集的继承关系

路由Routers

视图方法对比

其它

限流

过滤

排序

分页

异常处理

自动生成接口文档

 


DRF简介

Django REST framework 框架是一个用于构建Web API 的强大而又灵活的工具。

通常简称为DRF框架 或 REST framework。

DRF框架是建立在Django框架基础之上 所以前提是需要安装Django环境

安装Django环境

pip install -i https://pypi.douban.com/simple django

安装DRF

pip install -i https://pypi.douban.com/simple djangorestframework

 在settings.py文件 INSTALLED_APPS 选项中增加DRF应用配置 这儿名称为rest_framework

INSTALLED_APPS = [
    ...

    'rest_framework',
]

DRF认证

JWT

JWT 即Json Web Token 我们使用JWT来认证请求用户

安装JWT插件 这儿使用推荐使用djangorestframework-simplejwt 因为djangorestframework-jwt据说已经不更新了

pip install -i https://pypi.douban.com/simple djangorestframework-simplejwt

在settings.py中添加配置项

# DRF一般的配置项在此REST_FRAMEWORK对象配置中定义
REST_FRAMEWORK = {
    # 默认认证类  是一个列表或元祖对象
    'DEFAULT_AUTHENTICATION_CLASSES': [
        'rest_framework_simplejwt.authentication.JWTAuthentication',
        'rest_framework.authentication.SessionAuthentication',
        'rest_framework.authentication.BasicAuthentication',
    ],
}


# JWT相关配置
SIMPLE_JWT = {
        'ACCESS_TOKEN_LIFETIME': datetime.timedelta(hours=8), # 访问token有效期8小时
        'REFRESH_TOKEN_LIFETIME': datetime.timedelta(days=30), # 刷新token有效期30天
        'ROTATE_REFRESH_TOKENS': False,
        'BLACKLIST_AFTER_ROTATION': True,

        'ALGORITHM': 'HS256',
        'SIGNING_KEY': SECRET_KEY,
        'VERIFYING_KEY': None,
        'AUDIENCE': None,
        'ISSUER': None,

        'AUTH_HEADER_TYPES': ('Bearer',),
        'USER_ID_FIELD': 'id',
        'USER_ID_CLAIM': 'user_id',

        # 'AUTH_TOKEN_CLASSES': ('rest_framework_simplejwt.tokens.AccessToken',),
        # 'TOKEN_TYPE_CLAIM': 'token_type',

        'JTI_CLAIM': 'jti',

        'SLIDING_TOKEN_REFRESH_EXP_CLAIM': 'refresh_exp',
        'SLIDING_TOKEN_LIFETIME': datetime.timedelta(minutes=5),
        'SLIDING_TOKEN_REFRESH_LIFETIME': datetime.timedelta(days=1),
    }

 配置url 在根url文件urls.py中增加JWT的签发token路由和刷新token路由的项

urlpatterns = [
    path('admin/', admin.site.urls),

   
    path('api-auth/', include('rest_framework.urls', namespace='rest_framework')),

    url(r'^api-token-auth/', TokenObtainPairView.as_view(), name='token_obtain_pair'),
    url(r'^api-token-refresh/', TokenRefreshView.as_view(), name='token_refresh'),
    url(r'^api/token/verify/', TokenVerifyView.as_view(), name='token_verify'),

]

启动项目 使用postman测试 输入url地址:http://127.0.0.1:8000/api-token-auth/  选择POST方法 在Body选择form-data 传入username和password参数 alice为数据库中已存在的用户

 发送请求后返回access字段和refresh字段 access字段就是包含了alice用户信息的JWT加密后的字符串 可以使用它在请求API时进行身份验证 由于在配置文件中设置了8小时有效期 所以8小时后此token将失效 需要携带refresh token来刷新access token

编写一个简单的视图 请求用户列表 如果不使用JWT 将会提示如下

 这是因为启用了JWT认证 而请求中并没有发送使用任何验证用户身份的识别信息 所以要通过验证 需要使用access token 因为在JWT配置中指定了AUTH_HEADER_TYPES为Bearer 所以在postman的Authorization项中配置TYPE为Bearer Token 并写入通过http://127.0.0.1:8000/api-token-auth/路由请求到的access 再次发送请求

如果是前后端分离项目 假如前端使用Vue框架的话 可以在登录通过后获取到后端返回的结果 将其存储在localStorage中 需要注意的是在发送到后端验证时JWT需要有Bearer前缀 与JWT字符串用一个空格连接 可以在存储时加上前缀 也可以在验证时加上前缀 两者选择其中一种方式即可

Login.vue

methods: {
        login () {
        let _this = this;
        if (this.loginForm.username === '' || this.loginForm.password === '') {
            alert('账号或密码不能为空');
        } else {
            this.$http({
            method: 'post',
            url: '/api-token-auth/',
            data: {username: _this.loginForm.username,
                    password: _this.loginForm.password}

            }).then(res => {

            window.localStorage.setItem('access-token','Bearer ' + res.data.access)
            window.localStorage.setItem('refresh-token','Bearer ' + res.data.refresh)
            _this.$router.push('/index');
            alert('登陆成功');

            }).catch(error => {
            alert('账号或密码错误');
            });
        }
        }
    }

在main.js中设置请求拦截器

// 请求拦截器
axios.interceptors.request.use(config => {
  config.headers.Authorization = window.localStorage.getItem('access-token')
  return config
})

DRF权限管理

认证一般是和权限配合使用的 认证让系统知道你是谁 权限管理赋予你对应身份的权限 对哪些视图拥有什么动作权限 例如:

对于未认证的匿名用户只能执行GET请求获取查看用户列表 对于已认证用户拥有POST、PUT、DELETE请求权限来执行用户增加、更新和删除操作

在settings.py文件REST_FRAMEWORK配置中增加DEFAULT_PERMISSION_CLASSES选项 这儿是全局配置

REST_FRAMEWORK = {
        # 默认的认证管理类
        'DEFAULT_AUTHENTICATION_CLASSES': [
            'rest_framework_simplejwt.authentication.JWTAuthentication',
            'rest_framework.authentication.SessionAuthentication',
            'rest_framework.authentication.BasicAuthentication',
        ],

        # 默认权限级别 AllowAny 允许所有的访问 一般设置为 IsAuthenticated 允许已认证用户
        'DEFAULT_PERMISSION_CLASSES': (
           # 'rest_framework.permissions.AllowAny',
           'rest_framework.permissions.IsAuthenticated',
        ),

}

此时的权限允许已通过JWT认证通过的用户执行所有的动作 为了测试 我们在视图内部定义局部的权限配置 局部配置将在生效的视图范围内覆盖全局配置 即局部配置优先级高于全局配置优先级

# 查询、更新、删除数据
class PersonnelDetail(generics.RetrieveUpdateDestroyAPIView):
  
    authentication_classes = [authentication.JWTAuthentication]
    permission_classes = [permissions.IsAuthenticatedOrReadOnly,]

    queryset = STModel.objects.all()
    serializer_class = ST_Serializers

    def get(self, request, *args, **kwargs):
        print(request.user)
        print(request.auth)
        return self.retrieve(request, *args, **kwargs)

 此时的定义为访问此API使用JWT认证 权限为如果已认证可以GET(获取数据)、PUT(更新数据)、和DELETE(删除数据)  如果未认证即匿名用户只能GET(获取数据)

通过JWT认证

匿名用户

认证和权限管理的选项很多 可根据需求进行搭配组合使用

DRF视图

Django内置的视图类View方便我们编写代码 但DRF带来了更强大更多的方式来满足不同的开发需求 所以基本上 使用Django框架大多都会使用REST Framework扩展来提升开发效率和简化代码的逻辑

DRF有很多类和mixin扩展可使用 为了便于快速上手和区分 这儿大概将其分为3类

  • APIView
  • GenericAPIView(以及它的拓展和mixin)
  • ViewSet

APIView

APIView是REST framework提供的所有视图的基类  继承自Django的View父类

支持定义的属性:

  • authentication_classes 列表或元祖,身份认证类
  • permissoin_classes 列表或元祖,权限检查类
  • throttle_classes 列表或元祖,流量控制类

APIView中仍以常规的类视图定义方法来实现get() 、post() 或者其他请求方式的方法。

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

# url(r'^books/$', views.BookListView.as_view()),
class BookListView(APIView):

    def get(self, request):

        books = BookInfo.objects.all()
        serializer = BookInfoSerializer(books, many=True)

        return Response(serializer.data)

GenericAPIView

GenericAPIView继承自APIVIew,增加了对于列表视图和详情视图可能用到的通用支持方法。通常使用时,可搭配一个或多个Mixin扩展类

支持定义的属性:

  • 列表视图与详情视图通用:
    • queryset 列表视图的查询集
    • serializer_class 视图使用的序列化器
  • 列表视图使用:
    • pagination_class 分页控制类
    • filter_backends 过滤控制后端
  • 详情页视图使用:
    • lookup_field 查询单一数据库对象时使用的条件字段,默认为'pk'
    • lookup_url_kwarg 查询单一数据时URL中的参数关键字名称,默认与look_field相同

提供的方法

列表视图与详情视图通用

  • get_queryset(self)

    返回视图使用的查询集,是列表视图与详情视图获取数据的基础,默认返回queryset属性,可以重写,例如

def get_queryset(self):
    user = self.request.user
    return user.accounts.all()

 

  • get_serializer_class(self)

返回序列化器类,默认返回serializer_class,可以重写

def get_serializer_class(self):
    if self.request.user.is_staff:
        return FullAccountSerializer
    return BasicAccountSerializer
  • get_serializer(self, args, *kwargs)

返回序列化器对象,被其他视图或扩展类使用,如果我们在视图中想要获取序列化器对象,可以直接调用此方法

注意,在提供序列化器对象的时候,REST framework会向对象的context属性补充三个数据:request、format、view,这三个数据对象可以在定义序列化器时使用

详情视图使用

  • get_object(self) 返回详情视图所需的模型类数据对象,默认使用lookup_field参数来过滤queryset。 在试图中可以调用该方法获取详情信息的模型类对象。

    若详情访问的模型类对象不存在,会返回404。

    该方法会默认使用APIView提供的check_object_permissions方法检查当前对象是否有权限被访问。

# url(r'^books/(?P<pk>\d+)/$', views.BookDetailView.as_view()),
class BookDetailView(GenericAPIView):
    queryset = BookInfo.objects.all()
    serializer_class = BookInfoSerializer

    def get(self, request, pk):
        book = self.get_object()
        serializer = self.get_serializer(book)
        return Response(serializer.data)

五个扩展类

1)ListModelMixin

列表视图扩展类,提供list(request, *args, **kwargs)方法快速实现列表视图,返回200状态码。

该Mixin的list方法会对数据进行过滤和分页。

源代码:

class ListModelMixin(object):
    """
    List a queryset.
    """
    def list(self, request, *args, **kwargs):
        # 过滤
        queryset = self.filter_queryset(self.get_queryset())
        # 分页
        page = self.paginate_queryset(queryset)
        if page is not None:
            serializer = self.get_serializer(page, many=True)
            return self.get_paginated_response(serializer.data)
        # 序列化
        serializer = self.get_serializer(queryset, many=True)
        return Response(serializer.data)

举例:

from rest_framework.mixins import ListModelMixin

class BookListView(ListModelMixin, GenericAPIView):
    queryset = BookInfo.objects.all()
    serializer_class = BookInfoSerializer

    def get(self, request):
        return self.list(request)

2)CreateModelMixin

创建视图扩展类,提供create(request, *args, **kwargs)方法快速实现创建资源的视图,成功返回201状态码。

如果序列化器对前端发送的数据验证失败,返回400错误。

源代码:

class CreateModelMixin(object):
    """
    Create a model instance.
    """
    def create(self, request, *args, **kwargs):
        # 获取序列化器
        serializer = self.get_serializer(data=request.data)
        # 验证
        serializer.is_valid(raise_exception=True)
        # 保存
        self.perform_create(serializer)
        headers = self.get_success_headers(serializer.data)
        return Response(serializer.data, status=status.HTTP_201_CREATED, headers=headers)

    def perform_create(self, serializer):
        serializer.save()

    def get_success_headers(self, data):
        try:
            return {'Location': str(data[api_settings.URL_FIELD_NAME])}
        except (TypeError, KeyError):
            return {}

3) RetrieveModelMixin

详情视图扩展类,提供retrieve(request, *args, **kwargs)方法,可以快速实现返回一个存在的数据对象。

如果存在,返回200, 否则返回404。

源代码:

class RetrieveModelMixin(object):
    """
    Retrieve a model instance.
    """
    def retrieve(self, request, *args, **kwargs):
        # 获取对象,会检查对象的权限
        instance = self.get_object()
        # 序列化
        serializer = self.get_serializer(instance)
        return Response(serializer.data)

举例:

class BookDetailView(RetrieveModelMixin, GenericAPIView):
    queryset = BookInfo.objects.all()
    serializer_class = BookInfoSerializer

    def get(self, request, pk):
        return self.retrieve(request)

4)UpdateModelMixin

更新视图扩展类,提供update(request, *args, **kwargs)方法,可以快速实现更新一个存在的数据对象。

同时也提供partial_update(request, *args, **kwargs)方法,可以实现局部更新。

成功返回200,序列化器校验数据失败时,返回400错误。

源代码:

class UpdateModelMixin(object):
    """
    Update a model instance.
    """
    def update(self, request, *args, **kwargs):
        partial = kwargs.pop('partial', False)
        instance = self.get_object()
        serializer = self.get_serializer(instance, data=request.data, partial=partial)
        serializer.is_valid(raise_exception=True)
        self.perform_update(serializer)

        if getattr(instance, '_prefetched_objects_cache', None):
            # If 'prefetch_related' has been applied to a queryset, we need to
            # forcibly invalidate the prefetch cache on the instance.
            instance._prefetched_objects_cache = {}

        return Response(serializer.data)

    def perform_update(self, serializer):
        serializer.save()

    def partial_update(self, request, *args, **kwargs):
        kwargs['partial'] = True
        return self.update(request, *args, **kwargs)

5)DestroyModelMixin

删除视图扩展类,提供destroy(request, *args, **kwargs)方法,可以快速实现删除一个存在的数据对象。

成功返回204,不存在返回404。

源代码:

class DestroyModelMixin(object):
    """
    Destroy a model instance.
    """
    def destroy(self, request, *args, **kwargs):
        instance = self.get_object()
        self.perform_destroy(instance)
        return Response(status=status.HTTP_204_NO_CONTENT)

    def perform_destroy(self, instance):
        instance.delete()

几个可用子类视图

1) CreateAPIView

提供 post 方法

继承自: GenericAPIView、CreateModelMixin

2)ListAPIView

提供 get 方法

继承自:GenericAPIView、ListModelMixin

3)RetireveAPIView

提供 get 方法

继承自: GenericAPIView、RetrieveModelMixin

4)DestoryAPIView

提供 delete 方法

继承自:GenericAPIView、DestoryModelMixin

5)UpdateAPIView

提供 put 和 patch 方法

继承自:GenericAPIView、UpdateModelMixin

6)RetrieveUpdateAPIView

提供 get、put、patch方法

继承自: GenericAPIView、RetrieveModelMixin、UpdateModelMixin

7)RetrieveUpdateDestoryAPIView

提供 get、put、patch、delete方法

继承自:GenericAPIView、RetrieveModelMixin、UpdateModelMixin、DestoryModelMixin

ViewSet

使用视图集ViewSet,可以将一系列逻辑相关的动作放到一个类中:

  • list() 提供一组数据
  • retrieve() 提供单个数据
  • create() 创建数据
  • update() 保存数据
  • destory() 删除数据

ViewSet视图集类不再实现get()、post()等方法,而是实现动作 action 如 list() 、create() 等。

视图集只在使用as_view()方法的时候,才会将action动作与具体请求方式对应上。

例如

class BookInfoViewSet(viewsets.ViewSet):

    def list(self, request):
        ...

    def retrieve(self, request, pk=None):
        ...

在设置路由时,我们可以如下操作

urlpatterns = [
    url(r'^books/$', BookInfoViewSet.as_view({'get':'list'}),
    url(r'^books/(?P<pk>\d+)/$', BookInfoViewSet.as_view({'get': 'retrieve'})
]

action属性

在视图集中,我们可以通过action对象属性来获取当前请求视图集时的action动作是哪个。

def get_serializer_class(self):
    if self.action == 'create':
        return OrderCommitSerializer
    else:
        return OrderDataSerializer

常用视图集父类

  • ViewSet

继承自APIView,作用也与APIView基本类似,提供了身份认证、权限校验、流量管理等。

在ViewSet中,没有提供任何动作action方法,需要我们自己实现action方法。

  • GenericViewSet

继承自GenericAPIView,作用也与GenericAPIVIew类似,提供了get_object、get_queryset等方法便于列表视图与详情信息视图的开发。

  • ModelViewSet

继承自GenericAPIVIew,同时包括了ListModelMixin、RetrieveModelMixin、CreateModelMixin、UpdateModelMixin、DestoryModelMixin。

  • ReadOnlyModelViewSet

继承自GenericAPIVIew,同时包括了ListModelMixin、RetrieveModelMixin。

视图集中定义附加action动作

在视图集中,除了上述默认的方法动作外,还可以添加自定义动作。

添加自定义动作需要使用rest_framework.decorators.action装饰器。

以action装饰器装饰的方法名会作为action动作名,与list、retrieve等同。

action装饰器可以接收两个参数:

  • methods: 该action支持的请求方式,列表传递
  • detail: 表示是action中要处理的是否是视图资源的对象(即是否通过url路径获取主键)
    • True 表示使用通过URL获取的主键对应的数据对象
    • False 表示不使用URL获取主键

 例如

from rest_framework import mixins
from rest_framework.viewsets import GenericViewSet
from rest_framework.decorators import action

class BookInfoViewSet(mixins.ListModelMixin, mixins.RetrieveModelMixin, GenericViewSet):
    queryset = BookInfo.objects.all()
    serializer_class = BookInfoSerializer

    # detail为False 表示不需要处理具体的BookInfo对象
    @action(methods=['get'], detail=False)
    def latest(self, request):
        """
        返回最新的图书信息
        """
        book = BookInfo.objects.latest('id')
        serializer = self.get_serializer(book)
        return Response(serializer.data)

    # detail为True,表示要处理具体与pk主键对应的BookInfo对象
    @action(methods=['put'], detail=True)
    def read(self, request, pk):
        """
        修改图书的阅读量数据
        """
        book = self.get_object()
        book.bread = request.data.get('read')
        book.save()
        serializer = self.get_serializer(book)
        return Response(serializer.data)

url的定义

urlpatterns = [
    url(r'^books/$', views.BookInfoViewSet.as_view({'get': 'list'})),
    url(r'^books/latest/$', views.BookInfoViewSet.as_view({'get': 'latest'})),
    url(r'^books/(?P<pk>\d+)/$', views.BookInfoViewSet.as_view({'get': 'retrieve'})),
    url(r'^books/(?P<pk>\d+)/read/$', views.BookInfoViewSet.as_view({'put': 'read'})),
]

 视图集的继承关系

路由Routers

对于视图集ViewSet,我们除了可以自己手动指明请求方式与动作action之间的对应关系外,还可以使用Routers来帮助我们快速实现路由信息。

REST framework提供了两个router

  • SimpleRouter
  • DefaultRouter

使用方法

 创建router对象,并注册视图集

from rest_framework import routers

router = routers.SimpleRouter()
router.register(r'books', BookInfoViewSet, base_name='book')

register(prefix, viewset, base_name)

  • prefix 该视图集的路由前缀
  • viewset 视图集
  • base_name 路由名称的前缀

如上述代码会形成的路由如下

^books/$    name: book-list
^books/{pk}/$   name: book-detail

添加路由数据

可以有两种方式

urlpatterns = [
    ...
]
urlpatterns += router.urls

 或

urlpatterns = [
    ...
    url(r'^', include(router.urls))
]

 视图集中包含附加action的

class BookInfoViewSet(mixins.ListModelMixin, mixins.RetrieveModelMixin, GenericViewSet):
    queryset = BookInfo.objects.all()
    serializer_class = BookInfoSerializer

    @action(methods=['get'], detail=False)
    def latest(self, request):
        ...

    @action(methods=['put'], detail=True)
    def read(self, request, pk):
        ...

此视图集会形成的路由

^books/latest/$    name: book-latest
^books/{pk}/read/$  name: book-read

 路由router形成URL的方式

SimpleRouter

DefaultRouter

DefaultRouter与SimpleRouter的区别是,DefaultRouter会多附带一个默认的API根视图,返回一个包含所有列表视图的超链接响应数据

视图方法对比

APIView代码量相对来说是最多最直观的 灵活性可定制性最佳 开发时间也最长

GenericAPIView代码量适中 灵活性也是中等 可以根据不同的需求使用不同的扩展类

ViewSet是常用类高度集成 抽象度最高 代码最为简洁 效率高但也不容易理解和定制特殊的需求的方法

其它

限流

过滤

Filtering

对于列表数据可能需要根据字段进行过滤,我们可以通过添加django-fitlter扩展来增强支持。

pip install -i https://pypi.douban.com/simple django-filter

在配置文件中增加过滤后端的设置:

INSTALLED_APPS = [
    ...
    'django_filters',  # 需要注册应用,
]

REST_FRAMEWORK = {
    'DEFAULT_FILTER_BACKENDS': ('django_filters.rest_framework.DjangoFilterBackend',)
}

在视图中添加filter_fields属性,指定可以过滤的字段

class BookListView(ListAPIView):
    queryset = BookInfo.objects.all()
    serializer_class = BookInfoSerializer
    filter_fields = ('btitle', 'bread')

# 127.0.0.1:8000/books/?btitle=西游记

排序

分页

分页Pagination

REST framework提供了分页的支持  可以在配置文件中设置全局的分页方式  

REST_FRAMEWORK = {

    ......


    'DEFAULT_PAGINATION_CLASS':  'rest_framework.pagination.PageNumberPagination',
    'PAGE_SIZE': 10  # 每页数目
}

也可通过自定义Pagination类,来为视图添加不同分页行为

class GenericSetPagination(PageNumberPagination):
    page_size = 10
    page_size_query_param = 'page_size'
    max_page_size = 10000

在视图中通过pagination_class属性来指明 

class BookDetailView(RetrieveAPIView):
    queryset = BookInfo.objects.all()
    serializer_class = BookInfoSerializer
    pagination_class = GenericSetPagination

注意:如果在视图内关闭分页功能,只需在视图内设置

pagination_class = None

使用PageNumberPagination分页器 url应如 http://127.0.0.1:8000/api/books/?page=1&page_size=10

全局配置时默认page参数是可用的 page_size是未启用的 可以通过自定义分页器指定属性page_size_query_param = 'page_size' 或者其它字符串来启用

可选分页器

PageNumberPagination

前端访问网址形式: GET  http://api.example.org/books/?page=4

可以在子类中定义的属性:

  • page_size 每页数目
  • page_query_param 前端发送的页数关键字名,默认为"page"
  • page_size_query_param 前端发送的每页数目关键字名,默认为None
  • max_page_size 前端最多能设置的每页数量
from rest_framework.pagination import PageNumberPagination

class StandardPageNumberPagination(PageNumberPagination):
    page_size_query_param = 'page_size'
    max_page_size = 10

class BookListView(ListAPIView):
    queryset = BookInfo.objects.all().order_by('id')
    serializer_class = BookInfoSerializer
    pagination_class = StandardPageNumberPagination

# 127.0.0.1/books/?page=1&page_size=2

LimitOffsetPagination

前端访问网址形式:GET http://api.example.org/books/?limit=10&offset=40

可以在子类中定义的属性:

  • default_limit 默认限制,默认值与PAGE_SIZE设置一致
  • limit_query_param limit参数名,默认'limit'
  • offset_query_param offset参数名,默认'offset'
  • max_limit 最大limit限制,默认None
from rest_framework.pagination import LimitOffsetPagination

class BookListView(ListAPIView):
    queryset = BookInfo.objects.all().order_by('id')
    serializer_class = BookInfoSerializer
    pagination_class = LimitOffsetPagination

# 127.0.0.1:8000/books/?offset=3&limit=2

异常处理

自动生成接口文档

 

 

微信公众号

 

 

 

 

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

我变了_我没变

随意 。。。

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值