- Исправлена потеря документов при обновлении черновика (SQL объединяет вместо перезаписи) - Исправлено определение типа документа (приоритет field_label над field_name) - Исправлены дубликаты в documents_meta и documents_uploaded - Добавлена передача group_index с фронтенда для правильного field_name - Исправлены все документы в таблице clpr_claim_documents с правильными field_name - Обновлены SQL запросы: claimsave и claimsave_final для нового флоу - Добавлена поддержка multi-file upload для одного документа - Исправлены дубликаты в списке загруженных документов на фронтенде Файлы: - SQL: SQL_CLAIMSAVE_FIXED_NEW_FLOW.sql, SQL_CLAIMSAVE_FINAL_FIXED_NEW_FLOW_WITH_UPLOADED.sql - n8n: N8N_CODE_PROCESS_UPLOADED_FILES_FIXED.js (поддержка group_index) - Backend: documents.py (передача group_index в n8n) - Frontend: StepWizardPlan.tsx (передача group_index, исправление дубликатов) - Скрипты: fix_claim_documents_field_names.py, fix_documents_meta_duplicates.py Результат: документы больше не теряются, имеют правильные типы и field_name
910 lines
33 KiB
Python
910 lines
33 KiB
Python
"""
|
||
Documents API Routes - Загрузка и обработка документов
|
||
|
||
Новый флоу: поэкранная загрузка документов
|
||
"""
|
||
from fastapi import APIRouter, HTTPException, UploadFile, File, Form, Request
|
||
from typing import Optional, List
|
||
import httpx
|
||
import json
|
||
import uuid
|
||
from datetime import datetime
|
||
import logging
|
||
from ..services.redis_service import redis_service
|
||
from ..config import settings
|
||
|
||
router = APIRouter(prefix="/api/v1/documents", tags=["Documents"])
|
||
logger = logging.getLogger(__name__)
|
||
|
||
# n8n webhook для загрузки документов
|
||
N8N_DOCUMENT_UPLOAD_WEBHOOK = "https://n8n.clientright.pro/webhook/webform_document_upload"
|
||
|
||
|
||
@router.post("/upload")
|
||
async def upload_document(
|
||
request: Request,
|
||
file: UploadFile = File(...),
|
||
claim_id: str = Form(...),
|
||
session_id: str = Form(...),
|
||
document_type: str = Form(...),
|
||
document_name: Optional[str] = Form(None),
|
||
document_description: Optional[str] = Form(None),
|
||
unified_id: Optional[str] = Form(None),
|
||
contact_id: Optional[str] = Form(None),
|
||
phone: Optional[str] = Form(None),
|
||
):
|
||
"""
|
||
Загрузка одного документа.
|
||
|
||
Принимает файл и метаданные, отправляет в n8n для:
|
||
1. Сохранения в S3
|
||
2. OCR обработки
|
||
3. Обновления черновика в PostgreSQL
|
||
|
||
После успешной обработки n8n публикует событие document_ocr_completed в Redis.
|
||
"""
|
||
try:
|
||
# Генерируем уникальный ID файла
|
||
file_id = f"doc_{uuid.uuid4().hex[:12]}"
|
||
|
||
logger.info(
|
||
"📤 Document upload received",
|
||
extra={
|
||
"claim_id": claim_id,
|
||
"session_id": session_id,
|
||
"document_type": document_type,
|
||
"file_name": file.filename,
|
||
"file_size": file.size if hasattr(file, 'size') else 'unknown',
|
||
"content_type": file.content_type,
|
||
},
|
||
)
|
||
|
||
# Читаем содержимое файла
|
||
file_content = await file.read()
|
||
file_size = len(file_content)
|
||
|
||
# Получаем IP клиента
|
||
client_ip = request.client.host if request.client else "unknown"
|
||
forwarded_for = request.headers.get("x-forwarded-for", "").split(",")[0].strip()
|
||
if forwarded_for:
|
||
client_ip = forwarded_for
|
||
|
||
# Формируем данные в формате совместимом с существующим n8n воркфлоу
|
||
form_data = {
|
||
# Основные идентификаторы
|
||
"form_id": "ticket_form",
|
||
"stage": "document_upload",
|
||
"session_id": session_id,
|
||
"claim_id": claim_id,
|
||
"client_ip": client_ip,
|
||
|
||
# Идентификаторы пользователя
|
||
"unified_id": unified_id or "",
|
||
"contact_id": contact_id or "",
|
||
"phone": phone or "",
|
||
|
||
# Информация о документе
|
||
"document_type": document_type,
|
||
"file_id": file_id,
|
||
"original_filename": file.filename,
|
||
"content_type": file.content_type or "application/octet-stream",
|
||
"file_size": str(file_size),
|
||
"upload_timestamp": datetime.utcnow().isoformat(),
|
||
|
||
# Формат uploads_* для совместимости
|
||
"uploads_field_names[0]": document_type,
|
||
"uploads_field_labels[0]": document_name or document_type,
|
||
"uploads_descriptions[0]": document_description or "",
|
||
}
|
||
|
||
# Файл для multipart (ключ uploads[0] для совместимости)
|
||
files = {
|
||
"uploads[0]": (file.filename, file_content, file.content_type or "application/octet-stream")
|
||
}
|
||
|
||
# Отправляем в n8n
|
||
async with httpx.AsyncClient(timeout=120.0) as client:
|
||
response = await client.post(
|
||
N8N_DOCUMENT_UPLOAD_WEBHOOK,
|
||
data=form_data,
|
||
files=files,
|
||
)
|
||
|
||
response_text = response.text or ""
|
||
|
||
if response.status_code == 200:
|
||
logger.info(
|
||
"✅ Document uploaded to n8n",
|
||
extra={
|
||
"claim_id": claim_id,
|
||
"document_type": document_type,
|
||
"file_id": file_id,
|
||
"response_preview": response_text[:200],
|
||
},
|
||
)
|
||
|
||
# Парсим ответ от n8n
|
||
try:
|
||
n8n_response = json.loads(response_text)
|
||
except json.JSONDecodeError:
|
||
n8n_response = {"raw": response_text}
|
||
|
||
# Публикуем событие в Redis для фронтенда
|
||
event_data = {
|
||
"event_type": "document_uploaded",
|
||
"status": "processing",
|
||
"claim_id": claim_id,
|
||
"session_id": session_id,
|
||
"document_type": document_type,
|
||
"file_id": file_id,
|
||
"original_filename": file.filename,
|
||
"timestamp": datetime.utcnow().isoformat(),
|
||
}
|
||
|
||
await redis_service.publish(
|
||
f"ocr_events:{session_id}",
|
||
json.dumps(event_data, ensure_ascii=False)
|
||
)
|
||
|
||
return {
|
||
"success": True,
|
||
"file_id": file_id,
|
||
"document_type": document_type,
|
||
"ocr_status": "processing",
|
||
"message": "Документ загружен и отправлен на обработку",
|
||
"n8n_response": n8n_response,
|
||
}
|
||
|
||
else:
|
||
logger.error(
|
||
"❌ n8n document upload error",
|
||
extra={
|
||
"status_code": response.status_code,
|
||
"body": response_text[:500],
|
||
},
|
||
)
|
||
raise HTTPException(
|
||
status_code=response.status_code,
|
||
detail=f"Ошибка n8n: {response_text}",
|
||
)
|
||
|
||
except httpx.TimeoutException:
|
||
logger.error("⏱️ n8n document upload timeout")
|
||
raise HTTPException(status_code=504, detail="Таймаут загрузки документа")
|
||
|
||
except HTTPException:
|
||
raise
|
||
|
||
except Exception as e:
|
||
logger.exception("❌ Document upload error")
|
||
raise HTTPException(
|
||
status_code=500,
|
||
detail=f"Ошибка загрузки документа: {str(e)}",
|
||
)
|
||
|
||
|
||
@router.post("/upload-multiple")
|
||
async def upload_multiple_documents(
|
||
request: Request,
|
||
files: List[UploadFile] = File(...),
|
||
claim_id: str = Form(...),
|
||
session_id: str = Form(...),
|
||
document_type: str = Form(...),
|
||
document_name: Optional[str] = Form(None),
|
||
document_description: Optional[str] = Form(None),
|
||
unified_id: Optional[str] = Form(None),
|
||
contact_id: Optional[str] = Form(None),
|
||
phone: Optional[str] = Form(None),
|
||
):
|
||
"""
|
||
Загрузка нескольких файлов для одного документа (например, несколько страниц паспорта).
|
||
Все файлы отправляются одним запросом в n8n.
|
||
"""
|
||
try:
|
||
logger.info(
|
||
"📤 Multiple documents upload received",
|
||
extra={
|
||
"claim_id": claim_id,
|
||
"session_id": session_id,
|
||
"document_type": document_type,
|
||
"files_count": len(files),
|
||
"file_names": [f.filename for f in files],
|
||
},
|
||
)
|
||
|
||
# Получаем IP клиента
|
||
client_ip = request.client.host if request.client else "unknown"
|
||
forwarded_for = request.headers.get("x-forwarded-for", "").split(",")[0].strip()
|
||
if forwarded_for:
|
||
client_ip = forwarded_for
|
||
|
||
# Генерируем ID для каждого файла и читаем контент
|
||
file_ids = []
|
||
files_multipart = {}
|
||
|
||
for i, file in enumerate(files):
|
||
file_id = f"doc_{uuid.uuid4().hex[:12]}"
|
||
file_ids.append(file_id)
|
||
|
||
file_content = await file.read()
|
||
files_multipart[f"uploads[{i}]"] = (
|
||
file.filename,
|
||
file_content,
|
||
file.content_type or "application/octet-stream"
|
||
)
|
||
|
||
# Формируем данные формы
|
||
form_data = {
|
||
# Основные идентификаторы
|
||
"form_id": "ticket_form",
|
||
"stage": "document_upload",
|
||
"session_id": session_id,
|
||
"claim_id": claim_id,
|
||
"client_ip": client_ip,
|
||
|
||
# Идентификаторы пользователя
|
||
"unified_id": unified_id or "",
|
||
"contact_id": contact_id or "",
|
||
"phone": phone or "",
|
||
|
||
# Информация о документе
|
||
"document_type": document_type,
|
||
"files_count": str(len(files)),
|
||
"upload_timestamp": datetime.utcnow().isoformat(),
|
||
}
|
||
|
||
# ✅ Получаем group_index из Form (индекс документа в documents_required)
|
||
form_params = await request.form()
|
||
group_index = form_params.get("group_index")
|
||
if group_index:
|
||
form_data["group_index"] = group_index
|
||
logger.info(f"📋 group_index передан в n8n: {group_index}")
|
||
|
||
# Добавляем информацию о каждом файле
|
||
for i, (file, file_id) in enumerate(zip(files, file_ids)):
|
||
form_data[f"file_ids[{i}]"] = file_id
|
||
form_data[f"uploads_field_names[{i}]"] = document_type
|
||
form_data[f"uploads_field_labels[{i}]"] = document_name or document_type
|
||
form_data[f"uploads_descriptions[{i}]"] = document_description or ""
|
||
form_data[f"original_filenames[{i}]"] = file.filename
|
||
|
||
# Отправляем в n8n одним запросом
|
||
async with httpx.AsyncClient(timeout=180.0) as client:
|
||
response = await client.post(
|
||
N8N_DOCUMENT_UPLOAD_WEBHOOK,
|
||
data=form_data,
|
||
files=files_multipart,
|
||
)
|
||
|
||
response_text = response.text or ""
|
||
|
||
if response.status_code == 200:
|
||
logger.info(
|
||
"✅ Multiple documents uploaded to n8n",
|
||
extra={
|
||
"claim_id": claim_id,
|
||
"document_type": document_type,
|
||
"file_ids": file_ids,
|
||
"files_count": len(files),
|
||
},
|
||
)
|
||
|
||
# Парсим ответ от n8n
|
||
try:
|
||
n8n_response = json.loads(response_text)
|
||
except json.JSONDecodeError:
|
||
n8n_response = {"raw": response_text}
|
||
|
||
# Публикуем событие в Redis
|
||
event_data = {
|
||
"event_type": "documents_uploaded",
|
||
"status": "processing",
|
||
"claim_id": claim_id,
|
||
"session_id": session_id,
|
||
"document_type": document_type,
|
||
"file_ids": file_ids,
|
||
"files_count": len(files),
|
||
"original_filenames": [f.filename for f in files],
|
||
"timestamp": datetime.utcnow().isoformat(),
|
||
}
|
||
|
||
await redis_service.publish(
|
||
f"ocr_events:{session_id}",
|
||
json.dumps(event_data, ensure_ascii=False)
|
||
)
|
||
|
||
return {
|
||
"success": True,
|
||
"file_ids": file_ids,
|
||
"files_count": len(files),
|
||
"document_type": document_type,
|
||
"ocr_status": "processing",
|
||
"message": f"Загружено {len(files)} файл(ов)",
|
||
"n8n_response": n8n_response,
|
||
}
|
||
|
||
else:
|
||
logger.error(
|
||
"❌ n8n multiple upload error",
|
||
extra={
|
||
"status_code": response.status_code,
|
||
"body": response_text[:500],
|
||
},
|
||
)
|
||
raise HTTPException(
|
||
status_code=response.status_code,
|
||
detail=f"Ошибка n8n: {response_text}",
|
||
)
|
||
|
||
except httpx.TimeoutException:
|
||
logger.error("⏱️ n8n multiple upload timeout")
|
||
raise HTTPException(status_code=504, detail="Таймаут загрузки документов")
|
||
|
||
except HTTPException:
|
||
raise
|
||
|
||
except Exception as e:
|
||
logger.exception("❌ Multiple upload error")
|
||
raise HTTPException(
|
||
status_code=500,
|
||
detail=f"Ошибка загрузки документов: {str(e)}",
|
||
)
|
||
|
||
|
||
@router.get("/status/{claim_id}")
|
||
async def get_documents_status(claim_id: str):
|
||
"""
|
||
Получить статус обработки документов для заявки.
|
||
|
||
Возвращает:
|
||
- Список загруженных документов и их OCR статус
|
||
- Общий прогресс обработки
|
||
"""
|
||
try:
|
||
# TODO: Запрос в PostgreSQL для получения статуса документов
|
||
# Пока возвращаем mock данные
|
||
|
||
return {
|
||
"success": True,
|
||
"claim_id": claim_id,
|
||
"documents": [],
|
||
"ocr_progress": {
|
||
"total": 0,
|
||
"completed": 0,
|
||
"processing": 0,
|
||
"failed": 0,
|
||
},
|
||
"wizard_ready": False,
|
||
"claim_ready": False,
|
||
}
|
||
|
||
except Exception as e:
|
||
logger.exception("❌ Error getting documents status")
|
||
raise HTTPException(
|
||
status_code=500,
|
||
detail=f"Ошибка получения статуса: {str(e)}",
|
||
)
|
||
|
||
|
||
@router.post("/generate-list")
|
||
async def generate_documents_list(request: Request):
|
||
"""
|
||
Запрос на генерацию списка документов для проблемы.
|
||
|
||
Принимает описание проблемы, отправляет в n8n для быстрого AI-анализа.
|
||
n8n публикует результат в Redis канал ocr_events:{session_id} с event_type=documents_list_ready.
|
||
"""
|
||
try:
|
||
body = await request.json()
|
||
|
||
session_id = body.get("session_id")
|
||
problem_description = body.get("problem_description")
|
||
|
||
if not session_id or not problem_description:
|
||
raise HTTPException(
|
||
status_code=400,
|
||
detail="session_id и problem_description обязательны",
|
||
)
|
||
|
||
logger.info(
|
||
"📝 Generate documents list request",
|
||
extra={
|
||
"session_id": session_id,
|
||
"description_length": len(problem_description),
|
||
},
|
||
)
|
||
|
||
# Публикуем событие в Redis для n8n
|
||
event_data = {
|
||
"type": "generate_documents_list",
|
||
"session_id": session_id,
|
||
"claim_id": body.get("claim_id"),
|
||
"unified_id": body.get("unified_id"),
|
||
"phone": body.get("phone"),
|
||
"problem_description": problem_description,
|
||
"timestamp": datetime.utcnow().isoformat(),
|
||
}
|
||
|
||
channel = f"{settings.redis_prefix}documents_list"
|
||
|
||
subscribers = await redis_service.publish(
|
||
channel,
|
||
json.dumps(event_data, ensure_ascii=False)
|
||
)
|
||
|
||
logger.info(
|
||
"✅ Documents list request published",
|
||
extra={
|
||
"channel": channel,
|
||
"subscribers": subscribers,
|
||
},
|
||
)
|
||
|
||
return {
|
||
"success": True,
|
||
"message": "Запрос на генерацию списка документов отправлен",
|
||
"channel": channel,
|
||
}
|
||
|
||
except HTTPException:
|
||
raise
|
||
|
||
except Exception as e:
|
||
logger.exception("❌ Error generating documents list")
|
||
raise HTTPException(
|
||
status_code=500,
|
||
detail=f"Ошибка генерации списка: {str(e)}",
|
||
)
|
||
from typing import Optional, List
|
||
import httpx
|
||
import json
|
||
import uuid
|
||
from datetime import datetime
|
||
import logging
|
||
from ..services.redis_service import redis_service
|
||
from ..config import settings
|
||
|
||
router = APIRouter(prefix="/api/v1/documents", tags=["Documents"])
|
||
logger = logging.getLogger(__name__)
|
||
|
||
# n8n webhook для загрузки документов
|
||
N8N_DOCUMENT_UPLOAD_WEBHOOK = "https://n8n.clientright.pro/webhook/webform_document_upload"
|
||
|
||
|
||
@router.post("/upload")
|
||
async def upload_document(
|
||
request: Request,
|
||
file: UploadFile = File(...),
|
||
claim_id: str = Form(...),
|
||
session_id: str = Form(...),
|
||
document_type: str = Form(...),
|
||
document_name: Optional[str] = Form(None),
|
||
document_description: Optional[str] = Form(None),
|
||
unified_id: Optional[str] = Form(None),
|
||
contact_id: Optional[str] = Form(None),
|
||
phone: Optional[str] = Form(None),
|
||
):
|
||
"""
|
||
Загрузка одного документа.
|
||
|
||
Принимает файл и метаданные, отправляет в n8n для:
|
||
1. Сохранения в S3
|
||
2. OCR обработки
|
||
3. Обновления черновика в PostgreSQL
|
||
|
||
После успешной обработки n8n публикует событие document_ocr_completed в Redis.
|
||
"""
|
||
try:
|
||
# Генерируем уникальный ID файла
|
||
file_id = f"doc_{uuid.uuid4().hex[:12]}"
|
||
|
||
logger.info(
|
||
"📤 Document upload received",
|
||
extra={
|
||
"claim_id": claim_id,
|
||
"session_id": session_id,
|
||
"document_type": document_type,
|
||
"file_name": file.filename,
|
||
"file_size": file.size if hasattr(file, 'size') else 'unknown',
|
||
"content_type": file.content_type,
|
||
},
|
||
)
|
||
|
||
# Читаем содержимое файла
|
||
file_content = await file.read()
|
||
file_size = len(file_content)
|
||
|
||
# Получаем IP клиента
|
||
client_ip = request.client.host if request.client else "unknown"
|
||
forwarded_for = request.headers.get("x-forwarded-for", "").split(",")[0].strip()
|
||
if forwarded_for:
|
||
client_ip = forwarded_for
|
||
|
||
# Формируем данные в формате совместимом с существующим n8n воркфлоу
|
||
form_data = {
|
||
# Основные идентификаторы
|
||
"form_id": "ticket_form",
|
||
"stage": "document_upload",
|
||
"session_id": session_id,
|
||
"claim_id": claim_id,
|
||
"client_ip": client_ip,
|
||
|
||
# Идентификаторы пользователя
|
||
"unified_id": unified_id or "",
|
||
"contact_id": contact_id or "",
|
||
"phone": phone or "",
|
||
|
||
# Информация о документе
|
||
"document_type": document_type,
|
||
"file_id": file_id,
|
||
"original_filename": file.filename,
|
||
"content_type": file.content_type or "application/octet-stream",
|
||
"file_size": str(file_size),
|
||
"upload_timestamp": datetime.utcnow().isoformat(),
|
||
|
||
# Формат uploads_* для совместимости
|
||
"uploads_field_names[0]": document_type,
|
||
"uploads_field_labels[0]": document_name or document_type,
|
||
"uploads_descriptions[0]": document_description or "",
|
||
}
|
||
|
||
# Файл для multipart (ключ uploads[0] для совместимости)
|
||
files = {
|
||
"uploads[0]": (file.filename, file_content, file.content_type or "application/octet-stream")
|
||
}
|
||
|
||
# Отправляем в n8n
|
||
async with httpx.AsyncClient(timeout=120.0) as client:
|
||
response = await client.post(
|
||
N8N_DOCUMENT_UPLOAD_WEBHOOK,
|
||
data=form_data,
|
||
files=files,
|
||
)
|
||
|
||
response_text = response.text or ""
|
||
|
||
if response.status_code == 200:
|
||
logger.info(
|
||
"✅ Document uploaded to n8n",
|
||
extra={
|
||
"claim_id": claim_id,
|
||
"document_type": document_type,
|
||
"file_id": file_id,
|
||
"response_preview": response_text[:200],
|
||
},
|
||
)
|
||
|
||
# Парсим ответ от n8n
|
||
try:
|
||
n8n_response = json.loads(response_text)
|
||
except json.JSONDecodeError:
|
||
n8n_response = {"raw": response_text}
|
||
|
||
# Публикуем событие в Redis для фронтенда
|
||
event_data = {
|
||
"event_type": "document_uploaded",
|
||
"status": "processing",
|
||
"claim_id": claim_id,
|
||
"session_id": session_id,
|
||
"document_type": document_type,
|
||
"file_id": file_id,
|
||
"original_filename": file.filename,
|
||
"timestamp": datetime.utcnow().isoformat(),
|
||
}
|
||
|
||
await redis_service.publish(
|
||
f"ocr_events:{session_id}",
|
||
json.dumps(event_data, ensure_ascii=False)
|
||
)
|
||
|
||
return {
|
||
"success": True,
|
||
"file_id": file_id,
|
||
"document_type": document_type,
|
||
"ocr_status": "processing",
|
||
"message": "Документ загружен и отправлен на обработку",
|
||
"n8n_response": n8n_response,
|
||
}
|
||
|
||
else:
|
||
logger.error(
|
||
"❌ n8n document upload error",
|
||
extra={
|
||
"status_code": response.status_code,
|
||
"body": response_text[:500],
|
||
},
|
||
)
|
||
raise HTTPException(
|
||
status_code=response.status_code,
|
||
detail=f"Ошибка n8n: {response_text}",
|
||
)
|
||
|
||
except httpx.TimeoutException:
|
||
logger.error("⏱️ n8n document upload timeout")
|
||
raise HTTPException(status_code=504, detail="Таймаут загрузки документа")
|
||
|
||
except HTTPException:
|
||
raise
|
||
|
||
except Exception as e:
|
||
logger.exception("❌ Document upload error")
|
||
raise HTTPException(
|
||
status_code=500,
|
||
detail=f"Ошибка загрузки документа: {str(e)}",
|
||
)
|
||
|
||
|
||
@router.post("/upload-multiple")
|
||
async def upload_multiple_documents(
|
||
request: Request,
|
||
files: List[UploadFile] = File(...),
|
||
claim_id: str = Form(...),
|
||
session_id: str = Form(...),
|
||
document_type: str = Form(...),
|
||
document_name: Optional[str] = Form(None),
|
||
document_description: Optional[str] = Form(None),
|
||
unified_id: Optional[str] = Form(None),
|
||
contact_id: Optional[str] = Form(None),
|
||
phone: Optional[str] = Form(None),
|
||
):
|
||
"""
|
||
Загрузка нескольких файлов для одного документа (например, несколько страниц паспорта).
|
||
Все файлы отправляются одним запросом в n8n.
|
||
"""
|
||
try:
|
||
logger.info(
|
||
"📤 Multiple documents upload received",
|
||
extra={
|
||
"claim_id": claim_id,
|
||
"session_id": session_id,
|
||
"document_type": document_type,
|
||
"files_count": len(files),
|
||
"file_names": [f.filename for f in files],
|
||
},
|
||
)
|
||
|
||
# Получаем IP клиента
|
||
client_ip = request.client.host if request.client else "unknown"
|
||
forwarded_for = request.headers.get("x-forwarded-for", "").split(",")[0].strip()
|
||
if forwarded_for:
|
||
client_ip = forwarded_for
|
||
|
||
# Генерируем ID для каждого файла и читаем контент
|
||
file_ids = []
|
||
files_multipart = {}
|
||
|
||
for i, file in enumerate(files):
|
||
file_id = f"doc_{uuid.uuid4().hex[:12]}"
|
||
file_ids.append(file_id)
|
||
|
||
file_content = await file.read()
|
||
files_multipart[f"uploads[{i}]"] = (
|
||
file.filename,
|
||
file_content,
|
||
file.content_type or "application/octet-stream"
|
||
)
|
||
|
||
# Формируем данные формы
|
||
form_data = {
|
||
# Основные идентификаторы
|
||
"form_id": "ticket_form",
|
||
"stage": "document_upload",
|
||
"session_id": session_id,
|
||
"claim_id": claim_id,
|
||
"client_ip": client_ip,
|
||
|
||
# Идентификаторы пользователя
|
||
"unified_id": unified_id or "",
|
||
"contact_id": contact_id or "",
|
||
"phone": phone or "",
|
||
|
||
# Информация о документе
|
||
"document_type": document_type,
|
||
"files_count": str(len(files)),
|
||
"upload_timestamp": datetime.utcnow().isoformat(),
|
||
}
|
||
|
||
# ✅ Получаем group_index из Form (индекс документа в documents_required)
|
||
form_params = await request.form()
|
||
group_index = form_params.get("group_index")
|
||
if group_index:
|
||
form_data["group_index"] = group_index
|
||
logger.info(f"📋 group_index передан в n8n: {group_index}")
|
||
|
||
# Добавляем информацию о каждом файле
|
||
for i, (file, file_id) in enumerate(zip(files, file_ids)):
|
||
form_data[f"file_ids[{i}]"] = file_id
|
||
form_data[f"uploads_field_names[{i}]"] = document_type
|
||
form_data[f"uploads_field_labels[{i}]"] = document_name or document_type
|
||
form_data[f"uploads_descriptions[{i}]"] = document_description or ""
|
||
form_data[f"original_filenames[{i}]"] = file.filename
|
||
|
||
# Отправляем в n8n одним запросом
|
||
async with httpx.AsyncClient(timeout=180.0) as client:
|
||
response = await client.post(
|
||
N8N_DOCUMENT_UPLOAD_WEBHOOK,
|
||
data=form_data,
|
||
files=files_multipart,
|
||
)
|
||
|
||
response_text = response.text or ""
|
||
|
||
if response.status_code == 200:
|
||
logger.info(
|
||
"✅ Multiple documents uploaded to n8n",
|
||
extra={
|
||
"claim_id": claim_id,
|
||
"document_type": document_type,
|
||
"file_ids": file_ids,
|
||
"files_count": len(files),
|
||
},
|
||
)
|
||
|
||
# Парсим ответ от n8n
|
||
try:
|
||
n8n_response = json.loads(response_text)
|
||
except json.JSONDecodeError:
|
||
n8n_response = {"raw": response_text}
|
||
|
||
# Публикуем событие в Redis
|
||
event_data = {
|
||
"event_type": "documents_uploaded",
|
||
"status": "processing",
|
||
"claim_id": claim_id,
|
||
"session_id": session_id,
|
||
"document_type": document_type,
|
||
"file_ids": file_ids,
|
||
"files_count": len(files),
|
||
"original_filenames": [f.filename for f in files],
|
||
"timestamp": datetime.utcnow().isoformat(),
|
||
}
|
||
|
||
await redis_service.publish(
|
||
f"ocr_events:{session_id}",
|
||
json.dumps(event_data, ensure_ascii=False)
|
||
)
|
||
|
||
return {
|
||
"success": True,
|
||
"file_ids": file_ids,
|
||
"files_count": len(files),
|
||
"document_type": document_type,
|
||
"ocr_status": "processing",
|
||
"message": f"Загружено {len(files)} файл(ов)",
|
||
"n8n_response": n8n_response,
|
||
}
|
||
|
||
else:
|
||
logger.error(
|
||
"❌ n8n multiple upload error",
|
||
extra={
|
||
"status_code": response.status_code,
|
||
"body": response_text[:500],
|
||
},
|
||
)
|
||
raise HTTPException(
|
||
status_code=response.status_code,
|
||
detail=f"Ошибка n8n: {response_text}",
|
||
)
|
||
|
||
except httpx.TimeoutException:
|
||
logger.error("⏱️ n8n multiple upload timeout")
|
||
raise HTTPException(status_code=504, detail="Таймаут загрузки документов")
|
||
|
||
except HTTPException:
|
||
raise
|
||
|
||
except Exception as e:
|
||
logger.exception("❌ Multiple upload error")
|
||
raise HTTPException(
|
||
status_code=500,
|
||
detail=f"Ошибка загрузки документов: {str(e)}",
|
||
)
|
||
|
||
|
||
@router.get("/status/{claim_id}")
|
||
async def get_documents_status(claim_id: str):
|
||
"""
|
||
Получить статус обработки документов для заявки.
|
||
|
||
Возвращает:
|
||
- Список загруженных документов и их OCR статус
|
||
- Общий прогресс обработки
|
||
"""
|
||
try:
|
||
# TODO: Запрос в PostgreSQL для получения статуса документов
|
||
# Пока возвращаем mock данные
|
||
|
||
return {
|
||
"success": True,
|
||
"claim_id": claim_id,
|
||
"documents": [],
|
||
"ocr_progress": {
|
||
"total": 0,
|
||
"completed": 0,
|
||
"processing": 0,
|
||
"failed": 0,
|
||
},
|
||
"wizard_ready": False,
|
||
"claim_ready": False,
|
||
}
|
||
|
||
except Exception as e:
|
||
logger.exception("❌ Error getting documents status")
|
||
raise HTTPException(
|
||
status_code=500,
|
||
detail=f"Ошибка получения статуса: {str(e)}",
|
||
)
|
||
|
||
|
||
@router.post("/generate-list")
|
||
async def generate_documents_list(request: Request):
|
||
"""
|
||
Запрос на генерацию списка документов для проблемы.
|
||
|
||
Принимает описание проблемы, отправляет в n8n для быстрого AI-анализа.
|
||
n8n публикует результат в Redis канал ocr_events:{session_id} с event_type=documents_list_ready.
|
||
"""
|
||
try:
|
||
body = await request.json()
|
||
|
||
session_id = body.get("session_id")
|
||
problem_description = body.get("problem_description")
|
||
|
||
if not session_id or not problem_description:
|
||
raise HTTPException(
|
||
status_code=400,
|
||
detail="session_id и problem_description обязательны",
|
||
)
|
||
|
||
logger.info(
|
||
"📝 Generate documents list request",
|
||
extra={
|
||
"session_id": session_id,
|
||
"description_length": len(problem_description),
|
||
},
|
||
)
|
||
|
||
# Публикуем событие в Redis для n8n
|
||
event_data = {
|
||
"type": "generate_documents_list",
|
||
"session_id": session_id,
|
||
"claim_id": body.get("claim_id"),
|
||
"unified_id": body.get("unified_id"),
|
||
"phone": body.get("phone"),
|
||
"problem_description": problem_description,
|
||
"timestamp": datetime.utcnow().isoformat(),
|
||
}
|
||
|
||
channel = f"{settings.redis_prefix}documents_list"
|
||
|
||
subscribers = await redis_service.publish(
|
||
channel,
|
||
json.dumps(event_data, ensure_ascii=False)
|
||
)
|
||
|
||
logger.info(
|
||
"✅ Documents list request published",
|
||
extra={
|
||
"channel": channel,
|
||
"subscribers": subscribers,
|
||
},
|
||
)
|
||
|
||
return {
|
||
"success": True,
|
||
"message": "Запрос на генерацию списка документов отправлен",
|
||
"channel": channel,
|
||
}
|
||
|
||
except HTTPException:
|
||
raise
|
||
|
||
except Exception as e:
|
||
logger.exception("❌ Error generating documents list")
|
||
raise HTTPException(
|
||
status_code=500,
|
||
detail=f"Ошибка генерации списка: {str(e)}",
|
||
)
|
||
|