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

推薦閱讀

建表

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

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

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 中間表.vue

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 哦.laravel

建模

# 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 ,接下來就可以實現帖子詳情頁設計稿的最後一部分啦git

專題收入

首先是專題收錄部分, 按照 RESTful 的規範,咱們能夠設計出這樣一條 APIgithub

test.com/api/posts/{… , 此處編碼較爲簡單,參考源碼便可算法

推薦閱讀

首先仍是按照 RESTful 規範 來設計 API數據庫

test.com/api/posts/{…api

相應的控制器代碼工具

# 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 文檔附加在文末

在控制器代碼中, 將相應的 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 文檔.

相關

相關文章
相關標籤/搜索