🎯 Основные изменения: 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
435 lines
9.8 KiB
Markdown
435 lines
9.8 KiB
Markdown
# 📝 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?** 🚀
|
||
|