Files
aiform_prod/N8N_SQL_QUERIES.md
AI Assistant 647abf6578 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
2025-10-27 08:33:16 +03:00

435 lines
9.8 KiB
Markdown
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 📝 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?** 🚀