第六章 跟蹤用戶動作

6 跟蹤用戶動作

在上一章中,你用jQuery實現(xiàn)了AJAX視圖,并構建了一個分享其它網(wǎng)站內(nèi)容的JavaScript書簽工具。

本章中,你將學習如何構建關注系統(tǒng)和用戶活動流。你會了解Django的信號(signals)如何工作,并在項目中集成Redis快速I/O存儲,用于存儲項視圖。

本章將會覆蓋以下知識點:

  • 用中介模型創(chuàng)建多對多關系
  • 構建AJAX視圖
  • 創(chuàng)建活動流應用
  • 為模型添加通用關系
  • 優(yōu)化關聯(lián)對象的QuerySet
  • 使用信號進行反規(guī)范化計數(shù)
  • 在Redis中存儲項的瀏覽次數(shù)

6.1 構建關注系統(tǒng)

我們將在項目中構建關注系統(tǒng)。用戶可以相互關注,并跟蹤其他用戶在平臺分享的內(nèi)容。用戶之間是多對多的關系,一個用戶可以關注多個用戶,也可以被多個用戶關注。

6.1.1 用中介模型創(chuàng)建多對多關系

在上一章中,通過在一個關聯(lián)模型中添加ManyToManyField,我們創(chuàng)建了多對多的關系,并讓Django為這種關系創(chuàng)建了一張數(shù)據(jù)庫表。這種方式適用于大部分情況,但有時候你需要為這種關系創(chuàng)建一個中介模型。當你希望存儲這種關系的額外信息(比如關系創(chuàng)建的時間,或者描述關系類型的字段)時,你需要創(chuàng)建中介模型。

我們將創(chuàng)建一個中介模型用于構建用戶之間的關系。我們使用中介模型有兩個原因:

  • 我們使用的是Django提供的User模型,不想修改它。
  • 我們想要存儲關系創(chuàng)建的時間。

編輯account應用的models.py文件,添加以下代碼:

from django.contrib.auth.models import User

class Contact(models.Model):
    user_from = models.ForeignKey(User, related_name='rel_from_set')
    user_to = models.ForeignKey(User, related_name='rel_to_set')
    created = models.DateTimeField(auto_now_add=True, db_index=True)

    class Meta:
        ordering = ('-created', )

    def __str__(self):
        return '{} follows {}'.format(self.user_from, self.user_to)

我們將把Contact模型用于用戶關系。它包括以下字段:

  • user_from:指向創(chuàng)建關系的用戶的ForeignKey
  • user_to:指向被關注用戶的ForeignKey
  • created:帶auto_new_add=TrueDateTimeField字段,存儲創(chuàng)建關系的時間

數(shù)據(jù)庫會自動在ForeignKey字段上創(chuàng)建索引。我們在created字段上用db_index=True創(chuàng)建數(shù)據(jù)庫索引。當用這個字段排序QuerySet時,可以提高查詢效率。

通過ORM,我們可以創(chuàng)建用戶user1關注用戶user2的關系,如下所示:

user1 = User.objects.get(id=1)
user2 = User.objects.get(id=2)
Contact.objects.create(user_from=user1, user_to=user2)

關聯(lián)管理器rel_from_setrel_to_set會返回Contact模型的QuerySet。為了從User模型訪問關系的另一端,我們希望User模型包括一個ManyToManyField,如下所示:

following = models.ManyToManyField(
    'self',
    through=Contact,
    related_name='followers',
    symmetrical=False)

這個例子中,通過在ManyToManyField字段中添加through=Contact,我們告訴Django使用自定義的中介模型。這是從User模型到它自身的多對多關系:我們在ManyToManyField字段中引用'self'來創(chuàng)建到同一個模型的關系。

當你在多對多的關系中需要額外字段時,可以在關系兩端創(chuàng)建帶ForeignKey的自定義模型。在其中一個關聯(lián)模型中添加ForeignKey,并通過through參數(shù)指向中介模型,讓Django使用該中介模型。

如果User模型屬于我們的應用,我們就可以把上面這個字段添加到模型中。但是我們不能直接修改它,因為它屬于django.contrib.auth應用。我們將采用略微不同的方法:動態(tài)的添加該字段到User模型中。編輯account應用的models.py文件,添加以下代碼:

User.add_to_class('following', 
    models.ManyToManyField('self', 
        through=Contact, 
        related_name='followers', 
        symmetrical=False))

在這段代碼中,我們使用Django模型的add_to_class()方法添加monkey-patchUser模型中。不推薦使用add_to_class()為模型添加字段。但是,我們在這里使用這種方法有以下幾個原因:

  • 通過Django ORM的user.followers.all()user.following.all(),可以簡化檢索關聯(lián)對象。我們使用Contact中介模型,避免涉及數(shù)據(jù)庫連接(join)的復雜查詢。如果我們在Profile模型中定義關系,則需要使用復雜查詢。
  • 這個多對多關系的數(shù)據(jù)庫表會使用Contact模型創(chuàng)建。因此,動態(tài)添加的ManyToManyField不會對Django的User模型數(shù)據(jù)庫做任何修改。
  • 我們避免創(chuàng)建自定義的用戶模型,充分利用Django內(nèi)置的User模型。

記住,在大部分情況下都推薦使用添加字段到我們之前創(chuàng)建的Profile模型,而不是添加monkey-patchUser模型。Django也允許你使用自定義的用戶模型。如果你想使用自定義的用戶模型,請參考文檔。

你可以看到,關系中包括symmetrical=False。當你定義ManyToManyField到模型自身時,Django強制關系是對稱的。在這里,我們設置symmetrical=False定義一個非對稱關系。也就是說,如果我關注了你,你不會自動關注我。

當使用中介模型定義多對多關系時,一些關系管理器的方法將不可用,比如add(),create(),remove()。你需要創(chuàng)建或刪除中介模型來代替。

執(zhí)行以下命令為account應用生成初始數(shù)據(jù)庫遷移:

python manage.py makemigrations account

你會看到以下輸出:

Migrations for 'account':
  account/migrations/0002_contact.py
    - Create model Contact

現(xiàn)在執(zhí)行以下命令同步數(shù)據(jù)庫和應用:

python manage.py migrate account

你會看到包括下面這一行的輸出:

Applying account.0002_contact... OK

現(xiàn)在Contact模型已經(jīng)同步到數(shù)據(jù)庫中,我們可以在用戶之間創(chuàng)建關系了。但是我們的網(wǎng)站還不能瀏覽用戶,或者查看某個用戶的個人資料。讓我們?yōu)?code>User模型創(chuàng)建列表和詳情視圖。

6.1.2 為用戶資料創(chuàng)建列表和詳情視圖

打開account應用的views.py文件,添加以下代碼:

from django.shortcuts import get_object_or_404
from django.contrib.auth.models import User

@login_required
def user_list(request):
    users = User.objects.filter(is_active=True)
    return render(request, 'account/user/list.html', {'section': 'people', 'users': users})

@login_required
def user_detail(request, username):
    user = get_object_or_404(User, username=username, is_active=True)
    return render(request, 'account/user/detail.html', {'section': 'people', 'user': user})

這是User對象簡單的列表和詳情視圖。user_list視圖獲得所有激活的用戶。Django的User模型包括一個is_active標記,表示用戶賬戶是否激活。我們通過is_active=True過濾查詢,只返回激活的用戶。這個視圖返回了所有結果,你可以跟image_list視圖那樣,為它添加分頁。

user_detail視圖使用get_object_or_404()快捷方法,檢索指定用戶名的激活用戶。如果沒有找到指定用戶名的激活用戶,該視圖返回HTTP 404響應。

編輯account應用的urls.py文件,為每個視圖添加URL模式,如下所示:

urlpatterns= [
    # ...
    url(r'^users/$', views.user_list, name='user_list'),
    url(r'^users/(?P<username>[-\w]+)/$', views.user_detail, name='user_detail'),
]

我們將使用user_detail URL模式為用戶生成標準URL。你已經(jīng)在模型中定義過get_absolute_url()方法,為每個對象返回標準URL。另一種方式是在項目中添加ABSOLUTE_URL_OVERRIDES設置。

編輯項目的settings.py文件,添加以下代碼:

ABSOLUTE_URL_OVERRIDES = {
    'auth.user': lambda u: reverse_lazy('user_detail', args=[u.username])
}

Django為ABSOLUTE_URL_OVERRIDES設置中的所有模型動態(tài)添加get_absolute_url()方法。這個方法返回給定模型的對應URL。我們?yōu)榻o定用戶返回user_detail URL?,F(xiàn)在你可以在User實例上調(diào)用get_absolute_url()方法獲得相應的URL。用python manage.py shell打開Python終端,執(zhí)行以下命令測試:

>>> from django.contrib.auth.models import User
>>> user = User.objects.latest('id')
>>> str(user.get_absolute_url())
'/account/users/Antonio/'

返回的結果是期望的URL。我們需要為剛創(chuàng)建的視圖創(chuàng)建模板。在account應用的templates/account/目錄中添加以下目錄和文件:

user/
    detail.html
    list.html

編輯account/user/list.html模板,添加以下代碼:

{% extends "base.html" %}
{% load thumbnail %}

{% block title %}People{% endblock %}

{% block content %}
    <h1>People</h1>
    <div id="people-list">
        {% for user in users %}
            <div class="user">
                <a href="{{ user.get_absolute_url }}">
                    {% thumbnail user.profile.photo "180x180" crop="100%" as im %}
                        ![]({{ im.url }})
                    {% endthumbnail %}
                </a>
                <div class="info">
                    <a href="{{ user.get_absolute_url }}" class="title">
                        {{ user.get_full_name }}
                    </a>
                </div>
            </div>
        {% endfor %}
    </div>
{% endblock %}

該模板列出網(wǎng)站中所有激活的用戶。我們迭代給定的用戶,使用sorl-thumbnail{% thumbnail %}模板標簽生成個人資料的圖片縮略圖。

打開項目的base.html文件,在以下菜單項的href屬性中包括user_list URL:

<li {% if section == "people" %}class="selected"{% endif %}>
    <a href="{% url "user_list" %}">People</a>
</li>

執(zhí)行python manage.py runserver命令啟動開發(fā)服務器,然后在瀏覽器中打開http://127.0.0.1/8000/account/users/。你會看到用戶列表,如下圖所示:

編輯account應用的account/user/detail.html模板,添加以下代碼:

{% extends "base.html" %}
{% load thumbnail %}

{% block title %}{{ user.get_full_name }}{% endblock %}

{% block content %}
    <h1>{{ user.get_full_name }}</h1>
    <div class="profile-info">
        {% thumbnail user.profile.photo "180x180" crop="100%" as im %}
            ![]({{ im.url }})
        {% endthumbnail %}
    </div>
    {% with total_followers=user.followers.count %}
        <span class="count">
            <span class="total">{{ total_followers }}</span>
            follower{{ total_followers|pluralize }}
        </span>
        <a href="#" data-id="{{ user.id }}" 
            data-action="{% if request.user in user.followers.all %}un{% endif %}follow" 
            class="follow button">
            {% if request.user not in user.followers.all %}
                Follow
            {% else %}
                Unfollow
            {% endif %}
        </a>
        <div id="image-list" class="image-container">
            {% include "images/image/list_ajax.html" with images=user.images_created.all %}
        </div>
    {% endwith %}
{% endblock %}

我們在詳情模板中顯示用戶個人資料,并使用{% thumbnail %}模板標簽顯示個人資料圖片。我們顯示關注者總數(shù)和一個用于follow/unfollow的鏈接。如果用戶正在查看自己的個人資料,我們會隱藏該鏈接,防止用戶關注自己。我們將執(zhí)行AJAX請求來follow/unfollow指定用戶。我們在<a>元素中添加data-iddata-action屬性,其中分別包括用戶ID和點擊鏈接時執(zhí)行的操作(關注或取消關注),這取決于請求該頁面的用戶是否已經(jīng)關注了這個用戶。我們用list_ajax.html模板顯示這個用戶標記過的圖片。

再次打開瀏覽器,點擊標記過一些圖片的用戶。你會看到個人資料詳情,如下圖所示:

6.1.3 構建關注用戶的AJAX視圖

我們將使用AJAX創(chuàng)建一個簡單視圖,用于關注或取消關注用戶。編輯account用于的views.py文件,添加以下代碼:

from django.http import JsonResponse
from django.views.decorators.http import require_POST
from common.decrorators import ajax_required
from .models import Contact

@ajax_required
@require_POST
@login_required
def user_follow(request):
    user_id = request.POST.get('id')
    action = request.POST.get('action')
    if user_id and action:
        try:
            user = User.objects.get(id=user_id)
            if action == 'follow':
                Contact.objects.get_or_create(user_from=request.user, user_to=user)
            else:
                Contact.objects.filter(user_from=request.user, user_to=user).delete()
            return JsonResponse({'status': 'ok'})
        except User.DoesNotExist:
            return JsonResponse({'status': 'ko'})
    
    return JsonResponse({'status': 'ko'})

user_follow視圖跟我們之前創(chuàng)建的image_like視圖很像。因為我們?yōu)橛脩舻亩鄬Χ嚓P系使用了自定義的中介模型,所以ManyToManyField自動生成的管理器的默認add()remove()方法不可用了。我們使用Contact中介模型創(chuàng)建或刪除用戶關系。

account應用的urls.py文件中導入你剛創(chuàng)建的視圖,然后添加以下URL模式:

url(r'^users/follow/$', views.user_follow, name='user_follow'),

確保你把這個模式放在user_detail模式之前。否則任何到/users/follow/的請求都會匹配user_detail模式的正則表達式,然后執(zhí)行user_detail視圖。記住,每一個HTTP請求時,Django會按每個模式出現(xiàn)的先后順序匹配請求的URL,并在第一次匹配成功后停止。

編輯account應用的user/detail.html模板,添加以下代碼:

{% block domready %}
    $('a.follow').click(function(e){
        e.preventDefault();
        $.post('{% url "user_follow" %}', {
            id: $(this).data('id'),
            action: $(this).data('action')
        },
        function(data){
            if (data['status'] == 'ok') {
                var previous_action = $('a.follow').data('action');

                // toggle data-action
                $('a.follow').data('action', previous_action == 'follow' ? 'unfollow' : 'follow');
                // toggle link text
                $('a.follow').text(previous_action == 'follow' ? 'Unfollow' : 'Follow');

                // update total followers
                var previous_followers = parseInt($('span.count .total').text())
                $('span.count .total').text(previous_action == 'follow' ? previous_followers+1 : previous_followers - 1);
            }
        });
    });
{% endblock %}

這段JavaScript代碼執(zhí)行關注或取消關注指定用戶的AJAX請求,同時切換follow/unfollow鏈接。我們用jQuery執(zhí)行AJAX請求,并根據(jù)之前的值設置data-action屬性和<a>元素的文本。AJAX操作執(zhí)行完成后,我們更新頁面顯示的關注總數(shù)。打開一個已存在用戶的詳情頁面,點擊FOLLOW鏈接,測試我們剛添加的功能。

6.2 構建通用的活動流應用

很多社交網(wǎng)站都會給用戶顯示活動流,讓用戶可以跟蹤其他用戶在平臺上做了什么?;顒恿魇且粋€或一組用戶最近執(zhí)行的活動列表。比如,F(xiàn)acebook的News Feed就是一個活動流。又或者用戶X標記了圖片Y,或者用戶X不再關注用戶Y。我們將構造一個活動流應用,讓每個用戶都可以看到他關注的用戶最近的操作。要實現(xiàn)這個功能,我們需要一個模型,存儲用戶在網(wǎng)站中執(zhí)行的操作,并提供簡單的添加操作的方式。

用以下命令在項目中創(chuàng)建actions應用:

django-admin startapp actions

在項目的settings.py文件的INSTALLED_APPS中添加actions,讓Django知道新應用已經(jīng)激活:

INSTALLED_APPS = (
    # ...
    'actions',
)

編輯actions應用的models.py文件,添加以下代碼:

from django.db import models
from django.contrib.auth.models import User

class Action(models.Model):
    user = models.ForeignKey(User, related_name='actions', db_index=True)
    verb = models.CharField(max_length=255)
    created = models.DateTimeField(auto_now_add=True, db_index=True)

    class Meta:
        ordering = ('-created', )

這是Actioin模型,用于存儲用戶活動。該模型的字段有:

  • user:執(zhí)行這個操作的用戶。這是一個指向Django的User模型的ForeignKey。
  • verb:描述用戶執(zhí)行的操作。
  • created:該操作創(chuàng)建的日期和時間。我們使用auto_now_add=True自動設置為對象第一次在數(shù)據(jù)庫中保存的時間。

通過這個基礎模型,我們只能存儲類似用戶X做了某些事情的操作。我們需要一個額外的ForeignKey字段,存儲涉及目標對象的操作,比如用戶X標記了圖片Y,或者用戶X關注了用戶Y。你已經(jīng)知道,一個普通的ForeignKey字段只能指向另一個模型。但是我們需要一種方式,讓操作的目標對象可以是任何一個已經(jīng)存在的模型的實例。這就是Django的contenttypes框架的作用。

6.2.1 使用contenttypes框架

Django的contenttypes框架位于django.contrib.contenttypes中。這個應用可以跟蹤項目中安裝的所有模型,并提供一個通用的接口與模型交互。

當你使用startproject命令創(chuàng)建新項目時,django.contrib.contenttypes已經(jīng)包括在INSTALLED_APPS設置中。它被其它contrib包(比如authentication框架和admin應用)使用。

contenttypes應用包括一個ContentType模型。這個模型的實例代表你的應用中的真實模型,當你的項目中安裝了一個新模型時,會自動創(chuàng)建一個新的ContentType實例。ContentType模型包括以下字段:

  • app_label:模型所屬應用的名字。它會自動從模型Meta選項的app_label屬性中獲得。例如,我們的Image模型屬于images應用。
  • model:模型的類名。
  • name:模型的人性化名字。它自動從模型Meta選項的verbose_name屬性中獲得。

讓我們看下如何與ContentType對象交互。使用python manage.py shell命令打開Python終端。通過執(zhí)行帶label_namemodel屬性的查詢,你可以獲得指定模型對應的ContentType對象,比如:

>>> from django.contrib.contenttypes.models import ContentType
>>> image_type = ContentType.objects.get(app_label='images',model='image')
>>> image_type
<ContentType: image>

你也可以通過調(diào)用ContentType對象的model_class()方法,反向查詢模型類:

>>> image_type.model_class()
<class 'images.models.Image'>

從指定的模型類獲得ContentType對象操作也很常見:

>>> from images.models import Image
>>> ContentType.objects.get_for_model(Image)
<ContentType: image>

這些只是使用contenttypes的幾個示例。Django提供了更多使用它們的方式。你可以在官方文檔學習contenttypes框架。

6.2.2 在模型中添加通用關系

在通用關系中,ContentType對象指向關系中使用的模型。在模型中設置通用關系,你需要三個字段:

  • 一個ForeignKey字段指向ContentType。這會告訴我們關系中的模型。
  • 一個存儲關聯(lián)對象主鍵的字段。通常這是一個PositiveIntegerField,來匹配Django自動生成的主鍵字段。
  • 一個使用上面兩個字段定義和管理通用關系的字段。contenttypes框架為此定義了GenericForeignKey字段。

編輯actions應用的models.py文件,如下所示:

from django.db import models
from django.contrib.auth.models import User
from django.contrib.contenttypes.models import ContentType
from django.contrib.contenttypes.fields import GenericForeignKey

class Action(models.Model):
    user = models.ForeignKey(User, related_name='actions', db_index=True)
    verb = models.CharField(max_length=255)

    target_ct = models.ForeignKey(ContentType, blank=True, null=True, related_name='target_obj')
    target_id = models.PositiveIntegerField(null=True, blank=True, db_index=True)
    target = GenericForeignKey('target_ct', 'target_id')
    created = models.DateTimeField(auto_now_add=True, db_index=True)

    class Meta:
        ordering = ('-created', )

我們在Action模型中添加了以下字段:

  • target_ct:一個指向ContentType模型的ForeignKey字段。
  • target_id:一個用于存儲關聯(lián)對象主鍵的PositiveIntegerField
  • target:一個指向由前兩個字段組合的關聯(lián)對象的GenericForeignKey字段。

Django不會在數(shù)據(jù)庫中為GenericForeignKey字段創(chuàng)建任何字段。只有target_cttarget_id字段會映射到數(shù)據(jù)庫的字段。因為這兩個字段都有blank=Truenull=True屬性,所以保存Action對象時target對象不是必需的。

使用通用關系有意義的時候,你可以使用它代替外鍵,讓應用更靈活。

執(zhí)行以下命令為這個應用創(chuàng)建初始的數(shù)據(jù)庫遷移:

python manage.py makemigrations actions

你會看到以下輸出:

Migrations for 'actions':
  actions/migrations/0001_initial.py
    - Create model Action

接著執(zhí)行以下命令同步應用和數(shù)據(jù)庫:

python manage.py migrate

這個命令的輸入表示新的數(shù)據(jù)庫遷移已經(jīng)生效:

Applying actions.0001_initial... OK

當我們把Action模型添加到管理站點。編輯actions應用的admin.py文件,添加以下代碼:

from django.contrib import admin
from .models import Action

class ActionAdmin(admin.ModelAdmin):
    list_display = ('user', 'verb', 'target', 'created')
    list_filter = ('created', )
    search_fields = ('verb', )

admin.site.register(Action, ActionAdmin)

你剛剛在管理站點注冊了Action模型。執(zhí)行python manage.py runserver命令啟動開服務器,然后在瀏覽器中打開http://127.0.0.1:8000/actions/action/add/。你會看到創(chuàng)建一個新的Action對象的頁面,如下圖所示:

正如你所看到的,只有target_cttarget_id字段映射到實際的數(shù)據(jù)庫字段,而GenericForeignKey沒有在這里出現(xiàn)。target_ct允許你選擇在Django項目中注冊的任何模型。使用target_ct字段的limit_choices_to屬性,可以讓contenttypes從一個限制的模型集合中選擇:limit_choices_to屬性允許你限制ForeignKey字段的內(nèi)容為一組指定的值。

actions應用目錄中創(chuàng)建一個utils.py文件。我們將定義一些快捷方法,快速創(chuàng)建Action對象。編輯這個新文件,添加以下代碼:

from django.contrib.contenttypes.models import ContentType
from .models import Action

def create_action(user, verb, target=None):
    action = Action(user=user, verb=verb, target=target)
    action.save()

create_action()方法允許我們創(chuàng)建Action對象,其中包括一個可選的target對象。我們可以在任何地方使用這個函數(shù)添加新操作到活動流中。

6.2.3 避免活動流中的重復操作

有時候用戶可能執(zhí)行一個操作多次。他們可能在很短的時間內(nèi)多次點擊like/unlike按鈕,或者執(zhí)行同一個操作多次。最終會讓你存儲和顯示重復操作。為了避免這種情況,我們會完善create_acion()函數(shù),避免大部分重復操作。

編輯actions應用的utils.py文件,如下所示:

import datetime
from django.utils import timezone
from django.contrib.contenttypes.models import ContentType
from .models import Action

def create_action(user, verb, target=None):
    # check for any similar action made in the last minute
    now = timezone.now()
    last_minute = now - datetime.timedelta(seconds=60)
    similar_actions = Action.objects.filter(user_id=user.id, verb=verb, created__gte=last_minute)
    if target:
        target_ct = ContentType.objects.get_for_model(target)
        similar_actions = similar_actions.filter(target_ct=target_ct, targt_id=target.id)

    if not similar_actions:
        # no existing actions found
        action = Action(user=user, verb=verb, target=target)
        action.save()
        return True
    return False

我們修改了create_action()函數(shù),避免保存重復操作,并返回一個布爾值,表示操作是否保存。我們是這樣避免重復的:

  • 首先使用Django提供的timezone.now()方法獲得當前時間。這個函數(shù)的作用與datetime.datetime.now()相同,但它返回一個timezone-aware對象。Django提供了一個USE_TZ設置,用于啟用或禁止時區(qū)支持。使用startproject命令創(chuàng)建的默認settings.py文件中,包括USE_TZ=True。
  • 我們使用last_minute變量存儲一分鐘之前的時間,然后我們檢索用戶從那之后執(zhí)行的所有相同操作。
  • 如果最后一分鐘沒有相同的操作,則創(chuàng)建一個Action對象。如果創(chuàng)建了Action對象,則返回True,否則返回False。

6.2.4 添加用戶操作到活動流中

是時候為用戶添加一些操作到視圖中,來創(chuàng)建活動流了。我們將為以下幾種交互存儲操作:

  • 用戶標記圖片
  • 用戶喜歡或不喜歡一張圖片
  • 用戶創(chuàng)建賬戶
  • 用戶關注或取消關注其它用戶

編輯images應用的views.py文件,添加以下導入:

from actions.utils import create_action

image_create視圖中,在保存圖片之后添加create_action()

new_item.save()
create_action(request.user, 'bookmarked image', new_item)

image_like視圖中,在添加用戶到users_like關系之后添加create_action()

image.users_like.add(request.user)
create_action(request.user, 'likes', image)

現(xiàn)在編輯account應用的views.py文件,添加以下導入:

from actions.utils import create_action

register視圖中,在創(chuàng)建Profile對象之后添加create_action()

new_user.save()
profile = Profile.objects.create(user=new_user)
create_action(new_user, 'has created an account')

user_follow視圖中,添加create_action()

Contact.objects.get_or_create(user_from=request.user, user_to=user)
create_action(request.user, 'is following', user)

正如你所看到的,多虧了Action模型和幫助函數(shù),讓我們很容易的在活動流中保存新操作。

6.2.5 顯示活動流

最后,我們需要為每個用戶顯示活動流。我們將把它包括在用戶的儀表盤中。編輯account應用的views.py文件。導入Action模型,并修改dashboard視圖:

from actions.models import Action

@login_required
def dashboard(request):
    # Display all actions by default
    actions = Action.objects.exclude(user=request.user)
    following_ids = request.user.following.values_list('id', flat=True)
    if following_ids:
        # If user is following others, retrieve only their actions
        actions = actions.filter(user_id__in=following_ids)
    actions = actions[:10]

    return render(request,
                  'account/dashboard.html',
                  {'section': 'dashboard', 'actions': actions})

在這個視圖中,我們從數(shù)據(jù)庫中檢索當前用戶之外的所有用戶執(zhí)行的操作。如果用戶還沒有關注任何人,我們顯示其它用戶最近的操作。這是用戶沒有關注其他用戶時的默認行為。如果用戶關注了其他用戶,我們限制查詢只顯示他關注的用戶執(zhí)行的操作。最后,我們限制只返回前10個操作。在這里沒有使用order_by()進行排序,因為我們使用Action模型的Meta選項提供的默認排序。因為我們在Action模型中設置了ordering = ('-created',),所以會先返回最新的操作。

6.2.6 優(yōu)化涉及關聯(lián)對象的QuerySet

每次檢索一個Action對象時,你可能需要訪問與它關聯(lián)的User對象,以及該用戶關聯(lián)的Profile對象。Django ORM提供了一種方式,可以一次檢索關聯(lián)對象,避免額外的數(shù)據(jù)庫查詢。

6.2.6.1 使用select_related

Django提供了一個select_related方法,允許你檢索一對多關系的關聯(lián)對象。它會轉換為單個更復雜的QuerySet,但是訪問關聯(lián)對象時,可以避免額外的查詢。select_related方法用于ForeignKeyOneToOne字段。它在SELECT語句中執(zhí)行SQL JOIN,并且包括了關聯(lián)對象的字段。

要使用select_related(),需要編輯之前代碼的這一行:

actions = actions.filter(user_id__in=following_ids)

并在你會使用的字段上添加select_related

actions = actions.filter(user_id__in=following_ids)\
    .select_related('user', 'user__profile')

我們用user__profile在單條SQL查詢中連接了Profile表。如果調(diào)用select_related()時沒有傳遞參數(shù),那么它會從所有ForeignKey關系中檢索對象。總是將之后會訪問的關系限制為select_related()。

仔細使用select_related()可以大大減少執(zhí)行時間。

6.2.6.2 使用prefetch_related

正如你所看到的,在一對多關系中檢索關聯(lián)對象時,select_related()會提高執(zhí)行效率。但是select_related()不能用于多對多或者多對一關系。Django提供了一個名為prefetch_relatedQuerySet方法,除了select_related()支持的關系之外,還可以用于多對多和多對一關系。prefetch_related()方法為每個關系執(zhí)行獨立的查詢,然后用Python連接結果。該方法還支持GenericRelationGenericForeignKey的預讀取。

GenericForeignKey字段target添加prefetch_related(),完成這個查詢:

actions = actions.filter(user_id__in=following_ids)\
    .select_related('user', 'user__profile')\
    .prefetch_related('target')

現(xiàn)在查詢已經(jīng)優(yōu)化,用于檢索包括關聯(lián)對象的用戶操作。

6.2.7 為操作創(chuàng)建模板

我們將創(chuàng)建模板用于顯示特定的Action對象。在actions應用目錄下創(chuàng)建templates目錄,并添加以下文件結構:

actions/
    action/
        detail.html

編輯actions/action/detail.html目錄文件,并添加以下代碼:

{% load thumbnail %}

{% with user=action.user profile=action.user.profile %}
    <div class="action">
        <div class="images">
            {% if profile.photo %}
                {% thumbnail user.profile.photo "80x80" crop="100%" as im %}
                    <a href="{{ user.get_absolute_url }}">
                        ![]({{ im.url }})
                    </a>
                {% endthumbnail %}
            {% endif %}

            {% if action.target %}
                {% with target=action.target %}
                    {% if target.image %}
                        {% thumbnail target.image "80x80" crop="100%" as im %}
                            <a href="{{ target.get_absolute_url }}">
                                ![]({{ im.url }})
                            </a>
                        {% endthumbnail %}
                    {% endif %}
                {% endwith %}
            {% endif %}
        </div>
        <div class="info">
            <p>
                <span class="date">{{ action.created|timesince }} age</span>
                <br />
                <a href="{{ user.get_absolute_url }}">
                    {{ user.first_name }}
                </a>
                {{ action.verb }}
                {% if action.target %}
                    {% with target=action.target %}
                        <a href="{{ target.get_absolute_url }}">{{ target }}</a>
                    {% endwith %}
                {% endif %}
            </p>
        </div>
    </div>
{% endwith %}

這是顯示一個Action對象的模板。首先,我們使用{% with %}模板標簽檢索執(zhí)行操作的用戶和他們的個人資料。接著,如果Action對象有關聯(lián)的target對象,則顯示target對象的圖片。最后,我們顯示執(zhí)行操作的用戶鏈接,描述,以及target對象(如果有的話)。

現(xiàn)在編輯account/dashboard.html模板,在content塊底部添加以下代碼:

<h2>What's happening</h2>
<div id="action-list">
    {% for action in actions %}
        {% include "actions/action/detail.html" %}
    {% endfor %}
</div>

在瀏覽器中打開http://127.0.0.1:8000/account/。用已存在的用戶登錄,并執(zhí)行一些操作存儲在數(shù)據(jù)庫中。接著用另一個用戶登錄,并關注之前那個用戶,然后在儀表盤頁面查看生成的活動流,如下圖所示:

我們?yōu)橛脩魟?chuàng)建了一個完整的活動流,并且能很容易的添加新的用戶操作。你還可以通過AJAX分頁,在活動流中添加無限滾動,就像我們在image_list視圖中那樣。

6.3 使用信號進行反規(guī)范化計數(shù)

某些情況下你希望對數(shù)據(jù)進行反規(guī)范化處理。反規(guī)范化(denormalization)是在一定程度上制造一些冗余數(shù)據(jù),從而優(yōu)化讀取性能。你必須小心使用反規(guī)范化,只有當你真的需要的時候才使用。反規(guī)范化最大的問題是很難保持數(shù)據(jù)的更新。

我們將通過一個例子解釋如何通過反規(guī)范化計數(shù)來改善查詢。缺點是我們必須保持冗余數(shù)據(jù)的更新。我們將在Image模型中使用反規(guī)范數(shù)據(jù),并使用Django的信號來保持數(shù)據(jù)的更新。

6.3.1 使用信號

Django自帶一個信號調(diào)度程序,當特定動作發(fā)生時,允許接收函數(shù)獲取通知。當某些事情發(fā)生時,你的代碼需要完成某些工作,信號非常有用。你也可以創(chuàng)建自己的信號,當事件發(fā)生時,其他人可以獲得通知。

Django在django.db.models.signals中為模型提供了幾種信號,其中包括:

  • pre_savepost_save:調(diào)用模型的save()方法之前或之后發(fā)送
  • pre_deletepost_delete:調(diào)用模型或QuerySetdelete()方法之前或之后發(fā)送
  • m2m_changed:當模型的ManyToManyField改變時發(fā)送

這只是Django提供了部分信號。你可以在這里查看Django的所有內(nèi)置信號。

我們假設你想獲取熱門圖片。你可以使用Django聚合函數(shù),按用戶喜歡數(shù)量進行排序。記住你已經(jīng)在第三章中使用了聚合函數(shù)。以下代碼按喜歡數(shù)量查詢圖片:

from django.db.models import Count
from images.models import Image

images_by_popularity = Image.objects.annotate(total_likes=Count('users_like')).order_by('-total_likes')

但是,通過統(tǒng)計圖片的喜歡數(shù)量比直接使用一個存儲喜歡數(shù)量的字段更費時。你可以在Image模型中添加一個字段,用來反規(guī)范化喜歡數(shù)量,從而提高涉及這個字段的查詢性能。如何保持這個字段的更新呢?

編輯images應用的models.py文件,為Image模型添加以下字段:

total_likes = models.PositiveIntegerField(db_index=True, default=0)

total_likes字段允許我們存儲每張圖片被用戶喜歡的數(shù)量。當你希望過濾或者排序QuerySet時,反規(guī)范計數(shù)非常有用。

在使用反規(guī)范字段之前,你必須考慮其它提升性能的方式。比如數(shù)據(jù)庫索引,查詢優(yōu)化和緩存。

執(zhí)行以下命令為新添加的字段創(chuàng)建數(shù)據(jù)庫遷移:

python manage.py makemigrations images

你會看到以下輸出:

Migrations for 'images':
  images/migrations/0002_image_total_likes.py
    - Add field total_likes to image

接著執(zhí)行以下命令讓遷移生效:

python manage.py migrate images

輸出中會包括這一行:

Applying images.0002_image_total_likes... OK

我們將會為m2m_changed信號附加一個receiver函數(shù)。在images應用目錄下創(chuàng)建一個signals.py文件,添加以下代碼:

from django.db.models.signals import m2m_changed
from django.dispatch import receiver
from .models import Image

@receiver(m2m_changed, sender=Image.users_like.through)
def users_like_changed(sender, instance, **kwargs):
    instance.total_likes = instance.users_like.count()
    instance.save()

首先,我們使用receiver()裝飾器注冊users_like_changed函數(shù)為receiver()函數(shù),并把它附加給m2m_changed信號。我們把函數(shù)連接到Image.users_like.throuth,只有這個發(fā)送者發(fā)起m2m_changed信號時,這個方法才會被調(diào)用。還可以使用Signal對象的connect()方法來注冊receiver()函數(shù)。

Django信號是同步和阻塞的。不要用異步任務導致信號混亂。但是,當你的代碼從信號中獲得通知時,你可以組合兩者來啟動異步任務。

你必須把接收器函數(shù)連接到一個信號,這樣每次發(fā)送信號時,接收器函數(shù)才會調(diào)用。注冊信號的推薦方式是在應用配置類的ready()函數(shù)中導入它們。Django提供了一個應用注冊表,用于配置和內(nèi)省應用。

6.3.2 定義應用配置類

Django允許你為應用指定配置類。要為應用提供一個自定義配置,你需要創(chuàng)建一個自定義類,它繼承自位于django.apps中的AppConfig類。應用配置類允許為應用存儲元數(shù)據(jù)和配置,并提供內(nèi)省。

你可以在這里閱讀更多關于應用配置的信息。

為了注冊你的信號接收函數(shù),當你使用receiver()裝飾器時,你只需要在AppConfig類的ready()方法中導入應用的信號模塊。一旦應用注冊表完全填充,就會調(diào)用這個方法。這個方法中應該包括應用的所有初始化工作。

images應用目錄下創(chuàng)建apps.py文件,并添加以下代碼:

from django.apps import AppConfig


class ImagesConfig(AppConfig):
    name = 'images'
    verbose_name = 'Image bookmarks'

    def ready(self):
        # import signal handlers
        import images.signals

譯者注:Django 1.11版本中,默認已經(jīng)生成了apps.py文件,只需要在其中添加ready()方法。

其中,name屬性定義應用的完整Python路徑;verbose_name屬性設置應用的可讀名字。它會在管理站點中顯示。我們在ready()方法中導入該應用的信號。

現(xiàn)在我們需要告訴Django應用配置的位置。編輯images應用目錄的__init__.py文件,添加這一行代碼:

default_app_config = 'images.apps.ImagesConfig'

在瀏覽器中查看圖片詳情頁面,并點擊like按鈕。然后回到管理站點查看total_likes屬性。你會看到total_likes已經(jīng)更新,如下圖所示:

現(xiàn)在你可以使用total_likes屬性按熱門排序圖片,或者在任何地方顯示它,避免了用復雜的查詢來計算。以下按圖片被喜歡的總數(shù)量排序的查詢:

images_by_popularity = Image.objects.annotate(likes=Count('users_like')).order_by('-likes')

可以變?yōu)檫@樣:

images_by_popularity = Image.objects.order_by('-total_likes')

通過更快的SQL查詢就返回了這個結果。這只是使用Django信號的一個示例。

小心使用信號,因為它會讓控制流更難理解。如果你知道會通知哪個接收器,很多情況下就能避免使用信號。

你需要設置初始計數(shù),來匹配數(shù)據(jù)庫的當前狀態(tài)。使用python manage.py shell命令打開終端,執(zhí)行以下命令:

from images.models import Image
for image in Image.objects.all():
    image.total_likes = image.users_like.count()
    image.save()

現(xiàn)在每張圖片被喜歡的總數(shù)量已經(jīng)更新了。

6.4 用Redis存儲項視圖

Redis是一個高級的鍵值對數(shù)據(jù)庫,允許你存儲不同類型的數(shù)據(jù),并且能進行非??焖俚腎/O操作。Redis在內(nèi)存中存儲所有數(shù)據(jù),但數(shù)據(jù)集可以一次性持久化到硬盤中,或者添加每條命令到日志中。與其它鍵值對存儲相比,Redis更通用:它提供了一組功能強大的命令,并支持各種各樣的數(shù)據(jù)結構,比如strings,hashes,lists,sets,ordered sets,甚至bitmapsHyperLogLogs

SQL最適合于模式定義的持久化數(shù)據(jù)存儲,而當處理快速變化的數(shù)據(jù),短暫的存儲,或者快速緩存時,Redis有更多的優(yōu)勢。讓我們看看如何使用Redis為我們的項目添加新功能。

6.4.1 安裝Redis

這里下載最新的Redis版本。解壓tar.gz文件,進入redis目錄,使用make命令編譯Redis:

cd redis-3.2.8
make

安裝完成后,使用以下命令初始化Redis服務器:

src/redis-server

你會看到結尾的輸出為:

19608:M 08 May 17:04:38.217 # Server started, Redis version 3.2.8
19608:M 08 May 17:04:38.217 * The server is now ready to accept connections on port 6379

默認情況下,Redis在6379端口運行,但你可以使用--port之指定自定義端口,比如:redis-server --port 6655。服務器就緒后,使用以下命令在另一個終端打開Redis客戶端:

src/redis-cli

你會看到Redis客戶端終端:

127.0.0.1:6379>

你可以直接在Redis客戶端執(zhí)行Redis命令。讓我們嘗試一下。在Redis終端輸入SET命令,在鍵中存儲一個值:

127.0.0.1:6379> SET name "Peter"
OK

以上命令在Redis數(shù)據(jù)庫中創(chuàng)建了一個字符串值為Petername鍵。輸出OK表示鍵已經(jīng)成功保存。接收,使用GET命令查詢值:

127.0.0.1:6379> GET name
"Peter"

我們也可以使用EXISTS命令檢查一個叫鍵是否存在。如果存在返回1,否則返回0

127.0.0.1:6379> EXISTS name
(integer) 1

你可以使用EXPIRE命令為鍵設置過期時間,這個命令允許你設置鍵的存活秒數(shù)。另一個選項是使用EXPIREAT命令,它接收一個Unix時間戳。把Redis作為緩存,或者存儲臨時數(shù)據(jù)時,鍵過期非常有用:

127.0.0.1:6379> EXPIRE name 2
(integer) 1

等待2秒,再次獲取同樣的鍵:

127.0.0.1:6379> GET name
(nil)

返回值(nil)是一個空返回,表示沒有找到鍵。你也可以使用DEL命令刪除鍵:

127.0.0.1:6379> SET total 1
OK
127.0.0.1:6379> DEL total
(integer) 1
127.0.0.1:6379> GET total
(nil)

這只是鍵操作的基本命令。Redis為每種數(shù)據(jù)類型(比如strings,hasheslists,setsordered sets等等)提供了大量的命令。你可以在這里查看所有Redis命令,在這里查看所有Redis數(shù)據(jù)類型。

6.4.2 在Python中使用Redis

我們需要為Redis綁定Python。通過pip安裝redis-py

pip install redis

你可以在這里查看redis-py的文檔。

redis-py提供了兩個類用于與Redis交互:StricRedisRedis。兩個類提供了相同的功能。StricRedis類視圖遵守官方Redis命令語法。Redis類繼承自StricRedis,覆寫了一些方法,提供向后的兼容性。我們將使用StrictRedis類,因為它遵循Redis命令語法。打開Python終端,執(zhí)行以下命令:

>>> import redis
>>> r = redis.StrictRedis(host='localhost', port=6379, db=0)

這段代碼創(chuàng)建了一個Redis數(shù)據(jù)連接。在Redis中,數(shù)據(jù)由整數(shù)索引區(qū)分,而不是數(shù)據(jù)庫名。默認情況下,客戶端連接到數(shù)據(jù)庫0。Redis數(shù)據(jù)庫有效的數(shù)字到16,但你可以在redis.conf文件中修改這個值。

現(xiàn)在使用Python終端設置一個鍵:

>>> r.set('foo', 'bar')
True

命令返回True表示鍵創(chuàng)建成功?,F(xiàn)在你可以使用get()命令查詢鍵:

>>> r.get('foo')
b'bar'

正如你鎖看到的,StrictRedis方法遵循Redis命令語法。

讓我們在項目中集成Redis。編輯bookmarks項目的settings.py文件,添加以下設置:

REDIS_HOST = 'localhost'
REDIS_PORT = 6379
REDIS_DB = 0

以上設置了Redis服務器和我們在項目中使用的數(shù)據(jù)庫。

6.4.3 在Redis中存儲項的瀏覽次數(shù)

讓我們存儲一張圖片被查看的總次數(shù)。如果我們使用Django ORM,則每次顯示圖片后,都會涉及UPDATE語句。如果使用Redis,我們只需要增加內(nèi)存中的計數(shù),從而獲得更好的性能。

編輯images應用的views.py文件,添加以下代碼:

import redis
from django.conf import settings

# connect to redis
r = redis.StrictRedis(host=settings.REDIS_HOST,
                      port=settings.REDIS_PORT,
                      db=settings.REDIS_DB)

我們建立了Redis連接,以便在視圖中使用。修改image_detail視圖,如下所示:

def image_detail(request, id, slug):
    image = get_object_or_404(Image, id=id, slug=slug)
    # increament total image views by 1
    total_views = r.incr('image:{}:views'.format(image.id))
    return render(request, 
                  'images/image/detail.html', 
                  {'section': 'images', 'image': image, 'total_views': total_views})

在這個視圖中,我們使用INCR命令把一個鍵的值加1,如果鍵不存在,則在執(zhí)行操作之前設置值為0。incr()方法返回執(zhí)行操作之后鍵的值,我們把它存在total_views變量中。我們用object-type:id:field(比如image:33:id:views)構建Redis鍵。

Redis鍵的命名慣例是使用冒號分割,來創(chuàng)建帶命名空間的鍵。這樣鍵名會很詳細,并且相關的鍵共享部分相同的模式。

編輯image/detail.html模板,在<span class="count">元素之后添加以下代碼:

<span class="count">
    <span class="total">{{ total_views }}</span>
    view{{ total_views|pluralize }}
</span>

現(xiàn)在在瀏覽器中打開圖片詳情頁面,加載多次。你會看到每次執(zhí)行視圖,顯示的瀏覽總數(shù)都會加1,如下圖所示:

你已經(jīng)成功的在項目集成了Redis,來存儲項的瀏覽次數(shù)。

6.4.4 在Redis中存儲排名

讓我們用Redis構建更多功能。我們將創(chuàng)建瀏覽次數(shù)最多的圖片排名。我們將使用Redis的sorted set來構建排名。一個sorted set是一個不重復的字符串集合,每個成員關聯(lián)一個分數(shù)。項通過它們的分數(shù)存儲。

編輯images應用的views.py文件,修改image_detail視圖,如下所示:

def image_detail(request, id, slug):
    image = get_object_or_404(Image, id=id, slug=slug)
    # increament total image views by 1
    total_views = r.incr('image:{}:views'.format(image.id))
    # increament image ranking by 1
    r.zincrby('image_ranking', image.id, 1)
    return render(request, 
                  'images/image/detail.html', 
                  {'section': 'images', 'image': image, 'total_views': total_views})

我們用zincrby()命令在sorted set中存儲圖片瀏覽次數(shù),其中鍵為image_ranking。我們存儲圖片id,分數(shù)1會被加到sorted set中這個元素的總分上。這樣就可以全局追蹤所有圖片的瀏覽次數(shù),并且有一個按瀏覽次數(shù)排序的sorted set。

現(xiàn)在創(chuàng)建一個新視圖,用于顯示瀏覽次數(shù)最多的圖片排名。在views.py文件中添加以下代碼:

@login_required
def image_ranking(request):
    # get image ranking dictinary
    image_ranking = r.zrange('image_ranking', 0, -1, desc=True)[:10]
    image_ranking_ids = [int(id) for id in image_ranking]
    # get most viewed images
    most_viewed = list(Image.objects.filter(id__in=image_ranking_ids))
    most_viewed.sort(key=lambda x: image_ranking_ids.index(x.id))
    return render(request, 'images/image/ranking.html', {'section': 'images', 'most_viewed': most_viewed})

這是image_ranking視圖。我們用zrange()命令獲得sorted set中的元素。這個命令通過最低和最高分指定自定義范圍。通過0作為最低,-1作為最高分,我們告訴Redis返回sorted set中的所有元素。我們還指定desc=True,按分數(shù)的降序排列返回元素。最后,我們用[:10]切片操作返回分數(shù)最高的前10個元素。我們構建了一個返回的圖片ID列表,并作為整數(shù)列表存在image_ranking_ids變量中。我們迭代這些ID的Image對象,并使用list()函數(shù)強制執(zhí)行查詢。強制QuerySet執(zhí)行很重要,因為之后我們要調(diào)用列表的sort()方法(此時我們需要一組對象,而不是一個QuerySet)。我們通過Image對象在圖片排名中的索引進行排序?,F(xiàn)在我們可以在模板中使用most_viewed列表顯示瀏覽次數(shù)最多的前10張圖片。

創(chuàng)建image/ranking.html模板文件,并添加以下代碼:

{% extends "base.html" %}

{% block title %}Images ranking{% endblock %}

{% block content %}
    <h1>Images ranking</h1>
    <ol>
        {% for image in most_viewed %}
            <li>
                <a href="{{ image.get_absolute_url }}">
                    {{ image.title }}
                </a>
            </li>
        {% endfor %}
    </ol>
{% endblock %}

這個模板非常簡單,我們迭代most_viewed列表中的Image對象。

最后為新視圖創(chuàng)建URL模式。編輯images應用的urls.py文件,添加以下模式:

url(r'^/ranking/$', views.image_ranking, name='ranking')

在瀏覽器中打開http://127.0.0.1:8000/images/ranking/,你會看到圖片排名,如下圖所示:

6.4.5 Redis的后續(xù)功能

Redis不是SQL數(shù)據(jù)庫的替代者,而是更適用于特定任務的快速的內(nèi)存存儲。當你真的需要時可以使用它。Redis非常適合以下場景:

  • 計數(shù):正如你所看到的,使用Redis管理計算非常簡單。你可以使用incr()incrby()計數(shù)。
  • 存儲最近的項:你可以使用lpush()rpush()在列表開頭或結尾添加項。使用lpop()rpop()移除并返回第一或最后一項。你可以使用ltrim()截斷列表長度。
  • 隊列:除了pushpop命令,Redis還提供了阻塞隊列的命令。
  • 緩存:使用expire()expireat()允許你把Redis當做緩存。你還可以找到Django的第三方Redis緩存后臺。
  • 訂閱/發(fā)布:Redis還為訂閱/取消訂閱,以及發(fā)送消息給頻道提供了命令。
  • 排名和排行榜:Redis的sorted set可以很容易創(chuàng)建排行榜。
  • 實時跟蹤:Redis的快速I/O非常適合實時場景。

6.5 總結

這一章中,你構建了關注系統(tǒng)和用戶活動流。你學習了Django信號是如何工作的,并在項目中集成了Redis。

下一章中,你會學習如何構建一個在線商店。你將創(chuàng)建一個產(chǎn)品目錄,并使用會話構建購物車。你還講學習如何使用Celery啟動異步任務。

最后編輯于
?著作權歸作者所有,轉載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容