# Архитектура личного кабинета и возобновления заполнения формы ## Сценарии использования ### 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` **Значение:** ```json { "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): ```python # В 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. При чтении данных (личный кабинет): ```python 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. При обновлении данных: ```python 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 при каждом чтении **Реализация:** ```python 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 час) **Реализация:** ```python ttl = 604800 if status == "draft" else 3600 # 7 дней или 1 час await redis.set_json(redis_key, data, expire=ttl) ``` --- ## Личный кабинет: Список незавершенных заявок ### Как получить список: **Вариант 1: Из PostgreSQL (рекомендую)** ```sql 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 (если нужно очень быстро)** ```python # Ищем все ключи 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: ```sql CREATE INDEX idx_clpr_claims_status_channel ON clpr_claims(status_code, channel) WHERE status_code IN ('draft', 'in_work'); ``` --- ## Рекомендуемая архитектура ### Для веб-формы: 1. **Основное хранилище:** PostgreSQL (`clpr_claims`) - Полные данные - История изменений - Надежность 2. **Кеш:** Redis (`claim:CLM-...`) - Быстрый доступ - TTL 24 часа - Lazy loading (загружаем при первом обращении) 3. **Алгоритм:** ``` Чтение: 1. Redis (если есть) → возврат 2. PostgreSQL → загрузка → сохранение в Redis → возврат Запись: 1. PostgreSQL (основное) 2. Redis (обновление кеша или удаление) ``` 4. **TTL стратегия:** - Незавершенные заявки (`draft`, `in_work`): 7 дней - Завершенные заявки (`submitted`): 1 час - Продлеваем TTL при обращении --- ## Реализация в n8n ### После `claimsave`: ```javascript // 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 }} ``` ### При чтении (личный кабинет): ```javascript // 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; ``` --- ## Итог ### Рекомендуемая архитектура: 1. **PostgreSQL** - основное хранилище (источник истины) 2. **Redis** - кеш для быстрого доступа (TTL 24 часа, продлеваем при обращении) 3. **Lazy loading** - загружаем в Redis при первом обращении 4. **Инвалидация** - обновляем или удаляем кеш при изменении данных ### Преимущества: - ✅ Быстрый доступ (если есть в кеше) - ✅ Надежность (данные в PostgreSQL) - ✅ Автоматическая очистка (TTL) - ✅ Гибкость (можно отключить кеш, если не нужен) ### Когда использовать: - ✅ Личный кабинет (список незавершенных заявок) - ✅ Возобновление заполнения формы - ✅ Быстрая загрузка состояния формы