false, 'error' => [ 'type' => 'fatal', 'message' => 'Internal error' ] ], 500); } }); } // Инициализация CRM require_once 'config.inc.php'; require_once 'include/utils/utils.php'; require_once 'includes/Loader.php'; vimport('includes.runtime.Globals'); require_once 'include/database/PearDatabase.php'; require_once 'modules/Users/Users.php'; require_once 'include/Webservices/Utils.php'; require_once 'include/Webservices/Create.php'; require_once 'include/Webservices/Login.php'; require_once 'include/Webservices/AuthToken.php'; require_once 'include/Webservices/AddRelated.php'; require_once 'data/CRMEntity.php'; require_once 'modules/Vtiger/CRMEntity.php'; $adb = PearDatabase::getInstance(); // Вспомогательные функции function getUserWsPrefix() { global $adb; $rs = $adb->pquery("SELECT id FROM vtiger_ws_entity WHERE name=?", ['Users']); return ($rs && $adb->num_rows($rs) > 0) ? $adb->query_result($rs, 0, 'id') : 19; } function getProjectWsIdFromDB($projectId) { global $adb; $rs = $adb->pquery("SELECT id FROM vtiger_ws_entity WHERE name=?", ['Project']); return ($rs && $adb->num_rows($rs) > 0) ? $adb->query_result($rs, 0, 'id') . 'x' . (int)$projectId : null; } function getDocumentFoldersWsPrefix() { global $adb; $rs = $adb->pquery("SELECT id FROM vtiger_ws_entity WHERE name=?", ['DocumentFolders']); return ($rs && $adb->num_rows($rs) > 0) ? (int)$adb->query_result($rs, 0, 'id') : 22; } function getFolderWsIdByName($folderName) { global $adb; $rs = $adb->pquery('SELECT folderid FROM vtiger_attachmentsfolder WHERE foldername = ? LIMIT 1', [$folderName]); if ($rs && $adb->num_rows($rs) > 0) { $folderId = (int)$adb->query_result($rs, 0, 'folderid'); $prefix = getDocumentFoldersWsPrefix(); return $prefix . 'x' . $folderId; } return null; } /** * Извлекает S3 метаданные из URL */ function parseS3Url($fileUrl) { $parsed = parse_url($fileUrl); if (!$parsed || !isset($parsed['host']) || !isset($parsed['path'])) { return null; } $path = ltrim($parsed['path'], '/'); $pathParts = explode('/', $path, 2); if (count($pathParts) < 2) { return null; } return [ 'bucket' => $pathParts[0], 'key' => $pathParts[1], 'host' => $parsed['host'] ]; } /** * Получает размер файла из S3 */ function getS3FileSize($fileUrl) { try { $ch = curl_init(); curl_setopt($ch, CURLOPT_URL, $fileUrl); curl_setopt($ch, CURLOPT_NOBODY, true); curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); curl_setopt($ch, CURLOPT_HEADER, true); curl_setopt($ch, CURLOPT_TIMEOUT, 10); curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true); $response = curl_exec($ch); $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE); curl_close($ch); if ($httpCode === 200 && $response) { if (preg_match('/Content-Length:\s*(\d+)/i', $response, $matches)) { return (int)$matches[1]; } } return 0; } catch (Exception $e) { writeLog("Ошибка получения размера файла: " . $e->getMessage(), 'ERROR'); return 0; } } /** * Обновляет документ в базе данных напрямую с S3 метаданными */ function updateDocumentS3Metadata($documentId, $s3Info, $fileSize, $originalFileName) { global $adb; try { writeLog("Обновляем S3 метаданные для документа $documentId", 'DEBUG'); writeLog("S3 Bucket: {$s3Info['bucket']}, Key: {$s3Info['key']}, Size: $fileSize", 'DEBUG'); $updateQuery = " UPDATE vtiger_notes SET filename = ?, filelocationtype = 'E', filesize = ?, s3_bucket = ?, s3_key = ?, s3_etag = '' WHERE notesid = ? "; // Создаем полный S3 URL для отображения в CRM $s3Url = "https://s3.twcstorage.ru/{$s3Info['bucket']}/{$s3Info['key']}"; $params = [ $s3Url, // Используем полный S3 URL вместо оригинального имени файла $fileSize, $s3Info['bucket'], $s3Info['key'], $documentId ]; writeLog("SQL: $updateQuery", 'DEBUG'); writeLog("Params: " . json_encode($params), 'DEBUG'); $result = $adb->pquery($updateQuery, $params); if ($result) { writeLog("✅ S3 метаданные успешно обновлены для документа $documentId", 'SUCCESS'); return true; } else { writeLog("❌ Ошибка обновления S3 метаданных: " . $adb->database->errorMsg(), 'ERROR'); return false; } } catch (Exception $e) { writeLog("❌ Исключение при обновлении S3 метаданных: " . $e->getMessage(), 'ERROR'); return false; } } // 🚀 УЛУЧШЕННАЯ ФУНКЦИЯ: Создание документов с правильными S3 метаданными function createDocumentsWithSession($data) { global $adb, $current_user; $sessionName = $data['sessionName']; $projectId = (int)$data['projectid']; $contactId = (int)$data['contactid']; $userId = (int)($data['user_id'] ?? 1); $filesArray = $data['files'] ?? []; writeLog('🚀 Начинаем создание документов с правильными S3 метаданными', 'INFO'); writeLog("📋 Проект: $projectId, Контакт: $contactId, Пользователь: $userId", 'INFO'); writeLog('📋 Файлов к обработке: ' . count($filesArray), 'INFO'); // Упрощенный подход: используем только webservice API без обращений к БД $projectWsId = '20x' . $projectId; // Предполагаем стандартный префикс для Project $assignedUserWsId = '19x' . $userId; // Предполагаем стандартный префикс для Users $folderWsId = '22x1'; // Default папка writeLog("✅ Project WS ID: $projectWsId", 'DEBUG'); writeLog("👤 User WS ID: $assignedUserWsId", 'DEBUG'); writeLog("📁 Папка: $folderWsId", 'DEBUG'); // Инициализируем CURL $ch = curl_init(); curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false); curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, 2); curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1); curl_setopt($ch, CURLOPT_URL, 'https://crm.clientright.ru/webservice.php'); curl_setopt($ch, CURLOPT_POST, 1); $results = []; foreach ($filesArray as $i => $file) { $fileName = $file['original_file_name'] ?? $file['file_name'] ?? 'Unknown'; writeLog("📄 Обрабатываем файл #{$i}: $fileName", 'INFO'); try { // Проверяем обязательные поля if (empty($file['file_url'])) { throw new Exception("Отсутствует file_url"); } // Парсим S3 URL для получения метаданных $s3Info = parseS3Url($file['file_url']); if (!$s3Info) { throw new Exception("Не удалось распарсить S3 URL: " . $file['file_url']); } writeLog("🔍 S3 Bucket: {$s3Info['bucket']}, Key: {$s3Info['key']}", 'DEBUG'); // Получаем размер файла из S3 $fileSize = getS3FileSize($file['file_url']); writeLog("📏 Размер файла: " . number_format($fileSize) . " байт", 'DEBUG'); // Определяем название и описание документа $documentTitle = $file['upload_description'] ?? $file['original_file_name'] ?? $file['file_name'] ?? 'Документ'; $documentContent = sprintf( 'Загружено через n8n. ID: %s, Контакт: %d, Проект: %d, Загружено: %s', $file['id'] ?? 'N/A', $contactId, $projectId, $file['uploaded_at'] ?? date('Y-m-d H:i:s') ); // Создаём документ БЕЗ S3 метаданных (webservice не поддерживает их) $docElement = [ 'notes_title' => $documentTitle, 'filename' => $file['original_file_name'] ?? $file['file_name'], // Оригинальное имя файла 'assigned_user_id' => $assignedUserWsId, 'notecontent' => $documentContent, 'filetype' => 'application/pdf', 'filesize' => (string)$fileSize, 'filelocationtype' => 'E', // External URL 'fileversion' => '1.0', 'filestatus' => '1', // Active 'folderid' => $folderWsId, ]; writeLog("📤 Создаём документ '$documentTitle' через webservice", 'INFO'); writeLog("📤 Элемент документа: " . json_encode($docElement, JSON_UNESCAPED_UNICODE), 'DEBUG'); // Отправляем запрос на создание документа curl_setopt($ch, CURLOPT_POSTFIELDS, [ 'operation' => 'create', 'sessionName' => $sessionName, 'elementType' => 'Documents', 'element' => json_encode($docElement, JSON_UNESCAPED_UNICODE), ]); $resp = curl_exec($ch); if ($resp === false) { throw new Exception('CURL error: ' . curl_error($ch)); } $resp = ltrim($resp, "\xEF\xBB\xBF\x00\x09\x0A\x0D\x20"); writeLog("📥 Ответ создания документа: " . substr($resp, 0, 500), 'DEBUG'); $doc = json_decode($resp, true); if (!$doc || !$doc['success'] || empty($doc['result']['id'])) { throw new Exception('Failed to create document: ' . substr($resp, 0, 200)); } $documentWsId = $doc['result']['id']; list(, $docNumericId) = explode('x', $documentWsId, 2); writeLog("✅ Документ создан: $documentWsId (numeric: $docNumericId)", 'SUCCESS'); // ВАЖНО: Обновляем S3 метаданные напрямую в базе данных $s3UpdateSuccess = updateDocumentS3Metadata($docNumericId, $s3Info, $fileSize, $file['original_file_name'] ?? $file['file_name']); // Привязываем к проекту writeLog("🔗 Привязываем документ $documentWsId к проекту $projectWsId", 'INFO'); curl_setopt($ch, CURLOPT_POSTFIELDS, [ 'operation' => 'AddRelated', 'sessionName' => $sessionName, 'sourceRecordId' => $projectWsId, 'relatedRecordId' => $documentWsId, ]); $resp = curl_exec($ch); $relationOk = false; if ($resp !== false) { $resp = ltrim($resp, "\xEF\xBB\xBF\x00\x09\x0A\x0D\x20"); writeLog("📥 Ответ AddRelated: " . substr($resp, 0, 200), 'DEBUG'); $rel = json_decode($resp, true); $relationOk = isset($rel['result']['message']) && $rel['result']['message'] === 'successfull'; } // Если webservice не сработал - используем прямую привязку if (!$relationOk) { writeLog("⚠️ AddRelated не сработал, используем прямую привязку", 'WARNING'); try { // Устанавливаем current_user для CRMEntity if (!isset($current_user) || !$current_user) { $current_user = new Users(); $current_user->retrieveCurrentUserInfoFromFile($userId); } $focus = CRMEntity::getInstance('Project'); relateEntities($focus, 'Project', $projectId, 'Documents', (int)$docNumericId); writeLog("✅ Прямая привязка успешна", 'SUCCESS'); } catch (Exception $directRelationError) { writeLog("❌ Прямая привязка не удалась: " . $directRelationError->getMessage(), 'ERROR'); } } else { writeLog("✅ AddRelated успешен", 'SUCCESS'); } // Возвращаем результат с сохранением всех исходных данных $result = array_merge($file, [ 'status' => 'success', 'projectid' => $projectId, 'contactid' => $contactId, 'crm_result' => [ 'document_id' => $documentWsId, 'document_numeric_id' => $docNumericId, 'project_id' => $projectId, 'contact_id' => $contactId, 'folder_id' => $folderWsId, 's3_bucket' => $s3Info['bucket'], 's3_key' => $s3Info['key'], 'file_size' => $fileSize, 's3_metadata_updated' => $s3UpdateSuccess, 'message' => 'Документ создан с правильными S3 метаданными и привязан к проекту' . (!$relationOk ? ' (прямая привязка)' : '') ] ]); $results[] = $result; writeLog("✅ Файл '$fileName' успешно обработан с S3 метаданными", 'SUCCESS'); } catch (Exception $e) { writeLog("❌ Ошибка для файла '$fileName': " . $e->getMessage(), 'ERROR'); $results[] = array_merge($file, [ 'status' => 'error', 'projectid' => $projectId, 'contactid' => $contactId, 'crm_result' => [ 'message' => $e->getMessage() ] ]); } } curl_close($ch); $successCount = count(array_filter($results, function($r) { return $r['status'] === 'success'; })); writeLog("🏁 Обработка завершена. Успешно: $successCount/" . count($results), 'INFO'); return $results; } // Обработка запроса if ($IS_POST) { writeLog('=== START POST REQUEST V2 (DETAILED LOGGING) ===', 'INFO'); writeLog('Headers: ' . json_encode(getallheaders(), JSON_UNESCAPED_UNICODE), 'DEBUG'); // Получаем и проверяем входные данные $input = file_get_contents('php://input'); writeLog('Raw input: ' . substr($input, 0, 1000) . (strlen($input) > 1000 ? '...(truncated)' : ''), 'DEBUG'); $input = ltrim($input, "\xEF\xBB\xBF\x00\x09\x0A\x0D\x20"); $data = json_decode($input, true); if (json_last_error() !== JSON_ERROR_NONE) { writeLog('❌ JSON Error: ' . json_last_error_msg(), 'ERROR'); json_response([ 'success' => false, 'error' => ['message' => 'Invalid JSON: ' . json_last_error_msg()] ], 400); } writeLog('Parsed data keys: ' . implode(', ', array_keys($data)), 'DEBUG'); // Проверяем обязательные поля if (empty($data['sessionName'])) { writeLog('❌ Error: sessionName is required', 'ERROR'); json_response([ 'success' => false, 'error' => ['message' => 'sessionName is required in request data'] ], 400); } if (empty($data['projectid'])) { writeLog('❌ Error: projectid is required', 'ERROR'); json_response([ 'success' => false, 'error' => ['message' => 'projectid is required in request data'] ], 400); } if (empty($data['contactid'])) { writeLog('❌ Error: contactid is required', 'ERROR'); json_response([ 'success' => false, 'error' => ['message' => 'contactid is required in request data'] ], 400); } // Поддерживаем оба формата: files и documents $filesArray = null; if (!empty($data['files']) && is_array($data['files'])) { $filesArray = $data['files']; } elseif (!empty($data['documents']) && is_array($data['documents'])) { $filesArray = $data['documents']; } if (empty($filesArray)) { writeLog('❌ Error: files or documents array is required', 'ERROR'); json_response([ 'success' => false, 'error' => ['message' => 'files or documents array is required in request data'] ], 400); } writeLog("🔑 Сессия: {$data['sessionName']}", 'INFO'); writeLog("📋 Проект: {$data['projectid']}, Контакт: {$data['contactid']}", 'INFO'); writeLog('📄 Файлов: ' . count($filesArray), 'INFO'); // Нормализуем данные: всегда используем 'files' внутри функции $normalizedData = $data; $normalizedData['files'] = $filesArray; // Создаём документы с правильными S3 метаданными $results = createDocumentsWithSession($normalizedData); // Успешный ответ writeLog('✅ Success: processed ' . count($results) . ' files with S3 metadata', 'SUCCESS'); json_response([ 'success' => true, 'total_processed' => count($filesArray), 'results' => $results, 'session_used' => $sessionName ]); } else { // GET запрос - документация API header('Content-Type: application/json; charset=utf-8'); echo json_encode([ 'success' => true, 'message' => 'Upload Documents API v2 - Detailed Logging Version', 'endpoint' => 'POST to this URL with complete data object', 'logging' => 'Detailed logs available in logs/upload_documents_v2_detailed.log', 'format' => [ 'sessionName' => 'string (required - from n8n CRM login)', 'projectid' => 'string (required - project ID)', 'contactid' => 'string (required - contact ID)', 'user_id' => 'string (optional - default "1")', 'files' => [ // или 'documents' - поддерживаются оба формата [ 'id' => 'string (file UUID)', 'file_id' => 'string (S3 path)', 'file_url' => 'string (required - full S3 URL)', 'file_name' => 'string (S3 filename)', 'field_name' => 'string (form field)', 'uploaded_at' => 'string (ISO datetime)', 'original_file_name' => 'string (user filename)', 'upload_description' => 'string (document description)', 'filename_for_upload' => 'string (display filename)' ] ] ], 'changes' => [ 'Added detailed logging with levels (INFO, DEBUG, SUCCESS, WARNING, ERROR)', 'Fixed S3 metadata handling via direct database update', 'Proper filelocationtype=E setting', 'Correct filename field usage', 'Added S3 bucket and key extraction', 'Added file size detection from S3', 'Added s3_metadata_updated flag in response' ] ], JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT); } ?>