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:
305
CreateCourtEvent_v2.php
Normal file
305
CreateCourtEvent_v2.php
Normal 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
224
ParseAndCreateEvent.php
Normal 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);
|
||||||
|
}
|
||||||
|
?>
|
||||||
260
parscourt.php
260
parscourt.php
@@ -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,107 +31,150 @@ $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("Парсим данные из ссылки: $link");
|
log_message("Режим парсера: " . ($use_new_parser ? "НОВЫЙ (универсальный)" : "СТАРЫЙ (legacy)"));
|
||||||
|
if ($skip_duplicate_check) {
|
||||||
// Загружаем HTML-контент страницы дела
|
log_message("⚠️ ТЕСТОВЫЙ РЕЖИМ: Проверка дубликатов ОТКЛЮЧЕНА");
|
||||||
$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();
|
|
||||||
@$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;
|
$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 ?? '');
|
if ($use_new_parser) {
|
||||||
$event_time = trim($xpath->query('./td[3]', $row)->item(0)->nodeValue ?? '');
|
try {
|
||||||
$location = trim($xpath->query('./td[4]', $row)->item(0)->nodeValue ?? '');
|
require_once 'parsers/CourtParserFactory.php';
|
||||||
$event_result = trim($xpath->query('./td[5]', $row)->item(0)->nodeValue ?? '');
|
|
||||||
$event_basis = trim($xpath->query('./td[6]', $row)->item(0)->nodeValue ?? '');
|
$parser = CourtParserFactory::getParser($link, $pdo, $case_number, $uid, $skip_duplicate_check, $project_id);
|
||||||
$note = trim($xpath->query('./td[7]', $row)->item(0)->nodeValue ?? '');
|
|
||||||
$publication_date = trim($xpath->query('./td[8]', $row)->item(0)->nodeValue ?? '');
|
if ($parser === null) {
|
||||||
|
log_message("ПРЕДУПРЕЖДЕНИЕ: Не найден подходящий парсер для URL: $link. Используем fallback на старый парсер.");
|
||||||
// Логируем каждую строку
|
$use_new_parser = false; // Переключаемся на старый парсер
|
||||||
log_message("Найдено событие: $event_name, Дата: $event_date, Время: $event_time, Место: $location, Результат: $event_result, Основание: $event_basis, Примечание: $note, Дата размещения: $publication_date");
|
} else {
|
||||||
|
$parserClass = get_class($parser);
|
||||||
// Пропускаем записи, если название события не указано или дата неверная
|
log_message("Выбран парсер: $parserClass");
|
||||||
if (empty($event_name) || empty($event_date) || $event_date === '1970-01-01') {
|
|
||||||
log_message("Пропущено событие: название или дата не указаны.");
|
$last_event = $parser->parse($link, $status);
|
||||||
continue; // Пропустить итерацию
|
}
|
||||||
|
} 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('<?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");
|
||||||
|
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) {
|
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
|
||||||
]
|
]
|
||||||
]);
|
];
|
||||||
|
|
||||||
|
// Логируем ответ для отладки
|
||||||
|
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 {
|
} else {
|
||||||
echo json_encode([
|
// Всегда возвращаем last_event, даже если он пустой (для совместимости с workflow)
|
||||||
|
$response = [
|
||||||
"status" => "success",
|
"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("Парсинг завершен.");
|
log_message("Парсинг завершен.");
|
||||||
?>
|
?>
|
||||||
|
|||||||
80
parsers/BaseCourtParser.php
Normal file
80
parsers/BaseCourtParser.php
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
?>
|
||||||
|
|
||||||
36
parsers/CourtParserFactory.php
Normal file
36
parsers/CourtParserFactory.php
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
?>
|
||||||
|
|
||||||
542
parsers/MoscowCourtParser.php
Normal file
542
parsers/MoscowCourtParser.php
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
?>
|
||||||
|
|
||||||
92
parsers/RegionalCourtParser.php
Normal file
92
parsers/RegionalCourtParser.php
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
?>
|
||||||
|
|
||||||
Reference in New Issue
Block a user