hash === md5(Doo::userToken()) ? 1 : 0; } /** ****************************************************************************** */ /** ****************************************************************************** */ /** ****************************************************************************** */ /** * 缓存key * @param string $hash * @return string */ private static function ck(string $hash): string { return "user_devices:{$hash}"; } /** * 解析 UA 获取设备信息 * @param string $ua * @return array */ private static function getDeviceInfo(string $ua): array { $result = [ 'ip' => Base::getIp(), 'type' => '电脑', 'os' => 'Unknown', 'browser' => 'Unknown', 'version' => '', 'app_type' => '', // 客户端类型 'app_version' => '', // 客户端版本 ]; if (empty($ua)) { return $result; } // 使用 Device-Detector 解析 UA $dd = new DeviceDetector($ua); // 解析 UA 字符串 $dd->parse(); // 获取客户端信息(浏览器) $clientInfo = $dd->getClient(); if (!empty($clientInfo)) { $result['browser'] = $clientInfo['name'] ?? 'Unknown'; $result['version'] = $clientInfo['version'] ?? ''; } // 获取操作系统信息 $osInfo = $dd->getOs(); if (!empty($osInfo)) { $result['os'] = trim(($osInfo['name'] ?? '') . ' ' . ($osInfo['version'] ?? '')); if (empty($result['os'])) { $result['os'] = 'Unknown'; } } if (preg_match("/android_kuaifan_eeui/i", $ua)) { // Android 客户端 $result['app_type'] = 'Android'; $result['app_version'] = self::getAfterVersion($ua, 'kuaifan_eeui/'); } elseif (preg_match("/ios_kuaifan_eeui/i", $ua)) { // iOS 客户端 $result['app_type'] = 'iOS'; $result['app_version'] = self::getAfterVersion($ua, 'kuaifan_eeui/'); } elseif (preg_match("/dootask/i", $ua)) { // DooTask 客户端 $result['app_type'] = $osInfo['name']; $result['app_version'] = self::getAfterVersion($ua, 'dootask/'); } else { // 其他客户端 $result['app_type'] = 'Web'; $result['app_version'] = Base::getClientVersion(); } return $result; } /** * 从 ua 的 find 之后的内容获取版本号 * @param string $ua * @param string $find * @return string */ private static function getAfterVersion(string $ua, string $find): string { $findPattern = preg_quote($find, '/'); if (preg_match("/{$findPattern}(.*?)(?:\s|$)/i", $ua, $matches)) { $appInfo = $matches[1]; // 从内容中提取版本号(寻找符合x.x.x格式的部分) if (preg_match("/(\d+\.\d+(?:\.\d+)*)/", $appInfo, $versionMatches)) { return $versionMatches[1]; } } return ''; } /** ****************************************************************************** */ /** ****************************************************************************** */ /** ****************************************************************************** */ /** * 检查用户是否存在 * @return string|null */ public static function check(): ?string { $token = Doo::userToken(); $userid = Doo::userId(); $hash = md5($token); if (Cache::has(self::ck($hash))) { return $hash; } $row = self::whereHash($hash)->first(); if ($row) { // 判断是否过期 if (Carbon::parse($row->expired_at)->isPast()) { self::forget($row); return null; } // 更新缓存 self::record(); return $hash; } // 没有记录,尝试创建一个(防止升级后所有登录都失效,保证留一个可以保持登录) // todo 后期删除 return AbstractModel::transaction(function () use ($hash, $userid) { if (self::whereUserid($userid)->withoutTrashed()->lockForUpdate()->exists()) { return null; } return self::record() ? $hash : null; }); } /** * 记录设备(添加、更新) * @param string|null $token * @return self|null */ public static function record(string $token = null): ?self { if (empty($token)) { $token = Doo::userToken(); $userid = Doo::userId(); $expiredAt = Doo::userExpiredAt() ?: null; } else { $info = Doo::tokenDecode($token); $userid = $info['userid'] ?? 0; $expiredAt = $info['expired_at'] ?? null; } $deviceData = [ 'detail' => Base::array2json(self::getDeviceInfo($_SERVER['HTTP_USER_AGENT'] ?? '')), 'expired_at' => $expiredAt, ]; $hash = md5($token); $row = self::updateInsert([ 'userid' => $userid, 'hash' => $hash, ], function() use ($deviceData) { if (!Request::hasHeader('version')) { unset($deviceData['detail']); } return $deviceData; }, $deviceData, $isInsert); if ($isInsert) { $currentDeviceCount = self::whereUserid($userid)->count(); if ($currentDeviceCount > self::$deviceLimit) { // 删除多余的设备记录 $rows = self::whereUserid($userid)->orderBy('id')->take($currentDeviceCount - self::$deviceLimit)->get(); foreach ($rows as $row) { UserDevice::forget($row); } } } if ($row) { Cache::put(self::ck($hash), $row->userid, now()->addHour()); return $row; } return null; } /** * 忘记设备(删除) * @param UserDevice|string|int|null $token * - UserDevice 表示指定的设备对象 * - string 表示指定的 token * - int 表示指定的数据ID * - null 表示当前登录的设备 * @return void */ public static function forget(UserDevice|string|int $token = null): void { if ($token instanceof UserDevice) { $hash = $token->hash; $token->delete(); } elseif (Base::isNumber($token)) { $row = self::find(intval($token)); if ($row) { $hash = $row->hash; $row->delete(); } } else { if ($token === null) { $token = Doo::userToken(); } if ($token) { $hash = md5($token); self::whereHash($hash)->delete(); } } if (isset($hash)) { Cache::forget(self::ck($hash)); UmengAlias::whereDeviceHash($hash)->delete(); } } }