- Добавлены логи в frontend (ClaimForm.tsx) для отслеживания unified_id и запросов к API - Добавлены логи в backend (claims.py) для отладки SQL запросов - Создан лог сессии с описанием проблемы и текущего состояния - Проблема: API возвращает 0 черновиков, хотя в БД есть данные
10 KiB
10 KiB
Исправленный SQL запрос для сохранения заявки
Проблема
Оригинальный SQL запрос использует $2::uuid, но передается строка "CLM-2025-11-18-GEQ3KL", что вызывает ошибку:
invalid input syntax for type uuid: "CLM-2025-11-18-GEQ3KL"
Решение
Изменить SQL запрос так, чтобы он:
- Принимал
claim_idкак строку (VARCHAR) - Искал запись в
clpr_claimsпоpayload->>'claim_id'или создавал новую - Использовал найденный UUID для дальнейших операций
Исправленный 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;
Изменения
claim_id_strвместоuuid:$2::text AS claim_id_strвместо$2::uuid AS cidclaim_lookupCTE: Находит существующую запись поpayload->>'claim_id'или генерирует новый UUIDclaim_createdCTE: Создает новую запись, если её нет- Использование
claim_uuid: Во всех местах используется UUID изclaim_lookup, а не строка claim_idвclpr_claim_documents: Преобразуется в строкуclaim_uuid::text, т.к. в таблицеclaim_idимеет типcharacter varying
Параметры запроса
// В n8n PostgreSQL Node
Parameters:
$1 = JSONB с данными (payload_partial_json)
$2 = TEXT с claim_id ("CLM-2025-11-18-GEQ3KL")
Альтернативное решение (если не хотите менять SQL)
Если не хотите менять SQL запрос, можно изменить логику в n8n:
-
Перед SQL запросом добавить Code Node, который:
- Находит запись в
clpr_claimsпоpayload->>'claim_id' - Если найдена - использует её
id(UUID) - Если не найдена - создает новую запись и возвращает её
id
- Находит запись в
-
Передавать UUID вместо строки
claim_idв SQL запрос
Но первый вариант (изменение SQL) более надежный и правильный.