IncreaseViewsListener.php 6.8 KB
<?php
/**
+-----------------------------------------------------------------------------------------------------------------------
 * 事件层监听:浏览量自增事件类
+-----------------------------------------------------------------------------------------------------------------------
 *
 * PHP version 7
 *
 * @category  App\Listeners
 * @package   App\Listeners
 * @author    Richer <yangzi1028@163.com>
 * @date      2020年12月28日14:01:24
 * @copyright 2021-2022 Richer (http://www.Richer.com/)
 * @license   http://www.Richer.com/ License
 * @link      http://www.Richer.com/
 */
namespace App\Listeners;

use App\Events\IncreaseViews;
use App\Models\Traits\DeviceDetectorTrait;
use DeviceDetector\Parser\Client\Browser;
use Illuminate\Support\Facades\Redis;
use Illuminate\Support\Facades\Schema;

class IncreaseViewsListener
{
    use DeviceDetectorTrait;

    /**
     * 同一post最大访问次数,再刷新数据库
     */
    const VIEW_LIMIT = 10; // redis 中访问了多少次后更新数据库

    /**
     * 同一用户浏览同一post过期时间
     */
    const EXPIRE_SECOND   = 10; // 用户该时间段内多次刷新只算一次

    /**
     * Create the event listener.
     *
     * @return void
     */
    public function __construct()
    {
        //
    }

    /**
     * Handle the event.
     *
     * @param  IncreaseViews  $event
     * @return void
     */
    public function handle(IncreaseViews $event)
    {
        $model  = $event->model;
        $ip     = request()->ip();
        $table  = $model::TABLE;
        $id     = $model->id;

        //首先判断下 EXPIRE_SECOND 秒时间内,同一IP访问多次,仅仅作为1次访问量
        if ($this->ipViewLimit($table, $id, $ip)) {
            //一个IP在 EXPIRE_SECOND 秒时间内访问第一次时,刷新下该对象的浏览量
            $this->updateCacheViewCount($model, $table, $id, $ip);
        }
    }

    /**
     * 一段时间内,限制同一IP访问,防止增加无效浏览次数
     * @param $table
     * @param $id
     * @param $ip
     * @return bool
     */
    public function ipViewLimit($table, $id, $ip): bool
    {
//        $ip = '1.1.1.6';
        //redis中键值分割都以:来做,可以理解为PHP的命名空间namespace一样
        $ipViewKey    = $table.':ip:limit:'.$id;
        //Redis命令SISMEMBER检查集合类型Set中有没有该键,该指令时间复杂度O(1),Set集合类型中值都是唯一
        $existsInRedisSet = Redis::command('SISMEMBER', [$ipViewKey, $ip]);

        if (!$existsInRedisSet) {
            //SADD,集合类型指令,向ipPostViewKey键中加一个值ip
            Redis::command('SADD', [$ipViewKey, $ip]);
            //并给该键设置生命时间,这里设置300秒,300秒后同一IP访问就当做是新的浏览量了
            Redis::command('EXPIRE', [$ipViewKey, self::EXPIRE_SECOND]);
            return true;
        }

        return false;
    }

    /**
     * 不同用户访问,更新缓存中浏览次数
     *
     * @param $model
     * @param $table
     * @param $id
     * @param $ip
     */
    public function updateCacheViewCount($model, $table, $id, $ip)
    {
        $cacheKey        = $table.':view:'.$id;
//        dump($cacheKey);
        // 这里以Redis哈希类型存储键,就和数组类似,$cacheKey就类似数组名,$ip为$key.HEXISTS指令判断$key是否存在$cacheKey中
        if (Redis::command('HEXISTS', [$cacheKey, $ip])) {
            //哈希类型指令HINCRBY,就是给$cacheKey[$ip]加上一个值,这里一次访问就是1
            $incre_count = Redis::command('HINCRBY', [$cacheKey, $ip, 1]);
//            dump("incre_count:$incre_count");
            //redis中这个存储浏览量的值达到30后,就往MySQL里刷下,这样就不需要每一次浏览,来一次query,效率不高
            if ($incre_count == self::VIEW_LIMIT) {
                $this->updateModelViewCount($model, $table, $id, $incre_count);
                // 本篇post,redis中浏览量刷进MySQL后,把该对象的浏览量键抹掉,等着下一次请求重新开始计数
                Redis::command('HDEL', [$cacheKey, $ip]);
                //同时,抹掉post内容的缓存键,这样就不用等10分钟后再更新view_count了,
                //如该篇post在100秒内就达到了30访问量,就在3分钟时更新下MySQL,并把缓存抹掉,下一次请求就从MySQL中请求到最新的view_count,
                //当然,100秒内view_count还是缓存的旧数据,极端情况300秒内都是旧数据,而缓存里已经有了29个新增访问量
                //实际上也可以这样做:在缓存post的时候,可以把view_count单独拿出来存入键值里如single_view_count,每一次都是给这个值加1,然后把这个值传入视图里
                //或者平衡设置下VIEW_LIMIT和EXPIRE_SECOND这两个参数,对于view_count这种实时性要求不高的可以这样做来着
                //加上laravel前缀,因为Cache::remember会自动在每一个key前加上laravel前缀,可以看cache.php中这个字段:'prefix' => 'laravel'
                Redis::command('DEL', ['laravel:'.$table.':cache:'.$id]);
            }
        } else {
            //哈希类型指令HSET,和数组类似,就像$cacheKey[$ip] = 1;
            Redis::command('HSET', [$cacheKey, $ip, '1']);
        }
    }

    /**
     * 更新DB中post浏览次数
     * @param $model
     * @param $count
     * @return bool
     */
    public function updateModelViewCount($model, $table, $id, $count)
    {
//        dump($count);
//        dump(Schema::hasColumn($table, 'views'));
        // 判断是否有该字段 ,如果有该字段就在访问的时候进行增加一次点击量
        if (Schema::hasColumn($table, 'views')) {
//            $model->where('id', $id)->increment("views", 1);// 自增1
            $model->views += $count;
            $model->save();

            // 判断是否存在方法,不存在退出
//            if (method_exists($model, 'accessRecords')) {
//                $model->accessRecords()->create([
//                    'client_type'   => $this->getDeviceType(),
//                    'ip'            => request()->ip(),
//                    'method'        => request()->method(),
//                    'device_family' => Browser::deviceFamily(),
//                    'device_model' => Browser::deviceModel(),
//                    'mobile_grade' => Browser::mobileGrade(),
//                    'platform_name' => Browser::platformName(),
//                    'platform_family' => Browser::platformFamily(),
//                    'platform_version' => Browser::platformVersion(),
//                    'user_agent' => Browser::userAgent(),
//                    'input     ' => json_encode(request()->all()),
//                    'url' => request()->fullUrl(),
//                ]);
//
//                return true;
//            }
        }
    }
}