dao = $dao; } /** * save * * @param array $data * * @return Admin|null * @throws \core\exception\handler\AdminException */ public function save(array $data): Admin|null { try { return $this->transaction(function () use ($data) { $data['password'] = password_hash($data['password'], PASSWORD_DEFAULT); $roles = $this->normalizeRelationIds($data['role_id_list'] ?? []); $posts = $this->normalizeRelationIds($data['post_id_list'] ?? []); $depts = $this->normalizeRelationIds($data['dept_id_list'] ?? []); $mainDeptId = $data['main_dept_id'] ?? null; $mainPosId = $data['main_post_id'] ?? null; unset($data['role_id_list'], $data['post_id_list'], $data['dept_id_list'], $data['main_dept_id'], $data['main_post_id']); $model = $this->dao->save($data); $this->updateModel($model, $data, $depts, $posts, $roles); $this->syncRoles($model, $roles); $this->syncMainInfo($model, $mainDeptId, $mainPosId); return $model; }); } catch (\Throwable $e) { throw new AdminException($e->getMessage()); } } /** * 编辑 * * @param int|string $id * @param array $data * * @return \app\model\system\Admin|null * @throws \core\exception\handler\AdminException */ public function update(int|string $id, array $data): ?Admin { try { return $this->transaction(function () use ($id, $data) { $this->updatePasswordIfNeeded($data); $roles = $this->normalizeRelationIds($data['role_id_list'] ?? []); $posts = $this->normalizeRelationIds($data['post_id_list'] ?? []); $depts = $this->normalizeRelationIds($data['dept_id_list'] ?? []); $mainDeptId = $data['main_dept_id'] ?? null; $mainPosId = $data['main_post_id'] ?? null; unset($data['role_id_list'], $data['post_id_list'], $data['dept_id_list'], $data['main_dept_id'], $data['main_post_id']); $model = $this->dao->getModel() ->findOrFail($id); $this->updateModel($model, $data, $depts, $posts, $roles); $this->syncRoles($model, $roles); $this->syncMainInfo($model, $mainDeptId, $mainPosId); return $model; }); } catch (\Throwable $e) { throw new AdminException($e->getMessage()); } } private function normalizeRelationIds(mixed $value): array { if (is_array($value)) { return array_values(array_filter($value, static fn ($item) => $item !== null && $item !== '')); } if (is_string($value)) { return array_values(array_filter(array_map('trim', explode(',', $value)), static fn ($item) => $item !== '')); } if ($value === null || $value === '') { return []; } return [$value]; } /** * 密码处理 * * @param array $data */ private function updatePasswordIfNeeded(array &$data): void { if (isset($data['password'])) { $data['password'] = password_hash($data['password'], PASSWORD_DEFAULT); } } /** * 更新-部门职位关系 * * @param \app\model\system\Admin $model * @param array $data * @param array $depts * @param array $posts * @param array $roles */ private function updateModel(Admin $model, array $data, array $depts, array $posts, array $roles): void { $model->fill($data); $model->save(); $model->depts()->sync($depts); $model->posts()->sync($posts); $model->roles()->sync($roles); } /** * 同步-角色 * * @param \app\model\system\Admin $model * @param array $roles */ private function syncRoles(Admin $model, array $roles): void { $model->roles()->sync($roles); } private function syncMainInfo(Admin $model, ?string $mainDeptId, ?string $mainPosId): void { $mainInfo = \app\model\system\AdminMain::updateOrCreate( ['admin_id' => (string)$model->id], [ 'main_dept_id' => $mainDeptId, 'main_post_id' => $mainPosId, ] ); } /** * destroy * * @param $id * @param $force * * @return mixed * @throws \Exception */ public function destroy($id, $force): mixed { $ret = $this->dao->count([['id', 'in', $id], ['is_super', '=', 1]]); if ($ret > 0) { throw new AdminException('系统内置用户,不允许删除'); } return $this->dao->destroy($id); } /** * 用户-冻结 * * @param array|string $id */ public function locked(array|string $id): void { try { if (is_string($id)) { $id = array_map('trim', explode(',', $id)); } $ret = $this->dao->count([['id', 'in', $id], ['is_super', '=', 1]]); if ($ret > 0) { throw new AdminException('系统内置用户,不允许冻结'); } $this->dao->batchUpdate($id, ['is_locked' => 1]); } catch (\Throwable $e) { throw new AdminException($e->getMessage()); } } /** * 用户-解除冻结 * * @param array|string $id */ public function unLocked(array|string $id): void { try { if (is_string($id)) { $id = array_map('trim', explode(',', $id)); } $this->dao->batchUpdate($id, ['is_locked' => 0]); } catch (\Throwable $e) { throw new AdminException($e->getMessage()); } } /** * 用户登录 * * @param string $username * @param string $password * @param string $type * @param string $grantType * @param array $params * * @return array * @throws \core\exception\handler\AdminException */ public function login(string $username, string $password = '', string $type = 'admin', string $grantType = 'default', array $params = []): array { $adminInfo = $this->getAdminByName($username, [AccessPermissionScope::class]); $this->validateAdminStatus($adminInfo); $decryptedPassword = $this->validateRsaKeys($params['key_id'], $password); // $decryptedPassword= $password;//临时注释关闭RSA验证要不无法调试 $this->validatePassword($adminInfo, $decryptedPassword, $grantType); [$userInfo, $token] = $this->generateTokenData($adminInfo, $type); $this->emitLoginSuccessEvent(array_merge($userInfo, $token), $tenant?->id ?? null); return $token ?? []; } /** * 第三方应用登录 * * @param mixed $acctId * @param mixed $appId * @param mixed $appSecret * @param mixed $userName * @param string $type * * @return array * @throws \core\exception\handler\AdminException */ public function thirdPartyLogin(mixed $acctId, mixed $appId, mixed $appSecret, mixed $userName, string $type = 'admin'): array { $adminInfo = $this->getAdminByName($userName); $this->validateAdminStatus($adminInfo); $this->validateThirdPartyApp($acctId, $appId, $appSecret); [$userInfo, $token] = $this->generateTokenData($adminInfo, $type); $this->emitLoginSuccessEvent(array_merge($userInfo, $token), $tenant?->id ?? null); return $token ?? []; } private function validateThirdPartyApp(mixed $acctId, mixed $appId, mixed $appSecret) { if ($acctId !== config('app.acct_id')) { throw new AdminException('第三方应用不存在或已删除'); } if ($appId !== config('app.app_id')) { throw new AdminException('第三方应用ID错误'); } if ($appSecret !== config('app.app_secret')) { throw new AdminException('第三方应用ID或密钥错误'); } } /** * 验证管理员状态 * * @param \app\model\system\Admin|null $adminInfo * * @throws \core\exception\handler\AdminException */ private function validateAdminStatus(?Admin $adminInfo): void { if (!$adminInfo) { throw new AdminException('账号或密码错误,请重新输入!'); } if ($adminInfo->enabled === 0) { throw new AdminException('您已被禁止登录!'); } if ($adminInfo->is_locked === 1) { throw new AdminException('您的账号已被锁定,禁止登录!'); } } /** * 验证密码 * * @param \app\model\system\Admin $adminInfo * @param string $password * @param string $grantType * * @throws \core\exception\handler\AdminException */ private function validatePassword(Admin $adminInfo, string $password, string $grantType): void { if (!in_array($grantType, ['sms', 'refresh_token']) && !password_verify($password, $adminInfo->password)) { $msg = '账号或密码错误,请重新输入!'; $this->emitLoginFailedEvent($adminInfo->toArray(), $msg); throw new AdminException($msg); } } /** * token生成 * * @param \app\model\system\Admin $adminInfo * @param string $type * * @return array */ private function generateTokenData(Admin $adminInfo, string $type): array { $loginIp = request()->getRealIp(); $userAgent = request()->header('user-agent', ''); $browser = $this->getBrowser($userAgent); $os = $this->getOs($userAgent); $ipLocation = $this->getIpLocation($loginIp); $adminInfo->login_time = time(); $adminInfo->login_ip = $loginIp; $adminInfo->save(); $userInfo = $adminInfo->makeHidden([ 'password', 'backend_setting', 'created_by', 'updated_by', 'created_at', 'deleted_at', 'remark', 'created_date', 'updated_date', ])->toArray(); // 追加扩展数据到 userInfo(用于 JWT extra) $userInfo['ip'] = $loginIp; $userInfo['ip_location'] = $ipLocation; $userInfo['browser'] = $browser; $userInfo['os'] = $os; // 使用新的 JwtToken 生成 token $jwt = new JwtToken(); $tokenObj = $jwt->generate((string)$adminInfo->id, $type, $userInfo); $token = [ 'access_token' => $tokenObj->accessToken, 'refresh_token' => $tokenObj->refreshToken, 'expires_in' => $tokenObj->expiresIn, 'client_id' => $this->generateUniqueId($loginIp), 'expires_time' => time() + $tokenObj->expiresIn ]; return [$userInfo, $token]; } /** * 登录成功-事件发起 * * @param array $tokenData * @param int|null $tenantId */ private function emitLoginSuccessEvent(array $tokenData, ?int $tenantId = null): void { $loginIp = request()->getRealIp(); $event = new LoginLogEvent( '登录成功', request()->app, $loginIp, $this->getIpLocation($loginIp), $this->getBrowser(request()->header('user-agent', '')), $this->getOs(request()->header('user-agent', '')), 0, '登录成功', $tokenData['user_name'], $tokenData['id'], time(), $tokenData['access_token'], $tokenData['expires_time'] ); $event->dispatch(); } /** * 登录失败-事件发起 * * @param array $adminInfo * @param string $message */ private function emitLoginFailedEvent(array $adminInfo, string $message): void { $loginIp = request()->getRealIp(); $event = new LoginLogEvent( '登录失败', request()->app, $loginIp, $this->getIpLocation($loginIp), $this->getBrowser(request()->header('user-agent', '')), $this->getOs(request()->header('user-agent', '')), -1, $message, $adminInfo['user_name'], $adminInfo['id'], time(), '', time() ); $event->dispatch(); } /** * 获取浏览器信息 * * @param string $userAgent * @return string */ private function getBrowser(string $userAgent): string { $br = 'Unknown'; if (preg_match('/MSIE/i', $userAgent)) { $br = 'MSIE'; } elseif (preg_match('/Firefox/i', $userAgent)) { $br = 'Firefox'; } elseif (preg_match('/Edg/i', $userAgent)) { $br = 'Edge'; } elseif (preg_match('/OPR|Opera/i', $userAgent)) { $br = 'Opera'; } elseif (preg_match('/Chrome/i', $userAgent)) { $br = 'Chrome'; } elseif (preg_match('/Safari/i', $userAgent)) { $br = 'Safari'; } else { $br = 'Other'; } return $br; } /** * 获取操作系统信息 * * @param string $userAgent * @return string */ private function getOs(string $userAgent): string { $os = 'Unknown'; if (preg_match('/win/i', $userAgent)) { $os = 'Windows'; } elseif (preg_match('/mac/i', $userAgent)) { $os = 'Mac'; } elseif (preg_match('/linux/i', $userAgent)) { $os = 'Linux'; } else { $os = 'Other'; } return $os; } /** * 获取IP归属地 * * @param string $ip * @return string */ private function getIpLocation(string $ip): string { // 本地IP或无效IP直接返回 if (empty($ip) || in_array($ip, ['127.0.0.1', '::1', 'localhost', '0.0.0.0'])) { return '本地'; } // 使用新浪API获取IP归属地 try { // 创建Guzzle HTTP客户端 $client = new \GuzzleHttp\Client([ 'timeout' => 2, 'connect_timeout' => 1, 'verify' => false ]); $url = "http://int.dpool.sina.com.cn/iplookup/iplookup.php?format=json&ip=" . $ip; $response = $client->get($url); $content = $response->getBody()->getContents(); $data = json_decode($content, true); if (isset($data['ret']) && $data['ret'] === 1 && !empty($data['city'])) { $location = $data['city']; if (!empty($data['province']) && strpos($data['province'], $data['city']) === false) { $location = $data['province'] . ' ' . $location; } return $location; } } catch (\Throwable $e) { // 忽略异常 } return '未知'; } /** * 更新用户信息 * * @param string|int $id * @param array $data * * @return mixed * @throws \Exception */ public function updateUserInfo(string|int $id, array $data): mixed { return $this->dao->update(['id' => $id], $data); } /** * 更新用户头像 * * @param string|int $id * @param string $avatar * * @return mixed * @throws \Exception */ public function updateAvatarUser(string|int $id, string $avatar): mixed { return $this->dao->update(['id' => $id], ['avatar' => $this->getPathFromUrl($avatar)]); } /** * 更新个人用户密码 * * @param string|int $id * @param array $data * * @return void * @throws \Throwable */ public function updateUserPwd(string|int $id, array $data): void { $this->transaction(function () use ($id, $data) { $info = $this->dao->getModel()->where('id', $id)->first(); if (empty($info)) { throw new AdminException('用户不存在或被删除'); } if (!password_verify($data['old_password'], $info->password)) { throw new AdminException('旧密码错误,请重新输入!'); } return $this->dao->update(['id' => $id], ['password' => $this->passwordHash($data['new_password'])]); }); } /** * 更新个人偏好设置 * * @throws \Throwable */ public function updateUserPreferences(string|int $id, array $data = []): void { $this->transaction(function () use ($id, $data) { unset($data['id']); return $this->dao->update(['id' => $id], ['backend_setting' => $data]); }); } /** * 强制下线 * * @param $token * * @throws \Throwable */ public function kickoutByTokenValueUser($token): void { $this->transaction(function () use ($token) { /** @var LoginLogService $systemLoginLogService */ $systemLoginLogService = Container::make(LoginLogService::class); // 使用 firstOrFail 避免空值检查 $loginLog = $systemLoginLogService->getModel() ->where('key', $token) ->firstOrFail(); // 批量更新字段 $loginLog->update([ 'expires_at' => time(), // 使用 Carbon 时间 'remark' => '强制下线', 'updated_at' => time(), // 确保更新时间戳 ]); // 添加Token 致黑名单 $result = JwtToken::addToBlacklist($token, true); if (!$result) { throw new AdminException('操作失败'); } }); } /** * 删除关联源-并清理残留关联数据 * * @param array $ids * * @return array * @throws \core\exception\handler\AdminException */ public function batchDelete(array $ids): array { try { return $this->transaction(function () use ($ids) { // 1. 验证:禁止删除超级管理员 $superUserCount = $this->dao->getModel()->whereIn('id', $ids) ->where('is_super', 1) ->count(); if ($superUserCount > 0) { throw new AdminException('系统内置用户不允许删除'); } // 2. 关联同步:通过模型关联清理角色中间表数据 $admins = $this->dao->getModel()->whereIn('id', $ids)->get(); foreach ($admins as $admin) { // 解除角色关联 $admin->roles()->detach(); $admin->depts()->detach(); } // 4. 执行管理员数据删除 $deleteCount = $this->dao->destroy($ids); if ($deleteCount <= 0) { throw new AdminException('删除失败,未找到有效用户'); } return ['id' => $ids]; }); } catch (\Throwable $e) { throw new AdminException($e->getMessage()); } } /** * 生成唯一UUID * * @param string|null $currentIp * * @return string */ private function generateUniqueId(string|null $currentIp = null): string { if (empty($currentIp)) { $currentIp = request()->getRemoteIp(); } return uniqid($currentIp . '-', true); } /** * 入参移除url * * @param string $url * * @return string */ private function getPathFromUrl(string $url): string { // 检测协议头存在性 if (!preg_match('#^https?://#i', $url)) { return $url; // 非http(s)协议直接原样返回 } // 支持子域名和端口号 $pattern = '#^https?:// (?:[^/@]+@)? // 忽略用户认证信息 (?:[^/:]+|\[[^\]]+\]) // 匹配域名或IPv6地址 (?::\d+)? // 忽略端口号 (/[^\?#]*) // 精确捕获路径部分 #x'; preg_match($pattern, $url, $matches); return $matches[1] ?? ''; } /** * 校验密钥 * * @param $keyId * @param $encryptedPassword * * @return string * @throws AdminException */ private function validateRsaKeys($keyId, $encryptedPassword): string { $cache = Container::make(CacheService::class, []); $privateKey = $cache->get("rsa_private_key_$keyId"); if (!$privateKey) { throw new AdminException('私钥不存在或已过期,请刷新页面重试'); } return RSAService::decrypt($encryptedPassword, $privateKey); } }