Problem:
- After wizard form submission, need to wait for claim data from n8n
- Claim data comes via Redis channel claim:plan:{session_token}
- Need to display confirmation form with claim data
Solution:
1. Backend: Added SSE endpoint /api/v1/claim-plan/{session_token}
- Subscribes to Redis channel claim:plan:{session_token}
- Streams claim data from n8n to frontend
- Handles timeouts and errors gracefully
2. Frontend: Added subscription to claim:plan channel
- StepWizardPlan: After form submission, subscribes to SSE
- Waits for claim_plan_ready event
- Shows loading message while waiting
- On success: saves claimPlanData and shows confirmation step
3. New component: StepClaimConfirmation
- Displays claim confirmation form in iframe
- Receives claimPlanData from parent
- Generates HTML form (placeholder - should call n8n for real HTML)
- Handles confirmation/cancellation via postMessage
4. ClaimForm: Added conditional step for confirmation
- Shows StepClaimConfirmation when showClaimConfirmation=true
- Step appears after StepWizardPlan
- Only visible when claimPlanData is available
Flow:
1. User fills wizard form → submits
2. Form data sent to n8n via /api/v1/claims/wizard
3. Frontend subscribes to SSE /api/v1/claim-plan/{session_token}
4. n8n processes data → publishes to Redis claim:plan:{session_token}
5. Backend receives → streams to frontend via SSE
6. Frontend receives → shows StepClaimConfirmation
7. User confirms → proceeds to next step
Files:
- backend/app/api/events.py: Added stream_claim_plan endpoint
- frontend/src/components/form/StepWizardPlan.tsx: Added subscribeToClaimPlan
- frontend/src/components/form/StepClaimConfirmation.tsx: New component
- frontend/src/pages/ClaimForm.tsx: Added confirmation step to steps array
13 KiB
13 KiB
Архитектура личного кабинета и возобновления заполнения формы
Сценарии использования
1. Пользователь начинает заполнять форму
1. Вводит телефон → SMS верификация
2. Заполняет шаг 1 (полис)
3. Заполняет шаг 2 (визард)
4. Закрывает браузер (не завершил)
2. Пользователь возвращается через час/день/неделю
1. Заходит в личный кабинет
2. Видит список незавершенных заявок
3. Нажимает "Продолжить заполнение"
4. Форма должна быстро загрузиться с сохраненным состоянием
Варианты архитектуры
Вариант 1: Только PostgreSQL (простой)
Как работает:
Личный кабинет → Запрос в PostgreSQL → Получение данных → Отображение формы
Плюсы:
- ✅ Просто (один источник данных)
- ✅ Всегда актуальные данные
- ✅ Нет рассинхронизации
Минусы:
- ❌ Каждый раз запрос к PostgreSQL (1-10 мс)
- ❌ Нагрузка на БД при частых обращениях
Когда использовать:
- Небольшая нагрузка
- Простота важнее скорости
Вариант 2: PostgreSQL + Redis кеш (рекомендую)
Как работает:
При сохранении данных:
1. Сохраняем в PostgreSQL (основное хранилище)
2. Сохраняем в Redis с TTL 24 часа (быстрый доступ)
При чтении данных:
1. Пробуем Redis (быстро, 0.1-1 мс)
2. Если нет в кеше → PostgreSQL (1-10 мс)
3. Загружаем в Redis на 24 часа (для следующих обращений)
Плюсы:
- ✅ Быстрый доступ (если есть в кеше)
- ✅ Fallback на PostgreSQL (если кеш пуст)
- ✅ Автоматическая очистка (TTL 24 часа)
- ✅ Lazy loading (загружаем в Redis при первом обращении)
Минусы:
- ⚠️ Нужно обновлять оба хранилища
- ⚠️ Риск устаревших данных (если забыли обновить кеш)
Когда использовать:
- Средняя/высокая нагрузка
- Важна скорость загрузки
- Пользователи часто возвращаются к формам
Вариант 3: Только Redis с периодической синхронизацией
Как работает:
1. Основное хранилище - Redis (TTL 7 дней)
2. Периодически синхронизируем с PostgreSQL (раз в час/день)
3. При завершении формы - сохраняем в PostgreSQL
Плюсы:
- ✅ Очень быстрый доступ
- ✅ Автоматическая очистка старых сессий
Минусы:
- ❌ Риск потери данных (если Redis упал)
- ❌ Сложнее синхронизация
- ❌ Нет истории изменений
Когда использовать:
- Не рекомендуется (рискованно)
Рекомендуемая архитектура (Вариант 2)
Структура данных в Redis:
Ключ: claim:CLM-2025-11-18-GEQ3KL
Значение:
{
"claim_id": "CLM-2025-11-18-GEQ3KL",
"contact_id": "398523",
"phone": "72352352352",
"status": "draft",
"current_step": 3,
"payload": {
"answers": {...},
"wizard_plan": {...},
"documents_meta": [...]
},
"created_at": "2025-11-18T20:43:47.033Z",
"updated_at": "2025-11-18T20:44:59.217Z"
}
TTL: 24 часа (86400 секунд)
Алгоритм работы:
1. При сохранении данных (claimsave):
# В n8n workflow после SQL запроса
# 1. Сохраняем в PostgreSQL (уже сделано)
# 2. Сохраняем в Redis для быстрого доступа
redis_key = f"claim:{claim_id}"
redis_value = {
"claim_id": claim_id,
"contact_id": contact_id,
"phone": phone,
"status": "draft",
"current_step": current_step,
"payload": {
"answers": answers,
"wizard_plan": wizard_plan,
"documents_meta": documents_meta
},
"updated_at": datetime.now().isoformat()
}
await redis.set_json(
redis_key,
redis_value,
expire=86400 # 24 часа
)
2. При чтении данных (личный кабинет):
async def get_claim_for_resume(claim_id: str):
# 1. Пробуем Redis (быстро)
cached = await redis.get_json(f"claim:{claim_id}")
if cached:
logger.info(f"✅ Cache hit: {claim_id}")
return cached
# 2. Если нет в кеше - из PostgreSQL
logger.info(f"🔄 Cache miss: {claim_id}, loading from PostgreSQL")
claim = await db.get_claim_by_claim_id(claim_id)
if not claim:
return None
# 3. Формируем данные для Redis
redis_data = {
"claim_id": claim_id,
"contact_id": claim.payload.get("contact_id"),
"phone": claim.payload.get("phone"),
"status": claim.status_code,
"current_step": calculate_current_step(claim.payload),
"payload": {
"answers": claim.payload.get("answers", {}),
"wizard_plan": claim.payload.get("wizard_plan"),
"documents_meta": claim.payload.get("documents_meta", [])
},
"updated_at": claim.updated_at.isoformat()
}
# 4. Сохраняем в Redis на 24 часа (lazy loading)
await redis.set_json(f"claim:{claim_id}", redis_data, expire=86400)
return redis_data
3. При обновлении данных:
async def update_claim(claim_id: str, data: dict):
# 1. Обновляем PostgreSQL (основное хранилище)
await db.update_claim(claim_id, data)
# 2. Обновляем Redis кеш (если есть)
redis_key = f"claim:{claim_id}"
if await redis.exists(redis_key):
cached = await redis.get_json(redis_key)
if cached:
# Мерджим данные
cached.update(data)
cached["updated_at"] = datetime.now().isoformat()
await redis.set_json(redis_key, cached, expire=86400)
# Или просто удаляем кеш (при следующем чтении загрузится из PostgreSQL)
# await redis.delete(redis_key)
Стратегии TTL
Вариант A: Фиксированный TTL (24 часа)
Плюсы:
- ✅ Просто
- ✅ Автоматическая очистка старых данных
Минусы:
- ❌ Может истечь, даже если пользователь активен
Вариант B: Продлеваем TTL при обращении
Плюсы:
- ✅ Активные заявки не истекают
- ✅ Старые заявки автоматически очищаются
Минусы:
- ⚠️ Нужно продлевать TTL при каждом чтении
Реализация:
async def get_claim_with_refresh(claim_id: str):
cached = await redis.get_json(f"claim:{claim_id}")
if cached:
# Продлеваем TTL на 24 часа
await redis.expire(f"claim:{claim_id}", 86400)
return cached
# ... загрузка из PostgreSQL
Вариант C: Длинный TTL для незавершенных заявок
Плюсы:
- ✅ Незавершенные заявки хранятся долго (7 дней)
- ✅ Завершенные заявки удаляются быстро (1 час)
Реализация:
ttl = 604800 if status == "draft" else 3600 # 7 дней или 1 час
await redis.set_json(redis_key, data, expire=ttl)
Личный кабинет: Список незавершенных заявок
Как получить список:
Вариант 1: Из PostgreSQL (рекомендую)
SELECT
id,
payload->>'claim_id' as claim_id,
status_code,
payload->'answers' as answers,
updated_at
FROM clpr_claims
WHERE
payload->>'claim_id' LIKE 'CLM-%'
AND status_code IN ('draft', 'in_work')
AND channel = 'web_form'
AND updated_at > NOW() - INTERVAL '30 days'
ORDER BY updated_at DESC
LIMIT 20;
Вариант 2: Из Redis (если нужно очень быстро)
# Ищем все ключи claim:CLM-*
keys = await redis.keys("claim:CLM-*")
claims = []
for key in keys:
claim = await redis.get_json(key)
if claim and claim.get("status") in ["draft", "in_work"]:
claims.append(claim)
Проблема: Redis не предназначен для поиска по паттернам (медленно)
Решение: Использовать индекс в PostgreSQL:
CREATE INDEX idx_clpr_claims_status_channel
ON clpr_claims(status_code, channel)
WHERE status_code IN ('draft', 'in_work');
Рекомендуемая архитектура
Для веб-формы:
-
Основное хранилище: PostgreSQL (
clpr_claims)- Полные данные
- История изменений
- Надежность
-
Кеш: Redis (
claim:CLM-...)- Быстрый доступ
- TTL 24 часа
- Lazy loading (загружаем при первом обращении)
-
Алгоритм:
Чтение: 1. Redis (если есть) → возврат 2. PostgreSQL → загрузка → сохранение в Redis → возврат Запись: 1. PostgreSQL (основное) 2. Redis (обновление кеша или удаление) -
TTL стратегия:
- Незавершенные заявки (
draft,in_work): 7 дней - Завершенные заявки (
submitted): 1 час - Продлеваем TTL при обращении
- Незавершенные заявки (
Реализация в n8n
После claimsave:
// Code Node: Save to Redis
const claim = $json.claim;
const channel = $json.channel || 'web_form';
if (channel === 'web_form') {
// Определяем TTL в зависимости от статуса
const status = claim.status_code || 'draft';
const ttl = (status === 'draft' || status === 'in_work')
? 604800 // 7 дней для незавершенных
: 3600; // 1 час для завершенных
return {
redis_key: `claim:${claim.claim_id_str}`,
redis_value: JSON.stringify({
claim_id: claim.claim_id_str,
contact_id: claim.payload?.contact_id,
phone: claim.payload?.phone,
status: status,
current_step: calculateStep(claim.payload),
payload: {
answers: claim.payload?.answers,
wizard_plan: claim.payload?.wizard_plan,
documents_meta: claim.payload?.documents_meta
},
updated_at: new Date().toISOString()
}),
ttl: ttl
};
}
// Redis Node: SET with TTL
// Key: {{ $json.redis_key }}
// Value: {{ $json.redis_value }}
// TTL: {{ $json.ttl }}
При чтении (личный кабинет):
// Code Node: Get claim with cache
const claim_id = $json.claim_id;
// 1. Пробуем Redis
const cached = await redis.get(`claim:${claim_id}`);
if (cached) {
return JSON.parse(cached);
}
// 2. Если нет - из PostgreSQL
// (выполняется SQL запрос)
const claim = await postgres.get_claim(claim_id);
// 3. Сохраняем в Redis
if (claim) {
await redis.set(`claim:${claim_id}`, JSON.stringify(claim), 'EX', 86400);
}
return claim;
Итог
Рекомендуемая архитектура:
- PostgreSQL - основное хранилище (источник истины)
- Redis - кеш для быстрого доступа (TTL 24 часа, продлеваем при обращении)
- Lazy loading - загружаем в Redis при первом обращении
- Инвалидация - обновляем или удаляем кеш при изменении данных
Преимущества:
- ✅ Быстрый доступ (если есть в кеше)
- ✅ Надежность (данные в PostgreSQL)
- ✅ Автоматическая очистка (TTL)
- ✅ Гибкость (можно отключить кеш, если не нужен)
Когда использовать:
- ✅ Личный кабинет (список незавершенных заявок)
- ✅ Возобновление заполнения формы
- ✅ Быстрая загрузка состояния формы