Files
x-server-mobile/docs/database-analysis.md
root 0489da5e40 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>
2026-02-26 16:27:33 +08:00

14 KiB

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 (公司表)

CREATE TABLE `companies` (
  id BIGINT PRIMARY KEY,
  name VARCHAR(100),           -- 公司名称
  tenant_key VARCHAR(36),       -- 飞书企业标识
  feishu_enabled TINYINT,      -- 是否启用飞书
  ...
);

2. users (用户表) ⚠️ 关键

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 (飞书应用配置表)

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 (用户飞书绑定表)

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 表的飞书字段只能存储一个值

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 表 (推荐 ⭐)

ALTER TABLE users ADD COLUMN feishu_app_id INT DEFAULT NULL COMMENT '登录来源飞书应用ID';

优点:

  • 最小改动,只需加一个字段
  • 记录用户登录时使用的飞书应用
  • 发送通知时优先使用登录来源的应用

发送通知逻辑:

  1. 检查用户的 feishu_app_id
  2. 如果有,使用该应用发送
  3. 如果没有,使用公司的默认应用

方案B: 创建用户飞书应用绑定表

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: 保持当前设计 + 业务规则

不修改数据库,通过业务规则处理:

发送通知逻辑:

// 1. 获取审批单所属公司的飞书应用
// 2. 优先使用 system_id=1 的应用(默认应用)
// 3. 如果该应用发送失败,尝试其他应用

优点:

  • 无需修改数据库
  • 当前代码已基本支持

缺点:

  • 灵活性较低
  • 无法记录用户登录来源

五、推荐方案

采用 方案A: 扩展 User 表

理由:

  1. 改动最小,只需添加一个字段
  2. 满足当前业务需求(记录登录来源)
  3. 为后续扩展保留空间

实施步骤:

步骤1: 数据库迁移

-- 添加 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 模型

class User extends Model
{
    protected $fillable = [
        // ... 现有字段
        'feishu_app_id',  // 新增
    ];

    // 关联飞书应用
    public function feishuApp()
    {
        return $this->belongsTo(CompanyFeishuApp::class, 'feishu_app_id');
    }
}

步骤3: 修改登录逻辑

// FeishuService.php - createOrUpdateUser 方法
public function createOrUpdateUser(array $feishuData, int $companyId, int $appId): User
{
    // ... 现有逻辑

    $user->update([
        'feishu_user_id' => $userId,
        'feishu_app_id' => $appId,  // 记录登录来源应用
        // ... 其他字段
    ]);
}

步骤4: 修改通知发送逻辑

// 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 关联表

飞书应用选择策略

// 发送通知时的应用选择优先级:
// 1. 用户登录来源的应用 (feishu_app_id)
// 2. 公司的默认应用 (system_id=1)
// 3. 公司的第一个启用应用

是否需要我实施方案 A 的数据库迁移和代码修改?