- Добавлены логи в frontend (ClaimForm.tsx) для отслеживания unified_id и запросов к API - Добавлены логи в backend (claims.py) для отладки SQL запросов - Создан лог сессии с описанием проблемы и текущего состояния - Проблема: API возвращает 0 черновиков, хотя в БД есть данные
286 lines
10 KiB
Markdown
286 lines
10 KiB
Markdown
# Исправленный SQL запрос для сохранения заявки
|
||
|
||
## Проблема
|
||
|
||
Оригинальный SQL запрос использует `$2::uuid`, но передается строка `"CLM-2025-11-18-GEQ3KL"`, что вызывает ошибку:
|
||
```
|
||
invalid input syntax for type uuid: "CLM-2025-11-18-GEQ3KL"
|
||
```
|
||
|
||
## Решение
|
||
|
||
Изменить SQL запрос так, чтобы он:
|
||
1. Принимал `claim_id` как строку (VARCHAR)
|
||
2. Искал запись в `clpr_claims` по `payload->>'claim_id'` или создавал новую
|
||
3. Использовал найденный UUID для дальнейших операций
|
||
|
||
## Исправленный SQL запрос
|
||
|
||
```sql
|
||
WITH partial AS (
|
||
SELECT $1::jsonb AS p, $2::text AS claim_id_str
|
||
),
|
||
|
||
-- Сначала находим существующую запись или создаем новую
|
||
claim_lookup AS (
|
||
SELECT
|
||
COALESCE(
|
||
(SELECT id FROM clpr_claims WHERE payload->>'claim_id' = partial.claim_id_str LIMIT 1),
|
||
gen_random_uuid()
|
||
) AS claim_uuid
|
||
FROM partial
|
||
),
|
||
|
||
-- Если записи нет, создаем её
|
||
claim_created AS (
|
||
INSERT INTO clpr_claims (
|
||
id,
|
||
session_token,
|
||
channel,
|
||
type_code,
|
||
status_code,
|
||
payload,
|
||
created_at,
|
||
updated_at,
|
||
expires_at
|
||
)
|
||
SELECT
|
||
claim_lookup.claim_uuid,
|
||
COALESCE(partial.p->>'session_id', 'sess-' || gen_random_uuid()::text),
|
||
'web_form',
|
||
COALESCE(partial.p->>'type_code', 'consumer'),
|
||
'draft',
|
||
jsonb_build_object(
|
||
'claim_id', partial.claim_id_str,
|
||
'answers',
|
||
CASE
|
||
-- В корне
|
||
WHEN partial.p->>'wizard_answers' IS NOT NULL
|
||
THEN (partial.p->>'wizard_answers')::jsonb
|
||
-- В edit_fields_raw.body
|
||
WHEN partial.p->'edit_fields_raw'->'body'->>'wizard_answers' IS NOT NULL
|
||
THEN (partial.p->'edit_fields_raw'->'body'->>'wizard_answers')::jsonb
|
||
-- В edit_fields_parsed.body
|
||
WHEN partial.p->'edit_fields_parsed'->'body'->>'wizard_answers' IS NOT NULL
|
||
THEN (partial.p->'edit_fields_parsed'->'body'->>'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,
|
||
'documents_meta', COALESCE(partial.p->'documents_meta', '[]'::jsonb),
|
||
'wizard_plan',
|
||
CASE
|
||
-- В корне
|
||
WHEN partial.p->>'wizard_plan' IS NOT NULL
|
||
THEN (partial.p->>'wizard_plan')::jsonb
|
||
-- В edit_fields_raw.body
|
||
WHEN partial.p->'edit_fields_raw'->'body'->>'wizard_plan' IS NOT NULL
|
||
THEN (partial.p->'edit_fields_raw'->'body'->>'wizard_plan')::jsonb
|
||
-- В edit_fields_parsed.body
|
||
WHEN partial.p->'edit_fields_parsed'->'body'->>'wizard_plan' IS NOT NULL
|
||
THEN (partial.p->'edit_fields_parsed'->'body'->>'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
|
||
),
|
||
now(),
|
||
now(),
|
||
now() + interval '14 days'
|
||
FROM partial, claim_lookup
|
||
WHERE NOT EXISTS (
|
||
SELECT 1 FROM clpr_claims WHERE id = claim_lookup.claim_uuid
|
||
)
|
||
ON CONFLICT (id) DO NOTHING
|
||
RETURNING id
|
||
),
|
||
|
||
-- Получаем финальный UUID (из существующей записи или только что созданной)
|
||
claim_final AS (
|
||
SELECT
|
||
CASE
|
||
WHEN EXISTS (SELECT 1 FROM claim_created)
|
||
THEN (SELECT id FROM claim_created LIMIT 1)
|
||
ELSE claim_lookup.claim_uuid
|
||
END AS claim_uuid
|
||
FROM claim_lookup
|
||
),
|
||
|
||
inserted_docs AS (
|
||
INSERT INTO clpr_claim_documents
|
||
(claim_id, field_name, file_id, uploaded_at, file_name, original_file_name)
|
||
SELECT
|
||
claim_final.claim_uuid::text AS claim_id,
|
||
doc.field_name,
|
||
doc.file_id,
|
||
(doc.uploaded_at)::timestamptz AS uploaded_at,
|
||
doc.file_name,
|
||
doc.original_file_name
|
||
FROM partial, claim_final
|
||
CROSS JOIN LATERAL jsonb_to_recordset(
|
||
COALESCE(partial.p->'documents_meta','[]'::jsonb)
|
||
) AS doc(
|
||
field_name text,
|
||
file_id text,
|
||
file_name text,
|
||
original_file_name text,
|
||
uploaded_at text
|
||
)
|
||
ON CONFLICT (claim_id, field_name) DO UPDATE
|
||
SET file_id = EXCLUDED.file_id,
|
||
uploaded_at = EXCLUDED.uploaded_at,
|
||
file_name = EXCLUDED.file_name,
|
||
original_file_name = EXCLUDED.original_file_name
|
||
RETURNING id, claim_id, field_name, file_id
|
||
),
|
||
|
||
existing AS (
|
||
SELECT c.id, c.payload
|
||
FROM clpr_claims c, claim_final
|
||
WHERE c.id = claim_final.claim_uuid
|
||
FOR UPDATE
|
||
),
|
||
|
||
old AS (
|
||
SELECT
|
||
COALESCE(
|
||
(SELECT payload FROM existing LIMIT 1),
|
||
'{}'::jsonb
|
||
) AS old_payload
|
||
FROM claim_final
|
||
),
|
||
|
||
-- Парсим wizard_answers из строки в JSON объект
|
||
-- Ищем в разных местах: корень, edit_fields_raw.body, edit_fields_parsed.body
|
||
wizard_answers_parsed AS (
|
||
SELECT
|
||
CASE
|
||
-- В корне payload_partial_json
|
||
WHEN partial.p->>'wizard_answers' IS NOT NULL
|
||
THEN (partial.p->>'wizard_answers')::jsonb
|
||
-- В edit_fields_raw.body.wizard_answers
|
||
WHEN partial.p->'edit_fields_raw'->'body'->>'wizard_answers' IS NOT NULL
|
||
THEN (partial.p->'edit_fields_raw'->'body'->>'wizard_answers')::jsonb
|
||
-- В edit_fields_parsed.body.wizard_answers
|
||
WHEN partial.p->'edit_fields_parsed'->'body'->>'wizard_answers' IS NOT NULL
|
||
THEN (partial.p->'edit_fields_parsed'->'body'->>'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 из строки в JSON объект
|
||
-- Ищем в разных местах: корень, edit_fields_raw.body, edit_fields_parsed.body
|
||
wizard_plan_parsed AS (
|
||
SELECT
|
||
CASE
|
||
-- В корне payload_partial_json
|
||
WHEN partial.p->>'wizard_plan' IS NOT NULL
|
||
THEN (partial.p->>'wizard_plan')::jsonb
|
||
-- В edit_fields_raw.body.wizard_plan
|
||
WHEN partial.p->'edit_fields_raw'->'body'->>'wizard_plan' IS NOT NULL
|
||
THEN (partial.p->'edit_fields_raw'->'body'->>'wizard_plan')::jsonb
|
||
-- В edit_fields_parsed.body.wizard_plan
|
||
WHEN partial.p->'edit_fields_parsed'->'body'->>'wizard_plan' IS NOT NULL
|
||
THEN (partial.p->'edit_fields_parsed'->'body'->>'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
|
||
),
|
||
|
||
-- Объединяем documents_meta без дублирования (используем новый, если есть)
|
||
docs_merged AS (
|
||
SELECT
|
||
COALESCE(
|
||
NULLIF(partial.p->'documents_meta', 'null'::jsonb),
|
||
old.old_payload->'documents_meta',
|
||
'[]'::jsonb
|
||
) AS documents_meta
|
||
FROM old, partial
|
||
),
|
||
|
||
-- Формируем чистый payload (без лишних полей)
|
||
clean_payload AS (
|
||
SELECT jsonb_build_object(
|
||
'claim_id', partial.claim_id_str,
|
||
'answers', (SELECT answers FROM wizard_answers_parsed LIMIT 1),
|
||
'documents_meta', (SELECT documents_meta FROM docs_merged LIMIT 1),
|
||
'wizard_plan', (SELECT wizard_plan FROM wizard_plan_parsed LIMIT 1)
|
||
) AS clean
|
||
FROM partial
|
||
),
|
||
|
||
upd AS (
|
||
UPDATE clpr_claims c
|
||
SET
|
||
payload = (
|
||
-- Сохраняем только нужные поля из старого payload
|
||
COALESCE(old.old_payload, '{}'::jsonb) - 'answers' - 'documents_meta' - 'wizard_plan' - 'wizard_answers' - 'form_data' - 'edit_fields_raw' - 'edit_fields_parsed'
|
||
-- Добавляем чистый payload
|
||
|| (SELECT clean FROM clean_payload LIMIT 1)
|
||
),
|
||
status_code = CASE
|
||
WHEN ( (SELECT answers->>'docs_exist' FROM wizard_answers_parsed LIMIT 1) = 'true' )
|
||
THEN 'in_work'
|
||
ELSE COALESCE(c.status_code, 'draft')
|
||
END,
|
||
updated_at = now(),
|
||
expires_at = now() + interval '14 days'
|
||
FROM partial, old, claim_final, clean_payload
|
||
WHERE c.id = claim_final.claim_uuid
|
||
RETURNING c.id, c.status_code, c.payload
|
||
)
|
||
|
||
SELECT
|
||
(SELECT jsonb_build_object(
|
||
'claim_id', u.id::text,
|
||
'claim_id_str', (u.payload->>'claim_id'),
|
||
'status_code', u.status_code,
|
||
'payload', u.payload
|
||
) FROM upd u) AS claim,
|
||
(SELECT jsonb_agg(jsonb_build_object(
|
||
'id', id,
|
||
'field_name', field_name,
|
||
'file_id', file_id
|
||
)) FROM inserted_docs) AS documents;
|
||
```
|
||
|
||
## Изменения
|
||
|
||
1. **`claim_id_str` вместо `uuid`**: `$2::text AS claim_id_str` вместо `$2::uuid AS cid`
|
||
2. **`claim_lookup` CTE**: Находит существующую запись по `payload->>'claim_id'` или генерирует новый UUID
|
||
3. **`claim_created` CTE**: Создает новую запись, если её нет
|
||
4. **Использование `claim_uuid`**: Во всех местах используется UUID из `claim_lookup`, а не строка
|
||
5. **`claim_id` в `clpr_claim_documents`**: Преобразуется в строку `claim_uuid::text`, т.к. в таблице `claim_id` имеет тип `character varying`
|
||
|
||
## Параметры запроса
|
||
|
||
```javascript
|
||
// В n8n PostgreSQL Node
|
||
Parameters:
|
||
$1 = JSONB с данными (payload_partial_json)
|
||
$2 = TEXT с claim_id ("CLM-2025-11-18-GEQ3KL")
|
||
```
|
||
|
||
## Альтернативное решение (если не хотите менять SQL)
|
||
|
||
Если не хотите менять SQL запрос, можно изменить логику в n8n:
|
||
|
||
1. **Перед SQL запросом** добавить Code Node, который:
|
||
- Находит запись в `clpr_claims` по `payload->>'claim_id'`
|
||
- Если найдена - использует её `id` (UUID)
|
||
- Если не найдена - создает новую запись и возвращает её `id`
|
||
|
||
2. **Передавать UUID** вместо строки `claim_id` в SQL запрос
|
||
|
||
Но первый вариант (изменение SQL) более надежный и правильный.
|
||
|