| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728 | <?phpnamespace App\Modules\Book\Services;use App\Jobs\RunBookRentention;use App\Modules\Book\Models\Book;use App\Modules\Book\Models\BookCategory;use App\Modules\Book\Models\BookConfig;use App\Modules\Book\Models\NewBookTest;use App\Modules\Book\Models\ReadRecord;use App\Modules\Book\Models\RententionBookChapterUV;use App\Modules\Book\Models\RententionBookList;use App\Modules\Book\Models\RententionBookTaskList;use Illuminate\Support\Collection;/** * 留存书单 */class RententionBookService{    /**     * 爆款待测试     */    const HotStyleWaittingTest  = 11;    /**     * 爆款待精修     */    const HotStyleWaittingRefine  = 12;    /**     * 爆款     */    const HotStyleOK  = 13;    /**     * 优质待测试     */    const HighQualityWaittingTest  = 21;    /**     * 优质待精修     */    const HighQualityWaittingRefine  = 22;    /**     * 优质     */    const HighQualityOK  = 23;    /**     * 不达标     */    const BelowStandard = 3;    const new_status = 1;    const ready_status = 2;    const running_status = 3;    const completed_status = 4;    /**     * 查找书单     * @param string $book_name 书名     * @param int $bid 书号     * @param int $type 类型     * @param bool $is_page 是否分页     */    public static function findBooksNew(array $params, bool $is_page = true)    {        $sql = RententionBookList::leftJoin('new_book_tests', 'new_book_tests.bid', 'rentention_book_list.bid')            ->select('rentention_book_list.*', 'new_book_tests.type as test_type')            ->where('rentention_book_list.is_deleted', 0);        if ($params['book_name']) {            $sql->where('rentention_book_list.book_name', 'like', $params['book_name'] . '%');        }        if ($params['bid']) {            $sql->where('rentention_book_list.bid', $params['bid']);        }        if ($params['type']) {            $sql->where('rentention_book_list.type', $params['type']);        }        if ($params['operate'] && in_array($params['operate'], ['>', '<', '=', '>=', '<=']) && is_numeric($params['arpu'])) {            $sql->where('rentention_book_list.arpu', $params['operate'], $params['arpu']);        }        if ($is_page) {            $lists = $sql->paginate();            $collect = collect($lists->items());        } else {            $lists = $sql->get();            $collect = $lists;        }        $bids = $collect->pluck('bid')->all();        $books = Book::whereIn('id', $bids)->select('id', 'size', 'chapter_count')->get();        foreach ($lists as $k => $v) {            $book = $books->where('id', $v->bid)->first();            $v->chapter_count = $book ? $book->chapter_count : 0;            $v->size = $book ? $book->size : 0;            $lists[$k] = $v;        }        return $lists;    }    /**     * 删除书单中的书籍     * @param int $bid     */    public static function deleteRetentionBook(int $bid)    {        RententionBookList::where('bid', $bid)->update([            'is_deleted' => 1,            'updated_at' => now(),        ]);    }    /**     * 书籍模板     * @param int $bid     * @return RententionBook|null     */    public static function bookTypeModel(int $bid)    {        $model = new HotStyle($bid);        if (!$model->is_up_to_type) {            unset($model);            $model = new HighQuality($bid);            if (!$model->is_up_to_type) {                unset($model);                $model = new BelowStandard($bid);                if (!$model->is_up_to_type) {                    return null;                } else {                    return $model;                }            } else {                return $model;            }        } {            return $model;        }    }    /**     * 书籍模板     * @param int $bid     * @return RententionBook     */    public static function getRententionModel(int $bid)    {        $model = self::bookTypeModel($bid);        if (!$model) {            $model = new RententionBook($bid);        }        return $model;    }    /**     * 保存留存书籍数据     */    public static function saveRententionBook(int $bid, bool $is_force_update = false)    {        $model = self::bookTypeModel($bid);        if ($model) {            $model->saveRententionBook($is_force_update);        } else {            RententionBookList::where('bid', $bid)->update(['type' => 0]);        }    }    /**     * 跑书的留存     */    public static function runRentention(int $bid)    {        (new RententionBook($bid))->runRentention();    }    /**     * 查找任务     */    public static function findRententionBookTasks(array $params, bool $is_page = true)    {        $sql = RententionBookTaskList::where('is_deleted', 0);        if ($params['book_name']) {            $sql->where('book_name', 'like', $params['book_name'] . '%');        }        if ($params['bid']) {            $sql->where('bid', $params['bid']);        }        if ($is_page) {            $lists = $sql->paginate();            $collect = collect($lists->items());        } else {            $lists = $sql->get();            $collect = $lists;        }        $bids = $collect->pluck('bid')->all();        $books = Book::whereIn('id', $bids)->select('id', 'size', 'chapter_count')->get();        $rentention_books = RententionBookList::whereIn('bid', $bids)->select('bid', 'type')->get();        foreach ($lists as $k => $v) {            $book = $books->where('id', $v->bid)->first();            $rentention_book = $rentention_books->where('bid', $v->bid)->first();            $v->chapter_count = $book ? $book->chapter_count : 0;            $v->size = $book ? $book->size : 0;            $v->type = $rentention_book ? $rentention_book->type : 0;            $lists[$k] = $v;        }        return $lists;    }    /**     * 添加书单任务     */    public static function addRententionBookTask(int $bid)    {        $book_config = BookConfig::where('bid', $bid)->select('book_name')->first();        if ($book_config) {            RententionBookTaskList::create([                'bid' => $bid,                'book_name' => $book_config->book_name,                'status' => self::new_status            ]);        }    }    /**     * 更新任务状态     */    public static function updateRententionBookTask(int $id, int $status)    {        $task = RententionBookTaskList::find($id);        if ($task && $status != $task->status) {            $task->status = $status;            $task->save();            if ($status == self::ready_status) {                $job = new RunBookRentention($task->bid);                dispatch($job)->onConnection('rabbitmq')->onQueue('run_book_rentention');            }        }    }    /**     * 删除任务     */    public static function deleteRetentionBookTask(int $id)    {        RententionBookTaskList::where('id', $id)->update(['is_deleted' => 1, 'updated_at' => now()]);    }}/** * 爆款书 */class HotStyle extends RententionBook{    const HotStyle = 1;    const standard_rate = [        30 => 0.0615,        50 => 0.0436,        110 => 0.0309,        170 => 0.0207,        230 => 0.0187,        290 => 0.0155,        350 => 0.0133,        410 => 0.0113,        470 => 0.0106,        530 => 0.0105,    ];    const standard_levea_rate = [        30 => 0.3,    ];    const standard_average_rate = [        30 => 0.166,        50 => 0.254,        110 => 0.153,        170 => 0.101,        230 => 0.155,        290 => 0.125,        350 => 0.125,        410 => 0.226,        470 => 0.438,    ];    const arpu_level = 0.98;    public function __construct(int $bid)    {        $this->standard_rate = self::standard_rate;        $this->standard_levea_rate = self::standard_levea_rate;        $this->standard_average_rate = self::standard_average_rate;        $this->arpu_level = self::arpu_level;        $this->model_type = self::HotStyle;        parent::__construct($bid);    }}/** * 优质书 */class HighQuality  extends RententionBook{    const HighQuality = 2;    const standard_rate = [        30 => 0.0532,        50 => 0.0324,        110 => 0.0224,        170 => 0.0143,        230 => 0.0133,        290 => 0.0108,        350 => 0.0078,        410 => 0.0063,        470 => 0.0050,    ];    const standard_levea_rate = [        30 => 0.5,    ];    const standard_average_rate = [        30 => 0.277,        50 => 0.348,        110 => 0.309,        170 => 0.25,        230 => 0.266,        290 => 0.207,        350 => 0.182,        410 => 0.283,        470 => 0.438,    ];    const arpu_level = 0.65;    public function __construct(int $bid)    {        $this->standard_rate = self::standard_rate;        $this->standard_levea_rate = self::standard_levea_rate;        $this->standard_average_rate = self::standard_average_rate;        $this->arpu_level = self::arpu_level;        $this->model_type = self::HighQuality;        parent::__construct($bid);    }}/** * 不达标的书 */class BelowStandard extends RententionBook{    const BelowStandard = 3;    public function __construct(int $bid)    {        parent::__construct($bid);    }    protected function judgeIsUpToType(array $rates)    {        $book = Book::where('id', $this->bid)->select('id', 'created_at', 'size')->first();        $created_at = $book->created_at;        $is_recently = time() <= strtotime('+1 month', strtotime($created_at));        return $is_recently && ($book->size >= 1000000 || $this->findNewBookExists());    }    protected function judgeType()    {        return self::BelowStandard;    }    /**     * 新书测试是否存在     */    private function findNewBookExists()    {        return NewBookTest::where('bid', $this->bid)->exists();    }}/** * 留存书籍 * @property Collection $uvs 章节留存UV; * @property array $rates 章节留存详情; * @property bool $is_up_to_type 是否符合利率类型; * @property float $arpu 新书测试的apru值; * @property int $type 书籍类型; */class RententionBook{    const chapter_sequence = [        30,        50,        110,        170,        230,        290,        350,        410,        470,        530,    ];    const waitting_test = 1;    const waitting_refine = 2;    const ok = 3;    /**     * 标准留存率     */    protected $standard_rate;    /**     * 标准流失率     */    protected $standard_levea_rate;    /**     * 标准平均流失率     */    protected $standard_average_rate;    /**     * arpu标准值     */    protected $arpu_level;    /**     * 限制章节     */    protected $sequence_limit = 230;    /**     * 书籍类型     */    protected $model_type;    protected $bid;    public function __construct(int $bid)    {        $this->bid = $bid;    }    public function __get($name)    {        if (!isset($this->$name)) {            switch ($name) {                case 'uvs':                    $this->$name = $this->findBookChapterUvs($this->bid);                    break;                case 'rates':                    $this->$name = $this->findRententionRate($this->uvs);                    break;                case 'is_up_to_type':                    if ($this->rates) {                        $this->$name = $this->judgeIsUpToType($this->rates);                    } else {                        $this->$name = false;                    }                    break;                case 'arpu':                    $this->$name = $this->calcBookArpu();                    break;                case 'type':                    $this->$name = $this->judgeType();                    break;            }        }        return $this->$name;    }    /**     * 判断利率是否符合标准利率     */    private function judgeRateUpToStandard(Collection $rates, array $standard_rates, string $operate)    {        return $rates->where('sequence', '<=', $this->sequence_limit)            ->every(function ($item) use ($standard_rates, $operate) {                $sequence = $item['sequence'];                if (isset($standard_rates[$sequence])) {                    return $operate == '>' ? $item['rate'] >= $standard_rates[$sequence] : $item['rate'] <= $standard_rates[$sequence];                } else {                    return true;                }            });    }    /**     * 计算留存率     * @param int $first_chapter_uv 首章uv     * @param int $sequence 章节序号     * @param int $next_sequence 下一章章节序号     */    private function calcRententionRate(int $first_chapter_uv, int $sequence, int $next_sequence)    {        $model = $this->uvs->where('sequence', $sequence)->first();        if ($model) {            $uv = $model ? $model->uv : 0;            $model = $this->uvs->where('sequence', $next_sequence)->first();            $next_uv = $model ? $model->uv : 0;            $chapter_rate = $uv / $first_chapter_uv;            $chapter_leave_rate = 1 - $next_uv / $uv;            $sequence_key = $sequence . '-' . $next_sequence;            if ($next_sequence && $next_uv) {                $period_uv = $this->uvs->where('sequence', '<=', $next_sequence)                    ->where('sequence', '>', $sequence)                    ->sum('uv');                $chapter_average_rate = 1 - $period_uv / ($uv * ($next_sequence - $sequence));            } else {                $chapter_average_rate = 0;            }            return compact('sequence', 'chapter_rate', 'sequence_key', 'chapter_leave_rate', 'chapter_average_rate');        }    }    /**     * 计算新书测试的arpu值     * @return float     */    private function calcBookArpu()    {        $book_test = NewBookTest::where('bid', $this->bid)            ->select('uv_in_one_day', 'sub_amount_in_one_day')            ->orderBy('id', 'desc')            ->first();        if ($book_test && $book_test->uv_in_one_day > 0) {            return round($book_test->sub_amount_in_one_day / 100 / $book_test->uv_in_one_day, 2);        } else {            return 0;        }    }    /**     * 查找书籍留存uv     * @param int $bid     * @return  Collection     */    protected function findBookChapterUvs(int $bid)    {        $sequences = self::chapter_sequence;        $sequence = end($sequences);        return RententionBookChapterUV::where('bid', $bid)            ->where('sequence', '<=', $sequence)            ->select('sequence', 'uv')            ->get();    }    /**     * 查找书籍留存率     * @param int $bid     * @param Collection $uvs     * @return array     */    protected function findRententionRate()    {        $rates = [];        if (count($this->uvs) > 0) {            $model = $this->uvs->where('sequence', 1)->first();            $first_chapter_uv = $model ? $model->uv : 0;            $next_sequence = 0;            $sequences = self::chapter_sequence;            foreach ($sequences as $k => $sequence) {                $next_sequence = $k + 1 < count($sequences) ? $sequences[$k + 1] : 0;                $item = $this->calcRententionRate($first_chapter_uv, $sequence, $next_sequence);                if ($item) {                    $rates[] = $item;                } else {                    break;                }            }        }        return $rates;    }    /**     * 判断类型是否符合(只判断留存率和流失率)     */    protected function judgeIsUpToType(array $rates)    {        $rates = collect($rates);        $chapter_rates = $rates->map(function ($item) {            $data = [];            $data['sequence'] = $item['sequence'];            $data['rate'] = $item['chapter_rate'];            return $data;        });        $leave_rates = $rates->map(function ($item) {            $data = [];            $data['sequence'] = $item['sequence'];            $data['rate'] = $item['chapter_leave_rate'];            return $data;        });        return $this->judgeRateUpToStandard($chapter_rates, $this->standard_rate, '>') &&            $this->judgeRateUpToStandard($leave_rates, $this->standard_levea_rate, '<');    }    /**     * 判断类型     * @return int     */    protected function judgeType()    {        if ($this->is_up_to_type) {            if ($this->arpu <= 0) {                return (int) ($this->model_type . self::waitting_test);            } else {                if ($this->arpu >= $this->arpu_level) {                    return (int) ($this->model_type . self::ok);                } else {                    return (int) ($this->model_type . self::waitting_refine);                }            }        }        return 0;    }    /**     * 跑留存     */    public function runRentention()    {        $result = [];        for ($i = 0; $i < 2048; $i++) {            $records = $this->query(ReadRecord::model($i), $i);            $result = array_merge_recursive($result, $records);        }        $this->saveChapterUV(collect($result));    }    /**     * 查询章节uv数据     */    private function query(ReadRecord $readRecord, int $i)    {        return $readRecord->where('bid', $this->bid)            ->whereIn('uid', function ($query) use ($i) {                $query->select('uid')->from('record_records' . $i)->where('bid', $this->bid)->where('sequence', 1)->where('created_at', '<', date('Y-m-d H:i:s', strtotime('-1 day')));            })            ->groupBy('sequence')            ->selectRaw('count(distinct uid) as uv, sequence')            ->get()            ->toArray();    }    /**     * 保存章节uv     */    private function saveChapterUV(Collection $collection)    {        $groups = $collection->groupBy('sequence');        $exists = RententionBookChapterUV::where('bid', $this->bid)->exists();        if ($exists) {            $groups->each(function ($item, $key) {                RententionBookChapterUV::updateOrCreate([                    'bid' => $this->bid,                    'sequence' => $key,                ], [                    'uv' => $item->sum('uv'),                ]);            });        } else {            $data = $groups->map(function ($item, $key) {                $data = [];                $data['bid'] = $this->bid;                $data['sequence'] = $key;                $data['uv'] = $item->sum('uv');                $data['created_at'] = now();                return $data;            });            RententionBookChapterUV::insert($data->toArray());        }    }    /**     * 判断是否更新     */    private function judgeIsUpdated(int $chapter_count, int $size)    {        $first_uv = $this->uvs->where('sequence', 1)->first();        return  $first_uv && $first_uv->uv > 2000 && ($chapter_count > 530 || $size > 1000000) ? 0 : 1;    }    /**     * 保存留存书籍数据     * @param bool $is_force_update 是否强制更新类型     */    public function saveRententionBook(bool $is_force_update)    {        $book_config = BookConfig::where('bid', $this->bid)->select('book_name', 'is_on_shelf')->first();        $book = Book::where('id', $this->bid)->select('category_id', 'chapter_count', 'size')->first();        if ($book) {            $catetory = BookCategory::find($book->category_id);        }        $sex = $book && $catetory ? $catetory->pid : 0;        $book_name = $book_config ? $book_config->book_name : '';        if ($is_force_update) {            return $this->updateBookType($book_name, $sex);        } else {            $is_updated = $this->judgeIsUpdated($book->chapter_count, $book->size);            if (!$is_updated) {                return $this->saveRententionBookList([                    'type' => $this->type,                    'is_updated' => 0,                    'updated_time' => now(),                ]);            } else {                return $this->updateBookType($book_name, $sex);            }        }    }    /**     * 更新类型     */    private function updateBookType(string $book_name, int $sex)    {        return $this->saveRententionBookList([            'arpu' => $this->arpu,            'book_name' => $book_name,            'type' => $this->type,            'type_generate_time' => now(),            'sex' => $sex,        ]);    }    /**     * 保存书单     */    public function saveRententionBookList(array $data)    {        RententionBookList::updateOrCreate([            'bid' => $this->bid,        ], $data);    }}
 |