* @date 2020-01-16 17:06:43 */ declare(strict_types=1); namespace App\Repositories; use App\Events\Task\BindPhoneEvent; use App\Events\Task\FillCodeEvent; use App\Events\Task\InviteFriendsEvent; use App\Events\Task\TimeTaskEvent; use App\Exception\ApiException; use App\Model\Mongo\VideoHistoryModel; use App\Model\TaskAwarkLogModel; use App\Model\UserModel; use App\Model\VipCardModel; use App\Model\VipOrderModel; use App\Repositories\Service\CommonService; use App\Repositories\Service\QueueService; use App\Repositories\Utils\M3u8CacheRepository; use App\Repositories\Utils\RandomUserInfoRepository; use Carbon\Carbon; use Hyperf\Database\Model\Builder; use Hyperf\DbConnection\Db; use KZ2\Repositories\Model\Repository; use Modules\Api\Services\AuthService; use Modules\Api\Services\UserRelationService; use Psr\SimpleCache\CacheInterface; class UserRepository extends Repository { /** * 账号凭证加密的key,单个APP写死 * @var string */ private $_acKey = 'SbIQUxX6hSOJ8pdb'; /** * 账号凭证加密的iv,单个APP写死 * @var string */ private $_acIv = 'CJn2wekWF2OPUkXC'; // 目前APP的用户协议名 private $appUserScheme = 'user://'; /** * @return Builder|mixed|string */ public function model() { return UserModel::class; } /** * 随机产生昵称. * @return string */ public function randGenerateNick() { return RandomUserInfoRepository::getNickname(); } /** * 随机产生设备号 * @return string */ public function randGenerateDeviceNo() { return strtolower(random(32)); } /** * app用户退出登录 * @param $userId * @return bool */ public function userLogout($userId) { if (!$userId) { return false; } $userInfo = $this->getUserInfoFromCache($userId); if (!$userInfo) { return false; } $cacheKey = (new AuthService) ->getUserDeviceCacheKey( $userInfo['device_no'], $userInfo['device_type'] ); $cache = make(CacheInterface::class); $cache->delete($cacheKey); $this->delUserCache($userId); return true; } /** * 从cache里面取用户信息 头像不建议在这里面拼接URL * @param $userId * @return array|null */ public function getUserInfoFromCache($userId) { if (!$userId) { return null; } $cacheKey = 'user_info:'.$userId; //缓存时间 随机2-3小时内 $cacheTime = mt_rand(7200, 10800); $result = make(CacheInterface::class) ->remember( $cacheKey, $cacheTime, function () use ($userId) { $creatorRepo = make(CreatorRepository::class); $creatorTotalCoin = $creatorTotalIncome = $creatorIncome = $creatorCoin = 0; $creatorObj = $creatorRepo->detail($userId, ['id', 'total_coin', 'total_income']); if (!empty($creatorObj)) { $creatorTotalCoin = $creatorObj->total_coin; $creatorTotalIncome = $creatorObj->total_income; } $tempData = array(); $dbResult = UserModel::query()->where('code', $userId) ->first(); if ($dbResult) { $dbResult = $dbResult->toArray(); //去掉不必要字段 if ($dbResult['status'] != 1) { return null; } // $dbResult['avatar'] = make(CommonService::class) // ->getImageUrl($dbResult['avatar']); $dbResult['birth'] = strval($dbResult['birth']); $dbResult['vip_expired'] = strval($dbResult['vip_expired']); $dbResult['is_vip'] = 'n'; $now = Carbon::now(); $vipExpired = Carbon::parse($dbResult['vip_expired']); if ($dbResult['vip_expired'] && $vipExpired > $now) { $dbResult['is_vip'] = 'y'; } $relation = make(UserRelationService::class); $dbResult['fans'] = $relation->getTotal('fans', $userId); $dbResult['flow'] = $relation->getTotal('flow', $userId); $dbResult['like'] = $relation->getLike($userId); unset($now); unset($vipExpired); $tempData = $dbResult; $tempData['creator_total_coin'] = $creatorTotalCoin; $tempData['creator_total_income'] = $creatorTotalIncome; } return $tempData; } ); return $result; } /** * 清除用户的cache信息 操作了用户 如 封禁啊、增加会员了啊什么的调用此方法 * @return bool */ public function delUserCache($userId) { $cacheKey = 'user_info:'.$userId; make(CacheInterface::class)->delete($cacheKey); return true; } /** * 个人中心数据 * @param $userId * @return array */ public function getUserInfoData($userId) { $userInfo = $this->getUserInfoFromCache($userId); if (!$userInfo) { throw new ApiException('帳號異常'); } $user_count = make(VideoHistoryModel::class)->db()->countDocuments( ['uid' => $userInfo['id'], 'date' => Carbon::now()->toDateString()] ); $taskLogRepo = make(TaskAwarkLogRepository::class); $result = array( 'id' => $userInfo['id'], 'code' => $userInfo['code'], 'nick' => $userInfo['nick'].'', 'coins' => $userInfo['coins'], 'avatar'=> M3u8CacheRepository::getImageUrl($userInfo['avatar']), 'intro' => $userInfo['intro'].'', 'gender'=> intval($userInfo['gender']), 'is_vip'=> $userInfo['is_vip'], 'user_vip_level' => 0, // 字段vip_level,设计目的是给裂变业绩使用,所以此处有2层含义,需要通过is_vip来判断. 'play_num_notice'=> '', 'bind_mobile' => $userInfo['mobile'] ? '+'. $userInfo['area_code'] . $userInfo['mobile'] : '', // 是否注册绑定手机号通过这个是否为空字符串判断. 'flow' => $userInfo['flow'], 'fans' => $userInfo['fans'], 'like' => $userInfo['like'], 'invite' => $userInfo['invite'], 'has_parent'=> $userInfo['parent_uid'] > 0 ? 'y' : 'n', 'parent_uid' => $userInfo['parent_uid'], 'balance' => !empty($userInfo['balance']) ? $userInfo['balance'] : 0, // 余额 'max_view' => intval($userInfo['max_view']), 'channel_code' => $userInfo['channel_id'], // 渠道号. 'creator_level' => $userInfo['creator_level'] ?? 0, // 创作者等级. ); $playNumNotice = ''; $userVipLevel = 0; $commentSurplus = 0; // 剩余评论数 $videoSurplus = 0; // 剩余观影次数. $vipDaysSurplus = 0; // VIP剩余天数. $vipTitle = ''; // VIP套餐名称. $todayTaskVideoNum = make(TaskAwarkLogRepository::class)->coutTodayOnce($userInfo['code'], TaskAwarkLogModel::REWARD_TYPE_MOVIE_CONUT); $videoSurplus = intval($userInfo['max_view']) - $user_count + $todayTaskVideoNum; if ($videoSurplus < 0) { // 容错 $videoSurplus = 0; } if ($userInfo['is_vip'] == 'y') { $userVipLevel = $userInfo['vip_level']; $vipDaysSurplus = abs(Carbon::now()->diffInDays($userInfo['vip_expired'])); $videoSurplus = 999999; if ($vipDaysSurplus > (10 * 365)) { $playNumNotice = '无限观看'; } else { $playNumNotice = '剩余:' . strval($vipDaysSurplus) . '天'; } } else { $playNumNotice = '剩余:' . strval($vipDaysSurplus) . '天'; } $result['play_num_notice'] = $playNumNotice; $result['user_vip_level'] = $userVipLevel; // 最近购买最好的VIP. if ($vipDaysSurplus > 0) { $columns = ['id', 'vip_id', 'user_code', 'vip_title']; $lastVipObj = VipOrderModel::query()->where(['user_id' => $userInfo['id'], 'status' => 1])->orderBy('id', 'DESC')->first($columns); if (!empty($lastVipObj)) { $vipTitle = $lastVipObj->vip_title; } } $result['vip_title'] = $vipTitle; // VIP套餐名称. // 当天评论次数 $toDayCommentSurplus = $taskLogRepo->coutTodayOnce($userId, TaskAwarkLogModel::REWARD_TYPE_COMMENT_COUNT); //找回账号的凭证 $account = $this->appUserScheme .$result['code']; $account = hand_encrypt($account, $this->_acKey, $this->_acIv); $result['account_qrcode_content'] = $account; // TODO 给前端同学补结构,后面写业务逻辑. $result['video_surplus'] = $videoSurplus; // 剩余观影次数. $result['hotel_surplus'] = $userInfo['max_open_house']; // 开房查询剩余次数. todo 已开房次数还没减 $result['vip_days_surplus'] = $vipDaysSurplus; // VIP剩余天数. $result['max_comment'] = $commentSurplus + $userInfo['max_comment'] + $toDayCommentSurplus; // 每日最大评论数. $result['comment_used'] = make(CommentRepository::class)->countToday($userInfo['code']); // 今日使用过评论数. $commentSurplus = $result['max_comment'] - $result['comment_used']; if ($commentSurplus < 0) { // 容错. $commentSurplus = 0; } $result['comment_surplus'] = $commentSurplus; // 今日剩余评论数. return $result; } /** * 创建用户qr内容。 * @param $uid * @return string */ public function createUserQrCode($uid) { $info = $this->appUserScheme.$uid; return hand_encrypt($info, $this->_acKey, $this->_acIv); } /** * 根据账号凭证找回账号 * @param string $userId 当前账号邀请码 * @param string $qrCode 要找回的账号的二维码内容 * @return bool */ public function findByQrCode($userId, $qrCode) { //当前账号 $newUserInfo = $this->getUserInfoFromCache($userId); if (!$newUserInfo) { throw new ApiException('[1]帳號異常'); } $account = hand_decrypt($qrCode, $this->_acKey, $this->_acIv); if (empty($account)) { throw new ApiException('[1]无效二维码内容'); } $patternTxt = str_replace('/', '\\/', $this->appUserScheme); if (!preg_match('/^('. $patternTxt .')/i', $account)) { throw new ApiException('[2]无效二维码内容'); } $account = str_replace($this->appUserScheme, '', $account); $oldUserInfo = $this->getUserInfoFromCache($account); if (!$oldUserInfo) { throw new ApiException('[2]帳號異常'); } if ($newUserInfo['id'] == $oldUserInfo['id']) { throw new ApiException('此身份卡为当前账号,请勿重复扫描'); } Db::beginTransaction(); try { //交换两个账号的device_no //把旧账号的设备号、设备类型给新账号 $result1 = UserModel::query() ->where('id', $newUserInfo['id']) ->update( array( 'device_no'=>$oldUserInfo['device_no'], 'device_type'=>$oldUserInfo['device_type'], 'finger_hash'=>$oldUserInfo['finger_hash'], ) ); //把新账号的设备号、设备类型给旧账号 $result2 = UserModel::query()->where('id', $oldUserInfo['id']) ->update( array( 'device_no'=>$newUserInfo['device_no'], 'device_type'=>$newUserInfo['device_type'], 'finger_hash'=>$newUserInfo['finger_hash'], ) ); if ($result1 && $result2) { Db::commit(); //退出登录 $this->userLogout($newUserInfo['code']); $this->userLogout($oldUserInfo['code']); return true; } } catch (\Exception $e) { var_dump($e->getMessage()); } Db::rollBack(); return false; } /** * 绑定手机号、手机号不存在则绑定手机号。手机号存在则为根据手机号找回账号 * @param string $userId * @param string $phone * @param string $code * @return bool */ public function bindPhone($userId, $phone, $type) { if (!$userId || !$phone) { throw new ApiException('參數錯誤'); } $userInfo = $this->getUserInfoFromCache($userId); if (!$userInfo) { throw new ApiException('帳號異常'); } //处理逻辑 $mobileUser = make(UserModel::class) ->where('area_code', '=', 86) ->where('mobile', '=', $phone) ->first(); if ($type == 2 && $mobileUser) { throw new ApiException('该手机号已经绑定'); } if ($type == 1) { if (empty($mobileUser)) { throw new ApiException('账号不存在,请确认你的手机号是否正确'); } //手机号存在,实际逻辑为找回账号、交换两个用户的设备号信息 $mobileUser = $mobileUser->toArray(); Db::beginTransaction(); try { //交换两个账号的device_no //把手机号账号的设备号、设备类型给当前账号 $result1 = UserModel::query()->where('id', $userInfo['id']) ->update( array( 'device_no'=>$mobileUser['device_no'], 'device_type'=>$mobileUser['device_type'], 'finger_hash'=>$mobileUser['finger_hash'], ) ); //把当前账号的设备号、设备类型给手机号账号 $result2 = UserModel::where('id', $mobileUser['id']) ->update( array( 'device_no'=>$userInfo['device_no'], 'device_type'=>$userInfo['device_type'], 'finger_hash'=>$userInfo['finger_hash'], ) ); if ($result1 && $result2) { Db::commit(); //退出登录 $this->userLogout($userInfo['code']); $this->userLogout($mobileUser['code']); return true; } } catch (\Exception $e) { } Db::rollBack(); return false; } else { //手机号不存在、直接绑定手机号 UserModel::query()->where('id', $userInfo['id'])->update( array( 'mobile'=>$phone, ) ); //绑定手机号 dispatch_event(new BindPhoneEvent(['user_id' => $userInfo['code']])); $this->userLogout($userInfo['code']); return true; } } /** * 更新用户基本信息 * @param $userId * @param $data */ public function updateUserData($userId, $data) { if (!$userId || !$data) { throw new ApiException('參數錯誤'); } $userInfo = $this->getUserInfoFromCache($userId); if (!$userInfo) { throw new ApiException('帳號異常'); } $commonService = make(CommonService::class); $updateData = array(); if (isset($data['nick'])) { $data['nick'] = $commonService->filterString($data['nick']); $tempLen = mb_strlen($data['nick'], 'utf-8'); if ($tempLen > 9) { throw new ApiException('暱稱最長10個字符'); } $updateData['nick'] = $data['nick']; } if (isset($data['avatar'])) { $tempLen = strpos($data['avatar'], 'ceb'); if (!$tempLen) { throw new ApiException('頭像地址錯誤'); } $updateData['avatar'] = $data['avatar']; } if (isset($data['intro'])) { $data['intro'] = $commonService->filterString($data['intro']); $tempLen = mb_strlen($data['intro'], 'utf-8'); if ($tempLen > 199) { throw new ApiException('簡介最長200個字符'); } $updateData['intro'] = $data['intro']; } if (isset($data['gender'])) { $genderArr = array(0,1,2,3); if (!in_array($data['gender'], $genderArr)) { throw new ApiException('性別錯誤'); } $updateData['gender'] = $data['gender']; } if (isset($data['birth'])) { $updateData['birth'] = $data['birth']; } if (isset($data['live_code'])) { $updateData['live_code'] = $data['live_code']; } if (empty($updateData)) { throw new ApiException('無更新'); } UserModel::query()->where('id', $userInfo['id'])->update($updateData); $this->delUserCache($userId); return true; } /** * 绑定邀请码 * @param string $userId * @param string $parentCode */ public function bindCode($userId, $parentCode) { if (!$userId || !$parentCode || $userId == $parentCode) { throw new ApiException('參數錯誤'); } $userInfo = $this->getUserInfoFromCache($userId); $parentUser = $this->getUserInfoFromCache($parentCode); if (!$userInfo) { throw new ApiException('帳號異常'); } if (!$parentUser) { throw new ApiException('邀請碼不存在'); } //已绑定 if ($userInfo['parent_uid']) { throw new ApiException('已綁定'); } //互相推荐的情况 if ($parentUser['parent_uid'] == $userInfo['code']) { throw new ApiException('綁定異常'); } UserModel::query()->where('id', $userInfo['id']) ->update( array( 'parent_uid'=>$parentUser['code'], ) ); //会员裂变关系逻辑 丢队列异步执行 make(QueueService::class)->createUserRelationJob(['invite_id' => $parentUser['code'], 'invited_id' => $userInfo['code']]); // 绑定邀请码任务 dispatch_event(new FillCodeEvent(['user_id' => $userInfo['code']])); // 邀请好友任务 dispatch_event(new InviteFriendsEvent(['user_id' => $parentUser['code']])); // 限时任务队列 make(QueueService::class)->timeTaskSendRewardJob(['user_id' => $parentUser['code']], 5); $this->delUserCache($userId); $this->delUserCache($parentCode); return true; } /** * 解绑手机号 * @param string $userId * @param string $phone * @param string $code * @return bool */ public function unbindPhone($userId, $phone) { if (!$userId || !$phone) { throw new ApiException('參數錯誤'); } $userInfo = $this->getUserInfoFromCache($userId); if (!$userInfo) { throw new ApiException('帳號異常'); } //处理逻辑 $mobileUser = make(UserModel::class) ->where('area_code', '=', 86) ->where('mobile', '=', $phone) ->first(); if ($mobileUser) { if ($mobileUser->mobile != $phone) { throw new ApiException('帳號異常'); } $mobileUser->mobile = ''; //手机号存在,实际逻辑为找回账号、交换两个用户的设备号信息 $mobileUser = $mobileUser->toArray(); Db::beginTransaction(); try { $mobileUser->save(); Db::commit(); } catch (\Exception $e) { } Db::rollBack(); return false; } else { throw new ApiException('帳號異常'); } } /** * 获取aes-key * @return string */ public function getAcKey() { return $this->_acKey; } /** * 获取aes-iv * @return string */ public function getAcIv() { return $this->_acIv; } /** * 获取系统头像 * @return mixed */ public function getSysAvatars() { $cacheRepo = make(CacheInterface::class); $cacheKey = 'user:sys_avatars'; $arrData = $cacheRepo->rememberForever($cacheKey, function () { $arrData = []; $filePrefix = 'f2|/'. env('STATIC_DIR_NAME', 'doukui') . '/avatars/a_'; $maxNum = 11; for ($i = 1; $i <= $maxNum; $i++) { $tmpNum = str_pad(strval($i), 3, '0', STR_PAD_LEFT); $groupUrl = $filePrefix . $tmpNum . '.ceb'; $arrData[] = $groupUrl; } return $arrData; }); return $arrData; } }