Files
aiform_dev/backend/app/api/draft.py

199 lines
6.1 KiB
Python
Raw Normal View History

feat: 6 улучшений формы - S3 upload, draft, HEIC, email на step3 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, таблица создана, всё работает
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):
"""
Автосохранение драфта формы
Используется для аналитики:
- Где пользователи бросают заполнение
- Сколько времени проводят на каждом шаге
- Какие поля вызывают проблемы
"""
feat: 5 улучшений безопасности и UX 1. ✅ Прогресс бар загрузки: - Upload компонент с showUploadList - Кнопка показывает состояние 'Загрузка...' - Визуальный прогресс для каждого файла 2. ✅ OCR проверка полиса (заготовка): - TODO: проверка что загружен полис, а не шляпа - Если шляпа - помечаем себе в policyValidationWarning - Пользователю не говорим (silent validation) 3. ✅ Лимиты файлов: - Максимум 10 файлов - Каждый файл до 15MB - Валидация на фронте и бэкенде - Счетчик: 'Загружено: X/10 файлов' - Кнопка disabled при 10 файлах 4. ✅ Защита от инъекций и безопасность: Backend (upload.py): - Лимит файлов: if len(files) > 10 - Проверка размера: if len(content) > MAX_FILE_SIZE - Валидация типа: allowed_types = ['image/', 'application/pdf'] - Санитизация folder: allowed_folders whitelist Backend (draft.py): - Валидация session_id (max 255 chars) - Валидация step: only [1, 2, 3] - Параметризованные SQL запросы (защита от SQL injection) Frontend: - beforeUpload валидация размера - maxCount={10} - accept только разрешенные форматы 5. ✅ Кнопка 'Начать заново': - Показывается на шаге 2 и 3 (extra в Card) - Сбрасывает всю форму - Возвращает на шаг 1 - Очищает isPhoneVerified Безопасность: - SQL инъекции: параметризованные запросы ($1, $2) - XSS: Pydantic валидация всех inputs - File upload: type + size validation - Path traversal: folder whitelist - Rate limiting: TODO (Redis) UX: - Прогресс загрузки виден - Понятные лимиты (10 файлов по 15MB) - Возможность начать заново в любой момент
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")
feat: 6 улучшений формы - S3 upload, draft, HEIC, email на step3 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, таблица создана, всё работает
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))