Python+Django+Drf+drf-yasg 学习笔记

Python+Django+Drf+drf-yasg 学习笔记


References:

Django drf 教程

Django 项目启动:

# 创建环境 
python -m venv venv 

# 安装依赖
pip install -r requirement.txt

# 检测模型文件修改(初次启动的时候可以直接 migrate)
python manage.py makemigrations app_name

# 创建新定义的模型的数据表
python manage.py migrate

# 收集静态文件至 STATIC_ROOT 中
python manage.py collectstatic --noinput

# 启动服务
python manage.py runserver

# 进入命令行
python manage.py shell

从 0 开始构建一个 Django 项目

# 创建环境 
python -m venv venv 
# activate env
source venv/bin/activate

pip install django
python -m django --version

django-admin startproject mysite
# 此时已经能够启动一个项目了
python manage.py runserver

# 修改 settings 文件,
# 配置数据库,redis, 
# 配置静态文件并生成静态文件(主要是 admin 使用)
python manage.py collectstatic --noinput
# 编写应用,并在 settings 中添加到 INSTALLED_APPS
python manage.py makemigrations
python manage.py migrate
# 如果不想本次生成 migrate 文件,可以删除文件并在数据库中 django_migration 表中删除相关记录以及对应表

# 配置限流策略
# 配置日志输出路径
# 配置 swagger 等(如有需要)
# 编写业务代码
# 编写 entrypoint.sh 启动脚本,编写 .dockerfile 

模型层(models)

作用: 构建和操纵 web 应用的数据

特点:由于 Django 的 orm 即对象关系映射模式,每一个模型映射一张数据库表

  • 每个模型都是一个 Python 的类,这些类继承 django.db.models.Model

  • 模型类的每个属性都相当于一个数据库的字段

  • Q: model_name.objects.get()model_name.objects.filter() 有什么区别

    • A: get 可以理解为精准匹配,通常返回值是一个定义的model对象,只有一条记录返回的时候才正常,也就说明get的查询字段必须是主键或者唯一约束的字段。当返回多条记录或者是没有找到记录的时候都会抛出异常。
    • filter返回 QuerySet 对象列表,有没有匹配的记录都可以。

migrate

Django之 migration 原理

若对于新的未上线使用的数据库模型更改,可以将测试环境数据库中 django_migration 中对应 app 的记录删除,然后生成 migration 文件之后 migrate 方可不报错。

字段关联关系

通常我们建的各个表都有关联关系,比如商品表 Store 有外键 books,其为 Book 表的主键

Book 表有外键 authors,其为 Author 表的主键

django joins and aggregates 告诉我们如果想在 Store 表中知道每一本书籍的最高最低价格可以这么使用:

from django.db.models import Max, Min
# 每一本书都知道最高价和最低价
q = Store.objects.annotate(min_price=Min('books__price'), max_price=Max('books__price'))
>>> q[0].min_price

如果想知道每本书的价格可以这么使用:

F() 可以引用数据库模型字段值执行数据库操作,而不需要把其从数据库中拉出到 Python 内存中

from django.db.models import F
q = Store.objects.annotate(price=F('books__price'))
>>> q[0].price
annotate 和 aggretate 的区别

aggregate

annotate

  1. aggregate() 为所有的 QuerySet 生成一个汇总值,相当于 Count()。返回结果类型为 Dict。

  2. annotate() 为每一个 QuerySet 在指定属性上生成汇总值,相当于 GROUP BY。返回结果类型 QuerySet。

django 字段类型

django 字段对应 mysql 中的数据类型参考:Django中的常用字段类型

drf serializers

documentation

serializers 的适用对象

serializers 主要是对 django QuerySet 对象, Model 实例的操作(当然并不限于,它可以将 queryset 或者数据模型实例等转化成原生的 python 数据类型,进而能够渲染进如 json、xml 或其他内容类型),包括序列化(将 Python 数据类型转换为 json)和反序列化

如何使用 serializers:

  • 通常我们可能会在一个 app 的 serializers 文件中里面创建多个不同的 serializer 来辅助我们的请求,比如 AAACreateSerializer AAAListSerializer

  • 通常我们用来做请求参数的序列化、校验或返回结果的序列化。基于此,可以在 swagger_auto_schema 中注明 request_body 以及 response(两者使用的序列化类可以不一样)

serializer 的分类

  1. 直接继承 serializers.Serializer
    通常用作不涉及数据模型的 get post put delete 请求的请求参数或返回内容序列化,常用的 validation 可以参考
  • validation 相关内容

通常我们在视图中对数据反序列化处理的时候会使用 serializer.is_valid(raise_exception=True),这里就是进行验证。validation 在 serializer 中分为两类:

1. 字段级别的 validation
通过在 Serializer 类下写 `validate_<field_name>` 方法
2. 对象级别的 validation(对多个字段进行验证)
通过在 Serializer 类下写 `validate` 方法
  • 关于嵌套字典的序列化

可以参考 Django Rest Framework ListField and DictField
结论是不要嵌套 serializers.DictField 而是再定义一个 serializer 类做嵌套。

  • 其他

如果是做数组的序列化,可以这样标识

class TestSerializer(serializers.Serializer):
    a = Serializers.CharField(label='a')
    b = Serializers.CharField(label='a')

class ResSerializer(serializers.Serializer):
    res = TestSerializer(many=True)
# 最终使用 ResSerializer 序列化出的结果就是 [{a:'1', b:'2'}, {a:'2', b:'3'}]
  1. 继承于 serializers.ModelSerializer
  • ModelSerializer 的父类是Serializer,是 在Serializer的基础上的扩展
  • 可以直接通过 Class Meta 里配置 fields 来对 model 进行序列化,简化了 Serializer 的操作
  • Meta里可以配置如下配置:
    • model:指定需要序列化的 model
    • depth:序列化时,读取数据的深度,默认是0,0表示仅展示该model对象的值,1表示为展示与该model对象直接外键关联的对象的相关数据,以此类推
    • fields:需要序列化的字段,是一个元组,__all__ 表示所有字段
    • exclude:不需要序列化的字段,默认是在全字段的情况下剔除不需要序列化的字段,
      • 注意:fields和exclude不能同时使用
    • extra_kwargs:给fields中的字段,添加额外的属性,如只读(read_only)、只写(write_only)等
    • read_only_fields:只读字段
    • validators:可以直接在 Meta 中定义要使用到的 validator

备注:

  1. 起别名 & 外键:在 Meta 外显示指定序列化字段时,可以通过source='xxx’来指定需要关联的外键对象的值,通过点分式获取对应的字段,当然这也是一种给字段起别名的方式
    owner = serializers.ReadOnlyField(source='owner.username')
    class Meta:
     ...
  1. 在 Meta 外显示指定序列化字段时,除了 1 中的方式还可以使用 SerializerMethodField 通过在类上添加一个方法获取其值,添加任意类型的数据到对象的序列化表示上。

同时结合 drf-yasg @swagger_serializer_method 可以用这个装饰器来装饰 serializers.SerializerMethodField 来提示 swagger 如何描述这个字段

from django.contrib.auth.models import User
from django.utils.timezone import now
from rest_framework import serializers

class UserSerializer(serializers.ModelSerializer):
    days_since_joined = serializers.SerializerMethodField()

    class Meta:
        model = User
    @swagger_serializer_method(serializer_or_field=serializers.IntegerField)
    def get_days_since_joined(self, obj):
        return (now() - obj.date_joined).days
  1. 一般我们会在 serializer 中重写 create 或 update 等方法(如果你有这个需求需要添加你的逻辑,同时添加 Model.objects.update_or_create),然后在 views 中直接执行 serializer.save() 即可,因为请求到来后会先执行 views 中 create 方法,然后执行 serializer.save() 的时候执行 serializer 中的 create 方法
  2. 不是任何一个接口都需要 serializer,一些直接返回列表的接口则可以直接 Response(data),或直接 Response(Iterator)passing iterator,因为 Django 原生的 HttpResponse 会立即将 Iterator 消费掉
  3. django-rest-framework: Cannot call .is_valid() as no data= keyword argument was passed when instantiating the serializer instance 当你想通过 serializer 序列化数据时可以直接将数据作为第一个参数传入,得到 serializer.data;当你想反序列化数据时需要通过关键字参数 data 传入,此时如果想使用 serializer.data必须先调用 serializer.is_valid()
# serialize 常用于序列化 queryset
serializer = CommentSerializer(comment)
serializer.data
# {'email': u'leila@example.com', 'content': u'foo bar', 'created': datetime.datetime(2012, 8, 22, 16, 20, 9, 822774)}


# deserializer 常用于验证要存入数据库的数据,并存储
serializer = CommentSerializer(data=data)
serializer.is_valid()
# True
serializer.validated_data
# {'content': 'foo bar', 'email': 'leila@example.com', 'created': datetime.datetime(2012, 08, 22, 16, 20, 09, 822243)}

视图层(views)- 相当于 controller 层,负责处理用户的请求并返回响应

视图有基于函数的视图,有基于类的视图,一般使用 drf 之后,我们会继承 ModelViewSet 来创建基于类的视图

APIView

APIView 类是 drf 提供的 Django View 类的子类,和 View 类一样,进来的请求会被 dispatch 到一个准确的 handler 方法比如 .get() .post()

但他也有以下几个跟传统的 View 类不同的点:

  1. 传递给 handler 方法的请求是 drf 的 Request 实例,而不是 Django 的 HttpRequest 实例
  2. handler 方法返回的是 drf 的 Response,而非 Django 的 HttpResponse
  3. 任何 APIException 异常都将被捕获并调停到适当的响应中。
  4. 传入的请求将经过身份验证,并在将请求 dispatch 给处理程序方法之前运行适当的权限与/或限制检查。

ViewSets

这个类有以下两个特殊点:

  1. ViewSets 类是一类基于类的视图,它不提供各种像 .get .post 这样的方法 handler。而是提供想 .list .create() 这样的 actions

  2. 创建了继承 ViewSets 类的视图之后,不再需要显式的在 urlconf 中注册视图,而是通过 router 类注册视图,它会为你自动配置好 urlconf

router = DefaultRouter()
router.register('account', views.AccountViewSet)
app_name = 'account'

urlpatterns = [
    path('', include(router.urls))
]
  • ModelViewSet 继承自各个 mixin 类和 GenericViewSet,所以默认就提供了增删查改相关方法,具体在代码中点击进入看就会发现

django-filter 过滤

实现过滤能力,比如通过在 url 中传递参数进行过滤。可以使用默认的过滤后端,也可以按自己的需求实现过滤后端

代码实现参考:demo

路由

关于 URL:

  • Django 规定每一个模式要求 URL 以一个斜线结尾
  • urlpatterns 都可以 include 其他 URLconf 模块,即将一部分 URL 放置于其他 URL 下面
  • URL 模式可以使用正则表达式,使用 re_path() 而不是 path()

而对于 drf:常用路由是 DefaultRouter

  • drf 添加了对自动 URL 路由到 Django 的支持,提供了一种简单、快速和一致的方式将试图逻辑连接到一组 URL
  • 常用的是 DefaultRouter,默认会不全常见的 CRUD 接口
  • 自动基于 viewSet 生成 xxx-list 以及 xxx-detail,自定义的接口,默认将 “_” 替换成 “-”

Admin

这是 Django 最强大的部分之一,它从你的模型中读取元数据,提供一个快速的、以模型为中心的界面,受信任的用户可以管理你网站的内容(或者说元数据)。

  1. step1: 从 根 URLConf 中创建 admin 路由
  2. step2: python manage.py createsuperuser 创建管理员账号
  3. step3: 继承 ModelAdmin 用来处理自定义界面
  4. 其他处理主题的方式可以参考 https://docs.djangoproject.com/zh-hans/3.2/ref/contrib/admin/#hooking-adminsite-to-urlconf

管理静态文件

  1. 配置静态文件,定义 STATIC_URL STATIC_ROOT
  2. 配置 whiternoise 提供静态文件服务(可以不依赖像 nginx 这样的 external service)
  3. python manage.py collectstatic

gunicorn

WSGI是个啥?

WSGI到底是什么?

wsgi(Web Server Gateway Interface or Python Web Server Gateway Interface)

定义

python web 服务器网关

作用

它是为 Python 语言定义的 Web 服务器和 Web 应用程序或框架之间的一种简单而通用的接口。(貌似 js 没有)

Gunicorn 是一个 unix 上被广泛使用的高性能的 Python WSGI UNIX HTTP Server。

# 启动命令
gunicorn [OPTIONS] [WSGI_APP]

gunicorn --config etc/gunicorn.py cloud_resource.wsgi:application

Django log

日志管理快速入门

将所有日志写入一个文件并保存在服务器上:

通过调用 logging.getLogger() 时传入名字,同时结合 Handler 将日志输出到指定的以这个名称命名的文件。

(其中:Handler 是决定如何处理 logger 中每一条消息的引擎。它描述特定的日志行为,比如把消息输出到屏幕、文件或网络 socket。)

Django uuid 采坑

注意通常我们在 Django 代码中设置数据的 id 为 uuid 格式,通过 rest 接口返回给前端的时候即为 uuid string 的格式,落库的时候是普通没有横杠的字符串,读取时 Django 应该做了一次转换。

然而我们从数据库中直接 copy 出的 id 是没有短横杠的,将此传入参数中查询数据是查询不到的。

python 语法

# python 能表示的最大数
# 最大整数
sys.maxsize
# 最大浮点数
float('inf')
# 最小整数
-sys.maxsize-1
# 最小浮点数
float('-inf')

python 基本数据类型

numbers(数字),string(字符串),tuple(元组),list(列表),dict(字典), set(集合)

可变数据类型:存的是内存中的地址,修改时改值不改址:list(列表),dict(字典), set(集合)

不可变数据类型:存的是值,修改时改值也改址:numbers(数字),string(字符串),tuple(元组)

关于 tuple:

元组:一种有序列表,但和 list 不同的是一经定义无法改变,且定义时必须含有初值,因此安全性高。

*args **kwargs 区别

*args 无名参数,是一个 tuple

**kwargs 关键字参数,是一个 dict

列表相关

对于 iterable 类型的数据,可以多关注 itertools(为高效循环而创建迭代器的函数) 中的方法

注:凡是可作用于for循环的对象都是Iterable类型;

凡是可作用于next()函数的对象都是Iterator类型,它们表示一个惰性计算的序列;

集合数据类型如listdictstr等是Iterable但不是Iterator,不过可以通过iter()函数获得一个Iterator对象。参考:迭代器

# 遍历列表的索引和值
for idx, val in enumerate(list):
# 利用这种方式可以得到键为数组值,值为索引的字典
nums_dict = {val:idx for idx,val in enumerate(list)}

# 使用 groupby 对对象数组进行分组真的很方便
# 结合 itemgetter 使用更灵活,
from operator import itemgetter
import itertools
dict_list = [{'name': 'a', 'value': 1}, {'name': 'a', 'value': 2}, {'name': 'b', 'value': 1}, {'name': 'c', 'value': 2}]
after_grouped = [{"name":key, "data":list(group)} for key,group in itertools.groupby(a, key=itemgetter('name'))]
>>> [{'name': 'a', 'data': [{'name': 'a', 'value': 1}, {'name': 'a', 'value': 2}]}, {'name': 'b', 'data': [{'name': 'b', 'value': 1}]}, {'name': 'c', 'data': [{'name': 'c', 'value': 2}]}]
排序

python3 有两种排序方式,list.sort() 和 sorted() 函数,区别如下

  1. list.sort() 会直接修改原列表,sorted() 函数 会返回一个新的排序后列表
  2. list.sort() 只能应用于列表,sorted() 函数可以接受任何可迭代对象,因此推荐使用 sorted() 来解决问题
  3. 使用方式举例
# list sort 方法会改变原数组
list.sort(key=None, reverse=False)
# 使用 key 自定义排序
arr = [[1,2], [0,1], [7,2]]
arr.sort(key=lambda x:x[1])
# 自定义方法排序,使用比较函数,负值表示小于,零表示相等,正值表示大于
def my_sort(x, y):
    if x[0] == y[0]:
        return x[1] - y[1]
    else:
        return x[0] - y[0]
>>> [[0, 1], [1, 2], [7, 2]]
# sorted 应用于所有可迭代对象上,且不会对原对象进行修改,会返回一个新的对象
sorted(iterable, key=None, reverse=False)  

a = [[1, 0], [1, 1], [0, 9]]
a.sort(key=functools.cmp_to_key(my_sort))
test = sorted(a, key=functools.cmp_to_key(my_sort))
# 会是同样的结果
test = sorted(a, key=itemgetter(0, 1))
print(a) # [[0, 9], [1, 0], [1, 1]]
列表(数组)初始化

带初始值或定长的数组初始化:

# 一维数组
list0 = [0]*10

# 二维数组,注意一定不用一维数组的方式
list00 = [list() for i in range(10)]
# 已知 dict 的键不能是可变数据如 list dict 等,但为了能正常使用哈希表,可以用 id(obj) 返回 identity
id(list0)
>>> 450572xxxx
# 列表中某个元素的位置
aa = [1,2]
aa.index(2) # 1

# 批量替换列表中的内容
nums[l:r+1] = another_list
nums = [1,2,3]
nums[0:2] = [4,5] # nums 变成 [4,5,3]

不定长数组初始化:

list0 = []
# 声明了数组之后逐渐追加元素
list0.append(1)

求二维数组的最大值:

max_val = max(max(item) for item in res)
判断相关的语法
# 判断一个字符串是否为数字
str = "123"
str.isdigit() # True 
复制列表 or 字典
import copy

a = [1,2,3]
# b 即为和 a 中元素完全相同的列表,但 b 指向的地址和 a 不同
b = a[:]
b = a * 1
b = list(a)
# 但是上述方法只进行了一层拷贝,要想达到深拷贝
b = copy.deepcopy(a)

c = {"a":"b"}
d = copy.copy(c)

字符串相关

# 拼接字符串
str1 = "test0"
str2 = "--"
str = str1 + str2

# 重复字符串
str1 = "test"
str = str1 *2 # "testtest"

# 字符串转数组
str = "hello"
str_list = list(str)

# 数组转字符串(这个需要 str_list 里的元素也是字符串)
str = ''.join(str_list)
str = ''.join(list(map(str, int_list)))

# 统计某个字符出现的次数
("test").count('t')
>>> 2

# split You can specify the separator, default separator is any whitespace.
s = "a good   example"
s.split()
>>> ['a', 'good', 'example']

字典相关

# 判断字段中是否有某个 key
d = {'name':'Tom', 'age':10, 'Tel':110}
print('name' in d)

# 遍历字典
for key, val in dict.items()

# 移除字典键值对
del d['name']

随机数

import random
random.randint(1, 10) # [1,10]之间任意一个数,注意右边为闭区间
randow.randrange(1,10) # [1,10) 之间任意一个数,注意右边为开区间

数学运算

# 求绝对值
abs(val)
# 求二进制字符串
bin(4)
>>> '0b100'
# 统计字符串中某个值出现的次数
bin(4).count('1')
>>> 1

# python 表示二进制数的方式
0b10 == 2
>>> True

# 累积奇偶数用异或运算 ^
1^1
>>> 0
0^0
>>> 0
1^0
>>> 1
# ord() 返回能代表 unicode 字符的数字
ord('a')
>>> 97

python 装饰器 decorator

在代码运行期间动态添加附加功能的东西就是装饰器。很多语言中都有装饰器,比如 javascript 在 ts 中即可以使用 @decorator, python 也不例外

django 项目中自定义日志打印

类相关

python 中所有类默认继承 object 类。而 object 类提供了了很多原始的内建属性和方法,所以用户自定义的类在 Python 中也会继承这些内建属性, 比如 __init__ __str__

__str__:将实例对象按照自定义的格式用字符串的形式显示出来,提高可读性,通常调用 print(x) 的时候就会调用这个方法。

python operator

The operator module in Python (itemgetter, attrgetter, methodcaller)

python 提供内置的 operators 去获取 items, attributes, 调用 methods. 通常这些 operator 操作适用于 key 的操作中,比如 sorted/min/max 等

python 异常处理

我们知道当程序抛出异常的时候会导致 python interpreter 退出,有些场景比如 http 请求检测到 4xx 错误时,我们同样想让程序终止,该怎么做呢?

  1. raise Execption,注意可以使用 specific exception class to avoid unexpectation errors

  2. 使用 sys.exit(1),它其实也是抛出一个异常,但是继承于 BaseException

References:

Stop Using Exceptions Like This in Python

Difference between calling sys.exit() and throwing exception

Python SystemExit

requests

class requests.Response

resp.text 是 Content of the response, in unicode.

resp.content 是 Content of the response, in bytes.

如果想使用 json,直接 resp.json() 即可

**如何获得 url name ? **

通常写单测的时候会用到 reverse(url_name) 来获取 url 的方式,而往往我们知道 url 而不知道 url_name,此时可以通过在 views 里写如下方式来获取 url_name

        from django.urls import resolve
        print(resolve(request.path).view_name)

python 常见的配置文件格式

超全!Python 中常见的配置文件写法

文件类型复杂性分别为:ini < json ~ toml < yaml

为什么要使用配置文件

跨语言进行配置文件共享时,就需要选择使用通用的配置文件类型来存储配置内容,常见的配置文件格式类型由 ini/json/toml/yaml/xml

  1. ini 文件:默认值以字符串的形式呈现
from configparser import ConfigParser
cfg = ConfigParser()
cfg.read("/Users/Bobot/db.ini")
db_cfg = dict(cfg.items("localdb"))
con = pymysql.connect(**db_cfg)
  1. json 文件
with open('/Users/Bobot/db.json') as j:
	cfg = json.load(j)['localdb']
  1. tomal 文件:比 ini 文件扩展了时间戳、布尔值、数组等,样式和 python 原生写法十分相似,推荐使用
import toml
with open('/Users/Bobot/db.toml', 'r') as file:
	data = toml.load(file)
  1. yaml 文件:
from pyyaml import yaml
with open('/Users/Bobot/db.yaml', "r") as config:
	cfg = yaml.safe_load(config)
好的,下面我会简单介绍如何使用 DjangoDjango REST framework(DRF)来实现 CRUD 操作。CRUD 是指 Create(创建)、Read(读取)、Update(更新)和 Delete(删除),是开发 Web 应用程序时最基本的操作。 首先,你需要安装 DjangoDRF。你可以使用以下命令来安装: ``` pip install django pip install djangorestframework ``` 接下来,创建一个 Django 项目,并在项目中创建一个应用程序。你可以使用以下命令来创建: ``` django-admin startproject projectname cd projectname python manage.py startapp appname ``` 在应用程序的 `models.py` 文件中定义一个模型。例如,我们创建一个简单的 `Book` 模型: ```python from django.db import models class Book(models.Model): title = models.CharField(max_length=200) author = models.CharField(max_length=200) pub_date = models.DateField() price = models.DecimalField(max_digits=5, decimal_places=2) ``` 然后,在应用程序的 `serializers.py` 文件中创建一个序列化器。序列化器用于将模型转换为 JSON 格式,以便在 API 中返回。以下是一个简单的 `BookSerializer`: ```python from rest_framework import serializers from .models import Book class BookSerializer(serializers.ModelSerializer): class Meta: model = Book fields = ['id', 'title', 'author', 'pub_date', 'price'] ``` 接下来,我们需要创建视图。视图定义了 API 的行为。在 `views.py` 文件中,创建一个继承自 DRF 的 `ViewSet` 类的视图。以下是一个简单的 `BookViewSet`: ```python from rest_framework import viewsets from .models import Book from .serializers import BookSerializer class BookViewSet(viewsets.ModelViewSet): queryset = Book.objects.all() serializer_class = BookSerializer ``` 最后,我们需要将视图添加到路由中。在 `urls.py` 文件中,创建一个路由器,并将 `BookViewSet` 添加到路由器中。以下是一个简单的 `urls.py`: ```python from django.urls import path, include from rest_framework.routers import DefaultRouter from .views import BookViewSet router = DefaultRouter() router.register(r'books', BookViewSet) urlpatterns = [ path('', include(router.urls)), ] ``` 现在,你可以运行 Django 项目并使用 API 进行 CRUD 操作了。在命令行中运行 `python manage.py runserver` 启动 Django 服务器。然后,使用浏览器或其他 HTTP 客户端工具(例如 Postman)访问 API。 例如,要创建一个新的书籍,你可以向 `http://localhost:8000/books/` 发送一个 POST 请求,包含书籍的详细信息。要获取所有书籍的列表,你可以向 `http://localhost:8000/books/` 发送一个 GET 请求。要更新或删除特定的书籍,你可以向 `http://localhost:8000/books/<book_id>/` 发送一个 PUT 或 DELETE 请求,其中 `<book_id>` 是书籍的 ID。 这是一个非常简单的示例,但是它可以帮助你了解如何使用 DjangoDRF 来创建一个 CRUD API。
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值