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:
@@ -1046,3 +1046,192 @@ echo 'opcache.enable=0' >> /usr/local/etc/php/conf.d/99-disable-opcache.ini
|
||||
2. 登录页需要优先检查 token,已登录则直接跳转
|
||||
3. 多公司支持:URL 需要包含 `?company={id}` 参数
|
||||
4. 飞书卡片通知 URL 格式:`https://xsm.dev.hnxinyouhe.com/approval/detail/{order_id}?company={company_id}`
|
||||
|
||||
---
|
||||
|
||||
## 31. 多公司多飞书应用消息卡片更新失败
|
||||
|
||||
**问题**:
|
||||
在多公司多飞书应用场景下,当多个审批人中有一人拒绝/通过时,其他审批人的消息卡片没有更新,仍然显示操作按钮。
|
||||
|
||||
**场景描述**:
|
||||
- 公司A和公司B的员工共同在一个审批流程中
|
||||
- 公司A的bot发送了消息给公司A的员工
|
||||
- 公司B的bot发送了消息给公司B的员工
|
||||
- 当公司A的员工操作后,需要更新公司B员工的消息卡片
|
||||
- 使用公司A的bot去更新公司B发送的消息 → 失败
|
||||
|
||||
**错误信息**:
|
||||
```
|
||||
HTTP 400 - bot or user is not the message's sender
|
||||
```
|
||||
|
||||
**根本原因**:
|
||||
1. **飞书API限制**:只有发送消息的bot才能更新该消息
|
||||
2. **架构问题**:多公司场景下,每个公司有独立的飞书应用(app_id + app_secret)
|
||||
3. **数据缺失**:`feishu_message` 表没有记录是哪个飞书应用发送的消息
|
||||
|
||||
**解决方案**:
|
||||
|
||||
### 1. 数据库迁移
|
||||
```sql
|
||||
-- 添加 company_id 和 feishu_app_id 字段
|
||||
ALTER TABLE feishu_message
|
||||
ADD COLUMN company_id BIGINT UNSIGNED NULL AFTER user_id,
|
||||
ADD COLUMN feishu_app_id INT UNSIGNED NULL AFTER company_id,
|
||||
ADD INDEX idx_company (company_id);
|
||||
|
||||
-- 更新现有数据(如有)
|
||||
UPDATE feishu_message fm
|
||||
JOIN approval_order ao ON fm.order_id = ao.id
|
||||
JOIN user u ON ao.applicant_id = u.id
|
||||
SET fm.company_id = u.company_id;
|
||||
```
|
||||
|
||||
### 2. 修改 FeishuMessage 模型
|
||||
```php
|
||||
// backend/app/model/FeishuMessage.php
|
||||
protected $fillable = [
|
||||
'order_id',
|
||||
'node_id',
|
||||
'user_id',
|
||||
'company_id', // 新增
|
||||
'feishu_app_id', // 新增
|
||||
'message_id',
|
||||
'msg_token',
|
||||
'status',
|
||||
];
|
||||
|
||||
public static function saveMessage(
|
||||
int $orderId,
|
||||
int $nodeId,
|
||||
int $userId,
|
||||
string $messageId,
|
||||
?string $msgToken = null,
|
||||
?int $companyId = null, // 新增参数
|
||||
?int $feishuAppId = null // 新增参数
|
||||
): self {
|
||||
return self::updateOrCreate(
|
||||
[
|
||||
'order_id' => $orderId,
|
||||
'node_id' => $nodeId,
|
||||
'user_id' => $userId,
|
||||
],
|
||||
[
|
||||
'message_id' => $messageId,
|
||||
'msg_token' => $msgToken,
|
||||
'company_id' => $companyId,
|
||||
'feishu_app_id' => $feishuAppId,
|
||||
'status' => self::STATUS_ACTIVE,
|
||||
]
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
### 3. 修改发送逻辑
|
||||
```php
|
||||
// backend/app/service/FeishuNotificationService.php
|
||||
// 发送消息时保存 company_id 和 feishu_app_id
|
||||
\app\model\FeishuMessage::saveMessage(
|
||||
$orderId,
|
||||
$nodeId,
|
||||
$userId,
|
||||
$messageId,
|
||||
null,
|
||||
$user->company_id, // 保存公司ID
|
||||
$this->appId // 保存飞书应用ID
|
||||
);
|
||||
```
|
||||
|
||||
### 4. 修改更新逻辑
|
||||
```php
|
||||
// backend/app/service/FeishuNotificationService.php
|
||||
public function updateMessageCards(
|
||||
int $orderId,
|
||||
int $nodeId,
|
||||
?int $excludeUserId = null,
|
||||
string $result = 'approved',
|
||||
string $operatorName = ''
|
||||
): bool {
|
||||
$messages = \app\model\FeishuMessage::getMessagesByOrderAndNode($orderId, $nodeId);
|
||||
|
||||
foreach ($messages as $message) {
|
||||
// 获取消息发送时的公司ID和飞书应用ID
|
||||
$companyId = $message->company_id;
|
||||
$feishuAppId = $message->feishu_app_id;
|
||||
|
||||
if (!$companyId || !$feishuAppId) {
|
||||
error_log("❌ 消息记录缺少 company_id 或 feishu_app_id");
|
||||
continue;
|
||||
}
|
||||
|
||||
// 临时切换到该消息对应的飞书应用配置
|
||||
$originalCompanyId = $this->companyId;
|
||||
$originalAppId = $this->appId;
|
||||
|
||||
$this->companyId = $companyId;
|
||||
$this->appId = $feishuAppId;
|
||||
|
||||
try {
|
||||
$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()]
|
||||
);
|
||||
} finally {
|
||||
// 恢复原始配置
|
||||
$this->companyId = $originalCompanyId;
|
||||
$this->appId = $originalAppId;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
```
|
||||
|
||||
### 5. 添加 httpPatchJson 方法
|
||||
```php
|
||||
// backend/app/util/HttpClient.php
|
||||
public static function httpPatchJson(
|
||||
string $url,
|
||||
array $data,
|
||||
array $preencodedKeys = [],
|
||||
array $headers = [],
|
||||
int $timeout = 0
|
||||
): array {
|
||||
$ch = curl_init();
|
||||
curl_setopt($ch, CURLOPT_URL, $url);
|
||||
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'PATCH');
|
||||
|
||||
// 手动构建 JSON
|
||||
$jsonParts = [];
|
||||
foreach ($data as $key => $value) {
|
||||
$keyJson = json_encode($key, JSON_UNESCAPED_UNICODE);
|
||||
if (in_array($key, $preencodedKeys)) {
|
||||
$jsonParts[] = $keyJson . ':' . json_encode($value, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
|
||||
} else {
|
||||
$jsonParts[] = $keyJson . ':' . json_encode($value, JSON_UNESCAPED_UNICODE);
|
||||
}
|
||||
}
|
||||
$jsonPayload = '{' . implode(',', $jsonParts) . '}';
|
||||
|
||||
curl_setopt($ch, CURLOPT_POSTFIELDS, $jsonPayload);
|
||||
// ... 其余代码
|
||||
}
|
||||
```
|
||||
|
||||
**关键点**:
|
||||
1. ✅ 每个飞书应用只能更新自己发送的消息
|
||||
2. ✅ 需要在数据库中记录每条消息是由哪个飞书应用发送的
|
||||
3. ✅ 更新消息时使用原始发送者的飞书应用配置
|
||||
4. ✅ 使用 PATCH `/messages/{id}` 而不是 POST `/messages/{id}/update`
|
||||
|
||||
**相关文件**:
|
||||
- `backend/app/model/FeishuMessage.php` - 添加 company_id 和 feishu_app_id 字段
|
||||
- `backend/app/service/FeishuNotificationService.php` - 修改发送和更新逻辑
|
||||
- `backend/app/util/HttpClient.php` - 添加 httpPatchJson 方法
|
||||
- `docs/APPROVAL_NOTIFICATION_ARCHITECTURE.md` - 架构文档更新
|
||||
|
||||
**飞书API文档**:
|
||||
- 更新消息:https://open.feishu.cn/document/server-docs/im-v1/message/update_messages
|
||||
|
||||
Reference in New Issue
Block a user