DRF 商城项目 - 用户( 登录, 注册,登出,个人中心 ) 逻辑梳理

用户登录

自定义用户登录字段处理

用户的登录时通过 手机号也可以进行登录

需要重写登录验证逻辑

from django.contrib.auth.backends import ModelBackend

class CustomBackend(ModelBackend):

    def authenticate(self, username=None, password=None, **kwargs):
        try:
            user = User.objects.get(Q(username=username) | Q(mobile=username))
            # 前端的用户传递过来的密码和数据库的保存密码是不一致的, 因此需要使用 check_password 的方式进行比对
            if user.check_password(password):
                return user
        except Exception as e:
            return None

登录逻辑

通过 login 接口进入验证, 调用默认重写后的验证逻辑进行处理

 url(r'^login/', obtain_jwt_token)

验证成功后会返回 token

用户注册

用户注册基于 手机号注册

验证码发送基于 云片网 提供的技术支持

验证码逻辑

验证码API 接口

# 配置手机验证码发送 的 url
router.register(r'codes', SmsCodeViewset, base_name="codes")

验证码序列化组件

选取序列化方式的时候以为不是全部的字段都需要用上, 因此不需用到 ModelSerializer

需要对前端拿到的  mobile 字段进行相关的验证

是否注册, 是否合法, 以及频率限制

# 手机验证序列化组件
# 不使用 ModelSerializer, 并不需要所有的字段, 会有麻烦
class SmsSerializer(serializers.Serializer):
    mobile = serializers.CharField(max_length=11)

    # 验证手机号码
    # validate_ + 字段名 的格式命名
    def validate_mobile(self, mobile):

        # 手机是否注册
        if User.objects.filter(mobile=mobile).count():
            raise serializers.ValidationError("用户已经存在")

        # 验证手机号码是否合法
        if not re.match(REGEX_MOBILE, mobile):
            raise serializers.ValidationError("手机号码非法")

        # 验证码发送频率
        # 当前时间减去一分钟( 倒退一分钟 ), 然后发送时间要大于这个时间, 表示还在一分钟内
        one_mintes_ago = datetime.now() - timedelta(hours=0, minutes=1, seconds=0)
        if VerifyCode.objects.filter(add_time__gt=one_mintes_ago, mobile=mobile).count():
            raise serializers.ValidationError("距离上一次发送未超过60s")
        return mobile

验证码视图

视图主要处理 验证码生成发送相关逻辑

具体的云片网接口对接处理详情官网查阅

# 发送短信验证码
class SmsCodeViewset(CreateModelMixin, viewsets.GenericViewSet):
    serializer_class = SmsSerializer

    # 生成四位数字的验证码
    def generate_code(self):

        seeds = "1234567890"
        random_str = []
        for i in range(4):
            random_str.append(choice(seeds))
        return "".join(random_str)

    # 重写 create 方法
    def create(self, request, *args, **kwargs):
        serializer = self.get_serializer(data=request.data)
        serializer.is_valid(raise_exception=True)
        # 验证后即可取出数据
        mobile = serializer.validated_data["mobile"]
        yun_pian = YunPian(APIKEY)
        code = self.generate_code()
        sms_status = yun_pian.send_sms(code=code, mobile=mobile)

        if sms_status["code"] != 0:
            return Response({
                "mobile": sms_status["msg"]
            }, status=status.HTTP_400_BAD_REQUEST)
        else:
            # 确认无误后需要保存数据库中
            code_record = VerifyCode(code=code, mobile=mobile)
            code_record.save()
            return Response({
                "mobile": mobile
            }, status=status.HTTP_201_CREATED)

云片验证码工具文件

# _*_ coding:utf-8 _*_
from YtShop.settings import APIKEY

__author__ = "yangtuo"
__date__ = "2019/4/15 20:25"
import requests
import json


# 云片网短信发送功能类
class YunPian(object):

    def __init__(self, api_key):
        self.api_key = api_key
        self.single_send_url = "https://sms.yunpian.com/v2/sms/single_send.json"

    def send_sms(self, code, mobile):
        parmas = {
            "apikey": self.api_key,
            "mobile": mobile,
            "text": "您的验证码是{code}。如非本人操作,请忽略本短信".format(code=code)
        }

        response = requests.post(self.single_send_url, data=parmas)
        re_dict = json.loads(response.text)
        return re_dict


if __name__ == "__main__":
    yun_pian = YunPian(APIKEY)
    yun_pian.send_sms("2019", "")  # 参数为 code 以及 mobile

配置文件

需要用到两个配置添加

# 手机号码的验证正则式
REGEX_MOBILE = "^1[358]\d{9}$|^147\d{8}$|^176\d{8}$"

# 云片网的 APIKEY 设置
APIKEY = "2480f562xxxxxxxxxxxxxcb7673f8"

注册逻辑

注册 API 接口

# 配置用户注册的 url
router.register(r'users', UserViewset, base_name="users")

注册序列化组件

用户注册需要的字段较多

每个字段都有些独有的特殊裁定

用户名  要进行重复判断

验证码  要进行有效期, 正确性判断

密码  设置 输入框为密码格式

在最后回传的时候 code 是不需要的, 因此可以删除掉

# 用户注册
class UserRegSerializer(serializers.ModelSerializer):
    """
    max_length      最大长度
    min_length      最小长度
    label           显示名字
    help_text       帮助提示信息
    error_messages  错误类型映射提示
        blank         空字段提示
        required      必填字段提示
        max_length    超长度提示
        min_length    过短提示
    write_only      只读, 序列化的时候忽略字段, 不再返回给前端页面, 用于去除关键信息(密码等)或者某些不必要字段(验证码)
    style           更改输入标签显示类型
    validators      可以指明一些默认的约束类
        UniqueValidator             约束唯一
        UniqueTogetherValidator     联合约束唯一
        UniqueForMonthValidator
        UniqueForDateValidator
        UniqueForYearValidator
        ....
    """
    code = serializers.CharField(required=True, write_only=True, max_length=4, min_length=4, label="验证码",
                                 error_messages={
                                     "blank": "请输入验证码",
                                     "required": "请输入验证码",
                                     "max_length": "验证码格式错误",
                                     "min_length": "验证码格式错误"
                                 },
                                 help_text="验证码")

    # validators 可以指明一些默认的约束类, 此处的 UniqueValidator 表示唯一约束限制不能重名
    username = serializers.CharField(label="用户名", help_text="用户名", required=True, allow_blank=False,
                                     validators=[UniqueValidator(queryset=User.objects.all(), message="用户已经存在")])

    # style 可以设置为密文状态
    password = serializers.CharField(
        style={'input_type': 'password'}, help_text="密码", label="密码", write_only=True,
    )

    # 用户表中的 password 是需要加密后再保存的, 次数需要重写一次 create 方法
    # 当然也可以不这样做, 这里的操作利用 django 的信号来处理, 详情见 signals.py
    # def create(self, validated_data):
    #     user = super(UserRegSerializer, self).create(validated_data=validated_data)
    #     user.set_password(validated_data["password"])
    #     user.save()
    #     return user

    # 对验证码的验证处理
    # validate_ + 字段对个别字段进行单一处理
    def validate_code(self, code):

        # 如果使用 get 方式需要处理两个异常, 分别是查找到多个信息的情况以及查询到0信息的情况的异常
        # 但是使用 filter 方式查到多个就以列表方式返回, 如果查询不到数据就会返回空值, 各方面都很方便
        # try:
        #     verify_records = VerifyCode.objects.get(mobile=self.initial_data["username"], code=code)
        # except VerifyCode.DoesNotExist as e:
        #     pass
        # except VerifyCode.MultipleObjectsReturned as e:
        #     pass

        # 前端传过来的所有的数据都在, initial_data 字典里面, 如果是验证通过的数据则保存在 validated_data 字典中
        verify_records = VerifyCode.objects.filter(mobile=self.initial_data["username"]).order_by("-add_time")
        if verify_records:
            last_record = verify_records[0]  # 时间倒叙排序后的的第一条就是最新的一条
            # 当前时间回退5分钟
            five_mintes_ago = datetime.now() - timedelta(hours=0, minutes=5, seconds=0)
            # 最后一条短信记录的发出时间小于5分钟前, 表示是5分钟前发送的, 表示过期
            if five_mintes_ago > last_record.add_time:
                raise serializers.ValidationError("验证码过期")
            # 根据记录的 验证码 比对判断
            if last_record.code != code:
                raise serializers.ValidationError("验证码错误")
            # return code  # 没必要保存验证码记录, 仅仅是用作验证
        else:
            raise serializers.ValidationError("验证码错误")

    # 对所有的字段进行限制
    def validate(self, attrs):
        attrs["mobile"] = attrs["username"]  # 重命名一下
        del attrs["code"]  # 删除无用字段
        return attrs

    class Meta:
        model = User
        fields = ("username", "code", "mobile", "password")

注册视图

class UserViewset(CreateModelMixin, mixins.UpdateModelMixin, mixins.RetrieveModelMixin, viewsets.GenericViewSet):
    serializer_class = UserRegSerializer
    queryset = User.objects.all()

    # 重写 create 函数来完成注册后自动登录功能
    def create(self, request, *args, **kwargs):
        serializer = self.get_serializer(data=request.data)
        serializer.is_valid(raise_exception=True)
        user = self.perform_create(serializer)
        
        re_dict = serializer.data
        payload = jwt_payload_handler(user)
        # token 的添加只能用此方法, 此方法通过源码阅读查找到位置为
        re_dict["token"] = jwt_encode_handler(payload)
        # 自定义一个字段加入进去
        re_dict["name"] = user.name if user.name else user.username

        headers = self.get_success_headers(serializer.data)
        return Response(re_dict, status=status.HTTP_201_CREATED, headers=headers)

    def get_object(self):
        return self.request.user

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

信号量处理工具文件

注册后的信息回传给数据库保存的时候 密码是按照是未加密状态保存

此处需要进行加密后才可以, 因此这里可以用信号量来处理, post_save 触发

在此触发流程中完成加密后保存数据库

# _*_ coding:utf-8 _*_
__author__ = "yangtuo"
__date__ = "2019/4/15 20:25"

from django.db.models.signals import post_save
from django.dispatch import receiver
from rest_framework.authtoken.models import Token
from django.contrib.auth import get_user_model

User = get_user_model()


@receiver(post_save, sender=User)  # post_save 信号类型, sender 能触发信号的模型
def create_user(sender, instance=None, created=False, **kwargs):    # created 是否新建( update 就不会被识别 )
    # instance 表示保存对象, 在这里是被保存的 user 对象
    if created:
        password = instance.password
        instance.set_password(password)
        instance.save()
        # Token.objects.create(user=instance)
        # user 对象的保存一般是要伴随着 token 的, 这里已经使用 JWT 方式了, 因此就不需要这种 token 了.

注册后自动登录逻辑

目标预期

用户注册后自动跳转到主页

同时要实现注册用户已登录状态

需求分析

用户注册相关的操作本质是从前端拿到数据传送到后端通过 相关的 view 进行操作

本质是 底层的 create 方法, 默认的方法只能实现用户创建无法实现其他附加

( DRF 的视图 功能嵌套 层次详情点击 这里查看  )

因此我们需要重写 create 方法 

定位重写 create 方法

可见只有序列化类的更新和推送, 无其他功能

默认的 create 方法

如果想实现自动登录, 首先本质就是加入用户登录的状态, 即 token 的生成和保存

本次项目使用的是 JWT 作为 token 方案, 因此 需要考究在 JWT 的源码中 token 如何生成

定位 token 生成源码查阅

JWT 的源码入口 ( URL 对接视图 )

往上找到视图类

这里是做了一层很简单的封装, 以及可以看到熟悉的 as_view()

不过我们目前不关心这个, 这里同样基于 DRF 视图中类似

视图类中找到序列化处理

 这个 serializer_class 就是对应着序列化类的处理

 

序列化处理中对 token 的处理

其实我们已经知道了JWT 的方式是不会基于数据库的, 因此他们的序列化类中的是没有任何的字段

通过各种方法来实现字段的计算和生成

以下是全部的 相关逻辑

class JSONWebTokenSerializer(Serializer):
    """
    Serializer class used to validate a username and password.

    'username' is identified by the custom UserModel.USERNAME_FIELD.

    Returns a JSON Web Token that can be used to authenticate later calls.
    """
    def __init__(self, *args, **kwargs):
        """
        Dynamically add the USERNAME_FIELD to self.fields.
        """
        super(JSONWebTokenSerializer, self).__init__(*args, **kwargs)

        self.fields[self.username_field] = serializers.CharField()
        self.fields['password'] = PasswordField(write_only=True)

    @property
    def username_field(self):
        return get_username_field()

    def validate(self, attrs):
        credentials = {
            self.username_field: attrs.get(self.username_field),
            'password': attrs.get('password')
        }

        if all(credentials.values()):
            user = authenticate(**credentials)

            if user:
                if not user.is_active:
                    msg = _('User account is disabled.')
                    raise serializers.ValidationError(msg)

                payload = jwt_payload_handler(user)

                return {
                    'token': jwt_encode_handler(payload),
                    'user': user
                }
            else:
                msg = _('Unable to log in with provided credentials.')
                raise serializers.ValidationError(msg)
        else:
            msg = _('Must include "{username_field}" and "password".')
            msg = msg.format(username_field=self.username_field)
            raise serializers.ValidationError(msg)

定位到 token 的生成代码

可见 需要使用到 jwt_payload_handler 方法以及 jwt_encode_handler 方法

因此生成 token 就是在这里了, 为了生成 token 我们需要用到这两个方法, 使用方法就完全模仿源码即可

完成 create 重写

from rest_framework_jwt.serializers import jwt_payload_handler, jwt_encode_handler

class UserViewset(CreateModelMixin, mixins.UpdateModelMixin, mixins.RetrieveModelMixin, viewsets.GenericViewSet):
    serializer_class = UserRegSerializer
    queryset = User.objects.all()

    # 重写 create 函数来完成注册后自动登录功能
    def create(self, request, *args, **kwargs):
        serializer = self.get_serializer(data=request.data)
        serializer.is_valid(raise_exception=True)
        user = self.perform_create(serializer)
   
       # 此处为自定义的 token 的生成 
        re_dict = serializer.data
        payload = jwt_payload_handler(user)
        re_dict["token"] = jwt_encode_handler(payload)
        # 顺便把 用户名一并传过去
        re_dict["name"] = user.name if user.name else user.username

        headers = self.get_success_headers(serializer.data)
        return Response(re_dict, status=status.HTTP_201_CREATED, headers=headers)

    def get_object(self):
        return self.request.user

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

用户退出

不需要再写一个 logout 接口 

JWT 不需要服务器这边进行相关的操作

只需要前端进行一个 cookie 的清空然后跳转即可

跳转到 登录页面或者主页皆可

    loginOut(){
        cookie.delCookie('token');
        cookie.delCookie('name');
        //重新触发store
        //更新store数据
        this.$store.dispatch('setInfo');
        //跳转到登录
        this.$router.push({name: 'login'})
      },

用户个人中心 

retrieve 方式添加

用户中心的数据来源是对单一用户的详细数据请求, 因此需要在原有基础上加上对  retrieve 的处理

 mixins.RetrieveModelMixin

用户 id 传递

同时因为对单一用户的请求需要指明用户id, 有两种方式可以传递

第一种 直接在数据里面提供当前用户 id

第二种 重写 get_object 获取当前用户

# 因为要涉及到 个人中心的操作需要传递过去 用户的 id, 重写 get_object 来实现
    def get_object(self):
        return self.request.user

权限分离

用户中心必须指定当前用户只能访问自己, 因此需要对是否登录进行验证

但是当前视图的其他类型请求比如 create 的注册则不需要进行验证, 因此  permission_classes 无法满足需求

源码剖析

在继承了  ViewSetMixin 之后内部的 initialize_request 方面里面的 提供了 .action 在 request 中可以对请求类型进行分离

同时 APIView 内部的  get_permissions  方法负责提取认证类型, 因此重写此方法即可完成

 

此为 源码, 可见是直接使用一个列表表达式来获取当前视图的 permission_classes 里面的所有认证方式

实现重写

基于我们自己的需求进行重写, 利用 action 进行分流

注意其他未设置的最后一定要返回空

    # permission_classes = (permissions.IsAuthenticated, )  # 因为根据类型的不同权限的认证也不同, 不能再统一设置了
    def get_permissions(self):
        if self.action == "retrieve":
            return [permissions.IsAuthenticated()]
        elif self.action == "create":
            return []
        return []

序列化组件分离

创建组件

之前设置的序列化组件是为了注册用的, 只采集了注册相关的字段, 无法满足用户中心的其他字段处理

因此需要重新设置一个用户详情的 序列化组件

# 用户详情信息序列化类
class UserDetailSerializer(serializers.ModelSerializer):
    class Meta:
        model = User
        fields = ("name", "gender", "birthday", "email", "mobile")

源码剖析

同样是基于对 action 的方法进行分流, 对于 action 的位置在 权限分流的部分有图,

在   GenericAPIView 中存在 get_serializer_class  方法, 用于获取当前视图中的 序列化组件

实现重写

基于 action 进行分流, 然后进行对 get_serializer_class 进行重写

实现方式类似于 权限的分流

   def get_serializer_class(self):
        if self.action == "retrieve":
            return UserDetailSerializer
        elif self.action == "create":
            return UserRegSerializer
        return UserDetailSerializer

完整代码

用户视图代码

# 用户视图
class UserViewset(mixins.CreateModelMixin, mixins.UpdateModelMixin, mixins.RetrieveModelMixin, viewsets.GenericViewSet):
    serializer_class = UserRegSerializer
    queryset = User.objects.all()

    authentication_classes = (JSONWebTokenAuthentication, authentication.SessionAuthentication)

    # 用户中心的个人详情数据不能再基于统一设置的 UserRegSerializer 了
    # 用户注册和 用户详情分为了两个序列化组件
    # self.action 必须要继承了 ViewSetMixin 才有此功能
    # get_serializer_class 的源码位置在 GenericAPIView 中
    def get_serializer_class(self):
        if self.action == "retrieve":
            return UserDetailSerializer
        elif self.action == "create":
            return UserRegSerializer
        return UserDetailSerializer

    # permission_classes = (permissions.IsAuthenticated, )  # 因为根据类型的不同权限的认证也不同, 不能再统一设置了
    # get_permissions 的源码在 APIview 中
    def get_permissions(self):
        if self.action == "retrieve":
            return [permissions.IsAuthenticated()]
        elif self.action == "create":
            return []
        return []

    # 重写 create 函数来完成注册后自动登录功能
    def create(self, request, *args, **kwargs):
        serializer = self.get_serializer(data=request.data)
        serializer.is_valid(raise_exception=True)
        user = self.perform_create(serializer)

        """
        此处重写的源码分析以及 相关的逻辑
        详情点击此博客 
        https://www.cnblogs.com/shijieli/p/10726194.html
        """
        re_dict = serializer.data
        payload = jwt_payload_handler(user)
        # token 的添加只能用此方法, 此方法通过源码阅读查找到位置为
        re_dict["token"] = jwt_encode_handler(payload)
        # 自定义一个字段加入进去
        re_dict["name"] = user.name if user.name else user.username

        headers = self.get_success_headers(serializer.data)
        return Response(re_dict, status=status.HTTP_201_CREATED, headers=headers)

    # 因为要涉及到 个人中心的操作需要传递过去 用户的 id, 重写 get_object 来实现
    def get_object(self):
        return self.request.user

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

用户相关序列化组件

# _*_ coding:utf-8 _*_
__author__ = "yangtuo"
__date__ = "2019/4/15 20:25"

import re
from rest_framework import serializers
from django.contrib.auth import get_user_model
from datetime import datetime
from datetime import timedelta
from rest_framework.validators import UniqueValidator

from .models import VerifyCode
from YtShop.settings import REGEX_MOBILE

User = get_user_model()


# 手机验证序列化组件
# 不使用 ModelSerializer, 并不需要所有的字段, 会有麻烦
class SmsSerializer(serializers.Serializer):
    mobile = serializers.CharField(max_length=11)

    # 验证手机号码
    # validate_ + 字段名 的格式命名
    def validate_mobile(self, mobile):

        # 手机是否注册
        if User.objects.filter(mobile=mobile).count():
            raise serializers.ValidationError("用户已经存在")

        # 验证手机号码是否合法
        if not re.match(REGEX_MOBILE, mobile):
            raise serializers.ValidationError("手机号码非法")

        # 验证码发送频率
        # 当前时间减去一分钟( 倒退一分钟 ), 然后发送时间要大于这个时间, 表示还在一分钟内
        one_mintes_ago = datetime.now() - timedelta(hours=0, minutes=1, seconds=0)
        if VerifyCode.objects.filter(add_time__gt=one_mintes_ago, mobile=mobile).count():
            raise serializers.ValidationError("距离上一次发送未超过60s")
        return mobile


# 用户详情信息序列化类
class UserDetailSerializer(serializers.ModelSerializer):
    class Meta:
        model = User
        fields = ("name", "gender", "birthday", "email", "mobile")


# 用户注册
class UserRegSerializer(serializers.ModelSerializer):
    """
    max_length      最大长度
    min_length      最小长度
    label           显示名字
    help_text       帮助提示信息
    error_messages  错误类型映射提示
        blank         空字段提示
        required      必填字段提示
        max_length    超长度提示
        min_length    过短提示
    write_only      只读, 序列化的时候忽略字段, 不再返回给前端页面, 用于去除关键信息(密码等)或者某些不必要字段(验证码)
    style           更改输入标签显示类型
    validators      可以指明一些默认的约束类
        UniqueValidator             约束唯一
        UniqueTogetherValidator     联合约束唯一
        UniqueForMonthValidator
        UniqueForDateValidator
        UniqueForYearValidator
        ....
    """
    code = serializers.CharField(required=True, write_only=True, max_length=4, min_length=4, label="验证码",
                                 error_messages={
                                     "blank": "请输入验证码",
                                     "required": "请输入验证码",
                                     "max_length": "验证码格式错误",
                                     "min_length": "验证码格式错误"
                                 },
                                 help_text="验证码")

    # validators 可以指明一些默认的约束类, 此处的 UniqueValidator 表示唯一约束限制不能重名
    username = serializers.CharField(label="用户名", help_text="用户名", required=True, allow_blank=False,
                                     validators=[UniqueValidator(queryset=User.objects.all(), message="用户已经存在")])

    # style 可以设置为密文状态
    password = serializers.CharField(
        style={'input_type': 'password'}, help_text="密码", label="密码", write_only=True,
    )

    # 用户表中的 password 是需要加密后再保存的, 次数需要重写一次 create 方法
    # 当然也可以不这样做, 这里的操作利用 django 的信号来处理, 详情见 signals.py
    # def create(self, validated_data):
    #     user = super(UserRegSerializer, self).create(validated_data=validated_data)
    #     user.set_password(validated_data["password"])
    #     user.save()
    #     return user

    # 对验证码的验证处理
    # validate_ + 字段对个别字段进行单一处理
    def validate_code(self, code):

        # 如果使用 get 方式需要处理两个异常, 分别是查找到多个信息的情况以及查询到0信息的情况的异常
        # 但是使用 filter 方式查到多个就以列表方式返回, 如果查询不到数据就会返回空值, 各方面都很方便
        # try:
        #     verify_records = VerifyCode.objects.get(mobile=self.initial_data["username"], code=code)
        # except VerifyCode.DoesNotExist as e:
        #     pass
        # except VerifyCode.MultipleObjectsReturned as e:
        #     pass

        # 前端传过来的所有的数据都在, initial_data 字典里面 ,
        verify_records = VerifyCode.objects.filter(mobile=self.initial_data["username"]).order_by("-add_time")
        if verify_records:
            last_record = verify_records[0]  # 时间倒叙排序后的的第一条就是最新的一条
            # 当前时间回退5分钟
            five_mintes_ago = datetime.now() - timedelta(hours=0, minutes=5, seconds=0)
            # 最后一条短信记录的发出时间小于5分钟前, 表示是5分钟前发送的, 表示过期
            if five_mintes_ago > last_record.add_time:
                raise serializers.ValidationError("验证码过期")
            # 根据记录的 验证码 比对判断
            if last_record.code != code:
                raise serializers.ValidationError("验证码错误")
            # return code  # 没必要保存验证码记录, 仅仅是用作验证
        else:
            raise serializers.ValidationError("验证码错误")

    # 对所有的字段进行限制
    def validate(self, attrs):
        attrs["mobile"] = attrs["username"]  # 重命名一下
        del attrs["code"]  # 删除无用字段
        return attrs

    class Meta:
        model = User
        fields = ("username", "code", "mobile", "password")

 

转载于:https://www.cnblogs.com/shijieli/p/10726194.html

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值