UserRecentlyRead.php 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530
  1. <?php
  2. namespace app\common\model;
  3. use app\common\library\Redis;
  4. use app\main\helper\ArrayHelper;
  5. use app\main\service\UserService;
  6. use think\Log;
  7. use think\Model;
  8. use think\Cookie;
  9. use think\Db;
  10. class UserRecentlyRead extends Model
  11. {
  12. // 表名
  13. protected $table = 'user_recently_read';
  14. // 自动写入时间戳字段
  15. protected $autoWriteTimestamp = 'int';
  16. // 定义时间戳字段名
  17. protected $createTime = 'createtime';
  18. protected $updateTime = 'updatetime';
  19. // 追加属性
  20. protected $append = [
  21. ];
  22. //分库分表查询
  23. protected $db;
  24. //当前链接的user_id分库
  25. protected $connectUserId = null;
  26. /**
  27. * 设置分库链接数据
  28. * @param $user_id
  29. * @return $this
  30. */
  31. public function setConnect($user_id)
  32. {
  33. if ($this->connectUserId != $user_id) {
  34. $database = get_db_connect($this->table, $user_id);
  35. $this->setTable($database['table']);
  36. $this->connect($database);
  37. $this->connectUserId = $user_id;
  38. }
  39. return $this;
  40. }
  41. public function getone($user_id, $book_id)
  42. {
  43. return $this->setConnect($user_id)->where(['user_id' => $user_id, 'book_id' => $book_id])->find();
  44. }
  45. /**
  46. * @deprecated
  47. * @param $recentId
  48. * @param $book_id
  49. * @param null $user_id
  50. * @return bool
  51. */
  52. public function removeBook($recentId,$book_id,$user_id = null)
  53. {
  54. if(!$user_id) {
  55. $user_id = UserService::instance()->getUserInfo()->id;
  56. }
  57. $db_res = $this->setConnect($user_id)->where(['id'=>$recentId])->update(['flag' => 0]);
  58. if ($db_res) {
  59. $redis = Redis::instance();
  60. $key = $this->getURKey($user_id);
  61. $ubKey = $this->getUBKey($user_id, $book_id);
  62. $redis->zrem($key, $ubKey);
  63. $redis->del($ubKey);
  64. return true;
  65. }
  66. return false;
  67. }
  68. /**
  69. * 得到最近阅读记录
  70. * @param int $updatetime
  71. * @param int $pageSize
  72. * @param null $userId
  73. * @param bool $getBookInfo 是否获取书籍对象信息
  74. * @return array totalNum用户总阅读记录数量 data查到的阅读记录对象
  75. * @throws \Exception
  76. */
  77. public function getRecentlyRead($updatetime = 0, $pageSize = 10, $userId = null, $getBookInfo = false)
  78. {
  79. $data = [];
  80. if (!$userId) {
  81. $userId = UserService::instance()->getUserInfo()->id;
  82. }
  83. $redis = Redis::instance();
  84. $urKey = $this->getURKey($userId);
  85. if ($updatetime) {
  86. $pageBegin = $updatetime - 1;
  87. $resUR = $redis->zrevrangebyscore($urKey, $pageBegin, '-inf', array('limit' => array(0, $pageSize)));
  88. } else {
  89. $pageBegin = 0;
  90. $resUR = $redis->zrevrange($urKey, 0, $pageSize - 1);
  91. }
  92. if (empty($resUR)) {
  93. $res = $this->getRecentlyByDb($userId, $pageBegin, $pageSize);
  94. $result = $res['data'];
  95. $count = $res['count'];
  96. } else {
  97. $count = $redis->zCard($urKey);
  98. $result = $this->getRecentlyByRedis($userId, $resUR);
  99. }
  100. #region 向最近阅读结果中填充书籍信息
  101. if ($getBookInfo) {
  102. $bookIds = array_column($result, 'book_id');
  103. $booksInfo = model('Book')->getBooksInfo($bookIds);
  104. foreach ($result as &$item) {
  105. $bookId = $item['book_id'];
  106. if (isset($booksInfo[$bookId])) {
  107. $book = $booksInfo[$bookId];
  108. $item['image'] = $book['image'];
  109. $item['book_name'] = $book['name'];
  110. $item['last_chapter_name'] = $book['last_chapter_name'];
  111. $item['last_chapter_utime'] = $book['last_chapter_utime'];
  112. $item['state'] = $book['state'];
  113. $item['free_stime'] = $book['free_stime'];
  114. $item['free_etime'] = $book['free_etime'];
  115. $item['first_chapter_id'] = $book['first_chapter_id'];
  116. }
  117. }
  118. }
  119. #endregion
  120. $data['totalNum'] = $count;
  121. $data['data'] = $result;
  122. return $data;
  123. }
  124. /**
  125. * 从数据库中获取最近阅读记录
  126. * @param $userId
  127. * @param $pageBegin
  128. * @param $pageSize
  129. * @return array
  130. */
  131. private function getRecentlyByDb($userId, $pageBegin, $pageSize)
  132. {
  133. $redis = Redis::instance();
  134. $page = 0;
  135. $pageSz = 100;
  136. $recentlyData = [];
  137. #region 循环从数据库中获取用户阅读记录信息
  138. while (true) {
  139. $offset = $page * $pageSz;
  140. $recentlyPageData = $this->setConnect($userId)->where(['user_id' => $userId, 'flag' => 1])
  141. ->limit($offset, $pageSz)
  142. ->order('updatetime', 'desc')->select();
  143. if (count($recentlyPageData) == 0) {
  144. break;
  145. } elseif (count($recentlyPageData) == $pageSz) {
  146. $recentlyData = array_merge($recentlyData, $recentlyPageData);
  147. $page++;
  148. } elseif (count($recentlyPageData) < $pageSz) {
  149. $recentlyData = array_merge($recentlyData, $recentlyPageData);
  150. break;
  151. }
  152. }
  153. $count = count($recentlyData);
  154. #endregion
  155. #region 阅读记录转成数组
  156. $aRecentlyData = [];
  157. array_walk($recentlyData, function ($recentlyDatum) use (&$aRecentlyData) {
  158. $aRecentlyData[] = is_array($recentlyDatum) ? $recentlyDatum : $recentlyDatum->toArray();
  159. });
  160. #endregion
  161. #region 阅读记录列表插入key为U-R的zset结构的redis缓存中
  162. $redisKey = $this->getURKey($userId);
  163. $aUR_Value = [$redisKey];//构造U-R的值列表,U-R为zset类型,0=>reidsKey,1=>score1,2=>value1,3=>score2,4=>value2...
  164. $aUB_Key = [];//U-B的key的列表,U-B为hash类型。
  165. array_walk($aRecentlyData, function ($item) use (&$aUR_Value, &$aUB_Key, $userId) {
  166. $bookId = $item['book_id'];
  167. $userReadBookKey = $this->getUBKey($userId, $bookId);
  168. $aUR_Value[] = $item['updatetime'];
  169. $aUR_Value[] = $userReadBookKey;
  170. $aUB_Key[] = $userReadBookKey;
  171. });
  172. call_user_func_array(array($redis, 'zadd'), $aUR_Value);
  173. $redis->expire($redisKey, 43200);
  174. #endregion
  175. #region 阅读记录详细信息写入key为U-B的hash结构的redis缓存中
  176. $getUBRedisIndex = Redis::splitKeysByRule($aUB_Key);
  177. array_walk($getUBRedisIndex, function ($v, $k) use ($aRecentlyData) {
  178. $redis = Redis::getRedisConnect($k);
  179. $pipe = $redis->multi(\Redis::PIPELINE);
  180. array_walk($v, function ($UB_Key) use ($aRecentlyData, &$pipe) {
  181. $aKeys = explode(':', $UB_Key);
  182. $bookId = $aKeys[2];
  183. $recentItem = ArrayHelper::array_column_search($aRecentlyData, 'book_id', $bookId);
  184. $pipe->hmset($UB_Key, $recentItem);
  185. $pipe->expire($UB_Key, 4320);
  186. });
  187. $pipe->exec();
  188. });
  189. #endregion
  190. #region 按updatetime输出前$pageSize条数据
  191. $result = [];
  192. foreach ($aRecentlyData as $recentlyItem) {
  193. if (count($result) >= $pageSize) {
  194. break;
  195. }
  196. if ($pageBegin == 0) {
  197. $result[] = $recentlyItem;
  198. } elseif ($recentlyItem['updatetime'] < $pageBegin) {
  199. $result[] = $recentlyItem;
  200. }
  201. }
  202. #endregion
  203. $r = ['data' => $result, 'count' => $count];
  204. return $r;
  205. }
  206. /**
  207. * 从redis分页读取最近阅读记录
  208. * @param $userId
  209. * @param $resUR
  210. * @return array|null
  211. * @throws \Exception
  212. */
  213. private function getRecentlyByRedis($userId, $resUR)
  214. {
  215. $redis = Redis::instance();
  216. #region 获取U-R中获取最近阅读书籍列表
  217. $returnArr = [];
  218. $bookIdsFromUR = [];
  219. array_walk($resUR, function ($UB_key) use (&$bookIdsFromUR) {
  220. $aKeys = explode(':', $UB_key);
  221. $bookIdsFromUR[] = $aKeys[2];
  222. });
  223. #endregion
  224. #region 从redis中获取U-B信息
  225. $getUBKeyRedisIndex = Redis::splitKeysByRule($resUR);
  226. array_walk($getUBKeyRedisIndex, function ($v, $k) use (&$returnArr) {
  227. $redis = Redis::getRedisConnect($k);
  228. $pipe = $redis->multi(\Redis::PIPELINE);
  229. array_walk($v, function ($UB_Key) use (&$pipe) {
  230. $pipe->hgetall($UB_Key);
  231. });
  232. $aRecently = $pipe->exec();
  233. $aRecently = array_filter($aRecently);
  234. $returnArr = array_merge($returnArr, $aRecently);
  235. });
  236. #endregion
  237. $bookIdsFromUB = array_column($returnArr, 'book_id');
  238. $diffBookIds = array_diff($bookIdsFromUR, $bookIdsFromUB);#在U-R中存在,但是在U-B中不存在的书籍id
  239. if (!empty($diffBookIds)) {
  240. #region 从数据库中获取当前用户不在U-B中的书籍的阅读记录
  241. $recentListFromDb = $this->setConnect($userId)->where(['user_id' => $userId, 'flag' => 1])
  242. ->whereIn('book_id', $diffBookIds)->select();
  243. $aRecentFromDb = [];
  244. $aUBKeyWrite = [];//从数据库中获得的阅读记录,需要写入redis,rediskey
  245. array_walk($recentListFromDb, function ($item) use (&$aRecentFromDb, &$aUBKeyWrite, $userId) {
  246. $recentItem=$item->toArray();
  247. $aRecentFromDb[] = $recentItem;
  248. $aUBKeyWrite[] = $this->getUBKey($userId, $recentItem['book_id']);
  249. });
  250. #endregion
  251. #region 分片写入redis
  252. $getUBKeyWriteRedisIndex = Redis::splitKeysByRule($aUBKeyWrite);
  253. array_walk($getUBKeyWriteRedisIndex, function ($v, $k) use ($aRecentFromDb) {
  254. $redis = Redis::getRedisConnect($k);
  255. $pipe = $redis->multi(\Redis::PIPELINE);
  256. array_walk($v, function ($UB_key) use (&$pipe, $aRecentFromDb) {
  257. $aKeys = explode(':', $UB_key);
  258. $bookId = $aKeys[2];
  259. $recentItem = ArrayHelper::array_column_search($aRecentFromDb, 'book_id', $bookId);
  260. $pipe->hmset($UB_key, $recentItem);
  261. $pipe->expire($UB_key, 43200);
  262. });
  263. $pipe->exec();
  264. });
  265. #endregion
  266. $returnArr = array_merge($returnArr, $aRecentFromDb);
  267. }
  268. #region U-R中存在的记录,但是在U-B中不存在,需要在U-R中删除
  269. $UB_BookIds = array_column($returnArr, 'book_id');
  270. $removeBookIds = array_diff($bookIdsFromUR, $UB_BookIds);
  271. if (!empty($removeBookIds)) {
  272. $removeBookKeys = [];
  273. foreach ($removeBookIds as $removeBookId) {
  274. $removeBookKeys[] = $this->getUBKey($userId, $removeBookId);
  275. }
  276. array_unshift($removeBookKeys, $this->getURKey($userId));
  277. call_user_func_array([$redis, 'zrem'], $removeBookKeys);
  278. }
  279. #endregion
  280. //结果集使用updatetime逆序排序
  281. $sortArr = array_column($returnArr, 'updatetime');
  282. array_multisort($sortArr, SORT_DESC, $returnArr);
  283. return $returnArr;
  284. }
  285. public function getUBKey($userId, $bookId)
  286. {
  287. return "U-B:$userId:$bookId";
  288. }
  289. public function getURKey($userId)
  290. {
  291. return "U-R:$userId";
  292. }
  293. public function getUSKey($userId)
  294. {
  295. return "U-S:" . $userId;
  296. }
  297. public function getShelfRecentlyByDB($userId, $pageBegin, $pageSize)
  298. {
  299. $redis = Redis::instance();
  300. $page = 0;
  301. $pageSz = 100;
  302. $recentlyData = [];
  303. #region 循环从数据库中获取用户阅读记录信息
  304. while (true) {
  305. $offset = $page * $pageSz;
  306. $recentlyPageData = $this->setConnect($userId)->where([
  307. 'user_id' => $userId,
  308. 'book_shelf_add' => 1,
  309. 'book_shelf_flag' => 1
  310. ])
  311. ->limit($offset, $pageSz)
  312. ->order('updatetime', 'desc')->select();
  313. if (count($recentlyPageData) == 0) {
  314. break;
  315. } elseif (count($recentlyPageData) == $pageSz) {
  316. $recentlyData = array_merge($recentlyData, $recentlyPageData);
  317. $page++;
  318. } elseif (count($recentlyPageData) < $pageSz) {
  319. $recentlyData = array_merge($recentlyData, $recentlyPageData);
  320. break;
  321. }
  322. }
  323. #endregion
  324. #region 阅读记录转成数组
  325. $aRecentlyData = [];
  326. array_walk($recentlyData, function ($recentlyDatum) use (&$aRecentlyData) {
  327. $aRecentlyData[] = is_array($recentlyDatum) ? $recentlyDatum : $recentlyDatum->toArray();
  328. });
  329. #endregion
  330. #region 阅读记录列表插入key为U-S的zset结构的redis缓存中
  331. $redisKey = $this->getUSKey($userId);
  332. $aUS_Value = [$redisKey];//构造U-S的值列表,U-S为zset类型,0=>reidsKey,1=>score1,2=>value1,3=>score2,4=>value2...
  333. $aUB_Key = [];//U-B的key的列表,U-B为hash类型。
  334. array_walk($aRecentlyData, function ($item) use (&$aUS_Value, &$aUB_Key, $userId) {
  335. $bookId = $item['book_id'];
  336. $userReadBookKey = $this->getUBKey($userId, $bookId);
  337. $aUS_Value[] = $item['updatetime'];
  338. $aUS_Value[] = $userReadBookKey;
  339. $aUB_Key[] = $userReadBookKey;
  340. });
  341. call_user_func_array(array($redis, 'zadd'), $aUS_Value);
  342. $redis->expire($redisKey, 43200);
  343. #endregion
  344. #region 阅读记录详细信息写入key为U-B的hash结构的redis缓存中
  345. $getUBRedisIndex = Redis::splitKeysByRule($aUB_Key);
  346. array_walk($getUBRedisIndex, function ($v, $k) use ($aRecentlyData) {
  347. $redis = Redis::getRedisConnect($k);
  348. $pipe = $redis->multi(\Redis::PIPELINE);
  349. array_walk($v, function ($UB_Key) use ($aRecentlyData, &$pipe) {
  350. $aKeys = explode(':', $UB_Key);
  351. $bookId = $aKeys[2];
  352. $recentItem = ArrayHelper::array_column_search($aRecentlyData, 'book_id', $bookId);
  353. $pipe->hmset($UB_Key, $recentItem);
  354. $pipe->expire($UB_Key, 4320);
  355. });
  356. $pipe->exec();
  357. });
  358. #endregion
  359. #region 按updatetime输出前$pageSize条数据
  360. $result = [];
  361. foreach ($aRecentlyData as $recentlyItem) {
  362. if (count($result) >= $pageSize) {
  363. break;
  364. }
  365. if ($pageBegin == 0) {
  366. $result[] = $recentlyItem;
  367. } elseif ($recentlyItem['updatetime'] < $pageBegin) {
  368. $result[] = $recentlyItem;
  369. }
  370. }
  371. #endregion
  372. return $result;
  373. }
  374. public function getShelfRecentlyByRedis($userId, $resUS)
  375. {
  376. $redis = Redis::instance();
  377. #region 获取U-S中获取最近阅读书籍列表
  378. $returnArr = [];
  379. $bookIdsFromUS = [];
  380. array_walk($resUS, function ($UB_key) use (&$bookIdsFromUS) {
  381. $aKeys = explode(':', $UB_key);
  382. $bookIdsFromUS[] = $aKeys[2];
  383. });
  384. #endregion
  385. #region 从redis中获取U-B信息
  386. $getUBKeyRedisIndex = Redis::splitKeysByRule($resUS);
  387. array_walk($getUBKeyRedisIndex, function ($v, $k) use (&$returnArr) {
  388. $redis = Redis::getRedisConnect($k);
  389. $pipe = $redis->multi(\Redis::PIPELINE);
  390. array_walk($v, function ($UB_Key) use (&$pipe) {
  391. $pipe->hgetall($UB_Key);
  392. });
  393. $aRecently = $pipe->exec();
  394. $aRecently = array_filter($aRecently);
  395. $returnArr = array_merge($returnArr, $aRecently);
  396. });
  397. #endregion
  398. $bookIdsFromUB = array_column($returnArr, 'book_id');
  399. $diffBookIds = array_diff($bookIdsFromUS, $bookIdsFromUB);#在U-S中存在,但是在U-B中不存在的书籍id
  400. if (!empty($diffBookIds)) {
  401. #region 从数据库中获取当前用户不在U-B中的书籍的阅读记录
  402. $recentListFromDb = $this->setConnect($userId)->where([
  403. 'user_id' => $userId,
  404. 'book_shelf_add' => 1,
  405. 'book_shelf_flag' => 1
  406. ])->whereIn('book_id', $diffBookIds)->select();
  407. $aRecentFromDb = [];
  408. $aUBKeyWrite = [];//从数据库中获得的阅读记录,需要写入redis,rediskey
  409. array_walk($recentListFromDb, function ($item) use (&$aRecentFromDb, &$aUBKeyWrite, $userId) {
  410. $recentItem = $item->toArray();
  411. $aRecentFromDb[] = $recentItem;
  412. $aUBKeyWrite[] = $this->getUBKey($userId, $recentItem['book_id']);
  413. });
  414. #endregion
  415. #region 分片写入redis
  416. $getUBKeyWriteRedisIndex = Redis::splitKeysByRule($aUBKeyWrite);
  417. array_walk($getUBKeyWriteRedisIndex, function ($v, $k) use ($aRecentFromDb) {
  418. $redis = Redis::getRedisConnect($k);
  419. $pipe = $redis->multi(\Redis::PIPELINE);
  420. array_walk($v, function ($UB_key) use (&$pipe, $aRecentFromDb) {
  421. $aKeys = explode(':', $UB_key);
  422. $bookId = $aKeys[2];
  423. $recentItem = ArrayHelper::array_column_search($aRecentFromDb, 'book_id', $bookId);
  424. $pipe->hmset($UB_key, $recentItem);
  425. $pipe->expire($UB_key, 43200);
  426. });
  427. $pipe->exec();
  428. });
  429. #endregion
  430. $returnArr = array_merge($returnArr, $aRecentFromDb);
  431. }
  432. #region U-R中存在的记录,但是在U-B中不存在,需要在U-R中删除
  433. $UB_BookIds = array_column($returnArr, 'book_id');
  434. $removeBookIds = array_diff($bookIdsFromUS, $UB_BookIds);
  435. if (!empty($removeBookIds)) {
  436. $removeBookKeys = [];
  437. foreach ($removeBookIds as $removeBookId) {
  438. $removeBookKeys[] = $this->getUBKey($userId, $removeBookId);
  439. }
  440. array_unshift($removeBookKeys, $this->getURKey($userId));
  441. call_user_func_array([$redis, 'zrem'], $removeBookKeys);
  442. }
  443. #endregion
  444. //结果集使用updatetime逆序排序
  445. $sortArr = array_column($returnArr, 'updatetime');
  446. array_multisort($sortArr, SORT_DESC, $returnArr);
  447. return $returnArr;
  448. }
  449. /*
  450. * 阅读记录记入cookie 只记录5条数据
  451. */
  452. public function recentCookie($book_id, $chapterinfo)
  453. {
  454. $read_log = Cookie::get('read') ? Cookie::get('read') : [];
  455. unset($read_log[$book_id]);
  456. if (count($read_log) >= 5) {
  457. $first = array_keys($read_log)[0];
  458. unset($read_log[$first]);
  459. }
  460. $wait['book_id'] = $book_id;
  461. $wait['chapter_id'] = $chapterinfo['id'];
  462. $wait['chapter_name'] = $chapterinfo['name'];
  463. $wait['updatetime'] = time();
  464. $read_log[$book_id] = $wait;
  465. Cookie::set('read', $read_log);
  466. }
  467. }