# 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 的数据库迁移和代码修改?**