feat: Интеграция уведомлений для событий судов

- Добавлена поддержка project_id в parscourt.php для создания уведомлений
- Создана система парсеров судов (BaseCourtParser, MoscowCourtParser, RegionalCourtParser)
- Интегрирован Browserless для парсинга московских судов (mos-sud.ru, mos-gorsud.ru)
- Добавлены уведомления VDNotifierPro при обнаружении новых событий судов
- Создан ParseAndCreateEvent.php для интеграции с CRM workflow
- Создан CreateCourtEvent_v2.php для прямого создания событий в календаре CRM
- Поддержка проверки дубликатов событий (можно отключить для тестирования)
- Автоматическое определение типа суда и выбор подходящего парсера

Функции:
- Парсинг региональных судов (*.sudrf.ru) через HTML
- Парсинг московских судов через Browserless API
- Создание событий в CRM календаре с привязкой к проектам
- Уведомления ответственных пользователей о новых событиях
- Сохранение событий в таблицу subject для истории
This commit is contained in:
Fedor
2025-10-17 19:45:11 +03:00
parent 4721a04114
commit 3db9d06c86
7 changed files with 1450 additions and 89 deletions

305
CreateCourtEvent_v2.php Normal file
View File

@@ -0,0 +1,305 @@
<?php
/**
* Создание события в календаре CRM для судебного заседания (версия 2 - через SQL)
*
* Принимает POST запрос с данными:
* - project_id: ID проекта (обязательно)
* - event_name: Название события
* - event_date: Дата события (формат DD.MM.YYYY)
* - event_time: Время события (формат HH:MM)
* - location: Место проведения
* - result: Результат события
* - basis: Основание
* - note: Примечание
* - publication_date: Дата размещения
*/
// Устанавливаем рабочую директорию
chdir(__DIR__);
// Логирование
function log_event($level, $message) {
$log_file = 'logs/create_court_event.log';
$timestamp = date('Y-m-d H:i:s');
$log_entry = "{$timestamp} - {$level}: {$message}\n";
file_put_contents($log_file, $log_entry, FILE_APPEND | LOCK_EX);
}
// Функция для форматирования даты в формат CRM (YYYY-MM-DD)
function formatDateForCRM($dateString) {
if (empty($dateString)) {
return '';
}
// Если формат DD.MM.YYYY
if (preg_match('/^(\d{2})\.(\d{2})\.(\d{4})$/', $dateString, $matches)) {
return $matches[3] . '-' . $matches[2] . '-' . $matches[1];
}
// Если уже в формате YYYY-MM-DD
if (preg_match('/^\d{4}-\d{2}-\d{2}$/', $dateString)) {
return $dateString;
}
return $dateString;
}
// Функция для форматирования времени в формат CRM (HH:MM:SS)
function formatTimeForCRM($timeString) {
if (empty($timeString)) {
return '10:00:00'; // Время по умолчанию
}
// Если формат HH:MM
if (preg_match('/^(\d{1,2}):(\d{2})$/', $timeString, $matches)) {
return sprintf('%02d:%02d:00', $matches[1], $matches[2]);
}
// Если уже в формате HH:MM:SS
if (preg_match('/^\d{2}:\d{2}:\d{2}$/', $timeString)) {
return $timeString;
}
return '10:00:00';
}
try {
log_event('INFO', '=== НАЧАЛО ОБРАБОТКИ ЗАПРОСА ===');
// Получаем данные из stdin
$input = file_get_contents('php://stdin');
if (!empty($input)) {
log_event('DEBUG', "Входные данные из stdin: " . $input);
$data = json_decode($input, true);
if (json_last_error() !== JSON_ERROR_NONE) {
throw new Exception('Ошибка декодирования JSON: ' . json_last_error_msg());
}
} else {
// Если stdin пустой, используем $_POST
$data = $_POST;
log_event('DEBUG', "Используем \$_POST: " . json_encode($data, JSON_UNESCAPED_UNICODE));
}
// Проверяем обязательные параметры
if (empty($data['project_id'])) {
throw new Exception('Параметр project_id обязателен');
}
$projectId = intval($data['project_id']);
// Извлекаем данные события
$eventName = $data['event_name'] ?? 'Судебное заседание';
$eventDate = $data['event_date'] ?? '';
$eventTime = $data['event_time'] ?? '';
$location = $data['location'] ?? '';
$result = $data['result'] ?? '';
$basis = $data['basis'] ?? '';
$note = $data['note'] ?? '';
$publicationDate = $data['publication_date'] ?? '';
log_event('INFO', "Создаем событие для проекта: $projectId");
log_event('DEBUG', "Название: '$eventName', Дата: '$eventDate', Время: '$eventTime'");
log_event('DEBUG', "Полные входные данные: " . json_encode($data, JSON_UNESCAPED_UNICODE));
// Проверяем что дата не пустая
if (empty($eventDate)) {
throw new Exception('Дата события обязательна');
}
// Подключаемся к базе данных
$mysqli = new mysqli('localhost', 'ci20465_72new', 'EcY979Rn', 'ci20465_72new');
if ($mysqli->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);
}
?>

224
ParseAndCreateEvent.php Normal file
View File

@@ -0,0 +1,224 @@
<?php
/**
* Обёртка для парсинга ответа от parscourt.php и создания события
*
* Принимает те же параметры что и parscourt.php
* Вызывает parscourt.php, получает JSON с last_event
* И создаёт событие через CreateCourtEvent.php
*
* Этот скрипт можно дёргать из workflow вместо parscourt.php
*/
// Устанавливаем рабочую директорию
chdir(__DIR__);
// Логирование
function log_wrapper($level, $message) {
$log_file = 'logs/parse_and_create_event.log';
$timestamp = date('Y-m-d H:i:s');
$log_entry = "{$timestamp} - {$level}: {$message}\n";
file_put_contents($log_file, $log_entry, FILE_APPEND | LOCK_EX);
}
try {
log_wrapper('INFO', '=== НАЧАЛО ОБРАБОТКИ ===');
// Получаем параметры (из POST, GET или argv)
$params = array_merge($_GET, $_POST);
// Если параметров нет, пробуем argv (для вызова через CLI)
if (empty($params) && !empty($argv)) {
for ($i = 1; $i < count($argv); $i++) {
if (strpos($argv[$i], '=') !== false) {
list($key, $value) = explode('=', $argv[$i], 2);
$params[$key] = $value;
}
}
}
log_wrapper('DEBUG', "Параметры: " . json_encode($params, JSON_UNESCAPED_UNICODE));
// Проверяем обязательные параметры
if (empty($params['project_id'])) {
throw new Exception('Параметр project_id обязателен');
}
$projectId = $params['project_id'];
// Формируем параметры для parscourt.php
$parscourtParams = [
'project_id' => $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);
}
?>

View File

@@ -17,12 +17,13 @@ $user = 'court_usr'; // пользователь
$password = 'yOrjA9HdgwXO4JGJ'; // пароль $password = 'yOrjA9HdgwXO4JGJ'; // пароль
try { 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->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
$pdo->exec("SET NAMES utf8mb4");
log_message("Успешное подключение к базе данных '$dbname'."); log_message("Успешное подключение к базе данных '$dbname'.");
} catch (PDOException $e) { } catch (PDOException $e) {
log_message("Ошибка подключения к базе данных: " . $e->getMessage()); log_message("Ошибка подключения к базе данных: " . $e->getMessage());
die(json_encode(["status" => "error", "message" => "Ошибка подключения: " . $e->getMessage()])); die(json_encode(["status" => "error", "message" => "Ошибка подключения: " . $e->getMessage()], JSON_UNESCAPED_UNICODE));
} }
// Получаем параметры // Получаем параметры
@@ -30,13 +31,56 @@ $status = $_POST['status'] ?? null;
$link = $_POST['link1'] ?? ($_POST['link2'] ?? $_POST['link3'] ?? null); $link = $_POST['link1'] ?? ($_POST['link2'] ?? $_POST['link3'] ?? null);
$case_number = $_POST['case_number'] ?? null; $case_number = $_POST['case_number'] ?? null;
$uid = $_POST['uid'] ?? 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) { if (!$status || !$link || !$case_number) {
echo json_encode(["status" => "error", "message" => "Ошибка: Не все необходимые параметры переданы."]); echo json_encode(["status" => "error", "message" => "Ошибка: Не все необходимые параметры переданы."]);
exit; exit;
} }
log_message("Старт парсинга $case_number для статуса: $status"); log_message("========================================");
log_message("Режим парсера: " . ($use_new_parser ? "НОВЫЙ (универсальный)" : "СТАРЫЙ (legacy)"));
if ($skip_duplicate_check) {
log_message("⚠️ ТЕСТОВЫЙ РЕЖИМ: Проверка дубликатов ОТКЛЮЧЕНА");
}
$last_event = null;
// ========================================
// НОВЫЙ ПАРСЕР (с поддержкой московских судов)
// ========================================
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; // Переключаемся на старый парсер
}
}
// ========================================
// СТАРЫЙ ПАРСЕР (LEGACY - для обратной совместимости)
// ========================================
if (!$use_new_parser) {
log_message("Старт парсинга $case_number для статуса: $status (СТАРЫЙ ПАРСЕР)");
log_message("Парсим данные из ссылки: $link"); log_message("Парсим данные из ссылки: $link");
// Загружаем HTML-контент страницы дела // Загружаем HTML-контент страницы дела
@@ -52,7 +96,8 @@ log_message("Страница успешно загружена. Начинае
// Парсим HTML с помощью DOMDocument и XPath // Парсим HTML с помощью DOMDocument и XPath
$dom = new DOMDocument(); $dom = new DOMDocument();
@$dom->loadHTML($html); // Важно: указываем кодировку UTF-8 для корректного парсинга
@$dom->loadHTML('<?xml encoding="UTF-8">' . $html);
$xpath = new DOMXPath($dom); $xpath = new DOMXPath($dom);
// Определяем div для парсинга // Определяем div для парсинга
@@ -64,9 +109,6 @@ $div_id = ($status === 'представительство в суде 1й ин
$rows = $xpath->query("//div[@id='$div_id']//tr"); $rows = $xpath->query("//div[@id='$div_id']//tr");
log_message("Найдено строк (tr) в div с id '$div_id': " . $rows->length); log_message("Найдено строк (tr) в div с id '$div_id': " . $rows->length);
// Массив для хранения последнего события
$last_event = null;
// Обрабатываем каждую строку таблицы // Обрабатываем каждую строку таблицы
foreach ($rows as $row) { foreach ($rows as $row) {
$event_name = trim($xpath->query('./td[1]', $row)->item(0)->nodeValue ?? ''); $event_name = trim($xpath->query('./td[1]', $row)->item(0)->nodeValue ?? '');
@@ -120,17 +162,19 @@ foreach ($rows as $row) {
'publication_date' => $formatted_publication_date, 'publication_date' => $formatted_publication_date,
]; ];
} }
}
// Формируем ответ // Формируем ответ (ЕДИНЫЙ ФОРМАТ для обоих парсеров)
if ($last_event) { if ($last_event) {
// Преобразуем форматы дат // Преобразуем форматы дат
$formatted_event_date = DateTime::createFromFormat('Y-m-d', $last_event['event_date'])->format('d.m.Y'); $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'); $formatted_publication_date = DateTime::createFromFormat('Y-m-d', $last_event['publication_date'])->format('d.m.Y');
echo json_encode([ $response = [
"status" => "success", "status" => "success",
"message" => "Парсинг завершен.", "message" => "Парсинг завершен.",
"last_event" => [ "last_event" => [
// Кириллические ключи (для обратной совместимости)
"Наименование" => $last_event['event_name'], "Наименование" => $last_event['event_name'],
"Дата" => $formatted_event_date, "Дата" => $formatted_event_date,
"Время" => $last_event['event_time'], "Время" => $last_event['event_time'],
@@ -138,16 +182,54 @@ if ($last_event) {
"Результат" => $last_event['event_result'], "Результат" => $last_event['event_result'],
"Основание" => $last_event['event_basis'], "Основание" => $last_event['event_basis'],
"Примечание" => $last_event['note'], "Примечание" => $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
] ]
]); ];
} else {
echo json_encode([
"status" => "success",
"message" => "Парсинг завершен, но нет новых событий."
]);
}
// Логируем ответ для отладки
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 {
// Всегда возвращаем last_event, даже если он пустой (для совместимости с workflow)
$response = [
"status" => "success",
"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("Парсинг завершен."); log_message("Парсинг завершен.");
?> ?>

View File

@@ -0,0 +1,80 @@
<?php
/**
* Базовый класс для парсеров судов
*/
abstract class BaseCourtParser {
protected $pdo;
protected $case_number;
protected $uid;
protected $skip_duplicate_check;
protected $project_id;
public function __construct($pdo, $case_number, $uid, $skip_duplicate_check = false, $project_id = null) {
$this->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;
}
}
?>

View File

@@ -0,0 +1,36 @@
<?php
require_once 'RegionalCourtParser.php';
require_once 'MoscowCourtParser.php';
/**
* Фабрика для создания подходящего парсера
*/
class CourtParserFactory {
/**
* Получить подходящий парсер для данной ссылки
* @param string $url URL страницы дела
* @param PDO $pdo Соединение с БД
* @param string $case_number Номер дела
* @param string $uid УИД дела
* @param bool $skip_duplicate_check Пропустить проверку дубликатов (для тестирования)
* @param int|null $project_id ID проекта для уведомлений
* @return BaseCourtParser|null
*/
public static function getParser($url, $pdo, $case_number, $uid, $skip_duplicate_check = false, $project_id = null) {
$parsers = [
new MoscowCourtParser($pdo, $case_number, $uid, $skip_duplicate_check, $project_id),
new RegionalCourtParser($pdo, $case_number, $uid, $skip_duplicate_check, $project_id),
];
foreach ($parsers as $parser) {
if ($parser->canHandle($url)) {
return $parser;
}
}
return null;
}
}
?>

View File

@@ -0,0 +1,542 @@
<?php
require_once 'BaseCourtParser.php';
/**
* Парсер для московских судов (mos-gorsud.ru)
*/
class MoscowCourtParser extends BaseCourtParser {
public function canHandle($url) {
// Московские суды имеют домены mos-gorsud.ru и mos-sud.ru
return preg_match('/mos-(gorsud|sud)\.ru/', $url);
}
public function parse($url, $status) {
$this->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;
}
}
?>

View File

@@ -0,0 +1,92 @@
<?php
require_once 'BaseCourtParser.php';
/**
* Парсер для региональных судов (*.sudrf.ru)
*/
class RegionalCourtParser extends BaseCourtParser {
public function canHandle($url) {
// Региональные суды имеют домены вида: example--region.sudrf.ru
return preg_match('/\.sudrf\.ru/', $url) && !preg_match('/mos-gorsud\.ru/', $url);
}
public function parse($url, $status) {
$this->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('<?xml encoding="UTF-8">' . $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;
}
}
?>