接上一篇提到的,经过专题( 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数据库
相应的控制器代码工具
# 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 的使用场景.
$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 文档.