Files

390 lines
14 KiB
Markdown
Raw Permalink Normal View History

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