fix: Улучшена защита от дубликатов уведомлений и событий
- Изменена логика проверки уведомлений: теперь проверяются ВСЕ уведомления (не только непрочитанные) - Если уведомление прочитано - дубликат НЕ создаётся (ранее создавался) - Добавлена проверка статуса уведомления перед обновлением - Добавлены уведомления для RegionalCourtParser (ранее только для MoscowCourtParser) - Создана документация DUPLICATE_PREVENTION_GUIDE.md с описанием 3 уровней защиты Теперь система полностью защищена от дубликатов: 1. Уровень событий в таблице subject 2. Уровень уведомлений в vtiger_vdnotifierpro (с проверкой статуса) 3. Уровень календаря CRM Для продакшена: НЕ передавать skip_duplicate_check=true (по умолчанию false)
This commit is contained in:
196
DUPLICATE_PREVENTION_GUIDE.md
Normal file
196
DUPLICATE_PREVENTION_GUIDE.md
Normal file
@@ -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`
|
||||
- ✅ **Обновляет время** непрочитанных уведомлений
|
||||
- ✅ **Игнорирует** повторные запуски для прочитанных уведомлений
|
||||
|
||||
**Защита работает на всех трёх уровнях!** 🛡️
|
||||
|
||||
@@ -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']]);
|
||||
// Если уведомление уже есть - проверяем его статус
|
||||
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'];
|
||||
$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)";
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
?>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user