diff --git a/backend/app/model/FeishuMessage.php b/backend/app/model/FeishuMessage.php new file mode 100644 index 0000000..7cf98fd --- /dev/null +++ b/backend/app/model/FeishuMessage.php @@ -0,0 +1,72 @@ +where('node_id', $nodeId) + ->where('status', self::STATUS_ACTIVE) + ->get() + ->toArray(); + } + + /** + * 保存消息映射 + */ + public static function saveMessage( + int $orderId, + int $nodeId, + int $userId, + string $messageId, + ?string $msgToken = null + ): self { + return self::updateOrCreate( + [ + 'order_id' => $orderId, + 'node_id' => $nodeId, + 'user_id' => $userId, + ], + [ + 'message_id' => $messageId, + 'msg_token' => $msgToken, + 'status' => self::STATUS_ACTIVE, + ] + ); + } + + /** + * 标记消息为已更新 + */ + public static function markAsUpdated(int $orderId, int $nodeId): bool + { + return self::where('order_id', $orderId) + ->where('node_id', $nodeId) + ->update(['status' => self::STATUS_UPDATED]) > 0; + } +} diff --git a/backend/app/service/ApprovalService.php b/backend/app/service/ApprovalService.php index 221c184..65c8a22 100644 --- a/backend/app/service/ApprovalService.php +++ b/backend/app/service/ApprovalService.php @@ -111,6 +111,22 @@ class ApprovalService // 判断节点是否完成 if ($this->isNodeCompleted($order->id, $currentNode->id)) { + // 对于或签和单人审批,更新其他审批人的消息卡片 + if ($currentNode->type === ApprovalNode::TYPE_OR_APPROVE || $currentNode->type === 'approver') { + $approver = User::find($approverId); + $approverName = $approver ? $approver->name : '未知'; + + if ($this->notificationService) { + $this->notificationService->updateMessageCards( + $order->id, + $currentNode->id, + $approverId, + 'approved', + $approverName + ); + } + } + // 移动到下一节点 $nextNodes = $this->getNextNodes($order->flow, $currentNode->order); @@ -178,6 +194,20 @@ class ApprovalService 'comment' => $comment, ]); + // 更新其他审批人的消息卡片(拒绝时所有人都需要更新) + $approver = User::find($approverId); + $approverName = $approver ? $approver->name : '未知'; + + if ($this->notificationService && $currentNode) { + $this->notificationService->updateMessageCards( + $order->id, + $currentNode->id, + $approverId, + 'rejected', + $approverName + ); + } + // 更新状态为已拒绝 $order->status = ApprovalOrder::STATUS_REJECTED; $order->current_nodes = []; @@ -498,6 +528,9 @@ class ApprovalService error_log("🔔 通知下一节点审批人: order_id={$order->id}, approverIds=" . json_encode($approverIds)); + // 获取第一个下一节点的 ID(用于消息映射) + $nextNodeId = $nextNodes[0]['id'] ?? null; + // 为每个审批人发送通知 foreach ($approverIds as $approverId) { try { @@ -511,6 +544,7 @@ class ApprovalService 'content' => "**申请标题**: {$order->title}\n**申请人**: {$order->applicant->name}", 'url' => $detailUrl, 'order_id' => $order->id, + 'node_id' => $nextNodeId, // 添加节点ID用于保存消息映射 ]); error_log("✅ 审批人通知成功: approver_id={$approverId}"); } catch (\Exception $e) { diff --git a/backend/app/service/BusinessTripService.php b/backend/app/service/BusinessTripService.php index 15eacdf..b08a6a5 100644 --- a/backend/app/service/BusinessTripService.php +++ b/backend/app/service/BusinessTripService.php @@ -318,6 +318,7 @@ class BusinessTripService 'content' => $content, 'url' => $approvalUrl, 'order_id' => $approvalOrder->id, + 'node_id' => $node->id, // 添加节点ID用于保存消息映射 ]); error_log("✅ 审批人通知成功: approver_id={$approverId}"); } catch (\Exception $e) { diff --git a/backend/app/service/FeishuNotificationService.php b/backend/app/service/FeishuNotificationService.php index 81f3c8a..ac22c5f 100644 --- a/backend/app/service/FeishuNotificationService.php +++ b/backend/app/service/FeishuNotificationService.php @@ -119,14 +119,17 @@ class FeishuNotificationService * - content: 申请内容摘要 * - url: 详情链接 * - order_id: 审批单ID(用于构建审批操作按钮) + * - node_id: 审批节点ID(用于保存消息映射) * @return bool */ public function sendApprovalNotification(array $params): bool { $userId = $params['user_id'] ?? null; $applicantId = $params['applicant_id'] ?? null; + $orderId = $params['order_id'] ?? null; + $nodeId = $params['node_id'] ?? null; - error_log("📧 sendApprovalNotification 被调用: user_id={$userId}, applicant_id=" . ($applicantId ?? 'null')); + error_log("📧 sendApprovalNotification 被调用: user_id={$userId}, applicant_id=" . ($applicantId ?? 'null') . ", order_id={$orderId}, node_id={$nodeId}"); if (!$userId) { error_log('❌ sendApprovalNotification: 缺少 user_id'); @@ -157,7 +160,15 @@ class FeishuNotificationService error_log("📨 发送卡片消息: 不是自审批"); $card = $this->buildApprovalCard($params, $user->company_id); - return $this->sendMessage($feishuUserId, $card); + $messageId = $this->sendMessage($feishuUserId, $card); + + 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}"); + } + + return $messageId !== false; } catch (Exception $e) { error_log('❌ sendApprovalNotification 失败: ' . $e->getMessage()); return false; @@ -539,26 +550,19 @@ class FeishuNotificationService /** * 发送消息到飞书 + * @return string|false 返回 message_id 或 false */ - private function sendMessage(string $feishuUserId, array $card): bool + private function sendMessage(string $feishuUserId, array $card) { try { $app = $this->getAppConfig(); $token = $this->getAccessToken(); - $body = [ - 'receive_id_type' => 'user_id', - 'receive_id' => $feishuUserId, - 'msg_type' => 'interactive', - 'content' => json_encode(['card' => $card]), - ]; + $content = json_encode(['card' => $card]); error_log('📤 发送飞书消息: app_id=' . $app['app_id'] . ', feishu_user_id=' . $feishuUserId); error_log('📦 消息卡片: ' . json_encode($card, JSON_UNESCAPED_UNICODE)); - $content = json_encode($card); - error_log('📤 发送的content: ' . $content); - $response = HttpClient::httpPost( "https://open.feishu.cn/open-apis/im/v1/messages?receive_id_type=user_id", [ @@ -576,17 +580,108 @@ class FeishuNotificationService if ($code !== 0) { error_log('❌ 飞书消息发送失败: ' . json_encode($response)); - } else { - error_log('✅ 飞书消息发送成功'); + return false; } - return $code === 0; + $messageId = $response['data']['message_id'] ?? null; + if ($messageId) { + error_log('✅ 飞书消息发送成功, message_id=' . $messageId); + return $messageId; + } + + return false; } catch (Exception $e) { error_log('❌ sendMessage 异常: ' . $e->getMessage()); return false; } } + /** + * 更新消息卡片(用于或签/单人审批完成后,更新其他审批人的消息) + * + * @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 { + // 获取该节点的所有消息(排除已操作的用户) + $messages = \app\model\FeishuMessage::where('order_id', $orderId) + ->where('node_id', $nodeId) + ->where('user_id', '!=', $excludeUserId) + ->where('status', \app\model\FeishuMessage::STATUS_ACTIVE) + ->get(); + + if ($messages->isEmpty()) { + error_log("📭 没有需要更新的消息: order_id={$orderId}, node_id={$nodeId}"); + return true; + } + + $token = $this->getAccessToken(); + $statusText = $status === 'approved' ? '已通过' : '已拒绝'; + $statusColor = $status === 'approved' ? 'green' : 'red'; + + foreach ($messages as $message) { + try { + // 构建更新后的卡片(移除操作按钮) + $updatedCard = [ + 'config' => [ + 'wide_screen_mode' => false, + ], + 'header' => [ + 'title' => [ + 'content' => '📋 审批已完成', + 'tag' => 'plain_text', + ], + 'template' => $statusColor, + ], + 'elements' => [ + [ + 'tag' => 'div', + 'text' => [ + 'content' => "**操作人**: {$operatorName}\n**操作结果**: {$statusText}\n\n此审批已被其他审批人处理,无需重复操作。", + 'tag' => 'lark_md', + ], + ], + ], + ]; + + $response = HttpClient::httpPost( + "https://open.feishu.cn/open-apis/im/v1/messages/{$message->message_id}/update", + [ + 'msg_type' => 'interactive', + 'content' => json_encode(['card' => $updatedCard]), + ], + [ + 'Authorization: Bearer ' . $token, + ] + ); + + $code = $response['code'] ?? -1; + if ($code === 0) { + error_log("✅ 消息卡片更新成功: message_id={$message->message_id}"); + // 标记消息为已更新 + $message->status = \app\model\FeishuMessage::STATUS_UPDATED; + $message->save(); + } else { + error_log("❌ 消息卡片更新失败: message_id={$message->message_id}, error=" . json_encode($response)); + } + } catch (Exception $e) { + error_log("❌ 更新消息卡片异常: message_id={$message->message_id}, error=" . $e->getMessage()); + } + } + + return true; + } catch (Exception $e) { + error_log('❌ updateMessageCards 异常: ' . $e->getMessage()); + return false; + } + } + /** * 发送文本消息(用于自审批场景) */ diff --git a/backend/migrations/20250228_create_feishu_message_table.php b/backend/migrations/20250228_create_feishu_message_table.php new file mode 100644 index 0000000..c6a94f0 --- /dev/null +++ b/backend/migrations/20250228_create_feishu_message_table.php @@ -0,0 +1,32 @@ +