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

432 lines
13 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.

# Архитектура личного кабинета и возобновления заполнения формы
## Сценарии использования
### 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)
- ✅ Гибкость (можно отключить кеш, если не нужен)
### Когда использовать:
- ✅ Личный кабинет (список незавершенных заявок)
- ✅ Возобновление заполнения формы
- ✅ Быстрая загрузка состояния формы