day04view、cookie&session&token、静态文件、分页和aop

一、视图

Django中的视图主要用来接受Web请求,并做出响应。
视图的本质就是一个Python中的函数
视图的响应分为两大类
	1)以Json数据形式返回 (JsonResponse)
	2)以网页的形式返回
		2.1)重定向到另一个网页 (HttpResponseRedirect)
		2.2)错误视图(40X,50X)(HttpResponseNotFound,HttpResponseForbidden,				       							HttpResponseNotAllowed等)
视图响应过程: 浏览器输入 ->  django获取信息并去掉ip:端口,剩下路径 -> urls 路由匹配 - > 视图响应 -> 回馈到浏览器
视图参数:
	1)一个HttpRequest的实例,一般命名为request
	2)通过url正则表达式传递过来的参数
位置:通常在应用下的views.py中定义
	错误视图:	
		1404视图 (页面没找到)
		2400视图 (客户操作错误)
		3500视图(服务器内部错误)
	自定义错误视图
		在工程的templates文件夹下创建对应的错误文件
		在文件中定义自己的错误样式
		注意需要在关闭Debug的情况下,并设置ALLOWED_HOSTS才可以
		没有关闭Debug的情况下会在界面中直接显示log
HttpRequest
服务器在接收到Http请求后,会根据报文创建HttpRequest对象
视图中的第一个参数就是HttpRequest对象
Django框架接收到http请求之后会将http请求包装为HttpRequest对象,之后传递给视图。

request常用属性和方法:
	属性:	  path		请求的完整路径
		 	method	请求的方法,常用GET,POST	
			GET		类似字典的参数,包含了get的所有参数
			POST	类似字典的参数,包含了post所有参数
			FILES	类似字典的参数,包含了上传的文件
			COOKIES	字典,包含了所有COOKIE
			session		类似字典,表示会话
            META['REMOTE_ADDR']
			*encoding	编码方式,常用utf-8
	方法: is_ajax()		判断是否是ajax(),通常用在移动端和JS中
		  get_full_path()	 返回包含参数字符串的请求路径.

QueryDict: 类字典的对象
	类似字典的数据结构。与字典的区别:可以存在相同的键。
	QueryDict中数据获取方式
		dict['uname']dict.get('uname)
		获取指定key对应的所有值
		dict.getlist('uname')
                                 
    # request
    #   http协议:
    #       1.先客户端请求request,后服务器响应response
    #       2.无连接/短连接(http)(TCP是长连接)
    #       3.简单快速,对服务器压力小
    #       4.http的请求方式:GET,POST,...
    #       5.http的常见状态码
    #   url: http://www.baidu.com:80/goods/detail?id=101&en=123
    print(request, type(request))  # 请求对象
    print(request.method)  # 请求方式, GET
    print(request.path)  # 路径, /index/
    print(request.GET)  # 接收GET请求的参数,
                                 #<QueryDict: {'name': ['lisi'], 'like': ['movie', 'code']}>
    print(request.POST)  # 接收POST请求的参数
    print(request.COOKIES)  # cookies 字典
    print(request.session)  # session 类字典
    print(request.FILES)  # 接收上传的文件, <MultiValueDict: {}>
    print(request.META['REMOTE_ADDR'])  # 客户端IP
    print(request.GET.get('name'), request.GET['name'])
    print(request.GET.getlist('like'))
		
HttpResponse
服务器返回给客户端的数据
HttpResponse:
HttpResponse由程序员自己创建:
1)不使用模板,直接调用HttpResponse(),返回HttpResponse对象。
2)调用模板,进行渲染。
    使用render
    render(request,template_name[,context])
    参数:
        request 		请求体对象
        template_name	模板路径
        context		字典参数,用来填坑

HttpResponse属性:	
      content		返回的内容
	 charset		编码格式
	 status_code	响应状态码(200,3xx,404,5xx)
        
HttpResponse方法:
	write(xxx)			直接写出文本 
	flush()				冲刷缓冲区
	set_cookie(key,value='xxx',max_age=None)  设置cookie
	delete_cookie(key)	 删除cookie
	
HttpResponse子类
	HttpResponseRedirect
		响应重定向:可以实现服务器内部跳转
		return HttpResponseRedict('/grade/2017')
		使用的时候推荐使用反向解析
	JsonResponse
		返回Json数据的请求,通常用在异步请求上
			JsonResponse(dict)
		返回json数据时,Content-type是application/json
        
        
def response(request):
    res = HttpResponse('ok')
    print(res.charset)  # 'utf-8'  编码格式
    print(res.content)  # b'ok'   返回内容(二进制)
    print(res.status_code)  # 200  状态码
    res.write('hello')  # 直接写出文本
    res.flush()  # 冲刷缓冲区
    
	print(res.content)  # b'okhello'
    return res        

二. Cookie & Session&Token

Cookie
	理论上,一个用户的所有请求操作都应该属于同一个会话,而另一个用户的所有请求操作则应该属于另一个会话,二者不能混淆. 而Web应用程序是使用HTTP协议传输数据的。HTTP协议是无状态的协议。一旦数据交换完毕,客户端与服务器端的连接就会关闭,再次交换数据需要建立新的连接。这就意味着服务器无法从连接上跟踪会话。要跟踪该会话,必须引入一种机制。
	Cookie就是这样的一种机制。它可以弥补HTTP协议无状态的不足。在Session出现之前,基本上所有的网站都采用Cookie来跟踪会话。
	Cookie实际上是一小段的文本信息。客户端请求服务器,如果服务器需要记录该用户状态,就使用response向客户端浏览器颁发一个Cookie。客户端浏览器会把Cookie保存起来。当浏览器再请求该网站时,浏览器把请求的网址连同该Cookie一同提交给服务器。服务器检查该Cookie,以此来辨认用户状态。服务器还可以根据需要修改Cookie的内容。

    # 由于HTTP是一种无状态的协议,服务器单从网络连接上无从知道客户身份。怎么办呢?就给客户端们颁发一个通行证吧,每人一个,无论谁访问都必须携带自己通行证。这样服务器就能从通行证上确认客户身份了。这就是Cookie的工作原理。
	
    cookie本身由服务器生成,通过Response将cookie写到浏览器上,下一次访问,浏览器会根据不同的规则携带cookie过来。
	注意:cookie不能跨浏览器,一般不跨域
    
    设置cookie(使用response设置):
		response.set_cookie(key,value[,max_age=None,expires=None)]
			max_age: 整数 单位为秒,指定cookie过期时间
                      设置为None:浏览器关闭失效,默认值
			expires: 指定过期时间,还支持datetime或timedelta,可以指定一个具体日期时间
                  expires=datetime.datetime(2019, 1, 1, 2, 3, 4)
               或 datetime.datetime.now() + datetime.timedelta(days=10)
			
            注意:max_age和expries两个选一个指定
			# response.set_cookie('username', username, max_age=10)	
			# response.set_cookie("username", username1, expires=d)

	获取cookie(使用request获取):
		request.COOKIES.get('username')
	删除cookie(使用response删除):
		response.delete_cookie('username')
		
   cookie存储到客户端
   优点:
       数据存在在客户端,减轻服务器端的压力,提高网站的性能。
   缺点:
       安全性不高:在客户端机很容易被查看或破解用户会话信息

例如:
# 登录
def login(request):
    if request.method == 'GET':
        return render(request, 'login.html')

    elif request.method == 'POST':
        # 登录
        username = request.POST.get('username')
        password = request.POST.get('password')

        # 验证用户名和密码
        users = UserModel.objects.filter(username=username, password=password)
        if users.exists():
            # 验证成功,登录成功
            res = redirect(reverse('index'))

            # cookie
            #   1.cookie存储在客户端
            #   2.cookie一般不跨域,不能跨浏览器
            #   3.cookie一般是小于4KB的文本
            #   4.cookie是由服务器端设置,cookie内容一般和当前用户相关
		   #   5.cookie的作用:识别客户端身份(身份认证)                            
            # res.set_cookie('userid', users.first().id)
            #
            # cookie过期时间
            #   1.默认过期时间为浏览器会话时间(浏览器关闭时会删除cookie)
            #   2.expires: 设置一个指定的时间,datetime
            #   3.max_age: 设置从当前时间开始经过的秒数
            res.set_cookie('userid', users.first().id, max_age=30)  # 设置cookie和过期时间
            d = datetime.datetime(2030, 1, 2, 3, 4, 5)
            res.set_cookie('userid', users.first().id, expires=d)  # 设置cookie和过期时间

            # 进入到首页
            return res

        return render(request, 'login.html')     

# 注销
def logout(request):

    res = redirect(reverse('index'))
    res.delete_cookie('userid')   # 删除cookie
	return res                            
Session
服务器端会话技术,依赖于cookie.
django中启用SESSION
settings中
INSTALLED_APPS: 'django.contrib.sessions'
MIDDLEWARE:'django.contrib.sessions.middleware.SessionMiddleware'

'''
session 结合了cookie的功能与数据库的数据存储功能:


 django回调时视图时,将请求对象赋值给request参数, 在request对象中请求设置session,
 django就会在数据库表中插入设置的session,而且还会在返回浏览器的时候设置一个
 key为sessionid,value为数据库session表的session_key 的cookie
 (内部机制,只能设置一个名为sessionid的coocie)
 
 之后客户端再次请求会带着该cookies进行校验等操作
(利用cookie存储session_key,数据库存储session_key,session_data;
这样当浏览器再次请求的时候就可以利用session_key获取存储的数据判断是否同一个客户端等)
'''    
    
    
基本操作
设置Sessions值(使用request设置)
    request.session['user_id'] = user.id
    request.session.set_expiry(86400)  # 设置过期时间

获取Sessions值	
    get(key,default=None) 根据键获取会话的值
    username = request.session.get("user_id")  
    # 或 session_name = request.session["session_name"]

删除Sessions值
    # 获取当前请求的session的key
    session_key = request.session.session_key
    del request.session[session_key]
    # request.session.delete(session_key)

flush() 删除当前的会话数据并删除会话的cookie
clear() 清除所有会话

数据存储到数据库中会进行编码,使用的是Base64
每个HttpRequest对象都有一个session属性,也是一个类字典对象.


# 登录
def login(request):
    if request.method == 'GET':
        return render(request, 'login.html')

    elif request.method == 'POST':
        # 登录
        username = request.POST.get('username')
        password = request.POST.get('password')

        # 验证用户名和密码
        users = UserModel.objects.filter(username=username, password=password)
        if users.exists():
            # session
            request.session['userid'] = users.first().id  # 设置session
            request.session.set_expiry(60*60*24)  # 过期时间            
            return redirect(reverse('index')) # 进入到首页

        return render(request, 'login.html')
    
# 首页
def index(request):

    # session
    userid = request.session.get('userid', 0)  # 获取session,默认值为0
    user = UserModel.objects.filter(id=userid).first() # 模型查询
    return render(request, 'index.html', {'user': user})   # 页面渲染 


# 注销
def logout(request):

    # session
    session_key = request.session.session_key  # 获取session的键
    request.session.delete(session_key) # 删除session

    return redirect(reverse('index'))
Token
Token简介:
常用于用户数量较多和手机端用户
'''*******************
# 1.templates/login.html
注意:
*******************'''  
    <form action="{% url 'login' %}" method="post">
        {% csrf_token %}
        <p>用户名:<input type="text" name="username"></p>
        <p>密码:<input type="password" name="password"></p>
        <p><input type="submit" value="登录"></p>
    </form>

'''*******************
# 2.user/models.py
注意:
*******************'''          
class UserToken(models.Model):
    token = models.CharField(max_length=100, unique=True, null=False, verbose_name='用户令牌/用户的唯一标识')
    user = models.OneToOneField(UserModel, verbose_name='关联的用户')
    out_time = models.DateTimeField(null=False, verbose_name='过期时间')
    

    
'''*******************
# 2.user/views.py
注意:
*******************'''     
# 登录
def login(request):
    if request.method == 'GET':
        return render(request, 'login.html')

    elif request.method == 'POST':
        # 登录
        username = request.POST.get('username')
        password = request.POST.get('password')
        user = UserModel.objects.filter(username=username).first()
        # 用户名存在
        if user:
            # 再检测密码是否匹配
            if check_password(password, user.password):
                # 验证成功,登录成功
                res = redirect(reverse('index'))

                # token
                # 创建一个唯一的token
                token = get_token()
                # out_time = datetime.datetime(2020, 1, 1)
                out_time = datetime.datetime.utcnow() + datetime.timedelta(days=1)

                u_token = UserToken.objects.filter(user_id=user.id).first()
                # 如果当前登录的用户已经存在对应的token记录,则修改token记录即可
                if u_token:
                    u_token.token = token
                    u_token.out_time = out_time
                else:
                    # 如果第一次登录,创建token
                    user_token = UserToken()
                    user_token.token = token
                    user_token.user = user
                    user_token.out_time = out_time
                    user_token.save()

                # 借助cookie将token存入浏览器端
                res.set_cookie('token', token, max_age=86400)
                # return JsonResponse({'token': user_token})
                
                # 进入到首页
                return res

        return render(request, 'login.html')    
    
# 生成token值
def get_token():
    uid = uuid.uuid4()
    return uid.hex


# 首页
def index(request):

    # token
    token = request.COOKIES.get('token')
    user_token = UserToken.objects.filter(token=token).first()

    # 如果token存在
    user = None
    if user_token:
        # 判断token是否过期
        # if user_token.out_time > datetime.datetime.utcnow():
        #   timestamp: 转换成时间戳
        if user_token.out_time.timestamp() > datetime.datetime.utcnow().timestamp():
            # 没有过期, 获取用户信息
            user = user_token.user

    return render(request, 'index.html', {'user': user})


# 注销
def logout(request):
    res = redirect(reverse('index'))

    # token
    # 删除cookie中的token
    res.delete_cookie('token')
    # 删除usertoken表中的记录
    token = request.COOKIES.get('token')
    UserToken.objects.filter(token=token).delete()

    return res
    

三、csrf表单验证处理

csrf:跨站伪造请求
  解决csrf在post请求时产生的403错误:
  1.注释settings.py中MIDDLEWARE的csrf相关中间件
  2.在form表单中添加{% csrf_token %}
    
 模板设置:   
    <form action="{% url 'register' %}" method="post">
        {% csrf_token %}
        <p>用户名:<input type="text" name="username"></p>
        <p>密码:<input type="password" name="password"></p>
        <p>年龄:<input type="number" name="age"></p>
        <p><input type="submit" value="注册"></p>
    </form>    

四、静态文件与媒体文件

. 静态文件和媒体文件
媒体文件:用户上传的文件,叫做media
静态文件:存放在服务器的css,js,image等 叫做static

1. 在django中使用静态文件
    1)首先确保django.contrib.staticfiles在 INSTALLED_APPS中
    2)在settings中定义 STATIC_URL = '/static/'
    3)在你app的static目录中存放静态文件,比如:my_app/static/image/example.jpg.
    4)如果有别的静态资源文件,不在app下的static目录下,
       可以通过STATICFILES_DIRS来指定额外的静态文件搜索目录。
      	STATICFILES_DIRS = [
        	os.path.join(BASE_DIR, "static"),
        	...
    	]
    5)在模板中使用load标签去加载静态文件
    {% load static %}
    <img src="{% static "my_app/example.jpg" %}" />
    
2. 在django中使用媒体文件
在settings中配置 MEDIA_ROOT
		MEDIA_ROOT = os.path.join(BASE_DIR, "media")

如果上传的文件还需使用建议放到静态目录下:
# 媒体文件
MEDIA_ROOT = os.path.join(BASE_DIR, 'static/uploads')

实例:
'''*******************
# 3、单文件上传
views.py
注意:
request.FILES.get('icon')
*******************'''  
def index2(request):
    if request.method == 'GET':
        return render(request, 'index2.html')

    elif request.method == 'POST':
        # 接收前端参数
        username = request.POST.get('username')
        icon = request.FILES.get('icon')
        # print(icon, type(icon))
        # 11.jpg <class 'django.core.files.uploadedfile.InMemoryUploadedFile'>
        # print(icon.name, type(icon.name))  # '22.jpg' <class 'str'>

        # 1.将上传的头像存入服务器
        # file_name = icon_name() + icon.name[icon.name.rfind('.'):]
        # os.path.splitext(icon.name): ['11', '.jpg']
        file_name = icon_name() + os.path.splitext(icon.name)[-1]
        file_path = os.path.join(MEDIA_ROOT, file_name)
        with open(file_path, 'ab') as fp:
            for part in icon.chunks():  #  icon.chunks() 分块返回
                fp.write(part)
                fp.flush()

        # 2.存入数据库
        user = User()
        user.username = username
        user.icon = '/uploads/' + file_name
        user.save()

        return HttpResponse('ok')
    
# 生成唯一图片名称
def icon_name():
    ret = uuid.uuid4()
    # print(ret, type(ret))
    # 18a1610d-d96f-4968-bfcc-1566be74ba93 <class 'uuid.UUID'>
    return str(ret)
    
    
    
'''*******************
index2.html
注意:
{% csrf_token %}
enctype="multipart/form-data"
*******************'''    
    
    <form action="" method="post" enctype="multipart/form-data">
        {% csrf_token %}

        <p>用户名:<input type="text" name="username"></p>
        <p>头像:<input type="file" name="icon"></p>
        <p><input type="submit" value="上传"></p>

    </form>
    

'''*******************
# 4、多文件上传
注意:
request.FILES.getlist('icons') # 返回列表
*******************'''  

def index3(request):
    if request.method == 'GET':
        return render(request, 'index3.html')

    elif request.method == 'POST':
        # 接收前端参数
        username = request.POST.get('username')
        icons = request.FILES.getlist('icons')

        for icon in icons:

            # 1.将上传的头像存入服务器
            file_name = icon_name() + os.path.splitext(icon.name)[-1]
            file_path = os.path.join(MEDIA_ROOT, file_name)
            with open(file_path, 'ab') as fp:
                for part in icon.chunks():
                    fp.write(part)
                    fp.flush()

            # 2.存入数据库
            photo = Photo()
            photo.user = User.objects.get(username=username)
            photo.icon = '/uploads/' + file_name
            photo.save()

        return HttpResponse('ok')
    
'''*******************
index3.html
注意:
{% csrf_token %}
enctype="multipart/form-data"
<input type="file" name="icons" multiple>
*******************'''  
    <form action="" method="post" enctype="multipart/form-data">
        {% csrf_token %}
        <p>用户名:<input type="text" name="username"></p>
        <p>头像:<input type="file" name="icons" multiple></p>
        <p><input type="submit" value="上传"></p>

    </form>

五、分页

django提供了分页的工具,存在于django.core中	
	Paginator : 数据分页工具
	Page	: 具体的某一页面

导入Paginator: 	
	from django.core.paginator import Paginator

Paginator:	
对象创建: 
	Paginator(数据集,每一页数据数)
属性:	
	count:对象总数
	num_pages:页面总数
	page_range: 页码列表,从1开始
方法:	
	page(整数): 获得一个page对象

常见错误:	
	InvalidPage:page()传递无效页码
	PageNotAnInteger:page()传递的不是整数
	Empty:page()传递的值有效,但是没有数据
    
Page:
	对象获得,通过Paginator的page()方法获得
属性:
	object_list:	当前页面上所有的数据对象
	number:	当前页的页码值
	paginator:	当前page关联的Paginator对象
方法:
	has_next()	:判断是否有下一页
	has_previous():判断是否有上一页
	has_other_pages():判断是否有上一页或下一页
	next_page_number():返回下一页的页码
	previous_page_number():返回上一页的页码	
	len():返回当前页的数据的个数

'''*******************
# 分页的使用
注意:
Paginator   --分页对象
Paginator.page(页码)  --具体页
*******************'''  
# 分页
def paginate(request, page):
    if request.method == 'GET':
        page = int(page)
        per_page = 3 # 每页显示条数

        users = User.objects.all()

        # 分页对象
        p = Paginator(users, per_page)
        print(p.count)  # 10, 数据总条数
        print(p.num_pages)  # 4,总页数
        print(p.page_range)  # range(1, 5),页码范围

        # 每一页的对象
        p2 = p.page(page)
        print(p2.object_list)  # 当前页的所有数据,
            # <QuerySet [<User: User object>, <User: User object>, <User: User object>]>
        print(p2.number)  # 2, 当前页的页码
        print(p2.paginator)  # ==p对象
        
        print(p2.has_next())  # 是否有下一页,True
        print(p2.has_previous())  # 是否有上一页,True
        print(p2.has_other_pages())  # 是否有其他页,True
        
        print(p2.next_page_number())  # 下一页的页码,3
        print(p2.previous_page_number())  # 上一页的页码,1
        
        print(len(p2))  # 当前页的总数据条数,3

        data = {
            'p2': p2,
        }
        return render(request, 'paginate.html', data)
    
'''*******************
# paginate.html
结合bootstrap使用
*******************'''  
     <ul class="pagination">
        {#    上一页    #}
        <li>
            {% if p2.has_previous %}
                <a href="{% url 'paginate' p2.previous_page_number %}" >&laquo;</a>
            {% else %}
                <a href="#" >&laquo;</a>
            {% endif %}
        </li>

        {#   分页   #}
        {% for i in p2.paginator.page_range %}
            {% if p2.number == i %}
                <li class="active" >
            {% else %}
                <li>
            {% endif %}

            <a href="{% url 'paginate' i %}">{{ i }}</a></li>

        {% endfor %}

        {#   下一页   #}
        <li>
            {% if p2.has_next %}
                <a href="{% url 'paginate' p2.next_page_number %}" >&raquo;</a>
            {% else %}
                <a href="#" >&raquo;</a>
            {% endif %}
        </li>
      </ul>    

六、中间件&面向切面编程(AOP)

中间件:是一个轻量级的,底层的插件,可以介入Django的请求和响应过程(面向切面编程)

Django中间件的本质就是一个python类

面向切面编程(Aspect Oriented Programming)简称AOP。AOP的主要实现目的是针对业务处理过程中的切面进行提取,它所面对的是处理过程中的某个步骤或阶段,以获得逻辑过程中各部分之间低耦合的隔离效果。

中间件可实现功能
   - 统计
   - 黑名单
   - 白名单 
   - 反爬
   - 界面友好化(捕获异常)
    
    
   
'''*******************
2 切入函数
*******************'''     
    
__init__:
    没有参数,服务器响应第一个请求的时候自动调用,用户确定是否启用该中间件
    
process_request(self,request):
    在执行视图前被调用,每个请求上都会调用,不主动进行返回或返回HttpResponse对象
    
process_view(self,request,view_func,view_args,view_kwargs):
	调用视图之前执行,每个请求都会调用,不主动进行返回或返回HttpResponse对象
    
process_template_response(self,request,response):
    在视图刚好执行完后进行调用,每个请求都会调用,不主动进行返回或返回HttpResponse对象
    
process_response(self,request,response):
    所有响应返回浏览器之前调用,每个请求都会调用,不主动进行返回或返回HttpResponse对象
    
process_exception(self,request,exception):
    当视图抛出异常时调用,不主动进行返回或返回HttpResponse对象  
    

'''*******************
3、自定义中间件 
*******************'''        
自定义中间件流程
	1.在工程目录下创建middleware目录
	2.目录中创建一个python文件
	3.在python文件中导入中间件的基类
		from django.utils.deprecation import MiddlewareMixin
        
	4.在类中根据功能需求,创建切入需求类,重写切入点方法
        class LearnAOP(MiddlewareMixin):
            def process_request(self,request):
                print('request的路径',request.GET.path)
	5.启用中间件,在settings中进行配置,MIDDLEWARE中添加middleware.文件名.类名

'''*******************
4、自定义组件
middlewares.UserMiddleware.RegisterMiddleware
*******************'''       
    
# 自定义的注册中间件
class RegisterMiddleware(MiddlewareMixin):

    # process_request: 视图和路由之前调用
    def process_request(self, request):
        print('RegisterMiddleware => process_request')

        ip = request.META['REMOTE_ADDR']
        # print("客户端:", ip)

        # if ip == '10.20.157.51':
        #     pass
        #     return HttpResponse("小伙子,别刷了")

        # 练习:登录验证

    # 当系统报异常时调用,捕获异常
    def process_exception(self, request, exception):
        print("LoginMiddleware => process_exception")

        # 捕获异常,返回到首页
        return redirect('/index/')    
    
    
    
# 中间件
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',

    # 注册中间件
    # 'middlewares.UserMiddleware.LoginMiddleware',
    'middlewares.UserMiddleware.RegisterMiddleware',

]    
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值