VideoService.php 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404
  1. <?php
  2. /**
  3. * 视频服务.
  4. *
  5. * @author ningque
  6. * @date 2020-02-18 14:36:11
  7. */
  8. declare(strict_types=1);
  9. namespace Modules\Api\Services;
  10. use App\Constants\AdConstrant;
  11. use App\Constants\ChangeLogConstant;
  12. use App\Constants\CoinsConstant;
  13. use App\Constants\VideoConstant;
  14. use App\Exception\ApiException;
  15. use App\Jobs\AwardCreatorBuyVideoJob;
  16. use App\Jobs\VideoWatchedByBulkJob;
  17. use App\Model\AdModel;
  18. use App\Model\Mongo\VideoHistoryModel;
  19. use App\Model\Mongo\VideoLikeModel;
  20. use App\Model\TaskAwarkLogModel;
  21. use App\Model\UserBuyVideoModel;
  22. use App\Model\UserModel;
  23. use App\Model\VideoModel;
  24. use App\Repositories\CreatorRepository;
  25. use App\Repositories\RelationRepository;
  26. use App\Repositories\Service\QueueService;
  27. use App\Repositories\TaskAwarkLogRepository;
  28. use App\Repositories\UserRepository;
  29. use App\Repositories\Utils\M3u8CacheRepository;
  30. use Carbon\Carbon;
  31. use Hyperf\DbConnection\Db;
  32. use Modules\Manage\Controllers\AdController;
  33. use Psr\SimpleCache\CacheInterface;
  34. class VideoService
  35. {
  36. /**
  37. * 视频详情.
  38. * @param int
  39. * @return mixed
  40. */
  41. public function info($id, $userCode)
  42. {
  43. $relationRepo = make(RelationRepository::class);
  44. $user = make(UserRepository::class)->getUserInfoData($userCode);
  45. // $user = make(UserRepository::class)->getUserInfoFromCache($user_id);
  46. $columns = ['id','title', 'cover', 'like', 'uid', 'coins', 'pay_type', 'comment', 'mu', 'watch', 'resolution'];
  47. $ret = VideoModel::query()
  48. ->with(['type:type.id as type_id,type.title','user'])
  49. ->where('status', 1)
  50. ->find($id, $columns);
  51. if (!$ret) {
  52. throw new ApiException('没有此视频');
  53. }
  54. //鉴权
  55. $arrFlag = $this->videoAuth($user, $ret);
  56. /*$arrScreenshots = [];
  57. if (!empty($ret->screenshots)) {
  58. $arrTmp = explode(',', $ret->screenshots);
  59. foreach ($arrTmp as $v) {
  60. $arrScreenshots[] = M3u8CacheRepository::getImageUrl($v);
  61. }
  62. }
  63. $arrTags = [];
  64. if (!empty($ret->tags)) {
  65. $arrTmp = explode(',', $ret->tags);
  66. foreach ($arrTmp as $v) {
  67. $arrTags[] = $v;
  68. }
  69. }
  70. $ret->screenshots = $arrScreenshots;
  71. $ret->tags = $arrTags;
  72. $ret->cover = M3u8CacheRepository::getImageUrl($ret->cover);
  73. $ret->mu = M3u8CacheRepository::getVideoUrl($ret->mu);*/
  74. $result = $ret->toArray();
  75. $result['cover'] = M3u8CacheRepository::getImageUrl($result['cover']);
  76. if (!empty($result['user'])) {
  77. $result['user']['avatar'] = M3u8CacheRepository::getImageUrl($result['user']['avatar']);
  78. $result['user']['is_focus'] = $relationRepo->userIsFocusOtherUser($user['code'], $result['user']['code'], false);
  79. }
  80. $result['is_my_like'] = $relationRepo->userIsLikeVideo($user['code'], $result['id'], false);
  81. if (!empty($result['category'])) {
  82. foreach ($result['category'] as $ck => $t) {
  83. unset($result['category'][$ck]['pivot']);
  84. }
  85. }
  86. $result['auth_err'] = $arrFlag['err'];
  87. $result['video_surplus'] = $arrFlag['video_surplus'];
  88. $result['is_viewed'] = $arrFlag['is_viewed'];
  89. if ($result['auth_err']['key'] != 200) {
  90. // 非正常的,仅返回6秒预览m3u8地址.
  91. $m3u8Params1 = [
  92. 'url' => $result['mu'],
  93. 'is_preview' => true,
  94. ];
  95. $result['mu'] = M3u8CacheRepository::getVideoInnerUrl($m3u8Params1, $userCode, $id, 'video');
  96. } else {
  97. $result['mu'] = M3u8CacheRepository::getVideoInnerUrl($result['mu'], $userCode, $id, 'video');
  98. }
  99. $adItems = make(VideoService::class)->getAdList(AdConstrant::VIDEO_GUIDE);
  100. if (!empty($adItems)) {
  101. $ads = [];
  102. foreach ($adItems as $v) {
  103. $tmpLink = $v['link'];
  104. if (!empty($tmpLink)) {
  105. $tmpLink = M3u8CacheRepository::getLinkUrl($tmpLink, ['user_code' => $user->code]);
  106. }
  107. $ads[] = [
  108. 'title' => $v['title'],
  109. 'cover' => M3u8CacheRepository::getImageUrl($v['cover']),
  110. 'link' => $tmpLink,
  111. 'scheme' => $v['scheme'],
  112. ];
  113. }
  114. $result['ad'] = $ads;
  115. }
  116. $jobParams = [
  117. [
  118. 'date' => Carbon::now()->toDateString(),
  119. 'user_id' => $user['id'],
  120. 'user_code' => $user['code'],
  121. 'video_id' => $id,
  122. 'ad' => $ads
  123. ]
  124. ];
  125. // 异步处理.
  126. dispatch_job(new VideoWatchedByBulkJob($jobParams));
  127. return $result;
  128. }
  129. /**
  130. * 视频鉴权
  131. * @return array
  132. */
  133. public function videoAuth($user, $video)
  134. {
  135. $checkResult = [
  136. 'result' => false,
  137. 'video_surplus' => 0,
  138. 'is_viewed' => false,
  139. 'err' => [
  140. 'key' => 200,
  141. 'info' => '',
  142. ],
  143. ];
  144. //先看今天看过这部视频没有
  145. $data['date'] = Carbon::now()->toDateString();
  146. $data['user_id'] = $user['id'];
  147. $data['video_id'] = $video->id;
  148. if ($user['code'] === $video->uid && !empty($video->uid)) {
  149. // 原创者自己的视频,不限制.
  150. $count = 1;
  151. } else {
  152. $key = $data['date'] .'_'. $data['user_id'] .'_'. $data['video_id'];
  153. $_id = substr(md5($key), 8, 16);
  154. $data['_id'] = $_id;
  155. $count = make(VideoHistoryModel::class)->db()->countDocuments(
  156. ['_id' => $_id]
  157. );
  158. }
  159. // 今天一个人观看的次数统计.
  160. $realVideoViewedNum = make(VideoHistoryModel::class)->db()->countDocuments(
  161. ['uid' => $user['id'],'date' => $data['date']]
  162. );
  163. $todayTaskVideoNum = make(TaskAwarkLogRepository::class)->coutTodayOnce($user['code'], TaskAwarkLogModel::REWARD_TYPE_MOVIE_CONUT);
  164. // 今日剩余观影次数
  165. $videoSurplus = intval($user['max_view']) - $realVideoViewedNum + $todayTaskVideoNum;
  166. $oldSurplus = $videoSurplus;
  167. if ($user['is_vip'] == 'y') {
  168. $videoSurplus = 999999;
  169. }
  170. $isNegativeSurplus = false; // 是否是负数.
  171. if ($count) {
  172. // 观看过,依然允许他继续看.
  173. $checkResult['result'] = true;
  174. $checkResult['video_surplus'] = $videoSurplus;
  175. $checkResult['is_viewed'] = true;
  176. } else {
  177. $checkResult['result'] = true;
  178. //金币鉴权
  179. if ($video->coins > 0) {
  180. $buyNum = UserBuyVideoModel::query()->where(
  181. [
  182. 'user_id' => $user['id'],
  183. 'video_id' => $video->id,
  184. ]
  185. )->count();
  186. if (!$buyNum) {
  187. // 没购买过,无法观看.
  188. $checkResult['result'] = false;
  189. $checkResult['err'] = [
  190. 'key' => VideoConstant::VIDEO_COINS_NO,
  191. 'info' => VideoConstant::$messages[VideoConstant::VIDEO_COINS_NO],
  192. ];
  193. } else {
  194. // 不管是不是会员,超过每日观影次数没有,都记录.
  195. $this->addHistory($data);
  196. }
  197. } else {
  198. // 不需要金币,则检查剩余次数.
  199. if ($user['is_vip'] != 'y') {
  200. // 非VIP情况
  201. if ($videoSurplus > 0) {
  202. // 不管是不是会员,超过每日观影次数没有,都记录.
  203. $this->addHistory($data);
  204. $videoSurplus--;
  205. } else {
  206. $checkResult['result'] = false;
  207. $checkResult['err'] = [
  208. 'key' => VideoConstant::VIDEO_LIMIT_NO,
  209. 'info' => VideoConstant::$messages[VideoConstant::VIDEO_LIMIT_NO],
  210. ];
  211. }
  212. } else {
  213. $this->addHistory($data);
  214. }
  215. }
  216. }
  217. if ($videoSurplus < 0) {
  218. // 容错.
  219. $videoSurplus = 0;
  220. $isNegativeSurplus = true;
  221. }
  222. $checkResult['video_surplus'] = $videoSurplus;
  223. $userRepo = make(UserRepository::class);
  224. $userRepo->delUserCache($user['code']); // 清理这个用户的缓存.
  225. return $checkResult;
  226. }
  227. public function isLike($user_code, $video_id)
  228. {
  229. $key = $user_code .'_'. $video_id;
  230. $_id = substr(md5($key), 8, 16);
  231. $data = [
  232. '_id' => $_id
  233. ];
  234. $count = make(VideoLikeModel::class)->db()->countDocuments($data);
  235. if ($count) {
  236. $result = 1;
  237. } else {
  238. $result = 0;
  239. }
  240. return $result;
  241. }
  242. /**
  243. * 添加观影历史、更新观影次数
  244. * @return mixed
  245. */
  246. public function addHistory(array $data)
  247. {
  248. $result = make(VideoHistoryModel::class)->db()->insertOne(
  249. [
  250. '_id' => $data['_id'],
  251. 'date' => $data['date'],
  252. 'uid' => $data['user_id'],
  253. 'vid' => $data['video_id'],
  254. ]
  255. );
  256. return $result->getInsertedId();
  257. }
  258. /**
  259. * 返回播放地址(没有用).
  260. * @param string $m3u8
  261. * @return mixed
  262. */
  263. public function getPlayUrl($m3u8)
  264. {
  265. return M3u8CacheRepository::getVideoUrl($m3u8);
  266. }
  267. /**
  268. * 返回广告内容 .
  269. * @param string $position
  270. * @return array
  271. */
  272. public function getAdList($position)
  273. {
  274. $cacheKey = 'ad:' . $position;
  275. $ad = make(CacheInterface::class)->remember($cacheKey, 60 * 60, function () use ($position) {
  276. $items = AdModel::query()
  277. ->where('status', 1)
  278. ->where('position', '=', $position)
  279. ->orderBy('sort', 'DESC')
  280. ->get();
  281. if ($items->count()) {
  282. return $items->toArray();
  283. } else {
  284. return [];
  285. }
  286. });
  287. return $ad;
  288. // $now = Carbon::now()->toDateString();
  289. //
  290. // return collect($ad)->where('position', '=', $position)
  291. // ->where('start_time', '<=', $now)
  292. // ->where('end_time', '>=', $now)
  293. // ->all();
  294. }
  295. /**
  296. * 用户购买视频.
  297. * @param $userCode
  298. * @param $videoId
  299. * @return mixed
  300. */
  301. public function buyVideo($userCode, $videoId)
  302. {
  303. $userObj = UserModel::query()->where(['code' => $userCode])->first(); // 买家
  304. $buyNum = UserBuyVideoModel::query()->where(['user_id' => $userObj->id, 'video_id' => $videoId])->count();
  305. if ($buyNum) {
  306. throw new ApiException('您已经购买过,无需重复购买');
  307. }
  308. $videoObj = VideoModel::query()->where(['id' => $videoId])->first();
  309. if (empty($videoObj)) {
  310. throw new ApiException('视频不存在');
  311. }
  312. if ($videoObj->pay_type == 2) {
  313. if ($videoObj->coins < 1) {
  314. throw new ApiException('此视频不可使用金币购买');
  315. }
  316. } else {
  317. throw new ApiException('此视频不可使用金币购买');
  318. }
  319. if (intval($userObj->coins) < intval($videoObj->coins)) {
  320. throw new ApiException(sprintf('您金币不足%s,请先购买金币', $videoObj->coins), VideoConstant::VIDEO_COINS_NO);
  321. }
  322. // 视频创作者
  323. $creatorRepo = make(CreatorRepository::class);
  324. $creatorObj = $creatorRepo->detail($videoObj->uid);
  325. $isDeduction = 0;
  326. if (!empty($creatorObj->user_uid)) {
  327. $isDeduction = intval($creatorRepo->isDeduction($creatorObj->user_uid, $creatorObj->level)); // 是否扣量,默认不扣
  328. }
  329. Db::beginTransaction();
  330. $setarr = [
  331. 'user_id' => $userObj->id,
  332. 'video_id' => $videoObj->id,
  333. 'is_deduction' => $isDeduction,
  334. ];
  335. $buyObj = UserBuyVideoModel::query()->create($setarr);
  336. if (!$buyObj) {
  337. Db::rollBack();
  338. return '';
  339. }
  340. $userObj->coins = $userObj->coins - $videoObj->coins;
  341. if (!$userObj->save()) {
  342. Db::rollBack();
  343. return '';
  344. }
  345. Db::commit();
  346. $jobParams = [
  347. 'video_id' => $videoObj->id,
  348. 'buyer_uid' => $userObj->code,
  349. 'seller_uid' => !empty($creatorObj->user_uid) ? $creatorObj->user_uid : '',
  350. 'is_deduction' => $isDeduction,
  351. ];
  352. if (empty($isDeduction)) {
  353. // 异步奖励创作者.
  354. dispatch_job(new AwardCreatorBuyVideoJob($jobParams));
  355. }
  356. // 帐变记录
  357. $changeData = [
  358. 'user_uid' => $userObj->code,
  359. 'currency_type' => ChangeLogConstant::CHANGE_COIN,
  360. 'amount' => $videoObj->coins,
  361. 'way' => ChangeLogConstant::WAY_2,
  362. 'type' => ChangeLogConstant::TYPE_10,
  363. ];
  364. make(QueueService::class)->saveChangeLogJob($changeData);
  365. // 清除缓存
  366. make(UserRepository::class)->delUserCache($userObj->code);
  367. unset($userObj, $creatorObj); // 释放内存.
  368. return $videoObj;
  369. }
  370. }