diff --git a/CreateCourtEvent_v2.php b/CreateCourtEvent_v2.php new file mode 100644 index 00000000..30d05b75 --- /dev/null +++ b/CreateCourtEvent_v2.php @@ -0,0 +1,305 @@ +connect_error) { + throw new Exception('Не удалось подключиться к БД: ' . $mysqli->connect_error); + } + $mysqli->set_charset('utf8mb4'); + + // Получаем данные проекта + $query = "SELECT e.smownerid, p.projectname, p.linktoaccountscontacts FROM vtiger_crmentity e + JOIN vtiger_project p ON p.projectid = e.crmid + WHERE e.crmid = ? AND e.deleted = 0"; + $stmt = $mysqli->prepare($query); + $stmt->bind_param('i', $projectId); + $stmt->execute(); + $result_query = $stmt->get_result(); + + if ($result_query->num_rows === 0) { + throw new Exception("Проект $projectId не найден"); + } + + $row = $result_query->fetch_assoc(); + $ownerId = $row['smownerid']; + $projectName = $row['projectname']; + $contactId = $row['linktoaccountscontacts'] ?? null; + + log_event('DEBUG', "Владелец проекта: $ownerId, Название: $projectName, Контакт: " . ($contactId ?? 'нет')); + + // Форматируем дату и время для CRM + $formattedDate = formatDateForCRM($eventDate); + $formattedTime = formatTimeForCRM($eventTime); + $formattedDateTime = $formattedDate . ' ' . $formattedTime; + + // Вычисляем время окончания (+1 час) + $endDateTime = date('Y-m-d H:i:s', strtotime($formattedDateTime) + 3600); + + log_event('DEBUG', "Дата начала: $formattedDateTime, Дата окончания: $endDateTime"); + + // Определяем тип события и статус - используем настройки как в workflow 3 (блок 18) + $activityType = 'судебное заседание'; + $eventstatus = 'Planned'; // Запланировано (как в workflow) + + log_event('DEBUG', "Установлен тип 'судебное заседание' и статус 'Planned' (как в workflow 3)"); + + // Формируем тему события с названием проекта + $eventSubject = $eventName; + if (!empty($projectName)) { + $eventSubject = "[$projectName] $eventName"; + } + + log_event('DEBUG', "Тип события: $activityType, Статус: $eventstatus, Тема: $eventSubject"); + + // Формируем описание события + $description = "Автоматически созданное событие из судебного дела\n\n"; + + if (!empty($location)) { + $description .= "Место: $location\n"; + } + + if (!empty($result)) { + $description .= "Результат: $result\n"; + } + + if (!empty($basis)) { + $description .= "Основание: $basis\n"; + } + + if (!empty($note)) { + $description .= "Примечание: $note\n"; + } + + if (!empty($publicationDate)) { + $description .= "Дата размещения: $publicationDate\n"; + } + + // Получаем следующий ID + $result_id = $mysqli->query("SELECT MAX(crmid) as max_id FROM vtiger_crmentity"); + $row_id = $result_id->fetch_assoc(); + $eventId = ($row_id['max_id'] ?? 0) + 1; + + log_event('DEBUG', "Новый ID события: $eventId"); + + $created_time = date('Y-m-d H:i:s'); + + // Создаем запись в vtiger_crmentity + $sql = "INSERT INTO vtiger_crmentity (crmid, smcreatorid, smownerid, modifiedby, setype, description, createdtime, modifiedtime, presence, deleted, label) + VALUES (?, ?, ?, ?, 'Calendar', ?, ?, ?, 1, 0, ?)"; + $stmt = $mysqli->prepare($sql); + $stmt->bind_param('iiiissss', $eventId, $ownerId, $ownerId, $ownerId, $description, $created_time, $created_time, $eventSubject); + $stmt->execute(); + + log_event('DEBUG', "Запись в vtiger_crmentity создана"); + + // Создаем запись в vtiger_activity + $visibility = 'Public'; + + $endTime = date('H:i:s', strtotime($formattedDateTime) + 3600); + + $sql = "INSERT INTO vtiger_activity (activityid, subject, activitytype, date_start, time_start, due_date, time_end, location, visibility, eventstatus) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)"; + $stmt = $mysqli->prepare($sql); + $stmt->bind_param('isssssssss', $eventId, $eventSubject, $activityType, $formattedDate, $formattedTime, $formattedDate, + $endTime, $location, $visibility, $eventstatus); + $stmt->execute(); + + log_event('DEBUG', "Запись в vtiger_activity создана"); + + // Связываем событие с проектом (vtiger_seactivityrel) + $sql = "INSERT INTO vtiger_seactivityrel (crmid, activityid) VALUES (?, ?)"; + $stmt = $mysqli->prepare($sql); + $stmt->bind_param('ii', $projectId, $eventId); + $stmt->execute(); + + log_event('SUCCESS', "Событие привязано к проекту в vtiger_seactivityrel"); + + // Связываем событие с проектом через общую таблицу связей (vtiger_crmentityrel) + // Это ключевая связь для отображения события в интерфейсе проекта! + $sql = "INSERT INTO vtiger_crmentityrel (crmid, module, relcrmid, relmodule) VALUES (?, 'Project', ?, 'Calendar')"; + $stmt = $mysqli->prepare($sql); + $stmt->bind_param('ii', $projectId, $eventId); + $stmt->execute(); + + log_event('SUCCESS', "Событие привязано к проекту в vtiger_crmentityrel (для отображения в UI)"); + + // Связываем событие с контактом (если контакт указан в проекте) + if (!empty($contactId) && $contactId > 0) { + $sql = "INSERT INTO vtiger_cntactivityrel (contactid, activityid) VALUES (?, ?)"; + $stmt = $mysqli->prepare($sql); + $stmt->bind_param('ii', $contactId, $eventId); + $stmt->execute(); + log_event('SUCCESS', "Событие привязано к контакту: $contactId"); + } else { + log_event('DEBUG', "Контакт не указан в проекте, пропускаем связывание"); + } + + // Обновляем поля проекта с информацией о последнем событии + try { + // Формируем описание для cf_2496 + $cf2496Description = $eventSubject; + if (!empty($result) && trim($result) !== '') { + // Очищаем результат от лишних пробелов и дефисов + $cleanResult = trim($result); + $cf2496Description .= " - $cleanResult"; + } + + $sql = "UPDATE vtiger_projectcf SET cf_1682 = ?, cf_1684 = ?, cf_2496 = ? WHERE projectid = ?"; + $stmt = $mysqli->prepare($sql); + $stmt->bind_param('sssi', $formattedDate, $formattedTime, $cf2496Description, $projectId); + $stmt->execute(); + + log_event('SUCCESS', "Поля проекта обновлены (cf_1682, cf_1684, cf_2496)"); + log_event('DEBUG', "cf_2496 установлен: $cf2496Description"); + } catch (Exception $e) { + log_event('WARNING', "Не удалось обновить поля проекта: " . $e->getMessage()); + } + + // Обновляем последовательность + $mysqli->query("UPDATE vtiger_crmentity_seq SET id = $eventId"); + + $mysqli->close(); + + // Формируем успешный ответ + $response = [ + 'success' => true, + 'event_id' => '4x' . $eventId, + 'event_numeric_id' => $eventId, + 'event_name' => $eventName, + 'event_date' => $formattedDate, + 'event_time' => $formattedTime, + 'project_id' => $projectId, + 'message' => 'Событие успешно создано и привязано к проекту' + ]; + + log_event('SUCCESS', "=== ОБРАБОТКА ЗАВЕРШЕНА УСПЕШНО ==="); + log_event('SUCCESS', "Событие создано: 4x$eventId"); + + header('Content-Type: application/json; charset=utf-8'); + echo json_encode($response, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT); + + exit(0); + +} catch (Exception $e) { + $error_message = $e->getMessage(); + log_event('ERROR', "Ошибка: $error_message"); + + $response = [ + 'success' => false, + 'error' => $error_message, + 'timestamp' => date('Y-m-d H:i:s') + ]; + + header('Content-Type: application/json; charset=utf-8'); + http_response_code(500); + echo json_encode($response, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT); + + exit(1); +} +?> diff --git a/ParseAndCreateEvent.php b/ParseAndCreateEvent.php new file mode 100644 index 00000000..7d74a046 --- /dev/null +++ b/ParseAndCreateEvent.php @@ -0,0 +1,224 @@ + $projectId, + 'status' => $params['status'] ?? '', + 'link1' => $params['link1'] ?? '', + 'link2' => $params['link2'] ?? '', + 'link3' => $params['link3'] ?? '', + 'case_number' => $params['case_number'] ?? '02-15800/2025', // Дефолтный номер дела для тестирования + 'uid' => $params['uid'] ?? '', + 'use_new_parser' => $params['use_new_parser'] ?? 'true', + 'skip_duplicate_check' => $params['skip_duplicate_check'] ?? 'false' + ]; + + log_wrapper('INFO', "Вызываем parscourt.php для проекта $projectId"); + + // Формируем URL для вызова parscourt.php + $domain = $_SERVER['HTTP_HOST'] ?? 'crm.clientright.ru'; + $protocol = (isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] === 'on') ? 'https' : 'https'; // Всегда https для production + $parscourtUrl = $protocol . '://' . $domain . '/parscourt.php?' . http_build_query($parscourtParams); + + log_wrapper('DEBUG', "URL: $parscourtUrl"); + + // Вызываем через cURL с POST (parscourt.php принимает POST параметры) + $ch = curl_init(); + curl_setopt($ch, CURLOPT_URL, $protocol . '://' . $domain . '/parscourt.php'); + curl_setopt($ch, CURLOPT_POST, true); + curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query($parscourtParams)); + curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); + curl_setopt($ch, CURLOPT_TIMEOUT, 60); + curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true); + + $output = curl_exec($ch); + $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE); + curl_close($ch); + + if ($httpCode !== 200) { + throw new Exception("Ошибка вызова parscourt.php: HTTP $httpCode"); + } + + log_wrapper('DEBUG', "Ответ от parscourt.php: $output"); + + // Парсим JSON ответ + $parscourtResponse = json_decode($output, true); + + if (json_last_error() !== JSON_ERROR_NONE) { + throw new Exception('Ошибка декодирования JSON от parscourt.php: ' . json_last_error_msg()); + } + + log_wrapper('DEBUG', "Распарсенный ответ: " . json_encode($parscourtResponse, JSON_UNESCAPED_UNICODE)); + + // Проверяем наличие last_event и что он не пустой + if (empty($parscourtResponse['last_event']) || + !isset($parscourtResponse['last_event']['Наименование']) || + empty($parscourtResponse['last_event']['Наименование'])) { + log_wrapper('WARNING', 'Нет данных о событиях (last_event пустой или без названия)'); + + $response = [ + 'success' => true, + 'message' => 'Парсинг выполнен, но нет новых событий', + 'event_created' => false + ]; + + header('Content-Type: application/json; charset=utf-8'); + echo json_encode($response, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT); + exit(0); + } + + $lastEvent = $parscourtResponse['last_event']; + + // Извлекаем данные события (пробуем оба варианта ключей) + $eventName = $lastEvent['Наименование'] ?? $lastEvent['name'] ?? 'Судебное заседание'; + $eventDate = $lastEvent['Дата'] ?? $lastEvent['date'] ?? ''; + $eventTime = $lastEvent['Время'] ?? $lastEvent['time'] ?? ''; + $location = $lastEvent['Место'] ?? $lastEvent['location'] ?? ''; + $result = $lastEvent['Результат'] ?? $lastEvent['result'] ?? ''; + $basis = $lastEvent['Основание'] ?? $lastEvent['basis'] ?? ''; + $note = $lastEvent['Примечание'] ?? $lastEvent['note'] ?? ''; + $publicationDate = $lastEvent['Дата размещения'] ?? $lastEvent['publication_date'] ?? ''; + + log_wrapper('DEBUG', "Извлеченные данные: eventName='$eventName', eventDate='$eventDate', eventTime='$eventTime'"); + log_wrapper('INFO', "Событие извлечено: $eventName ($eventDate $eventTime)"); + + // Проверяем что дата не пустая + if (empty($eventDate)) { + log_wrapper('WARNING', 'Дата события пустая, пропускаем создание'); + + $response = [ + 'success' => true, + 'message' => 'Событие не создано: дата отсутствует', + 'event_created' => false + ]; + + header('Content-Type: application/json; charset=utf-8'); + echo json_encode($response, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT); + exit(0); + } + + // Формируем данные для CreateCourtEvent.php + log_wrapper('DEBUG', "Перед формированием данных: eventName='$eventName', result='$result'"); + + $eventData = [ + 'project_id' => $projectId, + 'event_name' => $eventName, + 'event_date' => $eventDate, + 'event_time' => $eventTime, + 'location' => $location, + 'result' => $result, + 'basis' => $basis, + 'note' => $note, + 'publication_date' => $publicationDate + ]; + + log_wrapper('INFO', "Создаём событие через CreateCourtEvent_v2.php"); + log_wrapper('DEBUG', "Данные события: " . json_encode($eventData, JSON_UNESCAPED_UNICODE)); + + // Вызываем CreateCourtEvent_v2.php через CLI + $createEventCommand = 'php ' . __DIR__ . '/CreateCourtEvent_v2.php'; + $eventDataJson = json_encode($eventData, JSON_UNESCAPED_UNICODE); + + // Передаём данные через временный файл + $tempFile = tempnam(sys_get_temp_dir(), 'event_data_'); + file_put_contents($tempFile, $eventDataJson); + + $createEventOutput = shell_exec('cat ' . escapeshellarg($tempFile) . ' | ' . $createEventCommand . ' 2>&1'); + + // Удаляем временный файл + unlink($tempFile); + + log_wrapper('DEBUG', "Ответ от CreateCourtEvent_v2.php: $createEventOutput"); + + // Фильтруем PHP Notice из ответа + $cleanOutput = preg_replace('/^PHP Notice:.*$/m', '', $createEventOutput); + $createEventResponse = json_decode($cleanOutput, true); + + if (json_last_error() !== JSON_ERROR_NONE || empty($createEventResponse['success'])) { + throw new Exception('Ошибка создания события: ' . ($createEventResponse['error'] ?? 'Неизвестная ошибка')); + } + + log_wrapper('SUCCESS', "Событие создано: " . $createEventResponse['event_id']); + + // Формируем финальный ответ + $response = [ + 'success' => true, + 'message' => 'Парсинг выполнен и событие создано', + 'event_created' => true, + 'event_id' => $createEventResponse['event_id'], + 'event_name' => $eventName, + 'event_date' => $eventDate, + 'event_time' => $eventTime, + 'project_id' => $projectId + ]; + + log_wrapper('SUCCESS', '=== ОБРАБОТКА ЗАВЕРШЕНА УСПЕШНО ==='); + + header('Content-Type: application/json; charset=utf-8'); + echo json_encode($response, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT); + + exit(0); + +} catch (Exception $e) { + $error_message = $e->getMessage(); + log_wrapper('ERROR', "Ошибка: $error_message"); + log_wrapper('ERROR', "Стек: " . $e->getTraceAsString()); + + $response = [ + 'success' => false, + 'error' => $error_message, + 'timestamp' => date('Y-m-d H:i:s') + ]; + + header('Content-Type: application/json; charset=utf-8'); + http_response_code(500); + echo json_encode($response, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT); + + exit(1); +} +?> diff --git a/parscourt.php b/parscourt.php index 7bfa25b3..195582b2 100644 --- a/parscourt.php +++ b/parscourt.php @@ -17,12 +17,13 @@ $user = 'court_usr'; // пользователь $password = 'yOrjA9HdgwXO4JGJ'; // пароль try { - $pdo = new PDO("mysql:host=$host;dbname=$dbname;charset=utf8", $user, $password); + $pdo = new PDO("mysql:host=$host;dbname=$dbname;charset=utf8mb4", $user, $password); $pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION); + $pdo->exec("SET NAMES utf8mb4"); log_message("Успешное подключение к базе данных '$dbname'."); } catch (PDOException $e) { log_message("Ошибка подключения к базе данных: " . $e->getMessage()); - die(json_encode(["status" => "error", "message" => "Ошибка подключения: " . $e->getMessage()])); + die(json_encode(["status" => "error", "message" => "Ошибка подключения: " . $e->getMessage()], JSON_UNESCAPED_UNICODE)); } // Получаем параметры @@ -30,107 +31,150 @@ $status = $_POST['status'] ?? null; $link = $_POST['link1'] ?? ($_POST['link2'] ?? $_POST['link3'] ?? null); $case_number = $_POST['case_number'] ?? null; $uid = $_POST['uid'] ?? null; +$project_id = $_POST['project_id'] ?? null; // ID проекта для уведомлений +$use_new_parser = isset($_POST['use_new_parser']) ? (bool)$_POST['use_new_parser'] : true; // По умолчанию используем новый парсер +$skip_duplicate_check = isset($_POST['skip_duplicate_check']) ? (bool)$_POST['skip_duplicate_check'] : false; // Для тестирования: отключить проверку дубликатов + +// Отладка: логируем входящие параметры +log_message("Входящие параметры: status=$status, case_number=$case_number, uid=$uid, project_id=$project_id, skip_duplicate_check=" . ($skip_duplicate_check ? '1' : '0')); if (!$status || !$link || !$case_number) { echo json_encode(["status" => "error", "message" => "Ошибка: Не все необходимые параметры переданы."]); exit; } -log_message("Старт парсинга $case_number для статуса: $status"); -log_message("Парсим данные из ссылки: $link"); - -// Загружаем HTML-контент страницы дела -$html = @file_get_contents($link); - -if ($html === false) { - log_message("Ошибка: не удалось загрузить страницу по ссылке: $link"); - echo json_encode(["status" => "error", "message" => "Ошибка: не удалось загрузить страницу по ссылке: $link"]); - exit; +log_message("========================================"); +log_message("Режим парсера: " . ($use_new_parser ? "НОВЫЙ (универсальный)" : "СТАРЫЙ (legacy)")); +if ($skip_duplicate_check) { + log_message("⚠️ ТЕСТОВЫЙ РЕЖИМ: Проверка дубликатов ОТКЛЮЧЕНА"); } -log_message("Страница успешно загружена. Начинаем парсинг..."); - -// Парсим HTML с помощью DOMDocument и XPath -$dom = new DOMDocument(); -@$dom->loadHTML($html); -$xpath = new DOMXPath($dom); - -// Определяем div для парсинга -$div_id = ($status === 'представительство в суде 1й инстанции' || - $status === 'выдача листа' || - $status === 'исполнительное производство' || - $status === 'заявление на лист') ? 'cont2' : 'cont3'; - -$rows = $xpath->query("//div[@id='$div_id']//tr"); -log_message("Найдено строк (tr) в div с id '$div_id': " . $rows->length); - -// Массив для хранения последнего события $last_event = null; -// Обрабатываем каждую строку таблицы -foreach ($rows as $row) { - $event_name = trim($xpath->query('./td[1]', $row)->item(0)->nodeValue ?? ''); - $event_date = trim($xpath->query('./td[2]', $row)->item(0)->nodeValue ?? ''); - $event_time = trim($xpath->query('./td[3]', $row)->item(0)->nodeValue ?? ''); - $location = trim($xpath->query('./td[4]', $row)->item(0)->nodeValue ?? ''); - $event_result = trim($xpath->query('./td[5]', $row)->item(0)->nodeValue ?? ''); - $event_basis = trim($xpath->query('./td[6]', $row)->item(0)->nodeValue ?? ''); - $note = trim($xpath->query('./td[7]', $row)->item(0)->nodeValue ?? ''); - $publication_date = trim($xpath->query('./td[8]', $row)->item(0)->nodeValue ?? ''); - - // Логируем каждую строку - log_message("Найдено событие: $event_name, Дата: $event_date, Время: $event_time, Место: $location, Результат: $event_result, Основание: $event_basis, Примечание: $note, Дата размещения: $publication_date"); - - // Пропускаем записи, если название события не указано или дата неверная - if (empty($event_name) || empty($event_date) || $event_date === '1970-01-01') { - log_message("Пропущено событие: название или дата не указаны."); - continue; // Пропустить итерацию +// ======================================== +// НОВЫЙ ПАРСЕР (с поддержкой московских судов) +// ======================================== +if ($use_new_parser) { + try { + require_once 'parsers/CourtParserFactory.php'; + + $parser = CourtParserFactory::getParser($link, $pdo, $case_number, $uid, $skip_duplicate_check, $project_id); + + if ($parser === null) { + log_message("ПРЕДУПРЕЖДЕНИЕ: Не найден подходящий парсер для URL: $link. Используем fallback на старый парсер."); + $use_new_parser = false; // Переключаемся на старый парсер + } else { + $parserClass = get_class($parser); + log_message("Выбран парсер: $parserClass"); + + $last_event = $parser->parse($link, $status); + } + } catch (Exception $e) { + log_message("ОШИБКА в новом парсере: " . $e->getMessage()); + log_message("Переключаемся на старый парсер (fallback)..."); + $use_new_parser = false; // Переключаемся на старый парсер } - - // Форматируем даты - $formatted_date = date('Y-m-d', strtotime($event_date)); - $current_datetime = date('Y-m-d H:i:s'); - $formatted_publication_date = date('Y-m-d', strtotime($publication_date)); - - // Проверяем на дублирование - $checkQuery = "SELECT COUNT(*) FROM subject WHERE event_name = ? AND event_date = ? AND publication_date = ?"; - $checkStmt = $pdo->prepare($checkQuery); - $checkStmt->execute([$event_name, $formatted_date, $formatted_publication_date]); - $exists = $checkStmt->fetchColumn() > 0; - - if ($exists) { - log_message("Дубликат найден для события: $event_name, пропускаем запись."); - continue; // Пропустить запись - } - - // Запись данных в таблицу subject - $insertQuery = "INSERT INTO subject (case_number, uid, event_name, event_date, event_time, location, event_result, event_basis, note, publication_date, update_datetime) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)"; - $insertStmt = $pdo->prepare($insertQuery); - $insertStmt->execute([$case_number, $uid, $event_name, $formatted_date, $event_time, $location, $event_result, $event_basis, $note, $formatted_publication_date, $current_datetime]); - - log_message("Данные успешно записаны в таблицу subject для события: $event_name"); - $last_event = [ - 'event_name' => $event_name, - 'event_date' => $formatted_date, - 'event_time' => $event_time, - 'location' => $location, - 'event_result' => $event_result, - 'event_basis' => $event_basis, - 'note' => $note, - 'publication_date' => $formatted_publication_date, - ]; } -// Формируем ответ +// ======================================== +// СТАРЫЙ ПАРСЕР (LEGACY - для обратной совместимости) +// ======================================== +if (!$use_new_parser) { + log_message("Старт парсинга $case_number для статуса: $status (СТАРЫЙ ПАРСЕР)"); + log_message("Парсим данные из ссылки: $link"); + + // Загружаем HTML-контент страницы дела + $html = @file_get_contents($link); + + if ($html === false) { + log_message("Ошибка: не удалось загрузить страницу по ссылке: $link"); + echo json_encode(["status" => "error", "message" => "Ошибка: не удалось загрузить страницу по ссылке: $link"]); + exit; + } + + log_message("Страница успешно загружена. Начинаем парсинг..."); + + // Парсим HTML с помощью DOMDocument и XPath + $dom = new DOMDocument(); + // Важно: указываем кодировку UTF-8 для корректного парсинга + @$dom->loadHTML('' . $html); + $xpath = new DOMXPath($dom); + + // Определяем div для парсинга + $div_id = ($status === 'представительство в суде 1й инстанции' || + $status === 'выдача листа' || + $status === 'исполнительное производство' || + $status === 'заявление на лист') ? 'cont2' : 'cont3'; + + $rows = $xpath->query("//div[@id='$div_id']//tr"); + log_message("Найдено строк (tr) в div с id '$div_id': " . $rows->length); + + // Обрабатываем каждую строку таблицы + foreach ($rows as $row) { + $event_name = trim($xpath->query('./td[1]', $row)->item(0)->nodeValue ?? ''); + $event_date = trim($xpath->query('./td[2]', $row)->item(0)->nodeValue ?? ''); + $event_time = trim($xpath->query('./td[3]', $row)->item(0)->nodeValue ?? ''); + $location = trim($xpath->query('./td[4]', $row)->item(0)->nodeValue ?? ''); + $event_result = trim($xpath->query('./td[5]', $row)->item(0)->nodeValue ?? ''); + $event_basis = trim($xpath->query('./td[6]', $row)->item(0)->nodeValue ?? ''); + $note = trim($xpath->query('./td[7]', $row)->item(0)->nodeValue ?? ''); + $publication_date = trim($xpath->query('./td[8]', $row)->item(0)->nodeValue ?? ''); + + // Логируем каждую строку + log_message("Найдено событие: $event_name, Дата: $event_date, Время: $event_time, Место: $location, Результат: $event_result, Основание: $event_basis, Примечание: $note, Дата размещения: $publication_date"); + + // Пропускаем записи, если название события не указано или дата неверная + if (empty($event_name) || empty($event_date) || $event_date === '1970-01-01') { + log_message("Пропущено событие: название или дата не указаны."); + continue; // Пропустить итерацию + } + + // Форматируем даты + $formatted_date = date('Y-m-d', strtotime($event_date)); + $current_datetime = date('Y-m-d H:i:s'); + $formatted_publication_date = date('Y-m-d', strtotime($publication_date)); + + // Проверяем на дублирование + $checkQuery = "SELECT COUNT(*) FROM subject WHERE event_name = ? AND event_date = ? AND publication_date = ?"; + $checkStmt = $pdo->prepare($checkQuery); + $checkStmt->execute([$event_name, $formatted_date, $formatted_publication_date]); + $exists = $checkStmt->fetchColumn() > 0; + + if ($exists) { + log_message("Дубликат найден для события: $event_name, пропускаем запись."); + continue; // Пропустить запись + } + + // Запись данных в таблицу subject + $insertQuery = "INSERT INTO subject (case_number, uid, event_name, event_date, event_time, location, event_result, event_basis, note, publication_date, update_datetime) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)"; + $insertStmt = $pdo->prepare($insertQuery); + $insertStmt->execute([$case_number, $uid, $event_name, $formatted_date, $event_time, $location, $event_result, $event_basis, $note, $formatted_publication_date, $current_datetime]); + + log_message("Данные успешно записаны в таблицу subject для события: $event_name"); + $last_event = [ + 'event_name' => $event_name, + 'event_date' => $formatted_date, + 'event_time' => $event_time, + 'location' => $location, + 'event_result' => $event_result, + 'event_basis' => $event_basis, + 'note' => $note, + 'publication_date' => $formatted_publication_date, + ]; + } +} + +// Формируем ответ (ЕДИНЫЙ ФОРМАТ для обоих парсеров) if ($last_event) { // Преобразуем форматы дат $formatted_event_date = DateTime::createFromFormat('Y-m-d', $last_event['event_date'])->format('d.m.Y'); $formatted_publication_date = DateTime::createFromFormat('Y-m-d', $last_event['publication_date'])->format('d.m.Y'); - - echo json_encode([ + + $response = [ "status" => "success", "message" => "Парсинг завершен.", "last_event" => [ + // Кириллические ключи (для обратной совместимости) "Наименование" => $last_event['event_name'], "Дата" => $formatted_event_date, "Время" => $last_event['event_time'], @@ -138,16 +182,54 @@ if ($last_event) { "Результат" => $last_event['event_result'], "Основание" => $last_event['event_basis'], "Примечание" => $last_event['note'], - "Дата размещения" => $formatted_publication_date + "Дата размещения" => $formatted_publication_date, + // Дублируем латинскими ключами (для надежности) + "name" => $last_event['event_name'], + "date" => $formatted_event_date, + "time" => $last_event['event_time'], + "location" => $last_event['location'], + "result" => $last_event['event_result'], + "basis" => $last_event['event_basis'], + "note" => $last_event['note'], + "publication_date" => $formatted_publication_date ] - ]); + ]; + + // Логируем ответ для отладки + log_message("JSON ответ: " . json_encode($response, JSON_UNESCAPED_UNICODE)); + log_message("Событие: " . $last_event['event_name'] . " (" . $formatted_event_date . " " . $last_event['event_time'] . ")"); + + echo json_encode($response, JSON_UNESCAPED_UNICODE); } else { - echo json_encode([ + // Всегда возвращаем last_event, даже если он пустой (для совместимости с workflow) + $response = [ "status" => "success", - "message" => "Парсинг завершен, но нет новых событий." - ]); + "message" => "Парсинг завершен, но нет новых событий.", + "last_event" => [ + "Наименование" => "", + "Дата" => "", + "Время" => "", + "Место" => "", + "Результат" => "", + "Основание" => "", + "Примечание" => "", + "Дата размещения" => "", + // Латинские ключи тоже пустые + "name" => "", + "date" => "", + "time" => "", + "location" => "", + "result" => "", + "basis" => "", + "note" => "", + "publication_date" => "" + ] + ]; + + log_message("JSON ответ (нет новых событий): " . json_encode($response, JSON_UNESCAPED_UNICODE)); + + echo json_encode($response, JSON_UNESCAPED_UNICODE); } - log_message("Парсинг завершен."); ?> diff --git a/parsers/BaseCourtParser.php b/parsers/BaseCourtParser.php new file mode 100644 index 00000000..79efe487 --- /dev/null +++ b/parsers/BaseCourtParser.php @@ -0,0 +1,80 @@ +pdo = $pdo; + $this->case_number = $case_number; + $this->uid = $uid; + $this->skip_duplicate_check = $skip_duplicate_check; + $this->project_id = $project_id; + } + + /** + * Определить, может ли этот парсер обработать данную ссылку + */ + abstract public function canHandle($url); + + /** + * Парсить страницу дела + * @return array|null Массив с данными последнего события или null + */ + abstract public function parse($url, $status); + + /** + * Логирование + */ + protected function log($message) { + $date = date('Y-m-d H:i:s'); + file_put_contents('logs/parser.log', "[$date] $message" . PHP_EOL, FILE_APPEND); + } + + /** + * Сохранить событие в БД + */ + protected function saveEvent($event) { + // Проверяем на дублирование (если не отключена проверка) + if (!$this->skip_duplicate_check) { + $checkQuery = "SELECT COUNT(*) FROM subject WHERE event_name = ? AND event_date = ? AND publication_date = ?"; + $checkStmt = $this->pdo->prepare($checkQuery); + $checkStmt->execute([$event['event_name'], $event['event_date'], $event['publication_date']]); + $exists = $checkStmt->fetchColumn() > 0; + + if ($exists) { + $this->log("Дубликат найден для события: {$event['event_name']}, пропускаем запись."); + return false; + } + } else { + $this->log("⚠️ ТЕСТОВЫЙ РЕЖИМ: Проверка дубликатов отключена для события: {$event['event_name']}"); + } + + // Запись данных в таблицу subject + $insertQuery = "INSERT INTO subject (case_number, uid, event_name, event_date, event_time, location, event_result, event_basis, note, publication_date, update_datetime) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)"; + $insertStmt = $this->pdo->prepare($insertQuery); + $insertStmt->execute([ + $this->case_number, + $this->uid, + $event['event_name'], + $event['event_date'], + $event['event_time'], + $event['location'], + $event['event_result'], + $event['event_basis'], + $event['note'], + $event['publication_date'], + date('Y-m-d H:i:s') + ]); + + $this->log("Данные успешно записаны в таблицу subject для события: {$event['event_name']}"); + return true; + } +} +?> + diff --git a/parsers/CourtParserFactory.php b/parsers/CourtParserFactory.php new file mode 100644 index 00000000..1dd9ce80 --- /dev/null +++ b/parsers/CourtParserFactory.php @@ -0,0 +1,36 @@ +canHandle($url)) { + return $parser; + } + } + + return null; + } +} +?> + diff --git a/parsers/MoscowCourtParser.php b/parsers/MoscowCourtParser.php new file mode 100644 index 00000000..894eba65 --- /dev/null +++ b/parsers/MoscowCourtParser.php @@ -0,0 +1,542 @@ +log("Старт парсинга {$this->case_number} для статуса: $status (МОСКОВСКИЙ СУД)"); + $this->log("Парсим данные из ссылки: $url"); + + // Используем Browserless для получения структурированных данных + $data = $this->loadPageContentViaBrowserless($url); + + if ($data === false) { + $this->log("Ошибка: не удалось получить данные через Browserless"); + return null; + } + + $this->log("Данные успешно получены через Browserless. Начинаем обработку..."); + + // Извлекаем последнее событие из структурированных данных + $last_event = $this->extractLastEventFromData($data); + + if ($last_event === null) { + $this->log("ВНИМАНИЕ: Не удалось извлечь события из данных Browserless."); + } + + return $last_event; + } + + /** + * Извлекает последнее событие из структурированных данных Browserless + */ + private function extractLastEventFromData($data) { + $last_event = null; + + // Проверяем наличие заседаний (hearings) + if (isset($data['hearings']) && is_array($data['hearings']) && !empty($data['hearings'])) { + $this->log("Найдено заседаний: " . count($data['hearings'])); + + // Берем последнее заседание + $hearing = end($data['hearings']); + + if (!empty($hearing['datetime'])) { + // Парсим дату и время + $datetime = $hearing['datetime']; + $event_date = ''; + $event_time = ''; + + // Формат: "27.10.2025 09:30" или "27.10.2025" + if (preg_match('/(\d{2}\.\d{2}\.\d{4})\s+(\d{2}:\d{2})/', $datetime, $matches)) { + $event_date = $matches[1]; + $event_time = $matches[2]; + } elseif (preg_match('/(\d{2}\.\d{2}\.\d{4})/', $datetime, $matches)) { + $event_date = $matches[1]; + $event_time = ''; + } + + if (!empty($event_date)) { + $event_name = $hearing['stage'] ?? 'Судебное заседание'; + $event_result = $hearing['result'] ?? ''; + $location = $hearing['hall'] ?? ''; + $basis = $hearing['basis'] ?? ''; + + $this->log("Найдено заседание: $event_name на $event_date в $event_time"); + + // Форматируем дату для БД + $formatted_date = date('Y-m-d', strtotime(str_replace('.', '-', $event_date))); + + $eventData = [ + 'event_name' => $event_name, + 'event_date' => $formatted_date, + 'event_time' => $event_time, + 'location' => $location, + 'event_result' => $event_result, + 'event_basis' => $basis, + 'note' => '', + 'publication_date' => $formatted_date, + ]; + + // Сохраняем событие в БД + $this->saveEvent($eventData); + $last_event = $eventData; + + // Создаём уведомление (если указан project_id) + if ($this->project_id) { + $notificationId = $this->createCourtEventNotification($this->project_id, $eventData); + if ($notificationId) { + $this->log("Создано уведомление ID: $notificationId для события: $event_name"); + } + } + } + } + } + + // Если заседаний нет, проверяем историю состояний + if ($last_event === null && isset($data['history']['states']) && is_array($data['history']['states'])) { + $this->log("Заседаний нет, проверяем историю состояний: " . count($data['history']['states'])); + + // Берем последнее состояние + $state = end($data['history']['states']); + + if (!empty($state['date']) && !empty($state['state'])) { + $event_date = $state['date']; + $event_name = $state['state']; + $basis = $state['basis_doc'] ?? ''; + + $this->log("Найдено состояние: $event_name на $event_date"); + + // Форматируем дату для БД + $formatted_date = date('Y-m-d', strtotime(str_replace('.', '-', $event_date))); + + $eventData = [ + 'event_name' => $event_name, + 'event_date' => $formatted_date, + 'event_time' => '', + 'location' => '', + 'event_result' => '', + 'event_basis' => $basis, + 'note' => '', + 'publication_date' => $formatted_date, + ]; + + // Сохраняем событие в БД + $this->saveEvent($eventData); + $last_event = $eventData; + + // Создаём уведомление (если указан project_id) + if ($this->project_id) { + $notificationId = $this->createCourtEventNotification($this->project_id, $eventData); + if ($notificationId) { + $this->log("Создано уведомление ID: $notificationId для состояния: $event_name"); + } + } + } + } + + // Логируем информацию о деле для отладки + if (isset($data['case'])) { + $case = $data['case']; + $this->log("Информация о деле:"); + $this->log(" UID: " . ($case['uid'] ?? 'не указан')); + $this->log(" Номер дела: " . ($case['case_number'] ?? 'не указан')); + $this->log(" Статус: " . ($case['current_status'] ?? 'не указан')); + $this->log(" Истец: " . ($case['plaintiff'] ?? 'не указан')); + $this->log(" Ответчик: " . ($case['defendant'] ?? 'не указан')); + } + + return $last_event; + } + + /** + * Создаёт уведомление о новом событии суда + */ + private function createCourtEventNotification($projectId, $eventData) { + try { + // Создаём отдельное соединение с основной БД CRM для уведомлений + $crmPdo = new PDO('mysql:host=localhost;dbname=ci20465_72new;charset=utf8mb4', 'ci20465_72new', 'EcY979Rn'); + $crmPdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION); + + // Получаем ответственного по проекту + $query = "SELECT e.smownerid, p.projectname FROM vtiger_crmentity e + JOIN vtiger_project p ON p.projectid = e.crmid + WHERE e.crmid = ? AND e.deleted = 0"; + $stmt = $crmPdo->prepare($query); + $stmt->execute([$projectId]); + $result = $stmt->fetch(PDO::FETCH_ASSOC); + + if (!$result) { + $this->log("Проект $projectId не найден для уведомления"); + return false; + } + + $userId = $result['smownerid']; + $projectName = $result['projectname']; + + $this->log("Создаем уведомление для пользователя $userId о событии в проекте $projectName"); + + // Формируем текст уведомления + $eventName = $eventData['event_name']; + $eventDate = $eventData['event_date']; + $eventTime = $eventData['event_time']; + + $timeStr = !empty($eventTime) ? " в $eventTime" : ""; + $notificationTitle = "Событие суда: $eventName на $eventDate$timeStr"; + + // Формируем ссылку на проект + $projectLink = "module=Project&view=Detail&record=$projectId"; + + // Проверяем, нет ли уже непрочитанного уведомления для этого события + $checkQuery = "SELECT id FROM vtiger_vdnotifierpro WHERE userid = ? AND crmid = ? AND title LIKE ? AND status = 5"; + $checkStmt = $crmPdo->prepare($checkQuery); + $checkStmt->execute([$userId, $projectId, "%$eventName%$eventDate%"]); + $existing = $checkStmt->fetch(PDO::FETCH_ASSOC); + + if ($existing) { + // Обновляем время существующего уведомления + $updateQuery = "UPDATE vtiger_vdnotifierpro SET modifiedtime = NOW() WHERE id = ?"; + $updateStmt = $crmPdo->prepare($updateQuery); + $updateStmt->execute([$existing['id']]); + + $this->log("Обновлено существующее уведомление ID: {$existing['id']}"); + return $existing['id']; + } else { + // Создаем новое уведомление + $insertQuery = "INSERT INTO vtiger_vdnotifierpro (userid, modulename, crmid, modiuserid, link, title, action, modifiedtime, status) VALUES (?, 'Project', ?, 0, ?, ?, '', NOW(), 5)"; + $insertStmt = $crmPdo->prepare($insertQuery); + $insertStmt->execute([$userId, $projectId, $projectLink, $notificationTitle]); + + $notificationId = $crmPdo->lastInsertId(); + $this->log("Создано новое уведомление ID: $notificationId для пользователя $userId"); + + return $notificationId; + } + + } catch (Exception $e) { + $this->log("Ошибка создания уведомления: " . $e->getMessage()); + return false; + } + } + + /** + * Загружает содержимое страницы через cURL + */ + private function loadPageContent($url) { + // Сначала пробуем обычный cURL + $ch = curl_init(); + curl_setopt($ch, CURLOPT_URL, $url); + curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); + curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true); + curl_setopt($ch, CURLOPT_TIMEOUT, 30); + curl_setopt($ch, CURLOPT_USERAGENT, 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36'); + curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false); + curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, false); + curl_setopt($ch, CURLOPT_ENCODING, ''); // Автоматически обрабатывает gzip, deflate, br + curl_setopt($ch, CURLOPT_HTTPHEADER, [ + 'Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8', + 'Accept-Language: ru-RU,ru;q=0.9,en;q=0.8', + 'Connection: keep-alive', + 'Upgrade-Insecure-Requests: 1', + 'Sec-Fetch-Dest: document', + 'Sec-Fetch-Mode: navigate', + 'Sec-Fetch-Site: none', + 'Cache-Control: max-age=0' + ]); + + $html = curl_exec($ch); + $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE); + curl_close($ch); + + // Если cURL успешен, возвращаем результат + if ($html !== false && $httpCode === 200) { + $this->log("Страница загружена через cURL (HTTP $httpCode)"); + return $html; + } + + $this->log("cURL не удался (HTTP $httpCode), пробуем Browserless..."); + + // Если cURL не сработал, пробуем Browserless + return $this->loadPageContentViaBrowserless($url); + } + + /** + * Загружает структурированные данные через Browserless function + */ + private function loadPageContentViaBrowserless($url) { + $browserlessUrl = 'http://147.45.146.17:3000/function'; + $browserlessToken = '9ahhnpjkchxtcho9'; + + // JavaScript код функции (тот же, что мы тестировали) + $jsFunction = ' +export default async function ({ page, context }) { + const caseUrl = + context.case_url || + "' . addslashes($url) . '"; + + // --- Установка заголовков и поведения браузера --- + await page.setViewport({ width: 1920, height: 1080 }); + await page.setExtraHTTPHeaders({ + "Referer": "https://mos-sud.ru/", + "Origin": "https://mos-sud.ru", + "Accept-Language": "ru,en;q=0.9", + "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8", + "Upgrade-Insecure-Requests": "1", + }); + await page.setUserAgent( + "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36" + ); + + await page.goto(caseUrl, { waitUntil: "networkidle2", timeout: 60000 }); + + // закрыть баннеры cookies, если есть + try { + await page.waitForSelector("#cookie-disclaimer .cd-close-button, .cookie-accept, .cookie__close", { timeout: 3000 }); + const btns = await page.$$("#cookie-disclaimer .cd-close-button, .cookie-accept, .cookie__close"); + if (btns[0]) await btns[0].click(); + } catch (_) {} + + // ждём карточку + await page.waitForSelector( + ".detail-cart .row_card, .case-card, .case-details, .content, main .wrapper_innercontent", + { timeout: 20000 } + ); + + // активируем вкладки + try { + for (const id of ["#ui-id-1", "#ui-id-2", "#ui-id-3"]) { + if (await page.$(id)) await page.click(id); + } + const tabLinks = await page.$$(`a[href^="#tabs-"], .tabs_wrapper a.ui-tabs-anchor`); + if (tabLinks.length) for (const a of tabLinks) await a.click(); + await page.waitForTimeout(300); + } catch (_) {} + + const data = await page.evaluate(() => { + const norm = (el) => (el ? el.textContent.replace(/\\s+/g, " ").trim() : ""); + const qsa = (sel) => Array.from(document.querySelectorAll(sel)); + + function collectRows() { + const rows = []; + qsa(".detail-cart .row_card").forEach((r) => { + const left = norm(r.querySelector(".left")); + const right = norm(r.querySelector(".right")); + if (left && right) rows.push({ left, right }); + }); + if (!rows.length) { + qsa("table, .case-card, .case-details").forEach((tbl) => { + qsa("tr", tbl).forEach((tr) => { + const tds = tr.querySelectorAll("td, th"); + if (tds.length === 2) { + const left = norm(tds[0]); + const right = norm(tds[1]); + if (left && right) rows.push({ left, right }); + } + }); + }); + } + if (!rows.length) { + qsa(".case-card__row, .kv-row").forEach((row) => { + const left = norm(row.querySelector(".case-card__key, .kv-key, .left")); + const right = norm(row.querySelector(".case-card__val, .kv-val, .right")); + if (left && right) rows.push({ left, right }); + }); + } + return rows; + } + + const rows = collectRows(); + const byLeft = (start) => { + const row = rows.find((r) => + r.left.toLowerCase().startsWith(start.toLowerCase()) + ); + return row ? row.right : null; + }; + + const uid = byLeft("Уникальный идентификатор дела") || byLeft("UID"); + const numberRaw = byLeft("Номер дела") || byLeft("№ дела") || byLeft("Номер дела ~ материала"); + let case_number = null, material_number = null; + if (numberRaw) { + if (numberRaw.includes("∼")) { + const parts = numberRaw.split("∼").map((s) => s.trim()).filter(Boolean); + case_number = parts[0] || null; + material_number = parts[1] || null; + } else { + case_number = numberRaw; + } + } + + const intake_date = byLeft("Дата поступления") || byLeft("Поступило"); + const partiesRaw = byLeft("Стороны") || byLeft("Участники") || ""; + let plaintiff = null, defendant = null; + if (partiesRaw) { + const m1 = partiesRaw.match(/Истец:\\s*([^<\\n]+)/i); + const m2 = partiesRaw.match(/Ответчик:\\s*([^<\\n]+)/i); + plaintiff = m1 ? m1[1].trim() : null; + defendant = m2 ? m2[1].trim() : null; + } + + const judge = byLeft("Судья") || byLeft("Cудья") || byLeft("Председательствующий судья"); + const category = byLeft("Категория дела") || byLeft("Категория"); + + const statusRaw = byLeft("Текущее состояние") || byLeft("Состояние"); + let current_status = null, current_status_date = null; + if (statusRaw) { + const m = statusRaw.match(/^(.+?),\\s*([\\d.]{10})$/); + current_status = m ? m[1].trim() : statusRaw; + current_status_date = m ? m[2] : null; + } + + const first_instance_date = + byLeft("Дата рассмотрения дела в первой инстанции") || byLeft("Дата рассмотрения (1 инстанция)"); + + const first_inst_decision_raw = + byLeft("Решение первой инстанции") || byLeft("Решение (1 инстанция)"); + let first_instance_decision = null, first_instance_decision_date = null; + if (first_inst_decision_raw) { + const m = first_inst_decision_raw.match(/^(.+?),\\s*([\\d.]{10})$/); + first_instance_decision = m ? m[1].trim() : first_inst_decision_raw; + first_instance_decision_date = m ? m[2] : null; + } + + // таблицы + function tableToRows(tbody) { + return Array.from(tbody.querySelectorAll("tr")).map((tr) => { + const tds = tr.querySelectorAll("td"); + return Array.from(tds).map((td) => norm(td.querySelector("div") || td)); + }); + } + + const stTbody = document.querySelector("#tabs-1 #state-history table tbody"); + const stateRows = stTbody ? tableToRows(stTbody) : []; + const states = stateRows.map((cols) => ({ + date: cols[0] || null, + state: cols[1] || null, + basis_doc: cols[2] || null, + })); + + const tab1Tbodies = document.querySelectorAll("#tabs-1 table tbody"); + const placeTbody = tab1Tbodies.length > 1 ? tab1Tbodies[1] : null; + const locationRows = placeTbody ? tableToRows(placeTbody) : []; + const locations = locationRows.map((cols) => ({ + date: cols[0] || null, + location: cols[1] || null, + comment: cols[2] || null, + })); + + const sessionsTbody = document.querySelector("#tabs-2 table tbody"); + const hearingsRows = sessionsTbody ? tableToRows(sessionsTbody) : []; + const hearings = hearingsRows.map((cols) => ({ + datetime: cols[0] || null, + hall: cols[1] || null, + stage: cols[2] || null, + result: cols[3] || null, + basis: cols[4] || null, + av_record: cols[5] || null, + type: cols[6] || null, + })); + + const docsTbody = document.querySelector("#tabs-3 table tbody"); + const docsRows = docsTbody ? tableToRows(docsTbody) : []; + const documents = docsRows.map((cols) => ({ + date: cols[0] || null, + kind: cols[1] || null, + text_status: cols[2] || null, + })); + + const court = + (document.querySelector(".court-name")?.textContent || "").trim() || + (document.querySelector(\'[class*="court"] [class*="name"]\')?.textContent || "").trim() || + (document.querySelector("title")?.textContent.match(/суд[^|]*/i)?.[0] || "").trim() || + null; + + const title = (document.querySelector("h1")?.textContent || "") + .replace(/\\s+/g, " ") + .trim(); + + return { + case: { + uid, + case_number, + material_number, + intake_date, + plaintiff, + defendant, + judge, + category, + current_status, + current_status_date, + first_instance_date, + first_instance_decision, + first_instance_decision_date, + }, + history: { states, locations }, + hearings, + documents, + meta: { court, title }, + }; + }); + + return { source_url: caseUrl, ...data }; +}'; + + // Подготавливаем данные для отправки + $postData = [ + 'code' => $jsFunction, + 'context' => [ + 'case_url' => $url + ] + ]; + + $this->log("Отправляем запрос в Browserless для URL: $url"); + + // Отправляем запрос в Browserless + $ch = curl_init(); + curl_setopt($ch, CURLOPT_URL, $browserlessUrl); + curl_setopt($ch, CURLOPT_POST, true); + curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($postData)); + curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); + curl_setopt($ch, CURLOPT_TIMEOUT, 120); // Увеличиваем таймаут + curl_setopt($ch, CURLOPT_HTTPHEADER, [ + 'Content-Type: application/json', + 'Authorization: Bearer ' . $browserlessToken + ]); + + $response = curl_exec($ch); + $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE); + $error = curl_error($ch); + curl_close($ch); + + $this->log("Browserless ответ: HTTP $httpCode, длина ответа: " . strlen($response)); + + if ($error) { + $this->log("cURL ошибка: $error"); + return false; + } + + if ($httpCode !== 200) { + $this->log("Browserless вернул HTTP $httpCode: " . substr($response, 0, 200)); + return false; + } + + // Парсим ответ + $data = json_decode($response, true); + if (json_last_error() !== JSON_ERROR_NONE) { + $this->log("Ошибка декодирования JSON: " . json_last_error_msg()); + $this->log("Ответ: " . substr($response, 0, 500)); + return false; + } + + $this->log("Данные успешно получены от Browserless"); + return $data; + } +} +?> + diff --git a/parsers/RegionalCourtParser.php b/parsers/RegionalCourtParser.php new file mode 100644 index 00000000..3b0ae561 --- /dev/null +++ b/parsers/RegionalCourtParser.php @@ -0,0 +1,92 @@ +log("Старт парсинга {$this->case_number} для статуса: $status (РЕГИОНАЛЬНЫЙ СУД)"); + $this->log("Парсим данные из ссылки: $url"); + + // Загружаем HTML-контент страницы дела + $html = @file_get_contents($url); + + if ($html === false) { + $this->log("Ошибка: не удалось загрузить страницу по ссылке: $url"); + return null; + } + + $this->log("Страница успешно загружена. Начинаем парсинг..."); + + // Парсим HTML с помощью DOMDocument и XPath + $dom = new DOMDocument(); + // Важно: указываем кодировку UTF-8 для корректного парсинга + @$dom->loadHTML('' . $html); + $xpath = new DOMXPath($dom); + + // Определяем div для парсинга + $div_id = ($status === 'представительство в суде 1й инстанции' || + $status === 'выдача листа' || + $status === 'исполнительное производство' || + $status === 'заявление на лист') ? 'cont2' : 'cont3'; + + $rows = $xpath->query("//div[@id='$div_id']//tr"); + $this->log("Найдено строк (tr) в div с id '$div_id': " . $rows->length); + + // Массив для хранения последнего события + $last_event = null; + + // Обрабатываем каждую строку таблицы + foreach ($rows as $row) { + $event_name = trim($xpath->query('./td[1]', $row)->item(0)->nodeValue ?? ''); + $event_date = trim($xpath->query('./td[2]', $row)->item(0)->nodeValue ?? ''); + $event_time = trim($xpath->query('./td[3]', $row)->item(0)->nodeValue ?? ''); + $location = trim($xpath->query('./td[4]', $row)->item(0)->nodeValue ?? ''); + $event_result = trim($xpath->query('./td[5]', $row)->item(0)->nodeValue ?? ''); + $event_basis = trim($xpath->query('./td[6]', $row)->item(0)->nodeValue ?? ''); + $note = trim($xpath->query('./td[7]', $row)->item(0)->nodeValue ?? ''); + $publication_date = trim($xpath->query('./td[8]', $row)->item(0)->nodeValue ?? ''); + + // Логируем каждую строку + $this->log("Найдено событие: $event_name, Дата: $event_date, Время: $event_time, Место: $location, Результат: $event_result, Основание: $event_basis, Примечание: $note, Дата размещения: $publication_date"); + + // Пропускаем записи, если название события не указано или дата неверная + if (empty($event_name) || empty($event_date) || $event_date === '1970-01-01') { + $this->log("Пропущено событие: название или дата не указаны."); + continue; + } + + // Форматируем даты + $formatted_date = date('Y-m-d', strtotime($event_date)); + $formatted_publication_date = date('Y-m-d', strtotime($publication_date)); + + $eventData = [ + 'event_name' => $event_name, + 'event_date' => $formatted_date, + 'event_time' => $event_time, + 'location' => $location, + 'event_result' => $event_result, + 'event_basis' => $event_basis, + 'note' => $note, + 'publication_date' => $formatted_publication_date, + ]; + + // Сохраняем событие в БД + $this->saveEvent($eventData); + + // Запоминаем последнее событие для ответа + $last_event = $eventData; + } + + return $last_event; + } +} +?> +