2025-11-21 15:57:18 +03:00
|
|
|
|
# Инструкция: Добавление обработки формы БЕЗ файлов в workflow form_get
|
|
|
|
|
|
|
|
|
|
|
|
**Дата:** 2025-11-21
|
|
|
|
|
|
**Workflow ID:** `8ZVMTsuH7Cmw7snw`
|
|
|
|
|
|
**Workflow Name:** `form_get`
|
|
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
|
|
## 🎯 Цель
|
|
|
|
|
|
|
|
|
|
|
|
Добавить ветку обработки для случая, когда форма отправляется **без файлов** (`has_files === false`).
|
|
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
|
|
## 📍 Где добавлять
|
|
|
|
|
|
|
|
|
|
|
|
В workflow `form_get` есть IF-нода **"проверка наличия файлов"** (ID: `b7497f29-dab3-41cd-aaa3-a43ee83e607c`):
|
|
|
|
|
|
|
|
|
|
|
|
- **TRUE ветка** (index 0) — файлов НЕТ → **ПУСТАЯ** ❌
|
|
|
|
|
|
- **FALSE ветка** (index 1) — файлы ЕСТЬ → существующий flow ✅
|
|
|
|
|
|
|
|
|
|
|
|
**Задача:** Добавить ноды в TRUE ветку.
|
|
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
|
|
## 📝 Пошаговая инструкция
|
|
|
|
|
|
|
|
|
|
|
|
### Шаг 1: Открыть workflow
|
|
|
|
|
|
|
|
|
|
|
|
1. Перейти в n8n: https://n8n.clientright.pro
|
|
|
|
|
|
2. Открыть workflow **"form_get"**
|
|
|
|
|
|
3. Найти ноду **"проверка наличия файлов"** (IF)
|
|
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
|
|
### Шаг 2: Добавить ноду #1 - Extract Data
|
|
|
|
|
|
|
|
|
|
|
|
**Название:** `extract_webhook_data_no_files`
|
|
|
|
|
|
**Тип:** `Edit Fields` (Set)
|
|
|
|
|
|
**Позиция:** справа от IF-ноды, выше основного flow
|
|
|
|
|
|
|
|
|
|
|
|
**Параметры:**
|
|
|
|
|
|
|
|
|
|
|
|
| Field Name | Type | Value |
|
|
|
|
|
|
|------------|------|-------|
|
|
|
|
|
|
| `session_id` | String | `={{ $('Webhook').item.json.body.session_id }}` |
|
|
|
|
|
|
| `claim_id` | String | `={{ $('Webhook').item.json.body.claim_id }}` |
|
|
|
|
|
|
| `unified_id` | String | `={{ $('Webhook').item.json.body.unified_id }}` |
|
|
|
|
|
|
| `contact_id` | String | `={{ $('Webhook').item.json.body.contact_id }}` |
|
|
|
|
|
|
| `phone` | String | `={{ $('Webhook').item.json.body.phone }}` |
|
|
|
|
|
|
| `wizard_plan` | Object | `={{ $('Webhook').item.json.body.wizard_plan }}` |
|
|
|
|
|
|
| `wizard_answers` | Object | `={{ $('Webhook').item.json.body.wizard_answers }}` |
|
|
|
|
|
|
| `type_code` | String | `={{ $('Webhook').item.json.body.type_code || 'consumer' }}` |
|
|
|
|
|
|
|
|
|
|
|
|
**Подключение:**
|
|
|
|
|
|
- Из ноды **"проверка наличия файлов"** → TRUE (верхний выход)
|
|
|
|
|
|
- К ноде **"extract_webhook_data_no_files"**
|
|
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
|
|
### Шаг 3: Добавить ноду #2 - Prepare Payload
|
|
|
|
|
|
|
|
|
|
|
|
**Название:** `prepare_payload_no_files`
|
|
|
|
|
|
**Тип:** `Edit Fields` (Set)
|
|
|
|
|
|
|
|
|
|
|
|
**Параметры:**
|
|
|
|
|
|
|
|
|
|
|
|
| Field Name | Type | Value |
|
|
|
|
|
|
|------------|------|-------|
|
|
|
|
|
|
| `payload_partial_json` | Object | См. ниже ⬇️ |
|
|
|
|
|
|
| `claim_id` | String | `={{ $json.claim_id }}` |
|
|
|
|
|
|
|
|
|
|
|
|
**Значение для `payload_partial_json`:**
|
|
|
|
|
|
|
|
|
|
|
|
```javascript
|
|
|
|
|
|
={{ {
|
|
|
|
|
|
session_id: $json.session_id,
|
|
|
|
|
|
unified_id: $json.unified_id,
|
|
|
|
|
|
contact_id: $json.contact_id,
|
|
|
|
|
|
phone: $json.phone,
|
|
|
|
|
|
type_code: $json.type_code,
|
|
|
|
|
|
wizard_plan: $json.wizard_plan,
|
|
|
|
|
|
wizard_answers: $json.wizard_answers,
|
|
|
|
|
|
documents_meta: []
|
|
|
|
|
|
} }}
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
**Подключение:**
|
|
|
|
|
|
- Из ноды **"extract_webhook_data_no_files"**
|
|
|
|
|
|
- К ноде **"prepare_payload_no_files"**
|
|
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
|
|
### Шаг 4: Добавить ноду #3 - Save to PostgreSQL
|
|
|
|
|
|
|
|
|
|
|
|
**Название:** `save_claim_no_files`
|
|
|
|
|
|
**Тип:** `Postgres`
|
|
|
|
|
|
**Operation:** `Execute Query`
|
|
|
|
|
|
|
|
|
|
|
|
**Credentials:** `timeweb_bd` (существующие)
|
|
|
|
|
|
|
|
|
|
|
|
**Query:**
|
|
|
|
|
|
|
|
|
|
|
|
```sql
|
|
|
|
|
|
WITH partial AS (
|
|
|
|
|
|
SELECT
|
|
|
|
|
|
$1::jsonb AS p,
|
|
|
|
|
|
$2::text AS claim_id_str
|
|
|
|
|
|
),
|
|
|
|
|
|
|
|
|
|
|
|
wizard_answers_parsed AS (
|
|
|
|
|
|
SELECT
|
|
|
|
|
|
CASE
|
|
|
|
|
|
WHEN partial.p->>'wizard_answers' IS NOT NULL
|
|
|
|
|
|
THEN (partial.p->>'wizard_answers')::jsonb
|
|
|
|
|
|
WHEN partial.p->'wizard_answers' IS NOT NULL
|
|
|
|
|
|
AND jsonb_typeof(partial.p->'wizard_answers') = 'object'
|
|
|
|
|
|
THEN partial.p->'wizard_answers'
|
|
|
|
|
|
ELSE '{}'::jsonb
|
|
|
|
|
|
END AS answers
|
|
|
|
|
|
FROM partial
|
|
|
|
|
|
),
|
|
|
|
|
|
|
|
|
|
|
|
wizard_plan_parsed AS (
|
|
|
|
|
|
SELECT
|
|
|
|
|
|
CASE
|
|
|
|
|
|
WHEN partial.p->>'wizard_plan' IS NOT NULL
|
|
|
|
|
|
THEN (partial.p->>'wizard_plan')::jsonb
|
|
|
|
|
|
WHEN partial.p->'wizard_plan' IS NOT NULL
|
|
|
|
|
|
AND jsonb_typeof(partial.p->'wizard_plan') = 'object'
|
|
|
|
|
|
THEN partial.p->'wizard_plan'
|
|
|
|
|
|
ELSE NULL
|
|
|
|
|
|
END AS wizard_plan
|
|
|
|
|
|
FROM partial
|
|
|
|
|
|
),
|
|
|
|
|
|
|
|
|
|
|
|
claim_upsert AS (
|
|
|
|
|
|
INSERT INTO clpr_claims (
|
|
|
|
|
|
id,
|
|
|
|
|
|
session_token,
|
|
|
|
|
|
unified_id,
|
|
|
|
|
|
contact_id,
|
|
|
|
|
|
phone,
|
|
|
|
|
|
channel,
|
|
|
|
|
|
type_code,
|
|
|
|
|
|
status_code,
|
|
|
|
|
|
payload,
|
|
|
|
|
|
created_at,
|
|
|
|
|
|
updated_at,
|
|
|
|
|
|
expires_at
|
|
|
|
|
|
)
|
|
|
|
|
|
SELECT
|
|
|
|
|
|
partial.claim_id_str::uuid,
|
|
|
|
|
|
COALESCE(partial.p->>'session_id', 'sess-unknown'),
|
|
|
|
|
|
partial.p->>'unified_id',
|
|
|
|
|
|
partial.p->>'contact_id',
|
|
|
|
|
|
partial.p->>'phone',
|
|
|
|
|
|
'web_form',
|
|
|
|
|
|
COALESCE(partial.p->>'type_code', 'consumer'),
|
|
|
|
|
|
'draft',
|
|
|
|
|
|
jsonb_build_object(
|
|
|
|
|
|
'claim_id', partial.claim_id_str,
|
|
|
|
|
|
'answers', (SELECT answers FROM wizard_answers_parsed),
|
|
|
|
|
|
'documents_meta', '[]'::jsonb,
|
|
|
|
|
|
'wizard_plan', (SELECT wizard_plan FROM wizard_plan_parsed)
|
|
|
|
|
|
),
|
|
|
|
|
|
COALESCE(
|
|
|
|
|
|
(SELECT created_at FROM clpr_claims WHERE id = partial.claim_id_str::uuid),
|
|
|
|
|
|
now()
|
|
|
|
|
|
),
|
|
|
|
|
|
now(),
|
|
|
|
|
|
now() + interval '14 days'
|
|
|
|
|
|
FROM partial
|
|
|
|
|
|
ON CONFLICT (id) DO UPDATE SET
|
|
|
|
|
|
session_token = EXCLUDED.session_token,
|
|
|
|
|
|
unified_id = COALESCE(EXCLUDED.unified_id, clpr_claims.unified_id),
|
|
|
|
|
|
contact_id = COALESCE(EXCLUDED.contact_id, clpr_claims.contact_id),
|
|
|
|
|
|
phone = COALESCE(EXCLUDED.phone, clpr_claims.phone),
|
|
|
|
|
|
status_code = 'draft',
|
|
|
|
|
|
payload = (
|
|
|
|
|
|
clpr_claims.payload
|
|
|
|
|
|
- 'answers'
|
|
|
|
|
|
- 'documents_meta'
|
|
|
|
|
|
- 'wizard_plan'
|
|
|
|
|
|
- 'claim_id'
|
|
|
|
|
|
) || EXCLUDED.payload,
|
|
|
|
|
|
updated_at = now(),
|
|
|
|
|
|
expires_at = now() + interval '14 days'
|
|
|
|
|
|
RETURNING id, status_code, payload, unified_id, contact_id, phone, session_token
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
SELECT
|
|
|
|
|
|
(SELECT jsonb_build_object(
|
|
|
|
|
|
'claim_id', cu.id::text,
|
|
|
|
|
|
'claim_id_str', (cu.payload->>'claim_id'),
|
|
|
|
|
|
'status_code', cu.status_code,
|
|
|
|
|
|
'unified_id', cu.unified_id,
|
|
|
|
|
|
'contact_id', cu.contact_id,
|
|
|
|
|
|
'phone', cu.phone,
|
|
|
|
|
|
'session_token', cu.session_token,
|
|
|
|
|
|
'payload', cu.payload
|
|
|
|
|
|
) FROM claim_upsert cu) AS claim;
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
**Query Replacement:** `={{ $json.payload_partial_json }}, {{ $json.claim_id }}`
|
|
|
|
|
|
|
|
|
|
|
|
**Подключение:**
|
|
|
|
|
|
- Из ноды **"prepare_payload_no_files"**
|
|
|
|
|
|
- К ноде **"save_claim_no_files"**
|
|
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
|
|
### Шаг 5: Добавить ноду #4 - Prepare Redis Event
|
|
|
|
|
|
|
|
|
|
|
|
**Название:** `prepare_redis_event_no_files`
|
|
|
|
|
|
**Тип:** `Edit Fields` (Set)
|
|
|
|
|
|
|
|
|
|
|
|
**Параметры:**
|
|
|
|
|
|
|
|
|
|
|
|
| Field Name | Type | Value |
|
|
|
|
|
|
|------------|------|-------|
|
|
|
|
|
|
| `redis_key` | String | `=ocr_events:{{ $('extract_webhook_data_no_files').item.json.session_id }}` |
|
|
|
|
|
|
| `redis_value` | Object | См. ниже ⬇️ |
|
|
|
|
|
|
|
|
|
|
|
|
**Значение для `redis_value`:**
|
|
|
|
|
|
|
|
|
|
|
|
```javascript
|
|
|
|
|
|
={{ {
|
|
|
|
|
|
event_type: 'form_saved_no_files',
|
|
|
|
|
|
claim_id: $json.claim.claim_id,
|
|
|
|
|
|
status_code: $json.claim.status_code,
|
|
|
|
|
|
unified_id: $json.claim.unified_id,
|
|
|
|
|
|
contact_id: $json.claim.contact_id,
|
|
|
|
|
|
phone: $json.claim.phone,
|
|
|
|
|
|
session_token: $json.claim.session_token,
|
|
|
|
|
|
has_files: false,
|
|
|
|
|
|
timestamp: new Date().toISOString()
|
|
|
|
|
|
} }}
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
**Подключение:**
|
|
|
|
|
|
- Из ноды **"save_claim_no_files"**
|
|
|
|
|
|
- К ноде **"prepare_redis_event_no_files"**
|
|
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
|
|
### Шаг 6: Добавить ноду #5 - Publish to Redis
|
|
|
|
|
|
|
|
|
|
|
|
**Название:** `publish_to_redis_no_files`
|
|
|
|
|
|
**Тип:** `Redis`
|
|
|
|
|
|
**Operation:** `Publish`
|
|
|
|
|
|
|
|
|
|
|
|
**Credentials:** `Local Redis` (существующие)
|
|
|
|
|
|
|
|
|
|
|
|
**Параметры:**
|
|
|
|
|
|
|
|
|
|
|
|
| Parameter | Value |
|
|
|
|
|
|
|-----------|-------|
|
|
|
|
|
|
| Channel | `={{ $json.redis_key }}` |
|
|
|
|
|
|
| Value | `={{ JSON.stringify($json.redis_value) }}` |
|
|
|
|
|
|
|
|
|
|
|
|
**Подключение:**
|
|
|
|
|
|
- Из ноды **"prepare_redis_event_no_files"**
|
|
|
|
|
|
- К ноде **"publish_to_redis_no_files"**
|
|
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
|
|
### Шаг 7: Добавить ноду #6 - Respond to Webhook
|
|
|
|
|
|
|
|
|
|
|
|
**Название:** `respond_no_files`
|
|
|
|
|
|
**Тип:** `Respond to Webhook`
|
|
|
|
|
|
**Response Code:** `200`
|
|
|
|
|
|
|
|
|
|
|
|
**Response Body:**
|
|
|
|
|
|
|
|
|
|
|
|
```javascript
|
|
|
|
|
|
={{ {
|
|
|
|
|
|
success: true,
|
|
|
|
|
|
claim_id: $('save_claim_no_files').item.json.claim.claim_id,
|
|
|
|
|
|
status_code: $('save_claim_no_files').item.json.claim.status_code,
|
|
|
|
|
|
has_files: false,
|
|
|
|
|
|
message: 'Заявка сохранена без файлов'
|
|
|
|
|
|
} }}
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
**Подключение:**
|
|
|
|
|
|
- Из ноды **"publish_to_redis_no_files"**
|
|
|
|
|
|
- К ноде **"respond_no_files"** (финальная нода)
|
|
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
|
|
## 🔄 Финальная структура workflow
|
|
|
|
|
|
|
|
|
|
|
|
```
|
|
|
|
|
|
Webhook → Code17 (парсинг файлов)
|
|
|
|
|
|
↓
|
|
|
|
|
|
IF "проверка наличия файлов"
|
|
|
|
|
|
│
|
|
|
|
|
|
├─ TRUE (файлов НЕТ) → [НОВАЯ ВЕТКА]
|
|
|
|
|
|
│ ↓
|
|
|
|
|
|
│ extract_webhook_data_no_files
|
|
|
|
|
|
│ ↓
|
|
|
|
|
|
│ prepare_payload_no_files
|
|
|
|
|
|
│ ↓
|
|
|
|
|
|
│ save_claim_no_files (PostgreSQL)
|
|
|
|
|
|
│ ↓
|
|
|
|
|
|
│ prepare_redis_event_no_files
|
|
|
|
|
|
│ ↓
|
|
|
|
|
|
│ publish_to_redis_no_files
|
|
|
|
|
|
│ ↓
|
|
|
|
|
|
│ respond_no_files
|
|
|
|
|
|
│
|
|
|
|
|
|
└─ FALSE (файлы ЕСТЬ) → существующий flow
|
|
|
|
|
|
↓
|
|
|
|
|
|
set_token1 → get_data1 → Upload → ...
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
|
|
## ✅ Проверка
|
|
|
|
|
|
|
|
|
|
|
|
После добавления нод:
|
|
|
|
|
|
|
|
|
|
|
|
1. **Сохранить workflow** (Ctrl+S)
|
|
|
|
|
|
2. **Активировать workflow** (если не активен)
|
|
|
|
|
|
3. **Протестировать:**
|
|
|
|
|
|
- Отправить форму БЕЗ файлов
|
|
|
|
|
|
- Проверить, что заявка сохранилась в PostgreSQL
|
|
|
|
|
|
- Проверить событие в Redis: `redis-cli GET "ocr_events:sess-xxx"`
|
|
|
|
|
|
- Проверить ответ webhook: `success: true, has_files: false`
|
|
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
|
|
## 📊 Ожидаемый результат
|
|
|
|
|
|
|
|
|
|
|
|
**Вход (webhook body):**
|
|
|
|
|
|
```json
|
|
|
|
|
|
{
|
|
|
|
|
|
"session_id": "sess_xxx",
|
|
|
|
|
|
"claim_id": "uuid",
|
|
|
|
|
|
"unified_id": "usr_xxx",
|
|
|
|
|
|
"contact_id": "12345",
|
|
|
|
|
|
"phone": "79262306381",
|
|
|
|
|
|
"wizard_answers": {"q1": "answer1"},
|
|
|
|
|
|
"wizard_plan": null
|
|
|
|
|
|
}
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
**Выход (claim в PostgreSQL):**
|
|
|
|
|
|
```json
|
|
|
|
|
|
{
|
|
|
|
|
|
"claim": {
|
|
|
|
|
|
"claim_id": "uuid",
|
|
|
|
|
|
"status_code": "draft",
|
|
|
|
|
|
"unified_id": "usr_xxx",
|
|
|
|
|
|
"contact_id": "12345",
|
|
|
|
|
|
"phone": "79262306381",
|
|
|
|
|
|
"session_token": "sess_xxx",
|
|
|
|
|
|
"payload": {
|
|
|
|
|
|
"claim_id": "uuid",
|
|
|
|
|
|
"answers": {"q1": "answer1"},
|
|
|
|
|
|
"documents_meta": [],
|
|
|
|
|
|
"wizard_plan": null
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
**Redis событие:**
|
|
|
|
|
|
```json
|
|
|
|
|
|
{
|
|
|
|
|
|
"event_type": "form_saved_no_files",
|
|
|
|
|
|
"claim_id": "uuid",
|
|
|
|
|
|
"status_code": "draft",
|
|
|
|
|
|
"has_files": false,
|
|
|
|
|
|
"timestamp": "2025-11-21T15:00:00.000Z"
|
|
|
|
|
|
}
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
|
|
## 🛠️ Troubleshooting
|
|
|
|
|
|
|
|
|
|
|
|
### Проблема 1: "session_token": "sess-unknown"
|
|
|
|
|
|
**Причина:** В payload не передан `session_id`
|
|
|
|
|
|
**Решение:** Проверить, что фронтенд отправляет `session_id` в body
|
|
|
|
|
|
|
|
|
|
|
|
### Проблема 2: "contact_id": null
|
|
|
|
|
|
**Причина:** Поле не извлекается из webhook
|
|
|
|
|
|
**Решение:** Проверить путь в expression: `$('Webhook').item.json.body.contact_id`
|
|
|
|
|
|
|
|
|
|
|
|
### Проблема 3: Ошибка PostgreSQL
|
|
|
|
|
|
**Причина:** Неправильный формат данных
|
|
|
|
|
|
**Решение:** Проверить логи n8n и формат `payload_partial_json`
|
|
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
|
|
**Автор:** AI Assistant
|
|
|
|
|
|
**Дата:** 2025-11-21
|
|
|
|
|
|
**Статус:** Готово к внедрению ✅
|
|
|
|
|
|
|
2025-11-24 13:36:14 +03:00
|
|
|
|
|