Files
crm.clientright.ru/parsers/MoscowCourtParser.php

553 lines
24 KiB
PHP
Raw Normal View History

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 для истории
2025-10-17 19:45:11 +03:00
<?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, status FROM vtiger_vdnotifierpro
WHERE userid = ? AND crmid = ? AND title = ?
ORDER BY id DESC LIMIT 1";
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 для истории
2025-10-17 19:45:11 +03:00
$checkStmt = $crmPdo->prepare($checkQuery);
$checkStmt->execute([$userId, $projectId, $notificationTitle]);
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 для истории
2025-10-17 19:45:11 +03:00
$existing = $checkStmt->fetch(PDO::FETCH_ASSOC);
if ($existing) {
// Если уведомление уже есть - проверяем его статус
if ($existing['status'] == 5) {
// Уведомление непрочитанное - обновляем только время
$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 {
// Уведомление прочитано - не создаём дубликат
$this->log("Уведомление ID: {$existing['id']} уже существует (статус: {$existing['status']}), дубликат не создан");
return $existing['id'];
}
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 для истории
2025-10-17 19:45:11 +03:00
} 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;
}
}
?>