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); } }