Django 官网最新 Tutorial 渣翻 - Part 2

上一节: Django 官网最新 Tutorial 渣翻 - Part 1

手写第一个Django应用, 第二部分

我们本节开始配置数据库,创建你的第一个model,并且快速了解下Django的自动生成的 admin (后台管理)页面.

数据库配置

打开 mysite/settings.py, 这是一个普通的python模块, 包含了Django配置的模块级变量。
默认情况下, Django 用的时 SQLite, 如果你是数据库新手, 或者仅仅想试一下Django. 这是最简单的选择, python 自带了SQLite模块, 所以无需再装其他东西, 当你真正想写一个项目时,你可能需要用到功能更丰富的数据库,比如说PostgreSQL,避免后面面临数据库切换的头疼问题.

如果你想用其他的数据库,请先安装好相应的数据库软件 ,并修改settings文件中DATABASES中 'default' 键的值,用于连接你的数据库:

  • ENGINE
    可以是’django.db.backends.sqlite3’或者’django.db.backends.postgresql’,’django.db.backends.mysql’, or ’django.db.backends.oracle’,当然 其它可用的也行.
  • NAME
    数据库的名字。如果你使用的是默认的SQLite,那么数据库将作为一个文件将存放在你的本地机器内,NAME应该是这个文件的完整绝对路径,包括文件名。设置中的默认值os.path.join(BASE_DIR, ’db.sqlite3’),将把该文件储存在你的项目目录下。

如果你不是用SQLite,那你需要配置其他的设置,如 USER, PASSWORD, and HOST 都是必需的. 详细请看 DATABASES相关.

SQLite外的其他数据库:
在使用非SQLite的数据库时,请务必首先在数据库提示符交互模式下创建数据库,你可以使用命令:“CREATE DATABASE database_name;” 确保你在settings文件中提供的数据库用户具有创建数据库表的权限,因为在接下来的教程中,我们需要自动创建一个test数据库。 如果你使用的是SQLite,那么你无需做任何预先配置,数据库文件会自动创建

在修改settings文件时,请顺便将TIME_ZONE设置为你所在的时区。(天朝: TIME_ZONE = 'UTC')

同时,请注意settings文件中顶部的INSTALLED_APPS设置项。它保存了所有的在当前项目中被激活的Django应用。你必须将你自定义的app注册在这里。每个应用可以被多个项目使用,而且你可以打包和分发给其他人在他们的项目中使用。

默认情况,INSTALLED_APPS 中包含以下应用,它们都是Django自带的:

以上应用默认会包含,对于大多数项目都是需要的.

上面的每个应用都至少需要使用一个数据库表,所以在使用它们之前我们需要在数据库中创建这些表。使用这个命令:

python manage.py migrate

migrate命令将根据INSTALLED_APPS、mysite/settings.py的数据库设置以及app的数据库迁移(稍后会讲到)信息创建相关的表 ,你将看到每一个数据库消息的信息。如果你感兴趣,可以在你的数据库命令行下输入:\dt (PostgreSQL), SHOW TABLES; (MySQL), 或 .schema (SQLite), 或SELECT TABLE_NAME FROM USER_TABLES; (Oracle) 来列出 Django 所创建的表。

关于简化
就像我们上面说的,默认情况下会自带那些公共应用,但不是每一个应用都需要这么多。如果你不需要其中一个或所有的,你可以在migrate前,将INSTALLED_APPS内注释掉或者删除对应的行。 migrate命令只会针对INSTALL_APPS进行数据库迁移.


创建Model

现在我们开始定义models - 本质上,就是你的数据库结构,和其他的一些元数据信息.

哲(♂)学
model就是你唯一明确的真实数据源。Django 遵循 DRY Principle.目的是让你可以在一个地方定义你的数据模型, 从中生成数据.

在我们的投票应用中,我们将创建两个模型:Question and Choice。Question表包含问题和发布日期两个字段。Choice表有两个字段:选择文本和投票计数。每一条choice都关联到一条Question。

这些概念都由python的类来定义。编辑polls/models.py,如下:

polls/models.py

from django.db import models


class Question(models.Model):
    question_text = models.CharField(max_length=200)
    pub_date = models.DateTimeField('date published')


class Choice(models.Model):
    question = models.ForeignKey(Question, on_delete=models.CASCADE)
    choice_text = models.CharField(max_length=200)
    votes = models.IntegerField(default=0)

以上代码简单明了。每一个model都由一个类来定义,该类是django.db.models.Model的子类。每个类都由数个变量组成,分别对应了数据库里的字段。

每一个字段都是Field 类的实例 -- 如 CharField是字符串字段,DateTimeField是时间字段。这告诉Django每个字段用来存储什么类型的数据。

每个Field的实例名(如question_text 或 pub_date)就是字段的名字名,一个机器友好型的名字。在代码里你会用到它,而数据库里用它来作字段名。

每个Filed里的第一个可选的位置参数,常常被用来指定一个可读性好的名字。这在Django的一些内部机制中有所用处,也可以兼做文档。如果没有指定这个字段,Django会默认使用机器可读的名字。在这个例子中,我们只为pub_data定义了友好的名字。model的其他字段已经通俗易懂。

有一些字段有必选参数。例如,CharField要求你给它一个max_length。这个参数不仅在建表时用到,在数据验证中也会用到,我们稍后会看到。

Field 还具有各种可选位置参数。在这个例子中,我们设置votes字段的default参数为0。

最后,注意我们使用ForeignKey定义了一个外键关联。它告诉Django每个Choice都只关联一个Question。Django支持所有常见的数据库关联:多对一、多对多和一对一。


激活 model

这简短的model代码给了Django许多信息。有了它,Django就可以:

  • 为该应用创建数据库表(CREATE TABLE 语句)。
  • 创建一个访问数据库的 python API 来访问Question对象和Choice对象。

但是,我们首先得告诉项目:polls应用已经安装。

哲学
Django 应用是“插件化”的,即可以在多个项目中使用同一个应用,也可以分发这些应用, 因为它们不需要与某个特定的Django安装绑定。

为了将App整合进项目里,我们需要在 settings.py 的 INSTALLED_APPS 里添加该 App 的引用,PollsConfig 类在 polls/apps.py 文件中,所以按照点来分割路径就是polls.apps.PollsConfig,编辑mysite/settings.py文件,在 INSTALLED_APPS中添加按点分割的路径,如下:

mysite/settings.py

INSTALLED_APPS = [
    'polls.apps.PollsConfig',
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
]

现在Django就知道他整合了polls应用。我们输入以下命令:

python manage.py makemigrations polls

你可以看到类似以下的输出:

Migrations for 'polls':
  polls/migrations/0001_initial.py:
    - Create model Choice
    - Create model Question
    - Add field question to choice

通过运行makemigrations,你将告诉Django你已经对你的models做了一些修改(在本例中,你创建了新的model类),这些修改会作为migration记录来存储。

Migrations 就是Django存储的models修改记录(也就是数据库表) - 他们只是在磁盘的文件, 你可以查看刚刚创建新model时产生的migration, 他们在polls/migrations/0001_initial.py文件里, 别担心, 你无需每次都去看一下, 但是他们是可编辑的, 以便有时候你想修改他们.

There’s a command that will run the migrations for you and manage your database schema automatically - that’s called migrate, and we’ll come to it in a moment - but first, let’s see what SQL that migration would run. The sqlmigrate command takes migration names and returns their SQL:

有一个命令可以运行这些migrations文件来自动管理你的数据库表, -- 它就是migrate, 我们待会儿会用到它 -- 但首先我们看一下,那些migration会执行哪些sql语句。sqlmigrate 命令接收migration文件名,并返回sql语句:

 python manage.py sqlmigrate polls 0001

你将看到类似以下输出(我们重新排版了下)

BEGIN;
--
-- Create model Choice
--
CREATE TABLE "polls_choice" (
    "id" serial NOT NULL PRIMARY KEY,
    "choice_text" varchar(200) NOT NULL,
    "votes" integer NOT NULL
);
--
-- Create model Question
--
CREATE TABLE "polls_question" (
    "id" serial NOT NULL PRIMARY KEY,
    "question_text" varchar(200) NOT NULL,
    "pub_date" timestamp with time zone NOT NULL
);
--
-- Add field question to choice
--
ALTER TABLE "polls_choice" ADD COLUMN "question_id" integer NOT NULL;
ALTER TABLE "polls_choice" ALTER COLUMN "question_id" DROP DEFAULT;
CREATE INDEX "polls_choice_7aa0f6ee" ON "polls_choice" ("question_id");
ALTER TABLE "polls_choice"
  ADD CONSTRAINT "polls_choice_question_id_246c99a640fbbd72_fk_polls_question_id"
    FOREIGN KEY ("question_id")
    REFERENCES "polls_question" ("id")
    DEFERRABLE INITIALLY DEFERRED;

COMMIT;

请注意以下几点:

  • 输出的具体内容会依据你使用的数据库而不同。 以上例子使用的数据库是PostgreSQL。
  • 表名是自动生成的,由app的名字(polls)和模型名字的小写字母组合而成 —— question和choice。(你可以重写这个行为。)
  • 主键(IDs)是自动添加的。 (你也可以重写这个行为。)
  • 按照惯例,Django会在外键的字段名后面添加 "_id"。(是的,你依然可以重写这个行为。)
  • 外键关系由FOREIGN KEY约束显式声明。不用在意DEFERRABLE 部分;它只是告诉PostgreSQL直到事务的最后再执行外键关联。
  • 这些SQL语句是针对你所使用的数据库定制的,所以会为你自动处理某些数据库所特有的字段例如auto_increment(MySQL)、 serial (PostgreSQL)或integer primary key autoincrement (SQLite) 。在处理字段名的引号时也是如此 —— 例如,使用双引号还是单引号。
  • sqlmigrate 命令并不会在你的数据库上真正运行迁移文件 —— 它只是把Django 认为需要的SQL打印在屏幕上以让你能够看到。 这对于检查Django将要进行的数据库操作或者你的数据库管理员需要这些SQL脚本是非常有用的。

如果有兴趣,你还可以运行python manage.py check, 它会检查你的项目中的模型是否存在问题,而不用执行迁移或者操作数据库

现在,再次运行 migrate, 在你的数据库中创建model所对应的表:

$ python manage.py migrate
Operations to perform:
  Apply all migrations: admin, auth, contenttypes, polls, sessions
Running migrations:
  Rendering model states... DONE
  Applying polls.0001_initial... OK

migrate 命令将找到所有尚未被应用的迁移文件(Django有专门的一张表 - django_migrations, 来追踪哪些迁移文件已经被应用过),并且在你的数据库上运行它们 —— 本质上来讲,就是同步你对models的修改到数据库表中。)

Migrations 功能非常强大,在你开发项目的过程中,当你在更改models后,无需删除数据库或表再重建。它专注于热升级你的数据库且不丢数据。我们将在本教程的后续章节中详细介绍,但是现在,请记住模型变更的三个基本步骤:

将生成和应用Migrations命令分开, 是因为你可能需要将迁移文件提交到你的版本控制器并跟随你的应用, 这样做不仅使开发变得更加简单,而且对其他开发者以及线上环境非常有用。

阅读 django-admin documentation 来详细了解manage.py 工具所能做的事情。

玩玩API

现在,我们切换到python shell的交互模式,感受一下Django给的API。用如下命令来调出python shell:

$ python manage.py shell

我们使用这个(python manage.py shell)而不是直接进入python交互环境(python), 因为manage.py设置了DJANGO_SETTINGS_MODULE环境变量. 它告诉Django导入了mysite/settings.py模块.

一旦你进入了这个python shell, 那我们就开始探索这些database API吧:

>>> from polls.models import Choice, Question  # 导入我们刚刚创建的model类.

# 目前还没有questions对象
>>> Question.objects.all()
<QuerySet []>

# 创建一个Questions对象
# 默认的设置文件中是支持时区的, 
# 所以Django会通过 tzinfo 模块给 pub_date 一个datetime.
# 使用timezone.now(), 它会替代datetime.datetime.now()完成时间设置工作.
>>> from django.utils import timezone
>>> q = Question(question_text="What's new?", pub_date=timezone.now())

# 调用 save() 来将对象保存进数据库
>>> q.save()

# 现在他有一个id了
>>> q.id
1

# 通过访问Python属性的方式来访问model字段的值
>>> q.question_text
"What's new?"
>>> q.pub_date
datetime.datetime(2012, 2, 26, 13, 0, 0, 775217, tzinfo=<UTC>)

# 通过改变对象属性的方式来改变字段的值, 改变后需要调用save()才能生效
>>> q.question_text = "What's up?"
>>> q.save()

# objects.all() 显示所有在数据库中的questions对象
>>> Question.objects.all()
<QuerySet [<Question: Question object (1)>]>

秋德马嘚, <Question: Question object (1)>是什么鬼? 这可不是一种友好的对象表示方式, 让我们编辑Question 模型(在 polls/models.py 文件中)来完善他, 对Question and Choice类都添加__str__() 方法:

polls/models.py

from django.db import models

class Question(models.Model):
    # ...
    def __str__(self):
        return self.question_text

class Choice(models.Model):
    # ...
    def __str__(self):
        return self.choice_text

给你的模型类添加__str__() 方法很重要,不仅会使你自己在使用交互式命令行时看得更加方便,而且会在Django自动生成的管理界面中对模型对象使用这种表示。

这是非常普通的Python方法。现在让我们来演示一下如何添加自定义方法:

polls/models.py

import datetime

from django.db import models
from django.utils import timezone


class Question(models.Model):
    # ...
    def was_published_recently(self):
        return self.pub_date >= timezone.now() - datetime.timedelta(days=1)

注意import datetimefrom django.utils import timezone分别引用Python 的标准datetime 模块和 Django的django.utils.timezone中时区相关的工具, 如果你不了解Python中时区的处理方法,你可以在time zone support docs中了解更多的知识。

保存这些改动,然后通过python manage.py shell另外打开一个新的Python 交互式shell:

>>> from polls.models import Choice, Question

# 确保我们添加的__str__方法生效了
>>> Question.objects.all()
<QuerySet [<Question: What's up?>]>

# Django提供大量的数据库查询API, 通过关键字参数的方式来调用.
>>> Question.objects.filter(id=1)
<QuerySet [<Question: What's up?>]>
>>> Question.objects.filter(question_text__startswith='What')
<QuerySet [<Question: What's up?>]>

# 得到今年发布的question对象
>>> from django.utils import timezone
>>> current_year = timezone.now().year
>>> Question.objects.get(pub_date__year=current_year)
<Question: What's up?>

# 当请求的ID不存在时, 会抛出异常
>>> Question.objects.get(id=2)
Traceback (most recent call last):
    ...
DoesNotExist: Question matching query does not exist.

# 大多数情况下都是通过主键进行查询, 所以Django提供一个关于主键查询的快捷方式
# 以下与Question.objects.get(id=1)是一样的.
>>> Question.objects.get(pk=1)
<Question: What's up?>

# 确保我们自定义方法生效
>>> q = Question.objects.get(pk=1)
>>> q.was_published_recently()
True

# 每个question对象都有多个choice对象(一对多), 调用 create 来构造一个新的 choice 对象,
# 它会使用 INSERT 语句, 添加choice对象的一系列字段, 然后返回一个choice对象, 
# django通过API来创建一个含有"反向关联"关系的外键集合.首先, 我们要获取指定的question对象
>>> q = Question.objects.get(pk=1)

# 显示所有与choices对象有关系的对象集合 -- 目前没有
>>> q.choice_set.all()
<QuerySet []>

# 创建3个choice对象
# (译者注: choice类中定义了question的外键, 所以question可以通过`类名_set`来反向关联, 
# 也可以通过定义外键时设置的related_name参数的值来进行反向关联)
>>> q.choice_set.create(choice_text='Not much', votes=0)
<Choice: Not much>
>>> q.choice_set.create(choice_text='The sky', votes=0)
<Choice: The sky>
>>> c = q.choice_set.create(choice_text='Just hacking again', votes=0)

# Choice 对象有API来访问他们关联的 Question 对象
>>> c.question
<Question: What's up?>

# 反之亦然: Question对象也可以访问Choice对象.
>>> q.choice_set.all()
<QuerySet [<Choice: Not much>, <Choice: The sky>, <Choice: Just hacking again>]>
>>> q.choice_set.count()
3

# The API automatically follows relationships as far as you need.
# Use double underscores to separate relationships.
# This works as many levels deep as you want; there's no limit.
# 我们可以使用双下划线来调用API自动的处理关系, 其中关系的深度没有限制.(这段感觉没翻译好, 原文在)
# 如下, 找到所有 pub_date 为今年, 并且可以是属于任何Question的Choice(这里重用了我们在上面创建的`current_year`变量.
>>> Choice.objects.filter(question__pub_date__year=current_year)
<QuerySet [<Choice: Not much>, <Choice: The sky>, <Choice: Just hacking again>]>

# 我们可以使用delete()来删除这些choices QuerySet中的一个.
>>> c = q.choice_set.filter(choice_text__startswith='Just hacking')
>>> c.delete()

更多关于模型相关的信息,请查看 Accessing related objects。更多关于如何在这些API中使用双下划线来执行字段查询的信息,请查看 Field lookups.。想了解所有数据库API的信息,请查看我们的 Database API reference.


介绍 Django Admin

哲♂学
为你的团队或客户编写增删改查的admin管理系统是非常乏味的,也没有多少创造性价值。基于这考虑,Django自动的为你的models创建了管理接口。
Django最初是为新闻站点开发的,新闻发布者和公共站点界限明显。管理员通过这个系统添加新闻、时间、体育赛事等,这些信息展示在公共网站上。Django提供了一个统一接口给站点管理员来编辑内容。
admin系统并不是为了访客设计的,只对站点管理员开放。

创建管理员

首先我们需要创建一个能登录admin站点的用户,运行以下命令:

python manage.py createsuperuser

输入你喜欢的名字,按下回车:

Username: admin

然后会提示输入你的邮箱:

Email address: admin@example.com

最后一步是输入密码。你需要输入两次,第二次作为第一次的进一步确认:

Password: **********
Password (again): *********
Superuser created successfully.

启动开发服务器

Django的admin站点默认是激活的。让我们开启开发服务器来探索一下:

如果服务器没开启,用下面的方法来启动:

python manage.py runserver

现在,打开web浏览器,本地域名访问“/admin/” -- 比如说http://127.0.0.1:8000/admin/。你可以看到admin的登录界面:

translation 默认是开启的,登录界面显示的应该是你的母语,这取决于你的浏览器设置,如果Django有这种翻译版本的话。


进入admin页面

现在,你可以用你刚刚创建的超级用户来登录了。登录后你应该可以看到如下index页面:


当前已经有两个可编辑的内容:groups和users。它们是 django.contrib.auth模块提供的身份认证框架。

在admin中注册投票应用

但是我们创建的投票应用哪去了呢?它并没有在admin的首页里。我们还有一件事要做:告诉admin站点Question要有admin交互界面。编辑polls/admin.py文件,代码如下:

polls/admin.py

from django.contrib import admin

from .models import Question

admin.site.register(Question)

探索Admin功能

Question注册之后,Django就知道要将它展示在admin的首页了。



点击“Questions”。 现在,你会进入Question的“变更列表”。 这个界面显示了数据库中的所有question,你可以选择一个来更改它。 我们在前面创建的“What’s up?” Question对象也在这里:



点击“What’s up?” Question对象来编辑它:

注意事项:

  • 这个表单是根据Question 模型文件自动生成的。
  • 模型中不同类型的字段(DateTimeField, CharField)会对应相应的HTML输入控件。每一种类型的字段,Django管理站点都知道如何显示它们。
  • 每个DateTimeField 字段都会有个方便的JavaScript快捷键。Date有个“Today”的快捷键和一个弹出式日历,time栏有个“Now”的快捷键和一个列出常用时间选项的弹出式窗口。

界面的底部提供了几个按钮:

  • Save —— 保存更改,并返回当前类型对象的变更列表界面。
  • Save and continue editing —— 保存更改并且重新载入当前对象的管理界面。
  • Save and add another —— 保存更改并且载入一个当前类型对象的新的、空白的表单。
  • Delete —— 显示一个删除确认界面。

如果“Date published”的值和你在教程1中创建这个Question对象时的时间不相符,可能是因为你忘记将TIME_ZONE设置为你当地的时区。修改它,然后刷新界面,再次检查一下是否正确。

通过“Today”和“Now”这两个快捷键更改“Date published”字段。,然后点击 “Save and continue editing”。然后点击右上角的“History”按钮。 你将看到一个页面,列出了通过Django管理界面对此对象所做的全部更改的清单,包含有时间戳和修改人的姓名等信息:

当你适应了models的API,熟悉了admin站点后,请移步 part 3 of this tutorial 来学习如何给投票应用编写更多的views函数.

下一节: Django 官网最新 Tutorial 渣翻 - Part 3

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,271评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,275评论 2 380
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,151评论 0 336
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,550评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,553评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,559评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,924评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,580评论 0 257
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,826评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,578评论 2 320
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,661评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,363评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,940评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,926评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,156评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,872评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,391评论 2 342

推荐阅读更多精彩内容