fix: 修复多公司多飞书应用消息卡片更新失败问题
- feishu_message 表新增 company_id 和 feishu_app_id 字段追踪消息发送者 - 修改 FeishuMessage::saveMessage() 支持保存公司和飞书应用信息 - 修改 FeishuNotificationService 使用原始发送者的飞书应用配置更新消息 - 新增 HttpClient::httpPatchJson() 方法支持 PATCH 请求 - 文档更新:PROJECT_ISSUES.md 添加问题31,APPROVAL_NOTIFICATION_ARCHITECTURE.md 添加多公司架构说明 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -537,12 +537,263 @@ ORDER BY created_at DESC;
|
||||
|
||||
---
|
||||
|
||||
## 🏢 多公司多飞书应用架构
|
||||
|
||||
### 架构概述
|
||||
|
||||
在多租户场景下,每个公司可以有独立的飞书应用配置:
|
||||
- 不同的 `app_id`
|
||||
- 不同的 `app_secret`
|
||||
- 不同的 bot 身份
|
||||
|
||||
### 核心约束
|
||||
|
||||
**飞书API限制**:只有发送消息的bot才能更新该消息。
|
||||
|
||||
这意味着:
|
||||
- 公司A的bot发送的消息,只能由公司A的bot更新
|
||||
- 公司B的bot无法更新公司A发送的消息
|
||||
- 在跨公司审批流程中,需要追踪每条消息的发送者
|
||||
|
||||
### 数据模型设计
|
||||
|
||||
#### feishu_message 表结构
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
|------|------|------|
|
||||
| `id` | bigint | 主键 |
|
||||
| `order_id` | bigint | 审批订单ID |
|
||||
| `node_id` | bigint | 审批节点ID |
|
||||
| `user_id` | bigint | 接收消息的用户ID |
|
||||
| `company_id` | bigint | **发送消息的公司ID** |
|
||||
| `feishu_app_id` | int | **发送消息的飞书应用ID** |
|
||||
| `message_id` | varchar | 飞书消息ID |
|
||||
| `msg_token` | varchar | 消息token(用于更新) |
|
||||
| `status` | varchar | 消息状态:active/updated/deleted |
|
||||
|
||||
### 发送消息流程
|
||||
|
||||
```php
|
||||
// FeishuNotificationService::sendApprovalNotification()
|
||||
public function sendApprovalNotification(array $params): bool
|
||||
{
|
||||
$userId = $params['user_id'];
|
||||
$orderId = $params['order_id'];
|
||||
$nodeId = $params['node_id'];
|
||||
|
||||
// 1. 获取用户信息(包含公司ID)
|
||||
$user = User::findOrFail($userId);
|
||||
|
||||
// 2. 发送飞书消息
|
||||
$response = $this->sendMessage($user->feishu_user_id, $card);
|
||||
$messageId = $response['data']['message_id'];
|
||||
|
||||
// 3. 保存消息映射(包含公司ID和飞书应用ID)
|
||||
\app\model\FeishuMessage::saveMessage(
|
||||
$orderId,
|
||||
$nodeId,
|
||||
$userId,
|
||||
$messageId,
|
||||
null,
|
||||
$user->company_id, // ← 记录公司ID
|
||||
$this->appId // ← 记录飞书应用ID
|
||||
);
|
||||
|
||||
return true;
|
||||
}
|
||||
```
|
||||
|
||||
### 更新消息流程
|
||||
|
||||
```php
|
||||
// FeishuNotificationService::updateMessageCards()
|
||||
public function updateMessageCards(
|
||||
int $orderId,
|
||||
int $nodeId,
|
||||
?int $excludeUserId = null,
|
||||
string $result = 'approved',
|
||||
string $operatorName = ''
|
||||
): bool {
|
||||
// 1. 获取该订单+节点的所有消息
|
||||
$messages = \app\model\FeishuMessage::getMessagesByOrderAndNode($orderId, $nodeId);
|
||||
|
||||
foreach ($messages as $message) {
|
||||
// 2. 获取消息发送时的配置
|
||||
$companyId = $message->company_id;
|
||||
$feishuAppId = $message->feishu_app_id;
|
||||
|
||||
if (!$companyId || !$feishuAppId) {
|
||||
error_log("❌ 消息记录缺少 company_id 或 feishu_app_id");
|
||||
continue;
|
||||
}
|
||||
|
||||
// 3. 临时切换到该消息对应的飞书应用配置
|
||||
$originalCompanyId = $this->companyId;
|
||||
$originalAppId = $this->appId;
|
||||
|
||||
$this->companyId = $companyId;
|
||||
$this->appId = $feishuAppId;
|
||||
|
||||
try {
|
||||
// 4. 使用正确的飞书应用更新消息
|
||||
$response = HttpClient::httpPatchJson(
|
||||
"https://open.feishu.cn/open-apis/im/v1/messages/{$message->message_id}",
|
||||
['msg_type' => 'interactive', 'content' => $contentJson],
|
||||
['content'],
|
||||
['Authorization: Bearer ' . $this->getAccessToken()]
|
||||
);
|
||||
|
||||
error_log("✅ 更新消息成功: {$message->message_id}");
|
||||
} catch (\Exception $e) {
|
||||
error_log("❌ 更新消息失败: {$e->getMessage()}");
|
||||
} finally {
|
||||
// 5. 恢复原始配置
|
||||
$this->companyId = $originalCompanyId;
|
||||
$this->appId = $originalAppId;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
```
|
||||
|
||||
### 跨公司审批场景示例
|
||||
|
||||
**场景**:公司A的员工发起审批,审批人包括公司A和公司B的员工
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ 审批订单创建 │
|
||||
│ - 申请人:公司A-用户1 │
|
||||
│ - 审批人:[公司A-用户2, 公司B-用户1] │
|
||||
└─────────────────────────────────────────────────────────────┘
|
||||
│
|
||||
▼
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ 发送审批通知 │
|
||||
│ - 公司A-用户2:使用公司A的飞书应用发送 │
|
||||
│ - 公司B-用户1:使用公司B的飞书应用发送 │
|
||||
└─────────────────────────────────────────────────────────────┘
|
||||
│
|
||||
▼
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ 公司A-用户2 点击"拒绝" │
|
||||
│ - 触发 ApprovalService::reject() │
|
||||
│ - 订单状态变为 rejected │
|
||||
│ - 需要更新公司B-用户1的消息卡片 │
|
||||
└─────────────────────────────────────────────────────────────┘
|
||||
│
|
||||
▼
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ updateMessageCards() 执行 │
|
||||
│ - 查询 feishu_message 表 │
|
||||
│ - 公司B-用户1的消息记录: │
|
||||
│ · company_id = 公司B的ID │
|
||||
│ · feishu_app_id = 公司B的飞书应用ID │
|
||||
│ - 切换到公司B的飞书应用配置 │
|
||||
│ - 调用 PATCH /messages/{id} 更新消息 │
|
||||
└─────────────────────────────────────────────────────────────┘
|
||||
│
|
||||
▼
|
||||
✅ 成功
|
||||
```
|
||||
|
||||
### 关键实现要点
|
||||
|
||||
#### 1. FeishuNotificationService 支持动态切换配置
|
||||
|
||||
```php
|
||||
class FeishuNotificationService
|
||||
{
|
||||
private int $companyId;
|
||||
private string $appId;
|
||||
private string $appSecret;
|
||||
|
||||
// 支持运行时切换配置
|
||||
public function switchCompany(int $companyId): void
|
||||
{
|
||||
$this->companyId = $companyId;
|
||||
$feishuConfig = $this->getFeishuConfig($companyId);
|
||||
$this->appId = $feishuConfig['app_id'];
|
||||
$this->appSecret = $feishuConfig['app_secret'];
|
||||
}
|
||||
|
||||
private function getAccessToken(): string
|
||||
{
|
||||
$cacheKey = "feishu:access_token:{$this->companyId}";
|
||||
// 使用 company_id 维度的缓存key
|
||||
// ...
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### 2. HttpClient 支持预编码JSON字段
|
||||
|
||||
```php
|
||||
// httpPatchJson 方法支持某些字段预编码
|
||||
HttpClient::httpPatchJson(
|
||||
$url,
|
||||
[
|
||||
'msg_type' => 'interactive',
|
||||
'content' => $preEncodedJson, // 已编码的JSON字符串
|
||||
],
|
||||
['content'], // 声明 content 字段需要预编码
|
||||
$headers
|
||||
);
|
||||
```
|
||||
|
||||
#### 3. 数据完整性
|
||||
|
||||
```sql
|
||||
-- 确保所有消息都有 company_id 和 feishu_app_id
|
||||
UPDATE feishu_message fm
|
||||
JOIN user u ON fm.user_id = u.id
|
||||
SET fm.company_id = u.company_id,
|
||||
fm.feishu_app_id = (
|
||||
SELECT id FROM feishu_config
|
||||
WHERE company_id = u.company_id
|
||||
LIMIT 1
|
||||
)
|
||||
WHERE fm.company_id IS NULL;
|
||||
```
|
||||
|
||||
### 常见问题
|
||||
|
||||
#### Q1: 为什么会出现 "bot or user is not the message's sender" 错误?
|
||||
|
||||
**A**: 使用了错误的飞书应用配置去更新消息。确保:
|
||||
1. `feishu_message` 表正确记录了 `company_id` 和 `feishu_app_id`
|
||||
2. 更新消息时使用记录中的配置,而不是当前操作者的配置
|
||||
|
||||
#### Q2: 如何调试多公司消息更新问题?
|
||||
|
||||
**A**: 查看日志中的关键信息:
|
||||
```bash
|
||||
docker logs x-server-backend -f | grep -E "(company_id|feishu_app_id|更新消息)"
|
||||
```
|
||||
|
||||
#### Q3: 历史数据缺少 company_id 怎么办?
|
||||
|
||||
**A**: 根据用户信息回填:
|
||||
```sql
|
||||
UPDATE feishu_message fm
|
||||
JOIN user u ON fm.user_id = u.id
|
||||
SET fm.company_id = u.company_id;
|
||||
```
|
||||
|
||||
### 相关问题文档
|
||||
|
||||
详见 `docs/PROJECT_ISSUES.md` 第31条:多公司多飞书应用消息卡片更新失败
|
||||
|
||||
---
|
||||
|
||||
## 📚 相关文档
|
||||
|
||||
- [数据库结构文档](./DATABASE_SCHEMA.md)
|
||||
- [项目文档](./PROJECT.md)
|
||||
- [问题汇总](./PROJECT_ISSUES.md)
|
||||
|
||||
---
|
||||
|
||||
**维护人**: 开发团队
|
||||
**最后更新**: 2026-02-28
|
||||
**最后更新**: 2026-03-01
|
||||
|
||||
Reference in New Issue
Block a user