Files
x-server-mobile/backend/app/service/FeishuService.php
T
root b1ca2c58b7 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>
2026-02-27 17:57:49 +08:00

279 lines
8.4 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?php
namespace app\service;
use app\model\Company;
use app\model\CompanyFeishuApp;
use app\model\User;
use app\util\FeishuUtil;
use app\util\HttpClient;
use Exception;
class FeishuService
{
private ?int $companyId = null;
private ?int $appId = null;
/**
* 设置当前操作的公司
*/
public function setCompany(int $companyId): self
{
$this->companyId = $companyId;
return $this;
}
/**
* 设置飞书应用ID(支持多应用)
*/
public function setApp(int $appId): self
{
$this->appId = $appId;
return $this;
}
/**
* 根据 tenant_key 获取公司配置
*/
public function getCompanyByTenantKey(string $tenantKey): ?Company
{
return Company::active()->byTenantKey($tenantKey)->first();
}
/**
* 获取公司的飞书应用配置
*/
private function getFeishuApp(int $appId): array
{
$app = CompanyFeishuApp::find($appId);
if (!$app || $app->status !== 1) {
throw new Exception('飞书应用配置不存在');
}
return $app->toArray();
}
/**
* 获取公司的默认飞书应用
*/
private function getDefaultFeishuApp(int $companyId): array
{
$app = CompanyFeishuApp::where('company_id', $companyId)
->where('system_id', 1)
->where('status', 1)
->orderBy('sort')
->first();
if (!$app) {
throw new Exception('未配置飞书应用');
}
return $app->toArray();
}
/**
* 获取飞书应用配置(优先使用指定应用,否则使用默认应用)
*/
private function getAppConfig(): array
{
if (!$this->companyId) {
throw new Exception('未设置公司ID');
}
if ($this->appId) {
return $this->getFeishuApp($this->appId);
}
return $this->getDefaultFeishuApp($this->companyId);
}
/**
* 获取当前应用ID
*/
private function getAppId(): int
{
$app = $this->getAppConfig();
return $app['id'];
}
/**
* 获取飞书 access_token(保留用于向后兼容)
* @deprecated 建议直接使用 FeishuUtil::getAppAccessToken()
*/
public function getAccessToken(): string
{
return FeishuUtil::getAppAccessToken($this->getAppId());
}
/**
* 根据 auth_code 获取用户信息(扫码登录)
*/
public function getUserInfo(string $authCode): array
{
$app = $this->getAppConfig();
$appId = $app['id'];
error_log("🔑 使用应用ID: {$appId}, app_id: {$app['app_id']}");
// 使用 FeishuUtil 获取完整用户信息
return FeishuUtil::getCompleteUserInfo($authCode, $appId);
}
/**
* 创建或更新用户
* 注意:同一用户在不同企业的账号是独立的,通过 company_id + union_id 组合查找
*
* @param array $feishuData 飞书用户数据
* @param int $companyId 公司ID
* @param int $appId 飞书应用配置ID
* @return User
*/
public function createOrUpdateUser(array $feishuData, int $companyId, int $appId): User
{
$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;
// 邮箱:优先使用 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');
}
// 获取飞书应用配置
$app = CompanyFeishuApp::where('company_id', $companyId)
->where('id', $appId)
->where('status', 1)
->first();
if (!$app) {
throw new Exception('飞书应用配置不存在或未启用');
}
// ⭐ 关键:通过 company_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}");
// 从 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($userSn, PASSWORD_DEFAULT), // 使用 sn 作为初始密码
'feishu_app_id' => $appId, // 记录登录来源应用
'status' => 1, // 新用户默认在职
]);
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}");
// 离职员工禁止登录
if ($user->status == 0) {
error_log("❌ 离职员工尝试登录: id={$user->id}, status=0");
throw new Exception('该员工已离职,无法登录系统');
}
// 更新用户信息和登录来源应用
$user->update([
'name' => $name,
'avatar' => $avatar,
'email' => $email,
'phone' => $phone,
'feishu_app_id' => $appId,
]);
}
// ⭐ 保存或更新用户飞书应用绑定关系
$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}");
}
/**
* 保存或更新用户飞书应用绑定关系
*/
private function saveUserFeishuBinding(int $userId, int $companyId, int $appId, ?string $feishuUserId, ?string $openId, ?string $tenantKey): void
{
$binding = \app\model\UserFeishuApp::where('user_id', $userId)
->where('company_id', $companyId)
->where('feishu_app_id', $appId)
->first();
$data = [
'feishu_user_id' => $feishuUserId,
'feishu_open_id' => $openId,
'tenant_key' => $tenantKey,
'last_login_at' => date('Y-m-d H:i:s'),
];
if ($binding) {
// 更新现有绑定
$binding->update($data);
error_log("🔄 更新飞书绑定: user_id={$userId}, app_id={$appId}, feishu_user_id={$feishuUserId}");
} else {
// 创建新绑定
\app\model\UserFeishuApp::create([
'user_id' => $userId,
'company_id' => $companyId,
'feishu_app_id' => $appId,
'is_default' => 0, // 后续可由用户或管理员设置
] + $data);
error_log("✨ 创建飞书绑定: user_id={$userId}, app_id={$appId}, feishu_user_id={$feishuUserId}");
}
}
}