validateUser($data['account'], $data['password']); $system = $this->validateSystem($data['system_code']); $this->validateSystemPermission($user, $system); return $this->generateTokensAndReturn($user, $system, $data); } /** * 飞书登录 * 如果用户不存在则新增用户信息 */ public function feishuLogin(array $data): array { $feishuUserInfo = $this->getFeishuUserInfo($data); $user = $this->getOrCreateUser($feishuUserInfo); $system = $this->validateSystem($data['system_code']); if ($user->isLogin) { $this->validateSystemPermission($user, $system); } return $this->generateTokensAndReturn($user, $system, $data); } /** * 注册 */ public function register(array $data) { try { // 获取可用用户编号 $sn = $this->getAvailableUserSn(); $data['sn'] = $sn->user_sn; // 对密码进行哈希处理 $data['password'] = password_hash($data['password'], PASSWORD_DEFAULT); // 获取企业ID $corpInfo = $this->validateCorp($data['corp_abbr']); $data['corp_id'] = $corpInfo->id; // 处理账户 $data = $data + normalizeAccount($data['account']); // 创建用户记录 $user = $this->createUser($data); // 更新用户编号的用户 ID $this->updateUserId($user->id, $sn->user_sn); $system = $this->validateSystem($data['system_code']); return $this->generateTokensAndReturn($user, $system, $data); } catch (\Throwable $e) { throw $e; } } /** * 查找或创建飞书用户 */ private function getOrCreateUser(array $feishuUserInfo) { try { $phone = $this->sanitizePhone($feishuUserInfo['mobile'] ?? ''); $email = $feishuUserInfo['enterprise_email']; // $user = User::where('corp_id', $feishuUserInfo['corp_id']) // ->where(function($query) use ($phone, $email) { // $query->where('phone', $phone) // ->orWhere('email', $email); // }) // ->first(); $userByPhone = User::where('corp_id', $feishuUserInfo['corp_id']) ->where('phone', $phone) ->where('phone', '!=', '') ->first(); $userByEmail = User::where('corp_id', $feishuUserInfo['corp_id']) ->where('email', $email) ->where('email', '!=', '') ->first(); $user = $userByPhone ?: $userByEmail; if (!$user) { // 获取编号 $userSn = $this->getAvailableUserSn(); // 创建用户 $user = User::create([ 'name' => $feishuUserInfo['name'], 'avatar' => $feishuUserInfo['avatar_middle'], 'phone' => $this->sanitizePhone($feishuUserInfo['mobile'] ?? ''), 'email' => $feishuUserInfo['enterprise_email'], 'union_id' => $feishuUserInfo['union_id'], 'user_id' => $feishuUserInfo['user_id'], 'open_id' => $feishuUserInfo['open_id'], 'tenant_key' => $feishuUserInfo['tenant_key'], 'corp_id' => $feishuUserInfo['corp_id'], 'password' => password_hash($userSn->user_sn, PASSWORD_DEFAULT) ]); // 更新使用 $this->updateUserId($user->id, $userSn->user_sn); // 更新编号 if (!$user->update(['sn'=>$userSn->user_sn])) { throw new BusinessException('编号绑定失败'); } // 是否登陆 $user->isLogin = false; $options = [ 'corp_id' => $user->corp_id, 'user_id' => $user->id ]; \Webman\RedisQueue\Redis::send('perm-agt', $options); } else { $updated = [ 'union_id' => $feishuUserInfo['union_id'], 'user_id' => $feishuUserInfo['user_id'], 'open_id' => $feishuUserInfo['open_id'], 'tenant_key' => $feishuUserInfo['tenant_key'] ]; if ($phone && $phone == $user->phone) { $updated['phone'] = $phone; } if ($phone && $phone == $user->phone) { $updated['email'] = $email; } // 更新飞书信息 $user->update($updated); // 是否登陆 $user->isLogin = true; } return $user; } catch (\Throwable $e) { throw $e; } } /** * 过滤和格式化手机号 */ private function sanitizePhone($mobile) { return isset($mobile) ? str_replace('+86', '', $mobile) : ''; } /** * 生成并记录token,返回包含token的响应数据 */ private function generateTokensAndReturn(User $user, Systems $system, array $data): array { $payload = $this->buildJwtPayload($user, $system); $token = $this->generateAccessToken($payload); $refreshToken = $this->generateRefreshToken($user->id, $data['system_code']); $homeUrl = $this->getHome($system->id, $payload); // 记录 token 到 Redis 和 MySQL $this->recordUserToken($user->id, $token, $data['system_code']); return [ 'access_token' => $token, 'refresh_token' => $refreshToken, 'expires_in' => config('jwt.jwt_expiration'), 'system_code' => $data['system_code'], 'corp_abbr' => $data['corp_abbr'], 'login_type' => $data['login_type'], 'home_url' => $homeUrl, // 'payload' => $payload, ]; } /** * 获取飞书用户信息 */ private function getFeishuUserInfo(array $data) { $certificate = $this->feishuAppInfo($data['corp_abbr'], $data['system_code']); $feishu = new Feishu($certificate['app_id'], $certificate['app_secret'], $certificate['abbr']); //判断移动端或pc端回调地址 if(!empty($data['is_mob']) && $data['is_mob'] == 1){ $redirect_uri = $certificate['m_url']; }else{ $redirect_uri = $certificate['r_url']; } $userAccessToken = $feishu->userAccessToken($data['feishu_code'], $redirect_uri); $userInfo = $feishu->userInfo($userAccessToken['access_token']); if ($userInfo['code'] != 0) { throw new BusinessException('授权失败,请重试'); } return $userInfo['data'] + $data + $certificate; } /** * 更新用户编号 */ private function updateUserId($userId, $userSn) { $updated = UserSn::where('user_sn', $userSn) ->update(['user_id' => $userId]); if ($updated === 0) { throw new BusinessException('用户编号不存在或更新失败'); } return $updated; } /** * 记录用户token到 Redis 和 MySQL */ protected function recordUserToken(int $userId, string $token, string $systemCode) { $request = request(); $ip = $request ? $request->getRealIp() : ''; $ua = $request ? $request->header('user-agent', '') : ''; $now = time(); $expireAt = $now + config('jwt.jwt_expiration'); // Redis Set 用户token集合 Redis::sAdd("user_tokens:{$userId}:{$systemCode}", $token); Redis::expire("user_tokens:{$userId}:{$systemCode}", config('jwt.jwt_expiration')); // Redis Hash token元信息 $meta = [ 'system_code' => $systemCode, 'login_ip' => $ip, 'user_agent' => $ua, 'login_time' => $now, 'expired_at' => $expireAt, ]; Redis::hMSet("user_token_meta:{$token}", $meta); Redis::expire("user_token_meta:{$token}", config('jwt.jwt_expiration')); // 可选:MySQL 持久化 try { UserTokenLog::create([ 'user_id' => $userId, 'token' => $token, 'system_code'=> $systemCode, 'login_ip' => $ip, 'user_agent' => $ua, 'login_time' => $now, 'expired_at' => $expireAt, 'is_valid' => 1, ]); } catch (\Throwable $e) { Log::error('Token log write failed: '.$e->getMessage()); } } /** * 校验token有效性 */ public function verifyToken(string $token, string $systemCode): array { if (!$systemCode) throw new BusinessException('缺少system_code', 422); $payload = (new Jwt())->verifyToken($token); // 校验黑名单 if (Redis::has("token_blacklist:{$token}")) { throw new BusinessException('token已失效', 403); } // 强制校验aud(系统隔离) if ($systemCode && $payload['aud'] != $systemCode && $payload['aud'] != ($payload['system_code'] ?? '')) { throw new BusinessException('token不属于当前系统', 403); } return (array)$payload; } /** * 获取飞书应用信息 */ public function feishuAppInfo(string $corpAbbr, string $systemCode) { $corpInfo = $this->validateCorp($corpAbbr); $systemInfo = $this->validateSystem($systemCode); $fsAppInfo = $this->validateFeishuApp($corpInfo->id, $systemInfo->id); return [ 'app_id' => $fsAppInfo->app_id, 'app_secret' => $fsAppInfo->app_secret, 'r_url' => $fsAppInfo->r_url, 'm_url' => $fsAppInfo->m_url, 'scope' => $fsAppInfo->scopes, 'state' => $fsAppInfo->state, 'abbr' => $corpAbbr, 'corp_id' => $corpInfo->id, 'system_id' => $systemInfo->id ]; } /** * 验证用户信息 */ private function validateUser(string $account, string $password) { $user = User::byAccount($account)->first(); if (!$user || !password_verify($password, $user->password)) { throw new BusinessException('账号或密码错误', 401); } if ($user->status != 1) { throw new BusinessException('账号已禁用', 403); } return $user; } /** * 验证系统信息 */ private function validateSystem(string $systemCode) { $system = Systems::where('code', $systemCode)->first(); if (!$system) { throw new BusinessException('系统不存在', 403); } return $system; } /** * 验证公司信息 */ private function validateCorp(string $corpAbbr) { $corp = Corp::where('abbr', $corpAbbr)->first(); if (!$corp) { throw new BusinessException('公司不存在', 403); } return $corp; } /** * 验证飞书应用信息 */ private function validateFeishuApp(int $corpId, int $systemId) { $feishuAppInfo = CorpFeishuApp::where('corp_id', $corpId)->where('system_id', $systemId)->first(); if (!$feishuAppInfo) { throw new BusinessException('飞书应用不存在', 403); } return $feishuAppInfo; } /** * 验证用户系统权限 */ private function validateSystemPermission(User $user, Systems $system) { if ($system->is_role) { $systemIds = $user->systems->pluck('id')->toArray(); if (!in_array($system->id, $systemIds)) { throw new BusinessException('无该系统权限', 403); } } } /** * 获取可用的用户编号 * * @return \app\common\model\UserSn 用户编号模型实例 * @throws BusinessException 当未找到适用编号时抛出 */ private function getAvailableUserSn(): \app\common\model\UserSn { $userSnModel = new UserSn(); $sn = $userSnModel->where('user_id', '=', 0)->inRandomOrder()->first(); if (!$sn) { Log::error('用户注册时未找到适用编号'); throw new BusinessException('未找到适用编号'); } return $sn; } /** * 创建用户记录 * * @param array $data 包含用户信息的数组 * @return \app\common\model\User 用户模型实例 */ private function createUser(array $data): \app\common\model\User { try { return User::create($data); } catch (\Throwable $e) { Log::error('用户注册时创建用户记录失败', [ 'data' => $data, 'error' => $e->getMessage() ]); throw new BusinessException('用户注册失败,请稍后重试', 500); } } /** * JWT payload 构建 * 可扩展 payload 字段 */ private function buildJwtPayload(User $user, ?Systems $system = null): array { $now = time(); $exp = $now + config('jwt.jwt_expiration'); $system_ids = $user->systems ? $user->systems->pluck('id')->toArray() : []; $role_ids = $user->roles ? $user->roles->pluck('id')->toArray() : []; return [ 'sub' => 'user_' . $user->id, 'aud' => $system->code, 'iat' => $now, 'nbf' => $now, 'exp' => $exp, 'typ' => 'access', 'uid' => $user->id, 'uname' => $user->name, 'role_ids' => $role_ids, 'system_ids' => $system_ids ]; } /** * JWT签发 */ private function generateAccessToken(array $payload) { return (new Jwt())->generateToken($payload); } /** * 刷新令牌生成 */ private function generateRefreshToken(int $userId, string $systemCode) { $refreshToken = bin2hex(random_bytes(32)); // user_id+system_code只允许一个refresh_token生效 Redis::set("refresh_token:{$userId}:{$systemCode}", $refreshToken, config('jwt.jwt_refresh_token_expiration')); // 建立refresh_token到user_id+system_code映射,便于校验 Redis::set("refresh_token_map:{$refreshToken}:{$systemCode}", $userId, config('jwt.jwt_refresh_token_expiration')); return $refreshToken; } /** * 刷新token * @param string $refreshToken * @param string|null $systemCode 可选,指定系统 * @return array */ public function refreshToken(string $refreshToken, ?string $systemCode = null): array { if (!$systemCode) throw new BusinessException('缺少system_code', 422); // 校验refresh_token $userId = Redis::get("refresh_token_map:{$refreshToken}:{$systemCode}"); if (!$userId) { throw new BusinessException('refresh_token无效或已过期', 401); } $validToken = Redis::get("refresh_token:{$userId}:{$systemCode}"); if ($validToken !== $refreshToken) { throw new BusinessException('refresh_token无效或已过期', 401); } $user = User::find($userId); if (!$user || $user->status != 1) { throw new BusinessException('用户无效', 403); } $system = $this->validateSystem($systemCode); $this->validateSystemPermission($user, $system); $payload = $this->buildJwtPayload($user, $system); $token = $this->generateAccessToken($payload); $refreshToken = $this->generateRefreshToken($user->id, $systemCode); $this->recordUserToken($user->id, $token, $systemCode); return [ 'access_token' => $token, 'refresh_token' => $refreshToken, 'expires_in' => config('jwt.jwt_expiration'), // 'payload' => $payload, ]; } /** * 登出/失效token */ public function logout(?string $token, ?string $systemCode = null) { if ($token) { Redis::set("token_blacklist:{$token}", 1, config('jwt.jwt_expiration')); $payload = null; try { $payload = (new Jwt())->verifyToken($token); } catch (\Throwable $e) {} $userId = $payload['uid'] ?? null; $aud = $payload['aud'] ?? null; $sc = $systemCode ?? $aud; if ($userId && $sc) { Redis::sRem("user_tokens:{$userId}:{$sc}", $token); } Redis::del("user_token_meta:{$token}"); UserTokenLog::where('token', $token)->update(['is_valid'=>0]); } } /** * 强制拉黑token */ public function blockToken(string $token, ?string $systemCode = null) { if (!$token) { throw new BusinessException('token不能为空', 422); } Redis::set("token_blacklist:{$token}", 1, config('jwt.jwt_expiration')); $payload = null; try { $payload = (new Jwt())->verifyToken($token); } catch (\Throwable $e) {} $userId = $payload['uid'] ?? null; $aud = $payload['aud'] ?? null; $sc = $systemCode ?? $aud; if ($userId && $sc) { Redis::sRem("user_tokens:{$userId}:{$sc}", $token); } Redis::del("user_token_meta:{$token}"); UserTokenLog::where('token', $token)->update(['is_valid'=>0]); } public function getHome($systemId, $data) { // // 2025.10.30 临时处理 ====== start // if ($systemId == 1) { // if (in_array(1, $data['role_ids'])) { // return '/dashboard/console'; // } // if (count($data['role_ids']) == 1 && in_array(5, $data['role_ids'])) { // return '/dashboard/console/backoffice'; // } // if (count($data['role_ids']) == 1 && in_array(6, $data['role_ids'])) { // return '/dashboard/console/finance'; // } // } // // 2025.10.30 临时处理 ====== end // // $info = Resource::where('system_id', $systemId) // ->where('is_home', 1) // ->first(); $resources_id = (new UserDashboardRuleLogic)->getUserDashboard($data['uid']); if(!empty($resources_id)){ $info = Resource::where('system_id', $systemId) ->where('id', $resources_id) ->first(); } if (!$info) { return '/dashboard/console'; } // 递归拼接 URL $url = $this->buildParentUrl($info); return $url ?: '/dashboard/console'; } /** * 递归构建父级 URL * @param Resource $resource * @return string */ protected function buildParentUrl(Resource $resource) { $url = $resource->url ?? ''; // 如果存在父级,则递归拼接 if ($resource->parent_id != 0) { $parent = Resource::find($resource->parent_id); if ($parent) { $parentUrl = $this->buildParentUrl($parent); $url = $parentUrl . ($url ? '/' . ltrim($url, '/') : ''); } } return $url; } }