Files
crm.clientright.ru/upload_documents_to_crm.php
Fedor 5f4f99245d feat: Добавлена поддержка привязки документов к HelpDesk
Изменения в upload_documents_to_crm.php:
 Добавлена логика привязки к HelpDesk (заявке) если указан ticket_id
 Если ticket_id не указан - привязываем к Project (старая логика)
 Обновлена функция normalizeInputData для передачи ticket_id
 Добавлено логирование привязки к HelpDesk/Project

Использование:
- Передать ticket_id в payload → документ привязывается к HelpDesk
- Не передавать ticket_id → документ привязывается к Project
2025-11-02 19:05:46 +03:00

644 lines
27 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<?php
error_reporting(E_ALL);
ini_set('display_errors', '1');
// Для POST запросов отключаем вывод ошибок в HTML
$IS_POST = ($_SERVER['REQUEST_METHOD'] === 'POST');
if ($IS_POST) {
ini_set('display_errors', '0');
ini_set('display_startup_errors', '0');
}
// Функция для записи в лог
function writeLog($message) {
$timestamp = date('Y-m-d H:i:s');
$line = "[$timestamp] $message\n";
// Пробуем писать в основной лог
@file_put_contents(__DIR__ . '/logs/upload_documents.log', $line, FILE_APPEND | LOCK_EX);
// И в /tmp как запасной вариант
@file_put_contents('/tmp/upload_documents.log', $line, FILE_APPEND | LOCK_EX);
// И в системный лог
error_log('[upload_documents] ' . $message);
}
// Функция для JSON ответа
function json_response($data, $code = 200) {
if (!headers_sent()) {
http_response_code($code);
header('Content-Type: application/json; charset=utf-8');
}
echo json_encode($data, JSON_UNESCAPED_UNICODE);
exit;
}
// Быстрый ping
if (isset($_GET['ping'])) {
header('Content-Type: text/plain; charset=utf-8');
echo 'pong';
exit;
}
// Для POST запросов регистрируем обработчик фатальных ошибок
if ($IS_POST) {
register_shutdown_function(function() {
$error = error_get_last();
if ($error && in_array($error['type'], [E_ERROR, E_PARSE, E_CORE_ERROR, E_COMPILE_ERROR])) {
writeLog('FATAL: ' . $error['message'] . ' in ' . $error['file'] . ':' . $error['line']);
json_response([
'success' => 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';
$adb = PearDatabase::getInstance();
// Вспомогательные функции из test_ws_documents.php
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());
return 0;
}
}
/**
* Обновляет документ в базе данных напрямую с S3 метаданными
*/
function updateDocumentS3Metadata($documentId, $s3Info, $fileSize, $originalFileName) {
global $adb;
try {
writeLog("Обновляем S3 метаданные для документа $documentId");
writeLog("S3 Bucket: {$s3Info['bucket']}, Key: {$s3Info['key']}, Size: $fileSize");
$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']}";
// Очищаем URL от HTML тегов (защита от <br> и других тегов)
$s3Url = strip_tags(trim($s3Url));
$params = [
$s3Url, // Используем полный S3 URL вместо оригинального имени файла
$fileSize,
$s3Info['bucket'],
$s3Info['key'],
$documentId
];
writeLog("SQL: $updateQuery");
writeLog("Params: " . json_encode($params));
$result = $adb->pquery($updateQuery, $params);
if ($result) {
writeLog("✅ S3 метаданные успешно обновлены для документа $documentId");
return true;
} else {
writeLog("❌ Ошибка обновления S3 метаданных: " . $adb->database->errorMsg());
return false;
}
} catch (Exception $e) {
writeLog("❌ Исключение при обновлении S3 метаданных: " . $e->getMessage());
return false;
}
}
// Функция нормализации входных данных
function normalizeInputData($data) {
writeLog('🔍 Нормализация входных данных...');
// Новый формат: {"documents": [...], "projectid": "..."}
if (isset($data['documents']) && isset($data['projectid'])) {
writeLog('📋 Обнаружен новый формат с массивом documents');
$filesArray = [];
foreach ($data['documents'] as $doc) {
// Определяем projectid - приоритет: projectid, group_session_token, 0
$projectid = null;
if (!empty($data['projectid'])) {
$projectid = (int)$data['projectid'];
} elseif (!empty($data['group_session_token']) && is_numeric($data['group_session_token'])) {
$projectid = (int)$data['group_session_token'];
} elseif (!empty($doc['projectid'])) {
$projectid = (int)$doc['projectid'];
} elseif (!empty($doc['group_session_token']) && is_numeric($doc['group_session_token'])) {
$projectid = (int)$doc['group_session_token'];
}
$normalizedFile = [
'url' => $doc['file_url'] ?? $doc['url'] ?? '',
'file_name' => $doc['file_name'] ?? $doc['original_file_name'] ?? '',
'description' => $doc['upload_description'] ?? $doc['description'] ?? '',
'projectid' => $projectid,
'ticket_id' => $data['ticket_id'] ?? $doc['ticket_id'] ?? null, // ✅ Поддержка привязки к заявке
'user_id' => $data['user_id'] ?? $doc['user_id'] ?? 1,
'contactid' => $doc['contactid'] ?? $data['contactid'] ?? 0,
'pages' => $doc['pages'] ?? 0,
'folder' => $doc['folder'] ?? '',
'newfile' => $doc['newfile'] ?? '',
'field_name' => $doc['field_name'] ?? '',
'prefix' => $doc['prefix'] ?? ''
];
$filesArray[] = $normalizedFile;
writeLog("✅ Нормализован файл: {$normalizedFile['file_name']} (projectid: {$projectid})");
}
return $filesArray;
}
// Старый формат: массив файлов напрямую
if (is_array($data)) {
writeLog('📋 Обнаружен массив данных');
// Проверяем, что это не объект с ключами как поля
if (isset($data['url']) || isset($data['projectid'])) {
writeLog('⚠️ Обнаружен объект с ключами полей, преобразуем в массив');
return [$data];
}
// Проверяем первый элемент массива на наличие projectid или group_session_token
if (!empty($data) && is_array($data[0])) {
$firstItem = $data[0];
// Если projectid равен null, но есть group_session_token - используем его
if ((!isset($firstItem['projectid']) || $firstItem['projectid'] === null) &&
!empty($firstItem['group_session_token']) && is_numeric($firstItem['group_session_token'])) {
writeLog('🔄 Обнаружен projectid=null, используем group_session_token как projectid');
// Нормализуем каждый элемент массива
$normalizedArray = [];
foreach ($data as $item) {
$normalizedItem = $item;
$normalizedItem['projectid'] = (int)$item['group_session_token'];
$normalizedArray[] = $normalizedItem;
writeLog("✅ Нормализован файл: {$item['file_name']} (projectid: {$normalizedItem['projectid']})");
}
return $normalizedArray;
}
}
return $data;
}
// Одиночный объект
if (is_object($data)) {
writeLog('📋 Обнаружен одиночный объект файла');
return [$data];
}
writeLog('❌ Неизвестный формат данных');
return [];
}
// Основная функция для создания документов
function createDocumentsInCRM($filesArray, $userName = 'api') {
global $adb, $current_user;
writeLog('🚀 Начинаем создание документов...');
// 1. Получаем access key пользователя
$rs = $adb->pquery('SELECT id, accesskey FROM vtiger_users WHERE user_name=?', [$userName]);
if (!$rs || $adb->num_rows($rs) == 0) {
writeLog("❌ Пользователь $userName не найден");
return ['error' => "Пользователь $userName не найден"];
}
$userId = $adb->query_result($rs, 0, 'id');
$accessKey = $adb->query_result($rs, 0, 'accesskey');
// 2. Получаем challenge token
$ch = curl_init();
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, 2);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
$endpointUrl = 'https://crm.clientright.ru/webservice.php';
// getchallenge
curl_setopt($ch, CURLOPT_URL, $endpointUrl . '?operation=getchallenge&username=' . urlencode($userName));
$resp = curl_exec($ch);
if ($resp === false) {
writeLog('❌ CURL error: ' . curl_error($ch));
return ['error' => 'Network error'];
}
$resp = ltrim($resp, "\xEF\xBB\xBF\x00\x09\x0A\x0D\x20");
$challenge = json_decode($resp, true);
if (!$challenge || empty($challenge['result']['token'])) {
writeLog('❌ Invalid challenge response: ' . substr($resp, 0, 100));
return ['error' => 'Authentication failed'];
}
$token = $challenge['result']['token'];
// 3. Логинимся
$key = md5($token . $accessKey);
curl_setopt($ch, CURLOPT_URL, $endpointUrl);
curl_setopt($ch, CURLOPT_POST, 1);
curl_setopt($ch, CURLOPT_POSTFIELDS, [
'operation' => 'login',
'username' => $userName,
'accessKey' => $key,
]);
$resp = curl_exec($ch);
if ($resp === false) {
writeLog('❌ CURL error: ' . curl_error($ch));
return ['error' => 'Network error'];
}
$resp = ltrim($resp, "\xEF\xBB\xBF\x00\x09\x0A\x0D\x20");
$login = json_decode($resp, true);
if (!$login || empty($login['result']['sessionName'])) {
writeLog('❌ Login failed: ' . substr($resp, 0, 100));
return ['error' => 'Authentication failed'];
}
$sessionId = $login['result']['sessionName'];
// 4. Устанавливаем current_user для CRMEntity
$current_user = new Users();
$current_user->retrieveCurrentUserInfoFromFile((int)$userId);
// 5. Создаём документы
$results = [];
foreach ($filesArray as $i => $f) {
writeLog("📄 Обрабатываем файл #{$i}: " . ($f['file_name'] ?? 'Unknown'));
try {
// Валидация обязательных полей
if (empty($f['url'])) {
throw new Exception("Отсутствует URL файла");
}
if (empty($f['file_name'])) {
throw new Exception("Отсутствует имя файла");
}
if (empty($f['projectid'])) {
throw new Exception("Отсутствует ID проекта");
}
if (empty($f['description'])) {
$f['description'] = $f['file_name']; // Используем имя файла как описание
writeLog("⚠️ Отсутствует описание, используем имя файла");
}
// Получаем WS ID проекта
$projectWsId = getProjectWsIdFromDB((int)$f['projectid']);
if (!$projectWsId) {
throw new Exception("Не найден Project ID {$f['projectid']}");
}
// WS ID пользователя
$usersPrefix = getUserWsPrefix();
$assignedUserWsId = $usersPrefix . 'x' . (int)$f['user_id'];
// Папка (сначала Суд, потом Default)
$folderWsId = getFolderWsIdByName('Суд');
if (!$folderWsId) {
$folderWsId = '22x1';
writeLog("⚠️ Папка 'Суд' не найдена, используем Default");
}
// Парсим S3 URL для получения метаданных
$s3Info = parseS3Url($f['url']);
if (!$s3Info) {
throw new Exception("Не удалось распарсить S3 URL: " . $f['url']);
}
writeLog("🔍 S3 Bucket: {$s3Info['bucket']}, Key: {$s3Info['key']}");
// Получаем размер файла из S3
$fileSize = getS3FileSize($f['url']);
writeLog("📏 Размер файла: " . number_format($fileSize) . " байт");
// Создаём документ (webservice не поддерживает S3 метаданные напрямую)
$docElement = [
'notes_title' => $f['description'] ?: $f['file_name'],
'filename' => $f['file_name'], // Оригинальное имя файла
'assigned_user_id' => $assignedUserWsId,
'notecontent' => 'Авто из S3. Контакт: ' . ($f['contactid'] ?? '') . ' | URL: ' . $f['url'],
'filetype' => 'application/pdf',
'filesize' => (string)$fileSize,
'filelocationtype' => 'E', // External URL
'fileversion' => '1.0',
'filestatus' => '1', // Active
'folderid' => $folderWsId,
];
curl_setopt($ch, CURLOPT_POSTFIELDS, [
'operation' => 'create',
'sessionName' => $sessionId,
'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");
$doc = json_decode($resp, true);
if (!$doc || empty($doc['result']['id'])) {
throw new Exception('Failed to create document: ' . substr($resp, 0, 100));
}
$documentWsId = $doc['result']['id'];
list(, $docNumericId) = explode('x', $documentWsId, 2);
writeLog("✅ Документ создан: $documentWsId (numeric: $docNumericId)");
// ВАЖНО: Обновляем S3 метаданные напрямую в базе данных
$s3UpdateSuccess = updateDocumentS3Metadata($docNumericId, $s3Info, $fileSize, $f['file_name']);
// Определяем к чему привязываем: к HelpDesk (заявке) или к Project (проекту)
$ticketId = isset($f['ticket_id']) ? (int)$f['ticket_id'] : null;
if ($ticketId) {
// Привязываем к HelpDesk (заявке)
writeLog("📎 Привязываем к HelpDesk ticket_id: $ticketId");
// Получаем WS ID для HelpDesk
$rs = $adb->pquery("SELECT id FROM vtiger_ws_entity WHERE name=?", ['HelpDesk']);
$helpdeskPrefix = ($rs && $adb->num_rows($rs) > 0) ? $adb->query_result($rs, 0, 'id') : 17;
$ticketWsId = $helpdeskPrefix . 'x' . $ticketId;
curl_setopt($ch, CURLOPT_POSTFIELDS, [
'operation' => 'AddRelated',
'sessionName' => $sessionId,
'sourceRecordId' => $ticketWsId,
'relatedRecordId' => $documentWsId,
]);
$resp = curl_exec($ch);
// Проверяем успех AddRelated
$ok = false;
if ($resp !== false) {
$resp = ltrim($resp, "\xEF\xBB\xBF\x00\x09\x0A\x0D\x20");
$rel = json_decode($resp, true);
$ok = isset($rel['result']['message']) && $rel['result']['message'] === 'successfull';
}
// Если webservice не сработал - используем прямую привязку
if (!$ok) {
writeLog("⚠️ AddRelated к HelpDesk не сработал, используем прямую привязку");
require_once 'data/CRMEntity.php';
require_once 'modules/Vtiger/CRMEntity.php';
$focus = CRMEntity::getInstance('HelpDesk');
relateEntities($focus, 'HelpDesk', $ticketId, 'Documents', (int)$docNumericId);
}
writeLog("✅ Документ привязан к HelpDesk #$ticketId");
} else {
// Привязываем к проекту (старая логика)
writeLog("📎 Привязываем к Project project_id: {$f['projectid']}");
curl_setopt($ch, CURLOPT_POSTFIELDS, [
'operation' => 'AddRelated',
'sessionName' => $sessionId,
'sourceRecordId' => $projectWsId,
'relatedRecordId' => $documentWsId,
]);
$resp = curl_exec($ch);
// Проверяем успех AddRelated
$ok = false;
if ($resp !== false) {
$resp = ltrim($resp, "\xEF\xBB\xBF\x00\x09\x0A\x0D\x20");
$rel = json_decode($resp, true);
$ok = isset($rel['result']['message']) && $rel['result']['message'] === 'successfull';
}
// Если webservice не сработал - используем прямую привязку
if (!$ok) {
writeLog("⚠️ AddRelated не сработал, используем прямую привязку");
require_once 'data/CRMEntity.php';
require_once 'modules/Vtiger/CRMEntity.php';
list(, $docNumericId) = explode('x', $documentWsId, 2);
$focus = CRMEntity::getInstance('Project');
relateEntities($focus, 'Project', (int)$f['projectid'], 'Documents', (int)$docNumericId);
}
writeLog("✅ Документ привязан к Project #{$f['projectid']}");
}
// Возвращаем все входные данные + результат
$result = array_merge($f, [
'status' => 'success',
'crm_result' => [
'document_id' => $documentWsId,
'document_numeric_id' => $docNumericId,
'project_id' => $f['projectid'],
'folder_id' => $folderWsId,
's3_bucket' => $s3Info['bucket'],
's3_key' => $s3Info['key'],
'file_size' => $fileSize,
's3_metadata_updated' => $s3UpdateSuccess,
'message' => 'Документ создан с правильными S3 метаданными и привязан к проекту' . (!$ok ? ' (прямая привязка)' : '')
]
]);
$results[] = $result;
writeLog("✅ Документ {$f['file_name']} успешно создан и привязан");
} catch (Exception $e) {
writeLog("❌ Ошибка для файла {$f['file_name']}: " . $e->getMessage());
// В случае ошибки тоже возвращаем все входные данные
$results[] = array_merge($f, [
'status' => 'error',
'crm_result' => [
'message' => $e->getMessage()
]
]);
}
}
curl_close($ch);
return $results;
}
// Обработка запроса
if ($IS_POST) {
writeLog('=== START POST REQUEST ===');
writeLog('Headers: ' . json_encode(getallheaders(), JSON_UNESCAPED_UNICODE));
// Получаем и проверяем входные данные
$input = file_get_contents('php://input');
writeLog('Raw input: ' . substr($input, 0, 1000) . (strlen($input) > 1000 ? '...(truncated)' : ''));
$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());
json_response([
'success' => false,
'error' => ['message' => 'Invalid JSON: ' . json_last_error_msg()]
], 400);
}
writeLog('Parsed data: ' . json_encode($data, JSON_UNESCAPED_UNICODE));
// Нормализуем формат данных
$filesArray = normalizeInputData($data);
// Проверяем массив
if (!is_array($filesArray)) {
writeLog('❌ Error: Input is not an array');
json_response([
'success' => false,
'error' => ['message' => 'Input must be an array of files']
], 400);
}
writeLog('Processing ' . count($filesArray) . ' files...');
// Создаём документы
$results = createDocumentsInCRM($filesArray);
// Проверяем результат
if (isset($results['error'])) {
writeLog('❌ Error: ' . $results['error']);
json_response([
'success' => false,
'error' => ['message' => $results['error']]
], 500);
}
// Успешный ответ
writeLog('✅ Success: processed ' . count($results) . ' files');
json_response([
'success' => true,
'total_processed' => count($filesArray),
'results' => $results
]);
} else {
// GET запрос - тестовый режим
$testData = [
[
'session_token' => 'sess_test',
'group_session_token' => 'token_test',
'user_id' => 1,
'group' => 'group_0',
'contactid' => 120374,
'files_count' => 2,
'group_index_num' => 0,
'description' => 'Иск',
'projectid' => 354918,
'telegram_id' => 295410106,
'unified_id' => 'usr_test',
'pages' => 2,
'url' => 'https://s3.twcstorage.ru/f9825c87-4e3558f6-f9b6-405c-ad3d-d1535c49b61c/clientright/120374/1757346451126.pdf',
'file_name' => '1757346451126.pdf',
'field_name' => '120374',
'folder' => 'clientright/120374',
'newfile' => 'clientright/120374/1757346451126.pdf'
]
];
$results = createDocumentsInCRM($testData);
header('Content-Type: application/json; charset=utf-8');
echo json_encode([
'success' => true,
'total_processed' => count($testData),
'results' => $results
], JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT);
}
?>