django 1.8 官方文档翻译: 3-1-1 URL调度器

URL调度器

简洁、优雅的URL 模式在高质量的Web 应用中是一个很是重要的细节。Django 容许你任意设计你的URL,不受框架束缚。php

不要求有.php.cgi,更不会要求相似0,2097,1-1-1928,00 这样无心义的东西。html

参见万维网的发明者Berners-Lee 的Cool URIs don’t change,里面有关于为何URL 应该保持整洁和有意义的卓越的论证。python

概览

为了给一个应用设计URL,你须要建立一个Python 模块,一般称为URLconf(URL configuration)。这个模块是纯粹的Python 代码,包含URL 模式(简单的正则表达式)到Python 函数(你的视图)的简单映射。web

映射可短可长,随便你。它能够引用其它的映射。并且,由于它是纯粹的Python 代码,它能够动态构造。正则表达式

Django 还提供根据当前语言翻译URL 的一种方法。更多信息参见国际化文档算法

Django 如何处理一个请求

当一个用户请求Django 站点的一个页面,下面是Django 系统决定执行哪一个Python 代码使用的算法:django

  1. Django 决定要使用的根URLconf 模块。一般,这个值就是ROOT_URLCONF 的设置,可是若是进来的HttpRequest 对象具备一个urlconf 属性(经过中间件request processing 设置),则使用这个值来替换ROOT_URLCONF 设置。浏览器

  2. Django 加载该Python 模块并寻找可用的urlpatterns。它是django.conf.urls.url() 实例的一个Python 列表。服务器

  3. Django 依次匹配每一个URL 模式,在与请求的URL 匹配的第一个模式停下来。app

  4. 一旦其中的一个正则表达式匹配上,Django 将导入并调用给出的视图,它是一个简单的Python 函数(或者一个基于类的视图)。视图将得到以下参数:

    • 一个HttpRequest 实例。

    • 若是匹配的正则表达式没有返回命名的组,那么正则表达式匹配的内容将做为位置参数提供给视图。

    • 关键字参数由正则表达式匹配的命名组组成,可是能够被django.conf.urls.url()的可选参数kwargs覆盖。

  5. 若是没有匹配到正则表达式,或者若是过程当中抛出一个异常,Django 将调用一个适当的错误处理视图。请参见下面的错误处理。

例子

下面是一个简单的 URLconf:

from django.conf.urls import url

from . import views

urlpatterns = [
    url(r'^articles/2003/$', views.special_case_2003),
    url(r'^articles/([0-9]{4})/$', views.year_archive),
    url(r'^articles/([0-9]{4})/([0-9]{2})/$', views.month_archive),
    url(r'^articles/([0-9]{4})/([0-9]{2})/([0-9]+)/$', views.article_detail),
]

注:

  • 若要从URL 中捕获一个值,只须要在它周围放置一对圆括号。

  • 不须要添加一个前导的反斜杠,由于每一个URL 都有。例如,应该是^articles 而不是 ^/articles

  • 每一个正则表达式前面的'r' 是可选的可是建议加上。它告诉Python 这个字符串是“原始的” —— 字符串中任何字符都不该该转义。参见Dive Into Python 中的解释。

一些请求的例子:

  • /articles/2005/03/ 请求将匹配列表中的第三个模式。Django 将调用函数views.month_archive(request, '2005', '03')

  • /articles/2005/3/ 不匹配任何URL 模式,由于列表中的第三个模式要求月份应该是两个数字。

  • /articles/2003/ 将匹配列表中的第一个模式不是第二个,由于模式按顺序匹配,第一个会首先测试是否匹配。请像这样自由插入一些特殊的状况来探测匹配的次序。

  • /articles/2003 不匹配任何一个模式,由于每一个模式要求URL 以一个反斜线结尾。

  • /articles/2003/03/03/ 将匹配最后一个模式。Django 将调用函数views.article_detail(request, '2003', '03', '03')

命名组

上面的示例使用简单的、没有命名的正则表达式组(经过圆括号)来捕获URL 中的值并以位置 参数传递给视图。在更高级的用法中,可使用命名的正则表达式组来捕获URL 中的值并以关键字 参数传递给视图。

在Python 正则表达式中,命名正则表达式组的语法是(?P<name>pattern),其中name 是组的名称,pattern 是要匹配的模式。

下面是以上URLconf 使用命名组的重写:

from django.conf.urls import url

from . import views

urlpatterns = [
    url(r'^articles/2003/$', views.special_case_2003),
    url(r'^articles/(?P<year>[0-9]{4})/$', views.year_archive),
    url(r'^articles/(?P<year>[0-9]{4})/(?P<month>[0-9]{2})/$', views.month_archive),
    url(r'^articles/(?P<year>[0-9]{4})/(?P<month>[0-9]{2})/(?P<day>[0-9]{2})/$', views.article_detail),
]

这个实现与前面的示例彻底相同,只有一个细微的差异:捕获的值做为关键字参数而不是位置参数传递给视图函数。例如:

  • /articles/2005/03/ 请求将调用views.month_archive(request, year='2005', month='03')函数,而不是views.month_archive(request, '2005', '03')

  • /articles/2003/03/03/ 请求将调用函数views.article_detail(request, year='2003', month='03', day='03')

在实际应用中,这意味你的URLconf 会更加明晰且不容易产生参数顺序问题的错误 —— 你能够在你的视图函数定义中从新安排参数的顺序。固然,这些好处是以简洁为代价;有些开发人员认为命名组语法丑陋而繁琐。

匹配/分组算法

下面是URLconf 解析器使用的算法,针对正则表达式中的命名组和非命名组:

  1. 若是有命名参数,则使用这些命名参数,忽略非命名参数。

  2. 不然,它将以位置参数传递全部的非命名参数。

根据传递额外的选项给视图函数(下文),这两种状况下,多余的关键字参数也将传递给视图。

URLconf 在什么上查找

URLconf 在请求的URL 上查找,将它当作一个普通的Python 字符串。不包括GET和POST参数以及域名。

例如,http://www.example.com/myapp/ 请求中,URLconf 将查找myapp/

http://www.example.com/myapp/?page=3 请求中,URLconf 仍将查找myapp/

URLconf 不检查请求的方法。换句话讲,全部的请求方法 —— 同一个URL的POSTGETHEAD等等 —— 都将路由到相同的函数。

捕获的参数永远是字符串

每一个捕获的参数都做为一个普通的Python 字符串传递给视图,不管正则表达式使用的是什么匹配方式。例如,下面这行URLconf 中:

url(r'^articles/(?P<year>[0-9]{4})/$', views.year_archive),

... views.year_archive()year 参数将是一个字符串,即便[0-9]{4} 值匹配整数字符串。

指定视图参数的默认值

有一个方便的小技巧是指定视图参数的默认值。 下面是一个URLconf 和视图的示例:

# URLconf
from django.conf.urls import url

from . import views

urlpatterns = [
    url(r'^blog/$', views.page),
    url(r'^blog/page(?P<num>[0-9]+)/$', views.page),
]

# View (in blog/views.py)
def page(request, num="1"):
    # Output the appropriate page of blog entries, according to num.
    ...

在上面的例子中,两个URL模式指向同一个视图views.page —— 可是第一个模式不会从URL 中捕获任何值。若是第一个模式匹配,page() 函数将使用num参数的默认值"1"。若是第二个模式匹配,page() 将使用正则表达式捕获的num 值。

性能

urlpatterns 中的每一个正则表达式在第一次访问它们时被编译。这使得系统至关快。

urlpatterns 变量的语法

urlpatterns 应该是url() 实例的一个Python 列表。

错误处理

当Django 找不到一个匹配请求的URL 的正则表达式时,或者当抛出一个异常时,Django 将调用一个错误处理视图。

这些状况发生时使用的视图经过4个变量指定。它们的默认值应该知足大部分项目,可是经过赋值给它们以进一步的自定义也是能够的。

完整的细节请参见自定义错误视图

这些值能够在你的根URLconf 中设置。在其它URLconf 中设置这些变量将不会生效果。

它们的值必须是可调用的或者是表示视图的Python 完整导入路径的字符串,能够方便地调用它们来处理错误状况。

这些值是:

  • handler404 —— 参见django.conf.urls.handler404

  • handler500 —— 参见django.conf.urls.handler500

  • handler403 —— 参见django.conf.urls.handler403

  • handler400 —— 参见django.conf.urls.handler400

包含其它的URLconfs

在任什么时候候,你的urlpatterns 均可以包含其它URLconf 模块。这实际上将一部分URL 放置与其它URL 下面。

例如,下面是URLconf for the Django 网站本身的URLconf 中一个片断。它包含许多其它URLconf:

from django.conf.urls import include, url

urlpatterns = [
    # ... snip ...
    url(r'^community/', include('django_website.aggregator.urls')),
    url(r'^contact/', include('django_website.contact.urls')),
    # ... snip ...
]

注意,这个例子中的正则表达式没有包含$(字符串结束匹配符),可是包含一个末尾的反斜杠。每当Django 遇到include()django.conf.urls.include())时,它会去掉URL 中匹配的部分并将剩下的字符串发送给包含的URLconf 作进一步处理。

另一种包含其它URL 模式的方式是使用一个url() 实例的列表。例如,请看下面的URLconf:

from django.conf.urls import include, url

from apps.main import views as main_views
from credit import views as credit_views

extra_patterns = [
    url(r'^reports/(?P<id>[0-9]+)/$', credit_views.report),
    url(r'^charge/$', credit_views.charge),
]

urlpatterns = [
    url(r'^$', main_views.homepage),
    url(r'^help/', include('apps.help.urls')),
    url(r'^credit/', include(extra_patterns)),
]

在这个例子中,/credit/reports/ URL将被 credit.views.report() 这个Django 视图处理。

这种方法能够用来去除URLconf 中的冗余,其中某个模式前缀被重复使用。例如,考虑这个URLconf:

from django.conf.urls import url
from . import views

urlpatterns = [
    url(r'^(?P<page_slug>[\w-]+)-(?P<page_id>\w+)/history/$', views.history),
    url(r'^(?P<page_slug>[\w-]+)-(?P<page_id>\w+)/edit/$', views.edit),
    url(r'^(?P<page_slug>[\w-]+)-(?P<page_id>\w+)/discuss/$', views.discuss),
    url(r'^(?P<page_slug>[\w-]+)-(?P<page_id>\w+)/permissions/$', views.permissions),
]

咱们能够改进它,经过只声明共同的路径前缀一次并将后面的部分分组:

from django.conf.urls import include, url
from . import views

urlpatterns = [
    url(r'^(?P<page_slug>[\w-]+)-(?P<page_id>\w+)/', include([
        url(r'^history/$', views.history),
        url(r'^edit/$', views.edit),
        url(r'^discuss/$', views.discuss),
        url(r'^permissions/$', views.permissions),
    ])),
]

捕获的参数

被包含的URLconf 会收到来之父URLconf 捕获的任何参数,因此下面的例子是合法的:

# In settings/urls/main.py
from django.conf.urls import include, url

urlpatterns = [
    url(r'^(?P<username>\w+)/blog/', include('foo.urls.blog')),
]

# In foo/urls/blog.py
from django.conf.urls import url
from . import views

urlpatterns = [
    url(r'^$', views.blog.index),
    url(r'^archive/$', views.blog.archive),
]

在上面的例子中,捕获的"username"变量将被如期传递给包含的 URLconf。

嵌套的参数

正则表达式容许嵌套的参数,Django 将解析它们并传递给视图。当反查时,Django 将尝试填满全部外围捕获的参数,并忽略嵌套捕获的参数。考虑下面的URL 模式,它带有一个可选的page 参数:

from django.conf.urls import url

urlpatterns = [
    url(r'blog/(page-(\d+)/)?$', blog_articles),                  # bad
    url(r'comments/(?:page-(?P<page_number>\d+)/)?$', comments),  # good
]

两个模式都使用嵌套的参数,其解析方式是:例如blog/page-2/ 将匹配blog_articles并带有两个位置参数page-2/ 和2。第二个comments 的模式将匹配comments/page-2/ 并带有一个值为2 的关键字参数page_number。这个例子中外围参数是一个不捕获的参数(?:...)

blog_articles 视图须要最外层捕获的参数来反查,在这个例子中是page-2/或者没有参数,而comments能够不带参数或者用一个page_number值来反查。

嵌套捕获的参数使得视图参数和URL 之间存在强耦合,正如blog_articles 所示:视图接收URL(page-2/)的一部分,而不仅是视图感兴趣的值。这种耦合在反查时更加显著,由于反查视图时咱们须要传递URL 的一个片断而不仅是page 的值。

做为一个经验的法则,当正则表达式须要一个参数但视图忽略它的时候,只捕获视图须要的值并使用非捕获参数。

传递额外的选项给视图函数

URLconfs 具备一个钩子,让你传递一个Python 字典做为额外的参数传递给视图函数。

django.conf.urls.url() 函数能够接收一个可选的第三个参数,它是一个字典,表示想要传递给视图函数的额外关键字参数。

例如:

from django.conf.urls import url
from . import views

urlpatterns = [
    url(r'^blog/(?P<year>[0-9]{4})/$', views.year_archive, {'foo': 'bar'}),
]

在这个例子中,对于/blog/2005/请求,Django 将调用views.year_archive(request, year='2005', foo='bar')

这个技术在Syndication 框架 中使用,来传递元数据和选项给视图。

处理冲突

URL 模式捕获的命名关键字参数和在字典中传递的额外参数有可能具备相同的名称。当这种状况发生时,将使用字典中的参数而不是URL 中捕获的参数。

传递额外的选项给include()

相似地,你能够传递额外的选项给include()。当你传递额外的选项给include() 时,被包含的URLconf 的每一 行将被传递这些额外的选项。

例如,下面两个URLconf 设置功能上彻底相同:

设置一次:

# main.py
from django.conf.urls import include, url

urlpatterns = [
    url(r'^blog/', include('inner'), {'blogid': 3}),
]

# inner.py
from django.conf.urls import url
from mysite import views

urlpatterns = [
    url(r'^archive/$', views.archive),
    url(r'^about/$', views.about),
]

设置两次:

# main.py
from django.conf.urls import include, url
from mysite import views

urlpatterns = [
    url(r'^blog/', include('inner')),
]

# inner.py
from django.conf.urls import url

urlpatterns = [
    url(r'^archive/$', views.archive, {'blogid': 3}),
    url(r'^about/$', views.about, {'blogid': 3}),
]

注意,额外的选项将永远传递给被包含的URLconf 中的每一行,不管该行的视图其实是否定为这些选项是合法的。因为这个缘由,该技术只有当你肯定被包含的URLconf 中的每一个视图都接收你传递给它们的额外的选项。

URL 的反向解析

在使用Django 项目时,一个常见的需求是得到URL 的最终形式,以用于嵌入到生成的内容中(视图中和显示给用户的URL等)或者用于处理服务器端的导航(重定向等)。

人们强烈但愿不要硬编码这些URL(费力、不可扩展且容易产生错误)或者设计一种与URLconf 绝不相关的专门的URL 生成机制,由于这样容易致使必定程度上产生过时的URL。

换句话讲,须要的是一个DRY 机制。除了其它有点,它还容许设计的URL 能够自动更新而不用遍历项目的源代码来搜索并替换过时的URL。

获取一个URL 最开始想到的信息是处理它视图的标识(例如名字),查找正确的URL 的其它必要的信息有视图参数的类型(位置参数、关键字参数)和值。

Django 提供一个办法是让URL 映射是URL 设计惟一的地方。你填充你的URLconf,而后能够双向使用它:

  • 根据用户/浏览器发起的URL 请求,它调用正确的Django 视图,并从URL 中提取它的参数须要的值。

  • 根据Django 视图的标识和将要传递给它的参数的值,获取与之关联的URL。

第一种方式是咱们在前面的章节中一直讨论的用法。第二种方式叫作反向解析URL、反向URL 匹配、反向URL 查询或者简单的URL 反查。

在须要URL 的地方,对于不一样层级,Django 提供不一样的工具用于URL 反查:

  • 在模板中:使用url 模板标签。

  • 在Python 代码中:使用django.core.urlresolvers.reverse() 函数。

  • 在更高层的与处理Django 模型实例相关的代码中:使用get_absolute_url() 方法。

例子

考虑下面的URLconf:

from django.conf.urls import url

from . import views

urlpatterns = [
    #...
    url(r'^articles/([0-9]{4})/$', views.year_archive, name='news-year-archive'),
    #...
]

根据这里的设计,某一年nnnn对应的归档的URL是/articles/nnnn/

你能够在模板的代码中使用下面的方法得到它们:

<a href="{% url 'news-year-archive' 2012 %}">2012 Archive</a>

<ul>
{% for yearvar in year_list %}
<li><a href="{% url 'news-year-archive' yearvar %}">{{ yearvar }} Archive</a></li>
{% endfor %}
</ul>

在Python 代码中,这样使用:

from django.core.urlresolvers import reverse
from django.http import HttpResponseRedirect

def redirect_to_year(request):
    # ...
    year = 2006
    # ...
    return HttpResponseRedirect(reverse('news-year-archive', args=(year,)))

若是出于某种缘由决定按年归档文章发布的URL应该调整一下,那么你将只须要修改URLconf 中的内容。

在某些场景中,一个视图是通用的,因此在URL 和视图之间存在多对一的关系。对于这些状况,当反查URL 时,只有视图的名字还不够。请阅读下一节来了解Django 为这个问题提供的解决办法。

命名URL 模式

为了完成上面例子中的URL 反查,你将须要使用命名的URL 模式。URL 的名称使用的字符串能够包含任何你喜欢的字符。不仅限制在合法的Python 名称。

当命名你的URL 模式时,请确保使用的名称不会与其它应用中名称冲突。若是你的URL 模式叫作comment,而另一个应用中也有一个一样的名称,当你在模板中使用这个名称的时候不能保证将插入哪一个URL。

在URL 名称中加上一个前缀,好比应用的名称,将减小冲突的可能。咱们建议使用myapp-comment 而不是comment

URL 命名空间

简介

URL 命名空间容许你反查到惟一的命名URL 模式,即便不一样的应用使用相同的URL 名称。第三方应用始终使用带命名空间的URL 是一个很好的实践(咱们在教程中也是这么作的)。相似地,它还容许你在一个应用有多个实例部署的状况下反查URL。换句话讲,由于一个应用的多个实例共享相同的命名URL,命名空间将提供一种区分这些命名URL 的方法。

在一个站点上,正确使用URL 命名空间的Django 应用能够部署屡次。例如,django.contrib.admin 具备一个AdminSite 类,它容许你很容易地部署多个管理站点的实例。在下面的例子中,咱们将讨论在两个不一样的地方部署教程中的polls 应用,这样咱们能够为两种不一样的用户(做者和发布者)提供相同的功能。

一个URL 命名空间有两个部分,它们都是字符串:

应用命名空间

它表示正在部署的应用的名称。一个应用的每一个实例具备相同的应用命名空间。例如,能够预见Django 的管理站点的应用命名空间是'admin'。

实例命名空间

它表示应用的一个特定的实例。实例的命名空间在你的所有项目中应该是惟一的。可是,一个实例的命名空间能够和应用的命名空间相同。它用于表示一个应用的默认实例。例如,Django 管理站点实例具备一个默认的实例命名空间'admin'。
URL 的命名空间使用':' 操做符指定。例如,管理站点应用的主页使用'admin:index'。它表示'admin' 的一个命名空间和'index' 的一个命名URL。

命名空间也能够嵌套。命名URL'sports:polls:index' 将在命名空间'polls'中查找'index',而poll 定义在顶层的命名空间'sports' 中。

反查带命名空间的URL

当解析一个带命名空间的URL(例如'polls:index')时,Django 将切分名称为多个部分,而后按下面的步骤查找:

  1. 首先,Django 查找匹配的应用的命名空间(在这个例子中为'polls')。这将获得该应用实例的一个列表。

  2. 若是有定义当前 应用,Django 将查找并返回那个实例的URL 解析器。当前 应用能够经过请求上的一个属性指定。但愿能够屡次部署的应用应该设置正在处理的request上的current_app 属性。

Changed in Django 1.8:

在之前版本的Django 中,你必须在用于渲染模板的每一个`Context` 或 `RequestContext`上设置`current_app` 属性。

当前应用还能够经过reverse() 函数的一个参数手工设定。

  1. 若是没有当前应用。Django 将查找一个默认的应用实例。默认的应用实例是实例命名空间应用命名空间 一致的那个实例(在这个例子中,polls 的一个叫作'polls' 的实例)。

  2. 若是没有默认的应用实例,Django 将该应用挑选最后部署的实例,无论实例的名称是什么。

  3. 若是提供的命名空间与第1步中的应用命名空间 不匹配,Django 将尝试直接将此命名空间做为一个实例命名空间查找。

若是有嵌套的命名空间,将为命名空间的每一个部分重复调用这些步骤直至剩下视图的名称还未解析。而后该视图的名称将被解析到找到的这个命名空间中的一个URL。

例子

为了演示解析的策略,考虑教程中polls 应用的两个实例:'author-polls' 和'publisher-polls'。假设咱们已经加强了该应用,在建立和显示投票时考虑了实例命名空间。

#urls.py

from django.conf.urls import include, url

urlpatterns = [
    url(r'^author-polls/', include('polls.urls', namespace='author-polls', app_name='polls')),
    url(r'^publisher-polls/', include('polls.urls', namespace='publisher-polls', app_name='polls')),
]
#polls/urls.py

from django.conf.urls import url

from . import views

urlpatterns = [
    url(r'^$', views.IndexView.as_view(), name='index'),
    url(r'^(?P<pk>\d+)/$', views.DetailView.as_view(), name='detail'),
    ...
]

根据以上设置,可使用下面的查询:

  • 若是其中一个实例是当前实例 —— 若是咱们正在渲染'author-polls' 实例的detail 页面 —— 'polls:index' 将解析成'author-polls' 实例的主页面;例以下面两个都将解析成"/author-polls/"。

在基于类的视图的方法中:

reverse('polls:index', current_app=self.request.resolver_match.namespace)

和在模板中:

{% url 'polls:index' %}

注意,在模板中的反查须要添加requestcurrent_app 属性,像这样:

def render_to_response(self, context, **response_kwargs):
    self.request.current_app = self.request.resolver_match.namespace
    return super(DetailView, self).render_to_response(context, **response_kwargs)
  • 若是没有当前实例 —— 若是咱们在站点的其它地方渲染一个页面 —— 'polls:index' 将解析到最后注册的polls的一个实例。由于没有默认的实例(命名空间为'polls'的实例),将使用注册的polls 的最后一个实例。它将是'publisher-polls',由于它是在urlpatterns中最后一个声明的。

  • 'author-polls:index' 将永远解析到 'author-polls' 实例的主页('publisher-polls' 相似)。

若是还有一个默认的实例 —— 例如,一个名为'polls' 的实例 —— 上面例子中惟一的变化是当没有当前实例的状况(上述第二种状况)。在这种状况下 'polls:index' 将解析到默认实例而不是urlpatterns 中最后声明的实例的主页。

URL 命名空间和被包含的URLconf

被包含的URLconf 的命名空间能够经过两种方式指定。

首先,在你构造你的URL 模式时,你能够提供 应用 和 实例的命名空间给include() 做为参数。例如:

url(r'^polls/', include('polls.urls', namespace='author-polls', app_name='polls')),

这将包含polls.urls 中定义的URL 到应用命名空间 'polls'中,其实例命名空间为'author-polls'。

其次,你能够include 一个包含嵌套命名空间数据的对象。若是你include() 一个url() 实例的列表,那么该对象中包含的URL 将添加到全局命名空间。然而,你还能够include() 一个3个元素的元组:

(<list of url() instances>, <application namespace>, <instance namespace>)

例如:

from django.conf.urls import include, url

from . import views

polls_patterns = [
    url(r'^$', views.IndexView.as_view(), name='index'),
    url(r'^(?P<pk>\d+)/$', views.DetailView.as_view(), name='detail'),
]

url(r'^polls/', include((polls_patterns, 'polls', 'author-polls'))),

这将include 命名的URL 模式到给定的应用和实例命名空间中。

例如,Django 的管理站点部署的实例叫AdminSiteAdminSite 对象具备一个urls 属性:一个3元组,包含管理站点中的全部URL 模式和应用的命名空间'admin'以及管理站点实例的名称。你include()到你项目的urlpatterns 中的是这个urls 属性。

请确保传递一个元组给include()。若是你只是传递3个参数:include(polls_patterns, 'polls', 'author-polls'),Django 不会抛出一个错误,可是根据include() 的功能,'polls' 将是实例的命名空间而'author-polls' 将是应用的命名空间,而不是反过来的。

译者:Django 文档协做翻译小组,原文:URLconfs

本文以 CC BY-NC-SA 3.0 协议发布,转载请保留做者署名和文章出处。

Django 文档协做翻译小组人手紧缺,有兴趣的朋友能够加入咱们,彻底公益性质。交流群:467338606。

相关文章
相关标签/搜索