Files
aiform_dev/docs/FIXED_SQL_QUERY.md
AI Assistant 4c8fda5f55 Добавлено логирование для отладки черновиков
- Добавлены логи в frontend (ClaimForm.tsx) для отслеживания unified_id и запросов к API
- Добавлены логи в backend (claims.py) для отладки SQL запросов
- Создан лог сессии с описанием проблемы и текущего состояния
- Проблема: API возвращает 0 черновиков, хотя в БД есть данные
2025-11-19 18:46:48 +03:00

286 lines
10 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Исправленный 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) более надежный и правильный.