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:
root
2026-02-26 16:27:33 +08:00
parent 04947ae52d
commit 0489da5e40
11 changed files with 942 additions and 45 deletions
+14 -10
View File
@@ -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}");
+11 -2
View File
@@ -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('测试消息发送失败,请查看日志');
+1
View File
@@ -11,6 +11,7 @@ class FeishuLoginSession extends Model
protected $fillable = [
'company_id',
'tenant_key',
'feishu_app_id',
'status',
'user_id',
'expires_at',
+73 -3
View File
@@ -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)
{
+78
View File
@@ -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;
+61 -11
View File
@@ -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;