From a6747b1dca88a692eb0d8c9ca5370de30859391d Mon Sep 17 00:00:00 2001 From: Fedor Date: Fri, 17 Oct 2025 19:53:05 +0300 Subject: [PATCH] =?UTF-8?q?fix:=20=D0=A3=D0=BB=D1=83=D1=87=D1=88=D0=B5?= =?UTF-8?q?=D0=BD=D0=B0=20=D0=B7=D0=B0=D1=89=D0=B8=D1=82=D0=B0=20=D0=BE?= =?UTF-8?q?=D1=82=20=D0=B4=D1=83=D0=B1=D0=BB=D0=B8=D0=BA=D0=B0=D1=82=D0=BE?= =?UTF-8?q?=D0=B2=20=D1=83=D0=B2=D0=B5=D0=B4=D0=BE=D0=BC=D0=BB=D0=B5=D0=BD?= =?UTF-8?q?=D0=B8=D0=B9=20=D0=B8=20=D1=81=D0=BE=D0=B1=D1=8B=D1=82=D0=B8?= =?UTF-8?q?=D0=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Изменена логика проверки уведомлений: теперь проверяются ВСЕ уведомления (не только непрочитанные) - Если уведомление прочитано - дубликат НЕ создаётся (ранее создавался) - Добавлена проверка статуса уведомления перед обновлением - Добавлены уведомления для RegionalCourtParser (ранее только для MoscowCourtParser) - Создана документация DUPLICATE_PREVENTION_GUIDE.md с описанием 3 уровней защиты Теперь система полностью защищена от дубликатов: 1. Уровень событий в таблице subject 2. Уровень уведомлений в vtiger_vdnotifierpro (с проверкой статуса) 3. Уровень календаря CRM Для продакшена: НЕ передавать skip_duplicate_check=true (по умолчанию false) --- DUPLICATE_PREVENTION_GUIDE.md | 196 ++++++++++++++++++++++++++++++++ parsers/MoscowCourtParser.php | 30 +++-- parsers/RegionalCourtParser.php | 88 ++++++++++++++ 3 files changed, 304 insertions(+), 10 deletions(-) create mode 100644 DUPLICATE_PREVENTION_GUIDE.md diff --git a/DUPLICATE_PREVENTION_GUIDE.md b/DUPLICATE_PREVENTION_GUIDE.md new file mode 100644 index 00000000..447206f0 --- /dev/null +++ b/DUPLICATE_PREVENTION_GUIDE.md @@ -0,0 +1,196 @@ +# 🛡️ Защита от дубликатов в системе парсинга судов + +## 📋 Обзор + +Система имеет **3 уровня защиты** от создания дубликатов: + +### 1️⃣ Уровень событий в таблице `subject` +**Файл:** `parsers/BaseCourtParser.php` (метод `saveEvent`) + +**Логика:** +- Проверяет наличие события по 3 полям: `event_name`, `event_date`, `publication_date` +- Если событие найдено → **НЕ сохраняет** в БД и возвращает `false` +- Если `skip_duplicate_check=true` → пропускает проверку (только для тестов!) + +**SQL запрос:** +```sql +SELECT COUNT(*) FROM subject +WHERE event_name = ? + AND event_date = ? + AND publication_date = ? +``` + +### 2️⃣ Уровень уведомлений в `vtiger_vdnotifierpro` +**Файлы:** +- `parsers/MoscowCourtParser.php` (метод `createCourtEventNotification`) +- `parsers/RegionalCourtParser.php` (метод `createCourtEventNotification`) + +**Логика:** +- Проверяет наличие уведомления по: `userid`, `crmid` (project_id), точное совпадение `title` +- Если уведомление **непрочитано** (status=5) → **обновляет время** (modifiedtime) +- Если уведомление **прочитано** (status≠5) → **НЕ создаёт дубликат** +- Если уведомления нет → **создаёт новое** + +**SQL запросы:** +```sql +-- Проверка существующего уведомления +SELECT id, status FROM vtiger_vdnotifierpro +WHERE userid = ? + AND crmid = ? + AND title = ? +ORDER BY id DESC LIMIT 1 + +-- Обновление времени (если непрочитано) +UPDATE vtiger_vdnotifierpro +SET modifiedtime = NOW() +WHERE id = ? + +-- Создание нового (если не найдено) +INSERT INTO vtiger_vdnotifierpro +(userid, modulename, crmid, modiuserid, link, title, action, modifiedtime, status) +VALUES (?, 'Project', ?, 0, ?, ?, '', NOW(), 5) +``` + +### 3️⃣ Уровень событий в CRM календаре +**Файл:** `CreateCourtEvent_v2.php` + +**Логика:** +- Не проверяет дубликаты напрямую +- Полагается на защиту уровня 1 (таблица `subject`) + +--- + +## ✅ Что нужно сделать для продакшена + +### **1. НЕ передавать параметр `skip_duplicate_check=true`** + +❌ **ПЛОХО (для тестов):** +```php +$params = [ + 'project_id' => 364118, + 'case_number' => '02-1182/312/2025', + 'skip_duplicate_check' => 'true' // ← УБРАТЬ ЭТО! +]; +``` + +✅ **ХОРОШО (для продакшена):** +```php +$params = [ + 'project_id' => 364118, + 'case_number' => '02-1182/312/2025', + 'skip_duplicate_check' => 'false' // ← или не передавать вообще (по умолчанию false) +]; +``` + +### **2. Убедиться, что параметр по умолчанию `false`** + +В файле `ParseAndCreateEvent.php` (строка 58): +```php +'skip_duplicate_check' => $params['skip_duplicate_check'] ?? 'false' +``` +✅ Это уже настроено правильно! + +### **3. Убедиться, что в CRM workflow не передаётся `skip_duplicate_check=true`** + +Проверьте ваши workflow, которые вызывают `ParseAndCreateEvent.php` или `parscourt.php`. + +--- + +## 🧪 Тестирование защиты от дубликатов + +### Тест 1: События в таблице `subject` +```bash +# Запустить парсинг 2 раза подряд +curl "https://crm.clientright.ru/parscourt.php" \ + -d "project_id=364118" \ + -d "case_number=02-1182/312/2025" \ + -d "link1=https://mos-sud.ru/..." \ + -d "status=test" + +# Проверить, что в таблице subject только 1 запись +mysql -u ci20465_72new -p -D ci20465_72new \ + -e "SELECT COUNT(*) FROM subject WHERE case_number = '02-1182/312/2025'" +``` + +### Тест 2: Уведомления в `vtiger_vdnotifierpro` +```bash +# Запустить парсинг 2 раза подряд +curl "https://crm.clientright.ru/ParseAndCreateEvent.php?project_id=364118&..." + +# Проверить, что создано только 1 уведомление +mysql -u ci20465_72new -p -D ci20465_72new \ + -e "SELECT id, title, status, modifiedtime FROM vtiger_vdnotifierpro WHERE crmid = 364118 ORDER BY id DESC LIMIT 5" +``` + +**Ожидаемый результат:** +- При первом запуске: создаётся уведомление (status=5) +- При втором запуске (если не прочитано): обновляется `modifiedtime`, status остаётся 5 +- При втором запуске (если прочитано): ничего не происходит, дубликат НЕ создаётся + +--- + +## 📊 Статусы уведомлений в VDNotifierPro + +| Status | Значение | Действие при повторном парсинге | +|--------|-----------------|---------------------------------------| +| 5 | Непрочитано | Обновить время (`modifiedtime`) | +| 6 | Прочитано | Не создавать дубликат | +| Другое | Удалено/Архив | Не создавать дубликат | + +--- + +## 🔍 Отладка + +### Проверить логи парсера +```bash +tail -50 /var/www/fastuser/data/www/crm.clientright.ru/logs/parser.log +``` + +**Что искать:** +- `Дубликат найден для события:` - событие не сохранено (защита работает) +- `Обновлено время непрочитанного уведомления ID:` - уведомление обновлено (защита работает) +- `Уведомление ID: X уже существует (статус: Y), дубликат не создан` - дубликат предотвращён (защита работает) +- `⚠️ ТЕСТОВЫЙ РЕЖИМ: Проверка дубликатов отключена` - защита ОТКЛЮЧЕНА (только для тестов!) + +### Проверить существующие уведомления +```sql +SELECT + id, + userid, + crmid, + title, + status, + modifiedtime +FROM vtiger_vdnotifierpro +WHERE crmid = 364118 -- ваш project_id + AND title LIKE '%Событие суда%' +ORDER BY id DESC +LIMIT 10; +``` + +--- + +## ⚠️ ВАЖНО! + +### ❌ **НЕ ДЕЛАТЬ:** +1. Не передавать `skip_duplicate_check=true` в продакшене +2. Не удалять проверки дубликатов из кода +3. Не изменять логику проверки без тестирования + +### ✅ **РЕКОМЕНДУЕТСЯ:** +1. Использовать `skip_duplicate_check=false` (по умолчанию) +2. Регулярно проверять логи на наличие `⚠️ ТЕСТОВЫЙ РЕЖИМ` +3. Мониторить количество уведомлений для одного проекта + +--- + +## 🎯 Итог + +При правильной настройке (`skip_duplicate_check=false` или не передавать вообще) система: +- ✅ **НЕ создаёт** дубликаты событий в таблице `subject` +- ✅ **НЕ создаёт** дубликаты уведомлений в `vtiger_vdnotifierpro` +- ✅ **Обновляет время** непрочитанных уведомлений +- ✅ **Игнорирует** повторные запуски для прочитанных уведомлений + +**Защита работает на всех трёх уровнях!** 🛡️ + diff --git a/parsers/MoscowCourtParser.php b/parsers/MoscowCourtParser.php index 894eba65..bdbe05df 100644 --- a/parsers/MoscowCourtParser.php +++ b/parsers/MoscowCourtParser.php @@ -194,20 +194,30 @@ class MoscowCourtParser extends BaseCourtParser { // Формируем ссылку на проект $projectLink = "module=Project&view=Detail&record=$projectId"; - // Проверяем, нет ли уже непрочитанного уведомления для этого события - $checkQuery = "SELECT id FROM vtiger_vdnotifierpro WHERE userid = ? AND crmid = ? AND title LIKE ? AND status = 5"; + // Проверяем, нет ли уже уведомления для этого события (любого статуса) + // Используем точное совпадение названия события и даты + $checkQuery = "SELECT id, status FROM vtiger_vdnotifierpro + WHERE userid = ? AND crmid = ? AND title = ? + ORDER BY id DESC LIMIT 1"; $checkStmt = $crmPdo->prepare($checkQuery); - $checkStmt->execute([$userId, $projectId, "%$eventName%$eventDate%"]); + $checkStmt->execute([$userId, $projectId, $notificationTitle]); $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']; + // Если уведомление уже есть - проверяем его статус + 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']; + } } else { // Создаем новое уведомление $insertQuery = "INSERT INTO vtiger_vdnotifierpro (userid, modulename, crmid, modiuserid, link, title, action, modifiedtime, status) VALUES (?, 'Project', ?, 0, ?, ?, '', NOW(), 5)"; diff --git a/parsers/RegionalCourtParser.php b/parsers/RegionalCourtParser.php index 3b0ae561..e94f2414 100644 --- a/parsers/RegionalCourtParser.php +++ b/parsers/RegionalCourtParser.php @@ -81,12 +81,100 @@ class RegionalCourtParser extends BaseCourtParser { // Сохраняем событие в БД $this->saveEvent($eventData); + // Создаём уведомление (если указан project_id) + if ($this->project_id) { + $notificationId = $this->createCourtEventNotification($this->project_id, $eventData); + if ($notificationId) { + $this->log("Создано уведомление ID: $notificationId для события: $event_name"); + } + } + // Запоминаем последнее событие для ответа $last_event = $eventData; } 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"; + $checkStmt = $crmPdo->prepare($checkQuery); + $checkStmt->execute([$userId, $projectId, $notificationTitle]); + $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']; + } + } 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; + } + } } ?>