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:
@@ -15,6 +15,8 @@ class FeishuMessage extends Model
|
||||
'order_id',
|
||||
'node_id',
|
||||
'user_id',
|
||||
'company_id',
|
||||
'feishu_app_id',
|
||||
'message_id',
|
||||
'msg_token',
|
||||
'status',
|
||||
@@ -44,7 +46,9 @@ class FeishuMessage extends Model
|
||||
int $nodeId,
|
||||
int $userId,
|
||||
string $messageId,
|
||||
?string $msgToken = null
|
||||
?string $msgToken = null,
|
||||
?int $companyId = null,
|
||||
?int $feishuAppId = null
|
||||
): self {
|
||||
return self::updateOrCreate(
|
||||
[
|
||||
@@ -55,6 +59,8 @@ class FeishuMessage extends Model
|
||||
[
|
||||
'message_id' => $messageId,
|
||||
'msg_token' => $msgToken,
|
||||
'company_id' => $companyId,
|
||||
'feishu_app_id' => $feishuAppId,
|
||||
'status' => self::STATUS_ACTIVE,
|
||||
]
|
||||
);
|
||||
|
||||
@@ -163,9 +163,17 @@ class FeishuNotificationService
|
||||
$messageId = $this->sendMessage($feishuUserId, $card, $orderId, $nodeId, $userId);
|
||||
|
||||
if ($messageId && $orderId && $nodeId) {
|
||||
// 保存消息映射
|
||||
\app\model\FeishuMessage::saveMessage($orderId, $nodeId, $userId, $messageId);
|
||||
error_log("💾 保存消息映射: order_id={$orderId}, node_id={$nodeId}, user_id={$userId}, message_id={$messageId}");
|
||||
// 保存消息映射(包含公司ID和飞书应用ID)
|
||||
\app\model\FeishuMessage::saveMessage(
|
||||
$orderId,
|
||||
$nodeId,
|
||||
$userId,
|
||||
$messageId,
|
||||
null,
|
||||
$user->company_id,
|
||||
$this->appId
|
||||
);
|
||||
error_log("💾 保存消息映射: order_id={$orderId}, node_id={$nodeId}, user_id={$userId}, company_id={$user->company_id}, feishu_app_id={$this->appId}, message_id={$messageId}");
|
||||
}
|
||||
|
||||
return $messageId !== false;
|
||||
@@ -662,6 +670,17 @@ class FeishuNotificationService
|
||||
* @param string $operatorName 操作人姓名
|
||||
* @return bool
|
||||
*/
|
||||
/**
|
||||
* 更新其他审批人的消息卡片
|
||||
* 多公司多飞书应用场景:使用每个消息对应的原始飞书应用去更新
|
||||
*
|
||||
* @param int $orderId 订单ID
|
||||
* @param int $nodeId 节点ID
|
||||
* @param int $excludeUserId 排除的用户ID(已经操作过的用户)
|
||||
* @param string $status 审批状态 (approved/rejected)
|
||||
* @param string $operatorName 操作人姓名
|
||||
* @return bool
|
||||
*/
|
||||
public function updateMessageCards(int $orderId, int $nodeId, int $excludeUserId, string $status, string $operatorName): bool
|
||||
{
|
||||
try {
|
||||
@@ -677,27 +696,26 @@ class FeishuNotificationService
|
||||
return true;
|
||||
}
|
||||
|
||||
// 获取订单申请人信息,从用户中获取公司ID
|
||||
$order = \app\model\ApprovalOrder::find($orderId);
|
||||
$companyId = null;
|
||||
if ($order) {
|
||||
$applicant = \app\model\User::find($order->applicant_id);
|
||||
if ($applicant && !empty($applicant->company_id)) {
|
||||
$companyId = $applicant->company_id;
|
||||
}
|
||||
}
|
||||
|
||||
// 如果获取到公司ID,设置飞书应用配置
|
||||
if ($companyId) {
|
||||
$this->setAppByUser($excludeUserId, $companyId);
|
||||
}
|
||||
|
||||
$token = $this->getAccessToken();
|
||||
$statusText = $status === 'approved' ? '已通过' : '已拒绝';
|
||||
$statusColor = $status === 'approved' ? 'green' : 'red';
|
||||
|
||||
// 为每个消息使用其对应的飞书应用去更新
|
||||
foreach ($messages as $message) {
|
||||
try {
|
||||
$companyId = $message->company_id;
|
||||
$feishuAppId = $message->feishu_app_id;
|
||||
|
||||
if (!$companyId || !$feishuAppId) {
|
||||
error_log("❌ 消息记录缺少 company_id 或 feishu_app_id: message_id={$message->message_id}");
|
||||
continue;
|
||||
}
|
||||
|
||||
error_log("🔍 准备更新消息: message_id={$message->message_id}, company_id={$companyId}, feishu_app_id={$feishuAppId}");
|
||||
|
||||
// 使用该消息对应的飞书应用配置
|
||||
$this->companyId = $companyId;
|
||||
$this->appId = $feishuAppId;
|
||||
|
||||
// 构建更新后的卡片(移除操作按钮)
|
||||
$updatedCard = [
|
||||
'config' => [
|
||||
@@ -714,86 +732,52 @@ class FeishuNotificationService
|
||||
[
|
||||
'tag' => 'div',
|
||||
'text' => [
|
||||
'content' => "**操作人**: {$operatorName}\n**操作结果**: {$statusText}\n\n此审批已被其他审批人处理,无需重复操作。",
|
||||
'content' => "**操作人**: {$operatorName}\n**操作结果**: {$statusText}\n\n此审批已被处理,无需重复操作。",
|
||||
'tag' => 'lark_md',
|
||||
],
|
||||
],
|
||||
[
|
||||
'tag' => 'action',
|
||||
'actions' => [
|
||||
[
|
||||
'tag' => 'button',
|
||||
'text' => [
|
||||
'content' => '查看详情',
|
||||
'tag' => 'plain_text',
|
||||
],
|
||||
'type' => 'default',
|
||||
'url' => "https://uniservice-mobile.hnxinyouhe.com/approval/detail/{$orderId}?company={$companyId}",
|
||||
],
|
||||
],
|
||||
],
|
||||
],
|
||||
];
|
||||
|
||||
$requestData = [
|
||||
'message_id' => $message->message_id,
|
||||
'card' => $updatedCard,
|
||||
'status' => $status,
|
||||
'operator' => $operatorName,
|
||||
];
|
||||
|
||||
// content 必须是 card 对象的 JSON 字符串,不是 {"card":{...}}
|
||||
// JSON_UNESCAPED_UNICODE 保留 emoji
|
||||
// JSON_UNESCAPED_SLASHES 避免 URL 中的 / 被转义为 \/
|
||||
$contentJson = json_encode($updatedCard, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
|
||||
|
||||
$response = HttpClient::httpPostJson(
|
||||
"https://open.feishu.cn/open-apis/im/v1/messages/{$message->message_id}/update",
|
||||
$response = HttpClient::httpPatchJson(
|
||||
"https://open.feishu.cn/open-apis/im/v1/messages/{$message->message_id}",
|
||||
[
|
||||
'msg_type' => 'interactive',
|
||||
'content' => $contentJson,
|
||||
],
|
||||
['content'], // content 字段是预编码的 JSON 字符串
|
||||
['content'],
|
||||
[
|
||||
'Authorization: Bearer ' . $token,
|
||||
'Authorization: Bearer ' . $this->getAccessToken(),
|
||||
]
|
||||
);
|
||||
|
||||
$code = $response['code'] ?? -1;
|
||||
if ($code === 0) {
|
||||
error_log("✅ 消息卡片更新成功: message_id={$message->message_id}");
|
||||
error_log("✅ 消息卡片更新成功: message_id={$message->message_id}, company_id={$companyId}");
|
||||
// 标记消息为已更新
|
||||
$message->status = \app\model\FeishuMessage::STATUS_UPDATED;
|
||||
$message->save();
|
||||
|
||||
// 记录成功日志
|
||||
\app\model\FeishuMessageLog::log(
|
||||
\app\model\FeishuMessageLog::ACTION_UPDATE_CARD,
|
||||
$orderId,
|
||||
$nodeId,
|
||||
$message->user_id,
|
||||
$message->message_id,
|
||||
\app\model\FeishuMessageLog::STATUS_SUCCESS,
|
||||
$requestData,
|
||||
$response
|
||||
);
|
||||
} else {
|
||||
error_log("❌ 消息卡片更新失败: message_id={$message->message_id}, error=" . json_encode($response));
|
||||
|
||||
// 记录失败日志
|
||||
\app\model\FeishuMessageLog::log(
|
||||
\app\model\FeishuMessageLog::ACTION_UPDATE_CARD,
|
||||
$orderId,
|
||||
$nodeId,
|
||||
$message->user_id,
|
||||
$message->message_id,
|
||||
\app\model\FeishuMessageLog::STATUS_FAILED,
|
||||
$requestData,
|
||||
$response,
|
||||
$response['msg'] ?? 'Unknown error'
|
||||
);
|
||||
error_log("❌ 消息卡片更新失败: message_id={$message->message_id}, company_id={$companyId}, error=" . json_encode($response));
|
||||
}
|
||||
} catch (Exception $e) {
|
||||
error_log("❌ 更新消息卡片异常: message_id={$message->message_id}, error=" . $e->getMessage());
|
||||
|
||||
// 记录异常日志
|
||||
\app\model\FeishuMessageLog::log(
|
||||
\app\model\FeishuMessageLog::ACTION_UPDATE_CARD,
|
||||
$orderId,
|
||||
$nodeId,
|
||||
$message->user_id,
|
||||
$message->message_id,
|
||||
\app\model\FeishuMessageLog::STATUS_FAILED,
|
||||
null,
|
||||
null,
|
||||
$e->getMessage()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -834,16 +818,23 @@ class FeishuNotificationService
|
||||
// 获取订单申请人信息,从用户中获取公司ID
|
||||
$order = \app\model\ApprovalOrder::find($orderId);
|
||||
$companyId = null;
|
||||
$applicantIdForLog = 'null';
|
||||
if ($order) {
|
||||
$applicantIdForLog = $order->applicant_id;
|
||||
$applicant = \app\model\User::find($order->applicant_id);
|
||||
if ($applicant && !empty($applicant->company_id)) {
|
||||
$companyId = $applicant->company_id;
|
||||
}
|
||||
}
|
||||
|
||||
error_log("🔍 updateMessageCards: order_id={$orderId}, applicant_id={$applicantIdForLog}, company_id={$companyId}");
|
||||
|
||||
// 如果获取到公司ID,设置飞书应用配置
|
||||
if ($companyId) {
|
||||
$this->setAppByUser($excludeUserId, $companyId);
|
||||
error_log("✅ setAppByUser called: user_id={$excludeUserId}, company_id={$companyId}");
|
||||
} else {
|
||||
error_log("❌ company_id is empty or order not found");
|
||||
}
|
||||
|
||||
$token = $this->getAccessToken();
|
||||
@@ -914,8 +905,8 @@ class FeishuNotificationService
|
||||
|
||||
$contentJson = json_encode($updatedCard, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
|
||||
|
||||
$response = HttpClient::httpPostJson(
|
||||
"https://open.feishu.cn/open-apis/im/v1/messages/{$message->message_id}/update",
|
||||
$response = HttpClient::httpPatchJson(
|
||||
"https://open.feishu.cn/open-apis/im/v1/messages/{$message->message_id}",
|
||||
[
|
||||
'msg_type' => 'interactive',
|
||||
'content' => $contentJson,
|
||||
|
||||
@@ -242,4 +242,75 @@ class HttpClient
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送 HTTP PATCH JSON 请求
|
||||
*
|
||||
* @param string $url 请求URL
|
||||
* @param array $data 请求数据(将被编码为 JSON)
|
||||
* @param array $preencodedKeys 需要预编码的字段名列表(这些字段的值已经是 JSON 字符串)
|
||||
* @param array $headers 额外的请求头
|
||||
* @param int $timeout 超时时间(秒)
|
||||
* @return array 响应数据(JSON解码后的数组)
|
||||
* @throws Exception
|
||||
*/
|
||||
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);
|
||||
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
|
||||
curl_setopt($ch, CURLOPT_TIMEOUT, $timeout ?: self::$defaultTimeout);
|
||||
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
|
||||
|
||||
$defaultHeaders = [
|
||||
'Content-Type: application/json',
|
||||
];
|
||||
|
||||
curl_setopt($ch, CURLOPT_HTTPHEADER, array_merge($defaultHeaders, $headers));
|
||||
|
||||
$response = curl_exec($ch);
|
||||
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
||||
$error = curl_error($ch);
|
||||
|
||||
// PHP 8.0+ curl 资源自动关闭,无需 curl_close()
|
||||
|
||||
self::logRequest('PATCH', $url, $data, $response, $httpCode);
|
||||
|
||||
if ($error) {
|
||||
throw new Exception("HTTP PATCH 请求失败: {$error}");
|
||||
}
|
||||
|
||||
if ($httpCode !== 200) {
|
||||
throw new Exception("HTTP PATCH 请求失败: HTTP {$httpCode}, Response: {$response}");
|
||||
}
|
||||
|
||||
$result = json_decode($response, true);
|
||||
|
||||
if (json_last_error() !== JSON_ERROR_NONE) {
|
||||
throw new Exception('JSON 解析失败: ' . $response);
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user