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;
|
||||
@@ -0,0 +1,389 @@
|
||||
# X-Server Mobile 项目数据库与多对多关系分析报告
|
||||
|
||||
## 一、项目概述
|
||||
|
||||
**项目名称**: X-Server Mobile
|
||||
**项目类型**: 企业内部H5移动工具平台
|
||||
**核心功能**: 审批流程引擎、多租户架构、飞书集成
|
||||
|
||||
## 二、数据库表结构分析
|
||||
|
||||
### 2.1 核心表关系图
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────────────┐
|
||||
│ 多对多关系架构 │
|
||||
├─────────────────────────────────────────────────────────────────────┤
|
||||
│ │
|
||||
│ ┌──────────────┐ │
|
||||
│ │ Person(人) │ ← 物理概念: 通过 union_id 识别 │
|
||||
│ │ union_id │ │
|
||||
│ └──────────────┘ │
|
||||
│ │ │
|
||||
│ ├────────────────────────────────────────────┐ │
|
||||
│ │ │ │
|
||||
│ ▼ ▼ │
|
||||
│ ┌──────────────┐ ┌──────────────┐ │
|
||||
│ │ 公司A Users │ │ 公司B Users │ │
|
||||
│ │ company_id=1 │ │ company_id=2 │ │
|
||||
│ └──────────────┘ └──────────────┘ │
|
||||
│ │ │ │
|
||||
│ │ 每个 User 有对应的飞书身份 │ │
|
||||
│ ▼ ▼ │
|
||||
│ ┌──────────────────────────────────────────────────────────┐ │
|
||||
│ │ 公司飞书应用 (company_feishu_apps) │ │
|
||||
│ │ ┌────────────┐ ┌────────────┐ ┌────────────┐ │ │
|
||||
│ │ │ OA系统应用 │ │ 考勤系统应用 │ │ 审批系统应用 │ │ │
|
||||
│ │ │ app_id=aaa │ │ app_id=bbb │ │ app_id=ccc │ │ │
|
||||
│ │ └────────────┘ └────────────┘ └────────────┘ │ │
|
||||
│ └──────────────────────────────────────────────────────────┘ │
|
||||
│ │
|
||||
└─────────────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
### 2.2 表结构详解
|
||||
|
||||
#### 1. companies (公司表)
|
||||
```sql
|
||||
CREATE TABLE `companies` (
|
||||
id BIGINT PRIMARY KEY,
|
||||
name VARCHAR(100), -- 公司名称
|
||||
tenant_key VARCHAR(36), -- 飞书企业标识
|
||||
feishu_enabled TINYINT, -- 是否启用飞书
|
||||
...
|
||||
);
|
||||
```
|
||||
|
||||
#### 2. users (用户表) ⚠️ 关键
|
||||
```sql
|
||||
CREATE TABLE `users` (
|
||||
id BIGINT PRIMARY KEY,
|
||||
company_id BIGINT, -- 所属公司 (单对一)
|
||||
union_id VARCHAR(60), -- 飞书UnionID (全局唯一)
|
||||
feishu_user_id VARCHAR(60), -- 该公司的飞书用户ID
|
||||
feishu_open_id VARCHAR(60), -- 飞书OpenID
|
||||
tenant_key VARCHAR(60), -- 当前企业标识
|
||||
...
|
||||
);
|
||||
```
|
||||
|
||||
**设计特点**:
|
||||
- ✅ 同一个人在不同公司 = 不同的 User 记录
|
||||
- ✅ 通过 `union_id + company_id` 唯一识别
|
||||
- ❌ 每个 User 只能存储一个公司的飞书身份
|
||||
|
||||
#### 3. company_feishu_apps (飞书应用配置表)
|
||||
```sql
|
||||
CREATE TABLE `company_feishu_apps` (
|
||||
id INT PRIMARY KEY,
|
||||
company_id INT, -- 所属公司
|
||||
app_id VARCHAR(60), -- 飞书应用ID
|
||||
tenant_key VARCHAR(60), -- 飞书租户key
|
||||
system_id INT, -- 系统ID (1=X-Server)
|
||||
...
|
||||
);
|
||||
```
|
||||
|
||||
**设计特点**:
|
||||
- ✅ 一个公司可以有多个飞书应用
|
||||
- ✅ 每个应用有独立的 app_id 和 tenant_key
|
||||
|
||||
#### 4. user_feishu_binding (用户飞书绑定表)
|
||||
```sql
|
||||
CREATE TABLE `user_feishu_binding` (
|
||||
id BIGINT PRIMARY KEY,
|
||||
user_id BIGINT,
|
||||
company_id BIGINT,
|
||||
feishu_open_id VARCHAR(100),
|
||||
UNIQUE KEY uk_user_company (user_id, company_id)
|
||||
);
|
||||
```
|
||||
|
||||
**状态**: ⚠️ **此表未被使用**
|
||||
|
||||
## 三、多对多关系分析
|
||||
|
||||
### 3.1 当前实现方式
|
||||
|
||||
**用户 ↔ 公司**: 通过 `union_id + company_id` 组合实现
|
||||
- 同一个人在不同公司有不同的 User 记录
|
||||
- 每个 User 记录存储该公司的飞书身份信息
|
||||
|
||||
**公司 ↔ 飞书应用**: 一对多
|
||||
- 一个公司可以有多个飞书应用配置
|
||||
- 每个应用独立配置 app_id, app_secret
|
||||
|
||||
### 3.2 现有设计的问题
|
||||
|
||||
#### 问题1: User 表的飞书字段只能存储一个值
|
||||
|
||||
```sql
|
||||
users 表:
|
||||
- feishu_user_id VARCHAR(60) -- 只能存一个值
|
||||
- feishu_open_id VARCHAR(60) -- 只能存一个值
|
||||
- tenant_key VARCHAR(60) -- 只能存一个值
|
||||
```
|
||||
|
||||
**场景问题**:
|
||||
如果公司A有2个飞书应用(OA、考勤),张三在两个应用中都有账号,但 User 表只能存一个 feishu_user_id。
|
||||
|
||||
#### 问题2: 发送通知时的应用选择
|
||||
|
||||
当前 `FeishuNotificationService` 使用默认应用(system_id=1),没有考虑:
|
||||
- 业务场景应该用哪个应用发送?
|
||||
- 接收人是否在该应用中存在?
|
||||
|
||||
#### 问题3: 登录来源应用未记录
|
||||
|
||||
用户通过不同飞书应用登录时,没有记录登录来源,后续发送通知时无法知道用哪个应用。
|
||||
|
||||
## 四、解决方案设计
|
||||
|
||||
### 方案A: 扩展 User 表 (推荐 ⭐)
|
||||
|
||||
```sql
|
||||
ALTER TABLE users ADD COLUMN feishu_app_id INT DEFAULT NULL COMMENT '登录来源飞书应用ID';
|
||||
```
|
||||
|
||||
**优点**:
|
||||
- 最小改动,只需加一个字段
|
||||
- 记录用户登录时使用的飞书应用
|
||||
- 发送通知时优先使用登录来源的应用
|
||||
|
||||
**发送通知逻辑**:
|
||||
1. 检查用户的 feishu_app_id
|
||||
2. 如果有,使用该应用发送
|
||||
3. 如果没有,使用公司的默认应用
|
||||
|
||||
### 方案B: 创建用户飞书应用绑定表
|
||||
|
||||
```sql
|
||||
CREATE TABLE user_feishu_apps (
|
||||
id BIGINT PRIMARY KEY,
|
||||
user_id BIGINT NOT NULL,
|
||||
company_feishu_app_id INT NOT NULL,
|
||||
feishu_user_id VARCHAR(60),
|
||||
feishu_open_id VARCHAR(60),
|
||||
is_default TINYINT DEFAULT 0,
|
||||
created_at TIMESTAMP,
|
||||
UNIQUE KEY uk_user_app (user_id, company_feishu_app_id),
|
||||
KEY idx_user_id (user_id),
|
||||
KEY idx_default (user_id, is_default)
|
||||
);
|
||||
```
|
||||
|
||||
**优点**:
|
||||
- 支持一个用户在同一公司关联多个飞书应用
|
||||
- 完整记录用户在不同应用的飞书身份
|
||||
- 灵活设置默认应用
|
||||
|
||||
**缺点**:
|
||||
- 需要创建新表
|
||||
- 需要修改较多代码
|
||||
|
||||
### 方案C: 保持当前设计 + 业务规则
|
||||
|
||||
不修改数据库,通过业务规则处理:
|
||||
|
||||
**发送通知逻辑**:
|
||||
```php
|
||||
// 1. 获取审批单所属公司的飞书应用
|
||||
// 2. 优先使用 system_id=1 的应用(默认应用)
|
||||
// 3. 如果该应用发送失败,尝试其他应用
|
||||
```
|
||||
|
||||
**优点**:
|
||||
- 无需修改数据库
|
||||
- 当前代码已基本支持
|
||||
|
||||
**缺点**:
|
||||
- 灵活性较低
|
||||
- 无法记录用户登录来源
|
||||
|
||||
## 五、推荐方案
|
||||
|
||||
### 采用 **方案A**: 扩展 User 表
|
||||
|
||||
**理由**:
|
||||
1. 改动最小,只需添加一个字段
|
||||
2. 满足当前业务需求(记录登录来源)
|
||||
3. 为后续扩展保留空间
|
||||
|
||||
**实施步骤**:
|
||||
|
||||
#### 步骤1: 数据库迁移
|
||||
|
||||
```sql
|
||||
-- 添加 feishu_app_id 字段
|
||||
ALTER TABLE users
|
||||
ADD COLUMN feishu_app_id INT DEFAULT NULL COMMENT '登录来源飞书应用ID'
|
||||
AFTER tenant_key;
|
||||
|
||||
-- 添加索引
|
||||
ALTER TABLE users
|
||||
ADD INDEX idx_feishu_app_id (feishu_app_id);
|
||||
```
|
||||
|
||||
#### 步骤2: 修改 User 模型
|
||||
|
||||
```php
|
||||
class User extends Model
|
||||
{
|
||||
protected $fillable = [
|
||||
// ... 现有字段
|
||||
'feishu_app_id', // 新增
|
||||
];
|
||||
|
||||
// 关联飞书应用
|
||||
public function feishuApp()
|
||||
{
|
||||
return $this->belongsTo(CompanyFeishuApp::class, 'feishu_app_id');
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### 步骤3: 修改登录逻辑
|
||||
|
||||
```php
|
||||
// FeishuService.php - createOrUpdateUser 方法
|
||||
public function createOrUpdateUser(array $feishuData, int $companyId, int $appId): User
|
||||
{
|
||||
// ... 现有逻辑
|
||||
|
||||
$user->update([
|
||||
'feishu_user_id' => $userId,
|
||||
'feishu_app_id' => $appId, // 记录登录来源应用
|
||||
// ... 其他字段
|
||||
]);
|
||||
}
|
||||
```
|
||||
|
||||
#### 步骤4: 修改通知发送逻辑
|
||||
|
||||
```php
|
||||
// FeishuNotificationService.php
|
||||
private function getAppConfig(): array
|
||||
{
|
||||
$app = null;
|
||||
|
||||
// 1. 如果指定了应用,使用指定的应用
|
||||
if ($this->appId) {
|
||||
$app = CompanyFeishuApp::find($this->appId);
|
||||
}
|
||||
|
||||
// 2. 否则使用公司的默认应用
|
||||
if (!$app) {
|
||||
$app = CompanyFeishuApp::where('company_id', $this->companyId)
|
||||
->where('system_id', 1)
|
||||
->where('status', 1)
|
||||
->orderBy('sort')
|
||||
->first();
|
||||
}
|
||||
|
||||
if (!$app || $app->status !== 1) {
|
||||
throw new Exception('未找到可用的飞书应用');
|
||||
}
|
||||
|
||||
return $app->toArray();
|
||||
}
|
||||
|
||||
// 新增:通过用户获取其登录来源应用
|
||||
public function setAppByUser(int $userId): self
|
||||
{
|
||||
$user = User::find($userId);
|
||||
if (!$user) {
|
||||
throw new Exception('用户不存在');
|
||||
}
|
||||
|
||||
$this->setCompany($user->company_id);
|
||||
|
||||
// 如果用户有登录来源应用,使用该应用
|
||||
if ($user->feishu_app_id) {
|
||||
$this->appId = $user->feishu_app_id;
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
```
|
||||
|
||||
## 六、业务流程说明
|
||||
|
||||
### 6.1 用户登录流程
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ 飞书登录流程 │
|
||||
├─────────────────────────────────────────────────────────────┤
|
||||
│ │
|
||||
│ 1. 用户通过飞书应用A登录 │
|
||||
│ ├─ 获取 app_id = cli_aaa │
|
||||
│ ├─ 获取用户信息 (union_id, feishu_user_id) │
|
||||
│ └─ 创建/更新 User 记录 │
|
||||
│ - company_id: 1 │
|
||||
│ - union_id: on_abc123 │
|
||||
│ - feishu_user_id: user_a │
|
||||
│ - feishu_app_id: 1 (应用A的ID) ✨ 新增 │
|
||||
│ │
|
||||
│ 2. 同一用户通过飞书应用B登录 (同一公司) │
|
||||
│ ├─ 获取 app_id = cli_bbb │
|
||||
│ ├─ 获取用户信息 (union_id 相同, feishu_user_id 不同) │
|
||||
│ └─ 更新 User 记录 │
|
||||
│ - feishu_user_id: user_b │
|
||||
│ - feishu_app_id: 2 (应用B的ID) ✨ 新增 │
|
||||
│ │
|
||||
└─────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
### 6.2 发送通知流程
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ 发送通知流程 │
|
||||
├─────────────────────────────────────────────────────────────┤
|
||||
│ │
|
||||
│ 场景:公司A 的用户张三提交出差申请,需要通知审批人李四 │
|
||||
│ │
|
||||
│ 1. 获取审批人李四的 User 记录 │
|
||||
│ └─ company_id: 1, feishu_app_id: 1 │
|
||||
│ │
|
||||
│ 2. 创建通知服务 │
|
||||
│ $notification = new FeishuNotificationService(); │
|
||||
│ $notification->setAppByUser($李四的user_id); ✨ 新增 │
|
||||
│ │
|
||||
│ 3. 发送通知 │
|
||||
│ └─ 使用李四登录来源的应用A发送消息 │
|
||||
│ └─ 如果应用A发送失败,降级使用默认应用 │
|
||||
│ │
|
||||
└─────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
## 七、总结
|
||||
|
||||
### 当前数据库设计评估
|
||||
|
||||
| 设计点 | 评估 | 说明 |
|
||||
|--------|------|------|
|
||||
| 用户 ↔ 公司 | ✅ 满足 | 通过 union_id + company_id 实现 |
|
||||
| 公司 ↔ 飞书应用 | ✅ 满足 | company_feishu_apps 表 |
|
||||
| 用户 ↔ 飞书应用 | ⚠️ 需改进 | 缺少登录来源记录 |
|
||||
|
||||
### 建议改进
|
||||
|
||||
**必须添加**:
|
||||
- `users.feishu_app_id` 字段 - 记录用户登录来源的飞书应用
|
||||
|
||||
**可选添加**:
|
||||
- 如果需要支持用户在同一公司关联多个飞书应用,可创建 `user_feishu_apps` 关联表
|
||||
|
||||
### 飞书应用选择策略
|
||||
|
||||
```php
|
||||
// 发送通知时的应用选择优先级:
|
||||
// 1. 用户登录来源的应用 (feishu_app_id)
|
||||
// 2. 公司的默认应用 (system_id=1)
|
||||
// 3. 公司的第一个启用应用
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
**是否需要我实施方案 A 的数据库迁移和代码修改?**
|
||||
Reference in New Issue
Block a user