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