post('username'); $password = $request->post('password'); if (!$username || !$password) { return $this->error('用户名或密码不能为空'); } // 通过邮箱或手机号查找用户 $user = User::where(function ($query) use ($username) { $query->where('email', $username) ->orWhere('phone', $username); })->where('status', 1) ->first(); if (!$user) { return $this->error('用户不存在或已被禁用'); } // 验证密码(如果没有设置密码,提示使用飞书登录) if (!$user->password) { return $this->error('该账号未设置密码,请使用飞书登录'); } if (!password_verify($password, $user->password)) { return $this->error('密码错误'); } // 生成 token $token = JwtUtil::encode([ 'user_id' => $user->id, 'company_id' => $user->company_id, 'username' => $user->name, ]); return $this->success([ 'token' => $token, 'user' => $user->load('company'), ]); } /** * 飞书免登 */ public function feishuLogin(Request $request): Response { $authCode = $request->post('auth_code'); $appId = $request->post('app_id'); $tenantKey = $request->post('tenant_key'); $companyParam = $request->post('company'); // 新增:company 参数 if (!$authCode) { return $this->error('缺少授权码'); } try { $feishuService = new FeishuService(); $companyId = null; $feishuApp = null; // 方式1: 根据 company 参数查找公司配置(新增,用于多公司支持) if ($companyParam) { error_log("🔍 使用 company 参数查找配置: {$companyParam}"); // company 参数是 company_id $company = \app\model\Company::find($companyParam); if (!$company) { return $this->error('公司不存在'); } $companyId = $company->id; $tenantKey = $company->tenant_key; // 从公司表获取 tenant_key // 获取该公司的默认飞书应用 $feishuApp = \app\model\CompanyFeishuApp::where('company_id', $companyId) ->where('system_id', 1) ->where('status', 1) ->first(); if (!$feishuApp) { return $this->error('该公司未配置飞书应用'); } error_log("✅ 通过 company 找到配置: company_id={$companyId}, tenant_key={$tenantKey}, feishu_app_id={$feishuApp->id}, app_id={$feishuApp->app_id}"); $feishuService->setCompany($companyId); $appId = $feishuApp->id; } // 方式2: 根据 app_id 查找飞书应用配置 elseif ($appId) { error_log("🔍 使用 app_id 查找配置: {$appId}"); // 根据 app_id 查找飞书应用 $feishuApp = \app\model\CompanyFeishuApp::where('app_id', $appId) ->where('status', 1) ->first(); if (!$feishuApp) { return $this->error('飞书应用配置不存在'); } $companyId = $feishuApp->company_id; error_log("✅ 找到飞书应用配置: company_id={$companyId}, feishu_app_id={$feishuApp->id}"); $feishuService->setCompany($companyId); } // 方式3: 根据 tenant_key 查找(兼容旧版) elseif ($tenantKey) { error_log("🔍 使用 tenant_key 查找配置: {$tenantKey}"); $company = $feishuService->getCompanyByTenantKey($tenantKey); if (!$company) { return $this->error('公司不存在'); } $companyId = $company->id; // 获取默认飞书应用 $feishuApp = \app\model\CompanyFeishuApp::where('company_id', $companyId) ->where('system_id', 1) ->where('status', 1) ->first(); if (!$feishuApp) { return $this->error('未配置飞书应用'); } error_log("✅ 找到公司配置: company_id={$companyId}, feishu_app_id={$feishuApp->id}"); $feishuService->setCompany($companyId); // 设置应用ID $appId = $feishuApp->id; } else { return $this->error('缺少 company、app_id 或 tenant_key 参数'); } // 获取用户信息 $feishuData = $feishuService->getUserInfo($authCode); error_log("👤 获取到用户信息: " . json_encode($feishuData)); // 创建或更新用户(传递应用ID) $user = $feishuService->createOrUpdateUser($feishuData, $companyId, $appId); // 生成 JWT Token $token = JwtUtil::encode([ 'user_id' => $user->id, 'company_id' => $user->company_id, 'feishu_app_id' => $user->feishu_app_id, 'union_id' => $user->union_id, ]); error_log("✅ 登录成功: user_id={$user->id}, company_id={$user->company_id}"); return $this->success([ 'token' => $token, 'user' => $user->load('company'), ]); } catch (\Exception $e) { error_log("❌ 登录失败: " . $e->getMessage()); return $this->error($e->getMessage()); } } /** * 获取当前用户信息 */ public function me(Request $request): Response { $userId = $this->getUserId($request); if (!$userId) { return $this->error('未登录', 401); } $user = User::with('company')->find($userId); if (!$user) { return $this->error('用户不存在'); } return $this->success($user); } /** * 生成飞书扫码登录二维码 */ public function qrcodeLogin(Request $request): Response { $tenantKey = $request->post('tenant_key'); $appId = $request->post('app_id'); $companyParam = $request->post('company'); // 新增:company 参数 error_log("📱 qrcodeLogin - tenant_key: {$tenantKey}, app_id: {$appId}, company: {$companyParam}"); try { $feishuService = new \app\service\FeishuService(); $companyId = null; $feishuApp = null; // 方式1: 根据 company 参数查找公司配置(新增,用于多公司支持) if ($companyParam) { error_log("🔍 使用 company 参数查找配置: {$companyParam}"); // company 参数是 company_id $company = \app\model\Company::find($companyParam); if (!$company) { return $this->error('公司不存在'); } $companyId = $company->id; $tenantKey = $company->tenant_key; // 从公司表获取 tenant_key // 获取该公司的默认飞书应用 $feishuApp = \app\model\CompanyFeishuApp::where('company_id', $companyId) ->where('system_id', 1) ->where('status', 1) ->first(); if (!$feishuApp) { return $this->error('该公司未配置飞书应用'); } error_log("✅ 通过 company 找到配置: company_id={$companyId}, tenant_key={$tenantKey}, feishu_app_id={$feishuApp->id}, app_id={$feishuApp->app_id}"); } // 方式2: 根据 app_id 查找飞书应用配置(推荐) elseif ($appId) { $feishuApp = \app\model\CompanyFeishuApp::where('app_id', $appId) ->where('status', 1) ->first(); if (!$feishuApp) { return $this->error('飞书应用配置不存在'); } $companyId = $feishuApp->company_id; error_log("✅ 通过 app_id 找到配置: company_id={$companyId}"); } // 方式3: 根据 tenant_key 查找(兼容旧版) elseif ($tenantKey) { $company = $feishuService->getCompanyByTenantKey($tenantKey); if (!$company) { return $this->error('公司不存在'); } $companyId = $company->id; // 获取默认飞书应用 $feishuApp = \app\model\CompanyFeishuApp::where('company_id', $companyId) ->where('system_id', 1) ->where('status', 1) ->first(); if (!$feishuApp) { return $this->error('未配置飞书应用'); } error_log("✅ 通过 tenant_key 找到配置: company_id={$companyId}"); } else { return $this->error('缺少 company、app_id 或 tenant_key 参数'); } $feishuService->setCompany($companyId); // 生成登录会话 $loginSession = \app\model\FeishuLoginSession::create([ 'company_id' => $companyId, 'tenant_key' => $tenantKey, 'feishu_app_id' => $feishuApp->id, 'status' => 'pending', 'expires_at' => date('Y-m-d H:i:s', time() + 300), // 5分钟过期 ]); // 构建扫码登录 URL(使用数据库中配置的回调地址) if ($feishuApp->mobile_callback_url) { $callbackUrl = $feishuApp->mobile_callback_url; } else { // 检查反向代理的协议头 $scheme = request()->header('x-forwarded-proto') ?? request()->header('x-scheme') ?? 'https'; $callbackUrl = $scheme . '://' . request()->host() . '/login/callback'; } $redirectUri = urlencode($callbackUrl); $authUrl = "https://passport.feishu.cn/suite/passport/oauth/authorize?client_id={$feishuApp->app_id}&redirect_uri={$redirectUri}&response_type=code&state={$loginSession->id}"; error_log("✅ 生成二维码会话: session_id={$loginSession->id}"); return $this->success([ 'session_id' => $loginSession->id, 'auth_url' => $authUrl, 'expires_at' => $loginSession->expires_at, ]); } catch (\Exception $e) { error_log("❌ qrcodeLogin 失败: " . $e->getMessage()); return $this->error($e->getMessage()); } } /** * 轮询扫码登录状态 */ public function qrcodeLoginStatus(Request $request): Response { $sessionId = $request->get('session_id'); if (!$sessionId) { return $this->error('缺少会话ID'); } $session = \app\model\FeishuLoginSession::find($sessionId); if (!$session) { return $this->error('登录会话不存在'); } // 检查是否过期 if (strtotime($session->expires_at) < time()) { $session->status = 'expired'; $session->save(); return $this->error('二维码已过期,请刷新重试'); } // 返回当前状态 if ($session->status === 'success') { // 生成 token $user = \app\model\User::find($session->user_id); if (!$user) { return $this->error('用户不存在'); } $token = \app\util\JwtUtil::encode([ 'user_id' => $user->id, 'company_id' => $user->company_id, 'feishu_user_id' => $user->feishu_user_id, 'union_id' => $user->union_id, ]); // 清除会话 $session->delete(); return $this->success([ 'status' => 'success', 'token' => $token, 'user' => $user->load('company'), ]); } else if ($session->status === 'expired') { return $this->error('二维码已过期,请刷新重试'); } else { // pending 或 scanning return $this->success([ 'status' => $session->status, ]); } } /** * 飞书扫码登录回调(飞书授权后的回调) */ public function qrcodeLoginCallback(Request $request): Response { // 从 state 参数获取 session_id $sessionId = $request->get('state'); // OAuth 2.0 标准参数是 code $authCode = $request->get('code'); error_log("🔔 QRCode callback received - session_id: {$sessionId}, code: " . substr($authCode ?? 'null', 0, 20) . "..."); // 如果没有参数,直接返回空页面(可能是 QR SDK 的预检请求) if (!$sessionId && !$authCode) { error_log("ℹ️ Callback called without parameters, returning empty page"); return new \Webman\Http\Response(200, ['Content-Type' => 'text/html; charset=utf-8'], ''); } if (!$sessionId) { error_log("❌ Callback missing session_id"); return $this->error('缺少会话ID'); } if (!$authCode) { error_log("❌ Callback missing auth_code"); return $this->error('缺少授权码'); } try { $session = \app\model\FeishuLoginSession::find($sessionId); error_log("🔍 Session found: " . ($session ? "id={$session->id}, status={$session->status}" : "null")); if (!$session) { error_log("❌ Session not found for id: {$sessionId}"); return $this->error('登录会话不存在'); } // 检查是否过期 if (strtotime($session->expires_at) < time()) { error_log("⏰ Session expired at: {$session->expires_at}"); $session->status = 'expired'; $session->save(); return $this->error('二维码已过期'); } error_log("🔄 Processing login for company_id: {$session->company_id}, feishu_app_id: {$session->feishu_app_id}"); // 使用 session 的公司信息 $feishuService = new \app\service\FeishuService(); $feishuService->setCompany($session->company_id); // 获取用户信息 $feishuData = $feishuService->getUserInfo($authCode); error_log("👤 Got user info: " . json_encode($feishuData)); // 创建或更新用户(传递飞书应用ID) $user = $feishuService->createOrUpdateUser($feishuData, $session->company_id, $session->feishu_app_id); error_log("✅ User created/updated: user_id={$user->id}"); // 更新会话状态 $session->status = 'success'; $session->user_id = $user->id; $saved = $session->save(); error_log("💾 Session save result: " . ($saved ? 'success' : 'failed') . ", new status: {$session->status}"); // 生成 JWT Token $token = \app\util\JwtUtil::encode([ 'user_id' => $user->id, 'company_id' => $user->company_id, 'feishu_user_id' => $user->feishu_user_id, 'union_id' => $user->union_id, ]); error_log("🎫 Generated token for user: {$user->id}"); // 返回成功页面(包含 token,前端会保存到 localStorage) $userName = htmlspecialchars($user->name ?? '用户'); // 准备用户数据JSON $userJson = json_encode([ 'id' => $user->id, 'name' => $user->name, 'email' => $user->email, 'avatar' => $user->avatar, 'phone' => $user->phone, 'company_id' => $user->company_id, 'status' => $user->status, ], JSON_HEX_APOS | JSON_HEX_QUOT | JSON_HEX_AMP | JSON_UNESCAPED_UNICODE); $html = << 登录成功
✅

登录成功!

欢迎回来,{$userName}

正在跳转到应用...

HTML; return new \Webman\Http\Response(200, ['Content-Type' => 'text/html; charset=utf-8'], $html); } catch (\Exception $e) { return $this->error($e->getMessage()); } } /** * 获取飞书配置(用于前端免登) * 根据 company 参数返回对应的 appId */ public function feishuConfig(Request $request): Response { $companyParam = $request->get('company'); if (!$companyParam) { return $this->error('缺少 company 参数'); } try { // company 参数是 company_id $company = \app\model\Company::find($companyParam); if (!$company) { return $this->error('公司不存在'); } $companyId = $company->id; // 获取该公司的默认飞书应用 $feishuApp = \app\model\CompanyFeishuApp::where('company_id', $companyId) ->where('system_id', 1) ->where('status', 1) ->first(); if (!$feishuApp) { return $this->error('该公司未配置飞书应用'); } error_log("✅ feishuConfig - company_id={$companyId}, app_id={$feishuApp->app_id}"); return $this->success([ 'app_id' => $feishuApp->app_id, 'company_id' => $companyId, ]); } catch (\Exception $e) { error_log("❌ feishuConfig 失败: " . $e->getMessage()); return $this->error($e->getMessage()); } } /** * 获取飞书 JS API 鉴权签名 * 用于前端初始化飞书 JS SDK * * company 参数可选,如果未提供则从登录 token 中获取用户所属公司 */ public function feishuJsapiSignature(Request $request): Response { $url = $request->get('url'); $companyParam = $request->get('company'); if (!$url) { return $this->error('缺少 url 参数'); } try { $companyId = null; // 优先使用传入的 company 参数 if ($companyParam) { $company = \app\model\Company::find($companyParam); if (!$company) { return $this->error('公司不存在'); } $companyId = $company->id; error_log("✅ 使用参数中的 company: {$companyId}"); } // 从 token 中获取用户所属公司 else { $companyId = $this->getCompanyId($request); if (!$companyId) { return $this->error('未登录且未指定公司', 401); } error_log("✅ 从 token 获取 company: {$companyId}"); } // 获取该公司的默认飞书应用 $feishuApp = \app\model\CompanyFeishuApp::where('company_id', $companyId) ->where('system_id', 1) ->where('status', 1) ->first(); if (!$feishuApp) { return $this->error('该公司未配置飞书应用'); } // 生成 JS API 签名 $signatureData = $this->generateJsapiSignature($feishuApp->id, $url); return $this->success([ 'app_id' => $feishuApp->app_id, 'company_id' => $companyId, 'jsapi_ticket' => $signatureData['jsapi_ticket'], 'noncestr' => $signatureData['noncestr'], 'timestamp' => $signatureData['timestamp'], 'url' => $signatureData['url'], 'signature' => $signatureData['signature'], ]); } catch (\Exception $e) { error_log("❌ feishuJsapiSignature 失败: " . $e->getMessage()); return $this->error($e->getMessage()); } } /** * 生成 JS API 鉴权签名 * * @param int $feishuAppId 飞书应用配置ID * @param string $url 当前页面URL * @return array 签名数据 */ private function generateJsapiSignature(int $feishuAppId, string $url): array { // 获取 jsapi_ticket $jsapiTicket = \app\util\FeishuUtil::getJsapiTicket($feishuAppId); // 清理 URL:移除 # 和 ? 后面的参数 $url = $this->cleanUrl($url); // 生成随机字符串 $noncestr = $this->generateNonceStr(); // 获取毫秒级时间戳 $timestamp = (int)round(microtime(true) * 1000); // 拼接签名字符串 $verifyStr = "jsapi_ticket={$jsapiTicket}&noncestr={$noncestr}×tamp={$timestamp}&url={$url}"; // 生成 SHA1 签名 $signature = sha1($verifyStr); error_log("✅ JS API 签名生成成功: url={$url}"); return [ 'jsapi_ticket' => $jsapiTicket, 'noncestr' => $noncestr, 'timestamp' => $timestamp, 'url' => $url, 'signature' => $signature, ]; } /** * 清理 URL,移除 # 和 ? 后面的参数 */ private function cleanUrl(string $url): string { // 移除 # 后面的内容 $pos = strpos($url, '#'); if ($pos !== false) { $url = substr($url, 0, $pos); } // 移除 ? 后面的内容 $pos = strpos($url, '?'); if ($pos !== false) { $url = substr($url, 0, $pos); } return $url; } /** * 生成随机字符串(由数字、字母组成) */ private function generateNonceStr(int $length = 16): string { $chars = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789'; $noncestr = ''; for ($i = 0; $i < $length; $i++) { $noncestr .= $chars[mt_rand(0, strlen($chars) - 1)]; } return $noncestr; } }