feat: Поддержка batch-обработки документов и умного парсинга S3 путей

Изменения в /api/n8n/documents/attach:
 Принимает массив документов (не одиночный объект)
 Умная обработка S3 путей:
   - /bucket/path → https://s3.twcstorage.ru/bucket/path
   - bucket/path → https://s3.twcstorage.ru/bucket/path
   - https://... → без изменений
 Поддержка обоих форматов полей:
   - file / file_url
   - filename / file_name
 Batch-обработка с детальной статистикой
 Возвращает результаты для каждого документа отдельно
 Логирование успешных и неуспешных операций

Формат ответа:
{
  total_processed: N,
  successful: M,
  failed: K,
  results: [...],
  errors: [...]
}

Тесты:
- TEST_REAL_DATA.sh - тест с реальными данными из n8n
- TEST_QUICK.sh - быстрые тесты

Документация обновлена с примерами batch-обработки
This commit is contained in:
AI Assistant
2025-11-02 19:21:02 +03:00
parent e27280e675
commit efb0cd6f05
4 changed files with 318 additions and 104 deletions

View File

@@ -249,55 +249,96 @@ async def proxy_create_claim(request: Request):
@router.post("/documents/attach")
async def attach_document_to_crm(request: Request):
"""
Привязывает загруженный файл к проекту или заявке в vTiger CRM
Привязывает загруженные файлы к проекту или заявке в vTiger CRM
Входные данные:
- contact_id: ID контакта
- project_id: ID проекта (обязательно)
- ticket_id: ID заявки (опционально, если указан - привязываем к заявке)
- file_url: URL файла в S3
- file_name: Имя файла
- file_type: Тип файла (описание, например: "flight_delay_boarding_or_ticket")
Входные данные (массив документов):
[
{
"claim_id": "CLM-2025-11-02-WNRZZZ",
"contact_id": "320096",
"project_id": "396868",
"ticket_id": "396936", // Опционально
"filename": "boarding_pass.pdf",
"file_type": "flight_delay_boarding_or_ticket",
"file": "/bucket/path/to/file.pdf" // Без хоста, добавим https://s3.twcstorage.ru
}
]
Логика:
- Если указан ticket_id → привязываем к HelpDesk (заявке)
- Иначе → привязываем к Project (проекту)
"""
CRM_UPLOAD_ENDPOINT = "https://crm.clientright.ru/upload_documents_to_crm.php"
S3_HOST = "https://s3.twcstorage.ru"
try:
body = await request.json()
contact_id = body.get('contact_id')
project_id = body.get('project_id')
ticket_id = body.get('ticket_id') # Опционально
file_url = body.get('file_url')
file_name = body.get('file_name')
file_type = body.get('file_type', 'Документ')
# Поддерживаем как массив, так и одиночный объект
documents_array = body if isinstance(body, list) else [body]
# Валидация обязательных полей
if not all([contact_id, project_id, file_url, file_name]):
raise HTTPException(
status_code=400,
detail="Обязательные поля: contact_id, project_id, file_url, file_name"
)
logger.info(f"📎 Attaching {len(documents_array)} document(s)")
logger.info(f"📎 Attaching document: {file_name} (type: {file_type})")
logger.info(f" Contact: {contact_id}, Project: {project_id}, Ticket: {ticket_id or 'N/A'}")
# Обрабатываем каждый документ
processed_documents = []
for idx, doc in enumerate(documents_array):
contact_id = doc.get('contact_id')
project_id = doc.get('project_id')
ticket_id = doc.get('ticket_id') # Опционально
# Поддерживаем оба формата: file_url и file
file_path = doc.get('file') or doc.get('file_url')
if not file_path:
raise HTTPException(
status_code=400,
detail=f"Document #{idx}: отсутствует поле 'file' или 'file_url'"
)
# Строим полный S3 URL если это путь без хоста
if file_path.startswith('/'):
file_url = S3_HOST + file_path
elif not file_path.startswith('http'):
file_url = S3_HOST + '/' + file_path
else:
file_url = file_path
# Поддерживаем оба формата: file_name и filename
file_name = doc.get('filename') or doc.get('file_name')
if not file_name:
raise HTTPException(
status_code=400,
detail=f"Document #{idx}: отсутствует поле 'filename' или 'file_name'"
)
file_type = doc.get('file_type', 'Документ')
# Валидация обязательных полей
if not all([contact_id, project_id]):
raise HTTPException(
status_code=400,
detail=f"Document #{idx}: обязательные поля: contact_id, project_id"
)
logger.info(f" [{idx+1}/{len(documents_array)}] {file_name} (type: {file_type})")
logger.info(f" Contact: {contact_id}, Project: {project_id}, Ticket: {ticket_id or 'N/A'}")
logger.info(f" File URL: {file_url}")
processed_documents.append({
"file_url": file_url,
"file_name": file_name,
"upload_description": file_type,
"contactid": int(contact_id),
"pages": 1
})
# Берем общие параметры из первого документа
first_doc = documents_array[0]
# Формируем payload для upload_documents_to_crm.php
upload_payload = {
"documents": [
{
"file_url": file_url,
"file_name": file_name,
"upload_description": file_type,
"contactid": int(contact_id),
"pages": 1
}
],
"projectid": int(project_id),
"ticket_id": int(ticket_id) if ticket_id else None, # Передаем ticket_id если есть
"documents": processed_documents,
"projectid": int(first_doc.get('project_id')),
"ticket_id": int(first_doc.get('ticket_id')) if first_doc.get('ticket_id') else None,
"user_id": 1
}
@@ -320,31 +361,53 @@ async def attach_document_to_crm(request: Request):
# Проверяем успешность
if result.get('success') and result.get('results'):
first_result = result['results'][0]
results_array = result['results']
if first_result.get('status') == 'success':
crm_result = first_result.get('crm_result', {})
return {
"success": True,
"result": {
# Обрабатываем результаты для каждого документа
processed_results = []
errors = []
for idx, res in enumerate(results_array):
if res.get('status') == 'success':
crm_result = res.get('crm_result', {})
processed_results.append({
"document_id": crm_result.get('document_id'),
"document_numeric_id": crm_result.get('document_numeric_id'),
"attached_to": "ticket" if ticket_id else "project",
"attached_to_id": ticket_id if ticket_id else project_id,
"file_name": file_name,
"file_type": file_type,
"attached_to": "ticket" if res.get('ticket_id') else "project",
"attached_to_id": res.get('ticket_id') or res.get('projectid'),
"file_name": res.get('file_name'),
"file_type": res.get('description'),
"s3_bucket": crm_result.get('s3_bucket'),
"s3_key": crm_result.get('s3_key'),
"file_size": crm_result.get('file_size'),
"message": crm_result.get('message')
}
})
logger.info(f" ✅ [{idx+1}] {res.get('file_name')}{crm_result.get('document_id')}")
else:
# Ошибка для конкретного документа
error_msg = res.get('crm_result', {}).get('message', 'Unknown error')
errors.append({
"file_name": res.get('file_name'),
"error": error_msg
})
logger.error(f" ❌ [{idx+1}] {res.get('file_name')}: {error_msg}")
# Если есть хотя бы один успешный результат - считаем успехом
if processed_results:
return {
"success": True,
"total_processed": len(results_array),
"successful": len(processed_results),
"failed": len(errors),
"results": processed_results,
"errors": errors if errors else None
}
else:
# Ошибка в CRM
error_msg = first_result.get('crm_result', {}).get('message', 'Unknown error')
logger.error(f"❌ CRM error: {error_msg}")
raise HTTPException(status_code=500, detail=f"CRM error: {error_msg}")
# Все документы упали с ошибкой
logger.error(f"❌ All documents failed: {errors}")
raise HTTPException(status_code=500, detail=f"Все документы не удалось привязать: {errors}")
else:
logger.error(f"❌ Unexpected CRM response: {result}")
raise HTTPException(status_code=500, detail="Неожиданный ответ от CRM")