diff --git a/backend/app/controller/AuthController.php b/backend/app/controller/AuthController.php
index fc7654d..a9e5c05 100644
--- a/backend/app/controller/AuthController.php
+++ b/backend/app/controller/AuthController.php
@@ -12,25 +12,38 @@ use Webman\Http\Response;
class AuthController extends BaseController
{
/**
- * 用户登录
+ * 用户登录(邮箱/手机号 + 密码)
*/
public function login(Request $request): Response
{
$username = $request->post('username');
$password = $request->post('password');
- // TODO: 实现真实的登录逻辑
if (!$username || !$password) {
return $this->error('用户名或密码不能为空');
}
- // 模拟用户数据(用于测试)
- $user = User::first();
+ // 通过邮箱或手机号查找用户
+ $user = User::where(function ($query) use ($username) {
+ $query->where('email', $username)
+ ->orWhere('phone', $username);
+ })->where('status', 1)
+ ->first();
if (!$user) {
- return $this->error('用户不存在');
+ 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,
@@ -39,7 +52,7 @@ class AuthController extends BaseController
return $this->success([
'token' => $token,
- 'user' => $user,
+ 'user' => $user->load('company'),
]);
}
@@ -49,6 +62,7 @@ class AuthController extends BaseController
public function feishuLogin(Request $request): Response
{
$authCode = $request->post('auth_code');
+ $appId = $request->post('app_id');
$tenantKey = $request->post('tenant_key');
if (!$authCode) {
@@ -58,33 +72,74 @@ class AuthController extends BaseController
try {
$feishuService = new FeishuService();
- // 根据 tenant_key 获取公司
- $company = $feishuService->getCompanyByTenantKey($tenantKey);
+ // 方式1: 根据 app_id 查找飞书应用配置(推荐)
+ if ($appId) {
+ error_log("🔍 使用 app_id 查找配置: {$appId}");
- if (!$company) {
- return $this->error('公司不存在');
+ // 根据 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}");
+
+ $feishuService->setCompany($companyId);
+ }
+ // 方式2: 根据 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}");
+ $feishuService->setCompany($companyId);
+ } else {
+ return $this->error('缺少 app_id 或 tenant_key 参数');
}
- // 设置公司并获取用户信息
- $feishuService->setCompany($company->id);
+ // 获取用户信息
$feishuData = $feishuService->getUserInfo($authCode);
+ error_log("👤 获取到用户信息: " . json_encode($feishuData));
// 创建或更新用户
- $user = $feishuService->createOrUpdateUser($feishuData, $company->id);
+ $user = $feishuService->createOrUpdateUser($feishuData, $companyId);
// 生成 JWT Token
$token = JwtUtil::encode([
'user_id' => $user->id,
'company_id' => $user->company_id,
'feishu_user_id' => $user->feishu_user_id,
- 'union_id' => $user->union_id,
+ 'union_id' => $user->feishu_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());
}
}
@@ -115,44 +170,75 @@ class AuthController extends BaseController
public function qrcodeLogin(Request $request): Response
{
$tenantKey = $request->post('tenant_key');
+ $appId = $request->post('app_id');
- if (!$tenantKey) {
- return $this->error('缺少租户密钥');
- }
+ error_log("📱 qrcodeLogin - tenant_key: {$tenantKey}, app_id: {$appId}");
try {
$feishuService = new \app\service\FeishuService();
+ $companyId = null;
+ $feishuApp = null;
- // 根据 tenant_key 获取公司
- $company = $feishuService->getCompanyByTenantKey($tenantKey);
+ // 方式1: 根据 app_id 查找飞书应用配置(推荐)
+ if ($appId) {
+ $feishuApp = \app\model\CompanyFeishuApp::where('app_id', $appId)
+ ->where('status', 1)
+ ->first();
- if (!$company) {
- return $this->error('公司不存在');
+ if (!$feishuApp) {
+ return $this->error('飞书应用配置不存在');
+ }
+
+ $companyId = $feishuApp->company_id;
+ error_log("✅ 通过 app_id 找到配置: company_id={$companyId}");
+ }
+ // 方式2: 根据 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('缺少 app_id 或 tenant_key 参数');
}
- $feishuService->setCompany($company->id);
+ $feishuService->setCompany($companyId);
// 生成登录会话
$loginSession = \app\model\FeishuLoginSession::create([
- 'company_id' => $company->id,
+ 'company_id' => $companyId,
'tenant_key' => $tenantKey,
'status' => 'pending',
'expires_at' => date('Y-m-d H:i:s', time() + 300), // 5分钟过期
]);
- // 构建扫码登录 URL(使用官方 passport 端点)
- $appId = 'cli_a8033d607cba500d';
- // 使用固定的 redirect_uri,session_id 通过 state 传递
+ // 构建扫码登录 URL
$redirectUri = urlencode("https://xsm.dev.hnxinyouhe.com/login/callback");
- $authUrl = "https://passport.feishu.cn/suite/passport/oauth/authorize?client_id={$appId}&redirect_uri={$redirectUri}&response_type=code&state={$loginSession->id}";
+ $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}");
- // 生成二维码内容(返回 URL,前端生成二维码)
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());
}
}
@@ -224,46 +310,191 @@ class AuthController extends BaseController
// 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}");
+
// 使用 session 的公司信息
$feishuService = new \app\service\FeishuService();
$feishuService->setCompany($session->company_id);
// 获取用户信息
$feishuData = $feishuService->getUserInfo($authCode);
+ error_log("👤 Got user info: " . json_encode($feishuData));
// 创建或更新用户
$user = $feishuService->createOrUpdateUser($feishuData, $session->company_id);
+ error_log("✅ User created/updated: user_id={$user->id}");
// 更新会话状态
$session->status = 'success';
$session->user_id = $user->id;
- $session->save();
+ $saved = $session->save();
+ error_log("💾 Session save result: " . ($saved ? 'success' : 'failed') . ", new status: {$session->status}");
- // 返回极简空白页面(飞书APP内扫码通常不会真正访问此URL)
- // 飞书会在APP内直接处理回调,返回到之前的界面
- return new \Webman\Http\Response(200, ['Content-Type' => 'text/html; charset=utf-8'], '');
+ // 生成 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->feishu_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());
}
diff --git a/backend/app/controller/UserController.php b/backend/app/controller/UserController.php
index 1893380..f73cf64 100644
--- a/backend/app/controller/UserController.php
+++ b/backend/app/controller/UserController.php
@@ -60,6 +60,47 @@ class UserController extends BaseController
return $this->success();
}
+ /**
+ * 修改密码
+ */
+ public function changePassword(Request $request): Response
+ {
+ $userId = $this->getUserId($request);
+ $user = User::find($userId);
+
+ if (!$user) {
+ return $this->error('用户不存在');
+ }
+
+ $oldPassword = $request->post('old_password');
+ $newPassword = $request->post('new_password');
+ $confirmPassword = $request->post('confirm_password');
+
+ // 验证参数
+ if (!$oldPassword || !$newPassword || !$confirmPassword) {
+ return $this->error('请填写完整信息');
+ }
+
+ if (strlen($newPassword) < 6) {
+ return $this->error('新密码长度不能少于6位');
+ }
+
+ if ($newPassword !== $confirmPassword) {
+ return $this->error('两次输入的密码不一致');
+ }
+
+ // 验证旧密码
+ if ($user->password && !password_verify($oldPassword, $user->password)) {
+ return $this->error('原密码错误');
+ }
+
+ // 更新密码
+ $user->password = password_hash($newPassword, PASSWORD_DEFAULT);
+ $user->save();
+
+ return $this->success(null, '密码修改成功');
+ }
+
/**
* 获取用户列表(用于选择审批人等)
*/
diff --git a/backend/app/service/FeishuNotificationService.php b/backend/app/service/FeishuNotificationService.php
new file mode 100644
index 0000000..8491adf
--- /dev/null
+++ b/backend/app/service/FeishuNotificationService.php
@@ -0,0 +1,478 @@
+companyId = $companyId;
+ return $this;
+ }
+
+ /**
+ * 设置飞书应用ID(支持多应用)
+ */
+ public function setApp(int $appId): self
+ {
+ $this->appId = $appId;
+ return $this;
+ }
+
+ /**
+ * 获取飞书应用配置
+ */
+ private function getAppConfig(): array
+ {
+ if (!$this->companyId) {
+ throw new Exception('未设置公司ID');
+ }
+
+ if ($this->appId) {
+ $app = \app\model\CompanyFeishuApp::find($this->appId);
+ if (!$app || $app->status !== 1) {
+ throw new Exception('飞书应用配置不存在');
+ }
+ return $app->toArray();
+ }
+
+ $app = \app\model\CompanyFeishuApp::where('company_id', $this->companyId)
+ ->where('system_id', 1)
+ ->where('status', 1)
+ ->orderBy('sort')
+ ->first();
+
+ if (!$app) {
+ throw new Exception('未配置飞书应用');
+ }
+
+ return $app->toArray();
+ }
+
+ /**
+ * 获取 access_token
+ */
+ private function getAccessToken(): string
+ {
+ $app = $this->getAppConfig();
+ return FeishuUtil::getAppAccessToken($app['id']);
+ }
+
+ /**
+ * 发送审批通知
+ *
+ * @param array $params
+ * - user_id: 接收人ID
+ * - title: 通知标题
+ * - applicant: 申请人姓名
+ * - type: 申请类型
+ * - content: 申请内容摘要
+ * - url: 详情链接
+ * - approve_url: 通过按钮链接(可选)
+ * - reject_url: 拒绝按钮链接(可选)
+ * @return bool
+ */
+ public function sendApprovalNotification(array $params): bool
+ {
+ $userId = $params['user_id'] ?? null;
+ if (!$userId) {
+ error_log('❌ sendApprovalNotification: 缺少 user_id');
+ return false;
+ }
+
+ try {
+ $user = User::find($userId);
+ if (!$user || !$user->feishu_user_id) {
+ error_log("❌ sendApprovalNotification: 用户不存在或未绑定飞书, user_id={$userId}");
+ return false;
+ }
+
+ $card = $this->buildApprovalCard($params);
+ return $this->sendMessage($user->feishu_user_id, $card);
+ } catch (Exception $e) {
+ error_log('❌ sendApprovalNotification 失败: ' . $e->getMessage());
+ return false;
+ }
+ }
+
+ /**
+ * 发送系统通知
+ *
+ * @param array $params
+ * - user_ids: 接收人ID数组
+ * - title: 通知标题
+ * - content: 通知内容
+ * - url: 跳转链接(可选)
+ * @return array 返回每个用户的发送结果
+ */
+ public function sendSystemNotification(array $params): array
+ {
+ $userIds = $params['user_ids'] ?? [];
+ if (empty($userIds)) {
+ error_log('❌ sendSystemNotification: 缺少 user_ids');
+ return [];
+ }
+
+ $results = [];
+ foreach ($userIds as $userId) {
+ try {
+ $user = User::find($userId);
+ if (!$user || !$user->feishu_user_id) {
+ $results[$userId] = false;
+ continue;
+ }
+
+ $card = $this->buildSystemCard($params);
+ $results[$userId] = $this->sendMessage($user->feishu_user_id, $card);
+ } catch (Exception $e) {
+ error_log("❌ sendSystemNotification 失败: user_id={$userId}, error=" . $e->getMessage());
+ $results[$userId] = false;
+ }
+ }
+
+ return $results;
+ }
+
+ /**
+ * 发送提醒通知
+ *
+ * @param array $params
+ * - user_id: 接收人ID
+ * - title: 提醒标题
+ * - content: 提醒内容
+ * - level: 提醒级别 (info/warning/urgent)
+ * - url: 跳转链接(可选)
+ * @return bool
+ */
+ public function sendReminderNotification(array $params): bool
+ {
+ $userId = $params['user_id'] ?? null;
+ if (!$userId) {
+ error_log('❌ sendReminderNotification: 缺少 user_id');
+ return false;
+ }
+
+ try {
+ $user = User::find($userId);
+ if (!$user || !$user->feishu_user_id) {
+ error_log("❌ sendReminderNotification: 用户不存在或未绑定飞书, user_id={$userId}");
+ return false;
+ }
+
+ $card = $this->buildReminderCard($params);
+ return $this->sendMessage($user->feishu_user_id, $card);
+ } catch (Exception $e) {
+ error_log('❌ sendReminderNotification 失败: ' . $e->getMessage());
+ return false;
+ }
+ }
+
+ /**
+ * 批量发送通知
+ *
+ * @param array $params
+ * - user_ids: 接收人ID数组
+ * - type: 通知类型 (approval/system/reminder)
+ * - 其他参数根据类型不同而不同
+ * @return array
+ */
+ public function sendBatchNotification(array $params): array
+ {
+ $userIds = $params['user_ids'] ?? [];
+ $type = $params['type'] ?? 'system';
+
+ $results = [];
+ foreach ($userIds as $userId) {
+ $notifyParams = array_merge($params, ['user_id' => $userId]);
+ unset($notifyParams['user_ids']);
+
+ switch ($type) {
+ case 'approval':
+ $results[$userId] = $this->sendApprovalNotification($notifyParams);
+ break;
+ case 'reminder':
+ $results[$userId] = $this->sendReminderNotification($notifyParams);
+ break;
+ case 'system':
+ default:
+ $results[$userId] = $this->sendSystemNotification(['user_ids' => [$userId]] + $params)[$userId] ?? false;
+ break;
+ }
+ }
+
+ return $results;
+ }
+
+ /**
+ * 构建审批通知卡片
+ */
+ private function buildApprovalCard(array $params): array
+ {
+ $title = $params['title'] ?? '新的审批申请';
+ $applicant = $params['applicant'] ?? '未知';
+ $type = $params['type'] ?? '申请';
+ $content = $params['content'] ?? '';
+ $url = $params['url'] ?? '';
+ $approveUrl = $params['approve_url'] ?? '';
+ $rejectUrl = $params['reject_url'] ?? '';
+
+ $elements = [
+ [
+ 'tag' => 'div',
+ 'fields' => [
+ [
+ 'is_short' => true,
+ 'text' => [
+ 'content' => "**申请人**: {$applicant}",
+ 'tag' => 'lark_md',
+ ],
+ ],
+ [
+ 'is_short' => true,
+ 'text' => [
+ 'content' => "**申请类型**: {$type}",
+ 'tag' => 'lark_md',
+ ],
+ ],
+ ],
+ ],
+ ];
+
+ if ($content) {
+ $elements[] = [
+ 'tag' => 'div',
+ 'text' => [
+ 'content' => "**申请内容**:\n{$content}",
+ 'tag' => 'lark_md',
+ ],
+ ];
+ }
+
+ // 构建操作按钮
+ $actions = [];
+
+ if ($approveUrl) {
+ $actions[] = [
+ 'tag' => 'button',
+ 'text' => [
+ 'content' => '通过',
+ 'tag' => 'plain_text',
+ ],
+ 'type' => 'primary',
+ 'url' => $approveUrl,
+ ];
+ }
+
+ if ($rejectUrl) {
+ $actions[] = [
+ 'tag' => 'button',
+ 'text' => [
+ 'content' => '拒绝',
+ 'tag' => 'plain_text',
+ ],
+ 'type' => 'default',
+ 'url' => $rejectUrl,
+ ];
+ }
+
+ if ($url) {
+ $actions[] = [
+ 'tag' => 'button',
+ 'text' => [
+ 'content' => '查看详情',
+ 'tag' => 'plain_text',
+ ],
+ 'type' => 'default',
+ 'url' => $url,
+ ];
+ }
+
+ if (!empty($actions)) {
+ $elements[] = [
+ 'tag' => 'action',
+ 'actions' => $actions,
+ ];
+ }
+
+ return [
+ 'header' => [
+ 'title' => [
+ 'content' => "📋 {$title}",
+ 'tag' => 'plain_text',
+ ],
+ 'template' => 'blue',
+ ],
+ 'elements' => $elements,
+ ];
+ }
+
+ /**
+ * 构建系统通知卡片
+ */
+ private function buildSystemCard(array $params): array
+ {
+ $title = $params['title'] ?? '系统通知';
+ $content = $params['content'] ?? '';
+ $url = $params['url'] ?? '';
+
+ $elements = [];
+
+ if ($content) {
+ $elements[] = [
+ 'tag' => 'div',
+ 'text' => [
+ 'content' => $content,
+ 'tag' => 'plain_text',
+ ],
+ ];
+ }
+
+ if ($url) {
+ $elements[] = [
+ 'tag' => 'action',
+ 'actions' => [
+ [
+ 'tag' => 'button',
+ 'text' => [
+ 'content' => '查看详情',
+ 'tag' => 'plain_text',
+ ],
+ 'type' => 'default',
+ 'url' => $url,
+ ],
+ ],
+ ];
+ }
+
+ return [
+ 'header' => [
+ 'title' => [
+ 'content' => "🔔 {$title}",
+ 'tag' => 'plain_text',
+ ],
+ 'template' => 'gray',
+ ],
+ 'elements' => $elements,
+ ];
+ }
+
+ /**
+ * 构建提醒通知卡片
+ */
+ private function buildReminderCard(array $params): array
+ {
+ $title = $params['title'] ?? '提醒';
+ $content = $params['content'] ?? '';
+ $level = $params['level'] ?? 'info'; // info, warning, urgent
+ $url = $params['url'] ?? '';
+
+ // 根据级别选择模板颜色
+ $templateMap = [
+ 'info' => 'blue',
+ 'warning' => 'orange',
+ 'urgent' => 'red',
+ ];
+ $template = $templateMap[$level] ?? 'blue';
+
+ // 根据级别选择图标
+ $iconMap = [
+ 'info' => '💡',
+ 'warning' => '⚠️',
+ 'urgent' => '🚨',
+ ];
+ $icon = $iconMap[$level] ?? '💡';
+
+ $elements = [];
+
+ if ($content) {
+ $elements[] = [
+ 'tag' => 'div',
+ 'text' => [
+ 'content' => $content,
+ 'tag' => 'plain_text',
+ ],
+ ];
+ }
+
+ if ($url) {
+ $elements[] = [
+ 'tag' => 'action',
+ 'actions' => [
+ [
+ 'tag' => 'button',
+ 'text' => [
+ 'content' => '查看',
+ 'tag' => 'plain_text',
+ ],
+ 'type' => 'primary',
+ 'url' => $url,
+ ],
+ ],
+ ];
+ }
+
+ return [
+ 'header' => [
+ 'title' => [
+ 'content' => "{$icon} {$title}",
+ 'tag' => 'plain_text',
+ ],
+ 'template' => $template,
+ ],
+ 'elements' => $elements,
+ ];
+ }
+
+ /**
+ * 发送消息到飞书
+ */
+ private function sendMessage(string $feishuUserId, array $card): bool
+ {
+ try {
+ $token = $this->getAccessToken();
+
+ $response = HttpClient::httpPost(
+ "https://open.feishu.cn/open-apis/message/v4/send_messages",
+ [
+ 'receive_id_type' => 'user_id',
+ 'receive_id' => $feishuUserId,
+ 'msg_type' => 'interactive',
+ 'content' => json_encode(['card' => $card]),
+ ],
+ [
+ 'Authorization: Bearer ' . $token,
+ ]
+ );
+
+ $code = $response['code'] ?? -1;
+ $success = $code === 0;
+
+ if ($success) {
+ error_log("✅ 飞书消息发送成功: feishu_user_id={$feishuUserId}");
+ } else {
+ error_log("❌ 飞书消息发送失败: feishu_user_id={$feishuUserId}, code={$code}, msg=" . ($response['msg'] ?? ''));
+ }
+
+ return $success;
+ } catch (Exception $e) {
+ error_log('❌ sendMessage 异常: ' . $e->getMessage());
+ return false;
+ }
+ }
+}
diff --git a/backend/app/service/FeishuService.php b/backend/app/service/FeishuService.php
index 23224f2..27c6074 100644
--- a/backend/app/service/FeishuService.php
+++ b/backend/app/service/FeishuService.php
@@ -122,6 +122,7 @@ class FeishuService
/**
* 创建或更新用户
+ * 注意:同一用户在不同企业的账号是独立的,通过 company_id + union_id 组合查找
*/
public function createOrUpdateUser(array $feishuData, int $companyId): User
{
@@ -135,27 +136,47 @@ class FeishuService
throw new Exception('缺少 union_id');
}
- // 查找或创建用户
- $user = User::byFeishuUnionId($unionId)->first();
+ // ⭐ 关键:通过 company_id + union_id 组合查找用户
+ // 这样同一个 union_id 在不同公司的账号是独立的
+ $user = User::where('company_id', $companyId)
+ ->where('union_id', $unionId)
+ ->first();
if (!$user) {
+ error_log("✨ 创建新用户: company={$companyId}, union_id={$unionId}, name={$name}");
+
+ // 初始密码使用 sn(工号),如果没有 sn 则使用 union_id 的前6位
+ $initialPassword = $feishuData['sn'] ?? substr($unionId, 0, 6);
+
$user = User::create([
'company_id' => $companyId,
'feishu_user_id' => $userId,
- 'feishu_union_id' => $unionId,
+ 'union_id' => $unionId,
'name' => $name,
'email' => $email,
'avatar' => $avatar,
- 'status' => 1,
+ 'password' => password_hash($initialPassword, PASSWORD_DEFAULT), // 初始密码加密
+ 'status' => 1, // 新用户默认在职
]);
+ error_log("🔑 初始密码设置为 sn: {$initialPassword}");
} else {
- // 更新用户信息
- $user->update([
+ error_log("🔄 更新用户: id={$user->id}, company={$companyId}, union_id={$unionId}, 当前状态={$user->status}");
+
+ // 离职员工禁止登录
+ if ($user->status == 0) {
+ error_log("❌ 离职员工尝试登录: id={$user->id}, status=0");
+ throw new Exception('该员工已离职,无法登录系统');
+ }
+
+ // 更新用户信息(在职员工)
+ $updateData = [
'feishu_user_id' => $userId,
'name' => $name,
'avatar' => $avatar,
'email' => $email,
- ]);
+ ];
+
+ $user->update($updateData);
}
return $user;
diff --git a/backend/app/util/FeishuUtil.php b/backend/app/util/FeishuUtil.php
index fbf9eef..7dacb8c 100644
--- a/backend/app/util/FeishuUtil.php
+++ b/backend/app/util/FeishuUtil.php
@@ -50,7 +50,8 @@ class FeishuUtil
]);
if ($response['code'] !== 0) {
- throw new Exception('获取 app_access_token 失败: ' . ($response['msg'] ?? '未知错误'));
+ $errorMsg = $response['msg'] ?? $response['message'] ?? '未知错误';
+ throw new Exception('获取 app_access_token 失败: ' . $errorMsg);
}
$token = $response['tenant_access_token'];
@@ -109,7 +110,8 @@ class FeishuUtil
error_log("📝 获取 user_access_token 响应: " . json_encode($response));
if ($response['code'] !== 0) {
- throw new Exception('获取 user_access_token 失败: ' . ($response['msg'] ?? '未知错误'));
+ $errorMsg = $response['msg'] ?? $response['message'] ?? '未知错误';
+ throw new Exception('获取 user_access_token 失败: ' . $errorMsg);
}
if (!isset($response['data']['access_token'])) {
@@ -149,7 +151,8 @@ class FeishuUtil
error_log("📝 获取用户信息响应: " . json_encode($response));
if ($response['code'] !== 0) {
- throw new Exception('获取用户信息失败: ' . ($response['msg'] ?? '未知错误'));
+ $errorMsg = $response['msg'] ?? $response['message'] ?? '未知错误';
+ throw new Exception('获取用户信息失败: ' . $errorMsg);
}
$userInfo = $response['data'] ?? [];
diff --git a/backend/config/route.php b/backend/config/route.php
index d30ef7f..f995bb3 100644
--- a/backend/config/route.php
+++ b/backend/config/route.php
@@ -24,6 +24,7 @@ Route::group('/api/v1/user', function () {
Route::get('/companies', [app\controller\UserController::class, 'companies']);
Route::get('/list', [app\controller\UserController::class, 'list']);
Route::post('/switch-company', [app\controller\UserController::class, 'switchCompany']);
+ Route::post('/change-password', [app\controller\UserController::class, 'changePassword']);
})->middleware($authMiddleware);
// 审批路由(需要认证)
diff --git a/docs/feishu-qrcode-login.md b/docs/feishu-qrcode-login.md
new file mode 100644
index 0000000..e63f3ab
--- /dev/null
+++ b/docs/feishu-qrcode-login.md
@@ -0,0 +1,1041 @@
+# 飞书登录实现指南
+
+本文档包含两种飞书登录方式:
+1. **扫码登录** - 用户在飞书外打开应用,扫描二维码登录
+2. **静默授权(免登)** - 用户在飞书客户端内打开应用,直接获取身份登录
+
+---
+
+## 一、前置条件
+
+### 1.1 飞书开放平台配置
+
+**必须配置**(关键!否则回调不会被调用):
+
+1. 登录 [飞书开放平台](https://open.feishu.cn/)
+2. 选择应用(如 `cli_a8033d607cba500d`)
+3. 进入 **权限管理** → **安全设置** → **重定向 URL**
+4. 添加回调地址:`https://your-domain.com/login/callback`
+5. **保存并发布应用**
+
+### 1.2 数据库表
+
+确保存在以下表:
+
+```sql
+-- 飞书登录会话表
+CREATE TABLE feishu_login_session (
+ id INT AUTO_INCREMENT PRIMARY KEY,
+ company_id INT NOT NULL,
+ tenant_key VARCHAR(255) NOT NULL,
+ status ENUM('pending', 'scanning', 'success', 'expired') DEFAULT 'pending',
+ user_id INT DEFAULT NULL,
+ expires_at DATETIME NOT NULL,
+ created_at DATETIME DEFAULT CURRENT_TIMESTAMP
+);
+
+-- 公司飞书应用配置表
+CREATE TABLE company_feishu_apps (
+ id INT AUTO_INCREMENT PRIMARY KEY,
+ company_id INT NOT NULL,
+ app_id VARCHAR(255) NOT NULL,
+ app_secret VARCHAR(255) NOT NULL,
+ callback_url VARCHAR(500),
+ mobile_callback_url VARCHAR(500),
+ status TINYINT DEFAULT 1
+);
+```
+
+---
+
+## 二、后端实现
+
+### 2.1 路由配置
+
+```php
+// backend/config/route.php
+
+// 飞书扫码登录相关路由
+Route::post('/api/v1/auth/qrcode-login', [app\controller\AuthController::class, 'qrcodeLogin']);
+Route::get('/api/v1/auth/qrcode-login/status', [app\controller\AuthController::class, 'qrcodeLoginStatus']);
+Route::get('/login/callback', [app\controller\AuthController::class, 'qrcodeLoginCallback']);
+```
+
+### 2.2 控制器实现
+
+```php
+post('tenant_key');
+
+ if (!$tenantKey) {
+ return $this->error('缺少租户密钥');
+ }
+
+ try {
+ $feishuService = new \app\service\FeishuService();
+
+ // 根据 tenant_key 获取公司
+ $company = $feishuService->getCompanyByTenantKey($tenantKey);
+ if (!$company) {
+ return $this->error('公司不存在');
+ }
+
+ $feishuService->setCompany($company->id);
+
+ // 获取飞书应用配置
+ $feishuApp = \app\model\CompanyFeishuApp::where('company_id', $company->id)
+ ->where('system_id', 1)
+ ->where('status', 1)
+ ->first();
+
+ if (!$feishuApp) {
+ return $this->error('未配置飞书应用');
+ }
+
+ // 生成登录会话(5分钟过期)
+ $loginSession = \app\model\FeishuLoginSession::create([
+ 'company_id' => $company->id,
+ 'tenant_key' => $tenantKey,
+ 'status' => 'pending',
+ 'expires_at' => date('Y-m-d H:i:s', time() + 300),
+ ]);
+
+ // 构建扫码登录 URL
+ $redirectUri = urlencode("https://your-domain.com/login/callback");
+ $authUrl = "https://passport.feishu.cn/suite/passport/oauth/authorize?"
+ . "client_id={$feishuApp->app_id}&"
+ . "redirect_uri={$redirectUri}&"
+ . "response_type=code&"
+ . "state={$loginSession->id}";
+
+ return $this->success([
+ 'session_id' => $loginSession->id,
+ 'auth_url' => $authUrl,
+ 'expires_at' => $loginSession->expires_at,
+ ]);
+ } catch (\Exception $e) {
+ return $this->error($e->getMessage());
+ }
+ }
+
+ /**
+ * 轮询扫码登录状态
+ * GET /api/v1/auth/qrcode-login/status?session_id=xxx
+ */
+ 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') {
+ $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->feishu_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,
+ ]);
+ }
+ }
+
+ /**
+ * 飞书扫码登录回调
+ * GET /login/callback?code=xxx&state=xxx
+ */
+ public function qrcodeLoginCallback(Request $request): Response
+ {
+ $sessionId = $request->get('state');
+ $authCode = $request->get('code');
+
+ error_log("🔔 QRCode callback - session_id: {$sessionId}");
+
+ // 无参数时返回空页面(SDK 初始化时的预检请求)
+ if (!$sessionId && !$authCode) {
+ return new \Webman\Http\Response(200, ['Content-Type' => 'text/html; charset=utf-8'], '');
+ }
+
+ if (!$sessionId || !$authCode) {
+ return $this->error('参数错误');
+ }
+
+ try {
+ $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('二维码已过期');
+ }
+
+ // 获取用户信息
+ $feishuService = new \app\service\FeishuService();
+ $feishuService->setCompany($session->company_id);
+ $feishuData = $feishuService->getUserInfo($authCode);
+
+ // 创建或更新用户
+ $user = $feishuService->createOrUpdateUser($feishuData, $session->company_id);
+
+ // 更新会话状态
+ $session->status = 'success';
+ $session->user_id = $user->id;
+ $session->save();
+
+ // 生成 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->feishu_union_id,
+ ]);
+
+ // 返回包含 token 的 HTML 页面
+ $userName = htmlspecialchars($user->name ?? '用户');
+ $html = $this->getSuccessHtml($token, $user->id, $userName);
+
+ return new \Webman\Http\Response(200, ['Content-Type' => 'text/html; charset=utf-8'], $html);
+
+ } catch (\Exception $e) {
+ error_log("❌ Callback error: " . $e->getMessage());
+ return $this->error($e->getMessage());
+ }
+ }
+
+ /**
+ * 生成登录成功 HTML 页面
+ */
+ private function getSuccessHtml(string $token, int $userId, string $userName): string
+ {
+ return <<
+
+
+
+
+ 登录成功
+
+
+
+
+
✅
+
登录成功!
+
欢迎回来,{$userName}
+
正在跳转到应用...
+
+
+
+
+
+HTML;
+ }
+}
+```
+
+---
+
+## 三、飞书静默授权(免登)实现
+
+飞书静默授权是指用户在飞书客户端内打开应用时,可以直接获取用户身份登录,无需扫码或输入密码。
+
+### 3.1 后端接口
+
+后端已有 `/api/v1/auth/feishu-login` 接口,用于处理飞书免登:
+
+```php
+// backend/app/controller/AuthController.php
+
+/**
+ * 飞书免登
+ */
+public function feishuLogin(Request $request): Response
+{
+ $authCode = $request->post('auth_code');
+ $tenantKey = $request->post('tenant_key');
+
+ if (!$authCode) {
+ return $this->error('缺少授权码');
+ }
+
+ try {
+ $feishuService = new FeishuService();
+
+ // 根据 tenant_key 获取公司
+ $company = $feishuService->getCompanyByTenantKey($tenantKey);
+ if (!$company) {
+ return $this->error('公司不存在');
+ }
+
+ // 设置公司并获取用户信息
+ $feishuService->setCompany($company->id);
+ $feishuData = $feishuService->getUserInfo($authCode);
+
+ // 创建或更新用户
+ $user = $feishuService->createOrUpdateUser($feishuData, $company->id);
+
+ // 生成 JWT Token
+ $token = JwtUtil::encode([
+ 'user_id' => $user->id,
+ 'company_id' => $user->company_id,
+ 'feishu_user_id' => $user->feishu_user_id,
+ 'union_id' => $user->union_id,
+ ]);
+
+ return $this->success([
+ 'token' => $token,
+ 'user' => $user->load('company'),
+ ]);
+ } catch (\Exception $e) {
+ return $this->error($e->getMessage());
+ }
+}
+```
+
+### 3.2 前端实现
+
+#### 3.2.1 引入飞书 JSAPI SDK
+
+在 `index.html` 中添加(**注意**:需要同时引入 JSAPI SDK 和 QR SDK):
+
+```html
+
+
+
+
+```
+
+#### 3.2.2 类型声明
+
+```typescript
+// src/types/feishu.d.ts
+
+declare global {
+ interface Window {
+ // 飞书 JSAPI(免登)
+ tt?: {
+ ready: (callback: () => void) => void
+ requestAuthCode: (params: {
+ appId: string
+ success?: (res: { code: string }) => void
+ fail?: (err: any) => void
+ }) => void
+ checkJsApi?: (params: {
+ jsApiList: string[]
+ success: (res: any) => void
+ fail?: (err: any) => void
+ }) => void
+ }
+ // 飞书 QR SDK(扫码登录)
+ QRLogin?: (config: {
+ id: string
+ goto: string
+ width?: string
+ height?: string
+ style?: string
+ }) => {
+ matchOrigin: (origin: string) => boolean
+ matchData: (data: any) => boolean
+ }
+ }
+}
+
+export {}
+```
+
+#### 3.2.3 登录组件实现
+
+```typescript
+// src/pages/login/index.tsx
+
+import { useEffect, useState } from 'react'
+import { useNavigate } from 'react-router-dom'
+import { Toast } from 'antd-mobile'
+import { useUserStore } from '@/stores/user'
+
+export default function Login() {
+ const navigate = useNavigate()
+ const { login } = useUserStore()
+ const [loading, setLoading] = useState(true)
+ const [environment, setEnvironment] = useState<'feishu' | 'external' | null>(null)
+
+ useEffect(() => {
+ // 检测是否在飞书客户端内
+ const ua = navigator.userAgent.toLowerCase()
+ const isInFeishu = ua.includes('feishu') || ua.includes('lark') || window.tt !== undefined
+
+ if (isInFeishu) {
+ // 飞书内:静默授权
+ setEnvironment('feishu')
+ handleFeishuAutoLogin()
+ } else {
+ // 飞书外:显示登录选项
+ setEnvironment('external')
+ setLoading(false)
+ }
+ }, [])
+
+ // 飞书内静默授权
+ const handleFeishuAutoLogin = () => {
+ const appId = 'cli_a8033d607cba500d'
+ const maxWaitTime = 5000 // 最多等待 5 秒
+ const checkInterval = 100 // 每 100ms 检查一次
+ let elapsedTime = 0
+
+ // 等待 tt API 加载完成
+ const waitForTT = () => {
+ return new Promise((resolve, reject) => {
+ const checkTT = () => {
+ elapsedTime += checkInterval
+
+ if (window.tt && typeof window.tt.requestAuthCode === 'function') {
+ resolve()
+ } else if (elapsedTime >= maxWaitTime) {
+ reject(new Error('tt API 加载超时'))
+ } else {
+ setTimeout(checkTT, checkInterval)
+ }
+ }
+ checkTT()
+ })
+ }
+
+ // 执行飞书免登
+ const doFeishuLogin = () => {
+ if (!window.tt) {
+ throw new Error('tt API 未加载')
+ }
+
+ // 调用 tt.ready 确保 SDK 准备就绪
+ window.tt.ready(() => {
+ // 调用 requestAuthCode 获取授权码
+ window.tt.requestAuthCode({
+ appId: appId,
+ success: (res) => {
+ // 用授权码登录后端
+ handleFeishuLogin(res.code)
+ },
+ fail: (error: any) => {
+ console.error('requestAuthCode failed:', error)
+ setLoading(false)
+ Toast.show('获取授权失败')
+ },
+ })
+ })
+ }
+
+ // 开始执行
+ waitForTT()
+ .then(() => doFeishuLogin())
+ .catch((error) => {
+ console.error('Feishu auto login failed:', error)
+ setLoading(false)
+ Toast.show('飞书免登失败')
+ })
+ }
+
+ // 处理飞书登录
+ const handleFeishuLogin = async (authCode: string) => {
+ try {
+ const response = await fetch('/api/v1/auth/feishu-login', {
+ method: 'POST',
+ headers: { 'Content-Type' => 'application/json' },
+ body: JSON.stringify({
+ auth_code: authCode,
+ tenant_key: 'your_tenant_key',
+ }),
+ })
+
+ const result = await response.json()
+
+ if (result.code === 0) {
+ login(result.data.user, result.data.token)
+ Toast.show('登录成功')
+ navigate('/dashboard')
+ } else {
+ Toast.show(result.message || '登录失败')
+ setLoading(false)
+ }
+ } catch (error) {
+ console.error('登录失败:', error)
+ Toast.show('登录失败,请重试')
+ setLoading(false)
+ }
+ }
+
+ if (loading) {
+ return 正在登录...
+ }
+
+ return (
+
+
企业工具台
+ {environment === 'external' && (
+
💡 提示:在飞书客户端内打开可自动登录
+ )}
+ {/* 显示其他登录方式... */}
+
+ )
+}
+```
+
+### 3.3 飞书免登流程
+
+```
+┌────────────────────────────────────────────────────────────┐
+│ 用户在飞书客户端内打开应用 │
+└───────────────────────────┬────────────────────────────────┘
+ │
+ ▼
+┌────────────────────────────────────────────────────────────┐
+│ 前端检测环境 (navigator.userAgent 或 window.tt) │
+│ - 检测到飞书环境 → 执行免登流程 │
+│ - 未检测到 → 显示登录选项(扫码登录等) │
+└───────────────────────────┬────────────────────────────────┘
+ │
+ ▼
+┌────────────────────────────────────────────────────────────┐
+│ 等待飞书 JSAPI SDK 加载完成 │
+│ - 检测 window.tt.requestAuthCode 是否可用 │
+│ - 超时时间:5 秒 │
+└───────────────────────────┬────────────────────────────────┘
+ │
+ ▼
+┌────────────────────────────────────────────────────────────┐
+│ 调用 tt.ready() 确保 SDK 准备就绪 │
+└───────────────────────────┬────────────────────────────────┘
+ │
+ ▼
+┌────────────────────────────────────────────────────────────┐
+│ 调用 tt.requestAuthCode({ appId, success, fail }) │
+│ - appId: 飞书应用 ID │
+│ - success: 回调函数,返回 { code: "xxx" } │
+│ - fail: 错误回调 │
+└───────────────────────────┬────────────────────────────────┘
+ │
+ ▼
+┌────────────────────────────────────────────────────────────┐
+│ 获取 auth_code(临时授权码) │
+│ - 有效期:5 分钟 │
+│ - 只能使用一次 │
+└───────────────────────────┬────────────────────────────────┘
+ │
+ ▼
+┌────────────────────────────────────────────────────────────┐
+│ 前端调用后端接口 POST /api/v1/auth/feishu-login │
+│ { auth_code: "xxx", tenant_key: "xxx" } │
+└───────────────────────────┬────────────────────────────────┘
+ │
+ ▼
+┌────────────────────────────────────────────────────────────┐
+│ 后端处理流程 │
+│ 1. 用 auth_code 换取 user_access_token │
+│ 2. 获取用户信息 │
+│ 3. 创建/更新用户记录 │
+│ 4. 生成 JWT token │
+│ 5. 返回 { token, user } │
+└───────────────────────────┬────────────────────────────────┘
+ │
+ ▼
+┌────────────────────────────────────────────────────────────┐
+│ 前端保存 token 并跳转到首页 │
+│ - localStorage.setItem('token', token) │
+│ - navigate('/dashboard') │
+└────────────────────────────────────────────────────────────┘
+```
+
+### 3.4 关键要点
+
+| 要点 | 说明 |
+|------|------|
+| **SDK 引入** | 必须引入飞书 JSAPI SDK |
+| **环境检测** | 通过 `navigator.userAgent` 或 `window.tt` 检测 |
+| **tt.ready** | 调用前必须确保 SDK 准备就绪 |
+| **auth_code** | 临时授权码,5 分钟有效期,只能用一次 |
+| **错误处理** | 需要处理 SDK 加载超时、授权失败等情况 |
+
+---
+
+## 四、前端实现(扫码登录)
+
+### 4.1 引入飞书 QR SDK
+
+在 `index.html` 中添加(**注意**:需与 JSAPI SDK 一起引入):
+
+```html
+
+
+
+
+```
+
+### 4.2 类型声明
+
+```typescript
+// src/types/feishu.d.ts
+
+declare global {
+ interface Window {
+ QRLogin?: (config: {
+ id: string
+ goto: string
+ width?: string
+ height?: string
+ style?: string
+ }) => {
+ matchOrigin: (origin: string) => boolean
+ matchData: (data: any) => boolean
+ }
+ }
+}
+
+export {}
+```
+
+### 3.3 React 组件实现
+
+```typescript
+// src/pages/login/QRCodeLogin.tsx
+
+import { useState, useEffect, useRef } from 'react'
+import { Dialog, Toast } from 'antd-mobile'
+import { useNavigate } from 'react-router-dom'
+import styles from './QRCodeLogin.module.css'
+
+interface QRCodeLoginProps {
+ visible: boolean
+ onClose: () => void
+}
+
+export default function QRCodeLogin({ visible, onClose }: QRCodeLoginProps) {
+ const navigate = useNavigate()
+ const [status, setStatus] = useState<'loading' | 'pending' | 'scanning' | 'success'>('loading')
+ const [qrcodeUrl, setQrcodeUrl] = useState('')
+ const qrContainerRef = useRef(null)
+ const qrObjRef = useRef(null)
+
+ useEffect(() => {
+ if (visible) {
+ initQRCode()
+ }
+
+ return () => {
+ // 清理
+ if (qrObjRef.current?._cleanup) {
+ qrObjRef.current._cleanup()
+ }
+ if (qrContainerRef.current) {
+ qrContainerRef.current.innerHTML = ''
+ }
+ qrObjRef.current = null
+ }
+ }, [visible])
+
+ const initQRCode = async () => {
+ setStatus('loading')
+ setQrcodeUrl('')
+
+ try {
+ // 等待 SDK 加载
+ await new Promise(resolve => setTimeout(resolve, 100))
+
+ // 生成登录会话
+ const response = await fetch('/api/v1/auth/qrcode-login', {
+ method: 'POST',
+ headers: { 'Content-Type' => 'application/json' },
+ body: JSON.stringify({
+ tenant_key: 'your_tenant_key', // 替换为实际的 tenant_key
+ }),
+ })
+
+ const result = await response.json()
+
+ if (result.code === 0) {
+ const authUrl = result.data.auth_url
+
+ if (window.QRLogin) {
+ setStatus('pending')
+
+ // 等待 DOM 渲染
+ const initQRSDK = (retryCount = 0) => {
+ const container = document.getElementById('feishu-qrcode-container')
+
+ if (container) {
+ container.innerHTML = ''
+
+ try {
+ // 初始化 QR SDK
+ qrObjRef.current = window.QRLogin({
+ id: 'feishu-qrcode-container',
+ goto: authUrl,
+ width: '250',
+ height: '250',
+ style: 'width:300px;height:270px;border:none'
+ })
+
+ setStatus('pending')
+
+ // ⭐ 关键:监听扫码后的 message 事件
+ const handleMessage = (event: MessageEvent) => {
+ // 安全校验
+ if (!qrObjRef.current?.matchOrigin(event.origin)) return
+ if (!qrObjRef.current?.matchData(event.data)) return
+
+ // 获取临时授权码
+ const loginTmpCode = event.data?.tmp_code
+ if (loginTmpCode) {
+ console.log('✅ Got tmp_code:', loginTmpCode)
+
+ // ⭐ 重定向到飞书授权页面(官方推荐方式)
+ const authUrlWithCode = `${authUrl}&tmp_code=${loginTmpCode}`
+ window.location.href = authUrlWithCode
+ }
+ }
+
+ window.addEventListener('message', handleMessage)
+
+ // 保存清理函数
+ qrObjRef.current._cleanup = () => {
+ window.removeEventListener('message', handleMessage)
+ }
+
+ } catch (e) {
+ console.error('❌ QR SDK init error:', e)
+ Toast.show('二维码生成失败')
+ }
+ } else if (retryCount < 10) {
+ setTimeout(() => initQRSDK(retryCount + 1), 100)
+ }
+ }
+
+ setTimeout(() => initQRSDK(), 100)
+ }
+ } else {
+ Toast.show(result.message || '生成二维码失败')
+ }
+ } catch (error) {
+ console.error('生成二维码失败:', error)
+ Toast.show('生成二维码失败')
+ }
+ }
+
+ const getStatusText = () => {
+ switch (status) {
+ case 'loading': return '正在生成二维码...'
+ case 'pending': return '请使用飞书扫码登录'
+ case 'scanning': return '已扫码,请在手机上确认'
+ case 'success': return '登录成功!'
+ default: return ''
+ }
+ }
+
+ return (
+