編寫具有描述性的 RESTful API (二): 推薦與 Observer

weiwenhao發表於2019-03-20

推薦閱讀

建表

接上一篇提到的,通過專題( collection ), 來實現推薦閱讀的編寫.

按照慣例,先來看看 專題的設計稿 ,然後設計出表結構.

 Schema::create('collections', function (Blueprint $table) {
     $table->increments('id');
     $table->string('name');
     $table->string('avatar');
     $table->string('description');

     $table->unsignedInteger('post_count')->default(0);
     $table->unsignedInteger('fans_count')->default(0);

     $table->unsignedInteger('user_id')->comment('建立者');

     $table->timestamps();
 });
複製程式碼

專題存在管理員( collection_admin )/投稿作者( collection_author )/關注者( collection_follower ) /帖子( collection_post ) 此處以 collection_post 為例看一下中間表的設計,其是 collection 和 post 中間表.

Schema::create('collection_post', function (Blueprint $table) {
    $table->unsignedInteger('post_id');
    $table->unsignedInteger('collection_id');

    $table->timestamp('passed_at')->nullable()->comment('稽核通過時間');

    $table->timestamps();

    $table->index('post_id');
    $table->index('collection_id');

    $table->unique(['post_id', 'collection_id']);
});
複製程式碼

建好表之後記得填充 seeder 哦.

建模

# Collection.php

<?php

namespace App\Models;

class Collection extends Model
{
    public function posts()
    {
        return $this->belongsToMany(Post::class, 'collection_post');
    }
}
複製程式碼
# Post.php

<?php

namespace App\Models;

class Post extends Model
{
	// ...

    public function collections()
    {
        return $this->belongsToMany(Collection::class, 'collection_post');
    }
}
複製程式碼

有了 Collection ,接下來就能夠實現帖子詳情頁設計稿的最後一部分啦

專題收入

編寫具有描述性的 RESTful API (二): 推薦與 Observer

首先是專題收錄部分, 按照 RESTful 的規範,我們可以設計出這樣一條 API

test.com/api/posts/{… , 此處編碼較為簡單,參考原始碼即可

推薦閱讀

編寫具有描述性的 RESTful API (二): 推薦與 Observer

首先還是按照 RESTful 規範 來設計 API

test.com/api/posts/{…

相應的控制器程式碼

# PostController.php

public function indexOfRecommend($post)
{
    $collectionIds = $post->collections()->pluck('id');

    $query = Post::whereHas('collections', function ($query) use ($collectionIds) {
        $query->whereIn('collection_id', $collectionIds);
    });

    // 排序問題
    $posts = $query->columns()->paginate();

    return PostResource::make($posts);
}
複製程式碼

這裡需要說明一下, laravel 提供的 whereHas 會生成一個效率不高的 SQL 語句,需要載入全表.但是系列的目的是編寫具有描述性的 RESTful API ,所以此處不做進一步優化.

Observer

Observer 既 觀察者,可以用於程式碼解耦,保持控制器簡潔. 接下來的兩個邏輯會涉及 Observer 的使用場景.

熱度

$posts = $query->columns()->paginate(); 這行語句在沒有指定 orderBy 時, MySQL 會按照 id , asc 的順序取出帖子,但是在一般的社群網站中,通常會有一個熱度,然後按照熱度將帖子取出來.

這部分的排序演算法又很多,按照產品給定的公式計算即可

下文假定熱度計算公式為 heat = a * (timestamp - 1546300800) + b * read_count + c * like_count

a/b/c 代表每一個特徵所佔的權重,可根據運營需求隨時調整, 由於時間戳過大,所以通過 減去 2019-01-01的時間戳 1546300800 ,來縮小時間戳數字, 當然即使如此依舊會得到一個很大的數字,所以 a 的值會很小

Schema::create('posts', function (Blueprint $table) {
	// ...
    
    $table->integer('heat')->index()->comment('熱度');
    
	// ...
});
複製程式碼

由於專案在開發階段,所以直接修改原有的 migration , 新增 heat 欄位.然後執行

> php artisan migrate:refresh --seed

heat 欄位的維護原則是,**檢測到 read_count 或者 like_count 發生變化時,則更新相關的熱度.**因此此處會用 observe來實現相關的功能.

按照文件建立觀察者並註冊後,可以編寫相關的程式碼

> php artisan make:observer PostObserver --model=Models/Post

class PostObserver
{
    /**
     * @param Post $post
     */
    public function saving(Post $post)
    {
        if ($post->isDirty(['like_count', 'read_count'])) {
            $heat = 0.001 * ($post->created_at->timestamp - 1546300800)
                + 10 * $post->read_count
                + 1000 * $post->like_count;
            
            $post->heat = (integer)$heat;
        }
    }
}
複製程式碼

呼叫 $model->save/update/create 都會在持久化到資料庫之前觸發 saving 方法.

建立評論

基礎編碼

<?php

namespace App\Http\Controllers\Api;

use App\Http\Controllers\Controller;
use App\Models\Comment;
use App\Resources\CommentResource;
use Illuminate\Http\Request;
use Illuminate\Http\Response;

class CommentController extends Controller
{
    /**
     * @param  \Illuminate\Http\Request $request
     * @return \Illuminate\Contracts\Routing\ResponseFactory|Response
     */
    public function store(Request $request)
    {
        $data = $request->all();

        $data['user_id'] = \Auth::id();
        $data['floor'] = Comment::where('post_id', $request->input('post_id'))->max('floor') + 1;
        $comment = Comment::create($data);

        // RESTful 規範中,建立成功應該返回201狀態碼
        return \response(CommentResource::make($comment), 201);
    }
}
複製程式碼

Model

<?php

namespace App\Models;

use Staudenmeir\EloquentEagerLimit\HasEagerLimit;

class Comment extends Model
{
    use HasEagerLimit;

    protected $fillable = ['content', 'user_id', 'post_id', 'floor', 'selected'];

    public function getLikeCountAttribute()
    {
        return $this->attributes['like_count'] ?? 0;
    }

    public function getReplyCountAttribute()
    {
        return $this->attributes['reply_count'] ?? 0;
    }
複製程式碼

由於使用了create 方法進行建立,因此需要在模型中宣告 $fillable

由於建表的時候為 like_count 和 reply_count 設定了預設值為 0 , 所以 在 create 時沒有設定 like_count , reply_count .但是這樣會造成控制器中的 store 方法中的 $comment 不存在 like_count , 和 reply_count 這兩個 key , 這對前端是非常不友好的. 例如在 vue 中此處通常的做法是 this.comments.push(comment) .有兩個辦法解決這個問題

  • create 時新增 $data['like_count'] = 0$data['reply_count'] = 0

  • 使用模型修改器設定這兩個 key 的預設值(上面的 Comment 模型中演示了該方法)

使用上述任意一種方法都能夠保證查詢與建立時的資料一致性.

API 展示, 相應的 Postman 文件附加在文末

編寫具有描述性的 RESTful API (二): 推薦與 Observer

在控制器程式碼中, 將相應的 Model 交給了 tree-ql 處理, 所以這裡依舊可以使用 include , 從而保證相應資料一致性.

posts 表中冗餘了 comment_count ,因此當建立一條評論時,還需要相應的 post.comment_count + 1 . 建立並註冊 CommentObserver. 然後完成相應的編碼

# CommentObserver.php

<?php

namespace App\Observers;

use App\Models\Comment;

class CommentObserver
{
    public function created(Comment $comment)
    {
        $comment->post()->increment('comment_count');
    }
}

複製程式碼

補充

帖子的釋出流程

一個可能存在的問題是,一篇已經發布的帖子當使用者想去再次修改它,此時如果修改到一半的帖子觸發了自動儲存機制,則會出現修改了一半的帖子被展示在首頁等.

因此一張 posts 表並不能滿足實際的需求,還需要增加一張 drafts 表來作為草稿箱, 使用者的建立與修改操作都是在該表下進行的,只有使用者點選發布時, 將相應的 drafts 同步到 posts 表即可. 相關流程參考簡書即可.

釋出流程編碼示例

# DraftController.php

public function published(Draft $draft)
{
    Validator::make($draft->getAttributes(), [
        'title' => 'required|max:255',
        'content' => 'required'
    ])->validate();

    $draft->published();

    return response(null, 201);
}
複製程式碼
public function published()
{
    if (!$this->post_id) {
        $post = Post::create([
            'user_id' => $this->user_id,
            'title' => $this->title,
            'content' => $this->content,
            'published_at' => $this->freshTimestampString(),
        ]);

        $this->post_id = $post->id;
        $this->save();
    } else {
        $post = Post::findOrFail($this->post_id);
        $post->title = $this->title;
        $post->content = $this->content;
        $post->save();
    }
}
複製程式碼

其餘部分參考原始碼,相關 API 參考 Postman 文件.

相關

相關文章