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; } } ?>