feat: Прямой PHP эндпоинт для привязки документов
Создан api_attach_documents.php: ✅ Прямой эндпоинт без backend proxy ✅ URL: https://crm.clientright.ru/api_attach_documents.php ✅ Принимает массив документов из n8n ✅ Умная обработка S3 путей (добавляет хост если нужно) ✅ Поддержка file/file_url, filename/file_name ✅ Привязка к HelpDesk или Project (зависит от ticket_id) ✅ Проксирует к upload_documents_to_crm.php ✅ Полное логирование в logs/api_attach_documents.log Готово к использованию в n8n!
This commit is contained in:
89
API_ATTACH_DOCS_README.md
Normal file
89
API_ATTACH_DOCS_README.md
Normal file
@@ -0,0 +1,89 @@
|
|||||||
|
# 📎 API для привязки документов
|
||||||
|
|
||||||
|
## ✅ Прямой эндпоинт (готов к использованию!)
|
||||||
|
|
||||||
|
```
|
||||||
|
POST https://crm.clientright.ru/api_attach_documents.php
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🚀 Формат запроса (из n8n)
|
||||||
|
|
||||||
|
```json
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"contact_id": "320096",
|
||||||
|
"project_id": "396868",
|
||||||
|
"ticket_id": "396936",
|
||||||
|
"filename": "boarding_pass.pdf",
|
||||||
|
"file_type": "flight_delay_boarding_or_ticket",
|
||||||
|
"file": "/f9825c87-4e3558f6-f9b6-405c-ad3d-d1535c49b61c/crm2/CRM_Active_Files/Documents/HelpDesk/ЗАЯВКА_827_396936/flight_delay_boarding_or_ticket.pdf"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
```
|
||||||
|
|
||||||
|
**Важно:**
|
||||||
|
- ✅ Массив `[...]` (даже для одного документа)
|
||||||
|
- ✅ `file` без хоста → автоматически добавится `https://s3.twcstorage.ru`
|
||||||
|
- ✅ `ticket_id` опционально (если есть → HelpDesk, иначе → Project)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📊 Формат ответа
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"success": true,
|
||||||
|
"total_processed": 1,
|
||||||
|
"successful": 1,
|
||||||
|
"failed": 0,
|
||||||
|
"results": [
|
||||||
|
{
|
||||||
|
"document_id": "15x396941",
|
||||||
|
"document_numeric_id": "396941",
|
||||||
|
"attached_to": "ticket",
|
||||||
|
"attached_to_id": "396936",
|
||||||
|
"file_name": "boarding_pass.pdf",
|
||||||
|
"file_type": "flight_delay_boarding_or_ticket",
|
||||||
|
"s3_bucket": "f9825c87-...",
|
||||||
|
"s3_key": "crm2/CRM_Active_Files/...",
|
||||||
|
"file_size": 85320,
|
||||||
|
"message": "Документ создан и привязан..."
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"errors": null
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🧪 Тест
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -X POST "https://crm.clientright.ru/api_attach_documents.php" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d '[
|
||||||
|
{
|
||||||
|
"contact_id": "320096",
|
||||||
|
"project_id": "396868",
|
||||||
|
"ticket_id": "396936",
|
||||||
|
"filename": "test.pdf",
|
||||||
|
"file_type": "flight_delay_boarding_or_ticket",
|
||||||
|
"file": "/bucket/path/to/file.pdf"
|
||||||
|
}
|
||||||
|
]'
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🔍 Логи
|
||||||
|
|
||||||
|
```bash
|
||||||
|
tail -f /var/www/fastuser/data/www/crm.clientright.ru/logs/api_attach_documents.log
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🎯 Готово к использованию в n8n!
|
||||||
|
|
||||||
250
api_attach_documents.php
Normal file
250
api_attach_documents.php
Normal file
@@ -0,0 +1,250 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* API для привязки документов к проекту/заявке
|
||||||
|
*
|
||||||
|
* Использование из n8n:
|
||||||
|
* POST https://crm.clientright.ru/api_attach_documents.php
|
||||||
|
*
|
||||||
|
* Входные данные (JSON массив):
|
||||||
|
* [
|
||||||
|
* {
|
||||||
|
* "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"
|
||||||
|
* }
|
||||||
|
* ]
|
||||||
|
*/
|
||||||
|
|
||||||
|
error_reporting(E_ALL);
|
||||||
|
ini_set('display_errors', '0');
|
||||||
|
|
||||||
|
// Функция для логирования
|
||||||
|
function log_message($message) {
|
||||||
|
$timestamp = date('Y-m-d H:i:s');
|
||||||
|
$line = "[$timestamp] $message\n";
|
||||||
|
@file_put_contents(__DIR__ . '/logs/api_attach_documents.log', $line, FILE_APPEND | LOCK_EX);
|
||||||
|
error_log('[api_attach_documents] ' . $message);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Функция для JSON ответа
|
||||||
|
function json_response($data, $code = 200) {
|
||||||
|
if (!headers_sent()) {
|
||||||
|
http_response_code($code);
|
||||||
|
header('Content-Type: application/json; charset=utf-8');
|
||||||
|
header('Access-Control-Allow-Origin: *');
|
||||||
|
header('Access-Control-Allow-Methods: POST, OPTIONS');
|
||||||
|
header('Access-Control-Allow-Headers: Content-Type');
|
||||||
|
}
|
||||||
|
echo json_encode($data, JSON_UNESCAPED_UNICODE);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
// CORS preflight
|
||||||
|
if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') {
|
||||||
|
json_response(['status' => 'ok']);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Только POST
|
||||||
|
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
|
||||||
|
json_response(['success' => false, 'error' => 'Method not allowed'], 405);
|
||||||
|
}
|
||||||
|
|
||||||
|
log_message('=== START API REQUEST ===');
|
||||||
|
|
||||||
|
// Получаем входные данные
|
||||||
|
$input = file_get_contents('php://input');
|
||||||
|
log_message('Raw input: ' . substr($input, 0, 500));
|
||||||
|
|
||||||
|
$input = ltrim($input, "\xEF\xBB\xBF\x00\x09\x0A\x0D\x20");
|
||||||
|
$data = json_decode($input, true);
|
||||||
|
|
||||||
|
if (json_last_error() !== JSON_ERROR_NONE) {
|
||||||
|
log_message('❌ JSON Error: ' . json_last_error_msg());
|
||||||
|
json_response([
|
||||||
|
'success' => false,
|
||||||
|
'error' => 'Invalid JSON: ' . json_last_error_msg()
|
||||||
|
], 400);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Поддерживаем как массив, так и одиночный объект
|
||||||
|
$documents_array = is_array($data) && isset($data[0]) ? $data : [$data];
|
||||||
|
|
||||||
|
log_message('Processing ' . count($documents_array) . ' document(s)');
|
||||||
|
|
||||||
|
// Обрабатываем каждый документ
|
||||||
|
$processed_documents = [];
|
||||||
|
$S3_HOST = 'https://s3.twcstorage.ru';
|
||||||
|
|
||||||
|
foreach ($documents_array as $idx => $doc) {
|
||||||
|
$contact_id = $doc['contact_id'] ?? null;
|
||||||
|
$project_id = $doc['project_id'] ?? null;
|
||||||
|
$ticket_id = $doc['ticket_id'] ?? null;
|
||||||
|
|
||||||
|
// Поддерживаем оба формата: file и file_url
|
||||||
|
$file_path = $doc['file'] ?? $doc['file_url'] ?? null;
|
||||||
|
|
||||||
|
if (!$file_path) {
|
||||||
|
log_message("❌ Document #{$idx}: missing 'file' or 'file_url'");
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Строим полный S3 URL
|
||||||
|
if (strpos($file_path, 'http') === 0) {
|
||||||
|
$file_url = $file_path;
|
||||||
|
} elseif (strpos($file_path, '/') === 0) {
|
||||||
|
$file_url = $S3_HOST . $file_path;
|
||||||
|
} else {
|
||||||
|
$file_url = $S3_HOST . '/' . $file_path;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Поддерживаем оба формата: filename и file_name
|
||||||
|
$file_name = $doc['filename'] ?? $doc['file_name'] ?? null;
|
||||||
|
|
||||||
|
if (!$file_name) {
|
||||||
|
log_message("❌ Document #{$idx}: missing 'filename' or 'file_name'");
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$file_type = $doc['file_type'] ?? 'Документ';
|
||||||
|
|
||||||
|
// Валидация обязательных полей
|
||||||
|
if (!$contact_id || !$project_id) {
|
||||||
|
log_message("❌ Document #{$idx}: missing contact_id or project_id");
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
log_message(" [{$idx}] {$file_name} (type: {$file_type})");
|
||||||
|
log_message(" Contact: {$contact_id}, Project: {$project_id}, Ticket: " . ($ticket_id ?: 'N/A'));
|
||||||
|
log_message(" File URL: {$file_url}");
|
||||||
|
|
||||||
|
$processed_documents[] = [
|
||||||
|
'url' => $file_url,
|
||||||
|
'file_name' => $file_name,
|
||||||
|
'description' => $file_type,
|
||||||
|
'projectid' => (int)$project_id,
|
||||||
|
'ticket_id' => $ticket_id ? (int)$ticket_id : null,
|
||||||
|
'contactid' => (int)$contact_id,
|
||||||
|
'pages' => 1
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (empty($processed_documents)) {
|
||||||
|
log_message('❌ No valid documents to process');
|
||||||
|
json_response([
|
||||||
|
'success' => false,
|
||||||
|
'error' => 'No valid documents to process'
|
||||||
|
], 400);
|
||||||
|
}
|
||||||
|
|
||||||
|
log_message('📤 Sending ' . count($processed_documents) . ' documents to upload_documents_to_crm.php');
|
||||||
|
|
||||||
|
// Формируем запрос к upload_documents_to_crm.php
|
||||||
|
$upload_url = 'https://crm.clientright.ru/upload_documents_to_crm.php';
|
||||||
|
|
||||||
|
// Берем общие параметры из первого документа
|
||||||
|
$first_doc = $processed_documents[0];
|
||||||
|
|
||||||
|
$payload = json_encode([
|
||||||
|
'documents' => $processed_documents,
|
||||||
|
'projectid' => $first_doc['projectid'],
|
||||||
|
'ticket_id' => $first_doc['ticket_id'],
|
||||||
|
'user_id' => 1
|
||||||
|
], JSON_UNESCAPED_UNICODE);
|
||||||
|
|
||||||
|
log_message('Payload: ' . substr($payload, 0, 500));
|
||||||
|
|
||||||
|
// Отправляем запрос
|
||||||
|
$ch = curl_init();
|
||||||
|
curl_setopt($ch, CURLOPT_URL, $upload_url);
|
||||||
|
curl_setopt($ch, CURLOPT_POST, true);
|
||||||
|
curl_setopt($ch, CURLOPT_POSTFIELDS, $payload);
|
||||||
|
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
|
||||||
|
curl_setopt($ch, CURLOPT_TIMEOUT, 60);
|
||||||
|
curl_setopt($ch, CURLOPT_HTTPHEADER, [
|
||||||
|
'Content-Type: application/json',
|
||||||
|
'Content-Length: ' . strlen($payload)
|
||||||
|
]);
|
||||||
|
|
||||||
|
$response = curl_exec($ch);
|
||||||
|
$http_code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
||||||
|
curl_close($ch);
|
||||||
|
|
||||||
|
if ($response === false) {
|
||||||
|
log_message('❌ CURL error: ' . curl_error($ch));
|
||||||
|
json_response([
|
||||||
|
'success' => false,
|
||||||
|
'error' => 'Internal error: ' . curl_error($ch)
|
||||||
|
], 500);
|
||||||
|
}
|
||||||
|
|
||||||
|
log_message("Response HTTP code: {$http_code}");
|
||||||
|
log_message("Response: " . substr($response, 0, 500));
|
||||||
|
|
||||||
|
// Парсим ответ
|
||||||
|
$result = json_decode($response, true);
|
||||||
|
|
||||||
|
if (json_last_error() !== JSON_ERROR_NONE) {
|
||||||
|
log_message('❌ Failed to parse response JSON: ' . json_last_error_msg());
|
||||||
|
json_response([
|
||||||
|
'success' => false,
|
||||||
|
'error' => 'Invalid response from upload service'
|
||||||
|
], 500);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Проверяем успешность
|
||||||
|
if ($result && $result['success'] && isset($result['results'])) {
|
||||||
|
$results_array = $result['results'];
|
||||||
|
|
||||||
|
// Формируем ответ
|
||||||
|
$processed_results = [];
|
||||||
|
$errors = [];
|
||||||
|
|
||||||
|
foreach ($results_array as $idx => $res) {
|
||||||
|
if ($res['status'] === 'success') {
|
||||||
|
$crm_result = $res['crm_result'] ?? [];
|
||||||
|
|
||||||
|
$processed_results[] = [
|
||||||
|
'document_id' => $crm_result['document_id'] ?? null,
|
||||||
|
'document_numeric_id' => $crm_result['document_numeric_id'] ?? null,
|
||||||
|
'attached_to' => isset($res['ticket_id']) && $res['ticket_id'] ? 'ticket' : 'project',
|
||||||
|
'attached_to_id' => $res['ticket_id'] ?? $res['projectid'] ?? null,
|
||||||
|
'file_name' => $res['file_name'] ?? null,
|
||||||
|
'file_type' => $res['description'] ?? null,
|
||||||
|
's3_bucket' => $crm_result['s3_bucket'] ?? null,
|
||||||
|
's3_key' => $crm_result['s3_key'] ?? null,
|
||||||
|
'file_size' => $crm_result['file_size'] ?? null,
|
||||||
|
'message' => $crm_result['message'] ?? null
|
||||||
|
];
|
||||||
|
|
||||||
|
log_message(" ✅ [{$idx}] {$res['file_name']} → {$crm_result['document_id']}");
|
||||||
|
} else {
|
||||||
|
$error_msg = $res['crm_result']['message'] ?? 'Unknown error';
|
||||||
|
$errors[] = [
|
||||||
|
'file_name' => $res['file_name'] ?? 'Unknown',
|
||||||
|
'error' => $error_msg
|
||||||
|
];
|
||||||
|
log_message(" ❌ [{$idx}] {$res['file_name']}: {$error_msg}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
log_message('✅ Success: ' . count($processed_results) . ' documents attached');
|
||||||
|
|
||||||
|
json_response([
|
||||||
|
'success' => true,
|
||||||
|
'total_processed' => count($results_array),
|
||||||
|
'successful' => count($processed_results),
|
||||||
|
'failed' => count($errors),
|
||||||
|
'results' => $processed_results,
|
||||||
|
'errors' => !empty($errors) ? $errors : null
|
||||||
|
]);
|
||||||
|
} else {
|
||||||
|
log_message('❌ Upload failed: ' . ($result['error']['message'] ?? 'Unknown error'));
|
||||||
|
json_response([
|
||||||
|
'success' => false,
|
||||||
|
'error' => $result['error']['message'] ?? 'Upload failed'
|
||||||
|
], 500);
|
||||||
|
}
|
||||||
|
|
||||||
Reference in New Issue
Block a user