編寫具備描述性的 RESTful API (四): 通知系統

上一節講到了用戶行爲,由用戶行爲天然的便引出了通知系統。當用戶喜歡了一篇帖子,那麼該帖子的做者應該收到一條提醒。php

laravel 提供了一套 Notification 組件,用於處理通知,其支持經過多種頻道發送通知,包括郵件、短信 、通知還能存儲到數據庫(站內信)以便後續在 Web 頁面中顯示,本節將重點放在站內信。前端

laravel 爲咱們擺平了通知的推送問題,可是還有一個問題,即通知(數據庫)的存儲問題須要咱們處理。通知的存儲一般有兩種作法。laravel

  • 將通知的主體內容與部分附加內容一同冗餘存儲到數據庫中,即 laravel 的默認形式。
  • 將通知的主體內容的主鍵與其附加內容的主鍵 既 id 存儲到數據庫中。

前者的好處是較小的查詢壓力,且數據具備持久性,不會由於被刪帖等問題而影響到通知內容。缺點則是佔用存儲空間,且缺少靈活性,後者則反之。git

源碼中選擇了前者,既默認形式。github

數據分析

經過抽象能夠獲得,一條通知由三部分組成 行爲的觸發者 trigger 、行爲主體(可能攜帶 內容) target 、須要通知的用戶 notifiable數據庫

此處最讓人疑惑的應該是 行爲的主體,根據實際需求稍微圖解一下。json

已 「Comment Post」爲例,在簡書中其實際的表現行爲以下api

根據上面的分析 notifications 表中的 data 須要冗餘以下數據,能夠根據運營的實際需求調整工具

public function toArray($notifiable) {
   $data = [
     'trigger' => [
       'id' => 1, // default type users
       'type' => 'users',
       'nickname' => 'nickname',
       'avatar' => 'xxx',
     ],
     'target' => [
       'id' => 12,
       'type' => 'posts',
       'text' => 'xxx',
     ],
     'content' => [
       'id' => 1,
       'type' => 'comment',
       'text' => 'xxx',
       'call_user' => [
         'id' => 'xxx',
         'nickname' => 'xxx'
       ]
     ]
   ];
  
   return $data;
 }
複製代碼

建立通知

已上一次的 「Like Post」 行爲爲例,當用戶點贊文章後,須要給文章的做者發送一條通知。post

# PostLikerObserver


/** * @param PostLiker $postLiker */
public function created(PostLiker $postLiker) {
  // ...

  // notify App\Notifications\LikePost
  User::findOrFail($postLiker->post->user_id)
    ->notify(new LikePost($user, $postLiker->post));
}
複製代碼
# LikerPost.php

namespace App\Notifications;


class LikePost extends Notification {
    use Queueable;
   
    private $post;
    private $trigger;
    private $target;

    public function __construct(User $trigger, Post $target) {
        $this->trigger = $trigger;
        $this->target = $target;
    }

    // ...
  
    public function toArray($notifiable) {
        $data = [
            'trigger' => [
                'id' => $this->trigger->id,
                'type' => $this->trigger->getTable(),
                'nickname' => $this->trigger->nickname,
                'avatar' => $this->trigger->avatar,
            ],
            'target' => [
                'id' => $this->target->id,
                'type' => $this->target->getTable(),
                'text' => $this->target->title,
            ]
        ];

        return $data;
    }
}
複製代碼

這樣就成功創建了一條 Notification , 相似「Comment Post」等用戶行爲依舊能夠按照這種思路完成。無非「Comment Post」須要在其 data 中添加 content 而已,這裏就不作展現了。

這裏須要提一下代碼優化,經過上面的「數據分析」,咱們已經把通知抽象爲 trigger / target / content ,所以並不須要再每種用戶行爲都編寫一堆 重複的構造方法,toArray 等方法。

徹底能夠編寫一個 Notification 基類來編寫上面的大部分代碼,從而減小重複的代碼。

相關優化已經完成,歡迎參考源碼。

通知的另外一種存儲方式

這裏稍微提一下另一種形式的通知存儲,即上文中提到的非冗餘形式。不過該方式須要修改 notifications 表的默認表結構。

Schema::create('notifications', function (Blueprint $table) {
  $table->uuid('id')->primary();
  $table->string('type');
  $table->morphs('notifiable');
  
  // $table->json('data')->nullable()->comment('target/content/trigger');
  $table->morphs('triggerable')->nullable();
  $table->morphs('targetable')->nullable()
  $table->morphs('contentable')->nullable();
  
  $table->timestamp('read_at')->nullable();
  $table->timestamps();
});
複製代碼

經過表結構相信你已經一目瞭然。萬變不離其宗,咱們始終都是在圍繞着 trigger / target / content 轉圈圈。

固然若是你瞭解 「 MySQL 生成列 」的話,徹底能夠寫出下敘語句,將通知從冗餘形式平滑過渡到到非冗餘形式。

$table->string('targetable_id')->virtualAs('data->>"$.target.id"')->index();

有了這樣的表結構,關聯關係走起來,須要的數據如 文章的 點贊量 / 閱讀量 等等都可以獲得,這裏就不詳細描述代碼編寫了。後續簡書的用戶動態模塊會再次運用這種非冗餘結構的編碼,到時再深刻講解相關的細節。

通知壓縮

咱們的通知採用了冗餘形式存儲,因此數據存儲空間優化是必須考慮的一個點。尤爲是在點贊這類通知中,對數據的浪費是很是巨大的。

所以能夠經過相似下面這樣的方式壓縮未讀通知

固然,若是你選擇非冗餘形式存儲通知數據,那麼將難以進行數據壓縮。

Notification 組件提供了通知後 事件 ,因此相應的邏輯將會在該事件的監聽者中完成。邏輯比較簡單,直接看編碼吧

# App\Listeners\CompressNotification

class CompressNotification {
    public function handle(NotificationSent $event) {
        $channel = $event->channel;

        if ($channel !== DatabaseChannel::class) {
            return;
        }

        $currentNotification = $event->response;
        if (!in_array($currentNotification->type, ['like_post', 'like_comment'])) {
            return;
        }

        $notifiable = $event->notifiable;
      
      	// 查找相同 target 的上一條通知
        $previousNotification = $notifiable->unreadNotifications()
            ->where('data->target->type', $currentNotification->data['target']['type'])
            ->where('data->target->id', $currentNotification->data['target']['id'])
            ->where('id', '<>', $currentNotification->id)
            ->first();

        if ($previousNotification) {
            $compressCount = $previousNotification->data['compress_count'] ?? 1;
            $triggers = $previousNotification->data['triggers'] ?? [$previousNotification->data['trigger']];


            $compressCount += 1;

            // 最多存儲三個觸發者
            if (count($triggers) < 3) {
                $triggers[] = $currentNotification->data['trigger'];
            }

            $previousNotification->delete();

            $data = $currentNotification->data;
            $data['compress_count'] = $compressCount;
            $data['triggers'] = $triggers;
            unset($data['trigger']);
          
            $currentNotification->data = $data;
            $currentNotification->save();
        }
    }
}
複製代碼

壓縮後的通知的 data 的 trigger Object 變成了 triggers Array ,而且增長了 compress_count 用來記錄壓縮條數,前端能夠經過該字段來判斷通知是否被壓縮過。

補充

  • 相應的 API 如通知列表,標記爲已讀等已經完成,歡迎參考源碼。
  • 一般會有一個與通知相似性質的功能,稱爲消息,或者說私信/聊天等。該功能依舊可使用 Notification 來完成,由於其也是由 **行爲的觸發者 trigger 、行爲主體(可能攜帶 內容) target 、須要通知的用戶 notifiable ** 構成。可是從解耦的角度來看,將其單獨成一個 「Chat 模塊」,且消息提醒依舊使用 「Notification」 來完成會是更好的選擇。
  • 一般會有一個與通知相似結構的功能,稱爲用戶動態,或者說用戶日誌。上文中有提到該功能,後續會完成該功能。
  • 未讀消息數在 users 表中冗餘 unread_notification_count ,而不是進行實時計數。

相關

相關文章
相關標籤/搜索