BookService.php 44 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047
  1. <?php
  2. namespace App\Services\Book;
  3. use App\Cache\StatisticCache;
  4. use App\Cache\UserCache;
  5. use App\Consts\BaseConst;
  6. use App\Consts\CoinConst;
  7. use App\Consts\ErrorConst;
  8. use App\Facade\Site;
  9. use App\Libs\Utils;
  10. use App\Models\Book\Book;
  11. use App\Models\Book\BookConfig;
  12. use App\Models\Book\Category;
  13. use App\Models\Book\Chapter;
  14. use App\Models\User\User;
  15. use App\Services\OpenApi\OpenService;
  16. use App\Services\Subscribe\ChapterOrderService;
  17. use App\Services\User\UserService;
  18. use Illuminate\Support\Facades\DB;
  19. use Illuminate\Support\Facades\Log;
  20. use Illuminate\Support\Facades\Redis;
  21. use Vinkla\Hashids\Facades\Hashids;
  22. class BookService
  23. {
  24. protected $userService;
  25. protected $openService;
  26. protected $chapterOrderService;
  27. public function __construct(UserService $userService, OpenService $openService, ChapterOrderService $chapterOrderService)
  28. {
  29. $this->userService = $userService;
  30. $this->openService = $openService;
  31. $this->chapterOrderService = $chapterOrderService;
  32. }
  33. /**
  34. * 书籍列表
  35. *
  36. * @param $data
  37. * @return mixed
  38. */
  39. public function getBookList($data)
  40. {
  41. $distribution_channel_id = Site::getCurrentChannelId();
  42. $gender = getProp($data, 'gender', 2); // 男女频
  43. $book_name = trim(getProp($data, 'book_name')); // 书名
  44. $charge_type = getProp($data, 'charge_type'); // 篇幅(长篇:CHAPTER 短篇:BOOK)
  45. $status = getProp($data, 'status'); // 连载状态(0.连载中 1.已完结)
  46. $roles = getProp($data, 'roles'); // 主角名称
  47. $page_number = getProp($data, 'page_number');
  48. $query = Book::leftJoin('book_configs', 'books.id', 'book_configs.bid')->whereIn('book_configs.is_on_shelf', [1, 2])
  49. ->leftJoin('book_categories', 'book_categories.id', 'books.ncategory_id')
  50. ->leftJoin('channel_book_charge_type', function ($join) use ($distribution_channel_id) {
  51. $join->on('channel_book_charge_type.bid', '=', 'books.id')->where('channel_book_charge_type.distribution_channel_id', '=', $distribution_channel_id);
  52. })
  53. ->leftJoin('books as b1', 'b1.id', 'book_configs.origin_bid')
  54. ->where('books.gender', $gender)->select('books.id', 'books.name', 'books.intro', 'books.cover',
  55. 'book_configs.origin_bid', 'books.author', 'books.category_name', 'books.gender', 'books.status',
  56. 'book_categories.category_name as ncategory_name', 'books.chapter_count', 'books.size', 'book_configs.book_name',
  57. 'book_configs.roles', 'book_configs.charge_type', 'books.created_at', 'b1.name as origin_book_name',
  58. 'channel_book_charge_type.book_calculate_price_type', 'channel_book_charge_type.book_coin');
  59. if ($book_name) {
  60. $bid = BookConfig::where('book_name', $book_name)->value('bid');
  61. if ($bid) { // 如果精准搜索到书籍
  62. $query->where('books.id', $bid)->orWhere('book_configs.origin_bid', $bid)->orderBy('book_configs.origin_bid');
  63. } else {
  64. $query->where('book_configs.book_name', 'like', '%' . $book_name . '%');
  65. }
  66. } else {
  67. $query->where('book_configs.origin_bid', 0);
  68. }
  69. if ($charge_type) {
  70. $query->where('book_configs.charge_type', $charge_type);
  71. }
  72. if ($status !== '') {
  73. $query->where('books.status', $status);
  74. }
  75. if ($roles) {
  76. $query->where('book_configs.roles', 'like', '%' . $roles . '%');
  77. }
  78. if ($page_number) {
  79. return $query->orderBy('books.created_at', 'desc')->paginate($page_number);
  80. } else {
  81. return $query->orderBy('books.created_at', 'desc')->paginate();
  82. }
  83. }
  84. public function simpleBookConfigs($bids)
  85. {
  86. return BookConfig::select('bid', 'book_name')->whereIn('bid', $bids)->get();
  87. }
  88. /**
  89. * 热门搜索
  90. *
  91. * @return array
  92. */
  93. public function getHotSearches()
  94. {
  95. $result = DB::table('hot_search_words')->get()->pluck('hot_word')->toArray();
  96. $result = array_unique($result);
  97. shuffle($result);
  98. return $result;
  99. }
  100. /**
  101. * 书籍详情
  102. *
  103. * @param $data
  104. * @return array
  105. */
  106. public function getBookDetail($data)
  107. {
  108. Log::info('书籍详情页入参: ' . json_encode($data, 256));
  109. $uid = Site::getUid();
  110. $user_info = User::find($uid);
  111. $bid = getProp($data, 'bid');
  112. if (mb_strlen($bid) === 32) {
  113. $bid = Hashids::decode($bid)[0];
  114. }
  115. if (!$bid) {
  116. Utils::throwError(ErrorConst::DATA_EXCEPTION);
  117. }
  118. $result = Book::leftJoin('book_configs', 'book_configs.bid', 'books.id')
  119. ->where('books.id', $bid)
  120. ->whereIn('book_configs.is_on_shelf', [1, 2])
  121. ->select('books.id as bid', 'books.name as book_name', 'books.category_name', 'books.category_id',
  122. 'books.size', 'books.status', 'books.cover', 'books.intro')->first();
  123. if (!$result) return [];
  124. $book['detail'] = $result->toArray();
  125. $book['user'] = [
  126. 'balance' => getProp($user_info, 'balance', 0),
  127. 'charge_balance' => getProp($user_info, 'charge_balance', 0),
  128. 'reward_balance' => getProp($user_info, 'reward_balance', 0),
  129. ];
  130. // 获取本书前两章内容
  131. $front_double_chapters = Chapter::leftJoin('chapter_contents', 'chapter_contents.id', 'chapters.chapter_content_id')
  132. ->select('chapters.id as cid', 'chapters.name as chapter_name', 'chapter_contents.content as chapter_content')
  133. ->where('chapters.bid', $bid)->where(['chapters.is_check' => 1, 'chapters.is_deleted' => 0])->orderBy('sequence')
  134. ->limit(2)->get()->toArray();
  135. $book['first_chapter'] = $book['second_chapter'] = [];
  136. $book['front_double_chapter_contents'] = '';
  137. $i = 1;
  138. foreach ($front_double_chapters as &$item) {
  139. $book['front_double_chapter_contents'] .= $item['chapter_name'] . PHP_EOL . filterContent($item['chapter_content']) . PHP_EOL . PHP_EOL;
  140. $item['cid'] = Hashids::encode($item['cid']);
  141. $item['chapter_content'] = filterContent($item['chapter_content']);
  142. if ($i === 1) $book['first_chapter'] = $item;
  143. if ($i === 2) $book['second_chapter'] = $item;
  144. $i++;
  145. }
  146. // 获取最新章节
  147. $book['latest_chapter'] = Chapter::where(['bid' => $bid, 'is_check' => 1, 'is_deleted' => 0])->orderBy('sequence', 'desc')
  148. ->select('id as cid', 'name as chapter_name')->first();
  149. if ($book['latest_chapter']) {
  150. $book['latest_chapter'] = $book['latest_chapter']->toArray();
  151. $book['latest_chapter']['cid'] = Hashids::encode($book['latest_chapter']['cid']);
  152. $book['latest_chapter']['status'] = $book['detail']['status'];
  153. } else {
  154. $book['latest_chapter'] = [];
  155. }
  156. // 格式化书籍数据
  157. $book['detail']['cover'] = addPrefix($book['detail']['cover']);
  158. $book['detail']['size'] = round($result['size'] / 10000, 1) . '万';
  159. $book['detail']['status_info'] = (int)$result['status'] === 1 ? '已完结' : '连载中';
  160. $book['detail']['bid'] = Hashids::encode($bid);
  161. if (!$book['detail']['category_name']) $book['detail']['category_name'] = '其他';
  162. // 获取充值排名前20的书籍(每次都随机排序)
  163. $charge_books = $this->getBookOrderByCharge($book['detail']['category_id']);
  164. $book['recommend_books'] = [];
  165. $max = DB::table('books')->count('id');
  166. if ($max < 20) $max -= 1;
  167. else $max = 19;
  168. $arr_index = range(0, $max);
  169. shuffle($arr_index);
  170. $arr_index = array_slice($arr_index, 0, 6);
  171. foreach ($arr_index as $index) {
  172. if (isset($charge_books[$index])) {
  173. $arr = $charge_books[$index];
  174. $book['recommend_books'][] = [
  175. 'bid' => Hashids::encode($arr['id']),
  176. 'cover' => addPrefix($arr['cover']),
  177. 'book_name' => $arr['name'],
  178. ];
  179. }
  180. }
  181. $book['promotion_code'] = '';
  182. // 生成视频推广口令码
  183. if ($uid) {
  184. $book['promotion_code'] = Utils::getPasswordCode($uid, $bid);
  185. }
  186. // 获取分享链接中的隐藏参数(如果存在隐藏参数邀请码则绑定)
  187. $invite_code = getProp($data, 'invite_code');
  188. $from_uid = User::where('invite_code', $invite_code)->value('id');
  189. if ($from_uid) {
  190. $info = $this->userService->bind($from_uid, $uid); // 绑定邀请码
  191. Log::info('书籍详情页邀请码绑定结果-----from_uid: ' . $from_uid . ';uid: ' . $uid . ';info: ' . $info);
  192. }
  193. // 获取分享链接中的隐藏参数(如果存在隐藏参数派单id则绑定)
  194. $send_order_id = getProp($data, 'send_order_id');
  195. if ($send_order_id && $uid) {
  196. $info = $this->userService->bindSendOrder($send_order_id); // 绑定派单链接
  197. Log::info('书籍详情页派单链接绑定结果-----send_order_id: ' . $send_order_id . ';uid: ' . $uid . ';info: ' . $info);
  198. }
  199. return $book;
  200. }
  201. /**
  202. * 获取某本书的阅读记录
  203. *
  204. * @param $data
  205. * @return array|false|mixed
  206. * @throws \App\Exceptions\ApiException
  207. */
  208. public function recentChapter($data)
  209. {
  210. $uid = Site::getUid();
  211. $bid = getProp($data, 'bid');
  212. if (mb_strlen($bid) === 32) {
  213. $bid = Hashids::decode($bid)[0];
  214. }
  215. if (!$bid) {
  216. Utils::throwError(ErrorConst::DATA_EXCEPTION);
  217. }
  218. $recent_book = UserCache::getRecentBooksByBid($uid, $bid);
  219. if ($recent_book) {
  220. $recent_book['bid'] = Hashids::encode($recent_book['bid']);
  221. $recent_book['cid'] = Hashids::encode($recent_book['cid']);
  222. } else {
  223. $chapter = Chapter::leftJoin('books', 'books.id', 'chapters.bid')
  224. ->leftJoin('book_configs', 'chapters.bid', 'book_configs.bid')->whereIn('book_configs.is_on_shelf', [1, 2])
  225. ->where(['chapters.bid' => $bid, 'chapters.is_check' => 1, 'chapters.is_deleted' => 0])
  226. ->orderBy('chapters.sequence')->select('chapters.bid', 'books.name as book_name', 'books.cover', 'chapters.id as cid',
  227. 'chapters.name as chapter_name', 'chapters.sequence', 'books.gender')->first();
  228. if ($chapter) {
  229. $chapter = $chapter->toArray();
  230. $chapter['bid'] = Hashids::encode($chapter['bid']);
  231. $chapter['cid'] = Hashids::encode($chapter['cid']);
  232. $chapter['read_time'] = '';
  233. $recent_book = $chapter;
  234. } else {
  235. $recent_book = new \stdClass();
  236. }
  237. }
  238. return $recent_book;
  239. }
  240. /**
  241. * 生成分享链接参数(小程序内部调用)
  242. *
  243. * @param $data
  244. * @return array|string
  245. * @throws \App\Exceptions\ApiException
  246. * @throws \GuzzleHttp\Exception\GuzzleException
  247. */
  248. public function setUrlLink($data)
  249. {
  250. $type = getProp($data, 'type');
  251. $send_order_id = getProp($data, 'send_order_id');
  252. $distribution_channel_id = getProp($data, 'distribution_channel_id');
  253. $template_id = getProp($data, 'template_id', env('SHARE_TEMPLATE_ID'));
  254. $uid = Site::getUid();
  255. $invite_code = User::where('id', $uid)->value('invite_code');
  256. if (!$invite_code) $invite_code = '';
  257. $data['invite_code'] = $invite_code;
  258. Log::info('小程序内部调用分享链接参数: ' . json_encode($data, 256));
  259. $bid = getProp($data, 'bid');
  260. $cid = getProp($data, 'cid');
  261. if (mb_strlen($bid) === 32) {
  262. $bid = Hashids::decode($bid)[0];
  263. }
  264. if (mb_strlen($cid) === 32) {
  265. $cid = Hashids::decode($cid)[0];
  266. }
  267. // 如果有派单链接,则替换bid和cid
  268. if ($send_order_id) {
  269. $send_order = DB::table('send_orders')->where('id', $send_order_id)->select('distribution_channel_id', 'book_id', 'chapter_id')->first();
  270. if ($send_order) {
  271. $bid = getProp($send_order, 'book_id');
  272. $cid = getProp($send_order, 'chapter_id');
  273. $distribution_channel_id = getProp($send_order, 'distribution_channel_id');
  274. }
  275. }
  276. $promotion_code = '';
  277. if ($bid) $promotion_code = DB::table('user_book_code')->where(['bid' => $bid, 'uid' => $uid])->value('id');
  278. $arr = [
  279. 'path' => 'pages/index/index',
  280. 'bid' => $bid ? Hashids::encode($bid) : '',
  281. 'cid' => $cid ? Hashids::encode($cid) : '',
  282. 'invite_code' => $invite_code,
  283. 'promotion_code' => $promotion_code,
  284. 'send_order_id' => $send_order_id,
  285. 'distribution_channel_id' => $distribution_channel_id,
  286. ];
  287. if ($send_order_id) $arr['jump'] = 'no'; // 增加阅读页跳转到选定章节的限制
  288. switch ($type) {
  289. case 'friend_url': // 私信朋友
  290. if ($bid) {
  291. if (!$cid) { // 未设置章节则默认第一章
  292. $first_cid = Chapter::where('bid', $bid)->where(['is_check' => 1, 'is_deleted' => 0])->orderBy('sequence')->limit(1)->value('id');
  293. $arr['cid'] = Hashids::encode($first_cid);
  294. }
  295. $arr['path'] = 'pages/reader/index';
  296. }
  297. break;
  298. case 'promotion_url':
  299. $arr['path'] = 'pages/detail/index';
  300. break;
  301. default:
  302. break;
  303. }
  304. // 统计(uv|pv)
  305. StatisticCache::setPV('set_url_link');
  306. if ($uid) StatisticCache::setUV('set_url_link', $uid);
  307. $path = $arr['path'];
  308. unset($arr['path']);
  309. return '/' . $path . '?' . http_build_query($arr);
  310. }
  311. /**
  312. * 生成分享链接(外网)
  313. *
  314. * @param $data
  315. * @return array|string
  316. * @throws \App\Exceptions\ApiException
  317. * @throws \GuzzleHttp\Exception\GuzzleException
  318. */
  319. public function setDyLink($data)
  320. {
  321. $type = getProp($data, 'type');
  322. $send_order_id = getProp($data, 'send_order_id');
  323. $distribution_channel_id = getProp($data, 'distribution_channel_id');
  324. $template_id = getProp($data, 'template_id', env('SHARE_TEMPLATE_ID'));
  325. $uid = Site::getUid();
  326. $invite_code = User::where('id', $uid)->value('invite_code');
  327. if (!$invite_code) $invite_code = '';
  328. $data['invite_code'] = $invite_code;
  329. Log::info('外网分享链接参数: ' . json_encode($data, 256));
  330. $bid = getProp($data, 'bid');
  331. $cid = getProp($data, 'cid');
  332. if (mb_strlen($bid) === 32) {
  333. $bid = Hashids::decode($bid)[0];
  334. }
  335. if (mb_strlen($cid) === 32) {
  336. $cid = Hashids::decode($cid)[0];
  337. }
  338. // 如果有派单链接,则替换bid和cid
  339. if ($send_order_id) {
  340. $send_order = DB::table('send_orders')->where('id', $send_order_id)->select('distribution_channel_id', 'book_id', 'chapter_id')->first();
  341. if ($send_order) {
  342. $bid = getProp($send_order, 'book_id');
  343. $cid = getProp($send_order, 'chapter_id');
  344. $distribution_channel_id = getProp($send_order, 'distribution_channel_id');
  345. }
  346. }
  347. $promotion_code = '';
  348. if ($bid) $promotion_code = DB::table('user_book_code')->where(['bid' => $bid, 'uid' => $uid])->value('id');
  349. $arr = [
  350. 'path' => 'pages/index/index',
  351. 'bid' => $bid ? Hashids::encode($bid) : '',
  352. 'cid' => $cid ? Hashids::encode($cid) : '',
  353. 'invite_code' => $invite_code,
  354. 'promotion_code' => $promotion_code,
  355. 'send_order_id' => $send_order_id,
  356. 'distribution_channel_id' => $distribution_channel_id,
  357. ];
  358. if ($send_order_id) $arr['jump'] = 'no'; // 增加阅读页跳转到选定章节的限制
  359. switch ($type) {
  360. case 'friend_url': // 私信朋友
  361. if ($bid) {
  362. if (!$cid) { // 未设置章节则默认第一章
  363. $first_cid = Chapter::where('bid', $bid)->where(['is_check' => 1, 'is_deleted' => 0])->orderBy('sequence')->limit(1)->value('id');
  364. $arr['cid'] = Hashids::encode($first_cid);
  365. }
  366. $arr['path'] = 'pages/reader/index';
  367. }
  368. break;
  369. case 'promotion_url':
  370. $arr['path'] = 'pages/detail/index';
  371. break;
  372. default:
  373. break;
  374. }
  375. // 统计(uv|pv)
  376. StatisticCache::setPV('set_dy_link');
  377. if ($uid) StatisticCache::setUV('set_dy_link', $uid);
  378. $path = $arr['path'];
  379. unset($arr['path']);
  380. $json = json_encode($arr, 256);
  381. // 调用抖音三方库生成分享链接
  382. $instance = $this->openService->getInstance(['sandbox' => env('DOUYIN_APP_SANDBOX')]);
  383. $urlLink = $instance->generateShareLink($path, $json);
  384. if (isset($urlLink['url_link']) && $send_order_id) {
  385. DB::table('send_orders')->where('id', $send_order_id)->update([
  386. 'send_order_url' => $urlLink['url_link'],
  387. 'updated_at' => date('Y-m-d H:i:s'),
  388. ]);
  389. }
  390. return $urlLink ? $urlLink : ['err_no' => 1, 'err_tips' => '', 'url_link' => ''];
  391. }
  392. /**
  393. * 轮播图
  394. *
  395. * @param $gender
  396. * @return mixed
  397. */
  398. public function getBanners($gender)
  399. {
  400. return Book::rightJoin('home_book_configs', 'home_book_configs.bid', 'books.zw_id')
  401. ->leftJoin('book_configs', 'books.id', 'book_configs.bid')->whereIn('book_configs.is_on_shelf', [1, 2])
  402. ->where('home_book_configs.type', 'banner')->where('home_book_configs.gender', $gender)
  403. ->select('books.id', 'books.cover', 'books.name', 'books.intro')->get()->toArray();
  404. }
  405. /**
  406. * 获取原创精选书籍
  407. *
  408. * @param $gender
  409. * @return mixed
  410. */
  411. public function getFeatureBooks($gender)
  412. {
  413. return Book::rightJoin('home_book_configs', 'home_book_configs.bid', 'books.zw_id')
  414. ->leftJoin('book_configs', 'books.id', 'book_configs.bid')->whereIn('book_configs.is_on_shelf', [1, 2])
  415. ->where('home_book_configs.type', 'feature')->where('home_book_configs.gender', $gender)
  416. ->select('books.id', 'books.name', 'books.cover', 'books.intro', 'books.gender', 'books.category_name')->get()->toArray();
  417. }
  418. /**
  419. * 获取当前阅读书籍
  420. *
  421. * @return array|mixed
  422. */
  423. public function getCurrentBook()
  424. {
  425. $uid = Site::getUid();
  426. if (!$uid) return [];
  427. return UserCache::getCurrentChapter($uid);
  428. }
  429. /**
  430. * 获取猜你喜欢书籍
  431. *
  432. * @param $gender
  433. * @return mixed
  434. */
  435. public function getFavoriteBooks($gender)
  436. {
  437. $uid = Site::getUid();
  438. $channel_type = Site::getChannelType();
  439. $result = [];
  440. // 获取当前阅读的redis数据
  441. $recent_books = UserCache::getRecentBooksKeys($uid);
  442. // 屏蔽首页banner图和精选好书模块的书籍(避免重复)
  443. $filter_bids = DB::table('home_book_configs')->where('gender', $gender)->select('bid')->get()->pluck('bid')->toArray();
  444. if ($recent_books) { // 有分类则推荐分类书籍
  445. $recent_categories = DB::table('books')->whereIn('id', $recent_books)->get()->pluck('category_id')->toArray();
  446. $query = Book::leftJoin('book_configs', 'books.id', 'book_configs.bid')
  447. ->whereIn('book_configs.is_on_shelf', [1, 2])->whereNotIn('books.zw_id', $filter_bids)
  448. ->select('books.id', 'books.name', 'books.intro', 'books.cover', 'books.author', 'books.category_name',
  449. 'books.gender')->whereIn('books.category_id', array_unique($recent_categories));
  450. // 会员制站点只展示短篇书籍
  451. // if ($channel_type == 'PERIOD') {
  452. // $query->where('book_configs.charge_type', 'BOOK');
  453. // }
  454. $result = $query->where('books.gender', $gender)->paginate();
  455. }
  456. // 无阅读分类则推荐默认书单
  457. if (!$result) {
  458. $query = Book::leftJoin('book_configs', 'books.id', 'book_configs.bid')
  459. ->whereIn('book_configs.is_on_shelf', [1, 2])->whereNotIn('books.zw_id', $filter_bids)
  460. ->select('books.id', 'books.name', 'books.intro', 'books.cover', 'books.author', 'books.category_name',
  461. 'books.gender');
  462. // 会员制站点只展示短篇书籍
  463. // if ($channel_type == 'PERIOD') {
  464. // $query->where('book_configs.charge_type', 'BOOK');
  465. // }
  466. $result = $query->where('books.gender', $gender)->paginate();
  467. }
  468. return $result;
  469. }
  470. /**
  471. * 获取充值前20书籍
  472. *
  473. * @param $category_id
  474. * @return mixed
  475. */
  476. public function getBookOrderByCharge($category_id)
  477. {
  478. $channel_type = Site::getChannelType();
  479. $query = Book::leftJoin('book_configs', 'books.id', 'book_configs.bid')
  480. ->whereIn('book_configs.is_on_shelf', [1, 2])
  481. ->select('books.id', 'books.name', 'books.intro', 'books.cover', 'books.author', 'books.category_name', 'books.gender',
  482. DB::raw("(select sum(price) from orders where from_bid = books.id and status = 'PAID') as all_price"));
  483. // 会员制站点只展示短篇书籍
  484. // if ($channel_type == 'PERIOD') {
  485. // $query->where('book_configs.charge_type', 'BOOK');
  486. // }
  487. return $query->where('books.category_id', $category_id)->orderBy('all_price', 'desc')->limit(20)->get()->toArray();
  488. }
  489. /**
  490. * 章节目录
  491. *
  492. * @param $data
  493. * @return mixed
  494. */
  495. public function getChapterList($data)
  496. {
  497. $distribution_channel_id = Site::getCurrentChannelId();
  498. $bid = getProp($data, 'bid');
  499. if (!$bid) {
  500. Utils::throwError(ErrorConst::DATA_EXCEPTION);
  501. }
  502. $order_type = getProp($data, 'order_type', 'asc');
  503. $page_number = getProp($data, 'page_number');
  504. $query = Chapter::where(['bid' => $bid, 'is_check' => 1, 'is_deleted' => 0])->select('id', 'name', 'charge_length', 'size', 'is_vip', 'sequence');
  505. if ($page_number) {
  506. $chapters = $query->orderBy('sequence', $order_type)->paginate($page_number);
  507. } else {
  508. $chapters = $query->orderBy('sequence', $order_type)->paginate(10);
  509. }
  510. if ($chapters) {
  511. $result['chapters'] = $chapters;
  512. } else {
  513. $result['chapters'] = [];
  514. }
  515. // 获取书籍信息
  516. $result['book'] = Book::leftJoin('book_configs', 'book_configs.bid', 'books.id')->where(['books.id'=>$bid])
  517. ->select('books.id as bid', 'book_configs.book_name', 'books.cover', 'books.size', 'books.chapter_count',
  518. 'books.intro', 'books.category_name', 'book_configs.feed_advertise_seq', 'book_configs.follow_seq')->first();
  519. if (!$result['book']) {
  520. Utils::throwError(ErrorConst::BOOK_NOT_EXIST);
  521. }
  522. $result['book'] = $result['book']->toArray();
  523. // 获取书籍收费方式
  524. $book_charge_type = DB::table('channel_book_charge_type')->where('distribution_channel_id', $distribution_channel_id)->where('bid', $result['book']['bid'])->first();
  525. if ($book_charge_type) {
  526. $result['book']['charge_info'] = [
  527. 'distribution_channel_id' => getProp($book_charge_type, 'distribution_channel_id'),
  528. 'book_calculate_price_type' => getProp($book_charge_type, 'book_calculate_price_type'),
  529. 'book_coin' => getProp($book_charge_type, 'book_coin'),
  530. ];
  531. }else {
  532. $result['book']['charge_info'] = [];
  533. }
  534. $result['book']['cover'] = addPrefix($result['book']['cover']);
  535. $result['book']['intro'] = filterIntro($result['book']['intro']);
  536. return $result;
  537. }
  538. /**
  539. * 设置书籍收费方式
  540. * @param $data
  541. * @return bool
  542. * @throws \App\Exceptions\ApiException
  543. */
  544. public function setBookChargeType($data) {
  545. $distribution_channel_id = Site::getCurrentChannelId();
  546. $bid = getProp($data, 'bid');
  547. $book_calculate_price_type = getProp($data, 'book_calculate_price_type');
  548. $book_coin = getProp($data, 'book_coin');
  549. if (!$bid || !$book_calculate_price_type || !$book_coin) {
  550. Utils::throwError(ErrorConst::DATA_EXCEPTION);
  551. }
  552. return DB::table('channel_book_charge_type')->updateOrInsert([
  553. 'distribution_channel_id' => $distribution_channel_id,
  554. 'bid' => $bid
  555. ], [
  556. 'book_calculate_price_type' => $book_calculate_price_type,
  557. 'book_coin' => $book_coin,
  558. 'created_at' => date('Y-m-d H:i:s'),
  559. 'updated_at' => date('Y-m-d H:i:s'),
  560. ]);
  561. }
  562. /**
  563. * 章节信息
  564. *
  565. * @param $data
  566. * @return array
  567. */
  568. public function getChapterInfo($data)
  569. {
  570. $cid = getProp($data, 'cid');
  571. if (!$cid) {
  572. Utils::throwError(ErrorConst::DATA_EXCEPTION);
  573. }
  574. $result = Chapter::leftJoin('chapter_contents', 'chapter_contents.id', 'chapters.chapter_content_id')
  575. ->leftJoin('books', 'books.id', 'chapters.bid')
  576. ->leftJoin('book_configs', 'book_configs.bid', 'chapters.bid')
  577. ->whereIn('book_configs.is_on_shelf', [1, 2])
  578. ->where('chapters.id', $cid)->where(['chapters.is_check' => 1, 'chapters.is_deleted' => 0])
  579. ->select('chapters.id as cid', 'chapters.bid', 'chapters.name as chapter_name', 'chapters.sequence', 'books.gender',
  580. 'chapter_contents.content as chapter_content', 'chapters.is_vip', 'books.name as book_name', 'books.cover', 'book_configs.charge_type')->first();
  581. if (!$result) {
  582. Utils::throwError(ErrorConst::CHAPTER_NOT_EXIST);
  583. }
  584. $chapter = $result->toArray();
  585. $chapter['cover'] = addPrefix($chapter['cover']);
  586. // 获取上一章和下一章cid
  587. $prev_cid = Chapter::where('bid', $chapter['bid'])->where(['is_check' => 1, 'is_deleted' => 0])
  588. ->where('sequence', '<', $chapter['sequence'])->orderBy('sequence', 'desc')->limit(1)->value('id');
  589. $next_cid = Chapter::where('bid', $chapter['bid'])->where(['is_check' => 1, 'is_deleted' => 0])
  590. ->where('sequence', '>', $chapter['sequence'])->orderBy('sequence', 'asc')->limit(1)->value('id');
  591. $chapter['prev_cid'] = $prev_cid ? $prev_cid : 0;
  592. $chapter['next_cid'] = $next_cid ? $next_cid : 0;
  593. $chapter['chapter_content'] = filterContent($chapter['chapter_content']);
  594. return $chapter;
  595. }
  596. /**
  597. * 获取全部一级分类
  598. *
  599. * @param $data
  600. * @return mixed
  601. */
  602. public function getCategoryList($data)
  603. {
  604. $gender = trim(getProp($data, 'gender'));
  605. $uid = Site::getUid();
  606. // 获取当前阅读书籍
  607. $current_chapter = UserCache::getCurrentChapter($uid);
  608. if (!$gender) $gender = $current_chapter['gender'] ? $current_chapter['gender'] : 1;
  609. return Category::where(['channel_id' => $gender, 'is_show' => 1])->where('pid', 0)
  610. ->select('id as category_id', 'category_name')->get()->toArray();
  611. }
  612. /**
  613. * 获取分类书籍
  614. *
  615. * @param $data
  616. * @return mixed
  617. */
  618. public function getCategoryBooks($data)
  619. {
  620. $gender = trim(getProp($data, 'gender'));
  621. $category_id = getProp($data, 'category_id');
  622. $search_key = getProp($data, 'search_key');
  623. $status = getProp($data, 'status');
  624. $page_number = getProp($data, 'page_number');
  625. $uid = Site::getUid();
  626. $channel_type = Site::getChannelType();
  627. // 获取当前阅读书籍
  628. $current_chapter = UserCache::getCurrentChapter($uid);
  629. if (!$gender) $gender = $current_chapter['gender'] ? $current_chapter['gender'] : 1;
  630. $query = Book::leftJoin('book_configs', 'books.id', 'book_configs.bid')->whereIn('book_configs.is_on_shelf', [1, 2])
  631. ->select('books.id', 'books.name', 'books.intro', 'books.cover', 'books.author', 'books.category_name', 'books.gender',
  632. DB::raw("(select sum(price) from orders where from_bid = books.id and status = 'PAID') as all_price"))
  633. ->where('books.gender', $gender);
  634. // 会员制站点只展示短篇书籍
  635. // if ($channel_type = 'PERIOD') {
  636. // $query->where('book_configs.charge_type', 'BOOK');
  637. // }
  638. if ($search_key) {
  639. // 书名或分类名模糊搜索
  640. if ($search_key == '其他') { // 其他分类按无分类算
  641. $query->where('books.name', 'like', '%' . $search_key . '%')->orWhere('books.category_name', '');
  642. } else {
  643. $query->where('books.name', 'like', '%' . $search_key . '%')->orWhere('books.category_name', 'like', '%' . $search_key . '%');
  644. }
  645. }
  646. if ($category_id) {
  647. $query->where('books.category_id', $category_id);
  648. }
  649. if ($status !== '') {
  650. $query->where('books.status', $status);
  651. }
  652. if ($page_number) {
  653. return $query->orderBy('all_price', 'desc')->paginate($page_number);
  654. } else {
  655. return $query->orderBy('all_price', 'desc')->paginate();
  656. }
  657. }
  658. /**
  659. * 生成派单链接
  660. *
  661. * @param $data
  662. * @throws \App\Exceptions\ApiException
  663. */
  664. public function setSendOrder($data)
  665. {
  666. $distribution_channel_id = Site::getCurrentChannelId();
  667. $bid = getProp($data, 'bid');
  668. $cid = getProp($data, 'cid');
  669. $template_id = getProp($data, 'template_id');
  670. $report_percent = getProp($data, 'report_percent', 0);
  671. $send_order_name = getProp($data, 'send_order_name');
  672. if (!is_numeric($report_percent) || $report_percent > 100) {
  673. Utils::throwError(ErrorConst::DATA_EXCEPTION);
  674. }
  675. if ($template_id) {
  676. $template = DB::table('channel_templates')->where('id', $template_id)->where('distribution_channel_id', $distribution_channel_id)->first();
  677. if (!$template) {
  678. Utils::throwError('1002:该充值模板不存在!');
  679. }
  680. }
  681. $chapter = Chapter::leftJoin('books', 'books.id', 'chapters.bid')
  682. ->leftJoin('book_configs', 'book_configs.bid', 'books.id')->whereIn('book_configs.is_on_shelf', [1, 2])
  683. ->where(['chapters.id' => $cid, 'chapters.bid' => $bid, 'chapters.is_check' => 1, 'chapters.is_deleted' => 0])
  684. ->select('chapters.bid as book_id', 'books.name as book_name', 'chapters.id as chapter_id', 'chapters.name as chapter_name')
  685. ->orderBy('chapters.sequence')->first();
  686. if (!$chapter) Utils::throwError(ErrorConst::CHAPTER_NOT_EXIST);
  687. $channel_info = DB::table('distribution_channels')->where('id', $distribution_channel_id)->select('id', 'name', 'channel_type', 'pay_id')->first();
  688. if (!$channel_info) Utils::throwError(ErrorConst::CHANNEL_NOT_EXIST);
  689. if (DB::table('send_orders')->where('name', $send_order_name)->value('id')) {
  690. Utils::throwError(ErrorConst::SEND_ORDER_NAME_INVALID);
  691. }
  692. $insert_data = $chapter->toArray();
  693. $insert_data['name'] = $send_order_name;
  694. $insert_data['distribution_channel_id'] = getProp($channel_info, 'id');
  695. $insert_data['channel_type'] = getProp($channel_info, 'channel_type');
  696. $insert_data['cost'] = '0';
  697. $insert_data['pay_id'] = getProp($channel_info, 'pay_id');
  698. $insert_data['report_percent'] = $report_percent;
  699. $insert_data['created_at'] = $insert_data['updated_at'] = $insert_data['send_time'] = date('Y-m-d H:i:s');
  700. if ($template_id) $insert_data['pay_id'] = $template_id;
  701. $send_order_id = DB::table('send_orders')->insertGetId($insert_data);
  702. // 调用抖音三方库生成分享链接(非本地模式调用)
  703. if ($send_order_id && env('APP_ENV') != 'local') {
  704. $arr = [
  705. 'bid' => $bid ? Hashids::encode($bid) : '',
  706. 'cid' => $cid ? Hashids::encode($cid) : '',
  707. 'send_order_id' => $send_order_id,
  708. 'distribution_channel_id' => $distribution_channel_id,
  709. 'jump' => 'no',
  710. ];
  711. $json = json_encode($arr, 256);
  712. $path = 'pages/reader/index'; // 固定页面(阅读页)
  713. $instance = $this->openService->getInstance(['sandbox' => env('DOUYIN_APP_SANDBOX')]);
  714. $urlLink = $instance->generateShareLink($path, $json);
  715. if (!isset($urlLink['url_link'])) {
  716. Utils::throwError(ErrorConst::SYS_EXCEPTION);
  717. }
  718. return DB::table('send_orders')->where('id', $send_order_id)->update([
  719. 'send_order_url' => $urlLink['url_link'],
  720. 'updated_at' => date('Y-m-d H:i:s')
  721. ]);
  722. }
  723. return $send_order_id;
  724. }
  725. /**
  726. * 计算扣减书币
  727. *
  728. * @param $bid
  729. * @param $charge_length
  730. * @param $user_vip_level
  731. * @return int
  732. */
  733. public function calcDeductCoin($bid, $charge_length, $user_vip_level = 0)
  734. {
  735. return $this->calNormalCoin($charge_length, $user_vip_level);
  736. }
  737. /**
  738. * 默认情况扣减书币的金额
  739. *
  740. * @param $charge_length
  741. * @param int $user_vip_level
  742. * @return int
  743. */
  744. private function calNormalCoin($charge_length, $user_vip_level = 0): int
  745. {
  746. // 正常价
  747. $coin = ceil(($charge_length / 1000) * CoinConst::DEFAULT_CHAPTER_PRICE * CoinConst::PRICE_COIN_RATIO);
  748. switch ($user_vip_level) {
  749. // 初级vip 77折
  750. case BaseConst::JUNIOR_VIP_LEVEL:
  751. $coin *= CoinConst::JUNIOR_CHAPTER_DISCOUNT;
  752. break;
  753. // 高级vip 55折
  754. case BaseConst::SENIOR_VIP_LEVEL:
  755. $coin *= CoinConst::SENIOR_CHAPTER_DISCOUNT;
  756. break;
  757. default:
  758. break;
  759. }
  760. return (int)round($coin);
  761. }
  762. /**
  763. * 执行扣减书币
  764. *
  765. * @param $user_info //用户信息
  766. * @param $chapter_info //书籍章节信息
  767. * @param $cid //章节id
  768. * @param $coin //待扣减的书币
  769. */
  770. private function decreaseBookCoin($user_info, $chapter_info, $cid, $coin)
  771. {
  772. $origin_coin = $coin;
  773. $balance = getProp($user_info, 'balance');
  774. $charge_balance = getProp($user_info, 'charge_balance');
  775. $reward_balance = getProp($user_info, 'reward_balance');
  776. if ($coin > $balance) return false;
  777. $update_data = [
  778. 'reward_balance' => $reward_balance,
  779. 'charge_balance' => $charge_balance
  780. ];
  781. $balance -= $coin;
  782. foreach ($update_data as $key => $val) {
  783. if ($coin <= 0) break;
  784. if ($coin > $val) {
  785. $coin -= $val;
  786. $val = 0;
  787. } else {
  788. $val -= $coin;
  789. $coin = 0;
  790. }
  791. $update_data[$key] = $val;
  792. }
  793. $update_data['balance'] = $balance;
  794. $update_data['updated_at'] = date('Y-m-d H:i:s');
  795. $decrease_charge_balance = $charge_balance - $update_data['charge_balance'];
  796. $decrease_reward_balance = $reward_balance - $update_data['reward_balance'];
  797. $chapter_order_data = [
  798. 'uid' => getProp($user_info, 'id'),
  799. 'fee' => $origin_coin,
  800. 'cid' => $cid,
  801. 'bid' => getProp($chapter_info, 'bid'),
  802. 'distribution_channel_id' => Site::getChannelId(),
  803. 'book_name' => getProp($chapter_info, 'book_name'),
  804. 'chapter_name' => getProp($chapter_info, 'chapter_name'),
  805. 'send_order_id' => Site::getSendOrderId(),
  806. 'charge_balance' => $decrease_charge_balance,
  807. 'reward_balance' => $decrease_reward_balance,
  808. 'created_at' => date('Y-m-d H:i:s'),
  809. 'updated_at' => date('Y-m-d H:i:s')
  810. ];
  811. try {
  812. DB::beginTransaction();
  813. // 更新用户书币
  814. $boolen = User::where('id', getProp($user_info, 'id'))->update($update_data);
  815. if (!$boolen) {
  816. $update_data['uid'] = getProp($user_info, 'id');
  817. DB::rollBack();
  818. dLog('chapter_orders')->info('更新用户书币余额失败: ', $update_data);
  819. Utils::throwError(ErrorConst::DB_INVALID);
  820. }
  821. // 订阅记录写入redis
  822. $subscribe_data = [
  823. 'bid' => getProp($chapter_info, 'bid'),
  824. 'day' => date('Y-m-d'),
  825. 'balance' => getProp($chapter_order_data, 'fee'),
  826. 'charge_balance' => getProp($chapter_order_data, 'charge_balance'),
  827. 'reward_balance' => getProp($chapter_order_data, 'reward_balance'),
  828. 'is_chapter' => getProp($chapter_info, 'charge_type') == 'CHAPTER' ? 1 : 0,
  829. 'count' => 1,
  830. 'uid' => getProp($user_info, 'id'),
  831. ];
  832. $boolen1 = StatisticCache::setSubscribe($subscribe_data, date('Ymd'));
  833. if (!$boolen1) {
  834. DB::rollBack();
  835. dLog('chapter_orders')->info('添加临时订阅记录失败: ', $chapter_order_data);
  836. Utils::throwError(ErrorConst::DB_INVALID);
  837. }
  838. // 增加订阅记录
  839. $boolen2 = $this->chapterOrderService->addChapterOrder($chapter_order_data);
  840. if (!$boolen2) {
  841. DB::rollBack();
  842. dLog('chapter_orders')->info('添加订阅记录失败: ', $chapter_order_data);
  843. Utils::throwError(ErrorConst::DB_INVALID);
  844. }
  845. } catch (\Exception $e) {
  846. DB::rollBack();
  847. return false;
  848. }
  849. DB::commit();
  850. return true;
  851. }
  852. // /**
  853. // * 扣减书币执行
  854. // * @param $uid
  855. // * @param $coin
  856. // * @param $changeType
  857. // * @param $extra
  858. // * @return array|bool
  859. // * @throws ApiException
  860. // */
  861. // public function decreaseBookCoin($uid, $coin, $changeType, $extra = [])
  862. // {
  863. // // 1. 获取用户书币信息
  864. // $userCoin = User::getUserCoin($uid, true);
  865. // $bookInfo = getProp($extra, 'bookInfo', []);
  866. // $chapterInfo = getProp($extra, 'chapterInfo', []);
  867. // $bid = getProp($extra, 'bid', 0);
  868. // $cid = getProp($extra, 'cid', 0);
  869. //
  870. // // 2. 扣费提示
  871. // if ($coin > $userCoin['total']) {
  872. // // 书币不够,弹框提示返回数据
  873. //
  874. // }
  875. //
  876. // // 3. 根据扣除规则,获取需要扣减的书币列表
  877. // [$updateData, $changeData] = $this->buildDecreaseData($uid, $coin, $userCoin);
  878. // [$amount, $awardTotal, $awardForever, $awardValid, $charge] = $this->filterFieldUpdate($changeData);
  879. //
  880. // // 5. 开启事务,执行扣除(添加操作记录)
  881. // try {
  882. // DB::beginTransaction();
  883. //
  884. // // 扣除书币
  885. // $upRes = true;
  886. // if ($updateData) {
  887. // foreach ($updateData as $data) {
  888. // $res = BookCoinLogs::getInstance($uid)->updateUserCoinLog($data['id'], $uid, $data['remain'], $data['updated_at']);
  889. // $upRes = $upRes && $res;
  890. // }
  891. // }
  892. //
  893. // $addRes = false;
  894. // $multiRes = false;
  895. //
  896. // // 扣除书币成功
  897. // if ($upRes) {
  898. // // 增加扣币记录
  899. // $addRes = $this->addOptToCoinLog([
  900. // 'uid' => $uid,
  901. // 'crease_type' => CoinConst::DECREASE,
  902. // 'coin_type' => '',
  903. // 'change' => $coin,
  904. // 'change_type' => $changeType,
  905. // 'extra' => json_encode([
  906. // 'updateData' => $changeData
  907. // ])
  908. // ]);
  909. //
  910. // // 批量更新用户相关计数
  911. // if ($amount) {
  912. // $multiRes = User::multiUpdateUserStat($uid, [
  913. // 'balance_total' => 'balance_total - ' . $amount, // 总书币
  914. // 'bonus_total' => 'bonus_total - ' . $awardTotal, // 奖金币汇总
  915. // 'bonus_forever' => 'bonus_forever - ' . $awardForever, // 永久奖金币
  916. // 'bonus_valid' => 'bonus_valid - ' . $awardValid, // 有效期奖金币
  917. // 'charge_balance' => 'charge_balance - ' . $charge, // 充值书币
  918. // ]);
  919. // } else {
  920. // // 兼容限免
  921. // $multiRes = true;
  922. // }
  923. // }
  924. //
  925. // // 判断返回结果
  926. // if (!$upRes || !$addRes || !$multiRes) {
  927. // myLog('decreaseBookCoin')->info('rollback', compact('uid', 'upRes', 'addRes', 'multiRes'));
  928. // DB::rollback();
  929. // return false;
  930. // }
  931. //
  932. // // 提交事务
  933. // DB::commit();
  934. // } catch (\Exception $e) {
  935. // myLog('decreaseBookCoin')->info('exception', [$e->getMessage(), $e->getTraceAsString()]);
  936. // DB::rollback();
  937. // return false;
  938. // }
  939. //
  940. // return ['originCoinChange' => (int)$charge, 'awardCoinChange' => (int)$amount - (int)$charge];
  941. // }
  942. }