refactor: 重构多飞书应用绑定架构和数据库清理
- 新增 user_feishu_apps 表支持用户绑定多个飞书应用 - users 表添加唯一约束(公司内邮箱/手机唯一) - 迁移现有飞书数据到 user_feishu_apps 表 - 清理冗余表(users_bak, user_feishu_binding) - 废弃 users 表中的 feishu_user_id/feishu_open_id/tenant_key 字段 - 更新 FeishuNotificationService 使用新的数据获取方式 - User 模型新增 getFeishuUserId() 等便捷方法 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -86,7 +86,7 @@ class AuthController extends BaseController
|
||||
}
|
||||
|
||||
$companyId = $feishuApp->company_id;
|
||||
error_log("✅ 找到飞书应用配置: company_id={$companyId}");
|
||||
error_log("✅ 找到飞书应用配置: company_id={$companyId}, feishu_app_id={$feishuApp->id}");
|
||||
|
||||
$feishuService->setCompany($companyId);
|
||||
}
|
||||
@@ -111,8 +111,11 @@ class AuthController extends BaseController
|
||||
return $this->error('未配置飞书应用');
|
||||
}
|
||||
|
||||
error_log("✅ 找到公司配置: company_id={$companyId}");
|
||||
error_log("✅ 找到公司配置: company_id={$companyId}, feishu_app_id={$feishuApp->id}");
|
||||
$feishuService->setCompany($companyId);
|
||||
|
||||
// 设置应用ID
|
||||
$appId = $feishuApp->id;
|
||||
} else {
|
||||
return $this->error('缺少 app_id 或 tenant_key 参数');
|
||||
}
|
||||
@@ -121,15 +124,15 @@ class AuthController extends BaseController
|
||||
$feishuData = $feishuService->getUserInfo($authCode);
|
||||
error_log("👤 获取到用户信息: " . json_encode($feishuData));
|
||||
|
||||
// 创建或更新用户
|
||||
$user = $feishuService->createOrUpdateUser($feishuData, $companyId);
|
||||
// 创建或更新用户(传递应用ID)
|
||||
$user = $feishuService->createOrUpdateUser($feishuData, $companyId, $appId);
|
||||
|
||||
// 生成 JWT Token
|
||||
$token = JwtUtil::encode([
|
||||
'user_id' => $user->id,
|
||||
'company_id' => $user->company_id,
|
||||
'feishu_user_id' => $user->feishu_user_id,
|
||||
'union_id' => $user->feishu_union_id,
|
||||
'union_id' => $user->union_id,
|
||||
]);
|
||||
|
||||
error_log("✅ 登录成功: user_id={$user->id}, company_id={$user->company_id}");
|
||||
@@ -222,6 +225,7 @@ class AuthController extends BaseController
|
||||
$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分钟过期
|
||||
]);
|
||||
@@ -279,7 +283,7 @@ class AuthController extends BaseController
|
||||
'user_id' => $user->id,
|
||||
'company_id' => $user->company_id,
|
||||
'feishu_user_id' => $user->feishu_user_id,
|
||||
'union_id' => $user->feishu_union_id,
|
||||
'union_id' => $user->union_id,
|
||||
]);
|
||||
|
||||
// 清除会话
|
||||
@@ -346,7 +350,7 @@ class AuthController extends BaseController
|
||||
return $this->error('二维码已过期');
|
||||
}
|
||||
|
||||
error_log("🔄 Processing login for company_id: {$session->company_id}");
|
||||
error_log("🔄 Processing login for company_id: {$session->company_id}, feishu_app_id: {$session->feishu_app_id}");
|
||||
|
||||
// 使用 session 的公司信息
|
||||
$feishuService = new \app\service\FeishuService();
|
||||
@@ -356,8 +360,8 @@ class AuthController extends BaseController
|
||||
$feishuData = $feishuService->getUserInfo($authCode);
|
||||
error_log("👤 Got user info: " . json_encode($feishuData));
|
||||
|
||||
// 创建或更新用户
|
||||
$user = $feishuService->createOrUpdateUser($feishuData, $session->company_id);
|
||||
// 创建或更新用户(传递飞书应用ID)
|
||||
$user = $feishuService->createOrUpdateUser($feishuData, $session->company_id, $session->feishu_app_id);
|
||||
error_log("✅ User created/updated: user_id={$user->id}");
|
||||
|
||||
// 更新会话状态
|
||||
@@ -371,7 +375,7 @@ class AuthController extends BaseController
|
||||
'user_id' => $user->id,
|
||||
'company_id' => $user->company_id,
|
||||
'feishu_user_id' => $user->feishu_user_id,
|
||||
'union_id' => $user->feishu_union_id,
|
||||
'union_id' => $user->union_id,
|
||||
]);
|
||||
error_log("🎫 Generated token for user: {$user->id}");
|
||||
|
||||
|
||||
@@ -18,13 +18,18 @@ class TestController extends BaseController
|
||||
try {
|
||||
$notification = new FeishuNotificationService();
|
||||
|
||||
// 获取用户的company_id
|
||||
// 获取用户的company_id和feishu_app_id
|
||||
$user = \app\model\User::find($userId);
|
||||
if (!$user) {
|
||||
return $this->error('用户不存在');
|
||||
}
|
||||
|
||||
if (!$user->feishu_app_id) {
|
||||
return $this->error('用户未绑定飞书应用');
|
||||
}
|
||||
|
||||
$notification->setCompany($user->company_id);
|
||||
$notification->setApp($user->feishu_app_id);
|
||||
|
||||
// 发送测试消息
|
||||
$result = $notification->sendApprovalNotification([
|
||||
@@ -37,11 +42,15 @@ class TestController extends BaseController
|
||||
]);
|
||||
|
||||
if ($result) {
|
||||
// 获取用户在该应用下的feishu_user_id
|
||||
$feishuUserId = $user->getFeishuUserId($user->feishu_app_id);
|
||||
|
||||
return $this->success([
|
||||
'message' => '测试消息发送成功',
|
||||
'user_id' => $userId,
|
||||
'user_name' => $user->name,
|
||||
'feishu_user_id' => $user->feishu_user_id,
|
||||
'feishu_user_id' => $feishuUserId,
|
||||
'feishu_app_id' => $user->feishu_app_id,
|
||||
]);
|
||||
} else {
|
||||
return $this->error('测试消息发送失败,请查看日志');
|
||||
|
||||
@@ -11,6 +11,7 @@ class FeishuLoginSession extends Model
|
||||
protected $fillable = [
|
||||
'company_id',
|
||||
'tenant_key',
|
||||
'feishu_app_id',
|
||||
'status',
|
||||
'user_id',
|
||||
'expires_at',
|
||||
|
||||
@@ -8,6 +8,14 @@ class User extends Model
|
||||
{
|
||||
protected $table = 'users';
|
||||
|
||||
/**
|
||||
* 可填充字段
|
||||
*
|
||||
* 注意:以下字段已废弃,保留仅用于向后兼容
|
||||
* @deprecated feishu_user_id - 请使用 UserFeishuApp 模型获取
|
||||
* @deprecated feishu_open_id - 请使用 UserFeishuApp 模型获取
|
||||
* @deprecated tenant_key - 请使用 UserFeishuApp 模型获取
|
||||
*/
|
||||
protected $fillable = [
|
||||
'company_id',
|
||||
'sn',
|
||||
@@ -21,9 +29,12 @@ class User extends Model
|
||||
'status',
|
||||
'user_type',
|
||||
'union_id',
|
||||
'feishu_user_id',
|
||||
'feishu_open_id',
|
||||
'tenant_key',
|
||||
// 废弃字段 - 请使用 UserFeishuApp 模型
|
||||
'feishu_user_id', // @deprecated
|
||||
'feishu_open_id', // @deprecated
|
||||
'tenant_key', // @deprecated
|
||||
// 保留字段:记录最后登录的应用,用于快速获取默认应用
|
||||
'feishu_app_id',
|
||||
'position',
|
||||
'sign_img',
|
||||
];
|
||||
@@ -66,6 +77,62 @@ class User extends Model
|
||||
return $this->hasMany(User::class, 'report_to');
|
||||
}
|
||||
|
||||
/**
|
||||
* 用户飞书应用绑定
|
||||
*/
|
||||
public function feishuApps()
|
||||
{
|
||||
return $this->hasMany(UserFeishuApp::class, 'user_id');
|
||||
}
|
||||
|
||||
/**
|
||||
* 最后登录的飞书应用
|
||||
*/
|
||||
public function lastFeishuApp()
|
||||
{
|
||||
return $this->belongsTo(CompanyFeishuApp::class, 'feishu_app_id');
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取用户在当前公司的默认飞书应用
|
||||
*/
|
||||
public function getDefaultFeishuApp()
|
||||
{
|
||||
return $this->hasOne(UserFeishuApp::class, 'user_id')
|
||||
->where('company_id', $this->company_id)
|
||||
->where('is_default', 1);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取用户在指定飞书应用下的绑定信息
|
||||
*/
|
||||
public function feishuAppBinding(int $feishuAppId)
|
||||
{
|
||||
return $this->hasOne(UserFeishuApp::class, 'user_id')
|
||||
->where('company_id', $this->company_id)
|
||||
->where('feishu_app_id', $feishuAppId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取用户在指定飞书应用下的 feishu_user_id
|
||||
*/
|
||||
public function getFeishuUserId(int $feishuAppId): ?string
|
||||
{
|
||||
$binding = $this->feishuAppBinding($feishuAppId)->first();
|
||||
return $binding ? $binding->feishu_user_id : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取用户在最后登录应用下的 feishu_user_id
|
||||
*/
|
||||
public function getLastFeishuUserId(): ?string
|
||||
{
|
||||
if (!$this->feishu_app_id) {
|
||||
return null;
|
||||
}
|
||||
return $this->getFeishuUserId($this->feishu_app_id);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取启用的用户
|
||||
*/
|
||||
@@ -84,6 +151,7 @@ class User extends Model
|
||||
|
||||
/**
|
||||
* 根据飞书用户ID查找
|
||||
* @deprecated 请通过 UserFeishuApp 模型查询
|
||||
*/
|
||||
public function scopeByFeishuUserId($query, ?string $feishuUserId)
|
||||
{
|
||||
@@ -95,6 +163,7 @@ class User extends Model
|
||||
|
||||
/**
|
||||
* 根据飞书 UnionID 查找
|
||||
* union_id 用于跨应用识别用户,保留此方法
|
||||
*/
|
||||
public function scopeByFeishuUnionId($query, ?string $feishuUnionId)
|
||||
{
|
||||
@@ -106,6 +175,7 @@ class User extends Model
|
||||
|
||||
/**
|
||||
* 根据OpenID查找
|
||||
* @deprecated 请通过 UserFeishuApp 模型查询
|
||||
*/
|
||||
public function scopeByFeishuOpenId($query, ?string $openId)
|
||||
{
|
||||
|
||||
@@ -0,0 +1,78 @@
|
||||
<?php
|
||||
|
||||
namespace app\model;
|
||||
|
||||
use support\Model;
|
||||
|
||||
/**
|
||||
* 用户飞书应用绑定模型
|
||||
*/
|
||||
class UserFeishuApp extends Model
|
||||
{
|
||||
protected $table = 'user_feishu_apps';
|
||||
|
||||
protected $fillable = [
|
||||
'user_id',
|
||||
'company_id',
|
||||
'feishu_app_id',
|
||||
'feishu_user_id',
|
||||
'feishu_open_id',
|
||||
'tenant_key',
|
||||
'is_default',
|
||||
'last_login_at',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'user_id' => 'integer',
|
||||
'company_id' => 'integer',
|
||||
'feishu_app_id' => 'integer',
|
||||
'is_default' => 'boolean',
|
||||
'last_login_at' => 'datetime',
|
||||
'created_at' => 'datetime',
|
||||
'updated_at' => 'datetime',
|
||||
];
|
||||
|
||||
/**
|
||||
* 关联用户
|
||||
*/
|
||||
public function user()
|
||||
{
|
||||
return $this->belongsTo(User::class, 'user_id');
|
||||
}
|
||||
|
||||
/**
|
||||
* 关联公司
|
||||
*/
|
||||
public function company()
|
||||
{
|
||||
return $this->belongsTo(Company::class, 'company_id');
|
||||
}
|
||||
|
||||
/**
|
||||
* 关联飞书应用配置
|
||||
*/
|
||||
public function feishuApp()
|
||||
{
|
||||
return $this->belongsTo(CompanyFeishuApp::class, 'feishu_app_id');
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取启用的绑定
|
||||
*/
|
||||
public function scopeActive($query)
|
||||
{
|
||||
return $query->whereHas('feishuApp', function ($q) {
|
||||
$q->where('status', 1);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取用户在某公司的默认应用
|
||||
*/
|
||||
public function scopeDefaultApp($query, int $userId, int $companyId)
|
||||
{
|
||||
return $query->where('user_id', $userId)
|
||||
->where('company_id', $companyId)
|
||||
->where('is_default', 1);
|
||||
}
|
||||
}
|
||||
@@ -27,7 +27,7 @@ class FeishuNotificationService
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置飞书应用ID(支持多应用)
|
||||
* 设置飞书应用ID(必须指定)
|
||||
*/
|
||||
public function setApp(int $appId): self
|
||||
{
|
||||
@@ -36,7 +36,29 @@ class FeishuNotificationService
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取飞书应用配置
|
||||
* 根据用户ID自动设置其最后登录的应用
|
||||
*/
|
||||
public function setAppByUser(int $userId): self
|
||||
{
|
||||
$user = \app\model\User::find($userId);
|
||||
if (!$user) {
|
||||
throw new Exception('用户不存在');
|
||||
}
|
||||
|
||||
$this->setCompany($user->company_id);
|
||||
|
||||
// 使用用户最后登录的应用
|
||||
if ($user->feishu_app_id) {
|
||||
$this->appId = $user->feishu_app_id;
|
||||
} else {
|
||||
throw new Exception('用户未绑定飞书应用,请先通过飞书登录');
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取飞书应用配置(必须先通过 setApp 或 setAppByUser 指定)
|
||||
*/
|
||||
private function getAppConfig(): array
|
||||
{
|
||||
@@ -44,22 +66,17 @@ class FeishuNotificationService
|
||||
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();
|
||||
if (!$this->appId) {
|
||||
throw new Exception('未指定飞书应用,请通过 setApp() 或 setAppByUser() 指定');
|
||||
}
|
||||
|
||||
$app = \app\model\CompanyFeishuApp::where('company_id', $this->companyId)
|
||||
->where('system_id', 1)
|
||||
->where('id', $this->appId)
|
||||
->where('status', 1)
|
||||
->orderBy('sort')
|
||||
->first();
|
||||
|
||||
if (!$app) {
|
||||
throw new Exception('未配置飞书应用');
|
||||
throw new Exception('指定的飞书应用不存在或未启用');
|
||||
}
|
||||
|
||||
return $app->toArray();
|
||||
@@ -74,6 +91,23 @@ class FeishuNotificationService
|
||||
return FeishuUtil::getAppAccessToken($app['id']);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取用户在指定飞书应用下的 feishu_user_id
|
||||
*/
|
||||
private function getFeishuUserId(int $userId, int $companyId): ?string
|
||||
{
|
||||
if (!$this->appId) {
|
||||
throw new Exception('未指定飞书应用,请先调用 setApp() 或 setAppByUser()');
|
||||
}
|
||||
|
||||
$binding = \app\model\UserFeishuApp::where('user_id', $userId)
|
||||
->where('company_id', $companyId)
|
||||
->where('feishu_app_id', $this->appId)
|
||||
->first();
|
||||
|
||||
return $binding ? $binding->feishu_user_id : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送审批通知
|
||||
*
|
||||
@@ -98,13 +132,20 @@ class FeishuNotificationService
|
||||
|
||||
try {
|
||||
$user = User::find($userId);
|
||||
if (!$user || !$user->feishu_user_id) {
|
||||
error_log("❌ sendApprovalNotification: 用户不存在或未绑定飞书, user_id={$userId}");
|
||||
if (!$user) {
|
||||
error_log("❌ sendApprovalNotification: 用户不存在, user_id={$userId}");
|
||||
return false;
|
||||
}
|
||||
|
||||
// 从 user_feishu_apps 获取指定应用下的 feishu_user_id
|
||||
$feishuUserId = $this->getFeishuUserId($userId, $user->company_id);
|
||||
if (!$feishuUserId) {
|
||||
error_log("❌ sendApprovalNotification: 用户未绑定飞书应用, user_id={$userId}");
|
||||
return false;
|
||||
}
|
||||
|
||||
$card = $this->buildApprovalCard($params);
|
||||
return $this->sendMessage($user->feishu_user_id, $card);
|
||||
return $this->sendMessage($feishuUserId, $card);
|
||||
} catch (Exception $e) {
|
||||
error_log('❌ sendApprovalNotification 失败: ' . $e->getMessage());
|
||||
return false;
|
||||
@@ -133,13 +174,20 @@ class FeishuNotificationService
|
||||
foreach ($userIds as $userId) {
|
||||
try {
|
||||
$user = User::find($userId);
|
||||
if (!$user || !$user->feishu_user_id) {
|
||||
if (!$user) {
|
||||
$results[$userId] = false;
|
||||
continue;
|
||||
}
|
||||
|
||||
// 从 user_feishu_apps 获取指定应用下的 feishu_user_id
|
||||
$feishuUserId = $this->getFeishuUserId($userId, $user->company_id);
|
||||
if (!$feishuUserId) {
|
||||
$results[$userId] = false;
|
||||
continue;
|
||||
}
|
||||
|
||||
$card = $this->buildSystemCard($params);
|
||||
$results[$userId] = $this->sendMessage($user->feishu_user_id, $card);
|
||||
$results[$userId] = $this->sendMessage($feishuUserId, $card);
|
||||
} catch (Exception $e) {
|
||||
error_log("❌ sendSystemNotification 失败: user_id={$userId}, error=" . $e->getMessage());
|
||||
$results[$userId] = false;
|
||||
@@ -170,13 +218,20 @@ class FeishuNotificationService
|
||||
|
||||
try {
|
||||
$user = User::find($userId);
|
||||
if (!$user || !$user->feishu_user_id) {
|
||||
error_log("❌ sendReminderNotification: 用户不存在或未绑定飞书, user_id={$userId}");
|
||||
if (!$user) {
|
||||
error_log("❌ sendReminderNotification: 用户不存在, user_id={$userId}");
|
||||
return false;
|
||||
}
|
||||
|
||||
// 从 user_feishu_apps 获取指定应用下的 feishu_user_id
|
||||
$feishuUserId = $this->getFeishuUserId($userId, $user->company_id);
|
||||
if (!$feishuUserId) {
|
||||
error_log("❌ sendReminderNotification: 用户未绑定飞书应用, user_id={$userId}");
|
||||
return false;
|
||||
}
|
||||
|
||||
$card = $this->buildReminderCard($params);
|
||||
return $this->sendMessage($user->feishu_user_id, $card);
|
||||
return $this->sendMessage($feishuUserId, $card);
|
||||
} catch (Exception $e) {
|
||||
error_log('❌ sendReminderNotification 失败: ' . $e->getMessage());
|
||||
return false;
|
||||
|
||||
@@ -123,11 +123,17 @@ class FeishuService
|
||||
/**
|
||||
* 创建或更新用户
|
||||
* 注意:同一用户在不同企业的账号是独立的,通过 company_id + union_id 组合查找
|
||||
*
|
||||
* @param array $feishuData 飞书用户数据
|
||||
* @param int $companyId 公司ID
|
||||
* @param int $appId 飞书应用配置ID
|
||||
* @return User
|
||||
*/
|
||||
public function createOrUpdateUser(array $feishuData, int $companyId): User
|
||||
public function createOrUpdateUser(array $feishuData, int $companyId, int $appId): User
|
||||
{
|
||||
$unionId = $feishuData['union_id'] ?? null;
|
||||
$userId = $feishuData['user_id'] ?? null;
|
||||
$feishuUserId = $feishuData['user_id'] ?? null;
|
||||
$openId = $feishuData['open_id'] ?? null;
|
||||
$name = $feishuData['name'] ?? '未知';
|
||||
$avatar = $feishuData['avatar_url'] ?? null;
|
||||
$email = $feishuData['email'] ?? null;
|
||||
@@ -136,8 +142,17 @@ class FeishuService
|
||||
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 组合查找用户
|
||||
// 这样同一个 union_id 在不同公司的账号是独立的
|
||||
$user = User::where('company_id', $companyId)
|
||||
->where('union_id', $unionId)
|
||||
->first();
|
||||
@@ -146,16 +161,16 @@ class FeishuService
|
||||
error_log("✨ 创建新用户: company={$companyId}, union_id={$unionId}, name={$name}");
|
||||
|
||||
// 初始密码使用 sn(工号),如果没有 sn 则使用 union_id 的前6位
|
||||
$initialPassword = $feishuData['sn'] ?? substr($unionId, 0, 6);
|
||||
$initialPassword = $feishuData['employee_no'] ?? substr($unionId, 0, 6);
|
||||
|
||||
$user = User::create([
|
||||
'company_id' => $companyId,
|
||||
'feishu_user_id' => $userId,
|
||||
'union_id' => $unionId,
|
||||
'name' => $name,
|
||||
'email' => $email,
|
||||
'avatar' => $avatar,
|
||||
'password' => password_hash($initialPassword, PASSWORD_DEFAULT), // 初始密码加密
|
||||
'feishu_app_id' => $appId, // 记录登录来源应用
|
||||
'status' => 1, // 新用户默认在职
|
||||
]);
|
||||
error_log("🔑 初始密码设置为 sn: {$initialPassword}");
|
||||
@@ -168,22 +183,57 @@ class FeishuService
|
||||
throw new Exception('该员工已离职,无法登录系统');
|
||||
}
|
||||
|
||||
// 更新用户信息(在职员工)
|
||||
$updateData = [
|
||||
'feishu_user_id' => $userId,
|
||||
// 更新用户信息和登录来源应用
|
||||
$user->update([
|
||||
'name' => $name,
|
||||
'avatar' => $avatar,
|
||||
'email' => $email,
|
||||
];
|
||||
|
||||
$user->update($updateData);
|
||||
'feishu_app_id' => $appId,
|
||||
]);
|
||||
}
|
||||
|
||||
// ⭐ 保存或更新用户飞书应用绑定关系
|
||||
$this->saveUserFeishuBinding($user->id, $companyId, $appId, $feishuUserId, $openId, $app->tenant_key);
|
||||
|
||||
return $user;
|
||||
}
|
||||
|
||||
/**
|
||||
* 保存或更新用户飞书应用绑定关系
|
||||
*/
|
||||
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}");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送审批消息通知
|
||||
* @deprecated 请使用 FeishuNotificationService::sendApprovalNotification() 代替
|
||||
*/
|
||||
public function sendApprovalMessage(int $userId, string $title, string $url): bool
|
||||
{
|
||||
|
||||
@@ -0,0 +1,118 @@
|
||||
-- X-Server 用户多飞书应用绑定功能
|
||||
-- 执行方式:在数据库中直接执行此SQL
|
||||
|
||||
USE xserver;
|
||||
|
||||
-- =====================================
|
||||
-- 1. 添加 users 表唯一约束
|
||||
-- =====================================
|
||||
|
||||
-- 删除可能存在的旧索引
|
||||
ALTER TABLE users DROP INDEX IF EXISTS idx_email;
|
||||
ALTER TABLE users DROP INDEX IF EXISTS idx_phone;
|
||||
|
||||
-- 添加 email 唯一索引(同一公司内邮箱唯一)
|
||||
CREATE UNIQUE INDEX uk_users_company_email ON users(company_id, email);
|
||||
|
||||
-- 添加 phone 唯一索引(同一公司内手机号唯一)
|
||||
CREATE UNIQUE INDEX uk_users_company_phone ON users(company_id, phone);
|
||||
|
||||
-- =====================================
|
||||
-- 2. 创建用户飞书应用绑定表
|
||||
-- =====================================
|
||||
|
||||
DROP TABLE IF EXISTS user_feishu_apps;
|
||||
|
||||
CREATE TABLE `user_feishu_apps` (
|
||||
`id` bigint unsigned NOT NULL AUTO_INCREMENT COMMENT "ID",
|
||||
`user_id` bigint unsigned NOT NULL COMMENT "用户ID",
|
||||
`company_id` bigint unsigned NOT NULL COMMENT "公司ID",
|
||||
`feishu_app_id` int unsigned NOT NULL COMMENT "飞书应用配置ID",
|
||||
|
||||
-- 该应用中的飞书用户信息
|
||||
`feishu_user_id` varchar(60) DEFAULT NULL COMMENT "该应用的飞书用户ID",
|
||||
`feishu_open_id` varchar(60) DEFAULT NULL COMMENT "该应用的飞书OpenID",
|
||||
`tenant_key` varchar(60) DEFAULT NULL COMMENT "该应用的租户Key",
|
||||
|
||||
-- 绑定信息
|
||||
`is_default` tinyint NOT NULL DEFAULT 0 COMMENT "是否为默认应用: 1=是, 0=否",
|
||||
`last_login_at` timestamp NULL DEFAULT NULL COMMENT "最后一次通过此应用登录时间",
|
||||
`created_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT "绑定时间",
|
||||
`updated_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT "更新时间",
|
||||
|
||||
PRIMARY KEY (`id`),
|
||||
UNIQUE KEY `uk_user_app` (`user_id`, `company_id`, `feishu_app_id`),
|
||||
KEY `idx_user_default` (`user_id`, `company_id`, `is_default`),
|
||||
KEY `idx_feishu_user_id` (`feishu_user_id`),
|
||||
KEY `idx_feishu_open_id` (`feishu_open_id`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT="用户飞书应用绑定表";
|
||||
|
||||
-- =====================================
|
||||
-- 3. 修改 users 表,移除不需要的飞书字段
|
||||
-- =====================================
|
||||
|
||||
-- 注意:为了兼容性,先保留现有字段,后续可考虑迁移
|
||||
-- ALTER TABLE users DROP COLUMN feishu_user_id;
|
||||
-- ALTER TABLE users DROP COLUMN feishu_open_id;
|
||||
-- ALTER TABLE users DROP COLUMN tenant_key;
|
||||
|
||||
-- 临时方案:添加 feishu_app_id 记录最后登录的应用
|
||||
ALTER TABLE users
|
||||
ADD COLUMN `feishu_app_id` int unsigned DEFAULT NULL
|
||||
COMMENT '最后登录的飞书应用ID'
|
||||
AFTER `tenant_key`;
|
||||
|
||||
ALTER TABLE users
|
||||
ADD INDEX `idx_feishu_app_id` (`feishu_app_id`);
|
||||
|
||||
-- =====================================
|
||||
-- 4. 迁移现有用户飞书绑定数据(如果有的话)
|
||||
-- =====================================
|
||||
|
||||
-- 将现有 users 表中的飞书信息迁移到绑定表
|
||||
-- 只处理有 feishu_user_id 的用户
|
||||
INSERT INTO user_feishu_apps (
|
||||
user_id,
|
||||
company_id,
|
||||
feishu_app_id,
|
||||
feishu_user_id,
|
||||
feishu_open_id,
|
||||
tenant_key,
|
||||
is_default,
|
||||
last_login_at
|
||||
)
|
||||
SELECT
|
||||
u.id as user_id,
|
||||
u.company_id,
|
||||
u.feishu_app_id as feishu_app_id,
|
||||
u.feishu_user_id,
|
||||
u.feishu_open_id,
|
||||
u.tenant_key,
|
||||
1 as is_default,
|
||||
NOW() as last_login_at
|
||||
FROM users u
|
||||
WHERE u.feishu_user_id IS NOT NULL
|
||||
AND u.feishu_app_id IS NOT NULL
|
||||
ON DUPLICATE KEY UPDATE
|
||||
last_login_at = NOW();
|
||||
|
||||
-- =====================================
|
||||
-- 验证结果
|
||||
-- =====================================
|
||||
|
||||
SELECT "=== 用户表唯一约束 ===" as "";
|
||||
SHOW INDEX FROM users WHERE Key_name LIKE 'uk_users_company%';
|
||||
|
||||
SELECT "=== 飞书应用绑定表结构 ===" as "";
|
||||
DESCRIBE user_feishu_apps;
|
||||
|
||||
SELECT "=== 现有用户飞书绑定数据 ===" as "";
|
||||
SELECT
|
||||
u.id as user_id,
|
||||
u.name,
|
||||
u.company_id,
|
||||
u.feishu_app_id,
|
||||
COUNT(ufa.id) as app_count
|
||||
FROM users u
|
||||
LEFT JOIN user_feishu_apps ufa ON u.id = ufa.user_id
|
||||
GROUP BY u.id, u.name, u.company_id, u.feishu_app_id;
|
||||
@@ -0,0 +1,92 @@
|
||||
-- X-Server 用户表飞书数据迁移
|
||||
-- 将 users 表中的飞书字段数据迁移到 user_feishu_apps 表
|
||||
|
||||
USE xserver;
|
||||
|
||||
-- =====================================
|
||||
-- 1. 迁移现有数据到 user_feishu_apps
|
||||
-- =====================================
|
||||
|
||||
-- 将 users 表中的飞书信息迁移到 user_feishu_apps 表
|
||||
-- 使用每个公司的默认飞书应用 (system_id=1)
|
||||
INSERT INTO user_feishu_apps (
|
||||
user_id,
|
||||
company_id,
|
||||
feishu_app_id,
|
||||
feishu_user_id,
|
||||
feishu_open_id,
|
||||
tenant_key,
|
||||
is_default,
|
||||
last_login_at,
|
||||
created_at,
|
||||
updated_at
|
||||
)
|
||||
SELECT
|
||||
u.id as user_id,
|
||||
u.company_id,
|
||||
cfa.id as feishu_app_id,
|
||||
u.feishu_user_id,
|
||||
u.feishu_open_id,
|
||||
u.tenant_key,
|
||||
1 as is_default,
|
||||
NOW() as last_login_at,
|
||||
u.created_at,
|
||||
u.updated_at
|
||||
FROM users u
|
||||
INNER JOIN company_feishu_apps cfa ON cfa.company_id = u.company_id
|
||||
WHERE u.feishu_user_id IS NOT NULL
|
||||
AND cfa.system_id = 1
|
||||
AND cfa.status = 1
|
||||
ON DUPLICATE KEY UPDATE
|
||||
feishu_user_id = VALUES(feishu_user_id),
|
||||
feishu_open_id = VALUES(feishu_open_id),
|
||||
tenant_key = VALUES(tenant_key),
|
||||
last_login_at = NOW();
|
||||
|
||||
-- =====================================
|
||||
-- 2. 更新 users 表的 feishu_app_id
|
||||
-- =====================================
|
||||
|
||||
UPDATE users u
|
||||
INNER JOIN company_feishu_apps cfa ON cfa.company_id = u.company_id
|
||||
SET u.feishu_app_id = cfa.id
|
||||
WHERE u.feishu_user_id IS NOT NULL
|
||||
AND cfa.system_id = 1
|
||||
AND cfa.status = 1;
|
||||
|
||||
-- =====================================
|
||||
-- 3. 验证迁移结果
|
||||
-- =====================================
|
||||
|
||||
SELECT "=== 迁移前 users 表数据 ===" as "";
|
||||
SELECT
|
||||
company_id,
|
||||
COUNT(*) as total_users,
|
||||
SUM(CASE WHEN feishu_user_id IS NOT NULL THEN 1 ELSE 0 END) as with_feishu_id,
|
||||
SUM(CASE WHEN feishu_app_id IS NOT NULL THEN 1 ELSE 0 END) as with_app_id
|
||||
FROM users
|
||||
GROUP BY company_id;
|
||||
|
||||
SELECT "=== 迁移后 user_feishu_apps 表数据 ===" as "";
|
||||
SELECT
|
||||
company_id,
|
||||
COUNT(DISTINCT user_id) as users_count,
|
||||
COUNT(*) as bindings_count
|
||||
FROM user_feishu_apps
|
||||
GROUP BY company_id;
|
||||
|
||||
-- =====================================
|
||||
-- 4. 标记冗余字段(不删除,保证向后兼容)
|
||||
-- =====================================
|
||||
|
||||
-- 注意:以下字段保留但不推荐使用
|
||||
-- - feishu_user_id (冗余,应从 user_feishu_apps 获取)
|
||||
-- - feishu_open_id (冗余,应从 user_feishu_apps 获取)
|
||||
-- - tenant_key (冗余,应从 user_feishu_apps 获取)
|
||||
--
|
||||
-- 推荐使用方式:
|
||||
-- $binding = UserFeishuApp::where('user_id', $userId)
|
||||
-- ->where('company_id', $companyId)
|
||||
-- ->where('feishu_app_id', $appId)
|
||||
-- ->first();
|
||||
-- $feishuUserId = $binding ? $binding->feishu_user_id : null;
|
||||
@@ -0,0 +1,31 @@
|
||||
-- X-Server 清理冗余表
|
||||
-- 执行前请确认已备份数据库
|
||||
|
||||
USE xserver;
|
||||
|
||||
-- =====================================
|
||||
-- 1. 删除 users_bak 表(用户表旧版备份)
|
||||
-- =====================================
|
||||
|
||||
DROP TABLE IF EXISTS users_bak;
|
||||
|
||||
-- =====================================
|
||||
-- 2. 删除 user_feishu_binding 表(已被 user_feishu_apps 替代)
|
||||
-- =====================================
|
||||
|
||||
DROP TABLE IF EXISTS user_feishu_binding;
|
||||
|
||||
-- =====================================
|
||||
-- 验证结果
|
||||
-- =====================================
|
||||
|
||||
SELECT "=== 当前表列表 ===" as "";
|
||||
SHOW TABLES;
|
||||
|
||||
SELECT "=== user_feishu_apps 统计 ===" as "";
|
||||
SELECT
|
||||
company_id,
|
||||
COUNT(DISTINCT user_id) as users_count,
|
||||
COUNT(*) as bindings_count
|
||||
FROM user_feishu_apps
|
||||
GROUP BY company_id;
|
||||
Reference in New Issue
Block a user