2025-10-24 21:24:00 +03:00
|
|
|
|
"""
|
|
|
|
|
|
Draft API Routes - Автосохранение драфтов форм
|
|
|
|
|
|
"""
|
|
|
|
|
|
from fastapi import APIRouter, HTTPException
|
|
|
|
|
|
from pydantic import BaseModel
|
|
|
|
|
|
from typing import Optional, Dict, Any
|
|
|
|
|
|
from datetime import datetime
|
|
|
|
|
|
import json
|
|
|
|
|
|
from ..services.database import db
|
|
|
|
|
|
import logging
|
|
|
|
|
|
|
|
|
|
|
|
router = APIRouter(prefix="/api/v1/draft", tags=["Draft"])
|
|
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class DraftSaveRequest(BaseModel):
|
|
|
|
|
|
"""Запрос на сохранение драфта"""
|
|
|
|
|
|
session_id: str # Уникальный ID сессии пользователя
|
|
|
|
|
|
step: int # Текущий шаг формы (1, 2, 3)
|
|
|
|
|
|
data: Dict[str, Any] # Данные формы
|
|
|
|
|
|
user_agent: Optional[str] = None
|
|
|
|
|
|
ip_address: Optional[str] = None
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@router.post("/save")
|
|
|
|
|
|
async def save_draft(request: DraftSaveRequest):
|
|
|
|
|
|
"""
|
|
|
|
|
|
Автосохранение драфта формы
|
|
|
|
|
|
|
|
|
|
|
|
Используется для аналитики:
|
|
|
|
|
|
- Где пользователи бросают заполнение
|
|
|
|
|
|
- Сколько времени проводят на каждом шаге
|
|
|
|
|
|
- Какие поля вызывают проблемы
|
|
|
|
|
|
"""
|
2025-10-24 21:34:50 +03:00
|
|
|
|
# Защита: валидация session_id
|
|
|
|
|
|
if not request.session_id or len(request.session_id) > 255:
|
|
|
|
|
|
raise HTTPException(status_code=400, detail="Invalid session_id")
|
|
|
|
|
|
|
|
|
|
|
|
# Защита: валидация step
|
|
|
|
|
|
if request.step not in [1, 2, 3]:
|
|
|
|
|
|
raise HTTPException(status_code=400, detail="Invalid step number")
|
|
|
|
|
|
|
2025-10-24 21:24:00 +03:00
|
|
|
|
try:
|
|
|
|
|
|
# Сериализуем данные в JSON
|
|
|
|
|
|
form_data_json = json.dumps(request.data, ensure_ascii=False)
|
|
|
|
|
|
|
|
|
|
|
|
# SQL для upsert (insert or update)
|
|
|
|
|
|
query = """
|
|
|
|
|
|
INSERT INTO claims_draft (
|
|
|
|
|
|
session_id,
|
|
|
|
|
|
current_step,
|
|
|
|
|
|
form_data,
|
|
|
|
|
|
user_agent,
|
|
|
|
|
|
ip_address,
|
|
|
|
|
|
created_at,
|
|
|
|
|
|
updated_at
|
|
|
|
|
|
) VALUES ($1, $2, $3, $4, $5, $6, $7)
|
|
|
|
|
|
ON CONFLICT (session_id)
|
|
|
|
|
|
DO UPDATE SET
|
|
|
|
|
|
current_step = EXCLUDED.current_step,
|
|
|
|
|
|
form_data = EXCLUDED.form_data,
|
|
|
|
|
|
user_agent = EXCLUDED.user_agent,
|
|
|
|
|
|
ip_address = EXCLUDED.ip_address,
|
|
|
|
|
|
updated_at = EXCLUDED.updated_at
|
|
|
|
|
|
RETURNING id
|
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
|
|
now = datetime.now()
|
|
|
|
|
|
|
|
|
|
|
|
result = await db.fetchval(
|
|
|
|
|
|
query,
|
|
|
|
|
|
request.session_id,
|
|
|
|
|
|
request.step,
|
|
|
|
|
|
form_data_json,
|
|
|
|
|
|
request.user_agent,
|
|
|
|
|
|
request.ip_address,
|
|
|
|
|
|
now,
|
|
|
|
|
|
now
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
logger.info(f"✅ Draft saved: session={request.session_id}, step={request.step}")
|
|
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
|
"success": True,
|
|
|
|
|
|
"message": "Драфт сохранен",
|
|
|
|
|
|
"draft_id": result
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
except Exception as e:
|
|
|
|
|
|
logger.error(f"Draft save error: {e}")
|
|
|
|
|
|
# Не падаем с ошибкой - просто логируем
|
|
|
|
|
|
# Автосохранение не должно блокировать пользователя
|
|
|
|
|
|
return {
|
|
|
|
|
|
"success": False,
|
|
|
|
|
|
"message": "Ошибка сохранения драфта"
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@router.get("/stats")
|
|
|
|
|
|
async def get_draft_stats():
|
|
|
|
|
|
"""
|
|
|
|
|
|
Статистика по драфтам
|
|
|
|
|
|
|
|
|
|
|
|
Показывает:
|
|
|
|
|
|
- Сколько людей бросают на каждом шаге
|
|
|
|
|
|
- Среднее время на шаге
|
|
|
|
|
|
- Количество драфтов за период
|
|
|
|
|
|
"""
|
|
|
|
|
|
try:
|
|
|
|
|
|
# Статистика по шагам
|
|
|
|
|
|
step_stats_query = """
|
|
|
|
|
|
SELECT
|
|
|
|
|
|
current_step,
|
|
|
|
|
|
COUNT(*) as count,
|
|
|
|
|
|
COUNT(DISTINCT session_id) as unique_users
|
|
|
|
|
|
FROM claims_draft
|
|
|
|
|
|
WHERE created_at >= NOW() - INTERVAL '7 days'
|
|
|
|
|
|
GROUP BY current_step
|
|
|
|
|
|
ORDER BY current_step
|
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
|
|
step_stats = await db.fetch(step_stats_query)
|
|
|
|
|
|
|
|
|
|
|
|
# Общая статистика
|
|
|
|
|
|
total_drafts_query = """
|
|
|
|
|
|
SELECT COUNT(*) as total
|
|
|
|
|
|
FROM claims_draft
|
|
|
|
|
|
WHERE created_at >= NOW() - INTERVAL '7 days'
|
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
|
|
total = await db.fetchval(total_drafts_query)
|
|
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
|
"success": True,
|
|
|
|
|
|
"period": "last_7_days",
|
|
|
|
|
|
"total_drafts": total,
|
|
|
|
|
|
"by_step": [
|
|
|
|
|
|
{
|
|
|
|
|
|
"step": row["current_step"],
|
|
|
|
|
|
"count": row["count"],
|
|
|
|
|
|
"unique_users": row["unique_users"]
|
|
|
|
|
|
}
|
|
|
|
|
|
for row in step_stats
|
|
|
|
|
|
]
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
except Exception as e:
|
|
|
|
|
|
logger.error(f"Draft stats error: {e}")
|
|
|
|
|
|
raise HTTPException(status_code=500, detail=str(e))
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@router.get("/list")
|
|
|
|
|
|
async def list_recent_drafts(limit: int = 50):
|
|
|
|
|
|
"""
|
|
|
|
|
|
Список последних драфтов
|
|
|
|
|
|
|
|
|
|
|
|
Для просмотра что люди заполняют
|
|
|
|
|
|
"""
|
|
|
|
|
|
try:
|
|
|
|
|
|
query = """
|
|
|
|
|
|
SELECT
|
|
|
|
|
|
id,
|
|
|
|
|
|
session_id,
|
|
|
|
|
|
current_step,
|
|
|
|
|
|
form_data,
|
|
|
|
|
|
created_at,
|
|
|
|
|
|
updated_at,
|
|
|
|
|
|
user_agent,
|
|
|
|
|
|
ip_address
|
|
|
|
|
|
FROM claims_draft
|
|
|
|
|
|
ORDER BY updated_at DESC
|
|
|
|
|
|
LIMIT $1
|
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
|
|
drafts = await db.fetch(query, limit)
|
|
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
|
"success": True,
|
|
|
|
|
|
"count": len(drafts),
|
|
|
|
|
|
"drafts": [
|
|
|
|
|
|
{
|
|
|
|
|
|
"id": row["id"],
|
|
|
|
|
|
"session_id": row["session_id"],
|
|
|
|
|
|
"step": row["current_step"],
|
|
|
|
|
|
"data": json.loads(row["form_data"]) if row["form_data"] else {},
|
|
|
|
|
|
"created_at": row["created_at"].isoformat(),
|
|
|
|
|
|
"updated_at": row["updated_at"].isoformat(),
|
|
|
|
|
|
"user_agent": row["user_agent"],
|
|
|
|
|
|
"ip_address": row["ip_address"]
|
|
|
|
|
|
}
|
|
|
|
|
|
for row in drafts
|
|
|
|
|
|
]
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
except Exception as e:
|
|
|
|
|
|
logger.error(f"Draft list error: {e}")
|
|
|
|
|
|
raise HTTPException(status_code=500, detail=str(e))
|
|
|
|
|
|
|