RententionBookService.php 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728
  1. <?php
  2. namespace App\Modules\Book\Services;
  3. use App\Jobs\RunBookRentention;
  4. use App\Modules\Book\Models\Book;
  5. use App\Modules\Book\Models\BookCategory;
  6. use App\Modules\Book\Models\BookConfig;
  7. use App\Modules\Book\Models\NewBookTest;
  8. use App\Modules\Book\Models\ReadRecord;
  9. use App\Modules\Book\Models\RententionBookChapterUV;
  10. use App\Modules\Book\Models\RententionBookList;
  11. use App\Modules\Book\Models\RententionBookTaskList;
  12. use Illuminate\Support\Collection;
  13. /**
  14. * 留存书单
  15. */
  16. class RententionBookService
  17. {
  18. /**
  19. * 爆款待测试
  20. */
  21. const HotStyleWaittingTest = 11;
  22. /**
  23. * 爆款待精修
  24. */
  25. const HotStyleWaittingRefine = 12;
  26. /**
  27. * 爆款
  28. */
  29. const HotStyleOK = 13;
  30. /**
  31. * 优质待测试
  32. */
  33. const HighQualityWaittingTest = 21;
  34. /**
  35. * 优质待精修
  36. */
  37. const HighQualityWaittingRefine = 22;
  38. /**
  39. * 优质
  40. */
  41. const HighQualityOK = 23;
  42. /**
  43. * 不达标
  44. */
  45. const BelowStandard = 3;
  46. const new_status = 1;
  47. const ready_status = 2;
  48. const running_status = 3;
  49. const completed_status = 4;
  50. /**
  51. * 查找书单
  52. * @param string $book_name 书名
  53. * @param int $bid 书号
  54. * @param int $type 类型
  55. * @param bool $is_page 是否分页
  56. */
  57. public static function findBooksNew(array $params, bool $is_page = true)
  58. {
  59. $sql = RententionBookList::leftJoin('new_book_tests', 'new_book_tests.bid', 'rentention_book_list.bid')
  60. ->select('rentention_book_list.*', 'new_book_tests.type as test_type')
  61. ->where('rentention_book_list.is_deleted', 0);
  62. if ($params['book_name']) {
  63. $sql->where('rentention_book_list.book_name', 'like', $params['book_name'] . '%');
  64. }
  65. if ($params['bid']) {
  66. $sql->where('rentention_book_list.bid', $params['bid']);
  67. }
  68. if ($params['type']) {
  69. $sql->where('rentention_book_list.type', $params['type']);
  70. }
  71. if ($params['operate'] && in_array($params['operate'], ['>', '<', '=', '>=', '<=']) && is_numeric($params['arpu'])) {
  72. $sql->where('rentention_book_list.arpu', $params['operate'], $params['arpu']);
  73. }
  74. if ($is_page) {
  75. $lists = $sql->paginate();
  76. $collect = collect($lists->items());
  77. } else {
  78. $lists = $sql->get();
  79. $collect = $lists;
  80. }
  81. $bids = $collect->pluck('bid')->all();
  82. $books = Book::whereIn('id', $bids)->select('id', 'size', 'chapter_count')->get();
  83. foreach ($lists as $k => $v) {
  84. $book = $books->where('id', $v->bid)->first();
  85. $v->chapter_count = $book ? $book->chapter_count : 0;
  86. $v->size = $book ? $book->size : 0;
  87. $lists[$k] = $v;
  88. }
  89. return $lists;
  90. }
  91. /**
  92. * 删除书单中的书籍
  93. * @param int $bid
  94. */
  95. public static function deleteRetentionBook(int $bid)
  96. {
  97. RententionBookList::where('bid', $bid)->update([
  98. 'is_deleted' => 1,
  99. 'updated_at' => now(),
  100. ]);
  101. }
  102. /**
  103. * 书籍模板
  104. * @param int $bid
  105. * @return RententionBook|null
  106. */
  107. public static function bookTypeModel(int $bid)
  108. {
  109. $model = new HotStyle($bid);
  110. if (!$model->is_up_to_type) {
  111. unset($model);
  112. $model = new HighQuality($bid);
  113. if (!$model->is_up_to_type) {
  114. unset($model);
  115. $model = new BelowStandard($bid);
  116. if (!$model->is_up_to_type) {
  117. return null;
  118. } else {
  119. return $model;
  120. }
  121. } else {
  122. return $model;
  123. }
  124. } {
  125. return $model;
  126. }
  127. }
  128. /**
  129. * 书籍模板
  130. * @param int $bid
  131. * @return RententionBook
  132. */
  133. public static function getRententionModel(int $bid)
  134. {
  135. $model = self::bookTypeModel($bid);
  136. if (!$model) {
  137. $model = new RententionBook($bid);
  138. }
  139. return $model;
  140. }
  141. /**
  142. * 保存留存书籍数据
  143. */
  144. public static function saveRententionBook(int $bid, bool $is_force_update = false)
  145. {
  146. $model = self::bookTypeModel($bid);
  147. if ($model) {
  148. $model->saveRententionBook($is_force_update);
  149. } else {
  150. RententionBookList::where('bid', $bid)->update(['type' => 0]);
  151. }
  152. }
  153. /**
  154. * 跑书的留存
  155. */
  156. public static function runRentention(int $bid)
  157. {
  158. (new RententionBook($bid))->runRentention();
  159. }
  160. /**
  161. * 查找任务
  162. */
  163. public static function findRententionBookTasks(array $params, bool $is_page = true)
  164. {
  165. $sql = RententionBookTaskList::where('is_deleted', 0);
  166. if ($params['book_name']) {
  167. $sql->where('book_name', 'like', $params['book_name'] . '%');
  168. }
  169. if ($params['bid']) {
  170. $sql->where('bid', $params['bid']);
  171. }
  172. if ($is_page) {
  173. $lists = $sql->paginate();
  174. $collect = collect($lists->items());
  175. } else {
  176. $lists = $sql->get();
  177. $collect = $lists;
  178. }
  179. $bids = $collect->pluck('bid')->all();
  180. $books = Book::whereIn('id', $bids)->select('id', 'size', 'chapter_count')->get();
  181. $rentention_books = RententionBookList::whereIn('bid', $bids)->select('bid', 'type')->get();
  182. foreach ($lists as $k => $v) {
  183. $book = $books->where('id', $v->bid)->first();
  184. $rentention_book = $rentention_books->where('bid', $v->bid)->first();
  185. $v->chapter_count = $book ? $book->chapter_count : 0;
  186. $v->size = $book ? $book->size : 0;
  187. $v->type = $rentention_book ? $rentention_book->type : 0;
  188. $lists[$k] = $v;
  189. }
  190. return $lists;
  191. }
  192. /**
  193. * 添加书单任务
  194. */
  195. public static function addRententionBookTask(int $bid)
  196. {
  197. $book_config = BookConfig::where('bid', $bid)->select('book_name')->first();
  198. if ($book_config) {
  199. RententionBookTaskList::create([
  200. 'bid' => $bid,
  201. 'book_name' => $book_config->book_name,
  202. 'status' => self::new_status
  203. ]);
  204. }
  205. }
  206. /**
  207. * 更新任务状态
  208. */
  209. public static function updateRententionBookTask(int $id, int $status)
  210. {
  211. $task = RententionBookTaskList::find($id);
  212. if ($task && $status != $task->status) {
  213. $task->status = $status;
  214. $task->save();
  215. if ($status == self::ready_status) {
  216. $job = new RunBookRentention($task->bid);
  217. dispatch($job)->onConnection('rabbitmq')->onQueue('run_book_rentention');
  218. }
  219. }
  220. }
  221. /**
  222. * 删除任务
  223. */
  224. public static function deleteRetentionBookTask(int $id)
  225. {
  226. RententionBookTaskList::where('id', $id)->update(['is_deleted' => 1, 'updated_at' => now()]);
  227. }
  228. }
  229. /**
  230. * 爆款书
  231. */
  232. class HotStyle extends RententionBook
  233. {
  234. const HotStyle = 1;
  235. const standard_rate = [
  236. 30 => 0.0615,
  237. 50 => 0.0436,
  238. 110 => 0.0309,
  239. 170 => 0.0207,
  240. 230 => 0.0187,
  241. 290 => 0.0155,
  242. 350 => 0.0133,
  243. 410 => 0.0113,
  244. 470 => 0.0106,
  245. 530 => 0.0105,
  246. ];
  247. const standard_levea_rate = [
  248. 30 => 0.3,
  249. ];
  250. const standard_average_rate = [
  251. 30 => 0.166,
  252. 50 => 0.254,
  253. 110 => 0.153,
  254. 170 => 0.101,
  255. 230 => 0.155,
  256. 290 => 0.125,
  257. 350 => 0.125,
  258. 410 => 0.226,
  259. 470 => 0.438,
  260. ];
  261. const arpu_level = 0.98;
  262. public function __construct(int $bid)
  263. {
  264. $this->standard_rate = self::standard_rate;
  265. $this->standard_levea_rate = self::standard_levea_rate;
  266. $this->standard_average_rate = self::standard_average_rate;
  267. $this->arpu_level = self::arpu_level;
  268. $this->model_type = self::HotStyle;
  269. parent::__construct($bid);
  270. }
  271. }
  272. /**
  273. * 优质书
  274. */
  275. class HighQuality extends RententionBook
  276. {
  277. const HighQuality = 2;
  278. const standard_rate = [
  279. 30 => 0.0532,
  280. 50 => 0.0324,
  281. 110 => 0.0224,
  282. 170 => 0.0143,
  283. 230 => 0.0133,
  284. 290 => 0.0108,
  285. 350 => 0.0078,
  286. 410 => 0.0063,
  287. 470 => 0.0050,
  288. ];
  289. const standard_levea_rate = [
  290. 30 => 0.5,
  291. ];
  292. const standard_average_rate = [
  293. 30 => 0.277,
  294. 50 => 0.348,
  295. 110 => 0.309,
  296. 170 => 0.25,
  297. 230 => 0.266,
  298. 290 => 0.207,
  299. 350 => 0.182,
  300. 410 => 0.283,
  301. 470 => 0.438,
  302. ];
  303. const arpu_level = 0.65;
  304. public function __construct(int $bid)
  305. {
  306. $this->standard_rate = self::standard_rate;
  307. $this->standard_levea_rate = self::standard_levea_rate;
  308. $this->standard_average_rate = self::standard_average_rate;
  309. $this->arpu_level = self::arpu_level;
  310. $this->model_type = self::HighQuality;
  311. parent::__construct($bid);
  312. }
  313. }
  314. /**
  315. * 不达标的书
  316. */
  317. class BelowStandard extends RententionBook
  318. {
  319. const BelowStandard = 3;
  320. public function __construct(int $bid)
  321. {
  322. parent::__construct($bid);
  323. }
  324. protected function judgeIsUpToType(array $rates)
  325. {
  326. $book = Book::where('id', $this->bid)->select('id', 'created_at', 'size')->first();
  327. $created_at = $book->created_at;
  328. $is_recently = time() <= strtotime('+1 month', strtotime($created_at));
  329. return $is_recently && ($book->size >= 1000000 || $this->findNewBookExists());
  330. }
  331. protected function judgeType()
  332. {
  333. return self::BelowStandard;
  334. }
  335. /**
  336. * 新书测试是否存在
  337. */
  338. private function findNewBookExists()
  339. {
  340. return NewBookTest::where('bid', $this->bid)->exists();
  341. }
  342. }
  343. /**
  344. * 留存书籍
  345. * @property Collection $uvs 章节留存UV;
  346. * @property array $rates 章节留存详情;
  347. * @property bool $is_up_to_type 是否符合利率类型;
  348. * @property float $arpu 新书测试的apru值;
  349. * @property int $type 书籍类型;
  350. */
  351. class RententionBook
  352. {
  353. const chapter_sequence = [
  354. 30,
  355. 50,
  356. 110,
  357. 170,
  358. 230,
  359. 290,
  360. 350,
  361. 410,
  362. 470,
  363. 530,
  364. ];
  365. const waitting_test = 1;
  366. const waitting_refine = 2;
  367. const ok = 3;
  368. /**
  369. * 标准留存率
  370. */
  371. protected $standard_rate;
  372. /**
  373. * 标准流失率
  374. */
  375. protected $standard_levea_rate;
  376. /**
  377. * 标准平均流失率
  378. */
  379. protected $standard_average_rate;
  380. /**
  381. * arpu标准值
  382. */
  383. protected $arpu_level;
  384. /**
  385. * 限制章节
  386. */
  387. protected $sequence_limit = 230;
  388. /**
  389. * 书籍类型
  390. */
  391. protected $model_type;
  392. protected $bid;
  393. public function __construct(int $bid)
  394. {
  395. $this->bid = $bid;
  396. }
  397. public function __get($name)
  398. {
  399. if (!isset($this->$name)) {
  400. switch ($name) {
  401. case 'uvs':
  402. $this->$name = $this->findBookChapterUvs($this->bid);
  403. break;
  404. case 'rates':
  405. $this->$name = $this->findRententionRate($this->uvs);
  406. break;
  407. case 'is_up_to_type':
  408. if ($this->rates) {
  409. $this->$name = $this->judgeIsUpToType($this->rates);
  410. } else {
  411. $this->$name = false;
  412. }
  413. break;
  414. case 'arpu':
  415. $this->$name = $this->calcBookArpu();
  416. break;
  417. case 'type':
  418. $this->$name = $this->judgeType();
  419. break;
  420. }
  421. }
  422. return $this->$name;
  423. }
  424. /**
  425. * 判断利率是否符合标准利率
  426. */
  427. private function judgeRateUpToStandard(Collection $rates, array $standard_rates, string $operate)
  428. {
  429. return $rates->where('sequence', '<=', $this->sequence_limit)
  430. ->every(function ($item) use ($standard_rates, $operate) {
  431. $sequence = $item['sequence'];
  432. if (isset($standard_rates[$sequence])) {
  433. return $operate == '>' ? $item['rate'] >= $standard_rates[$sequence] : $item['rate'] <= $standard_rates[$sequence];
  434. } else {
  435. return true;
  436. }
  437. });
  438. }
  439. /**
  440. * 计算留存率
  441. * @param int $first_chapter_uv 首章uv
  442. * @param int $sequence 章节序号
  443. * @param int $next_sequence 下一章章节序号
  444. */
  445. private function calcRententionRate(int $first_chapter_uv, int $sequence, int $next_sequence)
  446. {
  447. $model = $this->uvs->where('sequence', $sequence)->first();
  448. if ($model) {
  449. $uv = $model ? $model->uv : 0;
  450. $model = $this->uvs->where('sequence', $next_sequence)->first();
  451. $next_uv = $model ? $model->uv : 0;
  452. $chapter_rate = $uv / $first_chapter_uv;
  453. $chapter_leave_rate = 1 - $next_uv / $uv;
  454. $sequence_key = $sequence . '-' . $next_sequence;
  455. if ($next_sequence && $next_uv) {
  456. $period_uv = $this->uvs->where('sequence', '<=', $next_sequence)
  457. ->where('sequence', '>', $sequence)
  458. ->sum('uv');
  459. $chapter_average_rate = 1 - $period_uv / ($uv * ($next_sequence - $sequence));
  460. } else {
  461. $chapter_average_rate = 0;
  462. }
  463. return compact('sequence', 'chapter_rate', 'sequence_key', 'chapter_leave_rate', 'chapter_average_rate');
  464. }
  465. }
  466. /**
  467. * 计算新书测试的arpu值
  468. * @return float
  469. */
  470. private function calcBookArpu()
  471. {
  472. $book_test = NewBookTest::where('bid', $this->bid)
  473. ->select('uv_in_one_day', 'sub_amount_in_one_day')
  474. ->orderBy('id', 'desc')
  475. ->first();
  476. if ($book_test && $book_test->uv_in_one_day > 0) {
  477. return round($book_test->sub_amount_in_one_day / 100 / $book_test->uv_in_one_day, 2);
  478. } else {
  479. return 0;
  480. }
  481. }
  482. /**
  483. * 查找书籍留存uv
  484. * @param int $bid
  485. * @return Collection
  486. */
  487. protected function findBookChapterUvs(int $bid)
  488. {
  489. $sequences = self::chapter_sequence;
  490. $sequence = end($sequences);
  491. return RententionBookChapterUV::where('bid', $bid)
  492. ->where('sequence', '<=', $sequence)
  493. ->select('sequence', 'uv')
  494. ->get();
  495. }
  496. /**
  497. * 查找书籍留存率
  498. * @param int $bid
  499. * @param Collection $uvs
  500. * @return array
  501. */
  502. protected function findRententionRate()
  503. {
  504. $rates = [];
  505. if (count($this->uvs) > 0) {
  506. $model = $this->uvs->where('sequence', 1)->first();
  507. $first_chapter_uv = $model ? $model->uv : 0;
  508. $next_sequence = 0;
  509. $sequences = self::chapter_sequence;
  510. foreach ($sequences as $k => $sequence) {
  511. $next_sequence = $k + 1 < count($sequences) ? $sequences[$k + 1] : 0;
  512. $item = $this->calcRententionRate($first_chapter_uv, $sequence, $next_sequence);
  513. if ($item) {
  514. $rates[] = $item;
  515. } else {
  516. break;
  517. }
  518. }
  519. }
  520. return $rates;
  521. }
  522. /**
  523. * 判断类型是否符合(只判断留存率和流失率)
  524. */
  525. protected function judgeIsUpToType(array $rates)
  526. {
  527. $rates = collect($rates);
  528. $chapter_rates = $rates->map(function ($item) {
  529. $data = [];
  530. $data['sequence'] = $item['sequence'];
  531. $data['rate'] = $item['chapter_rate'];
  532. return $data;
  533. });
  534. $leave_rates = $rates->map(function ($item) {
  535. $data = [];
  536. $data['sequence'] = $item['sequence'];
  537. $data['rate'] = $item['chapter_leave_rate'];
  538. return $data;
  539. });
  540. return $this->judgeRateUpToStandard($chapter_rates, $this->standard_rate, '>') &&
  541. $this->judgeRateUpToStandard($leave_rates, $this->standard_levea_rate, '<');
  542. }
  543. /**
  544. * 判断类型
  545. * @return int
  546. */
  547. protected function judgeType()
  548. {
  549. if ($this->is_up_to_type) {
  550. if ($this->arpu <= 0) {
  551. return (int) ($this->model_type . self::waitting_test);
  552. } else {
  553. if ($this->arpu >= $this->arpu_level) {
  554. return (int) ($this->model_type . self::ok);
  555. } else {
  556. return (int) ($this->model_type . self::waitting_refine);
  557. }
  558. }
  559. }
  560. return 0;
  561. }
  562. /**
  563. * 跑留存
  564. */
  565. public function runRentention()
  566. {
  567. $result = [];
  568. for ($i = 0; $i < 2048; $i++) {
  569. $records = $this->query(ReadRecord::model($i), $i);
  570. $result = array_merge_recursive($result, $records);
  571. }
  572. $this->saveChapterUV(collect($result));
  573. }
  574. /**
  575. * 查询章节uv数据
  576. */
  577. private function query(ReadRecord $readRecord, int $i)
  578. {
  579. return $readRecord->where('bid', $this->bid)
  580. ->whereIn('uid', function ($query) use ($i) {
  581. $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')));
  582. })
  583. ->groupBy('sequence')
  584. ->selectRaw('count(distinct uid) as uv, sequence')
  585. ->get()
  586. ->toArray();
  587. }
  588. /**
  589. * 保存章节uv
  590. */
  591. private function saveChapterUV(Collection $collection)
  592. {
  593. $groups = $collection->groupBy('sequence');
  594. $exists = RententionBookChapterUV::where('bid', $this->bid)->exists();
  595. if ($exists) {
  596. $groups->each(function ($item, $key) {
  597. RententionBookChapterUV::updateOrCreate([
  598. 'bid' => $this->bid,
  599. 'sequence' => $key,
  600. ], [
  601. 'uv' => $item->sum('uv'),
  602. ]);
  603. });
  604. } else {
  605. $data = $groups->map(function ($item, $key) {
  606. $data = [];
  607. $data['bid'] = $this->bid;
  608. $data['sequence'] = $key;
  609. $data['uv'] = $item->sum('uv');
  610. $data['created_at'] = now();
  611. return $data;
  612. });
  613. RententionBookChapterUV::insert($data->toArray());
  614. }
  615. }
  616. /**
  617. * 判断是否更新
  618. */
  619. private function judgeIsUpdated(int $chapter_count, int $size)
  620. {
  621. $first_uv = $this->uvs->where('sequence', 1)->first();
  622. return $first_uv && $first_uv->uv > 2000 && ($chapter_count > 530 || $size > 1000000) ? 0 : 1;
  623. }
  624. /**
  625. * 保存留存书籍数据
  626. * @param bool $is_force_update 是否强制更新类型
  627. */
  628. public function saveRententionBook(bool $is_force_update)
  629. {
  630. $book_config = BookConfig::where('bid', $this->bid)->select('book_name', 'is_on_shelf')->first();
  631. $book = Book::where('id', $this->bid)->select('category_id', 'chapter_count', 'size')->first();
  632. if ($book) {
  633. $catetory = BookCategory::find($book->category_id);
  634. }
  635. $sex = $book && $catetory ? $catetory->pid : 0;
  636. $book_name = $book_config ? $book_config->book_name : '';
  637. if ($is_force_update) {
  638. return $this->updateBookType($book_name, $sex);
  639. } else {
  640. $is_updated = $this->judgeIsUpdated($book->chapter_count, $book->size);
  641. if (!$is_updated) {
  642. return $this->saveRententionBookList([
  643. 'type' => $this->type,
  644. 'is_updated' => 0,
  645. 'updated_time' => now(),
  646. ]);
  647. } else {
  648. return $this->updateBookType($book_name, $sex);
  649. }
  650. }
  651. }
  652. /**
  653. * 更新类型
  654. */
  655. private function updateBookType(string $book_name, int $sex)
  656. {
  657. return $this->saveRententionBookList([
  658. 'arpu' => $this->arpu,
  659. 'book_name' => $book_name,
  660. 'type' => $this->type,
  661. 'type_generate_time' => now(),
  662. 'sex' => $sex,
  663. ]);
  664. }
  665. /**
  666. * 保存书单
  667. */
  668. public function saveRententionBookList(array $data)
  669. {
  670. RententionBookList::updateOrCreate([
  671. 'bid' => $this->bid,
  672. ], $data);
  673. }
  674. }