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