feat: Интеграция n8n + Redis Pub/Sub + SSE для real-time обработки заявок
🎯 Основные изменения: Backend: - ✅ Добавлен SSE endpoint для real-time событий (/api/v1/events/{task_id}) - ✅ Redis Pub/Sub для публикации/подписки на события OCR/Vision - ✅ Удален aioboto3 из requirements.txt (конфликт зависимостей) - ✅ Добавлен OCR worker (deprecated, логика перенесена в n8n) Frontend (React): - ✅ Автогенерация claim_id и session_id - ✅ Клиентская конвертация файлов в PDF (JPG/PNG/HEIC/WEBP) - ✅ Сжатие изображений до 2MB перед конвертацией - ✅ SSE подписка на события OCR/Vision в Step1Policy - ✅ Валидация документов (полис vs неподходящий контент) - ✅ Real-time прогресс загрузки и обработки файлов - ✅ Интеграция с n8n webhooks для проверки полиса и загрузки файлов n8n Workflows: - ✅ Проверка полиса в MySQL + запись в PostgreSQL - ✅ Загрузка файлов в S3 + OCR + Vision AI - ✅ Публикация событий в Redis через backend API - ✅ Валидация документов (распознавание полисов ERV) Документация: - 📝 N8N_INTEGRATION.md - интеграция с n8n - 📝 N8N_SQL_QUERIES.md - SQL запросы для workflows - 📝 N8N_PDF_COMPRESS.md - сжатие PDF - 📝 N8N_STIRLING_COMPRESS.md - интеграция Stirling-PDF Утилиты: - 🔧 monitor_redis.py/sh - мониторинг Redis Pub/Sub - 🔧 test_redis_events.sh - тестирование событий - 🔧 pdfConverter.ts - клиентская конвертация в PDF Архитектура: React → n8n webhooks (sync) → MySQL/PostgreSQL/S3 → n8n workflows (async) → OCR/Vision → Redis Pub/Sub → SSE → React
This commit is contained in:
434
N8N_SQL_QUERIES.md
Normal file
434
N8N_SQL_QUERIES.md
Normal file
@@ -0,0 +1,434 @@
|
||||
# 📝 SQL запросы для n8n вебхуков
|
||||
|
||||
## PostgreSQL Connection:
|
||||
- **Host:** `147.45.189.234`
|
||||
- **Port:** `5432`
|
||||
- **Database:** `default_db`
|
||||
- **User:** `gen_user`
|
||||
- **Password:** `2~~9_^kVsU?2\S`
|
||||
|
||||
---
|
||||
|
||||
## 1️⃣ Создание заявки (при генерации claim_id)
|
||||
|
||||
**Вебхук:** `POST /webhook/create-claim`
|
||||
|
||||
**Input:**
|
||||
```json
|
||||
{
|
||||
"claim_id": "CLM-2025-10-25-A3F7G2",
|
||||
"voucher": "E1000-302372730",
|
||||
"client_phone": "",
|
||||
"client_email": "",
|
||||
"session_id": "sess-abc-123"
|
||||
}
|
||||
```
|
||||
|
||||
**SQL (PostgreSQL Node):**
|
||||
```sql
|
||||
INSERT INTO claims (
|
||||
claim_number,
|
||||
policy_number,
|
||||
client_phone,
|
||||
client_email,
|
||||
status,
|
||||
insurance_type,
|
||||
source,
|
||||
form_data,
|
||||
created_at
|
||||
) VALUES (
|
||||
'{{ $json.body.claim_id }}',
|
||||
'{{ $json.body.voucher }}',
|
||||
'{{ $json.body.client_phone || "" }}',
|
||||
'{{ $json.body.client_email || "" }}',
|
||||
'draft',
|
||||
'erv_travel',
|
||||
'web_form',
|
||||
'{{ JSON.stringify($json.body) }}',
|
||||
NOW()
|
||||
)
|
||||
ON CONFLICT (claim_number) DO UPDATE SET
|
||||
updated_at = NOW(),
|
||||
form_data = EXCLUDED.form_data
|
||||
RETURNING id, claim_number, created_at;
|
||||
```
|
||||
|
||||
**Response:**
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"claim_id": "CLM-2025-10-25-A3F7G2",
|
||||
"db_id": "uuid-from-db",
|
||||
"created_at": "2025-10-25T10:00:00Z"
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 2️⃣ Сохранение файла в claim_files
|
||||
|
||||
**После S3 Upload в том же workflow!**
|
||||
|
||||
**SQL (PostgreSQL Node после S3):**
|
||||
```sql
|
||||
INSERT INTO claim_files (
|
||||
claim_id,
|
||||
file_name,
|
||||
file_path,
|
||||
file_size,
|
||||
mime_type,
|
||||
file_type,
|
||||
s3_bucket,
|
||||
s3_key,
|
||||
s3_url,
|
||||
ocr_status,
|
||||
created_at
|
||||
)
|
||||
SELECT
|
||||
c.id,
|
||||
'{{ $json.file.original_name }}',
|
||||
'{{ $json.s3.key }}',
|
||||
{{ $json.file.size || 0 }},
|
||||
'{{ $json.file.mime_type }}',
|
||||
'{{ $json.claim.file_type }}',
|
||||
'f9825c87-4e3558f6-f9b6-405c-ad3d-d1535c49b61c',
|
||||
'{{ $json.s3.key }}',
|
||||
'{{ $('Upload a file1').item.json.Location }}',
|
||||
'pending',
|
||||
NOW()
|
||||
FROM claims c
|
||||
WHERE c.claim_number = '{{ $json.claim.claim_id }}'
|
||||
RETURNING id as file_id, s3_url, ocr_status;
|
||||
```
|
||||
|
||||
**Response (добавь в Respond):**
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"claim_id": "CLM-2025-10-25-A3F7G2",
|
||||
"file": {
|
||||
"file_id": "uuid-from-db",
|
||||
"type": "policy_scan",
|
||||
"url": "https://s3.../policy_scan.pdf",
|
||||
"s3_key": "files/erv/ticket/CLM-xxx/policy_scan.pdf",
|
||||
"ocr_status": "pending"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 3️⃣ Обновление OCR результата
|
||||
|
||||
**OCR Workflow (после обработки):**
|
||||
|
||||
**SQL:**
|
||||
```sql
|
||||
UPDATE claim_files
|
||||
SET
|
||||
ocr_status = 'completed',
|
||||
ocr_text = '{{ $json.ocr_text }}',
|
||||
processed_at = NOW()
|
||||
WHERE id = '{{ $json.file_id }}'
|
||||
RETURNING id, ocr_status;
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 4️⃣ Обновление Vision AI результата
|
||||
|
||||
**SQL:**
|
||||
```sql
|
||||
UPDATE claim_files
|
||||
SET
|
||||
ai_extracted_data = '{{ JSON.stringify($json.ai_analysis) }}',
|
||||
processed_at = NOW()
|
||||
WHERE id = '{{ $json.file_id }}'
|
||||
RETURNING id, ai_extracted_data;
|
||||
```
|
||||
|
||||
**Пример ai_extracted_data:**
|
||||
```json
|
||||
{
|
||||
"document_type": "policy",
|
||||
"is_valid": true,
|
||||
"confidence": 0.95,
|
||||
"voucher": "E1000-302372730",
|
||||
"holder_name": "IVANOV IVAN",
|
||||
"insured_from": "01.11.2025",
|
||||
"insured_to": "30.11.2025"
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 5️⃣ Получить все файлы заявки
|
||||
|
||||
**Вебхук:** `GET /webhook/get-claim-files/{claim_id}`
|
||||
|
||||
**SQL:**
|
||||
```sql
|
||||
SELECT
|
||||
cf.id,
|
||||
cf.file_name,
|
||||
cf.file_type,
|
||||
cf.s3_url,
|
||||
cf.file_size,
|
||||
cf.ocr_status,
|
||||
cf.ocr_text,
|
||||
cf.ai_extracted_data,
|
||||
cf.created_at,
|
||||
cf.processed_at
|
||||
FROM claim_files cf
|
||||
JOIN claims c ON c.id = cf.claim_id
|
||||
WHERE c.claim_number = '{{ $parameter.claim_id }}'
|
||||
ORDER BY cf.created_at;
|
||||
```
|
||||
|
||||
**Response:**
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"claim_id": "CLM-2025-10-25-A3F7G2",
|
||||
"files": [
|
||||
{
|
||||
"file_id": "...",
|
||||
"file_type": "policy_scan",
|
||||
"s3_url": "...",
|
||||
"ocr_status": "completed",
|
||||
"ocr_text": "ЕВРОИНС...",
|
||||
"ai_extracted_data": {...}
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 6️⃣ Финальная отправка заявки
|
||||
|
||||
**SQL (обновляем статус):**
|
||||
```sql
|
||||
UPDATE claims
|
||||
SET
|
||||
status = 'submitted',
|
||||
client_phone = '{{ $json.phone }}',
|
||||
client_email = '{{ $json.email }}',
|
||||
form_data = '{{ JSON.stringify($json.form_data) }}',
|
||||
submitted_at = NOW(),
|
||||
updated_at = NOW()
|
||||
WHERE claim_number = '{{ $json.claim_id }}'
|
||||
RETURNING id, claim_number, status, submitted_at;
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 7️⃣ Публикация результатов OCR/Vision в Redis
|
||||
|
||||
**После OCR/Vision обработки - отправляем результат в React через Redis Pub/Sub**
|
||||
|
||||
### Webhook для публикации:
|
||||
|
||||
**POST** `http://147.45.189.234:8000/events/{claim_id}`
|
||||
|
||||
**Headers:**
|
||||
```
|
||||
Content-Type: application/json
|
||||
```
|
||||
|
||||
**Body (n8n Code Node):**
|
||||
```json
|
||||
{
|
||||
"event_type": "ocr_completed",
|
||||
"status": "success",
|
||||
"data": {
|
||||
"file_id": "{{ $json.file_id }}",
|
||||
"file_type": "policy_scan",
|
||||
"is_valid_document": true,
|
||||
"document_type": "ERV Travel Insurance Policy",
|
||||
"ocr_text": "E1000-302372730",
|
||||
"confidence": 0.95,
|
||||
"ai_analysis": {
|
||||
"is_policy": true,
|
||||
"contains_policy_number": true,
|
||||
"is_nsfw": false,
|
||||
"warnings": []
|
||||
}
|
||||
},
|
||||
"message": "✅ Распознан полис страхования ERV",
|
||||
"timestamp": "{{ new Date().toISOString() }}"
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Code Node для валидации документа:
|
||||
|
||||
**После OCR + Vision:**
|
||||
|
||||
```javascript
|
||||
// Получаем результаты OCR и Vision
|
||||
const ocrData = $json.ocr_result; // Из предыдущей ноды
|
||||
const visionData = $json.vision_result;
|
||||
|
||||
// Валидация документа
|
||||
const validation = {
|
||||
is_valid_document: false,
|
||||
document_type: 'unknown',
|
||||
confidence: 0,
|
||||
warnings: []
|
||||
};
|
||||
|
||||
// 1. Проверка на NSFW
|
||||
if (visionData.nsfw === true || visionData.nsfw_score > 0.7) {
|
||||
validation.warnings.push('Неподходящее содержимое изображения');
|
||||
validation.is_valid_document = false;
|
||||
validation.document_type = 'inappropriate_content';
|
||||
}
|
||||
|
||||
// 2. Проверка текста OCR на наличие номера полиса
|
||||
const policyNumberRegex = /[A-Z]\d{4}-\d{9}/;
|
||||
const hasPolicyNumber = policyNumberRegex.test(ocrData.ocr_text);
|
||||
|
||||
if (hasPolicyNumber) {
|
||||
validation.is_valid_document = true;
|
||||
validation.document_type = 'ERV Travel Insurance Policy';
|
||||
validation.confidence = 0.9;
|
||||
} else {
|
||||
validation.warnings.push('Номер полиса не найден');
|
||||
}
|
||||
|
||||
// 3. Анализ Vision описания
|
||||
const visionText = visionData.content?.toLowerCase() || '';
|
||||
const insuranceKeywords = ['страхов', 'insurance', 'полис', 'policy', 'erv'];
|
||||
const hasInsuranceKeywords = insuranceKeywords.some(kw => visionText.includes(kw));
|
||||
|
||||
if (hasInsuranceKeywords) {
|
||||
validation.confidence += 0.05;
|
||||
} else {
|
||||
validation.warnings.push('Документ не похож на страховой полис');
|
||||
validation.is_valid_document = false;
|
||||
}
|
||||
|
||||
// 4. Формируем результат для публикации в Redis
|
||||
const result = {
|
||||
file_id: $json.file_id,
|
||||
claim_id: $json.claim_id,
|
||||
event_type: 'ocr_completed',
|
||||
status: validation.is_valid_document ? 'success' : 'error',
|
||||
data: {
|
||||
file_id: $json.file_id,
|
||||
file_type: $json.file_type,
|
||||
is_valid_document: validation.is_valid_document,
|
||||
document_type: validation.document_type,
|
||||
ocr_text: ocrData.ocr_text,
|
||||
confidence: validation.confidence,
|
||||
ai_analysis: {
|
||||
is_policy: validation.is_valid_document,
|
||||
contains_policy_number: hasPolicyNumber,
|
||||
is_nsfw: visionData.nsfw,
|
||||
nsfw_score: visionData.nsfw_score,
|
||||
warnings: validation.warnings
|
||||
}
|
||||
},
|
||||
message: validation.is_valid_document
|
||||
? '✅ Распознан полис страхования ERV'
|
||||
: `❌ ${validation.warnings.join(', ')}`,
|
||||
timestamp: new Date().toISOString()
|
||||
};
|
||||
|
||||
return result;
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### HTTP Request Node (публикация в Redis):
|
||||
|
||||
**Method:** `POST`
|
||||
**URL:** `http://147.45.189.234:8000/events/{{ $json.claim_id }}`
|
||||
**Headers:**
|
||||
```json
|
||||
{
|
||||
"Content-Type": "application/json"
|
||||
}
|
||||
```
|
||||
|
||||
**Body:**
|
||||
```json
|
||||
{{ $json }}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### React подписка на события:
|
||||
|
||||
**Frontend код:**
|
||||
|
||||
```typescript
|
||||
useEffect(() => {
|
||||
if (!claimId) return;
|
||||
|
||||
// Подключаемся к SSE
|
||||
const eventSource = new EventSource(
|
||||
`http://147.45.189.234:8000/events/${claimId}`
|
||||
);
|
||||
|
||||
eventSource.onmessage = (event) => {
|
||||
const data = JSON.parse(event.data);
|
||||
|
||||
if (data.event_type === 'ocr_completed') {
|
||||
setUploadProgress(''); // Убираем крутилку
|
||||
|
||||
if (data.status === 'success' && data.data.is_valid_document) {
|
||||
message.success(data.message);
|
||||
// ✅ Полис распознан - можно продолжать
|
||||
} else {
|
||||
message.error(data.message);
|
||||
// ❌ Это не полис - показываем предупреждение
|
||||
Modal.error({
|
||||
title: 'Документ не распознан',
|
||||
content: data.data.ai_analysis.warnings.join('\n')
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
return () => eventSource.close();
|
||||
}, [claimId]);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Полный workflow в n8n:
|
||||
|
||||
```
|
||||
Webhook (file upload)
|
||||
↓
|
||||
S3 Upload
|
||||
↓
|
||||
PostgreSQL (INSERT claim_files)
|
||||
↓
|
||||
OCR Service (HTTP Request)
|
||||
↓
|
||||
Vision Service (HTTP Request)
|
||||
↓
|
||||
Code Node (валидация документа)
|
||||
↓
|
||||
IF Node: is_valid_document?
|
||||
├─ TRUE → PostgreSQL UPDATE (ocr_status = 'valid')
|
||||
│ ↓
|
||||
│ HTTP POST → /events/{claim_id} (Redis Pub/Sub)
|
||||
│ ↓
|
||||
│ Respond to Webhook: {success: true}
|
||||
│
|
||||
└─ FALSE → PostgreSQL UPDATE (ocr_status = 'invalid')
|
||||
↓
|
||||
HTTP POST → /events/{claim_id} (Redis Pub/Sub)
|
||||
↓
|
||||
Respond to Webhook: {success: true, warning: true}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
**Готово! Теперь делаем вебхуки в n8n?** 🚀
|
||||
|
||||
Reference in New Issue
Block a user