• Django-C003-视图


    此文章完成度【100%】留着以后忘记的回顾。多写多练多思考,我会努力写出有意思的demo,如果知识点有错误、误导,欢迎大家在评论处写下你的感想或者纠错。

    在这个章节中,我们也一样需要练习过往已经掌握的技能:

    1.在学习阶段找到适合自己的版本,避免一些不必要的麻烦,通过以往的经验。更加高效的学习,这里就列出我所使用工具的版本信息。

    【Django version】: 2.1

    【pymysql version】:0.9.3

    【python version】: 3.7

    2.练习之前所掌握的,这也将成为我们接下来需要的项目

    # 1.创建一个项目,明确应用的名称(你可以选择在命令行中生成。也可以选择在pycharm中生成)
    # 2.项目名称view_practice 应用名称为school
    # 3.去settings配置里设置使用的数据库和你需要的信息,并确认配置是否都是自己需要的,下面是我更改过的配置
    DATABASES = {
        'default': {
            'ENGINE': 'django.db.backends.mysql',
            'NAME': 'view_school',
            'USER': 'root',
            'PASSWORD': 'toor',
            'HOST': 'localhost',
            'PORT': 3306,
        }
    }
    
    LANGUAGE_CODE = 'zh-hans'
    TIME_ZONE = 'Asia/Shanghai'
    
    # 4.创建数据库()
    create database view_school charset=utf8;
      # 4.1 导入pymysql
      # 4.2在view_practice中的__init__.py中导入pymysql
      import pymysql
      pymysql.install_as_MySQLdb()
    # 5.在 templates里添加一个文件夹用于存放html文件(这里是以应用名命名的),你也可以不创建直接放在这个文件夹下
    
    # 6.创建视图函数,你需要在school/views.py里创建
    from django.shortcuts import render, HttpResponse
    
    
    def index(request):
        return HttpResponse('测试成功')
    # 7.配置URLconf ,将浏览器请求的路径能够访问,在test3/urls.py配置 from django.contrib import admin from django.urls import path, include urlpatterns = [ path('admin/', admin.site.urls), path('', include('school.urls')), ]
    # 8.将对school这个应用的请求,同意传入到应用的urls.py中(你可以直接将,view_practice中的urls复制一份过来) from django.conf.urls import url from school import views urlpatterns = [ url(r'^index$', views.index), ]
    # 9.启动服务器测试 python manage.py runserver
    访问:127.0.0.1:8000/index
     
     
     
     
    视图层
    Django 具有 “视图”的概念,负责处理用户的请求并返回响应

     URL

    为了给一个应用设计路径,你需要创建一个Python 模块,通常被称为**URLconf**(urls.py)。这个模块是纯粹的Python 代码,包含URL 模式(简单的正则表达式)到Python 函数(你的视图)的简单映射。简单的理解,就是用户在浏览器的地址栏中输入网址 请求 网站,Django将决定返回什么给浏览器,就是通过url匹配找到的

    Django 如何处理一个请求

    当一个用户请求 Django网站的一个页面,下面是Django 系统决定执行哪个Python 代码执行步骤:

    • 第一步:Django会根据settings.py里面ROOT_URLCONF设置的值去查找匹配,但这个并不是一成不变的,如果传入的HttpRequest对象具有urlconf属性(由中间件设置),那么它的值将代替ROOT_URLCONF的设置。

          

    • 第二步:Django加载urls.py(ROOT_URLCONF的值找到的urls.py),并查找变量urlpatterns。这个变量会是Python的列表,列表可能是由 django.urls.path()、django.urls.re_path()的实例对象组成    

          

    • 第三步:Django 依次遍历并列表,遇到第一个与请求的url匹配的时候停止匹配

    • 第四步: 匹配成功后,django将调用给出的这个视图,这里可以是一个简单的python函数,也可以是一个基于视图的类,这个视图会获得传递过来的下面这些参数

      • 一个HttpRequest 实例(这是一定要有的)
      • 如果匹配的URL没有返回命名组,那么正则表达式中的匹配将作为位置参数提供。
      • 关键字参数由路径表达式里的这些命名部分匹配

        

        

    • 第五步:如果没有匹配,或在此过程中的任何异常,Django将调用默认错误处理视图。

           

     
     
     
     

    django.urls 用于URLconf的函数


     

    path()

    • path( route, view, kwargs=Nonename=None)

    • Django : 2.0 新特性 
    from django.urls import include, path
    
    urlpatterns = [
        path('index/', views.index, name='main-view'),
        path('bio/<username>/', views.bio, name='bio'),
        path('articles/<slug:title>/', views.article, name='article-detail'),
        path('articles/<slug:title>/<int:section>/', views.section, name='article-section'),
        path('weblog/', include('blog.urls')),
        ...
    ]
    • route参数:
    • 应该是一个字符串或者gettext_lazy(),通过URL获取字符串,这个字符串就要包含尖括号(就像上面的<username>),并将其作为关键字参数发送给视图,这个尖括号还包含了一些转义规范(就像int类型<int:section>)这可以限制字符类型,也可以更改传递给视图的变量的类型,比如, <int:section>可以传递一个小数的字符串转换成int类型,简单的理解就是URL当中可以传递你需要的值以字符串的形式,你还可以限制值的类型。
    • view参数:
    • 可以是一个视图函数或者一个视图类,也可以是django.urls.include()

     re_path()

    • re_path(routeviewkwargs=Nonename=None)

    • Django: 2.0 新特性 

    • from django.urls import include, re_path
      
      urlpatterns = [
          re_path(r'^index/$', views.index),
          re_path(r'^bio/(?P<username>w+)/$', views.bio),
          re_path(r'^weblog/', include('blog.urls')),
          ...
      ]
    • route参数:

    • 应该是一个字符串或者gettext_lazy(),它包含一个与Python的re模块兼容的正则表达式,从正则表达式捕获的组传递给视图——如果组已命名,则作为命名参数传递

    • view参数:可以是一个视图函数或者一个视图类,也可以是django.urls.include()

     include()

    • include(modulenamespace=None)  

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

      modult:应用名.urls

    获取值


     

    请求的url被看做是一个普通的python字符串,进行匹配时不包括域名、get或post参数。 如请求地址如下:

    http://127.0.0.1:8000/index

    1)去除掉域名和参数部分,并将最前面的/去除后,只剩下如下部分与正则匹配。

    index

    2) 使用school/urls.py 定义与这个地址匹配的url

    from django.urls import re_path
    from school import views
    urlpatterns = [
        re_path(r'^indexd+$', views.index),
    ]

    3)在视图中创建index

    def index(request):
        return HttpResponse('test ok')

    可以在匹配过程中从url中捕获参数,每个捕获的参数都作为一个普通的python字符串传递给视图。

    获取值需要在正则表达式中使用小括号,分为两种方式:

    • 位置参数
    • 关键字参数

     注意:两种参数的方式不要混合使用,在一个正则表达式中只能使用一种参数方式。

     位置参数

    直接使用小括号包起来,通过位置参数传递给视图

    from django.urls import re_path
    from school import views
    urlpatterns = [
        re_path(r'^index(d+)$', views.index),
    ]

    将视图接受传递的值

    def index(request, num):
        return HttpResponse('接受的位置参数是:%s' % num)

    关键字参数

     在正则表达式部分为组命名。

    urls

    re_path(r'^keyword(?P<num>d+)$', views.keyword),

    views

    def keyword(request, num):
        return HttpResponse('接收的关键字参数是:%s' % num)

    view


    视图就是Python中的函数,视图一般被定义在 应用/views.py文件中,视图必须返回一个HttpResponse对象或子类对象作为响应,响应可以是一个HTML内容,重定向,等等

    内置错误视图

    Django内置处理HTTP错误的视图,404、500 如果不喜欢默认的返回页面,你可以在templates中自己创建一个

    HttpReqeust对象

    服务器接受到HTTP协议的请求后,会根据报文创建HttpRequest对象,这个对象不要手动创建,直接使用服务器构造好的对象就可以,视图的第一个参数必须是HttpRequest对象

    属性

    • path : 字符串类型,请求页面的完整路径,不包含域名和参数
    • method :字符串类型,表示请求使用的HTTP方法,【GET】【POST】
      • 地址栏发出请求采用的是GET
      • 浏览器中点击表单的提交按钮发起请求,如表单的method设置为POST则为POST请求
    • encoding : 字符串类型,表示提交的数据的编码方式
      • 如果为None则表示使用浏览器的默认设置,一般UTF-8
      • 这个属性是可写的权限,可以通过修改它来修改访问表单数据使用的编码,接下来对属性的任何访问将使用新的encoding值
    • GET :QueryDict类型对象,类似于字典,包含GET请求方式的所有参数
    • POST :QueryDict类型对象,包含POST请求方式的所有参数
    • FILES :一个类似于字典的对象,包含所有的上传文件
    • COOKIES :一个标准的Python字典,包含所有的cookie。键和值都是字符串
    • SESSION :拥有读写权限,类似字典的对象,表示当前的会话,只有当Django启用会话的支持时才可用,

    访问网站,在浏览器中的开发者工具中看到请求信息:

     

    演示:属性 path、method、encoding

     视图

    def index(request):
        str = '%s : %s : %s' % (request.path, request.method, request.encoding)
        return render(request, 't_school/index.html', {'str': str})

    URLS

    from django.urls import re_path
    from school import views
    urlpatterns = [
        re_path(r'^index$', views.index),
    ]

    模板(Templates)

    <!DOCTYPE html>
    <html lang="en">
    <head>
        <meta charset="UTF-8">
        <title>Title</title>
    </head>
    <body>
        {{ str }}
    </body>
    </html>

    结果

    那么问题来了,POST的请求如何获取查看呢

    开发视图模块,直接将值返回给页面

    def post_method(request):
        return HttpResponse(request.method)

    配置URL

    re_path(r'^postmethod$', views.post_method),

     在模板下的index.html添加代码

        <a href='/postmethod'>get方式</a><br/>
        <form method="post" action="/postmethod">
            <input type="submit" value="post方式">
        </form>

    注意:这里需要注释掉一行代码。否则会拒绝请求,在settings中,将带有csrf这行注释掉

    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',
    ]

    阻止请求的页面展示

    展示一下效果

    点击post方式按钮

    QueryDict对象

    • 定义在Django.http.QueryDict
    • HttpRequest对象的属性GET、POST都是QueryDict类型的对象
    • 与python字典不同,QueryDict类型的对象用来处理同一个键带有多个值的情况
    • 方法,get() 根据键获取值
    • 如果同一个键同时拥有多个值获取最后一个
    • 如果键不存在,则返回None,可以设置默认值
    dict.get('',默认值)
    可简写为
    dict['']
    • 方法getlist():根据键获取值,值以列表返回,可以获取指定键的所有值
    • 如果键不存在则返回空列表, 可以设置默认值进行后续处理
    dict.getlist('',默认值)

     GET属性

    请求格式:在请求地址结尾使用?之后以 "键 " = " 值 " 的格式拼接,多个键值对之以&符连接。

    https://www.????.com/?a=1&b=2&c=circle

    其中的请求参数为:

    a=1&b=2&c=circle
    • 分析请求参数:  代表键的是 ’a'、’b'、’c',代表值的是:'1'、'2'、'circle'
    • Django中可以试用HttpRequest对象的GET属性,获得GET请求方式的参数
    • GET属性是一个QueryDict类型的对象,键和值都是字符串类型
    • 键是开发人员在编写代码时编写的
    • 值是根据数据生成的

    POST属性

    使用form表单请求时,method方式为POST则会发起POST方式的请求,需要使用HttpRequest对象POST属性接收参数,POST属性是一个QueryDIct类型的对象

    • 表单form如何提交参数?
      • 表单控件name属性的值,value属性的值为值,构成键值对提交
        • 如果表单控件没有name属性则不提交
        • 对于checkbox控件,name属性的值相同为一组,被选中的项会被提交,出现一键多值的情况
        • 键是表单控件name属性的值,是由开发人员编写的
        • 值是用户填写或选择的

    视图 views.py

    def receive_args(request):
        if request.method == 'GET':
            var1 = request.GET.get('a')
            var2 = request.GET.get('b')
            var3 = request.GET.get('c')
            g_dict = {'a': var1, 'b': var2, 'c': var3}
            return render(request, 't_school/receive_args.html', g_dict)
        if request.method == 'POST':
            name = request.POST.get('username')
            gender = request.POST.get('gender')
            hobbies = request.POST.getlist('hobbies')
            p_dict = {'name': name, 'gender': gender, 'hobbies': hobbies}
            return render(request, 't_school/receive_args.html', p_dict)

    URL : urls.py

    re_path(r'^receive_args$', views.receive_args),

    模板 :receive_args.html

    <!DOCTYPE html>
    <html lang="en">
    <head>
        <meta charset="UTF-8">
        <title>Title</title>
    </head>
    <body>
        3.提交数据的两种方式:
        get方式:<br/>
        <a href="/receive_args?a=1&b=2&c=python">get方式提交数据</a><br/>
        post方式:<br/>
        <form method="post" action="/receive_args">
            姓名:<input type="text" name="username"><br/>
            性别:男<input type="radio" name="gender" value="男"/><input type="radio" name="gender" value="女"/><br/>
            爱好:
            吃饭<input type="checkbox" name="hobbies" value="吃饭"/>
            睡觉<input type="checkbox" name="hobbies" value="睡觉"/>
            打豆豆<input type="checkbox" name="hobbies" value="打豆豆"/><br>
            <input type="submit" value="提交">
        </form>
        <br/>
        GET:{{ a }} : {{ b }} : {{ c }}
        <br/>
        <ul style="list-style-type:none">
            POST:{{ name }} : {{ gender }} : {% for hobby in hobbies %}
                <li>{{ hobby }}</li>
            {% endfor %}
        </ul>
    </body>
    </html>

    展示结果GET

     

    展示结果POST

    HttpResponse对象


     

     视图在接收请求并处理后,必须返回HttpResponse对象或子类对象,在django.http模块中定义,HttpResponse对象的API。HttpRequest对象是由Django创建,HttpResponse对象由开发人员创建

    属性

    • content : 表示返回的内容
    • charset : 表示response采用的编码字符集,默认utf8
    • status_code : 返回HTTP响应的状态码
    • content-type : 指定返回数据的MIME类型,默认为text/html

    方法

    • __init__ : 创建Httpresponse对象后完成返回内容的初始化
    • set_cookie :设置cookie信息
      • set_cookie(key, value='', max_age=None, expires=None)
    • cookie : 是网站以键值对格式存储在浏览器中的一段纯文本信息,用于实现用户跟踪
      • max_age 是一个整数,表示指定秒数后过期
      • expires 是一个datetime或timedelta对象,会话将在这个指定时间过期
      • max_age 和 expires 二选一
      • 如果不指定过期时间,在关闭浏览器时cookie过期
    • delete_cookie(key) : 删除指定的key的cookie,如果key不存在则什么不发生
    • write : 向响应体重写数据

    通过之前学习,我们可以看出调用模板的方式是用render:

    def index(request):
        return render(request, 't_school/index.html', {'a': 'a'})

    第一个参数就是必要HttpRequest对象 第二个参数就是模板,第三个参数是一个字典

    子类JsonResponse

    在浏览器中使用javascript发起ajax请求时,返回json格式的数据,此外以jquery的get()方法为例。JsonResponse继承子HttpResponse,被定义在django.http模块中,创建对象时接收字典作为参数, JsonResponse对象的content-type : "application/json"

    定义视图:views.py

    from django.http import JsonResponse
    ...
    def json1(request):
        return render(request,'t_school/json1.html') 
    def json2(request):
        return JsonResponse({'h1':'hello','h2':'world'})  # 返回json格式的数据

    URL : urls.py

        re_path(r'^json1$', views.json1),
        re_path(r'^json2$', views.json2),

    创建static/js 目录 把jquery文件拷贝到这个目录下

    打开settings 确认配置信息

     

     

    {% load static %} # 加载 static 模块
    {% static  "js/jquery-1.12.4.min.js" %}  # 指向文件

    模板 templates/t_school/json1.html

    <!DOCTYPE html>
    <html lang="en">
    <head>
        <meta charset="UTF-8">
        <title>Title</title>
    {% load static %}
    <script src="{% static  "js/jquery-1.12.4.min.js" %}"></script>
    <script>
        $(function () {
            $('#btnJson').click(function () {
                $.get('/json2',function (data) {
                    ul=$('#jsonList');
                    ul.append('<li>'+data['h1']+'</li>')
                    ul.append('<li>'+data['h2']+'</li>')
                })
            });
        });
    </script>
    </head>
    <body>
    <input type="button" id="btnJson" value="获取json数据">
    <ul id="jsonList"></ul>
    </body>
    </html>

    展示效果 

    json1.html执行过程

    子类HttpResponseRedirect

    当一个逻辑处理完,不需要向客户端呈现数据,而是转回到其他页面,如果添加成功、修改成功、删除成功、后显示数据列表,而数据的列表视图已经开发完成,此时不需要重新编写,而是转到这个视图就可,就叫做重定向

     Django中提供了HttpResponseRedirect对象实现重定向功能,这个类继承自HttpResponse,被定义在django.http模块中,返回状态码302

    视图:views.py

    from django.http import HttpResponseRedirect
    
    def redirect_index(request):
        return HttpResponseRedirect("index")

    URL: urls.py

    re_path(r'^redirect_index$', views.redirect_index),

    显示结果

    重定向简写函数redirect

     在django.shortcuts模块中为重定向类提供了简写函数redirect

    from django.shortcuts import redirect
    ...
    def redirect_index(request):
        return redirect('index')

    状态保持

    浏览器请求服务器是无状态的,无状态指一次用户请求时,浏览器,服务器无法知道之前这个用户做过什么,每次请求都是一次新的请求,无状态的应用层面的原因是:浏览器和服务器通信走遵守HTTP协议。根本原因是:浏览器与服务器是使用Socket套接字进行通信的,服务器将请求结果返回浏览器之后,会关闭当前的Socket链接,而且服务器也会在处理页面完毕之后销毁页面对象。但是有时候需要保存下来用户浏览的状态,比如用户是否登录过,浏览过哪些页面,实现状态保持主要有两种方式

    1. 在客户端存储信息使用Coolie
    2. 在服务器端存储信息使用Session

      

    Cookie:有时也用其复数形式Cookies,某网站为了辨别用户身份,进行session跟踪而存储在用户本地终端上的数据(通常是经过加密的),Cookie一般是由服务端生成,发送给User-Agent(一般是浏览器),浏览器会将Cookie的key/value保存在某个目录下的文本文件内,下次请求同一个网站时就发送该Cookie给服务器(前提是浏览器设置为启用cookie),Cookie名称和值可以由服务端开发自己定义,这样服务器可以知道该用户是否是合法用户以及是否需要重新登录等,服务器可以利用Cookie包含信息的任意性来筛选并经常维护这些信息。以判断在HTTP传输中的状态,Cookie最典型记住用户名

    Cookie是存储在浏览器中一段纯文本信息,建议不要存储敏感信息,因为电脑上的浏览器可以被其他人使用

    Cookie特点

    • Cookie以键值对的格式进行信息的存储
    • Cookie基于域名安全,不同域名的Cookie是不能互相访问的
    • 浏览器请求某网站时,会将浏览器存储的跟网站相关的所有Cookie信息提交给网站服务器

    典型应用:记住用户名,网站广告推送

    设置Cookie

    视图

    def cookie_set(request):
        response = HttpResponse("<h1>设置Cookie</h1>")
        response.set_cookie("id", 1)
        return response

    URL

    re_path(r'^cookie_set$', views.cookie_set),

    读取cookie

     Cookie信息被包含在请求头里,使用request对象的COOKIES属性访问

    视图

    def get_cookie(request):
        get_c = request.COOKIES["id"]
        return HttpResponse(get_c)

    URL

    re_path(r'^get_cookie$', views.get_cookie),

    返回结果

    Session

    对于敏感信息,建议要存储在服务端中,不能存储在浏览器中,那么就需要使用session

    启用session

    Django项目默认启用

    打开settings.py文件,在MIDDLEWARE_CLASSES中查看

    存储方式

    打开settings.py文件,设置SESSION_ENGINE指定session数据存储方式,可以存储在数据库、缓存、redis等

    1. 存储在数据库中,如下设置可以写,也可以不写,这是默认存储方式。
      1. SESSION_ENGINE = 'django.contrib.sessions.backends.db'
    2. 存储在缓存中:存储在本机内存中,如果丢失则不能找回,比数据库的方式读写更快。
      1. SESSION_ENGINE = 'django.contrib.sessions.backends.cache'
    3. 混合存储:优先从本机内存中存取,如果没有则从数据库中存取。
      1. SESSION_ENGINE = 'django.contrib.sessions.backends.cache_db'
    4. 如果存储在数据库中,需要在项INSTALLED_APPS中安装Session应用。
      1. INSTALLED_APPS = [
            'django.contrib.admin',
            'django.contrib.auth',
            'django.contrib.contenttypes',
            'django.contrib.sessions',
            'django.contrib.messages',
            'django.contrib.staticfiles',
            'school.apps.SchoolConfig',
        ]
    5. 迁移后会在数据库中创建出存储Session的表。  

    6. 表结构如下图。

        

    依赖于Cookie

    所有请求这的Session都会存储在服务器中,服务器如何区分请求者和Session数据的对应

      使用session后,会在Cookie中存储一个session的数据,每次请求时浏览器都会将这个数据发送给服务器,服务器在接收到sessionid后,会根据这个值找出请求者的session,如果想使用session,浏览器必须支持Cookie,否则就无法使用Session,存储session时,键与Cookie中的sessionid相同,值是开发人员设置的键值对信息,进行了base64编码,过期时间由开发人员设置

    对象及方法

    通过HttpRequest对象的session属性进行会话的读写操作

    1. 以键值对的格式写session    
        request.session['键']=值
    2. 根据键值取值
      request.session.get('键',默认值)
    3. 清除所有session,在存储中删除值部分
      request.session.clear()
    4. 删除session中的指定键及值,在储存中值删除某个键及对应的值
      del request.session['键']
    5. 清除session数据,在存储中删除session的整条数据
      request.session.flush()
    6. 设置session的超时时间,如果没有指定过期时间则两个星期后过期
      request.session.set_expiry(value)

    如果value是一个整数,session将在value秒没有活动后过期

    如果value是0,那么用户会话的Cookie将在用户关闭浏览器时过期

    如果value是None,那么session永不过期

    视图

    def session_test(request):
        request.session['user'] = 'circle'
        return HttpResponse('写入session')

    URL

    re_path(r'^session_test$', views.session_test),

    这里需要注意:如果始终没有执行迁移的话,访问网站的时候会报错:迁移的代码是: python manage.py migrate

    查看Cookie中的sessionid值为"hcmdhu458d4tgksu6f60zwfskyvgrjjf",数据表中session的键为“hcmdhu458d4tgksu6f60zwfskyvgrjjf”,是一样的,这样,服务器就可以在众多的请求者中找到对应的Session数据。

    在MySQL数据库中复制

    点击BASE64解码

     

    读取session

    视图

    def session_read(request):
        user = request.session.get('user')
        return HttpResponse(user)

     URL

     re_path(r'^session_read$', views.session_read),

    结果展示

     

    删除session中的指定键及值

    视图

    def session_del(request):
        del request.session['user']
        return HttpResponse("del ok")

    URL

    re_path(r'^session_del$', views.session_del),

     展示效果:对比之前{} 为空,所以键和值都被删除

    如果将所有的键及值都删除,逐个调用del太麻烦,可以使用clear()方法。

    删除session

    视图

    def session_flush(request):
        request.session.flush()
        return HttpResponse("flush ok")

    URL

    re_path(r'^session_flush$', views.session_flush),

    展示效果

    这当然看不出什么,最终结果在这里

    得以验证,数据库全没了

    -THE END-

  • 相关阅读:
    总结的CSS简写表
    ASP.net 2.0:我还有多少秘密你不知道?(1)
    判断自然数的阶乘大于等于400,然后计算此数的平方,再一次减1计算其平方和,直到数字减小到0(演示Exit DO)
    JSP留言板程序开发过程
    double>string的时候,如何保留两位小数?
    asp如何清除html代码
    利用ASP.NET来访问Excel文档
    C#日期函数所有样式大全
    ASP.net在线购物商城系统完全解析
    创立公司的准备
  • 原文地址:https://www.cnblogs.com/Hannibal-2018/p/11076298.html
Copyright © 2020-2023  润新知