123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728 |
- <?php
- namespace 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);
- }
- }
|