feat: 飞书登录支持多租户和用户工号管理
后端改动: - FeishuService: 从飞书用户信息获取 tenant_key 和手机号 - FeishuService: 新增 user_sn 表工号分配机制 - FeishuService: 优先使用 enterprise_email,支持手机号 - AuthController: 飞书免登支持 company 参数 前端改动: - FeishuAutoLogin: 支持多租户 company 参数 - login/index: 支持 redirect 参数和已登录检测 - QRCodeLogin: 支持多租户参数传递 - router: 新增路由守卫和参数处理 - api.ts: 优化飞书登录 API 调用 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -64,6 +64,7 @@ class AuthController extends BaseController
|
||||
$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('缺少授权码');
|
||||
@@ -71,9 +72,38 @@ class AuthController extends BaseController
|
||||
|
||||
try {
|
||||
$feishuService = new FeishuService();
|
||||
$companyId = null;
|
||||
$feishuApp = null;
|
||||
|
||||
// 方式1: 根据 app_id 查找飞书应用配置(推荐)
|
||||
if ($appId) {
|
||||
// 方式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 查找飞书应用
|
||||
@@ -90,7 +120,7 @@ class AuthController extends BaseController
|
||||
|
||||
$feishuService->setCompany($companyId);
|
||||
}
|
||||
// 方式2: 根据 tenant_key 查找(兼容旧版)
|
||||
// 方式3: 根据 tenant_key 查找(兼容旧版)
|
||||
elseif ($tenantKey) {
|
||||
error_log("🔍 使用 tenant_key 查找配置: {$tenantKey}");
|
||||
|
||||
@@ -117,7 +147,7 @@ class AuthController extends BaseController
|
||||
// 设置应用ID
|
||||
$appId = $feishuApp->id;
|
||||
} else {
|
||||
return $this->error('缺少 app_id 或 tenant_key 参数');
|
||||
return $this->error('缺少 company、app_id 或 tenant_key 参数');
|
||||
}
|
||||
|
||||
// 获取用户信息
|
||||
@@ -174,16 +204,42 @@ class AuthController extends BaseController
|
||||
{
|
||||
$tenantKey = $request->post('tenant_key');
|
||||
$appId = $request->post('app_id');
|
||||
$companyParam = $request->post('company'); // 新增:company 参数
|
||||
|
||||
error_log("📱 qrcodeLogin - tenant_key: {$tenantKey}, app_id: {$appId}");
|
||||
error_log("📱 qrcodeLogin - tenant_key: {$tenantKey}, app_id: {$appId}, company: {$companyParam}");
|
||||
|
||||
try {
|
||||
$feishuService = new \app\service\FeishuService();
|
||||
$companyId = null;
|
||||
$feishuApp = null;
|
||||
|
||||
// 方式1: 根据 app_id 查找飞书应用配置(推荐)
|
||||
if ($appId) {
|
||||
// 方式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();
|
||||
@@ -195,7 +251,7 @@ class AuthController extends BaseController
|
||||
$companyId = $feishuApp->company_id;
|
||||
error_log("✅ 通过 app_id 找到配置: company_id={$companyId}");
|
||||
}
|
||||
// 方式2: 根据 tenant_key 查找(兼容旧版)
|
||||
// 方式3: 根据 tenant_key 查找(兼容旧版)
|
||||
elseif ($tenantKey) {
|
||||
$company = $feishuService->getCompanyByTenantKey($tenantKey);
|
||||
if (!$company) {
|
||||
@@ -216,7 +272,7 @@ class AuthController extends BaseController
|
||||
|
||||
error_log("✅ 通过 tenant_key 找到配置: company_id={$companyId}");
|
||||
} else {
|
||||
return $this->error('缺少 app_id 或 tenant_key 参数');
|
||||
return $this->error('缺少 company、app_id 或 tenant_key 参数');
|
||||
}
|
||||
|
||||
$feishuService->setCompany($companyId);
|
||||
|
||||
@@ -134,9 +134,18 @@ class FeishuService
|
||||
$unionId = $feishuData['union_id'] ?? null;
|
||||
$feishuUserId = $feishuData['user_id'] ?? null;
|
||||
$openId = $feishuData['open_id'] ?? null;
|
||||
$tenantKey = $feishuData['tenant_key'] ?? null; // 从飞书用户信息获取 tenant_key
|
||||
$name = $feishuData['name'] ?? '未知';
|
||||
$avatar = $feishuData['avatar_url'] ?? null;
|
||||
$email = $feishuData['email'] ?? null;
|
||||
|
||||
// 邮箱:优先使用 enterprise_email,否则使用 email
|
||||
$email = $feishuData['enterprise_email'] ?? $feishuData['email'] ?? null;
|
||||
// 手机号:飞书返回的是 mobile 字段
|
||||
$phone = $feishuData['mobile'] ?? null;
|
||||
// 去掉手机号中的 +86 前缀
|
||||
if ($phone && str_starts_with($phone, '+86')) {
|
||||
$phone = substr($phone, 3);
|
||||
}
|
||||
|
||||
if (!$unionId) {
|
||||
throw new Exception('缺少 union_id');
|
||||
@@ -160,20 +169,25 @@ class FeishuService
|
||||
if (!$user) {
|
||||
error_log("✨ 创建新用户: company={$companyId}, union_id={$unionId}, name={$name}");
|
||||
|
||||
// 初始密码使用 sn(工号),如果没有 sn 则使用 union_id 的前6位
|
||||
$initialPassword = $feishuData['employee_no'] ?? substr($unionId, 0, 6);
|
||||
// 从 user_sn 表中随机获取一个未分配的 sn
|
||||
$userSn = $this->allocateUserSn();
|
||||
|
||||
$user = User::create([
|
||||
'company_id' => $companyId,
|
||||
'sn' => $userSn,
|
||||
'union_id' => $unionId,
|
||||
'name' => $name,
|
||||
'email' => $email,
|
||||
'phone' => $phone,
|
||||
'avatar' => $avatar,
|
||||
'password' => password_hash($initialPassword, PASSWORD_DEFAULT), // 初始密码加密
|
||||
'password' => password_hash($userSn, PASSWORD_DEFAULT), // 使用 sn 作为初始密码
|
||||
'feishu_app_id' => $appId, // 记录登录来源应用
|
||||
'status' => 1, // 新用户默认在职
|
||||
]);
|
||||
error_log("🔑 初始密码设置为 sn: {$initialPassword}");
|
||||
error_log("🔑 用户 sn: {$userSn}, 初始密码: {$userSn}");
|
||||
|
||||
// 更新 user_sn 表,标记 sn 已分配给新用户
|
||||
$this->markUserSnAllocated($userSn, $user->id);
|
||||
} else {
|
||||
error_log("🔄 更新用户: id={$user->id}, company={$companyId}, union_id={$unionId}, 当前状态={$user->status}");
|
||||
|
||||
@@ -188,16 +202,47 @@ class FeishuService
|
||||
'name' => $name,
|
||||
'avatar' => $avatar,
|
||||
'email' => $email,
|
||||
'phone' => $phone,
|
||||
'feishu_app_id' => $appId,
|
||||
]);
|
||||
}
|
||||
|
||||
// ⭐ 保存或更新用户飞书应用绑定关系
|
||||
$this->saveUserFeishuBinding($user->id, $companyId, $appId, $feishuUserId, $openId, $app->tenant_key);
|
||||
$this->saveUserFeishuBinding($user->id, $companyId, $appId, $feishuUserId, $openId, $tenantKey);
|
||||
|
||||
return $user;
|
||||
}
|
||||
|
||||
/**
|
||||
* 从 user_sn 表中随机获取一个未分配的 sn
|
||||
*/
|
||||
private function allocateUserSn(): string
|
||||
{
|
||||
// 随机获取一个未分配的 sn(user_id = 0)
|
||||
$snRecord = \support\Db::table('user_sn')
|
||||
->where('user_id', 0)
|
||||
->inRandomOrder()
|
||||
->first();
|
||||
|
||||
if (!$snRecord) {
|
||||
throw new Exception('没有可用的用户编号,请联系管理员');
|
||||
}
|
||||
|
||||
return (string) $snRecord->user_sn;
|
||||
}
|
||||
|
||||
/**
|
||||
* 标记 user_sn 已分配给指定用户
|
||||
*/
|
||||
private function markUserSnAllocated(string $userSn, int $userId): void
|
||||
{
|
||||
\support\Db::table('user_sn')
|
||||
->where('user_sn', $userSn)
|
||||
->update(['user_id' => $userId]);
|
||||
|
||||
error_log("✅ 用户编号 {$userSn} 已分配给用户 {$userId}");
|
||||
}
|
||||
|
||||
/**
|
||||
* 保存或更新用户飞书应用绑定关系
|
||||
*/
|
||||
|
||||
@@ -14,12 +14,15 @@ Route::group('/api/v1', function () {
|
||||
Route::get('/auth/qrcode-login/status', [app\controller\AuthController::class, 'qrcodeLoginStatus']);
|
||||
|
||||
// 测试接口
|
||||
Route::get('/test/notification', [app\controller\TestController::class, 'testNotification']);
|
||||
Route::get('/test/notification', [app\controller\TestController::class, 'notification']);
|
||||
})->middleware($apiMiddleware);
|
||||
|
||||
// 飞书登录回调(不在 /api/v1 下,供飞书重定向使用)
|
||||
Route::get('/login/callback', [app\controller\AuthController::class, 'qrcodeLoginCallback'])->middleware($apiMiddleware);
|
||||
|
||||
// 飞书卡片回调(不需要认证,飞书服务器直接调用)
|
||||
Route::post('/feishu/card/callback', [app\controller\FeishuCardController::class, 'callback']);
|
||||
|
||||
// 用户路由(需要认证)
|
||||
Route::group('/api/v1/user', function () {
|
||||
Route::get('/info', [app\controller\UserController::class, 'info']);
|
||||
|
||||
Reference in New Issue
Block a user