1. ✅ Placeholder с тире E1000-302538524 - Теперь в placeholder тоже тире 2. ✅ Email перенесен на Step3 - Убран с Step1 (проверка полиса) - Добавлен на Step3 (вместе с телефоном) - Теперь телефон + email + выплата на одном шаге 3. ✅ HEIC формат + мультилоад - Добавлена поддержка .heic, .heif (iPhone формат) - Убран maxCount - неограниченная загрузка - Параметр multiple для множественной загрузки 4. ✅ S3 Upload - Создан s3_service.py для работы с Timeweb S3 - Новый endpoint: POST /api/v1/upload/files - Поддержка мультизагрузки файлов - Автоматическая генерация уникальных имен - Файлы грузятся в S3, не локально 5. ✅ Draft автосохранение - Создана таблица claims_draft в PostgreSQL - Новый API: POST /api/v1/draft/save - GET /api/v1/draft/stats - статистика по шагам - GET /api/v1/draft/list - список последних драфтов - Для аналитики: где люди бросают заполнение 6. ✅ Миграция БД - 002_create_claims_draft.sql применена - Индексы для быстрого поиска - JSONB поле для гибкости данных Backend: - s3_service.py - сервис для S3 - draft.py - API автосохранения - upload.py - обновлен endpoint для S3 - main.py - добавлены роуты и подключения Frontend: - Step1Policy: убран email, добавлен HEIC - Step3Payment: добавлен email после телефона Статус: ✅ Backend подключен к S3, таблица создана, всё работает
191 lines
5.7 KiB
Python
191 lines
5.7 KiB
Python
"""
|
||
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):
|
||
"""
|
||
Автосохранение драфта формы
|
||
|
||
Используется для аналитики:
|
||
- Где пользователи бросают заполнение
|
||
- Сколько времени проводят на каждом шаге
|
||
- Какие поля вызывают проблемы
|
||
"""
|
||
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))
|
||
|