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:
AI Assistant
2025-10-27 08:33:16 +03:00
parent 1be922fdc3
commit 647abf6578
20 changed files with 2177 additions and 168 deletions

View File

@@ -1,5 +1,5 @@
import { Form, Input, Button, Select, DatePicker, Upload, message } from 'antd';
import { UploadOutlined } from '@ant-design/icons';
import { Form, Input, Button, Select, DatePicker, Upload, message, Spin, Alert } from 'antd';
import { UploadOutlined, LoadingOutlined } from '@ant-design/icons';
import { useState } from 'react';
import type { UploadFile } from 'antd/es/upload/interface';
import dayjs from 'dayjs';
@@ -29,6 +29,7 @@ export default function Step2Details({ formData, updateFormData, onNext, onPrev,
const [form] = Form.useForm();
const [fileList, setFileList] = useState<UploadFile[]>([]);
const [uploading, setUploading] = useState(false);
const [uploadProgress, setUploadProgress] = useState('');
const handleNext = async () => {
try {
@@ -37,28 +38,61 @@ export default function Step2Details({ formData, updateFormData, onNext, onPrev,
// Если есть файлы - загружаем
if (fileList.length > 0) {
setUploading(true);
setUploadProgress('📤 Подготавливаем документы...');
addDebugEvent?.('upload', 'pending', `📤 Загружаю ${fileList.length} документ(ов) в S3...`, {
addDebugEvent?.('upload', 'pending', `📤 Загружаю ${fileList.length} документ(ов) в S3 через n8n...`, {
count: fileList.length
});
const formData = new FormData();
fileList.forEach((file: any) => {
if (file.originFileObj) {
formData.append('files', file.originFileObj);
// Используем claim_id из formData (уже сгенерирован в Step1)
const claimId = formData.claim_id;
// Загружаем каждый документ через n8n вебхук
const uploadedFiles = [];
for (let i = 0; i < fileList.length; i++) {
const file = fileList[i];
if (!file.originFileObj) continue;
setUploadProgress(`📡 Загружаем документ ${i + 1} из ${fileList.length}: ${file.name}...`);
const uploadFormData = new FormData();
uploadFormData.append('claim_id', claimId);
uploadFormData.append('file_type', `document_${i + 1}`); // document_1, document_2, etc
uploadFormData.append('filename', file.name);
uploadFormData.append('voucher', formData.voucher || '');
uploadFormData.append('session_id', sessionStorage.getItem('session_id') || 'unknown');
uploadFormData.append('upload_timestamp', new Date().toISOString());
uploadFormData.append('file', file.originFileObj);
const uploadResponse = await fetch('https://n8n.clientright.pro/webhook/7e2abc64-eaca-4671-86e4-12786700fe95', {
method: 'POST',
body: uploadFormData,
});
setUploadProgress(`🔍 Обрабатываем документ ${i + 1} из ${fileList.length}...`);
const uploadResult = await uploadResponse.json();
const resultData = Array.isArray(uploadResult) ? uploadResult[0] : uploadResult;
if (resultData?.success) {
uploadedFiles.push({
filename: file.name,
success: true
});
}
});
}
const uploadResponse = await fetch('http://147.45.146.17:8100/api/v1/upload/files?folder=documents', {
method: 'POST',
body: formData,
});
const uploadResult = await uploadResponse.json();
const uploadResult = {
success: uploadedFiles.length > 0,
uploaded_count: uploadedFiles.length,
total_count: fileList.length,
files: uploadedFiles
};
if (uploadResult.success) {
addDebugEvent?.('upload', 'success', `✅ Документы загружены: ${uploadResult.uploaded_count}/${uploadResult.total_count}`, {
files: uploadResult.files
addDebugEvent?.('upload', 'success', `✅ Документы загружены через n8n: ${uploadResult.uploaded_count}/${uploadResult.total_count}`, {
files: uploadResult.files,
claim_id: claimId
});
updateFormData({
@@ -68,10 +102,12 @@ export default function Step2Details({ formData, updateFormData, onNext, onPrev,
} else {
message.error('Ошибка загрузки документов');
setUploading(false);
setUploadProgress('');
return;
}
setUploading(false);
setUploadProgress('');
} else {
updateFormData(values);
}
@@ -80,6 +116,7 @@ export default function Step2Details({ formData, updateFormData, onNext, onPrev,
} catch (error) {
message.error('Заполните все обязательные поля');
setUploading(false);
setUploadProgress('');
}
};
@@ -226,9 +263,18 @@ export default function Step2Details({ formData, updateFormData, onNext, onPrev,
message.error(`${file.name}: файл больше 15MB`);
return Upload.LIST_IGNORE;
}
const validTypes = ['image/jpeg', 'image/jpg', 'image/png', 'image/webp', 'application/pdf'];
const validExtensions = /\.(jpg|jpeg|png|pdf|heic|heif|webp)$/i;
if (!validTypes.includes(file.type) && !validExtensions.test(file.name)) {
message.error(`${file.name}: неподдерживаемый формат`);
return Upload.LIST_IGNORE;
}
return false;
}}
accept="image/*,.pdf,.heic,.heif"
accept="image/*,.pdf,.heic,.heif,.webp"
multiple
maxCount={5}
>
@@ -254,13 +300,23 @@ export default function Step2Details({ formData, updateFormData, onNext, onPrev,
message.error(`${file.name}: файл больше 15MB`);
return Upload.LIST_IGNORE;
}
if (fileList.length >= 10) {
message.error('Максимум 10 файлов');
return Upload.LIST_IGNORE;
}
const validTypes = ['image/jpeg', 'image/jpg', 'image/png', 'image/webp', 'application/pdf'];
const validExtensions = /\.(jpg|jpeg|png|pdf|heic|heif|webp)$/i;
if (!validTypes.includes(file.type) && !validExtensions.test(file.name)) {
message.error(`${file.name}: неподдерживаемый формат`);
return Upload.LIST_IGNORE;
}
return false;
}}
accept="image/*,.pdf,.heic,.heif"
accept="image/*,.pdf,.heic,.heif,.webp"
multiple
maxCount={10}
showUploadList={{
@@ -277,9 +333,20 @@ export default function Step2Details({ formData, updateFormData, onNext, onPrev,
</div>
</Form.Item>
{/* Прогресс обработки */}
{uploading && uploadProgress && (
<Alert
message={uploadProgress}
type="info"
showIcon
icon={<Spin indicator={<LoadingOutlined style={{ fontSize: 16 }} spin />} />}
style={{ marginBottom: 16, marginTop: 16 }}
/>
)}
<Form.Item>
<div style={{ display: 'flex', gap: 8, marginTop: 32 }}>
<Button onClick={onPrev} size="large">Назад</Button>
<Button onClick={onPrev} size="large" disabled={uploading}>Назад</Button>
<Button
type="primary"
onClick={handleNext}
@@ -287,7 +354,7 @@ export default function Step2Details({ formData, updateFormData, onNext, onPrev,
style={{ flex: 1 }}
size="large"
>
{uploading ? 'Загрузка документов...' : 'Далее'}
{uploading ? 'Обрабатываем...' : 'Далее'}
</Button>
</div>
</Form.Item>