fix: 修复审批详情显示实际审批人和飞书消息emoji编码

1. 审批详情显示修复
   - 新增 ApprovalService::getDetail() 方法
   - 从 form_data 获取用户实际选择的审批人和抄送人
   - 返回每个节点的 actual_approvers 和 approver_statuses
   - 修复详情页显示默认审批人而非实际审批人的问题

2. 飞书消息emoji编码修复
   - 修复 content 字段格式:应为 card 对象JSON字符串,不是 {"card":{...}}
   - 使用 JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES
   - 新增 HttpClient::httpPostJson() 方法处理预编码字段
   - 修复消息发送错误码 200621

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
root
2026-03-01 10:02:53 +08:00
parent 8403d24be3
commit 3ebbbefb8d
4 changed files with 209 additions and 12 deletions
+102
View File
@@ -252,6 +252,108 @@ class ApprovalService
return $order->fresh(['flow', 'applicant', 'records']);
}
/**
* 获取审批详情(包含实际审批人信息)
*/
public function getDetail(int $orderId): array
{
$order = ApprovalOrder::with(['flow.nodes', 'applicant', 'records.approver'])
->findOrFail($orderId);
// 获取 form_data 中的实际审批人和抄送人
$formData = $order->form_data ?? [];
$actualApprovers = $formData['approvers'] ?? [];
$actualCcUsers = $formData['cc_users'] ?? [];
// 兼容两种格式:对象数组 [{id:1, name:'xxx'}] 或纯ID数组 [1, 2, 3]
if (!empty($actualApprovers) && isset($actualApprovers[0]['id'])) {
$actualApproverIds = array_column($actualApprovers, 'id');
} else {
$actualApproverIds = $actualApprovers;
}
if (!empty($actualCcUsers) && isset($actualCcUsers[0]['id'])) {
$actualCcUserIds = array_column($actualCcUsers, 'id');
} else {
$actualCcUserIds = $actualCcUsers;
}
// 获取实际审批人的完整用户信息
$actualApproverUsers = [];
if (!empty($actualApproverIds)) {
$actualApproverUsers = User::whereIn('id', $actualApproverIds)
->get()
->keyBy('id')
->toArray();
}
// 获取实际抄送人的完整用户信息
$actualCcUserObjects = [];
if (!empty($actualCcUserIds)) {
$actualCcUserObjects = User::whereIn('id', $actualCcUserIds)
->get()
->keyBy('id')
->toArray();
}
// 处理节点信息,添加实际审批人
$nodes = [];
if ($order->flow && $order->flow->nodes) {
foreach ($order->flow->nodes as $node) {
$nodeArray = $node->toArray();
// 为审批节点添加实际审批人信息
if (in_array($node->type, ['approver', 'and_approve', 'or_approve'])) {
$nodeArray['actual_approvers'] = [];
foreach ($actualApproverIds as $approverId) {
if (isset($actualApproverUsers[$approverId])) {
$nodeArray['actual_approvers'][] = $actualApproverUsers[$approverId];
}
}
// 获取每个实际审批人的审批状态
$nodeArray['approver_statuses'] = [];
foreach ($actualApproverIds as $approverId) {
$record = ApprovalRecord::where('order_id', $orderId)
->where('node_id', $node->id)
->where('approver_id', $approverId)
->whereIn('action', ['approve', 'reject'])
->first();
$nodeArray['approver_statuses'][] = [
'user_id' => $approverId,
'user' => $actualApproverUsers[$approverId] ?? null,
'action' => $record->action ?? null,
'action_name' => $record ? ($record->action === 'approve' ? '已通过' : '已拒绝') : '待审批',
'comment' => $record->comment ?? null,
'created_at' => $record->created_at ?? null,
];
}
}
// 为通知节点添加实际抄送人
if ($node->type === 'notify') {
$nodeArray['actual_cc_users'] = [];
foreach ($actualCcUserIds as $ccUserId) {
if (isset($actualCcUserObjects[$ccUserId])) {
$nodeArray['actual_cc_users'][] = $actualCcUserObjects[$ccUserId];
}
}
}
$nodes[] = $nodeArray;
}
}
// 构建返回数据
$result = $order->toArray();
$result['flow']['nodes'] = $nodes;
$result['actual_approvers'] = array_values($actualApproverUsers);
$result['actual_cc_users'] = array_values($actualCcUserObjects);
return $result;
}
/**
* 获取审批列表
*/
@@ -566,13 +566,19 @@ class FeishuNotificationService
error_log('📤 发送飞书消息: app_id=' . $app['app_id'] . ', feishu_user_id=' . $feishuUserId);
error_log('📦 消息卡片: ' . json_encode($card, JSON_UNESCAPED_UNICODE));
$response = HttpClient::httpPost(
// content 必须是 card 对象的 JSON 字符串,不是 {"card":{...}}
// JSON_UNESCAPED_UNICODE 保留 emoji
// JSON_UNESCAPED_SLASHES 避免 URL 中的 / 被转义为 \/
$contentJson = json_encode($card, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
$response = HttpClient::httpPostJson(
"https://open.feishu.cn/open-apis/im/v1/messages?receive_id_type=user_id",
[
'receive_id' => $feishuUserId,
'msg_type' => 'interactive',
'content' => ['card' => $card], // 直接传递数组,让 HttpClient 进行编码
'content' => $contentJson,
],
['content'], // content 字段是预编码的 JSON 字符串
[
'Authorization: Bearer ' . $token,
]
@@ -707,13 +713,18 @@ class FeishuNotificationService
'operator' => $operatorName,
];
$response = HttpClient::httpPost(
// 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",
[
'msg_type' => 'interactive',
// 直接传递数组,让 HttpClient 进行编码
'content' => ['card' => $updatedCard],
'content' => $contentJson,
],
['content'], // content 字段是预编码的 JSON 字符串
[
'Authorization: Bearer ' . $token,
]
@@ -826,15 +837,18 @@ class FeishuNotificationService
$apiUrl = "https://open.feishu.cn/open-apis/im/v1/messages?receive_id_type=user_id";
// content 必须是 JSON 字符串,使用 httpPostJson 避免二次编码
$contentJson = json_encode(['text' => $text], JSON_UNESCAPED_UNICODE);
$requestData = [
'receive_id' => $feishuUserId,
'msg_type' => 'text',
'content' => json_encode(['text' => $text]),
'content' => $contentJson,
];
error_log('📤 发送文本消息: feishu_user_id=' . $feishuUserId . ', text=' . substr($text, 0, 100));
$responseData = \app\util\HttpClient::httpPost($apiUrl, $requestData, [
$responseData = \app\util\HttpClient::httpPostJson($apiUrl, $requestData, ['content'], [
'Authorization: Bearer ' . $accessToken,
]);