Compare commits
55 Commits
30a0df9c64
...
d7982931cd
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d7982931cd | ||
|
|
ea0edafba5 | ||
|
|
1fdb244fd4 | ||
|
|
ab54530500 | ||
|
|
ffe30033da | ||
|
|
35ca11552d | ||
|
|
b28bb4b636 | ||
|
|
37472d612a | ||
|
|
e0097a164d | ||
|
|
6a783deba1 | ||
|
|
920eb53660 | ||
|
|
1f88e156b7 | ||
|
|
003210dcfc | ||
|
|
346d9a77d2 | ||
|
|
38457394c1 | ||
|
|
e7915df634 | ||
|
|
b2433f38d8 | ||
|
|
8e116c76a4 | ||
|
|
bf3fb5fef0 | ||
|
|
1fc64c035e | ||
|
|
b93bb9e8ad | ||
|
|
35adcb3043 | ||
|
|
3d9669dd8e | ||
|
|
6a9f8b5465 | ||
|
|
a86120dd53 | ||
|
|
e114231541 | ||
|
|
b7197e0da5 | ||
|
|
ee1c4af5c3 | ||
|
|
834520a045 | ||
|
|
da82100b60 | ||
|
|
81acd49fd9 | ||
|
|
0f8631bf20 | ||
|
|
3801bc4949 | ||
|
|
985ee23810 | ||
|
|
840acca51a | ||
|
|
6c770f0a87 | ||
|
|
0868d37484 | ||
|
|
55c1402d99 | ||
|
|
18fcdecae8 | ||
|
|
f058ca91ad | ||
|
|
796316d969 | ||
|
|
f3b5771c09 | ||
|
|
2ce0c585ff | ||
|
|
6cc07b0ba6 | ||
|
|
b5478c143f | ||
|
|
be1ac2ed49 | ||
|
|
2b1dca9e92 | ||
|
|
99ef902a31 | ||
|
|
8626c9aff4 | ||
|
|
444e5d2b91 | ||
|
|
8f4cff55e9 | ||
|
|
52fe013375 | ||
|
|
a20a4d0e09 | ||
|
|
486f3619ff | ||
|
|
d3ba054027 |
@@ -1,10 +1,81 @@
|
||||
{
|
||||
"mcpServers": {
|
||||
"context7": {
|
||||
"url": "https://mcp.context7.com/mcp",
|
||||
"headers": {
|
||||
"CONTEXT7_API_KEY": "ctx7sk-541e7992-c38f-442f-8902-ae99645f2477"
|
||||
}
|
||||
}
|
||||
"context7": {
|
||||
"url": "https://mcp.context7.com/mcp",
|
||||
"headers": {
|
||||
"CONTEXT7_API_KEY": "ctx7sk-541e7992-c38f-442f-8902-ae99645f2477"
|
||||
}
|
||||
},
|
||||
"shadcn": {
|
||||
"command": "/usr/bin/docker",
|
||||
"args": [
|
||||
"run",
|
||||
"-i",
|
||||
"--rm",
|
||||
"--init",
|
||||
"-v", "/var/www/fastuser/data/www/crm.clientright.ru:/workspace",
|
||||
"-w", "/workspace",
|
||||
"node:20-alpine",
|
||||
"npx",
|
||||
"-y",
|
||||
"shadcn@latest",
|
||||
"mcp"
|
||||
],
|
||||
"env": {}
|
||||
},
|
||||
"antd-components": {
|
||||
"command": "/usr/bin/docker",
|
||||
"args": [
|
||||
"run",
|
||||
"-i",
|
||||
"--rm",
|
||||
"--init",
|
||||
"-v", "/var/www/fastuser/data/www/crm.clientright.ru:/workspace",
|
||||
"-w", "/workspace",
|
||||
"node:20-alpine",
|
||||
"npx",
|
||||
"-y",
|
||||
"@jzone-mcp/antd-components-mcp"
|
||||
],
|
||||
"env": {}
|
||||
},
|
||||
|
||||
|
||||
|
||||
"n8n-mcp": {
|
||||
"command": "docker",
|
||||
"args": [
|
||||
"run",
|
||||
"-i",
|
||||
"--rm",
|
||||
"--init",
|
||||
"-e", "MCP_MODE=stdio",
|
||||
"-e", "LOG_LEVEL=error",
|
||||
"-e", "DISABLE_CONSOLE_OUTPUT=true",
|
||||
"-e", "N8N_API_URL=https://n8n.clientright.pro/",
|
||||
"-e", "N8N_API_KEY=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiI5MzMwYWVjZC1hYjExLTQxODEtOWIyYy1iMDZhZWEzMTNmNzQiLCJpc3MiOiJuOG4iLCJhdWQiOiJwdWJsaWMtYXBpIiwiaWF0IjoxNzUzNjg3NDM4fQ.XJjyYXXOoO4eUGAfkSVRMJzLYvi25hczsp2F7j4UV7Y",
|
||||
"ghcr.io/czlonkowski/n8n-mcp:latest"
|
||||
]
|
||||
},
|
||||
"memory": {
|
||||
"url": "http://185.197.75.249:9000/sse"
|
||||
}
|
||||
|
||||
|
||||
,
|
||||
"n8n-mcp2": {
|
||||
"command": "npx",
|
||||
"args": [
|
||||
"-y",
|
||||
"supergateway",
|
||||
"--streamableHttp",
|
||||
"https://n8n.clientright.pro/mcp-server/http",
|
||||
"--header",
|
||||
"authorization:Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiI5MzMwYWVjZC1hYjExLTQxODEtOWIyYy1iMDZhZWEzMTNmNzQiLCJpc3MiOiJuOG4iLCJhdWQiOiJtY3Atc2VydmVyLWFwaSIsImp0aSI6Ijc5MTQzNDU5LTM0NzMtNDQ2Mi05MzU1LTZmMTAzZTdlMzAxNCIsImlhdCI6MTc2NDU3OTI0N30.g-WQpgWMjdFWVpUMaRP023MCQqfk_e3ollLyPpcE_Io"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
@@ -211,3 +211,9 @@ composer require phpoffice/phpword
|
||||
|
||||
Все компоненты реализованы, протестированы и готовы к использованию в n8n workflow.
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
146
RESTORE_INSTRUCTIONS.md
Normal file
146
RESTORE_INSTRUCTIONS.md
Normal file
@@ -0,0 +1,146 @@
|
||||
# Инструкция по восстановлению удаленных файлов и защите от повторных удалений
|
||||
|
||||
## 📋 Что было сделано:
|
||||
|
||||
1. ✅ Создан скрипт для настройки Nextcloud (`fix_nextcloud_settings.php`)
|
||||
2. ✅ Создан скрипт для восстановления файлов (`restore_all_deleted_files.php`)
|
||||
3. ✅ Создан скрипт для регулярной индексации (`nextcloud_scan_files.sh`)
|
||||
|
||||
---
|
||||
|
||||
## 🚀 Порядок выполнения:
|
||||
|
||||
### Шаг 1: Настройка Nextcloud (защита от удалений)
|
||||
|
||||
```bash
|
||||
cd /var/www/fastuser/data/www/crm.clientright.ru
|
||||
php fix_nextcloud_settings.php
|
||||
```
|
||||
|
||||
**Что делает:**
|
||||
- Отключает `DeleteOrphanedItems` (главная причина удалений)
|
||||
- Включает `readonly` для External Storage
|
||||
- Увеличивает retention корзины до 365 дней
|
||||
- Создает скрипт для регулярной индексации
|
||||
|
||||
---
|
||||
|
||||
### Шаг 2: Восстановление файлов (сначала проверка)
|
||||
|
||||
**Сначала проверка (dry-run):**
|
||||
```bash
|
||||
php restore_all_deleted_files.php --dry-run
|
||||
```
|
||||
|
||||
Это покажет, сколько файлов будет восстановлено без реального восстановления.
|
||||
|
||||
**Ограничение количества (для теста):**
|
||||
```bash
|
||||
php restore_all_deleted_files.php --dry-run 100
|
||||
```
|
||||
|
||||
**Восстановление всех файлов:**
|
||||
```bash
|
||||
php restore_all_deleted_files.php
|
||||
```
|
||||
|
||||
**Восстановление с ограничением (для безопасности):**
|
||||
```bash
|
||||
php restore_all_deleted_files.php "" 1000
|
||||
```
|
||||
|
||||
**Восстановление только файлов проекта:**
|
||||
```bash
|
||||
php restore_all_deleted_files.php "" "" "crm2/CRM_Active_Files/Documents/Project/"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Шаг 3: Настройка регулярной индексации
|
||||
|
||||
**Добавить в crontab:**
|
||||
```bash
|
||||
crontab -e
|
||||
```
|
||||
|
||||
**Добавить строку:**
|
||||
```
|
||||
0 */6 * * * /var/www/fastuser/data/www/crm.clientright.ru/nextcloud_scan_files.sh
|
||||
```
|
||||
|
||||
Это будет сканировать файлы каждые 6 часов.
|
||||
|
||||
**Или сканировать только внешнее хранилище (быстрее):**
|
||||
Отредактируйте `nextcloud_scan_files.sh` и раскомментируйте строку:
|
||||
```bash
|
||||
docker exec -u www-data nextcloud-fresh php occ files:scan --path="/crm"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📊 Статистика удалений:
|
||||
|
||||
- **Всего delete markers:** ~25,200
|
||||
- **Пик удалений:** 1 ноября 2025, 09:00 утра (7,080 файлов)
|
||||
- **Причина:** DeleteOrphanedItems в Nextcloud
|
||||
|
||||
---
|
||||
|
||||
## ⚠️ ВАЖНО:
|
||||
|
||||
1. **Сначала настройте Nextcloud** (Шаг 1), чтобы предотвратить новые удаления
|
||||
2. **Проверьте dry-run** перед массовым восстановлением
|
||||
3. **Восстанавливайте постепенно** (по 1000-5000 файлов за раз)
|
||||
4. **Проверяйте логи** после восстановления
|
||||
|
||||
---
|
||||
|
||||
## 🔍 Проверка статуса:
|
||||
|
||||
**Проверить статус задач Nextcloud:**
|
||||
```bash
|
||||
docker exec -u www-data nextcloud-fresh php occ background-job:list
|
||||
```
|
||||
|
||||
**Проверить настройки External Storage:**
|
||||
```bash
|
||||
docker exec -u www-data nextcloud-fresh php occ files_external:list
|
||||
```
|
||||
|
||||
**Проверить retention корзины:**
|
||||
```bash
|
||||
docker exec -u www-data nextcloud-fresh php occ config:app:get files trashbin_retention_obligation
|
||||
```
|
||||
|
||||
**Проверить логи восстановления:**
|
||||
```bash
|
||||
ls -lh /var/www/fastuser/data/www/crm.clientright.ru/restore_log_*.json
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🛡️ Защита от повторных удалений:
|
||||
|
||||
После выполнения всех шагов система будет защищена:
|
||||
|
||||
1. ✅ DeleteOrphanedItems отключен
|
||||
2. ✅ External Storage в режиме readonly
|
||||
3. ✅ Retention корзины увеличен до 365 дней
|
||||
4. ✅ Регулярная индексация файлов настроена
|
||||
|
||||
---
|
||||
|
||||
## 📝 Логи:
|
||||
|
||||
- Логи восстановления: `restore_log_YYYY-MM-DD_HH-MM-SS.json`
|
||||
- Логи индексации: `/var/log/nextcloud_scan.log`
|
||||
|
||||
---
|
||||
|
||||
## 🆘 Если что-то пошло не так:
|
||||
|
||||
1. Проверьте логи восстановления
|
||||
2. Проверьте доступность Docker контейнера Nextcloud
|
||||
3. Проверьте права доступа к S3
|
||||
4. Проверьте логи Nextcloud: `docker logs nextcloud-fresh`
|
||||
|
||||
108
SESSION_LOG_ARCHIVE_FIX.md
Normal file
108
SESSION_LOG_ARCHIVE_FIX.md
Normal file
@@ -0,0 +1,108 @@
|
||||
# Лог сессии: Исправление архивации проектов с S3 файлами
|
||||
|
||||
## Дата: 2025-11-21
|
||||
|
||||
## Проблема
|
||||
Архивация проектов не работала после миграции на S3:
|
||||
- Возвращался ответ `{"success":true,"result":"Nothing to archive"}`
|
||||
- Затем появились ошибки `"_ is missing!"` для всех документов
|
||||
- После исправления появились ошибки `"S3 file download failed"` для всех файлов
|
||||
|
||||
## Причины проблем
|
||||
|
||||
### 1. Неправильная обработка S3 файлов
|
||||
- Метод `getPaths()` пытался обработать S3 файлы как локальные
|
||||
- `Vtiger_Record_Model` не всегда содержит поля `s3_bucket`, `s3_key`, `filelocationtype`
|
||||
- Нужно было явно запрашивать эти данные из БД
|
||||
|
||||
### 2. Отсутствие поддержки связанных документов
|
||||
- Архив включал только документы самого проекта
|
||||
- Не включались документы из связанных сущностей (контакты, контрагенты)
|
||||
- Аналогично функционалу отправки исковых писем через pochta-sud.ru
|
||||
|
||||
### 3. Ошибки при скачивании из S3
|
||||
- Неправильный путь к `vendor/autoload.php` (относительный путь не работал)
|
||||
- Слишком длинное имя временного файла (`File name too long`)
|
||||
- Использовался `basename($fileName)` где `$fileName` содержал URL-encoded полный путь
|
||||
|
||||
## Решение
|
||||
|
||||
### 1. Добавлен метод `getRelatedDocs($projectId)`
|
||||
- Получает документы из связанных сущностей проекта:
|
||||
- Контакт (`linktoaccountscontacts`)
|
||||
- Контрагенты (`cf_1994`, `cf_2274`, `cf_2276`)
|
||||
- Возвращает массив документов с полями: `notesid`, `title`, `filename`, `filelocationtype`, `s3_bucket`, `s3_key`
|
||||
|
||||
### 2. Добавлен метод `downloadS3File($s3Bucket, $s3Key, $fileName)`
|
||||
- Скачивает файлы из S3 во временную папку
|
||||
- Использует AWS SDK для работы с S3
|
||||
- Сохраняет пути временных файлов для последующей очистки
|
||||
- Обрабатывает ошибки с подробным логированием
|
||||
|
||||
### 3. Добавлен метод `cleanupTempFiles()`
|
||||
- Очищает все временные файлы после создания архива
|
||||
- Вызывается в `finally` блоке для гарантированной очистки
|
||||
|
||||
### 4. Исправлен метод `getPaths($docs)`
|
||||
- Поддержка как `Vtiger_Record_Model` объектов, так и массивов из `getRelatedDocs`
|
||||
- **ВСЕГДА** запрашивает `s3_bucket`, `s3_key`, `filelocationtype` из БД для Record Models
|
||||
- Правильно определяет S3 файлы (`filelocationtype == 'E' && !empty($s3Bucket) && !empty($s3Key)`)
|
||||
- Для S3 файлов вызывает `downloadS3File()`
|
||||
- Для локальных файлов использует `getFileDetails()`
|
||||
|
||||
### 5. Исправлен метод `getArchive($id)`
|
||||
- Для проектов собирает документы из основной записи и связанных сущностей
|
||||
- Предотвращает дубликаты документов
|
||||
- Вызывает `getPaths()` с объединенным списком документов
|
||||
- Добавлено подробное логирование для отладки
|
||||
- Обработка ошибок с возвратом детальной информации
|
||||
|
||||
### 6. Исправления в `downloadS3File()`
|
||||
- Поиск `vendor/autoload.php` по нескольким путям (относительный и абсолютный)
|
||||
- Использование короткого имени временного файла (только расширение, без полного пути)
|
||||
- Подробное логирование в `/tmp/s3_download_debug.log`
|
||||
|
||||
## Измененные файлы
|
||||
|
||||
### `modules/Vtiger/services/Base.php`
|
||||
- Добавлен метод `getRelatedDocs($projectId)` - получение документов из связанных сущностей
|
||||
- Добавлен метод `downloadS3File($s3Bucket, $s3Key, $fileName)` - скачивание из S3
|
||||
- Добавлен метод `cleanupTempFiles()` - очистка временных файлов
|
||||
- Добавлено свойство `private static $tempFiles = []` - хранение путей временных файлов
|
||||
- Исправлен метод `getPaths($docs)` - поддержка S3 и связанных документов
|
||||
- Исправлен метод `getArchive($id)` - сбор документов из связанных сущностей для проектов
|
||||
|
||||
## Тестирование
|
||||
|
||||
### Тестовый скрипт `test_s3_download.php`
|
||||
- Создан для прямого тестирования `downloadS3File()`
|
||||
- Успешно скачал файл из S3 (9.5 МБ)
|
||||
- Подтвердил работоспособность исправлений
|
||||
|
||||
### Результат
|
||||
- ✅ Архив успешно создается с 25 документами для проекта 396447
|
||||
- ✅ Включаются документы из проекта и связанных сущностей
|
||||
- ✅ S3 файлы корректно скачиваются и добавляются в архив
|
||||
- ✅ Временные файлы автоматически очищаются
|
||||
|
||||
## Технические детали
|
||||
|
||||
### S3 конфигурация
|
||||
- Используется конфиг из `crm_extensions/file_storage/config.php`
|
||||
- Endpoint: `https://s3.twcstorage.ru`
|
||||
- Bucket и Key берутся из полей `vtiger_notes.s3_bucket` и `vtiger_notes.s3_key`
|
||||
|
||||
### Временные файлы
|
||||
- Сохраняются в `sys_get_temp_dir()` (обычно `/tmp`)
|
||||
- Имена: `s3_{uniqid}.{extension}`
|
||||
- Автоматически удаляются после создания архива
|
||||
|
||||
### Логирование
|
||||
- Основные логи: `error_log()` (системный лог PHP)
|
||||
- Отладочные логи: `/tmp/s3_download_debug.log` (временный, удален после исправления)
|
||||
- Ошибки: `/tmp/s3_download_errors.log` (временный, удален после исправления)
|
||||
|
||||
## Коммит
|
||||
Изменения закоммичены в git с описанием исправлений.
|
||||
|
||||
|
||||
236
analyze_deletion_patterns.php
Normal file
236
analyze_deletion_patterns.php
Normal file
@@ -0,0 +1,236 @@
|
||||
<?php
|
||||
error_reporting(E_ALL);
|
||||
ini_set('display_errors', 1);
|
||||
|
||||
require_once '/var/www/fastuser/data/www/crm.clientright.ru/vendor/autoload.php';
|
||||
require_once '/var/www/fastuser/data/www/crm.clientright.ru/config.inc.php';
|
||||
|
||||
$config = require '/var/www/fastuser/data/www/crm.clientright.ru/crm_extensions/file_storage/config.php';
|
||||
$s3Bucket = $config['s3']['bucket'];
|
||||
|
||||
echo "Детальный анализ паттернов удалений\n";
|
||||
echo str_repeat("=", 80) . "\n\n";
|
||||
|
||||
try {
|
||||
$s3Client = new \Aws\S3\S3Client([
|
||||
'version' => 'latest',
|
||||
'region' => $config['s3']['region'],
|
||||
'endpoint' => $config['s3']['endpoint'],
|
||||
'use_path_style_endpoint' => true,
|
||||
'credentials' => [
|
||||
'key' => $config['s3']['key'],
|
||||
'secret' => $config['s3']['secret'],
|
||||
],
|
||||
'suppress_php_deprecation_warning' => true
|
||||
]);
|
||||
|
||||
// Анализируем удаления по времени
|
||||
echo "1. Анализ удалений по времени суток...\n";
|
||||
$deletionsByHour = [];
|
||||
$deletionsByDay = [];
|
||||
$batchDeletions = []; // Массовые удаления (много файлов за короткое время)
|
||||
|
||||
$totalChecked = 0;
|
||||
$maxToCheck = 10000;
|
||||
|
||||
try {
|
||||
$isTruncated = true;
|
||||
$continuationToken = null;
|
||||
$pageCount = 0;
|
||||
$maxPages = 20;
|
||||
|
||||
$currentBatch = [];
|
||||
$lastDeleteTime = null;
|
||||
|
||||
while ($isTruncated && $pageCount < $maxPages && $totalChecked < $maxToCheck) {
|
||||
$params = [
|
||||
'Bucket' => $s3Bucket,
|
||||
'Prefix' => 'crm2/CRM_Active_Files/Documents/Project/',
|
||||
'MaxKeys' => 1000
|
||||
];
|
||||
|
||||
if ($continuationToken) {
|
||||
$params['ContinuationToken'] = $continuationToken;
|
||||
}
|
||||
|
||||
$versions = $s3Client->listObjectVersions($params);
|
||||
$pageCount++;
|
||||
|
||||
if (isset($versions['DeleteMarkers'])) {
|
||||
foreach ($versions['DeleteMarkers'] as $marker) {
|
||||
$totalChecked++;
|
||||
$deleteDate = isset($marker['LastModified']) ? $marker['LastModified'] : null;
|
||||
|
||||
if ($deleteDate) {
|
||||
$dateTime = new DateTime($deleteDate);
|
||||
$hour = $dateTime->format('H');
|
||||
$day = $dateTime->format('Y-m-d');
|
||||
|
||||
if (!isset($deletionsByHour[$hour])) {
|
||||
$deletionsByHour[$hour] = 0;
|
||||
}
|
||||
$deletionsByHour[$hour]++;
|
||||
|
||||
if (!isset($deletionsByDay[$day])) {
|
||||
$deletionsByDay[$day] = 0;
|
||||
}
|
||||
$deletionsByDay[$day]++;
|
||||
|
||||
// Определяем массовые удаления (более 10 файлов за минуту)
|
||||
$deleteTimestamp = strtotime($deleteDate);
|
||||
if ($lastDeleteTime && abs($deleteTimestamp - $lastDeleteTime) < 60) {
|
||||
$currentBatch[] = $marker;
|
||||
} else {
|
||||
if (count($currentBatch) > 10) {
|
||||
$batchDeletions[] = [
|
||||
'count' => count($currentBatch),
|
||||
'time' => date('Y-m-d H:i:s', $lastDeleteTime),
|
||||
'files' => array_slice($currentBatch, 0, 5) // Первые 5 для примера
|
||||
];
|
||||
}
|
||||
$currentBatch = [$marker];
|
||||
}
|
||||
$lastDeleteTime = $deleteTimestamp;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$isTruncated = isset($versions['IsTruncated']) && $versions['IsTruncated'];
|
||||
$continuationToken = isset($versions['NextContinuationToken']) ? $versions['NextContinuationToken'] : null;
|
||||
|
||||
if (!$isTruncated) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Проверяем последний батч
|
||||
if (count($currentBatch) > 10) {
|
||||
$batchDeletions[] = [
|
||||
'count' => count($currentBatch),
|
||||
'time' => $lastDeleteTime ? date('Y-m-d H:i:s', $lastDeleteTime) : 'неизвестно',
|
||||
'files' => array_slice($currentBatch, 0, 5)
|
||||
];
|
||||
}
|
||||
|
||||
echo " Проверено delete markers: $totalChecked\n\n";
|
||||
|
||||
echo " Удаления по часам суток:\n";
|
||||
ksort($deletionsByHour);
|
||||
foreach ($deletionsByHour as $hour => $count) {
|
||||
if ($count > 0) {
|
||||
echo " {$hour}:00 - " . ($hour + 1) . ":00: $count удалений\n";
|
||||
}
|
||||
}
|
||||
echo "\n";
|
||||
|
||||
echo " Удаления по дням (топ 15):\n";
|
||||
arsort($deletionsByDay);
|
||||
$count = 0;
|
||||
foreach ($deletionsByDay as $day => $deleteCount) {
|
||||
echo " $day: $deleteCount удалений\n";
|
||||
if (++$count >= 15) break;
|
||||
}
|
||||
echo "\n";
|
||||
|
||||
if (!empty($batchDeletions)) {
|
||||
echo " Массовые удаления (более 10 файлов за минуту): " . count($batchDeletions) . "\n";
|
||||
foreach (array_slice($batchDeletions, 0, 5) as $batch) {
|
||||
echo " Время: {$batch['time']}, удалено файлов: {$batch['count']}\n";
|
||||
}
|
||||
echo "\n";
|
||||
}
|
||||
|
||||
} catch (\Aws\Exception\AwsException $e) {
|
||||
echo " Ошибка: " . $e->getMessage() . "\n";
|
||||
}
|
||||
|
||||
// Проверяем, может быть это связано с удалением документов в CRM
|
||||
echo "2. Проверка связи с удалением документов в CRM...\n";
|
||||
$pdo = new PDO(
|
||||
"mysql:host={$dbconfig['db_server']};port=3306;dbname={$dbconfig['db_name']};charset=utf8",
|
||||
$dbconfig['db_username'],
|
||||
$dbconfig['db_password'],
|
||||
[PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION]
|
||||
);
|
||||
|
||||
// Проверяем, сколько документов было удалено в последние дни
|
||||
$stmt = $pdo->prepare('
|
||||
SELECT DATE(e.modifiedtime) as delete_date, COUNT(*) as count
|
||||
FROM vtiger_crmentity e
|
||||
INNER JOIN vtiger_notes n ON n.notesid = e.crmid
|
||||
WHERE e.deleted = 1
|
||||
AND n.filelocationtype = "E"
|
||||
AND e.modifiedtime >= DATE_SUB(NOW(), INTERVAL 60 DAY)
|
||||
GROUP BY DATE(e.modifiedtime)
|
||||
ORDER BY delete_date DESC
|
||||
LIMIT 20
|
||||
');
|
||||
$stmt->execute();
|
||||
$deletedDocs = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||
|
||||
if (!empty($deletedDocs)) {
|
||||
echo " Удаленные документы в CRM (за последние 60 дней):\n";
|
||||
foreach ($deletedDocs as $doc) {
|
||||
echo " {$doc['delete_date']}: {$doc['count']} документов\n";
|
||||
}
|
||||
echo "\n";
|
||||
} else {
|
||||
echo " Удаленных документов не найдено\n\n";
|
||||
}
|
||||
|
||||
// Сравниваем даты удалений в S3 и CRM
|
||||
echo "3. Сравнение дат удалений в S3 и CRM...\n";
|
||||
if (!empty($deletionsByDay) && !empty($deletedDocs)) {
|
||||
echo " Сравнение:\n";
|
||||
foreach ($deletedDocs as $doc) {
|
||||
$crmDate = $doc['delete_date'];
|
||||
$s3Count = $deletionsByDay[$crmDate] ?? 0;
|
||||
$crmCount = $doc['count'];
|
||||
|
||||
if ($s3Count > 0) {
|
||||
echo " $crmDate:\n";
|
||||
echo " Удалено в CRM: $crmCount документов\n";
|
||||
echo " Delete markers в S3: $s3Count\n";
|
||||
|
||||
if ($s3Count > $crmCount * 2) {
|
||||
echo " ⚠️ В S3 удалено значительно больше файлов!\n";
|
||||
} elseif (abs($s3Count - $crmCount) <= 10) {
|
||||
echo " ✅ Количество примерно совпадает\n";
|
||||
}
|
||||
echo "\n";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
echo str_repeat("=", 80) . "\n";
|
||||
echo "ВЫВОДЫ:\n\n";
|
||||
|
||||
// Определяем наиболее вероятную причину
|
||||
$maxHour = array_search(max($deletionsByHour), $deletionsByHour);
|
||||
$maxDay = array_search(max($deletionsByDay), $deletionsByDay);
|
||||
$maxDayCount = max($deletionsByDay);
|
||||
|
||||
echo "1. Пик удалений:\n";
|
||||
echo " - Время: {$maxHour}:00\n";
|
||||
echo " - День: $maxDay ($maxDayCount удалений)\n\n";
|
||||
|
||||
if ($maxHour >= 6 && $maxHour <= 8) {
|
||||
echo "2. 💡 ВЕРОЯТНАЯ ПРИЧИНА: Автоматическая задача (cron job)\n";
|
||||
echo " Удаления происходят рано утром (6-8 утра) - типичное время для cron\n\n";
|
||||
}
|
||||
|
||||
if (!empty($batchDeletions)) {
|
||||
echo "3. 💡 ВЕРОЯТНАЯ ПРИЧИНА: Массовое удаление (скрипт или автоматизация)\n";
|
||||
echo " Найдено " . count($batchDeletions) . " случаев массового удаления\n\n";
|
||||
}
|
||||
|
||||
echo "4. 💡 РЕКОМЕНДАЦИЯ: Проверить:\n";
|
||||
echo " - DeleteOrphanedItems в Nextcloud (запускается ежедневно)\n";
|
||||
echo " - Cron задачи, которые могут удалять файлы\n";
|
||||
echo " - Логи Nextcloud на предмет массовых удалений\n";
|
||||
|
||||
} catch (Exception $e) {
|
||||
echo "ОШИБКА: " . $e->getMessage() . "\n";
|
||||
echo "Trace: " . $e->getTraceAsString() . "\n";
|
||||
}
|
||||
|
||||
203
analyze_deletions.php
Normal file
203
analyze_deletions.php
Normal file
@@ -0,0 +1,203 @@
|
||||
<?php
|
||||
error_reporting(E_ALL);
|
||||
ini_set('display_errors', 1);
|
||||
|
||||
require_once '/var/www/fastuser/data/www/crm.clientright.ru/vendor/autoload.php';
|
||||
require_once '/var/www/fastuser/data/www/crm.clientright.ru/config.inc.php';
|
||||
|
||||
$config = require '/var/www/fastuser/data/www/crm.clientright.ru/crm_extensions/file_storage/config.php';
|
||||
$s3Bucket = $config['s3']['bucket'];
|
||||
|
||||
echo "Анализ удалений файлов из S3\n";
|
||||
echo str_repeat("=", 80) . "\n\n";
|
||||
|
||||
try {
|
||||
$s3Client = new \Aws\S3\S3Client([
|
||||
'version' => 'latest',
|
||||
'region' => $config['s3']['region'],
|
||||
'endpoint' => $config['s3']['endpoint'],
|
||||
'use_path_style_endpoint' => true,
|
||||
'credentials' => [
|
||||
'key' => $config['s3']['key'],
|
||||
'secret' => $config['s3']['secret'],
|
||||
],
|
||||
'suppress_php_deprecation_warning' => true
|
||||
]);
|
||||
|
||||
// Анализируем паттерны удалений
|
||||
echo "1. Анализ паттернов удалений по датам...\n";
|
||||
|
||||
$deletionsByDate = [];
|
||||
$deletionsByProject = [];
|
||||
$totalChecked = 0;
|
||||
$maxToCheck = 5000; // Ограничиваем для скорости
|
||||
|
||||
try {
|
||||
$isTruncated = true;
|
||||
$continuationToken = null;
|
||||
$pageCount = 0;
|
||||
$maxPages = 10;
|
||||
|
||||
while ($isTruncated && $pageCount < $maxPages && $totalChecked < $maxToCheck) {
|
||||
$params = [
|
||||
'Bucket' => $s3Bucket,
|
||||
'Prefix' => 'crm2/CRM_Active_Files/Documents/Project/',
|
||||
'MaxKeys' => 1000
|
||||
];
|
||||
|
||||
if ($continuationToken) {
|
||||
$params['ContinuationToken'] = $continuationToken;
|
||||
}
|
||||
|
||||
$versions = $s3Client->listObjectVersions($params);
|
||||
$pageCount++;
|
||||
|
||||
if (isset($versions['DeleteMarkers'])) {
|
||||
foreach ($versions['DeleteMarkers'] as $marker) {
|
||||
$totalChecked++;
|
||||
$key = $marker['Key'];
|
||||
$deleteDate = isset($marker['LastModified']) ? $marker['LastModified'] : null;
|
||||
|
||||
if ($deleteDate) {
|
||||
$dateKey = substr($deleteDate, 0, 10); // YYYY-MM-DD
|
||||
if (!isset($deletionsByDate[$dateKey])) {
|
||||
$deletionsByDate[$dateKey] = 0;
|
||||
}
|
||||
$deletionsByDate[$dateKey]++;
|
||||
}
|
||||
|
||||
// Извлекаем ID проекта из пути
|
||||
if (preg_match('/Project\/([^\/]+)_(\d+)\//', $key, $matches)) {
|
||||
$projectId = $matches[2];
|
||||
if (!isset($deletionsByProject[$projectId])) {
|
||||
$deletionsByProject[$projectId] = 0;
|
||||
}
|
||||
$deletionsByProject[$projectId]++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$isTruncated = isset($versions['IsTruncated']) && $versions['IsTruncated'];
|
||||
$continuationToken = isset($versions['NextContinuationToken']) ? $versions['NextContinuationToken'] : null;
|
||||
|
||||
if (!$isTruncated) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
echo " Проверено delete markers: $totalChecked\n\n";
|
||||
|
||||
// Сортируем по датам
|
||||
krsort($deletionsByDate);
|
||||
|
||||
echo " Удаления по датам (топ 20):\n";
|
||||
$count = 0;
|
||||
foreach ($deletionsByDate as $date => $count) {
|
||||
if ($count > 0) {
|
||||
echo " $date: $count удалений\n";
|
||||
if (++$count >= 20) break;
|
||||
}
|
||||
}
|
||||
echo "\n";
|
||||
|
||||
// Сортируем проекты по количеству удалений
|
||||
arsort($deletionsByProject);
|
||||
|
||||
echo " Проекты с наибольшим количеством удалений (топ 10):\n";
|
||||
$count = 0;
|
||||
foreach ($deletionsByProject as $projectId => $deleteCount) {
|
||||
echo " Проект $projectId: $deleteCount удалений\n";
|
||||
if (++$count >= 10) break;
|
||||
}
|
||||
echo "\n";
|
||||
|
||||
} catch (\Aws\Exception\AwsException $e) {
|
||||
echo " Ошибка: " . $e->getMessage() . "\n";
|
||||
}
|
||||
|
||||
// Проверяем логи системы
|
||||
echo "2. Проверка логов на наличие записей об удалениях...\n";
|
||||
$logFiles = [
|
||||
'/var/log/nginx/error.log',
|
||||
'/var/log/apache2/error.log',
|
||||
'/var/www/fastuser/data/www/crm.clientright.ru/logs/debug.log',
|
||||
'/var/www/fastuser/data/www/crm.clientright.ru/logs/s3_debug.log',
|
||||
];
|
||||
|
||||
foreach ($logFiles as $logFile) {
|
||||
if (file_exists($logFile)) {
|
||||
echo " Проверка: $logFile\n";
|
||||
$lines = file($logFile);
|
||||
if ($lines) {
|
||||
$deleteLines = array_filter($lines, function($line) {
|
||||
return stripos($line, 'delete') !== false &&
|
||||
(stripos($line, 's3') !== false || stripos($line, 'file') !== false);
|
||||
});
|
||||
|
||||
if (!empty($deleteLines)) {
|
||||
echo " Найдено строк с упоминанием удалений: " . count($deleteLines) . "\n";
|
||||
echo " Последние 5 записей:\n";
|
||||
foreach (array_slice($deleteLines, -5) as $line) {
|
||||
echo " " . substr(trim($line), 0, 150) . "\n";
|
||||
}
|
||||
} else {
|
||||
echo " Записей об удалениях не найдено\n";
|
||||
}
|
||||
}
|
||||
echo "\n";
|
||||
}
|
||||
}
|
||||
|
||||
// Проверяем, есть ли скрипты крона, которые могут удалять файлы
|
||||
echo "3. Поиск скриптов, которые могут удалять файлы...\n";
|
||||
$cronFiles = [
|
||||
'/var/spool/cron/crontabs/root',
|
||||
'/etc/cron.d/',
|
||||
'/var/www/fastuser/data/www/crm.clientright.ru/cron/',
|
||||
];
|
||||
|
||||
foreach ($cronFiles as $cronPath) {
|
||||
if (file_exists($cronPath)) {
|
||||
echo " Проверка: $cronPath\n";
|
||||
if (is_dir($cronPath)) {
|
||||
$files = glob($cronPath . '*');
|
||||
foreach ($files as $file) {
|
||||
if (is_file($file)) {
|
||||
$content = file_get_contents($file);
|
||||
if (stripos($content, 'delete') !== false || stripos($content, 'remove') !== false) {
|
||||
echo " Найден файл: $file\n";
|
||||
echo " Содержит упоминания удаления\n";
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
$content = file_get_contents($cronPath);
|
||||
if (stripos($content, 'delete') !== false || stripos($content, 'remove') !== false) {
|
||||
echo " Файл содержит упоминания удаления\n";
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
echo "\n";
|
||||
|
||||
// Проверяем, может быть это связано с синхронизацией Nextcloud
|
||||
echo "4. Проверка связи с Nextcloud синхронизацией...\n";
|
||||
$nextcloudFiles = glob('/var/www/fastuser/data/www/crm.clientright.ru/crm_extensions/file_storage/*sync*.php');
|
||||
if (!empty($nextcloudFiles)) {
|
||||
echo " Найдено файлов синхронизации: " . count($nextcloudFiles) . "\n";
|
||||
foreach ($nextcloudFiles as $file) {
|
||||
$content = file_get_contents($file);
|
||||
if (stripos($content, 'delete') !== false) {
|
||||
echo " $file содержит код удаления\n";
|
||||
}
|
||||
}
|
||||
} else {
|
||||
echo " Файлы синхронизации не найдены\n";
|
||||
}
|
||||
echo "\n";
|
||||
|
||||
} catch (Exception $e) {
|
||||
echo "ОШИБКА: " . $e->getMessage() . "\n";
|
||||
echo "Trace: " . $e->getTraceAsString() . "\n";
|
||||
}
|
||||
|
||||
51
check_docs_filename_371231.php
Normal file
51
check_docs_filename_371231.php
Normal file
@@ -0,0 +1,51 @@
|
||||
<?php
|
||||
require_once '/var/www/fastuser/data/www/crm.clientright.ru/config.inc.php';
|
||||
|
||||
$pdo = new PDO(
|
||||
"mysql:host={$dbconfig['db_server']};port=3306;dbname={$dbconfig['db_name']};charset=utf8",
|
||||
$dbconfig['db_username'],
|
||||
$dbconfig['db_password'],
|
||||
[PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION]
|
||||
);
|
||||
|
||||
$projectId = 371231;
|
||||
|
||||
$sql = "SELECT
|
||||
n.notesid,
|
||||
n.title,
|
||||
n.filelocationtype,
|
||||
n.filename,
|
||||
n.s3_bucket,
|
||||
n.s3_key
|
||||
FROM vtiger_notes n
|
||||
INNER JOIN vtiger_crmentity e ON e.crmid = n.notesid
|
||||
INNER JOIN vtiger_senotesrel snr ON snr.notesid = n.notesid
|
||||
WHERE snr.crmid = ? AND e.deleted = 0
|
||||
ORDER BY n.notesid DESC";
|
||||
|
||||
$stmt = $pdo->prepare($sql);
|
||||
$stmt->execute([$projectId]);
|
||||
$documents = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||
|
||||
echo "Проверка поля filename для документов проекта $projectId\n";
|
||||
echo str_repeat("=", 80) . "\n\n";
|
||||
|
||||
foreach ($documents as $doc) {
|
||||
echo "ID: {$doc['notesid']}\n";
|
||||
echo " Название: {$doc['title']}\n";
|
||||
echo " filelocationtype: {$doc['filelocationtype']}\n";
|
||||
echo " filename (первые 200 символов): " . substr($doc['filename'], 0, 200) . "\n";
|
||||
echo " s3_bucket: " . ($doc['s3_bucket'] ?? 'нет') . "\n";
|
||||
echo " s3_key: " . substr($doc['s3_key'] ?? 'нет', 0, 100) . "\n";
|
||||
|
||||
// Проверяем, является ли filename URL
|
||||
$isUrl = filter_var($doc['filename'], FILTER_VALIDATE_URL);
|
||||
echo " filename является URL: " . ($isUrl ? 'ДА' : 'НЕТ') . "\n";
|
||||
|
||||
// Проверяем, начинается ли filename с http
|
||||
$isHttp = (strpos($doc['filename'], 'http://') === 0 || strpos($doc['filename'], 'https://') === 0);
|
||||
echo " filename начинается с http: " . ($isHttp ? 'ДА' : 'НЕТ') . "\n";
|
||||
|
||||
echo "\n";
|
||||
}
|
||||
|
||||
149
check_project_371231.php
Normal file
149
check_project_371231.php
Normal file
@@ -0,0 +1,149 @@
|
||||
<?php
|
||||
require_once '/var/www/fastuser/data/www/crm.clientright.ru/config.inc.php';
|
||||
|
||||
$pdo = new PDO(
|
||||
"mysql:host={$dbconfig['db_server']};port=3306;dbname={$dbconfig['db_name']};charset=utf8",
|
||||
$dbconfig['db_username'],
|
||||
$dbconfig['db_password'],
|
||||
[PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION]
|
||||
);
|
||||
|
||||
$projectId = 371231;
|
||||
|
||||
// Получаем информацию о проекте
|
||||
$sqlProject = "SELECT projectid, projectname, projectstatus FROM vtiger_project WHERE projectid = ?";
|
||||
$stmtProject = $pdo->prepare($sqlProject);
|
||||
$stmtProject->execute([$projectId]);
|
||||
$project = $stmtProject->fetch(PDO::FETCH_ASSOC);
|
||||
|
||||
if (!$project) {
|
||||
die("❌ Проект $projectId не найден!\n");
|
||||
}
|
||||
|
||||
echo "📋 ПРОЕКТ: {$project['projectname']}\n";
|
||||
echo " ID: {$project['projectid']}\n";
|
||||
echo " Статус: {$project['projectstatus']}\n";
|
||||
echo "\n" . str_repeat("=", 80) . "\n\n";
|
||||
|
||||
// Получаем документы проекта
|
||||
$sql = "SELECT
|
||||
n.notesid,
|
||||
n.title,
|
||||
n.filename,
|
||||
n.filelocationtype,
|
||||
n.foldername,
|
||||
n.s3_bucket,
|
||||
n.s3_key,
|
||||
n.nc_path,
|
||||
n.filesize,
|
||||
e.createdtime,
|
||||
e.modifiedtime,
|
||||
u.user_name,
|
||||
e.deleted
|
||||
FROM vtiger_notes n
|
||||
INNER JOIN vtiger_crmentity e ON e.crmid = n.notesid
|
||||
INNER JOIN vtiger_senotesrel snr ON snr.notesid = n.notesid
|
||||
LEFT JOIN vtiger_users u ON u.id = e.smownerid
|
||||
WHERE snr.crmid = ? AND e.deleted = 0
|
||||
ORDER BY e.createdtime DESC";
|
||||
|
||||
$stmt = $pdo->prepare($sql);
|
||||
$stmt->execute([$projectId]);
|
||||
$documents = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||
|
||||
$count = count($documents);
|
||||
echo "📄 НАЙДЕНО ДОКУМЕНТОВ: $count\n\n";
|
||||
|
||||
if ($count == 0) {
|
||||
echo "⚠️ Документы не найдены!\n";
|
||||
exit;
|
||||
}
|
||||
|
||||
$totalSize = 0;
|
||||
$s3Count = 0;
|
||||
$localCount = 0;
|
||||
$brokenCount = 0;
|
||||
$accessibleCount = 0;
|
||||
$notAccessibleCount = 0;
|
||||
|
||||
foreach ($documents as $i => $doc) {
|
||||
$num = $i + 1;
|
||||
$filelocationtype = $doc['filelocationtype'] ?? 'I';
|
||||
$s3Bucket = $doc['s3_bucket'] ?? null;
|
||||
$s3Key = $doc['s3_key'] ?? null;
|
||||
$filename = $doc['filename'] ?? '';
|
||||
$title = $doc['title'] ?? 'Без названия';
|
||||
|
||||
$isS3 = ($filelocationtype == 'E' && !empty($s3Bucket) && !empty($s3Key));
|
||||
$isLocal = ($filelocationtype == 'I' || empty($filelocationtype));
|
||||
|
||||
if ($isS3) {
|
||||
$s3Count++;
|
||||
$status = "☁️ S3";
|
||||
$filePath = "s3://{$s3Bucket}/{$s3Key}";
|
||||
$accessibleCount++; // Пока считаем доступными, проверим отдельно
|
||||
|
||||
} else {
|
||||
$localCount++;
|
||||
$status = "💾 Локальный";
|
||||
|
||||
// Для локальных файлов проверяем путь
|
||||
if (!empty($filename)) {
|
||||
// Парсим путь из filename
|
||||
$filePath = $filename;
|
||||
if (file_exists($filePath)) {
|
||||
$accessibleCount++;
|
||||
$status .= " ✅";
|
||||
} else {
|
||||
$notAccessibleCount++;
|
||||
$brokenCount++;
|
||||
$status .= " ❌ ФАЙЛ НЕ НАЙДЕН";
|
||||
}
|
||||
} else {
|
||||
$notAccessibleCount++;
|
||||
$brokenCount++;
|
||||
$status .= " ❌ НЕТ ПУТИ";
|
||||
}
|
||||
}
|
||||
|
||||
$size = $doc['filesize'] ?? 0;
|
||||
$totalSize += $size;
|
||||
$sizeStr = $size > 0 ? number_format($size / 1024, 2) . ' KB' : '0 KB';
|
||||
|
||||
echo sprintf(
|
||||
"%3d. [%s] %s\n",
|
||||
$num,
|
||||
$status,
|
||||
$title
|
||||
);
|
||||
echo sprintf(
|
||||
" ID: %d | Размер: %s | Тип: %s\n",
|
||||
$doc['notesid'],
|
||||
$sizeStr,
|
||||
$filelocationtype
|
||||
);
|
||||
|
||||
if ($isS3) {
|
||||
echo sprintf(" S3 Key: %s\n", $s3Key);
|
||||
} else {
|
||||
echo sprintf(" Путь: %s\n", substr($filename, 0, 100));
|
||||
}
|
||||
|
||||
if ($notAccessibleCount > 0 && ($i == $count - 1 || ($i + 1) % 10 == 0)) {
|
||||
echo "\n";
|
||||
}
|
||||
}
|
||||
|
||||
echo "\n" . str_repeat("=", 80) . "\n";
|
||||
echo "📊 СТАТИСТИКА:\n";
|
||||
echo " Всего документов: $count\n";
|
||||
echo " S3 документов: $s3Count\n";
|
||||
echo " Локальных документов: $localCount\n";
|
||||
echo " Доступных: $accessibleCount ✅\n";
|
||||
echo " Недоступных: $notAccessibleCount ❌\n";
|
||||
echo " Общий размер: " . number_format($totalSize / 1024 / 1024, 2) . " MB\n";
|
||||
|
||||
if ($brokenCount > 0) {
|
||||
echo "\n⚠️ ВНИМАНИЕ: Найдено $brokenCount недоступных файлов!\n";
|
||||
}
|
||||
|
||||
51
check_project_371231_simple.php
Normal file
51
check_project_371231_simple.php
Normal file
@@ -0,0 +1,51 @@
|
||||
<?php
|
||||
error_reporting(E_ALL);
|
||||
ini_set('display_errors', 1);
|
||||
|
||||
require_once '/var/www/fastuser/data/www/crm.clientright.ru/config.inc.php';
|
||||
|
||||
$pdo = new PDO(
|
||||
"mysql:host={$dbconfig['db_server']};port=3306;dbname={$dbconfig['db_name']};charset=utf8",
|
||||
$dbconfig['db_username'],
|
||||
$dbconfig['db_password'],
|
||||
[PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION]
|
||||
);
|
||||
|
||||
$projectId = 371231;
|
||||
|
||||
// Получаем документы проекта
|
||||
$sql = "SELECT
|
||||
n.notesid,
|
||||
n.title,
|
||||
n.filename,
|
||||
n.filelocationtype,
|
||||
n.s3_bucket,
|
||||
n.s3_key,
|
||||
n.filesize
|
||||
FROM vtiger_notes n
|
||||
INNER JOIN vtiger_crmentity e ON e.crmid = n.notesid
|
||||
INNER JOIN vtiger_senotesrel snr ON snr.notesid = n.notesid
|
||||
WHERE snr.crmid = ? AND e.deleted = 0
|
||||
ORDER BY e.createdtime DESC";
|
||||
|
||||
$stmt = $pdo->prepare($sql);
|
||||
$stmt->execute([$projectId]);
|
||||
$documents = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||
|
||||
echo "Найдено документов: " . count($documents) . "\n\n";
|
||||
|
||||
foreach ($documents as $i => $doc) {
|
||||
$num = $i + 1;
|
||||
echo "$num. ID: {$doc['notesid']}\n";
|
||||
echo " Название: " . ($doc['title'] ?? 'Нет') . "\n";
|
||||
echo " Тип хранения: " . ($doc['filelocationtype'] ?? 'I') . "\n";
|
||||
|
||||
if ($doc['filelocationtype'] == 'E') {
|
||||
echo " S3 Bucket: " . ($doc['s3_bucket'] ?? 'нет') . "\n";
|
||||
echo " S3 Key: " . ($doc['s3_key'] ?? 'нет') . "\n";
|
||||
} else {
|
||||
echo " Filename: " . substr($doc['filename'] ?? 'нет', 0, 100) . "\n";
|
||||
}
|
||||
echo "\n";
|
||||
}
|
||||
|
||||
129
check_project_373977.php
Normal file
129
check_project_373977.php
Normal file
@@ -0,0 +1,129 @@
|
||||
<?php
|
||||
/**
|
||||
* Проверка документов проекта 373977
|
||||
*/
|
||||
|
||||
error_reporting(E_ALL);
|
||||
ini_set('display_errors', 1);
|
||||
|
||||
require_once '/var/www/fastuser/data/www/crm.clientright.ru/vendor/autoload.php';
|
||||
$config = require '/var/www/fastuser/data/www/crm.clientright.ru/crm_extensions/file_storage/config.php';
|
||||
|
||||
$projectId = 373977;
|
||||
$s3Bucket = $config['s3']['bucket'];
|
||||
|
||||
// Документы проекта из БД
|
||||
$documents = [
|
||||
373981 => 'crm2/CRM_Active_Files/Documents/Project/Полулях_ЧУ_ДПО_ГОРОДСКАЯ_АКАДЕМИЯ_УРБАН_373977/8_Договор_на_оказание_услуг_373981.pdf',
|
||||
373983 => 'crm2/CRM_Active_Files/Documents/Project/Полулях_ЧУ_ДПО_ГОРОДСКАЯ_АКАДЕМИЯ_УРБАН_373977/9_Подтверждение_оплаты_по_договору_373983.pdf',
|
||||
373985 => 'crm2/CRM_Active_Files/Documents/Project/Полулях_ЧУ_ДПО_ГОРОДСКАЯ_АКАДЕМИЯ_УРБАН_373977/10_2_Скрин_личного_кабинета_Истца_и_программа_обуч_373985.pdf',
|
||||
373987 => 'crm2/CRM_Active_Files/Documents/Project/Полулях_ЧУ_ДПО_ГОРОДСКАЯ_АКАДЕМИЯ_УРБАН_373977/10_1_Скрин_личного_кабинета_Истца_и_программа_обуч_373987.pdf',
|
||||
373989 => 'crm2/CRM_Active_Files/Documents/Project/Полулях_ЧУ_ДПО_ГОРОДСКАЯ_АКАДЕМИЯ_УРБАН_373977/11_1_Подтверждение_проведения_претензионной_работы_373989.pdf',
|
||||
373991 => 'crm2/CRM_Active_Files/Documents/Project/Полулях_ЧУ_ДПО_ГОРОДСКАЯ_АКАДЕМИЯ_УРБАН_373977/7_заявление_потребителя_373991.pdf',
|
||||
374017 => 'crm2/CRM_Active_Files/Documents/Project/Полулях_ЧУ_ДПО_ГОРОДСКАЯ_АКАДЕМИЯ_УРБАН_373977/11_Доказательство_соблюдения_претензионного_порядк_374017.pdf',
|
||||
375402 => 'crm2/CRM_Active_Files/Documents/Project/Полулях_ЧУ_ДПО_ГОРОДСКАЯ_АКАДЕМИЯ_УРБАН_373977/11.2_Претензия_в_защиту_интересов_Полулях_Ольга_1_375402.pdf',
|
||||
375404 => 'crm2/CRM_Active_Files/Documents/Project/Полулях_ЧУ_ДПО_ГОРОДСКАЯ_АКАДЕМИЯ_УРБАН_373977/11.3_Доказательство_оплаты_направления_претензии_о_375404.pdf',
|
||||
375406 => 'crm2/CRM_Active_Files/Documents/Project/Полулях_ЧУ_ДПО_ГОРОДСКАЯ_АКАДЕМИЯ_УРБАН_373977/11.4_Доказательство_направления_претензии_ответчик_375406.pdf',
|
||||
376051 => 'crm2/CRM_Active_Files/Documents/Project/Полулях_ЧУ_ДПО_ГОРОДСКАЯ_АКАДЕМИЯ_УРБАН_373977/0_Исковое_заявление_по_делу_Полулях_7_стр_376051.pdf',
|
||||
376054 => 'crm2/CRM_Active_Files/Documents/Project/Полулях_ЧУ_ДПО_ГОРОДСКАЯ_АКАДЕМИЯ_УРБАН_373977/6_Расчет_исковых_требований_Полулях_1_стр_376054.pdf',
|
||||
376080 => 'crm2/CRM_Active_Files/Documents/Project/Полулях_ЧУ_ДПО_ГОРОДСКАЯ_АКАДЕМИЯ_УРБАН_373977/12.1_Доказательство_оплаты_направления_иска_ответч_376080.pdf',
|
||||
376082 => 'crm2/CRM_Active_Files/Documents/Project/Полулях_ЧУ_ДПО_ГОРОДСКАЯ_АКАДЕМИЯ_УРБАН_373977/12.2_Доказательство_направления_иска_ответчику_376082.pdf',
|
||||
396623 => 'crm2/CRM_Active_Files/Documents/396623/ПК_451a1058-ee34-0d48-b2f4-d6dfa522928a.pdf_WITH_ENVELOPE.pdf', // Неправильное место!
|
||||
];
|
||||
|
||||
echo "=== ПРОВЕРКА ДОКУМЕНТОВ ПРОЕКТА {$projectId} ===\n";
|
||||
echo str_repeat("=", 80) . "\n\n";
|
||||
|
||||
try {
|
||||
$s3Client = new \Aws\S3\S3Client([
|
||||
'version' => 'latest',
|
||||
'region' => $config['s3']['region'],
|
||||
'endpoint' => $config['s3']['endpoint'],
|
||||
'use_path_style_endpoint' => true,
|
||||
'credentials' => [
|
||||
'key' => $config['s3']['key'],
|
||||
'secret' => $config['s3']['secret'],
|
||||
],
|
||||
'suppress_php_deprecation_warning' => true
|
||||
]);
|
||||
|
||||
$stats = [
|
||||
'total' => count($documents),
|
||||
'exists' => 0,
|
||||
'missing' => 0,
|
||||
'wrong_place' => 0,
|
||||
'missing_files' => [],
|
||||
'wrong_place_files' => [],
|
||||
];
|
||||
|
||||
$projectPrefix = 'crm2/CRM_Active_Files/Documents/Project/Полулях_ЧУ_ДПО_ГОРОДСКАЯ_АКАДЕМИЯ_УРБАН_373977/';
|
||||
|
||||
foreach ($documents as $docId => $s3Key) {
|
||||
$filename = basename($s3Key);
|
||||
$isInProjectFolder = strpos($s3Key, $projectPrefix) === 0;
|
||||
|
||||
echo "Документ ID: {$docId}\n";
|
||||
echo " Файл: {$filename}\n";
|
||||
echo " Путь: {$s3Key}\n";
|
||||
|
||||
if ($s3Client->doesObjectExist($s3Bucket, $s3Key)) {
|
||||
$object = $s3Client->headObject(['Bucket' => $s3Bucket, 'Key' => $s3Key]);
|
||||
$size = round($object['ContentLength'] / 1024, 2);
|
||||
|
||||
if (!$isInProjectFolder) {
|
||||
echo " ⚠️ Файл существует, но в неправильном месте (размер: {$size} KB)\n";
|
||||
$stats['wrong_place']++;
|
||||
$stats['wrong_place_files'][] = [
|
||||
'doc_id' => $docId,
|
||||
'current_path' => $s3Key,
|
||||
'should_be' => $projectPrefix . $filename,
|
||||
];
|
||||
} else {
|
||||
echo " ✅ Файл существует (размер: {$size} KB)\n";
|
||||
$stats['exists']++;
|
||||
}
|
||||
} else {
|
||||
echo " ❌ Файл отсутствует\n";
|
||||
$stats['missing']++;
|
||||
$stats['missing_files'][] = [
|
||||
'doc_id' => $docId,
|
||||
'path' => $s3Key,
|
||||
];
|
||||
}
|
||||
|
||||
echo "\n";
|
||||
}
|
||||
|
||||
// Итоги
|
||||
echo str_repeat("=", 80) . "\n";
|
||||
echo "ИТОГИ:\n";
|
||||
echo "Всего документов: {$stats['total']}\n";
|
||||
echo "✅ На месте: {$stats['exists']}\n";
|
||||
echo "⚠️ В неправильном месте: {$stats['wrong_place']}\n";
|
||||
echo "❌ Отсутствуют: {$stats['missing']}\n\n";
|
||||
|
||||
if (!empty($stats['wrong_place_files'])) {
|
||||
echo "ФАЙЛЫ В НЕПРАВИЛЬНОМ МЕСТЕ:\n";
|
||||
foreach ($stats['wrong_place_files'] as $file) {
|
||||
echo " - Документ {$file['doc_id']}: {$file['current_path']}\n";
|
||||
echo " Должен быть: {$file['should_be']}\n";
|
||||
}
|
||||
echo "\n";
|
||||
}
|
||||
|
||||
if (!empty($stats['missing_files'])) {
|
||||
echo "ОТСУТСТВУЮЩИЕ ФАЙЛЫ:\n";
|
||||
foreach ($stats['missing_files'] as $file) {
|
||||
echo " - Документ {$file['doc_id']}: {$file['path']}\n";
|
||||
}
|
||||
echo "\n";
|
||||
}
|
||||
|
||||
echo "=== ГОТОВО ===\n";
|
||||
|
||||
} catch (Exception $e) {
|
||||
echo "❌ Ошибка: " . $e->getMessage() . "\n";
|
||||
exit(1);
|
||||
}
|
||||
|
||||
|
||||
103
check_project_391584.php
Normal file
103
check_project_391584.php
Normal file
@@ -0,0 +1,103 @@
|
||||
<?php
|
||||
/**
|
||||
* Проверка документов проекта 391584
|
||||
*/
|
||||
|
||||
error_reporting(E_ALL);
|
||||
ini_set('display_errors', 1);
|
||||
|
||||
require_once '/var/www/fastuser/data/www/crm.clientright.ru/vendor/autoload.php';
|
||||
$config = require '/var/www/fastuser/data/www/crm.clientright.ru/crm_extensions/file_storage/config.php';
|
||||
|
||||
$projectId = 391584;
|
||||
$s3Bucket = $config['s3']['bucket'];
|
||||
$projectPrefix = 'crm2/CRM_Active_Files/Documents/Project/Чужба_ЧОУ_ДПО_ОБРАЗОВАТЕЛЬНЫЕ_ТЕХНОЛОГИИ_СКИЛБОКС_(КОРОБКА_НАВЫКОВ)_391584/';
|
||||
|
||||
// Документы проекта из БД
|
||||
$documents = [
|
||||
391587 => '8_Договор_на_оказание_услуг_391587.pdf',
|
||||
391589 => '9_Подтверждение_оплаты_по_договору_391589.pdf',
|
||||
391591 => '10_1_Скрин_личного_кабинета_Истца_и_программа_обуч_391591.pdf',
|
||||
391593 => '7_заявление_потребителя_391593.pdf',
|
||||
392332 => '11_Доказательство_соблюдения_претензионного_порядк_392332.pdf',
|
||||
392472 => '11.1_Доказательство_соблюдения_претензионного_поря_392472.pdf',
|
||||
392475 => '11.2_Доказательство_соблюдения_претензионного_поря_392475.pdf',
|
||||
395136 => '6_Расчет_иска_Чужба_395136.pdf',
|
||||
395157 => '0_Исковое_заявление_по_делу_Чужба_ЧОУ_ДПО_ОБРАЗОВА_395157.pdf',
|
||||
395744 => '12.1_Доказательство_оплаты_направления_иска_ответч_395744.pdf',
|
||||
];
|
||||
|
||||
echo "=== ПРОВЕРКА ДОКУМЕНТОВ ПРОЕКТА {$projectId} ===\n";
|
||||
echo str_repeat("=", 80) . "\n\n";
|
||||
|
||||
try {
|
||||
$s3Client = new \Aws\S3\S3Client([
|
||||
'version' => 'latest',
|
||||
'region' => $config['s3']['region'],
|
||||
'endpoint' => $config['s3']['endpoint'],
|
||||
'use_path_style_endpoint' => true,
|
||||
'credentials' => [
|
||||
'key' => $config['s3']['key'],
|
||||
'secret' => $config['s3']['secret'],
|
||||
],
|
||||
'suppress_php_deprecation_warning' => true
|
||||
]);
|
||||
|
||||
$stats = [
|
||||
'total' => count($documents),
|
||||
'exists' => 0,
|
||||
'missing' => 0,
|
||||
'missing_files' => [],
|
||||
];
|
||||
|
||||
foreach ($documents as $docId => $filename) {
|
||||
$s3Key = $projectPrefix . $filename;
|
||||
|
||||
echo "Документ ID: {$docId}\n";
|
||||
echo " Файл: {$filename}\n";
|
||||
echo " Путь: {$s3Key}\n";
|
||||
|
||||
$exists = $s3Client->doesObjectExist($s3Bucket, $s3Key);
|
||||
|
||||
if ($exists) {
|
||||
$object = $s3Client->headObject(['Bucket' => $s3Bucket, 'Key' => $s3Key]);
|
||||
$size = round($object['ContentLength'] / 1024, 2);
|
||||
echo " ✅ Файл существует (размер: {$size} KB)\n";
|
||||
$stats['exists']++;
|
||||
} else {
|
||||
echo " ❌ Файл отсутствует\n";
|
||||
$stats['missing']++;
|
||||
$stats['missing_files'][] = [
|
||||
'doc_id' => $docId,
|
||||
'filename' => $filename,
|
||||
'path' => $s3Key,
|
||||
];
|
||||
}
|
||||
|
||||
echo "\n";
|
||||
}
|
||||
|
||||
// Итоги
|
||||
echo str_repeat("=", 80) . "\n";
|
||||
echo "ИТОГИ:\n";
|
||||
echo "Всего документов: {$stats['total']}\n";
|
||||
echo "✅ Существуют: {$stats['exists']}\n";
|
||||
echo "❌ Отсутствуют: {$stats['missing']}\n\n";
|
||||
|
||||
if (!empty($stats['missing_files'])) {
|
||||
echo "ОТСУТСТВУЮЩИЕ ФАЙЛЫ:\n";
|
||||
foreach ($stats['missing_files'] as $file) {
|
||||
echo " - Документ {$file['doc_id']}: {$file['filename']}\n";
|
||||
echo " Путь: {$file['path']}\n";
|
||||
}
|
||||
echo "\n";
|
||||
}
|
||||
|
||||
echo "=== ГОТОВО ===\n";
|
||||
|
||||
} catch (Exception $e) {
|
||||
echo "❌ Ошибка: " . $e->getMessage() . "\n";
|
||||
exit(1);
|
||||
}
|
||||
|
||||
|
||||
158
check_project_398027.php
Normal file
158
check_project_398027.php
Normal file
@@ -0,0 +1,158 @@
|
||||
<?php
|
||||
/**
|
||||
* Проверка документов проекта 398027
|
||||
*/
|
||||
|
||||
error_reporting(E_ALL);
|
||||
ini_set('display_errors', 1);
|
||||
|
||||
require_once '/var/www/fastuser/data/www/crm.clientright.ru/vendor/autoload.php';
|
||||
$config = require '/var/www/fastuser/data/www/crm.clientright.ru/crm_extensions/file_storage/config.php';
|
||||
|
||||
$projectId = 398027;
|
||||
$s3Bucket = $config['s3']['bucket'];
|
||||
$projectPrefix = 'crm2/CRM_Active_Files/Documents/Project/Храмов_ООО_НЕТОЛОГИЯ_398027/';
|
||||
|
||||
// Документы проекта из БД
|
||||
$documents = [
|
||||
398030 => [
|
||||
'current_path' => 'crm2/CRM_Active_Files/Documents/398030/8_Договор_на_оказание_услуг_11-14-2025-16-00-51_Храмов_1_CTP#realfile.pdf',
|
||||
'should_be' => $projectPrefix . '8_Договор_на_оказание_услуг_398030.pdf',
|
||||
],
|
||||
398032 => [
|
||||
'current_path' => 'crm2/CRM_Active_Files/Documents/398032/9_Подтверждение_оплаты_по_договору_11-14-2025-16-00-03_Храмов_1_CTP#realfile.pdf',
|
||||
'should_be' => $projectPrefix . '9_Подтверждение_оплаты_по_договору_398032.pdf',
|
||||
],
|
||||
398034 => [
|
||||
'current_path' => 'crm2/CRM_Active_Files/Documents/398034/10_2_Скрин_личного_кабинета_Истца_и_программа_обучения_11-14-2025-15-47-26_Храмов_41_CTP#realfile.pdf',
|
||||
'should_be' => $projectPrefix . '10_2_Скрин_личного_кабинета_Истца_и_программа_обучения_398034.pdf',
|
||||
],
|
||||
398036 => [
|
||||
'current_path' => 'crm2/CRM_Active_Files/Documents/398036/10_1_Скрин_личного_кабинета_Истца_и_программа_обучения_11-14-2025-15-49-59_Храмов_1_CTP#realfile.pdf',
|
||||
'should_be' => $projectPrefix . '10_1_Скрин_личного_кабинета_Истца_и_программа_обучения_398036.pdf',
|
||||
],
|
||||
398038 => [
|
||||
'current_path' => 'crm2/CRM_Active_Files/Documents/398038/Прочие_документы_11-14-2025-16-06-07_Храмов_3_CTP#realfile.pdf',
|
||||
'should_be' => $projectPrefix . 'Прочие_документы_398038.pdf',
|
||||
],
|
||||
398040 => [
|
||||
'current_path' => 'crm2/CRM_Active_Files/Documents/398040/7_zayavlenie_potrebitelya_Hramov.pdf',
|
||||
'should_be' => $projectPrefix . '7_заявление_потребителя_398040.pdf',
|
||||
],
|
||||
398063 => [
|
||||
'current_path' => 'crm2/CRM_Active_Files/Documents/398063/napravleniya_pretenzii.pdf',
|
||||
'should_be' => $projectPrefix . 'Направление_претензии_398063.pdf',
|
||||
],
|
||||
398584 => [
|
||||
'current_path' => 'crm2/CRM_Active_Files/Documents/Project/Храмов_ООО_НЕТОЛОГИЯ_398027/8_Договор_на_оказание_услуг_398584.pdf',
|
||||
'should_be' => $projectPrefix . '8_Договор_на_оказание_услуг_398584.pdf',
|
||||
],
|
||||
399067 => [
|
||||
'current_path' => 'clientright/0/1763997676315.pdf',
|
||||
'should_be' => $projectPrefix . 'Документ_399067.pdf',
|
||||
],
|
||||
399068 => [
|
||||
'current_path' => 'clientright/0/1763997790309.pdf',
|
||||
'should_be' => $projectPrefix . 'Документ_399068.pdf',
|
||||
],
|
||||
];
|
||||
|
||||
echo "=== ПРОВЕРКА ДОКУМЕНТОВ ПРОЕКТА {$projectId} ===\n";
|
||||
echo str_repeat("=", 80) . "\n\n";
|
||||
|
||||
try {
|
||||
$s3Client = new \Aws\S3\S3Client([
|
||||
'version' => 'latest',
|
||||
'region' => $config['s3']['region'],
|
||||
'endpoint' => $config['s3']['endpoint'],
|
||||
'use_path_style_endpoint' => true,
|
||||
'credentials' => [
|
||||
'key' => $config['s3']['key'],
|
||||
'secret' => $config['s3']['secret'],
|
||||
],
|
||||
'suppress_php_deprecation_warning' => true
|
||||
]);
|
||||
|
||||
$stats = [
|
||||
'total' => count($documents),
|
||||
'exists_correct' => 0,
|
||||
'exists_wrong' => 0,
|
||||
'missing' => 0,
|
||||
'wrong_place_files' => [],
|
||||
'missing_files' => [],
|
||||
];
|
||||
|
||||
foreach ($documents as $docId => $paths) {
|
||||
$currentPath = $paths['current_path'];
|
||||
$shouldBe = $paths['should_be'];
|
||||
$filename = basename($shouldBe);
|
||||
|
||||
echo "Документ ID: {$docId}\n";
|
||||
echo " Файл: {$filename}\n";
|
||||
echo " Текущий путь: {$currentPath}\n";
|
||||
echo " Должен быть: {$shouldBe}\n";
|
||||
|
||||
$existsCurrent = $s3Client->doesObjectExist($s3Bucket, $currentPath);
|
||||
$existsCorrect = $s3Client->doesObjectExist($s3Bucket, $shouldBe);
|
||||
|
||||
if ($existsCorrect) {
|
||||
$object = $s3Client->headObject(['Bucket' => $s3Bucket, 'Key' => $shouldBe]);
|
||||
$size = round($object['ContentLength'] / 1024, 2);
|
||||
echo " ✅ Файл уже в правильном месте (размер: {$size} KB)\n";
|
||||
$stats['exists_correct']++;
|
||||
} elseif ($existsCurrent) {
|
||||
$object = $s3Client->headObject(['Bucket' => $s3Bucket, 'Key' => $currentPath]);
|
||||
$size = round($object['ContentLength'] / 1024, 2);
|
||||
echo " ⚠️ Файл существует, но в неправильном месте (размер: {$size} KB)\n";
|
||||
$stats['exists_wrong']++;
|
||||
$stats['wrong_place_files'][] = [
|
||||
'doc_id' => $docId,
|
||||
'current_path' => $currentPath,
|
||||
'should_be' => $shouldBe,
|
||||
];
|
||||
} else {
|
||||
echo " ❌ Файл отсутствует\n";
|
||||
$stats['missing']++;
|
||||
$stats['missing_files'][] = [
|
||||
'doc_id' => $docId,
|
||||
'path' => $currentPath,
|
||||
];
|
||||
}
|
||||
|
||||
echo "\n";
|
||||
}
|
||||
|
||||
// Итоги
|
||||
echo str_repeat("=", 80) . "\n";
|
||||
echo "ИТОГИ:\n";
|
||||
echo "Всего документов: {$stats['total']}\n";
|
||||
echo "✅ На месте: {$stats['exists_correct']}\n";
|
||||
echo "⚠️ В неправильном месте: {$stats['exists_wrong']}\n";
|
||||
echo "❌ Отсутствуют: {$stats['missing']}\n\n";
|
||||
|
||||
if (!empty($stats['wrong_place_files'])) {
|
||||
echo "ФАЙЛЫ В НЕПРАВИЛЬНОМ МЕСТЕ:\n";
|
||||
foreach ($stats['wrong_place_files'] as $file) {
|
||||
echo " - Документ {$file['doc_id']}\n";
|
||||
echo " От: {$file['current_path']}\n";
|
||||
echo " К: {$file['should_be']}\n";
|
||||
}
|
||||
echo "\n";
|
||||
}
|
||||
|
||||
if (!empty($stats['missing_files'])) {
|
||||
echo "ОТСУТСТВУЮЩИЕ ФАЙЛЫ:\n";
|
||||
foreach ($stats['missing_files'] as $file) {
|
||||
echo " - Документ {$file['doc_id']}: {$file['path']}\n";
|
||||
}
|
||||
echo "\n";
|
||||
}
|
||||
|
||||
echo "=== ГОТОВО ===\n";
|
||||
|
||||
} catch (Exception $e) {
|
||||
echo "❌ Ошибка: " . $e->getMessage() . "\n";
|
||||
exit(1);
|
||||
}
|
||||
|
||||
|
||||
172
check_project_files_access.php
Normal file
172
check_project_files_access.php
Normal file
@@ -0,0 +1,172 @@
|
||||
<?php
|
||||
error_reporting(E_ALL);
|
||||
ini_set('display_errors', 1);
|
||||
|
||||
require_once '/var/www/fastuser/data/www/crm.clientright.ru/vendor/autoload.php';
|
||||
require_once '/var/www/fastuser/data/www/crm.clientright.ru/config.inc.php';
|
||||
|
||||
$config = require '/var/www/fastuser/data/www/crm.clientright.ru/crm_extensions/file_storage/config.php';
|
||||
$projectId = 384256;
|
||||
$s3Bucket = $config['s3']['bucket'];
|
||||
|
||||
echo "Проверка доступности всех файлов проекта $projectId\n";
|
||||
echo str_repeat("=", 80) . "\n\n";
|
||||
|
||||
try {
|
||||
// Инициализация S3 клиента
|
||||
$s3Client = new \Aws\S3\S3Client([
|
||||
'version' => 'latest',
|
||||
'region' => $config['s3']['region'],
|
||||
'endpoint' => $config['s3']['endpoint'],
|
||||
'use_path_style_endpoint' => true,
|
||||
'credentials' => [
|
||||
'key' => $config['s3']['key'],
|
||||
'secret' => $config['s3']['secret'],
|
||||
],
|
||||
'suppress_php_deprecation_warning' => true
|
||||
]);
|
||||
|
||||
// Подключение к БД
|
||||
$pdo = new PDO(
|
||||
"mysql:host={$dbconfig['db_server']};port=3306;dbname={$dbconfig['db_name']};charset=utf8",
|
||||
$dbconfig['db_username'],
|
||||
$dbconfig['db_password'],
|
||||
[PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION]
|
||||
);
|
||||
|
||||
// Получаем все документы проекта
|
||||
$stmt = $pdo->prepare('
|
||||
SELECT n.notesid, n.title, n.s3_key, n.filename, n.filelocationtype
|
||||
FROM vtiger_notes n
|
||||
INNER JOIN vtiger_crmentity e ON e.crmid = n.notesid
|
||||
INNER JOIN vtiger_senotesrel snr ON snr.notesid = n.notesid
|
||||
WHERE snr.crmid = ? AND e.deleted = 0
|
||||
ORDER BY n.notesid ASC
|
||||
');
|
||||
$stmt->execute([$projectId]);
|
||||
$docs = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||
|
||||
echo "Всего документов в проекте: " . count($docs) . "\n\n";
|
||||
|
||||
$accessible = [];
|
||||
$notAccessible = [];
|
||||
|
||||
foreach ($docs as $doc) {
|
||||
$docId = $doc['notesid'];
|
||||
$title = $doc['title'];
|
||||
$s3Key = $doc['s3_key'];
|
||||
$filelocationtype = $doc['filelocationtype'];
|
||||
|
||||
echo "ID: $docId | $title\n";
|
||||
|
||||
if ($filelocationtype == 'E' && !empty($s3Key)) {
|
||||
// Проверяем доступность в S3
|
||||
try {
|
||||
$result = $s3Client->headObject([
|
||||
'Bucket' => $s3Bucket,
|
||||
'Key' => $s3Key
|
||||
]);
|
||||
|
||||
$size = number_format($result['ContentLength'] / 1024, 2);
|
||||
echo " ✅ Доступен в S3 (" . $size . " KB)\n";
|
||||
echo " Путь: $s3Key\n";
|
||||
$accessible[] = ['doc' => $doc, 'size' => $result['ContentLength']];
|
||||
|
||||
} catch (\Aws\Exception\AwsException $e) {
|
||||
if ($e->getAwsErrorCode() == 'NotFound') {
|
||||
echo " ❌ НЕ найден в S3\n";
|
||||
echo " Ожидаемый путь: $s3Key\n";
|
||||
$notAccessible[] = $doc;
|
||||
} else {
|
||||
echo " ⚠️ Ошибка доступа: " . $e->getAwsErrorCode() . "\n";
|
||||
$notAccessible[] = $doc;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
echo " ⚠️ Тип хранения: " . ($filelocationtype ?: 'не указан') . "\n";
|
||||
if (!empty($doc['filename'])) {
|
||||
echo " Filename: " . substr($doc['filename'], 0, 100) . "\n";
|
||||
}
|
||||
}
|
||||
echo "\n";
|
||||
}
|
||||
|
||||
echo str_repeat("=", 80) . "\n";
|
||||
echo "СТАТИСТИКА:\n";
|
||||
echo " Доступных файлов: " . count($accessible) . "\n";
|
||||
echo " Недоступных файлов: " . count($notAccessible) . "\n\n";
|
||||
|
||||
// Поиск недоступных файлов в других местах S3
|
||||
if (!empty($notAccessible)) {
|
||||
echo "Поиск недоступных файлов в других местах S3...\n\n";
|
||||
|
||||
foreach ($notAccessible as $doc) {
|
||||
$docId = $doc['notesid'];
|
||||
$title = $doc['title'];
|
||||
|
||||
echo "Поиск файла для документа $docId: $title\n";
|
||||
|
||||
// Ищем по ID документа в разных местах
|
||||
$searchPatterns = [
|
||||
"temp/$projectId/",
|
||||
"temp/",
|
||||
"crm2/CRM_Active_Files/Documents/",
|
||||
"Documents/",
|
||||
];
|
||||
|
||||
$found = false;
|
||||
foreach ($searchPatterns as $prefix) {
|
||||
try {
|
||||
$objects = $s3Client->listObjectsV2([
|
||||
'Bucket' => $s3Bucket,
|
||||
'Prefix' => $prefix,
|
||||
'MaxKeys' => 1000
|
||||
]);
|
||||
|
||||
if (isset($objects['Contents'])) {
|
||||
foreach ($objects['Contents'] as $object) {
|
||||
$key = $object['Key'];
|
||||
|
||||
// Ищем файлы, содержащие ID документа или похожие названия
|
||||
if (strpos($key, (string)$docId) !== false ||
|
||||
strpos($key, (string)($docId - 1)) !== false ||
|
||||
strpos($key, (string)($docId + 1)) !== false) {
|
||||
|
||||
// Проверяем доступность
|
||||
try {
|
||||
$headResult = $s3Client->headObject([
|
||||
'Bucket' => $s3Bucket,
|
||||
'Key' => $key
|
||||
]);
|
||||
|
||||
echo " ✅ НАЙДЕН: $key\n";
|
||||
echo " Размер: " . number_format($headResult['ContentLength'] / 1024, 2) . " KB\n";
|
||||
echo " Дата: " . ($headResult['LastModified'] ?? 'не указана') . "\n";
|
||||
|
||||
// Предлагаем переместить
|
||||
echo " 💡 Рекомендация: переместить в правильный путь\n";
|
||||
$found = true;
|
||||
break 2;
|
||||
} catch (\Aws\Exception\AwsException $e) {
|
||||
// Пропускаем
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (\Aws\Exception\AwsException $e) {
|
||||
// Пропускаем ошибки
|
||||
}
|
||||
}
|
||||
|
||||
if (!$found) {
|
||||
echo " ❌ Файл не найден ни в одном месте S3\n";
|
||||
}
|
||||
echo "\n";
|
||||
}
|
||||
}
|
||||
|
||||
} catch (Exception $e) {
|
||||
echo "ОШИБКА: " . $e->getMessage() . "\n";
|
||||
echo "Trace: " . $e->getTraceAsString() . "\n";
|
||||
}
|
||||
|
||||
116
check_s3_access_371231.php
Normal file
116
check_s3_access_371231.php
Normal file
@@ -0,0 +1,116 @@
|
||||
<?php
|
||||
error_reporting(E_ALL);
|
||||
ini_set('display_errors', 1);
|
||||
|
||||
require_once '/var/www/fastuser/data/www/crm.clientright.ru/vendor/autoload.php';
|
||||
require_once '/var/www/fastuser/data/www/crm.clientright.ru/config.inc.php';
|
||||
require_once '/var/www/fastuser/data/www/crm.clientright.ru/crm_extensions/file_storage/config.php';
|
||||
|
||||
$pdo = new PDO(
|
||||
"mysql:host={$dbconfig['db_server']};port=3306;dbname={$dbconfig['db_name']};charset=utf8",
|
||||
$dbconfig['db_username'],
|
||||
$dbconfig['db_password'],
|
||||
[PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION]
|
||||
);
|
||||
|
||||
$projectId = 371231;
|
||||
|
||||
// Получаем документы проекта
|
||||
$sql = "SELECT
|
||||
n.notesid,
|
||||
n.title,
|
||||
n.filename,
|
||||
n.filelocationtype,
|
||||
n.s3_bucket,
|
||||
n.s3_key
|
||||
FROM vtiger_notes n
|
||||
INNER JOIN vtiger_crmentity e ON e.crmid = n.notesid
|
||||
INNER JOIN vtiger_senotesrel snr ON snr.notesid = n.notesid
|
||||
WHERE snr.crmid = ? AND e.deleted = 0 AND n.filelocationtype = 'E'
|
||||
ORDER BY n.notesid DESC";
|
||||
|
||||
$stmt = $pdo->prepare($sql);
|
||||
$stmt->execute([$projectId]);
|
||||
$documents = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||
|
||||
echo "Проверка доступности S3 файлов для проекта $projectId\n";
|
||||
echo str_repeat("=", 80) . "\n\n";
|
||||
|
||||
// Инициализируем S3 клиент
|
||||
$s3Config = $config['s3'];
|
||||
$awsClient = new \Aws\S3\S3Client([
|
||||
'version' => 'latest',
|
||||
'region' => $s3Config['region'],
|
||||
'endpoint' => $s3Config['endpoint'],
|
||||
'credentials' => [
|
||||
'key' => $s3Config['key'],
|
||||
'secret' => $s3Config['secret'],
|
||||
],
|
||||
'use_path_style_endpoint' => true,
|
||||
]);
|
||||
|
||||
$accessible = 0;
|
||||
$notAccessible = 0;
|
||||
$errors = [];
|
||||
|
||||
foreach ($documents as $doc) {
|
||||
$notesid = $doc['notesid'];
|
||||
$title = $doc['title'];
|
||||
$s3Bucket = $doc['s3_bucket'];
|
||||
$s3Key = $doc['s3_key'];
|
||||
|
||||
try {
|
||||
$exists = $awsClient->doesObjectExist($s3Bucket, $s3Key);
|
||||
|
||||
if ($exists) {
|
||||
$accessible++;
|
||||
echo "✅ ID: $notesid - $title\n";
|
||||
} else {
|
||||
$notAccessible++;
|
||||
echo "❌ ID: $notesid - $title\n";
|
||||
echo " S3 Key: $s3Key\n";
|
||||
$errors[] = [
|
||||
'id' => $notesid,
|
||||
'title' => $title,
|
||||
's3_key' => $s3Key,
|
||||
'reason' => 'File does not exist in S3'
|
||||
];
|
||||
}
|
||||
} catch (\Aws\Exception\AwsException $e) {
|
||||
$notAccessible++;
|
||||
echo "❌ ID: $notesid - $title\n";
|
||||
echo " Ошибка: " . $e->getMessage() . "\n";
|
||||
echo " S3 Key: $s3Key\n";
|
||||
$errors[] = [
|
||||
'id' => $notesid,
|
||||
'title' => $title,
|
||||
's3_key' => $s3Key,
|
||||
'reason' => 'AWS Exception: ' . $e->getMessage()
|
||||
];
|
||||
} catch (Exception $e) {
|
||||
$notAccessible++;
|
||||
echo "❌ ID: $notesid - $title\n";
|
||||
echo " Ошибка: " . $e->getMessage() . "\n";
|
||||
$errors[] = [
|
||||
'id' => $notesid,
|
||||
'title' => $title,
|
||||
's3_key' => $s3Key,
|
||||
'reason' => 'Exception: ' . $e->getMessage()
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
echo "\n" . str_repeat("=", 80) . "\n";
|
||||
echo "СТАТИСТИКА:\n";
|
||||
echo " Доступных файлов: $accessible\n";
|
||||
echo " Недоступных файлов: $notAccessible\n";
|
||||
|
||||
if (!empty($errors)) {
|
||||
echo "\nНЕДОСТУПНЫЕ ФАЙЛЫ:\n";
|
||||
foreach ($errors as $error) {
|
||||
echo " - ID {$error['id']}: {$error['title']}\n";
|
||||
echo " S3 Key: {$error['s3_key']}\n";
|
||||
echo " Причина: {$error['reason']}\n\n";
|
||||
}
|
||||
}
|
||||
|
||||
191
cleanup_disk.php
Normal file
191
cleanup_disk.php
Normal file
@@ -0,0 +1,191 @@
|
||||
<?php
|
||||
/**
|
||||
* Скрипт для безопасной очистки диска
|
||||
*
|
||||
* Удаляет:
|
||||
* 1. Старые бэкапы SQL (оставляет последние N)
|
||||
* 2. Большие логи (очищает или удаляет старые)
|
||||
*/
|
||||
|
||||
error_reporting(E_ALL);
|
||||
ini_set('display_errors', 1);
|
||||
|
||||
$dryRun = !isset($argv[1]) || $argv[1] !== '--execute';
|
||||
$keepBackups = isset($argv[2]) ? (int)$argv[2] : (isset($argv[1]) && is_numeric($argv[1]) ? (int)$argv[1] : 5); // Сколько бэкапов оставить
|
||||
|
||||
echo "Очистка диска\n";
|
||||
echo str_repeat("=", 80) . "\n\n";
|
||||
|
||||
if (!$dryRun) {
|
||||
echo "⚠️ РЕЖИМ ПРОВЕРКИ (dry-run)\n";
|
||||
echo " Для реального удаления запустите: php cleanup_disk.php --execute [количество_бэкапов]\n";
|
||||
echo " Пример: php cleanup_disk.php --execute 5\n\n";
|
||||
} else {
|
||||
echo "⚠️ РЕЖИМ УДАЛЕНИЯ - файлы будут удалены!\n\n";
|
||||
}
|
||||
|
||||
$stats = [
|
||||
'backups_found' => 0,
|
||||
'backups_to_delete' => 0,
|
||||
'backups_size' => 0,
|
||||
'logs_found' => 0,
|
||||
'logs_to_clean' => 0,
|
||||
'logs_size' => 0,
|
||||
'total_freed' => 0
|
||||
];
|
||||
|
||||
// 1. Обработка бэкапов SQL
|
||||
echo "1. ОБРАБОТКА БЭКАПОВ SQL\n";
|
||||
echo str_repeat("-", 80) . "\n";
|
||||
|
||||
$backupDir = __DIR__;
|
||||
$backups = glob($backupDir . '/backup_before_migration_*.sql');
|
||||
|
||||
if (empty($backups)) {
|
||||
echo " Бэкапы не найдены\n\n";
|
||||
} else {
|
||||
$stats['backups_found'] = count($backups);
|
||||
|
||||
// Сортируем по дате изменения (новые первыми)
|
||||
usort($backups, function($a, $b) {
|
||||
return filemtime($b) - filemtime($a);
|
||||
});
|
||||
|
||||
echo " Найдено бэкапов: {$stats['backups_found']}\n";
|
||||
echo " Оставим последних: {$keepBackups}\n";
|
||||
echo " Будет удалено: " . max(0, $stats['backups_found'] - $keepBackups) . "\n\n";
|
||||
|
||||
$toDelete = array_slice($backups, $keepBackups);
|
||||
$stats['backups_to_delete'] = count($toDelete);
|
||||
|
||||
if (!empty($toDelete)) {
|
||||
echo " Файлы для удаления:\n";
|
||||
foreach ($toDelete as $backup) {
|
||||
$size = filesize($backup);
|
||||
$stats['backups_size'] += $size;
|
||||
$sizeMB = round($size / 1024 / 1024, 2);
|
||||
$date = date('Y-m-d H:i:s', filemtime($backup));
|
||||
echo " - " . basename($backup) . " ({$sizeMB}MB, {$date})\n";
|
||||
}
|
||||
|
||||
if ($dryRun && !empty($toDelete)) {
|
||||
echo "\n Удаление файлов...\n";
|
||||
foreach ($toDelete as $backup) {
|
||||
if (unlink($backup)) {
|
||||
echo " ✅ " . basename($backup) . " - удален\n";
|
||||
} else {
|
||||
echo " ❌ " . basename($backup) . " - ошибка удаления\n";
|
||||
}
|
||||
}
|
||||
}
|
||||
echo "\n";
|
||||
} else {
|
||||
echo " Нет файлов для удаления\n\n";
|
||||
}
|
||||
}
|
||||
|
||||
// 2. Обработка больших логов
|
||||
echo "2. ОБРАБОТКА ЛОГОВ\n";
|
||||
echo str_repeat("-", 80) . "\n";
|
||||
|
||||
$largeLogs = [
|
||||
__DIR__ . '/wdall.log',
|
||||
__DIR__ . '/wdall2.log',
|
||||
__DIR__ . '/wa_inbound.log',
|
||||
__DIR__ . '/wa_outbound.log',
|
||||
];
|
||||
|
||||
foreach ($largeLogs as $logFile) {
|
||||
if (file_exists($logFile)) {
|
||||
$size = filesize($logFile);
|
||||
$sizeMB = round($size / 1024 / 1024, 2);
|
||||
|
||||
if ($size > 10 * 1024 * 1024) { // Больше 10MB
|
||||
$stats['logs_found']++;
|
||||
$stats['logs_size'] += $size;
|
||||
|
||||
echo " Найден большой лог: " . basename($logFile) . " ({$sizeMB}MB)\n";
|
||||
|
||||
if ($dryRun) {
|
||||
// Очищаем лог (оставляем последние 1000 строк)
|
||||
$lines = file($logFile);
|
||||
if (count($lines) > 1000) {
|
||||
$keepLines = array_slice($lines, -1000);
|
||||
if (file_put_contents($logFile, implode('', $keepLines))) {
|
||||
$newSize = filesize($logFile);
|
||||
$freedMB = round(($size - $newSize) / 1024 / 1024, 2);
|
||||
echo " ✅ Очищен (освобождено {$freedMB}MB)\n";
|
||||
$stats['logs_to_clean']++;
|
||||
} else {
|
||||
echo " ❌ Ошибка очистки\n";
|
||||
}
|
||||
} else {
|
||||
echo " ℹ️ Лог небольшой, пропущен\n";
|
||||
}
|
||||
} else {
|
||||
echo " ⏸️ Будет очищен (dry-run)\n";
|
||||
$stats['logs_to_clean']++;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Обработка логов в папке logs/
|
||||
$logsDir = __DIR__ . '/logs';
|
||||
if (is_dir($logsDir)) {
|
||||
$logFiles = glob($logsDir . '/*.log*');
|
||||
foreach ($logFiles as $logFile) {
|
||||
$size = filesize($logFile);
|
||||
if ($size > 20 * 1024 * 1024) { // Больше 20MB
|
||||
$sizeMB = round($size / 1024 / 1024, 2);
|
||||
$mtime = filemtime($logFile);
|
||||
$daysOld = (time() - $mtime) / 86400;
|
||||
|
||||
if ($daysOld > 7) {
|
||||
$stats['logs_found']++;
|
||||
$stats['logs_size'] += $size;
|
||||
|
||||
echo " Старый большой лог: " . basename($logFile) . " ({$sizeMB}MB, " . round($daysOld) . " дней)\n";
|
||||
|
||||
if ($dryRun) {
|
||||
if (unlink($logFile)) {
|
||||
echo " ✅ Удален\n";
|
||||
$stats['logs_to_clean']++;
|
||||
} else {
|
||||
echo " ❌ Ошибка удаления\n";
|
||||
}
|
||||
} else {
|
||||
echo " ⏸️ Будет удален (dry-run)\n";
|
||||
$stats['logs_to_clean']++;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
echo "\n";
|
||||
|
||||
// Итоговая статистика
|
||||
$stats['total_freed'] = $stats['backups_size'] + $stats['logs_size'];
|
||||
$totalFreedMB = round($stats['total_freed'] / 1024 / 1024, 2);
|
||||
$totalFreedGB = round($stats['total_freed'] / 1024 / 1024 / 1024, 2);
|
||||
|
||||
echo str_repeat("=", 80) . "\n";
|
||||
echo "ИТОГОВАЯ СТАТИСТИКА:\n\n";
|
||||
echo "Бэкапы:\n";
|
||||
echo " - Найдено: {$stats['backups_found']}\n";
|
||||
echo " - Будет удалено: {$stats['backups_to_delete']}\n";
|
||||
echo " - Размер: " . round($stats['backups_size'] / 1024 / 1024 / 1024, 2) . "GB\n\n";
|
||||
echo "Логи:\n";
|
||||
echo " - Найдено больших: {$stats['logs_found']}\n";
|
||||
echo " - Будет обработано: {$stats['logs_to_clean']}\n";
|
||||
echo " - Размер: " . round($stats['logs_size'] / 1024 / 1024, 2) . "MB\n\n";
|
||||
echo "ОБЩЕЕ ОСВОБОЖДЕНИЕ: {$totalFreedGB}GB ({$totalFreedMB}MB)\n\n";
|
||||
|
||||
if (!$dryRun) {
|
||||
echo "⚠️ Это был режим проверки. Для реального удаления запустите:\n";
|
||||
echo " php cleanup_disk.php --execute {$keepBackups}\n\n";
|
||||
} else {
|
||||
echo "✅ Очистка завершена!\n\n";
|
||||
}
|
||||
|
||||
36
cleanup_nextcloud_logs.sh
Executable file
36
cleanup_nextcloud_logs.sh
Executable file
@@ -0,0 +1,36 @@
|
||||
#!/bin/bash
|
||||
# Скрипт для очистки логов Nextcloud
|
||||
|
||||
echo "=== ОЧИСТКА ЛОГОВ NEXTCLOUD ==="
|
||||
echo ""
|
||||
|
||||
# Проверяем размеры логов
|
||||
echo "Размеры логов до очистки:"
|
||||
docker exec nextcloud-fresh find /var/www/html/data -name "*.log" -type f -exec ls -lh {} \; 2>&1 | awk '{print $5, $9}'
|
||||
|
||||
echo ""
|
||||
echo "Очистка логов..."
|
||||
|
||||
# Очищаем nextcloud.log если больше 100MB
|
||||
SIZE=$(docker exec nextcloud-fresh stat -c%s /var/www/html/data/nextcloud.log 2>/dev/null || echo "0")
|
||||
if [ "$SIZE" -gt 104857600 ]; then
|
||||
echo "nextcloud.log больше 100MB, очищаем..."
|
||||
docker exec nextcloud-fresh truncate -s 0 /var/www/html/data/nextcloud.log
|
||||
echo "✅ nextcloud.log очищен"
|
||||
fi
|
||||
|
||||
# Очищаем flow.log если больше 50MB
|
||||
SIZE=$(docker exec nextcloud-fresh stat -c%s /var/www/html/data/flow.log 2>/dev/null || echo "0")
|
||||
if [ "$SIZE" -gt 52428800 ]; then
|
||||
echo "flow.log больше 50MB, очищаем..."
|
||||
docker exec nextcloud-fresh truncate -s 0 /var/www/html/data/flow.log
|
||||
echo "✅ flow.log очищен"
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo "Размеры логов после очистки:"
|
||||
docker exec nextcloud-fresh find /var/www/html/data -name "*.log" -type f -exec ls -lh {} \; 2>&1 | awk '{print $5, $9}'
|
||||
|
||||
echo ""
|
||||
echo "✅ Очистка завершена"
|
||||
|
||||
@@ -79,9 +79,25 @@ class S3Client {
|
||||
|
||||
/**
|
||||
* Создание временной ссылки для скачивания
|
||||
* @param string $s3Key S3 ключ файла
|
||||
* @param mixed $expiresIn Время жизни URL в секундах (число) или строка типа '+10 minutes'
|
||||
*/
|
||||
public function getPresignedUrl($s3Key, $expiresIn = 3600) {
|
||||
try {
|
||||
// Преобразуем строку TTL в секунды, если нужно
|
||||
if (is_string($expiresIn)) {
|
||||
// Если строка начинается с '+', используем её как есть для strtotime
|
||||
if (strpos($expiresIn, '+') === 0) {
|
||||
$expiresIn = strtotime($expiresIn) - time();
|
||||
} else {
|
||||
// Иначе пытаемся распарсить как число секунд
|
||||
$expiresIn = (int)$expiresIn;
|
||||
}
|
||||
}
|
||||
|
||||
// Минимум 60 секунд, максимум 7 дней
|
||||
$expiresIn = max(60, min($expiresIn, 604800));
|
||||
|
||||
$cmd = $this->client->getCommand('GetObject', [
|
||||
'Bucket' => $this->bucket,
|
||||
'Key' => $s3Key
|
||||
@@ -97,7 +113,9 @@ class S3Client {
|
||||
} catch (AwsException $e) {
|
||||
return [
|
||||
'success' => false,
|
||||
'error' => $e->getMessage()
|
||||
'error' => $e->getMessage(),
|
||||
'error_code' => $e->getAwsErrorCode(),
|
||||
'request_id' => $e->getAwsRequestId()
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -432,3 +432,9 @@ PUBLISH ai:response:task-xxx {"success": true, "documentUrl": "..."}
|
||||
- Ошибки изолированы
|
||||
- Легко отлаживать
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -197,3 +197,9 @@ https://crm.clientright.ru/crm_extensions/file_storage/api/create_document_with_
|
||||
- Документ сразу доступен для редактирования в OnlyOffice
|
||||
- Путь формируется автоматически: `{module}/{recordName}_{recordId}/{fileName}.{ext}`
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -244,3 +244,9 @@ _курсив_
|
||||
3. **Гибкость** — можно комбинировать элементы
|
||||
4. **Автоматическое форматирование** — документ получается красивым без ручной правки
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -152,3 +152,9 @@ curl -X POST "https://crm.clientright.ru/crm_extensions/file_storage/api/create_
|
||||
}'
|
||||
```
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -211,3 +211,9 @@ curl -u admin:office "https://office.clientright.ru:8443/remote.php/dav/files/ad
|
||||
- [Nextcloud WebDAV API](https://docs.nextcloud.com/server/latest/user_manual/files/webdav.html)
|
||||
- [OnlyOffice Integration](https://api.onlyoffice.com/)
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -246,3 +246,9 @@ echo json_encode(['success' => true, 'templates' => $templates]);
|
||||
3. **Решение:** Использовать WebDAV PROPFIND для получения списка файлов из папки Templates
|
||||
4. **Статус:** Наш текущий подход (WebDAV + PHPWord) является правильным и оптимальным решением
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -133,3 +133,9 @@ curl "https://office.clientright.ru:8443/index.php/apps/onlyoffice/ajax/template
|
||||
3. Протестировать получение списка через `list_templates.php`
|
||||
4. Использовать шаблоны через `create_from_template.php`
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -15,6 +15,10 @@ function openProjectFolder(projectId, projectName) {
|
||||
projectName = projectName.replace(/"/g, '_');
|
||||
// Заменяем ВСЕ пробелы на подчёркивания
|
||||
projectName = projectName.replace(/\s+/g, '_');
|
||||
// Заменяем множественные подчёркивания на одинарное
|
||||
projectName = projectName.replace(/_+/g, '_');
|
||||
// Убираем подчёркивания в начале и конце
|
||||
projectName = projectName.replace(/^_+|_+$/g, '');
|
||||
}
|
||||
|
||||
// Формируем URL для папки проекта в Nextcloud
|
||||
|
||||
132
fix_all_collation_utf8mb3.php
Normal file
132
fix_all_collation_utf8mb3.php
Normal file
@@ -0,0 +1,132 @@
|
||||
<?php
|
||||
/**
|
||||
* Исправление всех таблиц с utf8mb3_bin на utf8mb4_general_ci
|
||||
*/
|
||||
|
||||
$dbHost = '192.168.128.3';
|
||||
$dbUser = 'nextcloud';
|
||||
$dbPass = 'nextcloud_password';
|
||||
$dbName = 'nextcloud';
|
||||
|
||||
try {
|
||||
$pdo = new PDO(
|
||||
"mysql:host=$dbHost;dbname=$dbName;charset=utf8mb4",
|
||||
$dbUser,
|
||||
$dbPass,
|
||||
[PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION]
|
||||
);
|
||||
|
||||
echo "=== ИСПРАВЛЕНИЕ ВСЕХ ТАБЛИЦ С utf8mb3_bin ===\n\n";
|
||||
|
||||
// Находим все колонки с utf8mb3_bin
|
||||
$query = "
|
||||
SELECT
|
||||
TABLE_NAME,
|
||||
COLUMN_NAME,
|
||||
DATA_TYPE,
|
||||
COLUMN_TYPE,
|
||||
CHARACTER_SET_NAME,
|
||||
COLLATION_NAME
|
||||
FROM
|
||||
INFORMATION_SCHEMA.COLUMNS
|
||||
WHERE
|
||||
TABLE_SCHEMA = ?
|
||||
AND TABLE_NAME LIKE 'oc_%'
|
||||
AND COLLATION_NAME LIKE '%utf8mb3%'
|
||||
ORDER BY TABLE_NAME, COLUMN_NAME
|
||||
";
|
||||
|
||||
$stmt = $pdo->prepare($query);
|
||||
$stmt->execute([$dbName]);
|
||||
$columns = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||
|
||||
if (empty($columns)) {
|
||||
echo "✅ Все колонки уже имеют правильную collation!\n";
|
||||
exit(0);
|
||||
}
|
||||
|
||||
echo "Найдено колонок с utf8mb3: " . count($columns) . "\n\n";
|
||||
|
||||
$fixed = 0;
|
||||
$errors = 0;
|
||||
$tables = [];
|
||||
|
||||
foreach ($columns as $col) {
|
||||
$table = $col['TABLE_NAME'];
|
||||
$column = $col['COLUMN_NAME'];
|
||||
$dataType = $col['DATA_TYPE'];
|
||||
$columnType = $col['COLUMN_TYPE'];
|
||||
$charSet = $col['CHARACTER_SET_NAME'];
|
||||
$collation = $col['COLLATION_NAME'];
|
||||
|
||||
// Группируем по таблицам
|
||||
if (!isset($tables[$table])) {
|
||||
$tables[$table] = [];
|
||||
}
|
||||
$tables[$table][] = $col;
|
||||
}
|
||||
|
||||
// Исправляем каждую таблицу
|
||||
foreach ($tables as $table => $tableColumns) {
|
||||
echo "Таблица: $table\n";
|
||||
|
||||
foreach ($tableColumns as $col) {
|
||||
$column = $col['COLUMN_NAME'];
|
||||
$columnType = $col['COLUMN_TYPE'];
|
||||
|
||||
// Получаем полную информацию о колонке
|
||||
$colInfoQuery = "SHOW FULL COLUMNS FROM `$table` WHERE Field = ?";
|
||||
$colInfoStmt = $pdo->prepare($colInfoQuery);
|
||||
$colInfoStmt->execute([$column]);
|
||||
$colInfo = $colInfoStmt->fetch(PDO::FETCH_ASSOC);
|
||||
|
||||
if (!$colInfo) {
|
||||
echo " ⚠️ Не удалось получить информацию о колонке $column\n";
|
||||
continue;
|
||||
}
|
||||
|
||||
// Строим ALTER TABLE запрос
|
||||
$type = $colInfo['Type'];
|
||||
// Заменяем utf8mb3 на utf8mb4
|
||||
$type = preg_replace('/utf8mb3/i', 'utf8mb4', $type);
|
||||
$type = preg_replace('/utf8(_bin)?/i', 'utf8mb4', $type);
|
||||
// Убираем старую collation и добавляем новую
|
||||
$type = preg_replace('/COLLATE\s+\w+/i', '', $type);
|
||||
$type = preg_replace('/CHARACTER\s+SET\s+\w+/i', '', $type);
|
||||
|
||||
// Добавляем новую collation
|
||||
if (preg_match('/varchar|char|text/i', $type)) {
|
||||
$type .= ' CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci';
|
||||
}
|
||||
|
||||
$null = $colInfo['Null'] === 'YES' ? 'NULL' : 'NOT NULL';
|
||||
$default = '';
|
||||
if ($colInfo['Default'] !== null) {
|
||||
$default = "DEFAULT '" . addslashes($colInfo['Default']) . "'";
|
||||
}
|
||||
$extra = $colInfo['Extra'] ?: '';
|
||||
|
||||
$alterQuery = "ALTER TABLE `$table` MODIFY COLUMN `$column` $type $null $default $extra";
|
||||
|
||||
try {
|
||||
echo " Исправляю: $column ... ";
|
||||
$pdo->exec($alterQuery);
|
||||
echo "✅\n";
|
||||
$fixed++;
|
||||
} catch (PDOException $e) {
|
||||
echo "❌ Ошибка: " . $e->getMessage() . "\n";
|
||||
$errors++;
|
||||
}
|
||||
}
|
||||
echo "\n";
|
||||
}
|
||||
|
||||
echo "\n=== РЕЗУЛЬТАТ ===\n";
|
||||
echo "Исправлено колонок: $fixed\n";
|
||||
echo "Ошибок: $errors\n";
|
||||
|
||||
} catch (PDOException $e) {
|
||||
echo "❌ Ошибка подключения к БД: " . $e->getMessage() . "\n";
|
||||
exit(1);
|
||||
}
|
||||
|
||||
30
fix_indexes_collation.sh
Executable file
30
fix_indexes_collation.sh
Executable file
@@ -0,0 +1,30 @@
|
||||
#!/bin/bash
|
||||
# Исправление индексов с неправильной collation в Nextcloud
|
||||
|
||||
echo "=== ИСПРАВЛЕНИЕ ИНДЕКСОВ С НЕПРАВИЛЬНОЙ COLLATION ==="
|
||||
echo ""
|
||||
|
||||
# Получаем список таблиц с проблемными индексами
|
||||
docker exec nextcloud-db-fresh mariadb -unextcloud -pnextcloud_password nextcloud -e "
|
||||
SELECT DISTINCT TABLE_NAME
|
||||
FROM INFORMATION_SCHEMA.STATISTICS
|
||||
WHERE TABLE_SCHEMA = 'nextcloud'
|
||||
AND TABLE_NAME LIKE 'oc_%'
|
||||
AND COLLATION = 'utf8mb3_general_ci';
|
||||
" 2>&1 | grep -v "Warning" | grep -v "TABLE_NAME" | while read table; do
|
||||
if [ -n "$table" ]; then
|
||||
echo "Проверяю таблицу: $table"
|
||||
# Получаем информацию об индексах
|
||||
docker exec nextcloud-db-fresh mariadb -unextcloud -pnextcloud_password nextcloud -e "SHOW INDEX FROM \`$table\`;" 2>&1 | grep -i "utf8mb3" || echo " ✅ Нет проблемных индексов"
|
||||
fi
|
||||
done
|
||||
|
||||
echo ""
|
||||
echo "=== РЕКОМЕНДАЦИЯ ==="
|
||||
echo "Если проблема сохраняется, попробуйте:"
|
||||
echo "1. Пересоздать индексы через Nextcloud:"
|
||||
echo " docker exec nextcloud-fresh php occ db:add-missing-indices"
|
||||
echo ""
|
||||
echo "2. Или временно отключить синхронизацию в клиенте Nextcloud"
|
||||
echo " и открыть файлы через Web UI для индексации"
|
||||
|
||||
104
fix_nextcloud_collation.php
Normal file
104
fix_nextcloud_collation.php
Normal file
@@ -0,0 +1,104 @@
|
||||
<?php
|
||||
/**
|
||||
* Исправление проблемы с collation в БД Nextcloud
|
||||
* Заменяет utf8mb3_general_ci на utf8mb4_general_ci
|
||||
*/
|
||||
|
||||
error_reporting(E_ALL);
|
||||
ini_set('display_errors', 1);
|
||||
|
||||
echo "=== ИСПРАВЛЕНИЕ COLLATION В БД NEXTCLOUD ===\n";
|
||||
echo str_repeat("=", 80) . "\n\n";
|
||||
|
||||
// Подключение к БД Nextcloud
|
||||
$host = '192.168.128.3';
|
||||
$user = 'nextcloud';
|
||||
$password = 'nextcloud_password';
|
||||
$database = 'nextcloud';
|
||||
|
||||
try {
|
||||
$db = new mysqli($host, $user, $password, $database);
|
||||
|
||||
if ($db->connect_error) {
|
||||
throw new Exception("Ошибка подключения: " . $db->connect_error);
|
||||
}
|
||||
|
||||
$db->set_charset('utf8mb4');
|
||||
|
||||
echo "✅ Подключились к БД Nextcloud\n\n";
|
||||
|
||||
// Находим таблицы с неправильной collation
|
||||
echo "🔍 Поиск таблиц с неправильной collation...\n";
|
||||
$result = $db->query("
|
||||
SELECT TABLE_NAME, TABLE_COLLATION
|
||||
FROM information_schema.TABLES
|
||||
WHERE TABLE_SCHEMA = 'nextcloud'
|
||||
AND TABLE_COLLATION LIKE '%utf8mb3%'
|
||||
");
|
||||
|
||||
$tables = [];
|
||||
while ($row = $result->fetch_assoc()) {
|
||||
$tables[] = $row['TABLE_NAME'];
|
||||
echo " - {$row['TABLE_NAME']}: {$row['TABLE_COLLATION']}\n";
|
||||
}
|
||||
|
||||
if (empty($tables)) {
|
||||
echo "✅ Все таблицы имеют правильную collation\n";
|
||||
exit(0);
|
||||
}
|
||||
|
||||
echo "\n📊 Найдено таблиц для исправления: " . count($tables) . "\n\n";
|
||||
|
||||
// Исправляем collation для каждой таблицы
|
||||
echo "🔧 Исправление collation...\n\n";
|
||||
|
||||
$fixed = 0;
|
||||
$errors = 0;
|
||||
|
||||
foreach ($tables as $table) {
|
||||
echo " Исправление таблицы: {$table}... ";
|
||||
|
||||
// Изменяем collation таблицы
|
||||
$sql = "ALTER TABLE `{$table}` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci";
|
||||
|
||||
if ($db->query($sql)) {
|
||||
echo "✅\n";
|
||||
$fixed++;
|
||||
} else {
|
||||
echo "❌ Ошибка: " . $db->error . "\n";
|
||||
$errors++;
|
||||
}
|
||||
}
|
||||
|
||||
echo "\n";
|
||||
echo str_repeat("=", 80) . "\n";
|
||||
echo "ИТОГИ:\n";
|
||||
echo "Исправлено таблиц: {$fixed}\n";
|
||||
echo "Ошибок: {$errors}\n\n";
|
||||
|
||||
// Проверяем результат
|
||||
echo "🔍 Проверка результата...\n";
|
||||
$result = $db->query("
|
||||
SELECT COUNT(*) as count
|
||||
FROM information_schema.TABLES
|
||||
WHERE TABLE_SCHEMA = 'nextcloud'
|
||||
AND TABLE_COLLATION LIKE '%utf8mb3%'
|
||||
");
|
||||
|
||||
$row = $result->fetch_assoc();
|
||||
if ($row['count'] == 0) {
|
||||
echo "✅ Все таблицы исправлены!\n";
|
||||
} else {
|
||||
echo "⚠️ Осталось таблиц с неправильной collation: {$row['count']}\n";
|
||||
}
|
||||
|
||||
$db->close();
|
||||
|
||||
echo "\n=== ГОТОВО ===\n";
|
||||
|
||||
} catch (Exception $e) {
|
||||
echo "❌ Критическая ошибка: " . $e->getMessage() . "\n";
|
||||
exit(1);
|
||||
}
|
||||
|
||||
|
||||
137
fix_nextcloud_collation_all.php
Normal file
137
fix_nextcloud_collation_all.php
Normal file
@@ -0,0 +1,137 @@
|
||||
<?php
|
||||
/**
|
||||
* Исправление всех колонок с неправильной collation в Nextcloud
|
||||
* Исправляет utf8mb3_general_ci → utf8mb4_general_ci
|
||||
*/
|
||||
|
||||
$dbHost = '192.168.128.3';
|
||||
$dbUser = 'nextcloud';
|
||||
$dbPass = 'nextcloud_password';
|
||||
$dbName = 'nextcloud';
|
||||
|
||||
try {
|
||||
$pdo = new PDO(
|
||||
"mysql:host=$dbHost;dbname=$dbName;charset=utf8mb4",
|
||||
$dbUser,
|
||||
$dbPass,
|
||||
[PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION]
|
||||
);
|
||||
|
||||
echo "=== ИСПРАВЛЕНИЕ COLLATION В NEXTCLOUD ===\n\n";
|
||||
|
||||
// Находим все колонки с неправильной collation
|
||||
$query = "
|
||||
SELECT
|
||||
TABLE_NAME,
|
||||
COLUMN_NAME,
|
||||
DATA_TYPE,
|
||||
CHARACTER_SET_NAME,
|
||||
COLLATION_NAME
|
||||
FROM
|
||||
INFORMATION_SCHEMA.COLUMNS
|
||||
WHERE
|
||||
TABLE_SCHEMA = ?
|
||||
AND TABLE_NAME LIKE 'oc_%'
|
||||
AND COLLATION_NAME = 'utf8mb3_general_ci'
|
||||
ORDER BY TABLE_NAME, COLUMN_NAME
|
||||
";
|
||||
|
||||
$stmt = $pdo->prepare($query);
|
||||
$stmt->execute([$dbName]);
|
||||
$columns = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||
|
||||
if (empty($columns)) {
|
||||
echo "✅ Все колонки уже имеют правильную collation!\n";
|
||||
exit(0);
|
||||
}
|
||||
|
||||
echo "Найдено колонок с неправильной collation: " . count($columns) . "\n\n";
|
||||
|
||||
$fixed = 0;
|
||||
$errors = 0;
|
||||
|
||||
foreach ($columns as $col) {
|
||||
$table = $col['TABLE_NAME'];
|
||||
$column = $col['COLUMN_NAME'];
|
||||
$dataType = $col['DATA_TYPE'];
|
||||
$charSet = $col['CHARACTER_SET_NAME'];
|
||||
|
||||
// Определяем новый тип данных
|
||||
$newCharSet = 'utf8mb4';
|
||||
$newCollation = 'utf8mb4_general_ci';
|
||||
|
||||
// Для TEXT типов нужно указать CHARACTER SET
|
||||
$alterQuery = "ALTER TABLE `$table` MODIFY COLUMN `$column` ";
|
||||
|
||||
if (in_array(strtoupper($dataType), ['VARCHAR', 'CHAR', 'TEXT', 'TINYTEXT', 'MEDIUMTEXT', 'LONGTEXT'])) {
|
||||
// Получаем текущие параметры колонки
|
||||
$colInfoQuery = "SHOW FULL COLUMNS FROM `$table` WHERE Field = ?";
|
||||
$colInfoStmt = $pdo->prepare($colInfoQuery);
|
||||
$colInfoStmt->execute([$column]);
|
||||
$colInfo = $colInfoStmt->fetch(PDO::FETCH_ASSOC);
|
||||
|
||||
if ($colInfo) {
|
||||
$type = $colInfo['Type'];
|
||||
// Заменяем charset в типе
|
||||
$type = preg_replace('/utf8mb3/i', 'utf8mb4', $type);
|
||||
$type = preg_replace('/utf8(_general_ci)?/i', 'utf8mb4', $type);
|
||||
|
||||
$null = $colInfo['Null'] === 'YES' ? 'NULL' : 'NOT NULL';
|
||||
$default = $colInfo['Default'] !== null ? "DEFAULT '{$colInfo['Default']}'" : '';
|
||||
$extra = $colInfo['Extra'] ?: '';
|
||||
|
||||
$alterQuery .= "$type CHARACTER SET $newCharSet COLLATE $newCollation $null $default $extra";
|
||||
} else {
|
||||
echo "⚠️ Не удалось получить информацию о колонке $table.$column\n";
|
||||
continue;
|
||||
}
|
||||
} else {
|
||||
// Для других типов просто меняем collation
|
||||
$alterQuery .= "`$column` $dataType CHARACTER SET $newCharSet COLLATE $newCollation";
|
||||
}
|
||||
|
||||
try {
|
||||
echo "Исправляю: $table.$column ... ";
|
||||
$pdo->exec($alterQuery);
|
||||
echo "✅\n";
|
||||
$fixed++;
|
||||
} catch (PDOException $e) {
|
||||
echo "❌ Ошибка: " . $e->getMessage() . "\n";
|
||||
$errors++;
|
||||
}
|
||||
}
|
||||
|
||||
echo "\n=== РЕЗУЛЬТАТ ===\n";
|
||||
echo "Исправлено: $fixed\n";
|
||||
echo "Ошибок: $errors\n";
|
||||
|
||||
// Проверяем индексы
|
||||
echo "\n=== ПРОВЕРКА ИНДЕКСОВ ===\n";
|
||||
$indexQuery = "
|
||||
SELECT DISTINCT
|
||||
TABLE_NAME,
|
||||
INDEX_NAME
|
||||
FROM
|
||||
INFORMATION_SCHEMA.STATISTICS
|
||||
WHERE
|
||||
TABLE_SCHEMA = ?
|
||||
AND TABLE_NAME LIKE 'oc_%'
|
||||
AND COLLATION = 'utf8mb3_general_ci'
|
||||
";
|
||||
|
||||
$indexStmt = $pdo->prepare($indexQuery);
|
||||
$indexStmt->execute([$dbName]);
|
||||
$indexes = $indexStmt->fetchAll(PDO::FETCH_ASSOC);
|
||||
|
||||
if (!empty($indexes)) {
|
||||
echo "⚠️ Найдено индексов с неправильной collation: " . count($indexes) . "\n";
|
||||
echo "Индексы нужно пересоздать вручную или через Nextcloud\n";
|
||||
} else {
|
||||
echo "✅ Все индексы имеют правильную collation\n";
|
||||
}
|
||||
|
||||
} catch (PDOException $e) {
|
||||
echo "❌ Ошибка подключения к БД: " . $e->getMessage() . "\n";
|
||||
exit(1);
|
||||
}
|
||||
|
||||
38
fix_nextcloud_issues.sh
Executable file
38
fix_nextcloud_issues.sh
Executable file
@@ -0,0 +1,38 @@
|
||||
#!/bin/bash
|
||||
# Исправление проблем Nextcloud из страницы проверки безопасности
|
||||
|
||||
echo "=== ИСПРАВЛЕНИЕ ПРОБЛЕМ NEXTCLOUD ==="
|
||||
echo ""
|
||||
|
||||
# 1. Запуск background jobs вручную
|
||||
echo "1. Запуск background jobs..."
|
||||
docker exec nextcloud-fresh php occ background:cron 2>&1 | head -20
|
||||
|
||||
# 2. Проверка и исправление collation для поддержки 4-байтовых символов
|
||||
echo ""
|
||||
echo "2. Проверка collation для поддержки 4-байтовых символов..."
|
||||
docker exec nextcloud-db-fresh mariadb -unextcloud -pnextcloud_password nextcloud -e "
|
||||
SELECT
|
||||
TABLE_NAME,
|
||||
COUNT(*) as bad_cols
|
||||
FROM
|
||||
INFORMATION_SCHEMA.COLUMNS
|
||||
WHERE
|
||||
TABLE_SCHEMA = 'nextcloud'
|
||||
AND TABLE_NAME LIKE 'oc_%'
|
||||
AND COLLATION_NAME = 'utf8mb3_general_ci'
|
||||
GROUP BY TABLE_NAME
|
||||
ORDER BY bad_cols DESC;
|
||||
" 2>&1 | grep -v "Warning"
|
||||
|
||||
echo ""
|
||||
echo "=== РЕКОМЕНДАЦИИ ==="
|
||||
echo ""
|
||||
echo "Для автоматического запуска background jobs добавьте в crontab:"
|
||||
echo "*/5 * * * * docker exec nextcloud-fresh php occ background:cron"
|
||||
echo ""
|
||||
echo "Или используйте webcron (менее надежно):"
|
||||
echo "docker exec nextcloud-fresh php occ config:app:set core backgroundjobs_mode --value='webcron'"
|
||||
echo ""
|
||||
echo "Для исправления collation запустите скрипт fix_nextcloud_collation_all.php"
|
||||
|
||||
150
fix_nextcloud_settings.php
Executable file
150
fix_nextcloud_settings.php
Executable file
@@ -0,0 +1,150 @@
|
||||
<?php
|
||||
/**
|
||||
* Скрипт для настройки Nextcloud и защиты от удаления файлов
|
||||
*
|
||||
* Выполняет:
|
||||
* 1. Отключает DeleteOrphanedItems
|
||||
* 2. Включает readonly для External Storage
|
||||
* 3. Увеличивает retention корзины до 365 дней
|
||||
*/
|
||||
|
||||
error_reporting(E_ALL);
|
||||
ini_set('display_errors', 1);
|
||||
|
||||
echo "Настройка Nextcloud для защиты от удаления файлов\n";
|
||||
echo str_repeat("=", 80) . "\n\n";
|
||||
|
||||
// Проверяем, доступен ли Docker
|
||||
$dockerAvailable = shell_exec('which docker 2>/dev/null');
|
||||
if (!$dockerAvailable) {
|
||||
die("❌ Docker не найден. Убедитесь, что Docker установлен и доступен.\n");
|
||||
}
|
||||
|
||||
// Имя контейнера Nextcloud
|
||||
$containerName = 'nextcloud-fresh';
|
||||
$user = 'www-data';
|
||||
|
||||
// Проверяем, существует ли контейнер
|
||||
$containerExists = shell_exec("docker ps -a --filter 'name=$containerName' --format '{{.Names}}' 2>/dev/null");
|
||||
if (empty(trim($containerExists))) {
|
||||
echo "⚠️ Контейнер '$containerName' не найден.\n";
|
||||
echo "Попробуем найти контейнер Nextcloud...\n";
|
||||
|
||||
$allContainers = shell_exec("docker ps -a --format '{{.Names}}' 2>/dev/null");
|
||||
echo "Доступные контейнеры:\n";
|
||||
echo $allContainers . "\n";
|
||||
|
||||
echo "\nВведите имя контейнера Nextcloud (или нажмите Enter для пропуска): ";
|
||||
$handle = fopen("php://stdin", "r");
|
||||
$line = fgets($handle);
|
||||
$containerName = trim($line);
|
||||
fclose($handle);
|
||||
|
||||
if (empty($containerName)) {
|
||||
echo "Пропускаем настройку Nextcloud.\n";
|
||||
exit(0);
|
||||
}
|
||||
}
|
||||
|
||||
echo "Используем контейнер: $containerName\n\n";
|
||||
|
||||
$commands = [];
|
||||
$results = [];
|
||||
|
||||
// 1. Отключить DeleteOrphanedItems
|
||||
echo "1. Отключение DeleteOrphanedItems...\n";
|
||||
$jobId = 31; // ID задачи DeleteOrphanedItems
|
||||
$cmd = "docker exec -u $user $containerName php occ background-job:delete $jobId 2>&1";
|
||||
echo " Команда: $cmd\n";
|
||||
$output = shell_exec($cmd);
|
||||
$results['delete_orphaned'] = $output;
|
||||
echo " Результат: " . (empty($output) ? "✅ Команда выполнена" : $output) . "\n\n";
|
||||
|
||||
// Проверяем статус задачи
|
||||
$checkCmd = "docker exec -u $user $containerName php occ background-job:list 2>&1 | grep -i 'DeleteOrphanedItems' || echo 'Задача не найдена (возможно, уже отключена)'";
|
||||
$checkOutput = shell_exec($checkCmd);
|
||||
echo " Проверка: $checkOutput\n\n";
|
||||
|
||||
// 2. Включить readonly для External Storage
|
||||
echo "2. Включение readonly для External Storage...\n";
|
||||
// Сначала найдем ID внешнего хранилища
|
||||
$listCmd = "docker exec -u $user $containerName php occ files_external:list 2>&1";
|
||||
$listOutput = shell_exec($listCmd);
|
||||
echo " Список внешних хранилищ:\n";
|
||||
echo " $listOutput\n";
|
||||
|
||||
// Обычно ID = 1 для первого хранилища, но проверим
|
||||
$storageId = 1;
|
||||
$readonlyCmd = "docker exec -u $user $containerName php occ files_external:option $storageId readonly true 2>&1";
|
||||
echo " Команда: $readonlyCmd\n";
|
||||
$readonlyOutput = shell_exec($readonlyCmd);
|
||||
$results['readonly'] = $readonlyOutput;
|
||||
echo " Результат: " . (empty($readonlyOutput) || strpos($readonlyOutput, 'error') === false ? "✅ Readonly включен" : $readonlyOutput) . "\n\n";
|
||||
|
||||
// Проверяем настройки
|
||||
$verifyCmd = "docker exec -u $user $containerName php occ files_external:list --output json 2>&1";
|
||||
$verifyOutput = shell_exec($verifyCmd);
|
||||
echo " Проверка настроек:\n";
|
||||
echo " $verifyOutput\n\n";
|
||||
|
||||
// 3. Увеличить retention корзины до 365 дней
|
||||
echo "3. Увеличение retention корзины до 365 дней...\n";
|
||||
$retentionCmd = "docker exec -u $user $containerName php occ config:app:set files trashbin_retention_obligation --value=\"auto, 365\" 2>&1";
|
||||
echo " Команда: $retentionCmd\n";
|
||||
$retentionOutput = shell_exec($retentionCmd);
|
||||
$results['retention'] = $retentionOutput;
|
||||
echo " Результат: " . (empty($retentionOutput) || strpos($retentionOutput, 'error') === false ? "✅ Retention установлен на 365 дней" : $retentionOutput) . "\n\n";
|
||||
|
||||
// Проверяем текущее значение
|
||||
$checkRetentionCmd = "docker exec -u $user $containerName php occ config:app:get files trashbin_retention_obligation 2>&1";
|
||||
$checkRetentionOutput = shell_exec($checkRetentionCmd);
|
||||
echo " Текущее значение retention: $checkRetentionOutput\n\n";
|
||||
|
||||
// 4. Настройка регулярной индексации
|
||||
echo "4. Настройка регулярной индексации файлов...\n";
|
||||
echo " Рекомендуется добавить в crontab:\n";
|
||||
echo " 0 */6 * * * docker exec -u $user $containerName php occ files:scan --all\n";
|
||||
echo " (сканирование каждые 6 часов)\n\n";
|
||||
|
||||
// Создаем скрипт для cron
|
||||
$cronScript = <<<'SCRIPT'
|
||||
#!/bin/bash
|
||||
# Скрипт для регулярной индексации файлов Nextcloud
|
||||
# Запускать каждые 6 часов через cron
|
||||
|
||||
CONTAINER_NAME="nextcloud-fresh"
|
||||
USER="www-data"
|
||||
|
||||
# Сканируем все файлы
|
||||
docker exec -u $USER $CONTAINER_NAME php occ files:scan --all >> /var/log/nextcloud_scan.log 2>&1
|
||||
|
||||
# Сканируем только внешнее хранилище (быстрее)
|
||||
# docker exec -u $USER $CONTAINER_NAME php occ files:scan --path="/crm" >> /var/log/nextcloud_scan.log 2>&1
|
||||
|
||||
echo "$(date): Nextcloud files scan completed" >> /var/log/nextcloud_scan.log
|
||||
SCRIPT;
|
||||
|
||||
$cronScriptPath = '/var/www/fastuser/data/www/crm.clientright.ru/nextcloud_scan_files.sh';
|
||||
file_put_contents($cronScriptPath, $cronScript);
|
||||
chmod($cronScriptPath, 0755);
|
||||
echo " ✅ Создан скрипт: $cronScriptPath\n";
|
||||
echo " Для добавления в crontab выполните:\n";
|
||||
echo " crontab -e\n";
|
||||
echo " Добавьте строку: 0 */6 * * * $cronScriptPath\n\n";
|
||||
|
||||
echo str_repeat("=", 80) . "\n";
|
||||
echo "ИТОГОВЫЙ ОТЧЕТ:\n\n";
|
||||
|
||||
echo "✅ Выполнено:\n";
|
||||
echo " 1. DeleteOrphanedItems отключен\n";
|
||||
echo " 2. Readonly включен для External Storage\n";
|
||||
echo " 3. Retention корзины увеличен до 365 дней\n";
|
||||
echo " 4. Создан скрипт для регулярной индексации\n\n";
|
||||
|
||||
echo "⚠️ ВАЖНО:\n";
|
||||
echo " - Добавьте скрипт индексации в crontab\n";
|
||||
echo " - Проверьте логи Nextcloud на наличие ошибок\n";
|
||||
echo " - Регулярно проверяйте статус задач: docker exec -u $user $containerName php occ background-job:list\n\n";
|
||||
|
||||
echo "📝 Логи команд сохранены в переменных \$results\n";
|
||||
|
||||
@@ -115,10 +115,34 @@ try {
|
||||
continue; // Пропускаем пустые сообщения
|
||||
}
|
||||
|
||||
// Формируем timestamp в ISO формате для JavaScript
|
||||
$timestamp = null;
|
||||
if (isset($item['created_at']) && !empty($item['created_at'])) {
|
||||
$createdAt = $item['created_at'];
|
||||
// Если created_at уже в ISO формате (содержит 'T'), используем как есть
|
||||
if (strpos($createdAt, 'T') !== false) {
|
||||
// Уже в ISO формате (например, "2025-11-14T06:21:55.207Z"), используем как есть
|
||||
$timestamp = $createdAt;
|
||||
} else {
|
||||
// Если в другом формате, преобразуем в ISO
|
||||
$parsedTime = strtotime($createdAt);
|
||||
if ($parsedTime !== false) {
|
||||
$timestamp = date('c', $parsedTime); // ISO 8601 формат
|
||||
} else {
|
||||
// Если не удалось распарсить, используем текущее время
|
||||
error_log("Chat History: Failed to parse created_at: {$createdAt}, using current time");
|
||||
$timestamp = date('c');
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Если нет created_at, используем текущее время в ISO формате
|
||||
$timestamp = date('c'); // ISO 8601 формат
|
||||
}
|
||||
|
||||
$message = [
|
||||
'type' => isset($item['sender_type']) && $item['sender_type'] === 'user' ? 'user' : 'assistant',
|
||||
'message' => $item['content'] ?? '',
|
||||
'timestamp' => isset($item['created_at']) ? date('H:i:s', strtotime($item['created_at'])) : date('H:i:s'),
|
||||
'timestamp' => $timestamp,
|
||||
'id' => $item['id'] ?? '',
|
||||
'dialog_id' => $item['dialog_id'] ?? ''
|
||||
];
|
||||
@@ -131,7 +155,7 @@ try {
|
||||
$history[] = [
|
||||
'type' => 'assistant',
|
||||
'message' => "Привет! Я ваш AI ассистент. Работаем с '{$projectName}'. Чем могу помочь?",
|
||||
'timestamp' => date('H:i:s'),
|
||||
'timestamp' => date('c'), // ISO 8601 формат
|
||||
'id' => 'welcome-' . time(),
|
||||
'dialog_id' => 'new-dialog'
|
||||
];
|
||||
|
||||
175
include/Webservices/CreateClientProject.php
Normal file
175
include/Webservices/CreateClientProject.php
Normal file
@@ -0,0 +1,175 @@
|
||||
<?php
|
||||
/*********************************************************************************
|
||||
* API-интерфейс для создания Проекта КлиентПрав из web-формы ticket_form
|
||||
* Уникальность проекта обеспечивается по claim_id (cf_2620)
|
||||
* Автор: GPT-5.1 Codex & Фёдор, 2025-11-15
|
||||
********************************************************************************/
|
||||
|
||||
include_once 'include/Webservices/Query.php';
|
||||
include_once 'modules/Users/Users.php';
|
||||
require_once('include/Webservices/Utils.php');
|
||||
require_once 'include/Webservices/Create.php';
|
||||
require_once 'include/Webservices/Retrieve.php';
|
||||
require_once 'includes/Loader.php';
|
||||
vimport ('includes.runtime.Globals');
|
||||
vimport ('includes.runtime.BaseModel');
|
||||
vimport ('includes.runtime.LanguageHandler');
|
||||
|
||||
/**
|
||||
* Создание проекта КлиентПрав по заявке ticket_form
|
||||
*
|
||||
* @param string $contact_id - ID контакта (обязательное)
|
||||
* @param string $claim_id - Уникальный ID обращения (обязательное, cf_2620)
|
||||
* @param string $session_id - Сессия фронтенда (опционально, cf_2618)
|
||||
* @param string $description - Описание пользователя (опционально, description)
|
||||
* @param string $ai_response - Ответ AI модели (опционально, cf_2622)
|
||||
* @param string $phone - Телефон для генерации имени (опционально)
|
||||
* @param string $firstname - Имя контакта (опционально)
|
||||
* @param string $lastname - Фамилия контакта (опционально)
|
||||
* @return array {project_id, project_name, is_new}
|
||||
*/
|
||||
function vtws_createclientproject($contact_id, $claim_id, $session_id = '', $description = '', $ai_response = '', $phone = '', $firstname = '', $lastname = '', $user = false) {
|
||||
|
||||
ob_start();
|
||||
|
||||
$logPrefix = date("Y-m-d H:i:s") . ' ';
|
||||
file_put_contents('logs/CreateClientProject.log', $logPrefix . json_encode($_REQUEST) . PHP_EOL, FILE_APPEND);
|
||||
|
||||
try {
|
||||
global $adb, $current_user;
|
||||
|
||||
if (empty($claim_id)) {
|
||||
throw new WebServiceException(WebServiceErrorCode::$INVALIDID, "Не указан claim_id");
|
||||
}
|
||||
|
||||
if (empty($contact_id)) {
|
||||
throw new WebServiceException(WebServiceErrorCode::$INVALIDID, "Не указан ID контакта");
|
||||
}
|
||||
|
||||
$claim_id = trim($claim_id);
|
||||
$session_id = trim($session_id);
|
||||
|
||||
// Нормализуем contact_id
|
||||
$contactIdNumeric = preg_replace('/[^0-9]/', '', $contact_id);
|
||||
$contactIdWithPrefix = '12x' . $contactIdNumeric;
|
||||
|
||||
// Подтягиваем данные контакта (фамилию/телефон), если не переданы
|
||||
if (empty($lastname) || empty($firstname) || empty($phone)) {
|
||||
try {
|
||||
$contactRecord = vtws_retrieve($contactIdWithPrefix, $current_user);
|
||||
if (empty($lastname) && !empty($contactRecord['lastname'])) {
|
||||
$lastname = $contactRecord['lastname'];
|
||||
}
|
||||
if (empty($firstname) && !empty($contactRecord['firstname'])) {
|
||||
$firstname = $contactRecord['firstname'];
|
||||
}
|
||||
if (empty($phone) && !empty($contactRecord['phone'])) {
|
||||
$phone = preg_replace('/[^0-9]/', '', $contactRecord['phone']);
|
||||
}
|
||||
} catch (Exception $e) {
|
||||
file_put_contents('logs/CreateClientProject.log', $logPrefix . '⚠️ Не удалось получить контакт: ' . $e->getMessage() . PHP_EOL, FILE_APPEND);
|
||||
}
|
||||
}
|
||||
|
||||
$isNew = false;
|
||||
$output = null;
|
||||
|
||||
file_put_contents('logs/CreateClientProject.log', $logPrefix . "🔎 Ищем проект по claim_id={$claim_id}" . PHP_EOL, FILE_APPEND);
|
||||
|
||||
// Ищем проект по claim_id (cf_2620)
|
||||
$query = "SELECT p.projectid
|
||||
FROM vtiger_project p
|
||||
INNER JOIN vtiger_projectcf pcf ON p.projectid = pcf.projectid
|
||||
INNER JOIN vtiger_crmentity e ON e.crmid = p.projectid
|
||||
WHERE e.deleted = 0
|
||||
AND pcf.cf_2620 = ?
|
||||
LIMIT 1";
|
||||
$result = $adb->pquery($query, array($claim_id));
|
||||
|
||||
if (!$result) {
|
||||
throw new Exception("SQL error while searching project");
|
||||
}
|
||||
|
||||
$projectName = '';
|
||||
|
||||
if ($adb->num_rows($result) > 0) {
|
||||
$output = $adb->query_result($result, 0, 'projectid');
|
||||
$isNew = false;
|
||||
file_put_contents('logs/CreateClientProject.log', $logPrefix . "✅ Проект найден по claim_id {$claim_id}: {$output}" . PHP_EOL, FILE_APPEND);
|
||||
} else {
|
||||
// Генерируем имя проекта
|
||||
$lastname = trim($lastname);
|
||||
if (!empty($lastname)) {
|
||||
$projectName = $lastname . '_КлиентПрав';
|
||||
} elseif (!empty($phone)) {
|
||||
$projectName = $phone . '_КлиентПрав';
|
||||
} else {
|
||||
$projectName = 'КлиентПрав_' . $claim_id;
|
||||
}
|
||||
|
||||
$params = array(
|
||||
'projectname' => $projectName,
|
||||
'projectstatus' => 'Черновик',
|
||||
'projecttype' => 'претензионно-исковая работа',
|
||||
'linktoaccountscontacts' => $contactIdWithPrefix,
|
||||
'cf_1994' => '11x62345', // Заявитель (МОО КлиентПрав)
|
||||
'cf_2620' => $claim_id,
|
||||
'assigned_user_id' => vtws_getWebserviceEntityId('Users', $current_user->id)
|
||||
);
|
||||
|
||||
if (!empty($session_id)) {
|
||||
$params['cf_2618'] = $session_id;
|
||||
}
|
||||
if (!empty($description)) {
|
||||
$params['description'] = $description;
|
||||
}
|
||||
if (!empty($ai_response)) {
|
||||
$params['cf_2622'] = $ai_response;
|
||||
}
|
||||
|
||||
file_put_contents('logs/CreateClientProject.log', $logPrefix . 'Массив для создания: ' . json_encode($params) . PHP_EOL, FILE_APPEND);
|
||||
|
||||
try {
|
||||
$project = vtws_create('Project', $params, $current_user);
|
||||
$output = substr($project['id'], 3);
|
||||
$isNew = true;
|
||||
file_put_contents('logs/CreateClientProject.log', $logPrefix . "✅ Создан новый проект: {$output}" . PHP_EOL, FILE_APPEND);
|
||||
} catch (WebServiceException $ex) {
|
||||
file_put_contents('logs/CreateClientProject.log', $logPrefix . '❌ Ошибка создания: ' . $ex->getMessage() . PHP_EOL, FILE_APPEND);
|
||||
throw $ex;
|
||||
}
|
||||
}
|
||||
|
||||
// Получаем название проекта (если проект был найден, а не создан)
|
||||
if (empty($projectName) && !empty($output)) {
|
||||
try {
|
||||
$query = "SELECT projectname FROM vtiger_project WHERE projectid = ? LIMIT 1";
|
||||
$result = $adb->pquery($query, array($output));
|
||||
if ($adb->num_rows($result) > 0) {
|
||||
$projectName = $adb->query_result($result, 0, 'projectname');
|
||||
file_put_contents('logs/CreateClientProject.log', $logPrefix . "📝 Получено название проекта: {$projectName}" . PHP_EOL, FILE_APPEND);
|
||||
}
|
||||
} catch (Exception $e) {
|
||||
file_put_contents('logs/CreateClientProject.log', $logPrefix . '⚠️ Не удалось получить название проекта: ' . $e->getMessage() . PHP_EOL, FILE_APPEND);
|
||||
}
|
||||
}
|
||||
|
||||
$result = array(
|
||||
'project_id' => $output,
|
||||
'project_name' => $projectName,
|
||||
'is_new' => $isNew
|
||||
);
|
||||
|
||||
file_put_contents('logs/CreateClientProject.log', $logPrefix . 'Return: ' . json_encode($result) . PHP_EOL, FILE_APPEND);
|
||||
ob_end_clean();
|
||||
|
||||
return $result;
|
||||
|
||||
} catch (Exception $ex) {
|
||||
file_put_contents('logs/CreateClientProject.log', $logPrefix . '❌ Exception: ' . $ex->getMessage() . PHP_EOL, FILE_APPEND);
|
||||
ob_end_clean();
|
||||
throw $ex;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -65,6 +65,16 @@ function vtws_createwebclaim($title, $contact_id, $project_id, $event_type, $des
|
||||
|
||||
global $adb, $current_user;
|
||||
|
||||
// Нормализуем ID контакта и проекта (можно передавать как "12x123" или "123")
|
||||
$contactIdNumeric = preg_replace('/[^0-9]/', '', $contact_id);
|
||||
$projectIdNumeric = preg_replace('/[^0-9]/', '', $project_id);
|
||||
|
||||
$contactWsId = '12x' . $contactIdNumeric;
|
||||
$projectWsId = '33x' . $projectIdNumeric;
|
||||
|
||||
$logstring = date('Y-m-d H:i:s').' Нормализовали ID: contact='.$contactIdNumeric.' (raw='.$contact_id.'), project='.$projectIdNumeric.' (raw='.$project_id.')'.PHP_EOL;
|
||||
file_put_contents('logs/CreateWebClaim.log', $logstring, FILE_APPEND);
|
||||
|
||||
// Маппинг типов событий на русские названия для категории
|
||||
$eventTypeMap = array(
|
||||
'delay_flight' => 'Задержка рейса',
|
||||
@@ -108,8 +118,8 @@ function vtws_createwebclaim($title, $contact_id, $project_id, $event_type, $des
|
||||
'parent_id' => '11x67458', // Заявитель - контрагент
|
||||
'ticketcategories' => $ticketCategory,
|
||||
'ticketstatus' => 'рассмотрение',
|
||||
'contact_id' => '12x'.$contact_id,
|
||||
'cf_2066' => '33x'.$project_id, // Связь с проектом
|
||||
'contact_id' => $contactWsId,
|
||||
'cf_2066' => $projectWsId, // Связь с проектом
|
||||
'ticketpriorities' => 'High',
|
||||
'assigned_user_id' => vtws_getWebserviceEntityId('Users', $current_user->id),
|
||||
'description' => $fullDescription
|
||||
@@ -126,6 +136,32 @@ function vtws_createwebclaim($title, $contact_id, $project_id, $event_type, $des
|
||||
$logstring = date('Y-m-d H:i:s').' ✅ Создана Заявка id='.$ticketId.' ticket_no='.$ticketNumber.PHP_EOL;
|
||||
file_put_contents('logs/CreateWebClaim.log', $logstring, FILE_APPEND);
|
||||
|
||||
// 🚧 Создаём двустороннюю связь между Проектом и Заявкой
|
||||
try {
|
||||
$relationCheck = $adb->pquery(
|
||||
"SELECT 1 FROM vtiger_crmentityrel
|
||||
WHERE (crmid = ? AND relcrmid = ?)
|
||||
OR (crmid = ? AND relcrmid = ?)
|
||||
LIMIT 1",
|
||||
array($projectIdNumeric, $ticketId, $ticketId, $projectIdNumeric)
|
||||
);
|
||||
|
||||
if (!$relationCheck || $adb->num_rows($relationCheck) === 0) {
|
||||
$adb->pquery(
|
||||
"INSERT INTO vtiger_crmentityrel (crmid, module, relcrmid, relmodule) VALUES (?, ?, ?, ?)",
|
||||
array($projectIdNumeric, 'Project', $ticketId, 'HelpDesk')
|
||||
);
|
||||
$logstring = date('Y-m-d H:i:s').' 🔗 Добавлена связь Project('.$projectIdNumeric.') ⇄ HelpDesk('.$ticketId.')'.PHP_EOL;
|
||||
file_put_contents('logs/CreateWebClaim.log', $logstring, FILE_APPEND);
|
||||
} else {
|
||||
$logstring = date('Y-m-d H:i:s').' 🔗 Связь Project('.$projectIdNumeric.') ⇄ HelpDesk('.$ticketId.') уже существует'.PHP_EOL;
|
||||
file_put_contents('logs/CreateWebClaim.log', $logstring, FILE_APPEND);
|
||||
}
|
||||
} catch (Exception $relEx) {
|
||||
$logstring = date('Y-m-d H:i:s').' ⚠️ Ошибка связывания Project('.$projectIdNumeric.') ⇄ HelpDesk('.$ticketId.'): '.$relEx->getMessage().PHP_EOL;
|
||||
file_put_contents('logs/CreateWebClaim.log', $logstring, FILE_APPEND);
|
||||
}
|
||||
|
||||
// Возвращаем массив (vTiger сам сделает json_encode)
|
||||
$output = array(
|
||||
'ticket_id' => $ticketId,
|
||||
|
||||
288
include/Webservices/CreateWebClaimV2.php
Normal file
288
include/Webservices/CreateWebClaimV2.php
Normal file
@@ -0,0 +1,288 @@
|
||||
<?php
|
||||
/*********************************************************************************
|
||||
* API-интерфейс для создания Заявки (HelpDesk) из Web-формы (V2 - JSON версия)
|
||||
* Принимает JSON строку с данными заявки
|
||||
* Автор: Фёдор, 2025-12-29
|
||||
********************************************************************************/
|
||||
|
||||
include_once 'include/Webservices/Query.php';
|
||||
include_once 'modules/Users/Users.php';
|
||||
require_once('include/Webservices/Utils.php');
|
||||
require_once 'include/Webservices/Create.php';
|
||||
require_once 'includes/Loader.php';
|
||||
vimport ('includes.runtime.Globals');
|
||||
vimport ('includes.runtime.BaseModel');
|
||||
vimport ('includes.runtime.LanguageHandler');
|
||||
|
||||
/**
|
||||
* Создание заявки из web-формы ERV Platform (V2 - JSON версия)
|
||||
*
|
||||
* @param string $claim_json - JSON строка с данными заявки (обязательно)
|
||||
* @param object $user - пользователь (опционально)
|
||||
* @return array - {"ticket_id": "123", "ticket_number": "TT12345", "title": "...", "category": "...", "status": "..."}
|
||||
*/
|
||||
function vtws_createwebclaimv2($claim_json, $user = false) {
|
||||
|
||||
$logstring = date("Y-m-d H:i:s").' REQUEST: '.json_encode($_REQUEST);
|
||||
file_put_contents('logs/CreateWebClaimV2.log', $logstring.PHP_EOL, FILE_APPEND);
|
||||
|
||||
// Проверка обязательного параметра
|
||||
if(empty($claim_json)){
|
||||
$logstring = date("Y-m-d H:i:s").' Не передан параметр claim_json';
|
||||
file_put_contents('logs/CreateWebClaimV2.log', $logstring.PHP_EOL, FILE_APPEND);
|
||||
throw new WebServiceException(WebServiceErrorCode::$INVALIDID, "Не передан параметр claim_json");
|
||||
}
|
||||
|
||||
// Парсим JSON
|
||||
$claimData = json_decode($claim_json, true);
|
||||
if (json_last_error() !== JSON_ERROR_NONE) {
|
||||
// Пробуем очистить от возможных лишних символов
|
||||
$cleanedJson = trim($claim_json);
|
||||
$cleanedJson = preg_replace('/^[^{]*/', '', $cleanedJson); // Убираем всё до первой {
|
||||
$cleanedJson = preg_replace('/[^}]*$/', '', $cleanedJson); // Убираем всё после последней }
|
||||
$claimData = json_decode($cleanedJson, true);
|
||||
|
||||
if (json_last_error() !== JSON_ERROR_NONE) {
|
||||
$logstring = date("Y-m-d H:i:s").' Ошибка парсинга JSON: '.json_last_error_msg().', JSON: '.substr($claim_json, 0, 200);
|
||||
file_put_contents('logs/CreateWebClaimV2.log', $logstring.PHP_EOL, FILE_APPEND);
|
||||
throw new WebServiceException(WebServiceErrorCode::$INVALIDID, "Ошибка парсинга JSON: ".json_last_error_msg());
|
||||
}
|
||||
}
|
||||
|
||||
$logstring = date("Y-m-d H:i:s").' CLEANED JSON: '.json_encode($claimData);
|
||||
file_put_contents('logs/CreateWebClaimV2.log', $logstring.PHP_EOL, FILE_APPEND);
|
||||
|
||||
// Извлекаем обязательные поля
|
||||
$project_id = isset($claimData['project_id']) ? $claimData['project_id'] : '';
|
||||
$contact_id = isset($claimData['contact_id']) ? $claimData['contact_id'] : '';
|
||||
$event_type = isset($claimData['cf_1726']) ? $claimData['cf_1726'] : '';
|
||||
$description = isset($claimData['description']) ? $claimData['description'] : '';
|
||||
|
||||
// Проверка обязательных полей
|
||||
if(empty($project_id)){
|
||||
$logstring = date("Y-m-d H:i:s").' Не указано обязательное поле: project_id';
|
||||
file_put_contents('logs/CreateWebClaimV2.log', $logstring.PHP_EOL, FILE_APPEND);
|
||||
throw new WebServiceException(WebServiceErrorCode::$INVALIDID, "Не указан ID проекта");
|
||||
}
|
||||
|
||||
if(empty($contact_id)){
|
||||
$logstring = date("Y-m-d H:i:s").' Не указано обязательное поле: contact_id';
|
||||
file_put_contents('logs/CreateWebClaimV2.log', $logstring.PHP_EOL, FILE_APPEND);
|
||||
throw new WebServiceException(WebServiceErrorCode::$INVALIDID, "Не указан ID контакта");
|
||||
}
|
||||
|
||||
if(empty($event_type)){
|
||||
$logstring = date("Y-m-d H:i:s").' Не указано обязательное поле: cf_1726 (event_type)';
|
||||
file_put_contents('logs/CreateWebClaimV2.log', $logstring.PHP_EOL, FILE_APPEND);
|
||||
throw new WebServiceException(WebServiceErrorCode::$INVALIDID, "Не указан тип страхового случая");
|
||||
}
|
||||
|
||||
global $adb, $current_user;
|
||||
|
||||
// Нормализуем ID контакта и проекта
|
||||
$contactIdNumeric = preg_replace('/[^0-9]/', '', $contact_id);
|
||||
$projectIdNumeric = preg_replace('/[^0-9]/', '', $project_id);
|
||||
|
||||
$contactWsId = '12x' . $contactIdNumeric;
|
||||
$projectWsId = '33x' . $projectIdNumeric;
|
||||
|
||||
$logstring = date('Y-m-d H:i:s').' Нормализовали ID: contact='.$contactIdNumeric.' (raw='.$contact_id.'), project='.$projectIdNumeric.' (raw='.$project_id.')'.PHP_EOL;
|
||||
file_put_contents('logs/CreateWebClaimV2.log', $logstring, FILE_APPEND);
|
||||
|
||||
// Маппинг типов событий на русские названия для cf_2650
|
||||
$eventTypeMap = array(
|
||||
'delay_flight' => 'Задержка рейса',
|
||||
'cancel_flight' => 'Отмена рейса',
|
||||
'miss_connection' => 'Пропуск стыковки',
|
||||
'missed_connection' => 'Пропуск стыковки',
|
||||
'delay_train' => 'Задержка поезда',
|
||||
'cancel_train' => 'Отмена поезда',
|
||||
'delay_ferry' => 'Задержка парома',
|
||||
'cancel_ferry' => 'Отмена парома'
|
||||
);
|
||||
|
||||
// ticketcategories всегда "Цифровой адвокат ЕРВ"
|
||||
$ticketCategory = 'Цифровой адвокат ЕРВ';
|
||||
|
||||
// Нормализуем event_type для cf_2650
|
||||
$normalizedEventType = isset($eventTypeMap[$event_type]) ? $eventTypeMap[$event_type] : 'Цифровой адвокат ЕРВ';
|
||||
|
||||
// Извлекаем дополнительные поля
|
||||
$incident_date = isset($claimData['cf_2566']) ? $claimData['cf_2566'] : '';
|
||||
$transport_number = isset($claimData['cf_2568']) ? $claimData['cf_2568'] : '';
|
||||
$cf_1885 = isset($claimData['cf_1885']) ? $claimData['cf_1885'] : '';
|
||||
$lastname = isset($claimData['lastname']) ? $claimData['lastname'] : '';
|
||||
$firstname = isset($claimData['firstname']) ? $claimData['firstname'] : '';
|
||||
|
||||
// Формируем ticket_title: event_type_cf_1885_lastname_firstname
|
||||
$ticket_title = $event_type;
|
||||
if (!empty($cf_1885)) {
|
||||
$ticket_title .= '_' . $cf_1885;
|
||||
}
|
||||
if (!empty($lastname)) {
|
||||
$ticket_title .= '_' . $lastname;
|
||||
}
|
||||
if (!empty($firstname)) {
|
||||
$ticket_title .= '_' . $firstname;
|
||||
}
|
||||
|
||||
// Формируем описание
|
||||
$fullDescription = '';
|
||||
if (!empty($description)) {
|
||||
$fullDescription .= $description . "\n\n";
|
||||
}
|
||||
|
||||
$fullDescription .= "Тип события: " . $normalizedEventType . "\n";
|
||||
|
||||
if (!empty($incident_date)) {
|
||||
$fullDescription .= "Дата инцидента: " . $incident_date . "\n";
|
||||
}
|
||||
if (!empty($transport_number)) {
|
||||
$fullDescription .= "Номер рейса: " . $transport_number . "\n";
|
||||
}
|
||||
|
||||
// Добавляем cf_departure_flight и cf_departure_date, если есть
|
||||
$cf_departure_flight = isset($claimData['cf_departure_flight']) ? $claimData['cf_departure_flight'] : '';
|
||||
$cf_departure_date = isset($claimData['cf_departure_date']) ? $claimData['cf_departure_date'] : '';
|
||||
|
||||
if (!empty($cf_departure_flight)) {
|
||||
$fullDescription .= "Рейс стыковки: " . $cf_departure_flight . "\n";
|
||||
}
|
||||
if (!empty($cf_departure_date)) {
|
||||
$fullDescription .= "Дата стыковки: " . $cf_departure_date . "\n";
|
||||
}
|
||||
|
||||
$fullDescription .= "\nИсточник: ERV Platform Web Form";
|
||||
|
||||
// Формируем массив параметров для создания заявки
|
||||
$params = array(
|
||||
'ticket_title' => $ticket_title,
|
||||
'parent_id' => '11x67458', // Заявитель - контрагент
|
||||
'ticketcategories' => $ticketCategory,
|
||||
'ticketstatus' => 'рассмотрение',
|
||||
'contact_id' => $contactWsId,
|
||||
'cf_2066' => $projectWsId, // Связь с проектом
|
||||
'ticketpriorities' => 'High',
|
||||
'assigned_user_id' => vtws_getWebserviceEntityId('Users', $current_user->id),
|
||||
'description' => $fullDescription,
|
||||
'cf_1726' => $event_type, // Сырой тип события
|
||||
'cf_2650' => $normalizedEventType // Нормализованный тип события
|
||||
);
|
||||
|
||||
// Маппинг дополнительных полей
|
||||
if (!empty($incident_date)) {
|
||||
$params['cf_2566'] = $incident_date;
|
||||
}
|
||||
if (!empty($transport_number)) {
|
||||
$params['cf_2568'] = $transport_number;
|
||||
}
|
||||
if (!empty($cf_departure_flight)) {
|
||||
$params['cf_2630'] = $cf_departure_flight;
|
||||
}
|
||||
if (!empty($cf_departure_date)) {
|
||||
$params['cf_2632'] = $cf_departure_date;
|
||||
}
|
||||
|
||||
// Страна (cf_1909 → cf_2636)
|
||||
if (isset($claimData['cf_1909']) && !empty($claimData['cf_1909'])) {
|
||||
$params['cf_2636'] = $claimData['cf_1909'];
|
||||
}
|
||||
|
||||
// cf_2502 → cf_2572
|
||||
if (isset($claimData['cf_2502']) && !empty($claimData['cf_2502'])) {
|
||||
$params['cf_2572'] = $claimData['cf_2502'];
|
||||
}
|
||||
|
||||
// code → cf_2574
|
||||
if (isset($claimData['code']) && !empty($claimData['code'])) {
|
||||
$params['cf_2574'] = $claimData['code'];
|
||||
}
|
||||
|
||||
// cf_1885 → cf_2642
|
||||
if (!empty($cf_1885)) {
|
||||
$params['cf_2642'] = $cf_1885;
|
||||
}
|
||||
|
||||
// IP → cf_2634
|
||||
if (isset($claimData['ip']) && !empty($claimData['ip'])) {
|
||||
$params['cf_2634'] = $claimData['ip'];
|
||||
}
|
||||
|
||||
// region → cf_2640
|
||||
if (isset($claimData['region']) && !empty($claimData['region'])) {
|
||||
$params['cf_2640'] = $claimData['region'];
|
||||
}
|
||||
|
||||
// source → cf_2638
|
||||
if (isset($claimData['source']) && !empty($claimData['source'])) {
|
||||
$params['cf_2638'] = $claimData['source'];
|
||||
}
|
||||
|
||||
// cf_2508 → cf_2508 (прямое маппирование)
|
||||
if (isset($claimData['cf_2508']) && !empty($claimData['cf_2508'])) {
|
||||
$params['cf_2508'] = $claimData['cf_2508'];
|
||||
}
|
||||
|
||||
// cf_2648 → cf_2648 (прямое маппирование)
|
||||
if (isset($claimData['cf_2648']) && !empty($claimData['cf_2648'])) {
|
||||
$params['cf_2648'] = $claimData['cf_2648'];
|
||||
}
|
||||
|
||||
$logstring = date('Y-m-d H:i:s').' Массив для создания Заявки: '.json_encode($params).PHP_EOL;
|
||||
file_put_contents('logs/CreateWebClaimV2.log', $logstring, FILE_APPEND);
|
||||
|
||||
try {
|
||||
$result = vtws_create('HelpDesk', $params, $current_user);
|
||||
|
||||
$ticketId = substr($result['id'], 3); // Убираем префикс "17x"
|
||||
$ticketNumber = isset($result['ticket_no']) ? $result['ticket_no'] : 'N/A';
|
||||
|
||||
$logstring = date('Y-m-d H:i:s').' ✅ Создана Заявка id='.$ticketId.' ticket_no='.$ticketNumber.PHP_EOL;
|
||||
file_put_contents('logs/CreateWebClaimV2.log', $logstring, FILE_APPEND);
|
||||
|
||||
// Создаём двустороннюю связь между Проектом и Заявкой
|
||||
try {
|
||||
$relationCheck = $adb->pquery(
|
||||
"SELECT 1 FROM vtiger_crmentityrel
|
||||
WHERE (crmid = ? AND relcrmid = ?)
|
||||
OR (crmid = ? AND relcrmid = ?)
|
||||
LIMIT 1",
|
||||
array($projectIdNumeric, $ticketId, $ticketId, $projectIdNumeric)
|
||||
);
|
||||
|
||||
if (!$relationCheck || $adb->num_rows($relationCheck) === 0) {
|
||||
$adb->pquery(
|
||||
"INSERT INTO vtiger_crmentityrel (crmid, module, relcrmid, relmodule) VALUES (?, ?, ?, ?)",
|
||||
array($projectIdNumeric, 'Project', $ticketId, 'HelpDesk')
|
||||
);
|
||||
$logstring = date('Y-m-d H:i:s').' 🔗 Добавлена связь Project('.$projectIdNumeric.') ⇄ HelpDesk('.$ticketId.')'.PHP_EOL;
|
||||
file_put_contents('logs/CreateWebClaimV2.log', $logstring, FILE_APPEND);
|
||||
} else {
|
||||
$logstring = date('Y-m-d H:i:s').' 🔗 Связь Project('.$projectIdNumeric.') ⇄ HelpDesk('.$ticketId.') уже существует'.PHP_EOL;
|
||||
file_put_contents('logs/CreateWebClaimV2.log', $logstring, FILE_APPEND);
|
||||
}
|
||||
} catch (Exception $relEx) {
|
||||
$logstring = date('Y-m-d H:i:s').' ⚠️ Ошибка связывания Project('.$projectIdNumeric.') ⇄ HelpDesk('.$ticketId.'): '.$relEx->getMessage().PHP_EOL;
|
||||
file_put_contents('logs/CreateWebClaimV2.log', $logstring, FILE_APPEND);
|
||||
}
|
||||
|
||||
// Возвращаем массив
|
||||
$output = array(
|
||||
'ticket_id' => $ticketId,
|
||||
'ticket_number' => $ticketNumber,
|
||||
'title' => $ticket_title,
|
||||
'category' => $ticketCategory,
|
||||
'status' => 'рассмотрение'
|
||||
);
|
||||
|
||||
} catch (WebServiceException $ex) {
|
||||
$logstring = date('Y-m-d H:i:s').' ❌ Ошибка создания: '.$ex->getMessage().PHP_EOL;
|
||||
file_put_contents('logs/CreateWebClaimV2.log', $logstring, FILE_APPEND);
|
||||
throw $ex;
|
||||
}
|
||||
|
||||
$logstring = date('Y-m-d H:i:s').' Return: '.json_encode($output).PHP_EOL;
|
||||
file_put_contents('logs/CreateWebClaimV2.log', $logstring, FILE_APPEND);
|
||||
|
||||
return $output;
|
||||
}
|
||||
@@ -22,7 +22,7 @@ vimport ('includes.runtime.LanguageHandler');
|
||||
* @param string $firstname - имя (опционально)
|
||||
* @param string $lastname - фамилия (опционально)
|
||||
* @param string $email - email (опционально)
|
||||
* @return int - ID контакта
|
||||
* @return string - JSON строка с contact_id, is_new и cf_2624 (Данные подтверждены)
|
||||
*/
|
||||
function vtws_createwebcontact($mobile, $firstname = '', $lastname = '', $email = '', $user = false) {
|
||||
|
||||
@@ -56,18 +56,29 @@ function vtws_createwebcontact($mobile, $firstname = '', $lastname = '', $email
|
||||
$isNew = false; // Флаг: создан ли контакт сейчас
|
||||
|
||||
// Проверяем существование контакта по номеру телефона
|
||||
$query = "select c.contactid
|
||||
// ✅ Добавляем выборку поля cf_2624 (Данные подтверждены)
|
||||
$query = "select c.contactid, cf.cf_2624
|
||||
from vtiger_contactdetails c
|
||||
left join vtiger_crmentity e on e.crmid = c.contactid
|
||||
left join vtiger_crmentity e on e.crmid = c.contactid
|
||||
left join vtiger_contactscf cf on cf.contactid = c.contactid
|
||||
where e.deleted = 0 and c.mobile = ?
|
||||
limit 1";
|
||||
$result = $adb->pquery($query, array($mobile));
|
||||
|
||||
$cf_2624_value = "0"; // По умолчанию "Нет" (данные не подтверждены)
|
||||
|
||||
if ($adb->num_rows($result) > 0) {
|
||||
// Контакт существует - ПРОСТО ВОЗВРАЩАЕМ ID (НЕ обновляем!)
|
||||
$output = $adb->query_result($result, 0, 'contactid');
|
||||
$isNew = false;
|
||||
$logstring = date('Y-m-d H:i:s').' ✅ Контакт найден с id '.$output.' (БЕЗ обновления)'.PHP_EOL;
|
||||
|
||||
// ✅ Получаем значение поля cf_2624 (Данные подтверждены)
|
||||
$cf_2624_value = $adb->query_result($result, 0, 'cf_2624');
|
||||
if (empty($cf_2624_value)) {
|
||||
$cf_2624_value = "0"; // По умолчанию "Нет"
|
||||
}
|
||||
|
||||
$logstring = date('Y-m-d H:i:s').' ✅ Контакт найден с id '.$output.', cf_2624='.$cf_2624_value.' (БЕЗ обновления)'.PHP_EOL;
|
||||
file_put_contents('logs/CreateWebContact.log', $logstring, FILE_APPEND);
|
||||
} else {
|
||||
// Контакт НЕ существует - создаём новый
|
||||
@@ -92,6 +103,7 @@ function vtws_createwebcontact($mobile, $firstname = '', $lastname = '', $email
|
||||
'mailingstreet' => '', // Адрес пустой
|
||||
'cf_1849' => '', // Реквизиты пустые
|
||||
'cf_1580' => '', // Код пустой
|
||||
'cf_2624' => '0', // ✅ Данные подтверждены = "Нет" (по умолчанию для новых контактов)
|
||||
'assigned_user_id' => vtws_getWebserviceEntityId('Users', $current_user->id)
|
||||
);
|
||||
|
||||
@@ -102,7 +114,8 @@ function vtws_createwebcontact($mobile, $firstname = '', $lastname = '', $email
|
||||
$contact = vtws_create('Contacts', $params, $current_user);
|
||||
$output = substr($contact['id'], 3);
|
||||
$isNew = true; // Контакт только что создан!
|
||||
$logstring = date('Y-m-d H:i:s').' ✅ Создан новый Web Контакт с id '.$output.PHP_EOL;
|
||||
$cf_2624_value = "0"; // Новый контакт - данные не подтверждены
|
||||
$logstring = date('Y-m-d H:i:s').' ✅ Создан новый Web Контакт с id '.$output.', cf_2624=0'.PHP_EOL;
|
||||
file_put_contents('logs/CreateWebContact.log', $logstring, FILE_APPEND);
|
||||
} catch (WebServiceException $ex) {
|
||||
$logstring = date('Y-m-d H:i:s').' ❌ Ошибка создания: '.$ex->getMessage().PHP_EOL;
|
||||
@@ -111,10 +124,11 @@ function vtws_createwebcontact($mobile, $firstname = '', $lastname = '', $email
|
||||
}
|
||||
}
|
||||
|
||||
// Возвращаем JSON с флагом is_new
|
||||
// Возвращаем JSON с флагом is_new и значением cf_2624
|
||||
$result = array(
|
||||
'contact_id' => $output,
|
||||
'is_new' => $isNew
|
||||
'is_new' => $isNew,
|
||||
'cf_2624' => $cf_2624_value // ✅ "1" = данные подтверждены, "0" = не подтверждены
|
||||
);
|
||||
|
||||
$logstring = date('Y-m-d H:i:s').' Return: '.json_encode($result).PHP_EOL;
|
||||
|
||||
@@ -52,27 +52,28 @@ function vtws_createwebproject($policy_number, $contact_id, $period_start = '',
|
||||
// Валидация: убираем пробелы из номера полиса
|
||||
$policy_number = trim($policy_number);
|
||||
|
||||
$logstring = date('Y-m-d H:i:s').' Ищем проект по policy_number='.$policy_number.' И contact_id='.$contact_id.PHP_EOL;
|
||||
// Нормализуем contact_id: допускаем как "12x12345", так и "12345"
|
||||
$contactIdNumeric = preg_replace('/[^0-9]/', '', $contact_id);
|
||||
$contactIdWithPrefix = '12x' . $contactIdNumeric;
|
||||
|
||||
$logstring = date('Y-m-d H:i:s').' Ищем проект по policy_number='.$policy_number.' И contact_id='.$contactIdNumeric.' (raw='.$contact_id.')'.PHP_EOL;
|
||||
file_put_contents('logs/CreateWebProject.log', $logstring, FILE_APPEND);
|
||||
|
||||
global $adb, $current_user;
|
||||
|
||||
$isNew = false; // Флаг: создан ли проект сейчас
|
||||
|
||||
// Проверяем существование проекта по номеру полиса И привязке к контакту
|
||||
// (т.к. по одному полису может быть несколько застрахованных лиц)
|
||||
// Проверяем существование проекта по номеру полиса И прямой привязке к контакту
|
||||
// (без зависимости от заполнения vtiger_crmentityrel)
|
||||
$query = "SELECT p.projectid
|
||||
FROM vtiger_project p
|
||||
INNER JOIN vtiger_projectcf pcf ON p.projectid = pcf.projectid
|
||||
LEFT JOIN vtiger_crmentity e ON e.crmid = p.projectid
|
||||
LEFT JOIN vtiger_crmentityrel rel ON
|
||||
(rel.crmid = p.projectid AND rel.relcrmid = ?)
|
||||
OR (rel.relcrmid = p.projectid AND rel.crmid = ?)
|
||||
INNER JOIN vtiger_crmentity e ON e.crmid = p.projectid
|
||||
WHERE e.deleted = 0
|
||||
AND pcf.cf_1885 = ?
|
||||
AND rel.crmid IS NOT NULL
|
||||
AND p.linktoaccountscontacts = ?
|
||||
LIMIT 1";
|
||||
$result = $adb->pquery($query, array($contact_id, $contact_id, $policy_number));
|
||||
$result = $adb->pquery($query, array($policy_number, $contactIdNumeric));
|
||||
|
||||
if ($adb->num_rows($result) > 0) {
|
||||
// Проект существует - ПРОСТО ВОЗВРАЩАЕМ ID (НЕ обновляем!)
|
||||
@@ -90,7 +91,7 @@ function vtws_createwebproject($policy_number, $contact_id, $period_start = '',
|
||||
'projectname' => $projectname,
|
||||
'projectstatus' => 'модерация',
|
||||
'projecttype' => 'ерв урегулирование',
|
||||
'linktoaccountscontacts' => '12x'.$contact_id, // Привязка к контакту
|
||||
'linktoaccountscontacts' => $contactIdWithPrefix, // Привязка к контакту
|
||||
'cf_1994' => '11x67458', // Заявитель (контрагент record=67458)
|
||||
'cf_1885' => $policy_number, // Номер полиса
|
||||
'assigned_user_id' => vtws_getWebserviceEntityId('Users', $current_user->id)
|
||||
|
||||
225
include/Webservices/UpsertAccounts.php
Normal file
225
include/Webservices/UpsertAccounts.php
Normal file
@@ -0,0 +1,225 @@
|
||||
<?php
|
||||
/*********************************************************************************
|
||||
* API-интерфейс для создания/поиска нескольких Контрагентов (Upsert Batch)
|
||||
*
|
||||
* Принимает JSON массив offenders, для каждого:
|
||||
* - Ищет по ИНН
|
||||
* - Если найден — возвращает ID (БЕЗ обновления)
|
||||
* - Если не найден — создаёт новый
|
||||
*
|
||||
* Возвращает массив результатов с account_id для каждого offender
|
||||
*
|
||||
* Автор: Фёдор, 2025-12-01
|
||||
********************************************************************************/
|
||||
|
||||
include_once 'include/Webservices/Query.php';
|
||||
include_once 'modules/Users/Users.php';
|
||||
require_once('include/Webservices/Utils.php');
|
||||
require_once 'include/Webservices/Create.php';
|
||||
require_once 'includes/Loader.php';
|
||||
vimport('includes.runtime.Globals');
|
||||
vimport('includes.runtime.BaseModel');
|
||||
vimport('includes.runtime.LanguageHandler');
|
||||
|
||||
/**
|
||||
* Upsert нескольких контрагентов
|
||||
*
|
||||
* @param string $offenders_json - JSON массив offenders:
|
||||
* [
|
||||
* {
|
||||
* "accountname": "ООО Рога и Копыта",
|
||||
* "address": "Москва, ул. Ленина 1",
|
||||
* "email": "info@example.com",
|
||||
* "website": "example.com",
|
||||
* "phone": "+7 999 123-45-67",
|
||||
* "inn": "7712345678",
|
||||
* "ogrn": "1234567890123",
|
||||
* "role": "Турагент" // опционально, для информации
|
||||
* },
|
||||
* ...
|
||||
* ]
|
||||
* @param mixed $user - пользователь CRM
|
||||
* @return string JSON с результатами
|
||||
*/
|
||||
function vtws_upsertaccounts($offenders_json, $user = false) {
|
||||
$logFile = 'logs/UpsertAccounts.log';
|
||||
$logstring = date("Y-m-d H:i:s") . ' REQUEST: ' . substr($offenders_json, 0, 2000);
|
||||
file_put_contents($logFile, $logstring . PHP_EOL, FILE_APPEND);
|
||||
|
||||
global $adb, $current_user;
|
||||
|
||||
// Очистка JSON от мусора (лишние кавычки, BOM, пробелы)
|
||||
$offenders_json = trim($offenders_json);
|
||||
$offenders_json = preg_replace('/^\xEF\xBB\xBF/', '', $offenders_json); // Убираем BOM
|
||||
|
||||
// Если строка обёрнута в кавычки — убираем
|
||||
if (preg_match('/^".*"$/s', $offenders_json)) {
|
||||
$offenders_json = substr($offenders_json, 1, -1);
|
||||
$offenders_json = stripcslashes($offenders_json); // Убираем экранирование
|
||||
}
|
||||
|
||||
// Убираем лишнюю кавычку в конце (баг n8n)
|
||||
$offenders_json = preg_replace('/"\s*$/', '', rtrim($offenders_json, '"'));
|
||||
if (substr($offenders_json, -1) !== ']' && substr($offenders_json, -1) !== '}') {
|
||||
// Пробуем найти конец массива/объекта
|
||||
if (($pos = strrpos($offenders_json, ']')) !== false) {
|
||||
$offenders_json = substr($offenders_json, 0, $pos + 1);
|
||||
}
|
||||
}
|
||||
|
||||
$logstring = date("Y-m-d H:i:s") . ' CLEANED JSON: ' . substr($offenders_json, 0, 500);
|
||||
file_put_contents($logFile, $logstring . PHP_EOL, FILE_APPEND);
|
||||
|
||||
// Парсим JSON
|
||||
$offenders = json_decode($offenders_json, true);
|
||||
|
||||
if (json_last_error() !== JSON_ERROR_NONE) {
|
||||
$error = 'Ошибка парсинга JSON: ' . json_last_error_msg();
|
||||
file_put_contents($logFile, date("Y-m-d H:i:s") . ' ❌ ' . $error . PHP_EOL, FILE_APPEND);
|
||||
file_put_contents($logFile, date("Y-m-d H:i:s") . ' RAW: ' . $offenders_json . PHP_EOL, FILE_APPEND);
|
||||
throw new WebServiceException(WebServiceErrorCode::$INVALIDID, $error);
|
||||
}
|
||||
|
||||
if (!is_array($offenders)) {
|
||||
$offenders = [$offenders]; // Если передан один объект — оборачиваем в массив
|
||||
}
|
||||
|
||||
$logstring = date('Y-m-d H:i:s') . ' Получено offenders: ' . count($offenders);
|
||||
file_put_contents($logFile, $logstring . PHP_EOL, FILE_APPEND);
|
||||
|
||||
// Результаты
|
||||
$results = array(
|
||||
'success' => true,
|
||||
'total' => count($offenders),
|
||||
'created' => 0,
|
||||
'found' => 0,
|
||||
'errors' => 0,
|
||||
'accounts' => array()
|
||||
);
|
||||
|
||||
// Обрабатываем каждого offender
|
||||
foreach ($offenders as $index => $offender) {
|
||||
$accountResult = array(
|
||||
'index' => $index,
|
||||
'success' => false,
|
||||
'account_id' => null,
|
||||
'action' => null,
|
||||
'accountname' => $offender['accountname'] ?? '',
|
||||
'inn' => $offender['inn'] ?? '',
|
||||
'role' => $offender['role'] ?? null,
|
||||
'message' => ''
|
||||
);
|
||||
|
||||
try {
|
||||
// Извлекаем данные
|
||||
$accountname = trim($offender['accountname'] ?? '');
|
||||
$address = trim($offender['address'] ?? '');
|
||||
$email = trim($offender['email'] ?? '');
|
||||
$website = trim($offender['website'] ?? '');
|
||||
$phone = trim($offender['phone'] ?? '');
|
||||
$inn = preg_replace('/[^0-9]/', '', $offender['inn'] ?? ''); // Только цифры
|
||||
$ogrn = preg_replace('/[^0-9]/', '', $offender['ogrn'] ?? ''); // Только цифры
|
||||
$role = trim($offender['role'] ?? '');
|
||||
|
||||
// Проверка обязательных полей
|
||||
if (empty($accountname)) {
|
||||
throw new Exception('Не указано наименование контрагента (accountname)');
|
||||
}
|
||||
if (empty($inn)) {
|
||||
throw new Exception('Не указан ИНН');
|
||||
}
|
||||
|
||||
// Валидация ИНН (10 или 12 цифр)
|
||||
if (strlen($inn) != 10 && strlen($inn) != 12) {
|
||||
$logstring = date('Y-m-d H:i:s') . " ⚠️ Нестандартный ИНН: $inn (длина " . strlen($inn) . ')';
|
||||
file_put_contents($logFile, $logstring . PHP_EOL, FILE_APPEND);
|
||||
// Не падаем, просто логируем
|
||||
}
|
||||
|
||||
// ========================================
|
||||
// ПОИСК ПО ИНН
|
||||
// ========================================
|
||||
$query = "SELECT a.accountid, a.accountname
|
||||
FROM vtiger_account a
|
||||
LEFT JOIN vtiger_crmentity e ON e.crmid = a.accountid
|
||||
WHERE e.deleted = 0 AND a.inn = ?
|
||||
LIMIT 1";
|
||||
$res = $adb->pquery($query, array($inn));
|
||||
|
||||
if ($adb->num_rows($res) > 0) {
|
||||
// === НАЙДЕН — просто возвращаем ID ===
|
||||
$existingId = $adb->query_result($res, 0, 'accountid');
|
||||
$existingName = $adb->query_result($res, 0, 'accountname');
|
||||
|
||||
$accountResult['success'] = true;
|
||||
$accountResult['account_id'] = $existingId;
|
||||
$accountResult['action'] = 'found';
|
||||
$accountResult['message'] = 'Контрагент найден по ИНН';
|
||||
$accountResult['existing_name'] = $existingName;
|
||||
|
||||
$results['found']++;
|
||||
|
||||
$logstring = date('Y-m-d H:i:s') . " ✓ [$index] Найден: $existingId ($existingName) по ИНН $inn";
|
||||
file_put_contents($logFile, $logstring . PHP_EOL, FILE_APPEND);
|
||||
|
||||
} else {
|
||||
// === НЕ НАЙДЕН — создаём ===
|
||||
$params = array(
|
||||
'accountname' => $accountname,
|
||||
'bill_street' => $address,
|
||||
'email1' => $email,
|
||||
'website' => $website,
|
||||
'phone' => $phone,
|
||||
'inn' => $inn,
|
||||
'cf_1951' => $ogrn, // ОГРН в кастомном поле
|
||||
'assigned_user_id' => vtws_getWebserviceEntityId('Users', $current_user->id)
|
||||
);
|
||||
|
||||
$logstring = date('Y-m-d H:i:s') . " 🆕 [$index] Создаём: " . json_encode($params, JSON_UNESCAPED_UNICODE);
|
||||
file_put_contents($logFile, $logstring . PHP_EOL, FILE_APPEND);
|
||||
|
||||
$account = vtws_create('Accounts', $params, $current_user);
|
||||
$newAccountId = substr($account['id'], 3); // Убираем 11x
|
||||
|
||||
$accountResult['success'] = true;
|
||||
$accountResult['account_id'] = $newAccountId;
|
||||
$accountResult['action'] = 'created';
|
||||
$accountResult['message'] = 'Контрагент создан';
|
||||
|
||||
$results['created']++;
|
||||
|
||||
$logstring = date('Y-m-d H:i:s') . " ✅ [$index] Создан: $newAccountId";
|
||||
file_put_contents($logFile, $logstring . PHP_EOL, FILE_APPEND);
|
||||
}
|
||||
|
||||
} catch (WebServiceException $ex) {
|
||||
$accountResult['success'] = false;
|
||||
$accountResult['message'] = $ex->getMessage();
|
||||
$results['errors']++;
|
||||
|
||||
$logstring = date('Y-m-d H:i:s') . " ❌ [$index] WebService ошибка: " . $ex->getMessage();
|
||||
file_put_contents($logFile, $logstring . PHP_EOL, FILE_APPEND);
|
||||
|
||||
} catch (Exception $ex) {
|
||||
$accountResult['success'] = false;
|
||||
$accountResult['message'] = $ex->getMessage();
|
||||
$results['errors']++;
|
||||
|
||||
$logstring = date('Y-m-d H:i:s') . " ❌ [$index] Ошибка: " . $ex->getMessage();
|
||||
file_put_contents($logFile, $logstring . PHP_EOL, FILE_APPEND);
|
||||
}
|
||||
|
||||
$results['accounts'][] = $accountResult;
|
||||
}
|
||||
|
||||
// Итоговый статус
|
||||
$results['success'] = ($results['errors'] == 0);
|
||||
|
||||
$logstring = date('Y-m-d H:i:s') . ' RESULT: total=' . $results['total']
|
||||
. ', created=' . $results['created']
|
||||
. ', found=' . $results['found']
|
||||
. ', errors=' . $results['errors'] . PHP_EOL;
|
||||
file_put_contents($logFile, $logstring, FILE_APPEND);
|
||||
|
||||
return json_encode($results, JSON_UNESCAPED_UNICODE);
|
||||
}
|
||||
235
include/Webservices/UpsertContact.php
Normal file
235
include/Webservices/UpsertContact.php
Normal file
@@ -0,0 +1,235 @@
|
||||
<?php
|
||||
/*********************************************************************************
|
||||
* API-интерфейс для создания/обновления Контакта (Upsert)
|
||||
* Гибкий метод: обновляет если найден, создаёт если нет
|
||||
*
|
||||
* Приоритет поиска:
|
||||
* 1. contact_id (если передан - сразу обновляем)
|
||||
* 2. mobile (ищем по мобильному)
|
||||
* 3. tgid (ищем по полю phone, где хранится telegram_id)
|
||||
*
|
||||
* Все поля опциональны, кроме хотя бы одного идентификатора
|
||||
*
|
||||
* Автор: Фёдор, 2025-12-01
|
||||
********************************************************************************/
|
||||
|
||||
include_once 'include/Webservices/Query.php';
|
||||
include_once 'modules/Users/Users.php';
|
||||
require_once('include/Webservices/Utils.php');
|
||||
require_once 'include/Webservices/Create.php';
|
||||
require_once 'include/Webservices/Revise.php';
|
||||
require_once 'includes/Loader.php';
|
||||
vimport('includes.runtime.Globals');
|
||||
vimport('includes.runtime.BaseModel');
|
||||
vimport('includes.runtime.LanguageHandler');
|
||||
|
||||
/**
|
||||
* Upsert контакта - создание или обновление
|
||||
*
|
||||
* @param string $contact_id - ID контакта в CRM (если известен)
|
||||
* @param string $mobile - мобильный телефон
|
||||
* @param string $tgid - telegram ID
|
||||
* @param string $firstname - имя
|
||||
* @param string $secondname - отчество
|
||||
* @param string $lastname - фамилия
|
||||
* @param string $email - email
|
||||
* @param string $birthday - дата рождения
|
||||
* @param string $birthplace - место рождения
|
||||
* @param string $mailingstreet - адрес
|
||||
* @param string $inn - ИНН
|
||||
* @param string $requisites - реквизиты
|
||||
* @param string $code - SMS код верификации
|
||||
* @param mixed $user - пользователь CRM
|
||||
* @return string JSON с результатом
|
||||
*/
|
||||
function vtws_upsertcontact(
|
||||
$contact_id = '',
|
||||
$mobile = '',
|
||||
$tgid = '',
|
||||
$firstname = '',
|
||||
$secondname = '',
|
||||
$lastname = '',
|
||||
$email = '',
|
||||
$birthday = '',
|
||||
$birthplace = '',
|
||||
$mailingstreet = '',
|
||||
$inn = '',
|
||||
$requisites = '',
|
||||
$code = '',
|
||||
$user = false
|
||||
) {
|
||||
$logFile = 'logs/UpsertContact.log';
|
||||
$logstring = date("Y-m-d H:i:s") . ' REQUEST: ' . json_encode($_REQUEST);
|
||||
file_put_contents($logFile, $logstring . PHP_EOL, FILE_APPEND);
|
||||
|
||||
global $adb, $current_user;
|
||||
|
||||
// Результат
|
||||
$result = array(
|
||||
'success' => false,
|
||||
'contact_id' => null,
|
||||
'action' => null, // 'created', 'updated', 'found'
|
||||
'message' => ''
|
||||
);
|
||||
|
||||
// ========================================
|
||||
// 1. ФОРМАТИРОВАНИЕ ТЕЛЕФОНА
|
||||
// ========================================
|
||||
if (!empty($mobile)) {
|
||||
$mobile = preg_replace('/[^0-9]/', '', $mobile);
|
||||
if (strlen($mobile) == 11 && $mobile[0] == '8') {
|
||||
$mobile = "7" . substr($mobile, 1);
|
||||
} else if (strlen($mobile) == 10) {
|
||||
$mobile = "7" . $mobile;
|
||||
} else if (strlen($mobile) != 11) {
|
||||
// Некорректный номер - логируем, но не падаем
|
||||
$logstring = date("Y-m-d H:i:s") . ' ⚠️ Некорректный номер телефона: ' . $mobile . ' (игнорируем)';
|
||||
file_put_contents($logFile, $logstring . PHP_EOL, FILE_APPEND);
|
||||
$mobile = ''; // Обнуляем некорректный номер
|
||||
}
|
||||
}
|
||||
|
||||
// ========================================
|
||||
// 2. ПОИСК СУЩЕСТВУЮЩЕГО КОНТАКТА
|
||||
// ========================================
|
||||
$existingContactId = null;
|
||||
$searchMethod = '';
|
||||
|
||||
// 2.1 По contact_id (приоритет 1)
|
||||
if (!empty($contact_id)) {
|
||||
$contact_id = preg_replace('/[^0-9]/', '', $contact_id); // Очищаем от 12x префикса
|
||||
$query = "SELECT c.contactid FROM vtiger_contactdetails c
|
||||
LEFT JOIN vtiger_crmentity e ON e.crmid = c.contactid
|
||||
WHERE e.deleted = 0 AND c.contactid = ? LIMIT 1";
|
||||
$res = $adb->pquery($query, array($contact_id));
|
||||
if ($adb->num_rows($res) > 0) {
|
||||
$existingContactId = $adb->query_result($res, 0, 'contactid');
|
||||
$searchMethod = 'by_contact_id';
|
||||
}
|
||||
}
|
||||
|
||||
// 2.2 По mobile (приоритет 2)
|
||||
if (empty($existingContactId) && !empty($mobile)) {
|
||||
$query = "SELECT c.contactid FROM vtiger_contactdetails c
|
||||
LEFT JOIN vtiger_crmentity e ON e.crmid = c.contactid
|
||||
WHERE e.deleted = 0 AND c.mobile = ? LIMIT 1";
|
||||
$res = $adb->pquery($query, array($mobile));
|
||||
if ($adb->num_rows($res) > 0) {
|
||||
$existingContactId = $adb->query_result($res, 0, 'contactid');
|
||||
$searchMethod = 'by_mobile';
|
||||
}
|
||||
}
|
||||
|
||||
// 2.3 По tgid (приоритет 3) - tgid хранится в поле phone
|
||||
if (empty($existingContactId) && !empty($tgid)) {
|
||||
$query = "SELECT c.contactid FROM vtiger_contactdetails c
|
||||
LEFT JOIN vtiger_crmentity e ON e.crmid = c.contactid
|
||||
WHERE e.deleted = 0 AND c.phone = ? LIMIT 1";
|
||||
$res = $adb->pquery($query, array($tgid));
|
||||
if ($adb->num_rows($res) > 0) {
|
||||
$existingContactId = $adb->query_result($res, 0, 'contactid');
|
||||
$searchMethod = 'by_tgid';
|
||||
}
|
||||
}
|
||||
|
||||
$logstring = date('Y-m-d H:i:s') . ' Поиск: contact_id=' . $contact_id . ', mobile=' . $mobile . ', tgid=' . $tgid;
|
||||
$logstring .= ' → Найден: ' . ($existingContactId ? $existingContactId . ' (' . $searchMethod . ')' : 'НЕТ');
|
||||
file_put_contents($logFile, $logstring . PHP_EOL, FILE_APPEND);
|
||||
|
||||
// ========================================
|
||||
// 3. ФОРМИРУЕМ ПАРАМЕТРЫ
|
||||
// ========================================
|
||||
$params = array();
|
||||
|
||||
// Только непустые поля добавляем в params
|
||||
if (!empty($firstname)) $params['firstname'] = $firstname;
|
||||
if (!empty($secondname)) $params['cf_1157'] = $secondname; // Отчество
|
||||
if (!empty($lastname)) $params['lastname'] = $lastname;
|
||||
if (!empty($mobile)) $params['mobile'] = $mobile;
|
||||
if (!empty($email)) $params['email'] = $email;
|
||||
if (!empty($tgid)) $params['phone'] = $tgid; // TG ID в поле phone
|
||||
if (!empty($birthday)) $params['birthday'] = $birthday;
|
||||
if (!empty($birthplace)) $params['cf_1263'] = $birthplace; // Место рождения
|
||||
if (!empty($mailingstreet)) $params['mailingstreet'] = $mailingstreet;
|
||||
if (!empty($inn)) $params['cf_1257'] = $inn; // ИНН
|
||||
if (!empty($requisites)) $params['cf_1849'] = $requisites; // Реквизиты
|
||||
if (!empty($code)) $params['cf_1580'] = $code; // SMS код
|
||||
|
||||
// ========================================
|
||||
// 4. СОЗДАНИЕ ИЛИ ОБНОВЛЕНИЕ
|
||||
// ========================================
|
||||
try {
|
||||
if (!empty($existingContactId)) {
|
||||
// === ОБНОВЛЕНИЕ ===
|
||||
$params['id'] = '12x' . $existingContactId;
|
||||
|
||||
$logstring = date('Y-m-d H:i:s') . ' 📝 Обновляем контакт ' . $existingContactId . ': ' . json_encode($params);
|
||||
file_put_contents($logFile, $logstring . PHP_EOL, FILE_APPEND);
|
||||
|
||||
$contact = vtws_revise($params, $current_user);
|
||||
|
||||
$result['success'] = true;
|
||||
$result['contact_id'] = $existingContactId;
|
||||
$result['action'] = 'updated';
|
||||
$result['search_method'] = $searchMethod;
|
||||
$result['message'] = 'Контакт обновлён';
|
||||
|
||||
$logstring = date('Y-m-d H:i:s') . ' ✅ Контакт ' . $existingContactId . ' обновлён';
|
||||
file_put_contents($logFile, $logstring . PHP_EOL, FILE_APPEND);
|
||||
|
||||
} else {
|
||||
// === СОЗДАНИЕ ===
|
||||
|
||||
// Проверяем минимальные данные для создания
|
||||
if (empty($mobile) && empty($tgid)) {
|
||||
throw new WebServiceException(
|
||||
WebServiceErrorCode::$INVALIDID,
|
||||
"Для создания контакта нужен хотя бы mobile или tgid"
|
||||
);
|
||||
}
|
||||
|
||||
// Дефолтные значения для обязательных полей CRM
|
||||
if (empty($params['firstname'])) {
|
||||
$params['firstname'] = 'Клиент';
|
||||
}
|
||||
if (empty($params['lastname'])) {
|
||||
$suffix = !empty($mobile) ? substr($mobile, -4) : substr($tgid, -4);
|
||||
$params['lastname'] = 'Web_' . $suffix;
|
||||
}
|
||||
if (empty($params['birthday'])) {
|
||||
$params['birthday'] = '01-01-1990';
|
||||
}
|
||||
|
||||
// Назначаем ответственного
|
||||
$params['assigned_user_id'] = vtws_getWebserviceEntityId('Users', $current_user->id);
|
||||
|
||||
$logstring = date('Y-m-d H:i:s') . ' 🆕 Создаём контакт: ' . json_encode($params);
|
||||
file_put_contents($logFile, $logstring . PHP_EOL, FILE_APPEND);
|
||||
|
||||
$contact = vtws_create('Contacts', $params, $current_user);
|
||||
$newContactId = substr($contact['id'], 3); // Убираем 12x
|
||||
|
||||
$result['success'] = true;
|
||||
$result['contact_id'] = $newContactId;
|
||||
$result['action'] = 'created';
|
||||
$result['message'] = 'Контакт создан';
|
||||
|
||||
$logstring = date('Y-m-d H:i:s') . ' ✅ Создан контакт ' . $newContactId;
|
||||
file_put_contents($logFile, $logstring . PHP_EOL, FILE_APPEND);
|
||||
}
|
||||
|
||||
} catch (WebServiceException $ex) {
|
||||
$result['success'] = false;
|
||||
$result['message'] = $ex->getMessage();
|
||||
|
||||
$logstring = date('Y-m-d H:i:s') . ' ❌ Ошибка: ' . $ex->getMessage();
|
||||
file_put_contents($logFile, $logstring . PHP_EOL, FILE_APPEND);
|
||||
|
||||
throw $ex;
|
||||
}
|
||||
|
||||
$logstring = date('Y-m-d H:i:s') . ' RESULT: ' . json_encode($result, JSON_UNESCAPED_UNICODE) . PHP_EOL;
|
||||
file_put_contents($logFile, $logstring, FILE_APPEND);
|
||||
|
||||
return json_encode($result, JSON_UNESCAPED_UNICODE);
|
||||
}
|
||||
297
include/Webservices/UpsertProject.php
Normal file
297
include/Webservices/UpsertProject.php
Normal file
@@ -0,0 +1,297 @@
|
||||
<?php
|
||||
/*********************************************************************************
|
||||
* API-интерфейс для создания/обновления Проекта (Upsert)
|
||||
*
|
||||
* Логика:
|
||||
* - Если передан project_id → обновляем существующий проект
|
||||
* - Если project_id не передан → создаём новый
|
||||
*
|
||||
* Принимает JSON с данными проекта
|
||||
*
|
||||
* Автор: Фёдор, 2025-12-01
|
||||
********************************************************************************/
|
||||
|
||||
include_once 'include/Webservices/Query.php';
|
||||
include_once 'modules/Users/Users.php';
|
||||
require_once('include/Webservices/Utils.php');
|
||||
require_once 'include/Webservices/Create.php';
|
||||
require_once 'include/Webservices/Revise.php';
|
||||
require_once 'includes/Loader.php';
|
||||
vimport('includes.runtime.Globals');
|
||||
vimport('includes.runtime.BaseModel');
|
||||
vimport('includes.runtime.LanguageHandler');
|
||||
|
||||
/**
|
||||
* Upsert проекта
|
||||
*
|
||||
* @param string $project_json - JSON с данными проекта:
|
||||
* {
|
||||
* "project_id": "12345", // Опционально - если есть, обновляем
|
||||
* "claim_id": "uuid", // ID заявки из PostgreSQL
|
||||
* "contact_id": "320096", // ID контакта (обязательно для создания)
|
||||
* "result": "JSON string", // Результат UpsertAccounts (парсится автоматически)
|
||||
* "offender_ids": ["390680"], // Альтернатива result - массив ID контрагентов
|
||||
* "projectdata": { // Данные проекта (cf_* поля)
|
||||
* "cf_2206": "SMS код",
|
||||
* "cf_1830": "категория",
|
||||
* ...
|
||||
* }
|
||||
* }
|
||||
*
|
||||
* Контрагенты распределяются:
|
||||
* - accounts[0] → cf_2274 (основной ответчик)
|
||||
* - accounts[1] → cf_2276 (агент/второй ответчик)
|
||||
*
|
||||
* @param mixed $user - пользователь CRM
|
||||
* @return string JSON с результатом
|
||||
*/
|
||||
function vtws_upsertproject($project_json, $user = false) {
|
||||
$logFile = 'logs/UpsertProject.log';
|
||||
$logstring = date("Y-m-d H:i:s") . ' REQUEST: ' . substr($project_json, 0, 2000);
|
||||
file_put_contents($logFile, $logstring . PHP_EOL, FILE_APPEND);
|
||||
|
||||
global $adb, $current_user;
|
||||
|
||||
// Очистка JSON
|
||||
$project_json = trim($project_json);
|
||||
$project_json = preg_replace('/^\xEF\xBB\xBF/', '', $project_json);
|
||||
if (preg_match('/^".*"$/s', $project_json)) {
|
||||
$project_json = substr($project_json, 1, -1);
|
||||
$project_json = stripcslashes($project_json);
|
||||
}
|
||||
|
||||
// Парсим JSON
|
||||
$data = json_decode($project_json, true);
|
||||
|
||||
if (json_last_error() !== JSON_ERROR_NONE) {
|
||||
$error = 'Ошибка парсинга JSON: ' . json_last_error_msg();
|
||||
file_put_contents($logFile, date("Y-m-d H:i:s") . ' ❌ ' . $error . PHP_EOL, FILE_APPEND);
|
||||
throw new WebServiceException(WebServiceErrorCode::$INVALIDID, $error);
|
||||
}
|
||||
|
||||
// Результат
|
||||
$result = array(
|
||||
'success' => false,
|
||||
'project_id' => null,
|
||||
'claim_id' => null,
|
||||
'action' => null,
|
||||
'offender_id' => null,
|
||||
'agent_id' => null,
|
||||
'message' => ''
|
||||
);
|
||||
|
||||
// Извлекаем данные
|
||||
$project_id = trim($data['project_id'] ?? '');
|
||||
$claim_id = trim($data['claim_id'] ?? '');
|
||||
$contact_id = trim($data['contact_id'] ?? '');
|
||||
$projectdata = $data['projectdata'] ?? [];
|
||||
|
||||
// Извлекаем контрагентов из result (если передан) или из offender_ids
|
||||
$offender_ids = [];
|
||||
|
||||
if (!empty($data['result'])) {
|
||||
// Парсим result от UpsertAccounts
|
||||
$accountsResult = $data['result'];
|
||||
if (is_string($accountsResult)) {
|
||||
$accountsResult = json_decode($accountsResult, true);
|
||||
}
|
||||
|
||||
// Извлекаем account_id из accounts[]
|
||||
if (isset($accountsResult['accounts']) && is_array($accountsResult['accounts'])) {
|
||||
foreach ($accountsResult['accounts'] as $account) {
|
||||
if (!empty($account['account_id'])) {
|
||||
$offender_ids[] = $account['account_id'];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$logstring = date('Y-m-d H:i:s') . ' Извлечены offender_ids из result: ' . json_encode($offender_ids);
|
||||
file_put_contents($logFile, $logstring . PHP_EOL, FILE_APPEND);
|
||||
} elseif (!empty($data['offender_ids'])) {
|
||||
$offender_ids = $data['offender_ids'];
|
||||
}
|
||||
|
||||
// cf_2274 = первый контрагент (основной ответчик)
|
||||
// cf_2276 = второй контрагент (агент/второй ответчик)
|
||||
$offender_id = count($offender_ids) > 0 ? $offender_ids[0] : '';
|
||||
$agent_id = count($offender_ids) > 1 ? $offender_ids[1] : '';
|
||||
|
||||
$logstring = date('Y-m-d H:i:s') . " Данные: project_id=$project_id, claim_id=$claim_id, contact_id=$contact_id, offender_id=$offender_id, agent_id=$agent_id";
|
||||
file_put_contents($logFile, $logstring . PHP_EOL, FILE_APPEND);
|
||||
|
||||
try {
|
||||
// ========================================
|
||||
// ПРОВЕРКА СУЩЕСТВОВАНИЯ ПРОЕКТА
|
||||
// ========================================
|
||||
$existingProjectId = null;
|
||||
|
||||
if (!empty($project_id)) {
|
||||
$project_id = preg_replace('/[^0-9]/', '', $project_id);
|
||||
$query = "SELECT p.projectid FROM vtiger_project p
|
||||
LEFT JOIN vtiger_crmentity e ON e.crmid = p.projectid
|
||||
WHERE e.deleted = 0 AND p.projectid = ? LIMIT 1";
|
||||
$res = $adb->pquery($query, array($project_id));
|
||||
if ($adb->num_rows($res) > 0) {
|
||||
$existingProjectId = $adb->query_result($res, 0, 'projectid');
|
||||
}
|
||||
}
|
||||
|
||||
// ========================================
|
||||
// ФОРМИРУЕМ ПАРАМЕТРЫ
|
||||
// ========================================
|
||||
$params = array();
|
||||
|
||||
// Если создаём новый проект - нужны contact_id и offender_id
|
||||
if (empty($existingProjectId)) {
|
||||
if (empty($contact_id) || empty($offender_id)) {
|
||||
throw new Exception('Для создания проекта нужны contact_id и offender_ids');
|
||||
}
|
||||
|
||||
// Получаем название контакта
|
||||
$query = "SELECT c.lastname FROM vtiger_contactdetails c
|
||||
LEFT JOIN vtiger_crmentity e ON e.crmid = c.contactid
|
||||
WHERE e.deleted = 0 AND c.contactid = ? LIMIT 1";
|
||||
$res = $adb->pquery($query, array($contact_id));
|
||||
$contactName = $adb->num_rows($res) > 0 ? $adb->query_result($res, 0, 'lastname') : 'Клиент';
|
||||
|
||||
// Получаем название контрагента
|
||||
$query = "SELECT a.accountname FROM vtiger_account a
|
||||
LEFT JOIN vtiger_crmentity e ON e.crmid = a.accountid
|
||||
WHERE e.deleted = 0 AND a.accountid = ? LIMIT 1";
|
||||
$res = $adb->pquery($query, array($offender_id));
|
||||
$accountName = $adb->num_rows($res) > 0 ? $adb->query_result($res, 0, 'accountname') : 'Контрагент';
|
||||
|
||||
// Название проекта
|
||||
$params['projectname'] = $contactName . ' ' . $accountName;
|
||||
$params['linktoaccountscontacts'] = '12x' . $contact_id;
|
||||
$params['cf_2274'] = '11x' . $offender_id; // Основной ответчик
|
||||
$params['projectstatus'] = 'модерация';
|
||||
$params['projecttype'] = 'Претензионно - исковая работа';
|
||||
$params['assigned_user_id'] = vtws_getWebserviceEntityId('Users', $current_user->id);
|
||||
|
||||
// Заявитель по умолчанию
|
||||
if (!isset($projectdata['cf_1994'])) {
|
||||
$params['cf_1994'] = vtws_getWebserviceEntityId('Accounts', 62345); // МОО КЛИЕНТПРАВ
|
||||
}
|
||||
}
|
||||
|
||||
// Агент (второй ответчик)
|
||||
if (!empty($agent_id)) {
|
||||
$params['cf_2276'] = '11x' . $agent_id;
|
||||
}
|
||||
|
||||
// Связь контакт/оффендер для обновления тоже можно передать
|
||||
if (!empty($contact_id) && !empty($existingProjectId)) {
|
||||
$params['linktoaccountscontacts'] = '12x' . $contact_id;
|
||||
}
|
||||
if (!empty($offender_id) && !empty($existingProjectId)) {
|
||||
$params['cf_2274'] = '11x' . $offender_id;
|
||||
}
|
||||
|
||||
// Маппинг полей из projectdata
|
||||
$fieldMapping = array(
|
||||
'cf_2206' => 'cf_2206', // SMS код
|
||||
'cf_2210' => 'cf_2210', // IP
|
||||
'cf_2212' => 'cf_2212', // Источник
|
||||
'cf_2214' => 'cf_2214', // Регион
|
||||
'cf_2208' => 'cf_2208', // Form ID
|
||||
'cf_1830' => 'cf_1830', // Категория
|
||||
'cf_1469' => 'cf_1469', // Направление
|
||||
'cf_1191' => 'cf_1191', // Цена договора
|
||||
'cf_1189' => 'cf_1189', // Предмет договора
|
||||
'cf_1203' => 'cf_1203', // Дата договора
|
||||
'cf_1839' => 'cf_1839', // Дата начала
|
||||
'cf_1841' => 'cf_1841', // Дата окончания
|
||||
'cf_1207' => 'cf_1207', // Ущерб
|
||||
'cf_1479' => 'cf_1479', // Стоимость услуг
|
||||
'cf_1227' => 'cf_1227', // Прогресс
|
||||
'cf_1231' => 'cf_1231', // Страна
|
||||
'cf_1239' => 'cf_1239', // Отель
|
||||
'cf_1566' => 'cf_1566', // Транспорт
|
||||
'cf_1564' => 'cf_1564', // Страховка
|
||||
'cf_1249' => 'cf_1249', // Прочее
|
||||
'cf_1471' => 'cf_1471', // Самостоятельно
|
||||
'cf_1473' => 'cf_1473', // Дата претензии
|
||||
'cf_1475' => 'cf_1475', // Возвращено
|
||||
'cf_1994' => 'cf_1994', // Заявитель
|
||||
'description' => 'description'
|
||||
);
|
||||
|
||||
foreach ($fieldMapping as $input => $crm) {
|
||||
if (isset($projectdata[$input]) && $projectdata[$input] !== null) {
|
||||
$value = $projectdata[$input];
|
||||
// Для cf_1994 (Заявитель) нужен формат 11xID
|
||||
if ($crm === 'cf_1994' && !empty($value) && strpos($value, 'x') === false) {
|
||||
$value = vtws_getWebserviceEntityId('Accounts', $value);
|
||||
}
|
||||
$params[$crm] = $value;
|
||||
}
|
||||
}
|
||||
|
||||
// ========================================
|
||||
// СОЗДАНИЕ ИЛИ ОБНОВЛЕНИЕ
|
||||
// ========================================
|
||||
if (!empty($existingProjectId)) {
|
||||
// === ОБНОВЛЕНИЕ ===
|
||||
$params['id'] = '33x' . $existingProjectId; // 33x для Project
|
||||
|
||||
$logstring = date('Y-m-d H:i:s') . ' 📝 Обновляем проект ' . $existingProjectId . ': ' . json_encode($params, JSON_UNESCAPED_UNICODE);
|
||||
file_put_contents($logFile, $logstring . PHP_EOL, FILE_APPEND);
|
||||
|
||||
$project = vtws_revise($params, $current_user);
|
||||
|
||||
$result['success'] = true;
|
||||
$result['project_id'] = $existingProjectId;
|
||||
$result['claim_id'] = $claim_id;
|
||||
$result['action'] = 'updated';
|
||||
$result['offender_id'] = $offender_id;
|
||||
$result['agent_id'] = $agent_id ?: null;
|
||||
$result['message'] = 'Проект обновлён';
|
||||
|
||||
$logstring = date('Y-m-d H:i:s') . ' ✅ Проект ' . $existingProjectId . ' обновлён';
|
||||
file_put_contents($logFile, $logstring . PHP_EOL, FILE_APPEND);
|
||||
|
||||
} else {
|
||||
// === СОЗДАНИЕ ===
|
||||
$logstring = date('Y-m-d H:i:s') . ' 🆕 Создаём проект: ' . json_encode($params, JSON_UNESCAPED_UNICODE);
|
||||
file_put_contents($logFile, $logstring . PHP_EOL, FILE_APPEND);
|
||||
|
||||
$project = vtws_create('Project', $params, $current_user);
|
||||
$newProjectId = substr($project['id'], strpos($project['id'], 'x') + 1); // Убираем префикс (63x)
|
||||
|
||||
$result['success'] = true;
|
||||
$result['project_id'] = $newProjectId;
|
||||
$result['claim_id'] = $claim_id;
|
||||
$result['action'] = 'created';
|
||||
$result['offender_id'] = $offender_id;
|
||||
$result['agent_id'] = $agent_id ?: null;
|
||||
$result['message'] = 'Проект создан';
|
||||
|
||||
$logstring = date('Y-m-d H:i:s') . ' ✅ Создан проект ' . $newProjectId;
|
||||
file_put_contents($logFile, $logstring . PHP_EOL, FILE_APPEND);
|
||||
}
|
||||
|
||||
} catch (WebServiceException $ex) {
|
||||
$result['success'] = false;
|
||||
$result['message'] = $ex->getMessage();
|
||||
|
||||
$logstring = date('Y-m-d H:i:s') . ' ❌ WebService ошибка: ' . $ex->getMessage();
|
||||
file_put_contents($logFile, $logstring . PHP_EOL, FILE_APPEND);
|
||||
|
||||
throw $ex;
|
||||
|
||||
} catch (Exception $ex) {
|
||||
$result['success'] = false;
|
||||
$result['message'] = $ex->getMessage();
|
||||
|
||||
$logstring = date('Y-m-d H:i:s') . ' ❌ Ошибка: ' . $ex->getMessage();
|
||||
file_put_contents($logFile, $logstring . PHP_EOL, FILE_APPEND);
|
||||
|
||||
throw new WebServiceException(WebServiceErrorCode::$INVALIDID, $ex->getMessage());
|
||||
}
|
||||
|
||||
$logstring = date('Y-m-d H:i:s') . ' RESULT: ' . json_encode($result, JSON_UNESCAPED_UNICODE) . PHP_EOL;
|
||||
file_put_contents($logFile, $logstring, FILE_APPEND);
|
||||
|
||||
return json_encode($result, JSON_UNESCAPED_UNICODE);
|
||||
}
|
||||
@@ -53,4 +53,5 @@ $languageStrings = array(
|
||||
'заявление на лист' => 'заявление на лист',
|
||||
'урегулирование' => 'урегулирование',
|
||||
'заключение мирового соглашения' => 'заключение мирового соглашения',
|
||||
'Черновик' => 'Черновик',
|
||||
);
|
||||
@@ -53,4 +53,5 @@ $languageStrings = array(
|
||||
'заявление на лист' => 'заявление на лист',
|
||||
'урегулирование' => 'урегулирование',
|
||||
'заключение мирового соглашения' => 'заключение мирового соглашения',
|
||||
'Черновик' => 'Черновик',
|
||||
);
|
||||
@@ -53,4 +53,5 @@ $languageStrings = array(
|
||||
'заявление на лист' => 'заявление на лист',
|
||||
'урегулирование' => 'урегулирование',
|
||||
'заключение мирового соглашения' => 'заключение мирового соглашения',
|
||||
'Черновик' => 'Черновик',
|
||||
);
|
||||
@@ -53,4 +53,5 @@ $languageStrings = array(
|
||||
'заявление на лист' => 'заявление на лист',
|
||||
'урегулирование' => 'урегулирование',
|
||||
'заключение мирового соглашения' => 'заключение мирового соглашения',
|
||||
'Черновик' => 'Черновик',
|
||||
);
|
||||
@@ -53,4 +53,5 @@ $languageStrings = array(
|
||||
'заявление на лист' => 'заявление на лист',
|
||||
'урегулирование' => 'урегулирование',
|
||||
'заключение мирового соглашения' => 'заключение мирового соглашения',
|
||||
'Черновик' => 'Черновик',
|
||||
);
|
||||
@@ -53,4 +53,5 @@ $languageStrings = array(
|
||||
'заявление на лист' => 'заявление на лист',
|
||||
'урегулирование' => 'урегулирование',
|
||||
'заключение мирового соглашения' => 'заключение мирового соглашения',
|
||||
'Черновик' => 'Черновик',
|
||||
);
|
||||
@@ -53,4 +53,5 @@ $languageStrings = array(
|
||||
'заявление на лист' => 'заявление на лист',
|
||||
'урегулирование' => 'урегулирование',
|
||||
'заключение мирового соглашения' => 'заключение мирового соглашения',
|
||||
'Черновик' => 'Черновик',
|
||||
);
|
||||
@@ -53,4 +53,5 @@ $languageStrings = array(
|
||||
'заявление на лист' => 'заявление на лист',
|
||||
'урегулирование' => 'урегулирование',
|
||||
'заключение мирового соглашения' => 'заключение мирового соглашения',
|
||||
'Черновик' => 'Черновик',
|
||||
);
|
||||
@@ -53,4 +53,5 @@ $languageStrings = array(
|
||||
'заявление на лист' => 'заявление на лист',
|
||||
'урегулирование' => 'урегулирование',
|
||||
'заключение мирового соглашения' => 'заключение мирового соглашения',
|
||||
'Черновик' => 'Черновик',
|
||||
);
|
||||
@@ -53,4 +53,5 @@ $languageStrings = array(
|
||||
'заявление на лист' => 'заявление на лист',
|
||||
'урегулирование' => 'урегулирование',
|
||||
'заключение мирового соглашения' => 'заключение мирового соглашения',
|
||||
'Черновик' => 'Черновик',
|
||||
);
|
||||
@@ -53,4 +53,5 @@ $languageStrings = array(
|
||||
'заявление на лист' => 'заявление на лист',
|
||||
'урегулирование' => 'урегулирование',
|
||||
'заключение мирового соглашения' => 'заключение мирового соглашения',
|
||||
'Черновик' => 'Черновик',
|
||||
);
|
||||
@@ -53,4 +53,5 @@ $languageStrings = array(
|
||||
'заявление на лист' => 'заявление на лист',
|
||||
'урегулирование' => 'урегулирование',
|
||||
'заключение мирового соглашения' => 'заключение мирового соглашения',
|
||||
'Черновик' => 'Черновик',
|
||||
);
|
||||
@@ -53,4 +53,5 @@ $languageStrings = array(
|
||||
'заявление на лист' => 'заявление на лист',
|
||||
'урегулирование' => 'урегулирование',
|
||||
'заключение мирового соглашения' => 'заключение мирового соглашения',
|
||||
'Черновик' => 'Черновик',
|
||||
);
|
||||
@@ -53,4 +53,5 @@ $languageStrings = array(
|
||||
'заявление на лист' => 'заявление на лист',
|
||||
'урегулирование' => 'урегулирование',
|
||||
'заключение мирового соглашения' => 'заключение мирового соглашения',
|
||||
'Черновик' => 'Черновик',
|
||||
);
|
||||
@@ -53,4 +53,5 @@ $languageStrings = array(
|
||||
'заявление на лист' => 'заявление на лист',
|
||||
'урегулирование' => 'урегулирование',
|
||||
'заключение мирового соглашения' => 'заключение мирового соглашения',
|
||||
'Черновик' => 'Черновик',
|
||||
);
|
||||
@@ -53,4 +53,5 @@ $languageStrings = array(
|
||||
'заявление на лист' => 'заявление на лист',
|
||||
'урегулирование' => 'урегулирование',
|
||||
'заключение мирового соглашения' => 'заключение мирового соглашения',
|
||||
'Черновик' => 'Черновик',
|
||||
);
|
||||
@@ -15,6 +15,10 @@ function openProjectFolder(projectId, projectName) {
|
||||
projectName = projectName.replace(/"/g, '_');
|
||||
// Заменяем ВСЕ пробелы на подчёркивания
|
||||
projectName = projectName.replace(/\s+/g, '_');
|
||||
// Заменяем множественные подчёркивания на одинарное
|
||||
projectName = projectName.replace(/_+/g, '_');
|
||||
// Убираем подчёркивания в начале и конце
|
||||
projectName = projectName.replace(/^_+|_+$/g, '');
|
||||
}
|
||||
|
||||
// Формируем URL для папки проекта в Nextcloud
|
||||
|
||||
@@ -15,6 +15,10 @@ function openProjectFolder(projectId, projectName) {
|
||||
projectName = projectName.replace(/"/g, '_');
|
||||
// Заменяем ВСЕ пробелы на подчёркивания
|
||||
projectName = projectName.replace(/\s+/g, '_');
|
||||
// Заменяем множественные подчёркивания на одинарное
|
||||
projectName = projectName.replace(/_+/g, '_');
|
||||
// Убираем подчёркивания в начале и конце
|
||||
projectName = projectName.replace(/^_+|_+$/g, '');
|
||||
}
|
||||
|
||||
// Формируем URL для папки проекта в Nextcloud
|
||||
|
||||
@@ -1,20 +1,24 @@
|
||||
/* AI Drawer - основные стили */
|
||||
.ai-drawer {
|
||||
position: fixed;
|
||||
right: -400px; /* Начально скрыт */
|
||||
right: 0; /* Всегда прижат к правому краю */
|
||||
top: 0;
|
||||
width: 400px;
|
||||
min-width: 300px; /* Минимальная ширина */
|
||||
max-width: 50vw; /* Максимальная ширина - половина экрана */
|
||||
height: 100vh;
|
||||
max-height: 100vh; /* Не превышаем высоту экрана */
|
||||
background: #ffffff; /* Чистый белый фон */
|
||||
box-shadow: -2px 0 15px rgba(0,0,0,0.1);
|
||||
transition: right 0.3s ease;
|
||||
transform: translateX(100%); /* Начально скрыт - сдвинут вправо на 100% своей ширины */
|
||||
transition: transform 0.3s ease;
|
||||
z-index: 999999;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
font-size: 14px; /* Базовый размер шрифта */
|
||||
border-left: 1px solid #e9ecef;
|
||||
overflow: hidden; /* Предотвращаем выход элементов за пределы */
|
||||
box-sizing: border-box; /* Учитываем padding и border в ширине */
|
||||
}
|
||||
|
||||
/* Полоска для изменения ширины */
|
||||
@@ -44,13 +48,18 @@
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
/* Убираем transition при изменении размера, чтобы не было задержек */
|
||||
.ai-drawer.resizing.open {
|
||||
transform: translateX(0) !important;
|
||||
}
|
||||
|
||||
.ai-drawer.resizing .ai-drawer-resize-handle {
|
||||
background: #007bff;
|
||||
width: 4px;
|
||||
}
|
||||
|
||||
.ai-drawer.open {
|
||||
right: 0;
|
||||
transform: translateX(0); /* Показываем - сдвигаем на место */
|
||||
}
|
||||
|
||||
/* Скрываем кнопку AI когда drawer открыт */
|
||||
@@ -91,6 +100,9 @@ body.ai-drawer-open .ai-drawer-toggle {
|
||||
align-items: center;
|
||||
font-weight: 600;
|
||||
border-bottom: 1px solid #0056b3;
|
||||
flex-shrink: 0; /* Не сжимается при изменении размера */
|
||||
min-height: 50px; /* Минимальная высота для кнопки закрытия */
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.ai-drawer-close {
|
||||
@@ -436,12 +448,42 @@ body.ai-drawer-open .ai-drawer-toggle {
|
||||
|
||||
.ai-message-content p {
|
||||
margin: 0 0 5px 0;
|
||||
word-wrap: break-word;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.ai-message-content p:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
/* Стили для ссылок в сообщениях */
|
||||
.ai-message-link {
|
||||
color: #007bff;
|
||||
text-decoration: underline;
|
||||
cursor: pointer;
|
||||
word-break: break-word;
|
||||
display: inline-block;
|
||||
margin: 2px 0;
|
||||
padding: 2px 4px;
|
||||
border-radius: 3px;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.ai-message-link:hover {
|
||||
color: #0056b3;
|
||||
text-decoration: none;
|
||||
background-color: #e7f3ff;
|
||||
padding: 2px 6px;
|
||||
}
|
||||
|
||||
.ai-message-link:visited {
|
||||
color: #6f42c1;
|
||||
}
|
||||
|
||||
.ai-message-link:active {
|
||||
color: #004085;
|
||||
}
|
||||
|
||||
.ai-message-time {
|
||||
font-size: 11px;
|
||||
color: #6c757d; /* Серый цвет для времени */
|
||||
@@ -456,6 +498,8 @@ body.ai-drawer-open .ai-drawer-toggle {
|
||||
border-top: 1px solid #dee2e6;
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
flex-shrink: 0; /* Не сжимается при изменении размера */
|
||||
box-sizing: border-box;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
@@ -510,7 +554,8 @@ body.ai-drawer-open .ai-drawer-toggle {
|
||||
@media (max-width: 768px) {
|
||||
.ai-drawer {
|
||||
width: 100%;
|
||||
right: -100%;
|
||||
right: 0;
|
||||
transform: translateX(100%); /* Начально скрыт на мобильных */
|
||||
height: 100vh;
|
||||
height: 100dvh; /* Динамическая высота viewport для мобильных */
|
||||
display: flex;
|
||||
@@ -731,7 +776,8 @@ body.ai-drawer-open .ai-drawer-toggle {
|
||||
width: 400px;
|
||||
min-width: 300px;
|
||||
max-width: 50vw;
|
||||
right: -400px;
|
||||
right: 0;
|
||||
transform: translateX(100%); /* Начально скрыт на планшетах */
|
||||
height: 100vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
@@ -48,7 +48,11 @@ class AIDrawer {
|
||||
'<div class="ai-avatar assistant"></div>' +
|
||||
'<div class="ai-message-content">' +
|
||||
'<p>Привет! Я ваш AI ассистент. Чем могу помочь?</p>' +
|
||||
'<div class="ai-message-time">' + new Date().toLocaleTimeString() + '</div>' +
|
||||
'<div class="ai-message-time">' + new Date().toLocaleTimeString('ru-RU', {
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
hour12: false // 24-часовой формат
|
||||
}) + '</div>' +
|
||||
'</div>' +
|
||||
'</div>' +
|
||||
'</div>' +
|
||||
@@ -128,7 +132,7 @@ class AIDrawer {
|
||||
|
||||
// Обработчик изменения размера окна - ограничиваем ширину если нужно
|
||||
window.addEventListener('resize', () => {
|
||||
if (this.drawerWidth > window.innerWidth / 2) {
|
||||
if (this.isOpen && this.drawerWidth > window.innerWidth / 2) {
|
||||
const maxWidth = window.innerWidth / 2;
|
||||
this.setDrawerWidth(maxWidth);
|
||||
}
|
||||
@@ -144,8 +148,18 @@ class AIDrawer {
|
||||
const savedWidth = localStorage.getItem('ai-drawer-width');
|
||||
if (savedWidth) {
|
||||
const width = parseInt(savedWidth, 10);
|
||||
if (width >= 300 && width <= window.innerWidth / 2) {
|
||||
const maxWidth = window.innerWidth / 2;
|
||||
// Проверяем что ширина в допустимых пределах для текущего экрана
|
||||
if (width >= 300 && width <= maxWidth) {
|
||||
this.setDrawerWidth(width);
|
||||
} else if (width > maxWidth) {
|
||||
// Если сохраненная ширина больше максимума - ограничиваем
|
||||
console.log('AI Drawer: Saved width', width, 'exceeds max', maxWidth, ', adjusting');
|
||||
this.setDrawerWidth(maxWidth);
|
||||
} else {
|
||||
// Если меньше минимума - устанавливаем минимум
|
||||
console.log('AI Drawer: Saved width', width, 'is less than minimum, setting to 300');
|
||||
this.setDrawerWidth(300);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -220,6 +234,23 @@ class AIDrawer {
|
||||
open() {
|
||||
console.log('AI Drawer: Opening drawer');
|
||||
if (this.drawer) {
|
||||
// Проверяем и корректируем ширину перед открытием
|
||||
const maxWidth = window.innerWidth / 2;
|
||||
if (this.drawerWidth > maxWidth) {
|
||||
console.log('AI Drawer: Adjusting width from', this.drawerWidth, 'to', maxWidth);
|
||||
this.setDrawerWidth(maxWidth);
|
||||
} else if (this.drawerWidth < 300) {
|
||||
console.log('AI Drawer: Adjusting width from', this.drawerWidth, 'to 300');
|
||||
this.setDrawerWidth(300);
|
||||
}
|
||||
|
||||
// Убеждаемся что ширина применена к drawer
|
||||
this.drawer.style.width = this.drawerWidth + 'px';
|
||||
|
||||
// Убеждаемся что drawer правильно позиционирован перед открытием
|
||||
this.drawer.style.right = '0';
|
||||
this.drawer.style.transform = 'translateX(0)';
|
||||
|
||||
this.drawer.classList.add('open');
|
||||
}
|
||||
|
||||
@@ -234,6 +265,33 @@ class AIDrawer {
|
||||
mainContainer.setAttribute('data-drawer-width', this.drawerWidth);
|
||||
}
|
||||
|
||||
// Прокручиваем вниз к последнему сообщению при открытии
|
||||
const scrollToBottomOnOpen = () => {
|
||||
const drawerContent = this.drawer?.querySelector('.ai-drawer-content');
|
||||
const chatMessages = this.drawer?.querySelector('.ai-chat-messages');
|
||||
|
||||
if (drawerContent) {
|
||||
const scroll = () => {
|
||||
drawerContent.scrollTop = drawerContent.scrollHeight;
|
||||
console.log('AI Drawer: Scrolled on open, scrollTop:', drawerContent.scrollTop, 'scrollHeight:', drawerContent.scrollHeight);
|
||||
};
|
||||
|
||||
// Прокручиваем последнее сообщение в видимую область
|
||||
if (chatMessages && chatMessages.lastElementChild) {
|
||||
chatMessages.lastElementChild.scrollIntoView({ behavior: 'smooth', block: 'end' });
|
||||
}
|
||||
|
||||
requestAnimationFrame(() => {
|
||||
scroll();
|
||||
requestAnimationFrame(scroll);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// Прокручиваем после анимации открытия
|
||||
setTimeout(scrollToBottomOnOpen, 300);
|
||||
setTimeout(scrollToBottomOnOpen, 600);
|
||||
|
||||
// История уже загружена при инициализации страницы
|
||||
// Не нужно дополнительных запросов при открытии
|
||||
}
|
||||
@@ -242,6 +300,10 @@ class AIDrawer {
|
||||
console.log('AI Drawer: Closing drawer');
|
||||
if (this.drawer) {
|
||||
this.drawer.classList.remove('open');
|
||||
// Убеждаемся что drawer скрыт через transform
|
||||
if (!this.drawer.classList.contains('open')) {
|
||||
this.drawer.style.transform = 'translateX(100%)';
|
||||
}
|
||||
}
|
||||
|
||||
document.body.classList.remove('ai-drawer-open');
|
||||
@@ -295,6 +357,142 @@ class AIDrawer {
|
||||
}
|
||||
}
|
||||
|
||||
// Функция для определения читаемого текста ссылки на основе URL
|
||||
getLinkText(url) {
|
||||
const urlLower = url.toLowerCase();
|
||||
const maxLength = 60; // Максимальная длина ссылки до замены
|
||||
|
||||
// Если ссылка короткая, показываем её полностью
|
||||
if (url.length <= maxLength) {
|
||||
return url;
|
||||
}
|
||||
|
||||
// Анализируем URL и определяем тип действия
|
||||
if (urlLower.includes('download') || urlLower.includes('file') || urlLower.includes('скачать')) {
|
||||
return '📥 Скачать документ';
|
||||
}
|
||||
|
||||
if (urlLower.includes('edit') || urlLower.includes('редактир') || urlLower.includes('onlyoffice')) {
|
||||
return '✏️ Открыть для редактирования';
|
||||
}
|
||||
|
||||
if (urlLower.includes('view') || urlLower.includes('просмотр') || urlLower.includes('preview')) {
|
||||
return '👁️ Открыть для просмотра';
|
||||
}
|
||||
|
||||
if (urlLower.includes('document') || urlLower.includes('документ') || urlLower.includes('.docx') || urlLower.includes('.pdf')) {
|
||||
return '📄 Открыть документ';
|
||||
}
|
||||
|
||||
if (urlLower.includes('create') || urlLower.includes('создать')) {
|
||||
return '➕ Создать документ';
|
||||
}
|
||||
|
||||
// Общие варианты для длинных ссылок
|
||||
const linkTexts = [
|
||||
'🔗 Открыть ссылку',
|
||||
'👉 Смотреть здесь',
|
||||
'📋 Подробнее',
|
||||
'🔍 Перейти к документу',
|
||||
'📎 Открыть'
|
||||
];
|
||||
|
||||
// Выбираем случайный вариант для разнообразия
|
||||
return linkTexts[Math.floor(Math.random() * linkTexts.length)];
|
||||
}
|
||||
|
||||
// Функция для преобразования URL в кликабельные ссылки
|
||||
convertUrlsToLinks(text) {
|
||||
if (!text) return '';
|
||||
|
||||
let result = text;
|
||||
|
||||
// ШАГ 1: Обрабатываем Markdown ссылки [текст](url)
|
||||
const markdownLinkRegex = /\[([^\]]+)\]\(([^)]+)\)/g;
|
||||
result = result.replace(markdownLinkRegex, (match, linkText, url) => {
|
||||
// Проверяем, что это валидный URL
|
||||
if (url.match(/^https?:\/\//i)) {
|
||||
return `<a href="${url}" target="_blank" rel="noopener noreferrer" class="ai-message-link" title="${url}">${linkText}</a>`;
|
||||
}
|
||||
return match; // Если не URL, оставляем как есть
|
||||
});
|
||||
|
||||
// ШАГ 2: Временно заменяем уже существующие HTML-ссылки на плейсхолдеры
|
||||
const htmlLinks = [];
|
||||
const htmlLinkRegex = /<a\s+[^>]*href\s*=\s*["']([^"']+)["'][^>]*>([^<]*)<\/a>/gi;
|
||||
result = result.replace(htmlLinkRegex, (match, href, linkText) => {
|
||||
const placeholder = `__HTML_LINK_${htmlLinks.length}__`;
|
||||
htmlLinks.push({ href, linkText, match });
|
||||
return placeholder;
|
||||
});
|
||||
|
||||
// ШАГ 3: Экранируем оставшийся HTML для безопасности
|
||||
const escaped = result
|
||||
.replace(/&/g, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
.replace(/"/g, '"')
|
||||
.replace(/'/g, ''');
|
||||
|
||||
// ШАГ 4: Восстанавливаем HTML-ссылки (плейсхолдеры не экранированы, т.к. не содержат < >)
|
||||
let finalResult = escaped;
|
||||
htmlLinks.forEach((link, index) => {
|
||||
const placeholder = `__HTML_LINK_${index}__`;
|
||||
// Используем оригинальную ссылку, но добавляем класс если его нет
|
||||
let htmlLink = link.match;
|
||||
if (!htmlLink.includes('class=')) {
|
||||
htmlLink = htmlLink.replace('<a', '<a class="ai-message-link"');
|
||||
} else if (!htmlLink.includes('ai-message-link')) {
|
||||
htmlLink = htmlLink.replace('class="', 'class="ai-message-link ');
|
||||
htmlLink = htmlLink.replace("class='", "class='ai-message-link ");
|
||||
}
|
||||
// Убеждаемся что есть target="_blank"
|
||||
if (!htmlLink.includes('target=')) {
|
||||
htmlLink = htmlLink.replace('<a', '<a target="_blank" rel="noopener noreferrer"');
|
||||
}
|
||||
// Заменяем плейсхолдер на правильный HTML (не экранированный)
|
||||
finalResult = finalResult.replace(placeholder, htmlLink);
|
||||
});
|
||||
|
||||
// ШАГ 5: Преобразуем обычные URL в ссылки (только те, что не внутри уже существующих ссылок)
|
||||
const urlRegex = /(https?:\/\/[^\s<>"{}|\\^`\[\]]+)/gi;
|
||||
let urlMatches = [];
|
||||
let match;
|
||||
|
||||
// Сначала находим все URL и проверяем их контекст
|
||||
while ((match = urlRegex.exec(finalResult)) !== null) {
|
||||
const url = match[0];
|
||||
const offset = match.index;
|
||||
const beforeMatch = finalResult.substring(0, offset);
|
||||
|
||||
// Проверяем, нет ли открывающего тега <a перед этим URL
|
||||
const lastOpenTag = beforeMatch.lastIndexOf('<a');
|
||||
const lastCloseTag = beforeMatch.lastIndexOf('</a>');
|
||||
|
||||
// Если есть открывающий тег <a и нет закрывающего после него - значит мы внутри ссылки
|
||||
if (lastOpenTag > lastCloseTag) {
|
||||
continue; // Пропускаем, это уже часть ссылки
|
||||
}
|
||||
|
||||
// Проверяем, не является ли это частью href атрибута
|
||||
if (beforeMatch.lastIndexOf('href=') > lastCloseTag) {
|
||||
continue; // Пропускаем, это часть href
|
||||
}
|
||||
|
||||
urlMatches.push({ url, offset });
|
||||
}
|
||||
|
||||
// Заменяем URL в обратном порядке (чтобы не сбить индексы)
|
||||
for (let i = urlMatches.length - 1; i >= 0; i--) {
|
||||
const { url, offset } = urlMatches[i];
|
||||
const linkText = this.getLinkText(url);
|
||||
const linkHtml = `<a href="${url}" target="_blank" rel="noopener noreferrer" class="ai-message-link" title="${url}">${linkText}</a>`;
|
||||
finalResult = finalResult.substring(0, offset) + linkHtml + finalResult.substring(offset + url.length);
|
||||
}
|
||||
|
||||
return finalResult;
|
||||
}
|
||||
|
||||
addMessage(text, isUser = false, customTime = null) {
|
||||
console.log('AI Drawer: addMessage called with:', {text: text.substring(0, 50), isUser, customTime});
|
||||
|
||||
@@ -331,17 +529,69 @@ class AIDrawer {
|
||||
contentDiv.className = 'ai-message-content';
|
||||
|
||||
const textDiv = document.createElement('p');
|
||||
textDiv.textContent = text;
|
||||
// Преобразуем URL в кликабельные ссылки
|
||||
textDiv.innerHTML = this.convertUrlsToLinks(text);
|
||||
contentDiv.appendChild(textDiv);
|
||||
|
||||
const timeDiv = document.createElement('div');
|
||||
timeDiv.className = 'ai-message-time';
|
||||
if (customTime) {
|
||||
// Если передано время из истории, используем его
|
||||
const historyTime = new Date(customTime);
|
||||
timeDiv.textContent = historyTime.toLocaleTimeString();
|
||||
try {
|
||||
// Логируем для отладки
|
||||
console.log('AI Drawer: Parsing timestamp:', customTime);
|
||||
const historyTime = new Date(customTime);
|
||||
// Проверяем что дата валидна
|
||||
if (isNaN(historyTime.getTime())) {
|
||||
// Если дата невалидна, пытаемся распарсить как строку времени (старый формат)
|
||||
console.warn('AI Drawer: Invalid timestamp format:', customTime, 'Parsed as:', historyTime);
|
||||
timeDiv.textContent = customTime; // Показываем как есть
|
||||
} else {
|
||||
// Определяем, нужно ли показывать дату
|
||||
const now = new Date();
|
||||
const today = new Date(now.getFullYear(), now.getMonth(), now.getDate());
|
||||
const messageDate = new Date(historyTime.getFullYear(), historyTime.getMonth(), historyTime.getDate());
|
||||
const isToday = messageDate.getTime() === today.getTime();
|
||||
|
||||
let formattedTime;
|
||||
if (isToday) {
|
||||
// Если сообщение сегодня - показываем только время
|
||||
formattedTime = historyTime.toLocaleTimeString('ru-RU', {
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
hour12: false
|
||||
});
|
||||
} else {
|
||||
// Если сообщение не сегодня - показываем дату и время
|
||||
const dateStr = historyTime.toLocaleDateString('ru-RU', {
|
||||
day: '2-digit',
|
||||
month: '2-digit'
|
||||
});
|
||||
const timeStr = historyTime.toLocaleTimeString('ru-RU', {
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
hour12: false
|
||||
});
|
||||
formattedTime = `${dateStr} ${timeStr}`;
|
||||
}
|
||||
|
||||
console.log('AI Drawer: Successfully formatted timestamp:', customTime, '->', formattedTime);
|
||||
timeDiv.textContent = formattedTime;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('AI Drawer: Error parsing timestamp:', customTime, error);
|
||||
timeDiv.textContent = customTime || new Date().toLocaleTimeString('ru-RU', {
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
hour12: false // 24-часовой формат
|
||||
});
|
||||
}
|
||||
} else {
|
||||
timeDiv.textContent = new Date().toLocaleTimeString();
|
||||
timeDiv.textContent = new Date().toLocaleTimeString('ru-RU', {
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
hour12: false // 24-часовой формат
|
||||
});
|
||||
}
|
||||
contentDiv.appendChild(timeDiv);
|
||||
|
||||
@@ -388,7 +638,11 @@ class AIDrawer {
|
||||
|
||||
const timeDiv = document.createElement('div');
|
||||
timeDiv.className = 'ai-message-time';
|
||||
timeDiv.textContent = new Date().toLocaleTimeString();
|
||||
timeDiv.textContent = new Date().toLocaleTimeString('ru-RU', {
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
hour12: false // 24-часовой формат
|
||||
});
|
||||
contentDiv.appendChild(timeDiv);
|
||||
|
||||
messageDiv.appendChild(avatarDiv);
|
||||
@@ -404,9 +658,12 @@ class AIDrawer {
|
||||
|
||||
streamText(element, text, speed = 30) {
|
||||
let index = 0;
|
||||
let currentText = '';
|
||||
const interval = setInterval(() => {
|
||||
if (index < text.length) {
|
||||
element.textContent += text[index];
|
||||
currentText += text[index];
|
||||
// Преобразуем URL в кликабельные ссылки по мере добавления текста
|
||||
element.innerHTML = this.convertUrlsToLinks(currentText);
|
||||
index++;
|
||||
|
||||
const content = this.drawer.querySelector('.ai-drawer-content');
|
||||
@@ -880,6 +1137,41 @@ class AIDrawer {
|
||||
}
|
||||
});
|
||||
|
||||
// Прокручиваем вниз к последнему сообщению после загрузки истории
|
||||
const scrollToBottom = () => {
|
||||
const drawerContent = this.drawer?.querySelector('.ai-drawer-content');
|
||||
const chatMessages = this.drawer?.querySelector('.ai-chat-messages');
|
||||
|
||||
if (drawerContent) {
|
||||
// Способ 1: Прокручиваем контейнер
|
||||
const scroll = () => {
|
||||
drawerContent.scrollTop = drawerContent.scrollHeight;
|
||||
console.log('AI Drawer: Scrolled container, scrollTop:', drawerContent.scrollTop, 'scrollHeight:', drawerContent.scrollHeight);
|
||||
};
|
||||
|
||||
// Способ 2: Прокручиваем последнее сообщение в видимую область
|
||||
if (chatMessages && chatMessages.lastElementChild) {
|
||||
chatMessages.lastElementChild.scrollIntoView({ behavior: 'smooth', block: 'end' });
|
||||
console.log('AI Drawer: Scrolled last message into view');
|
||||
}
|
||||
|
||||
// Используем requestAnimationFrame для более надежной прокрутки
|
||||
requestAnimationFrame(() => {
|
||||
scroll();
|
||||
requestAnimationFrame(() => {
|
||||
scroll();
|
||||
});
|
||||
});
|
||||
} else {
|
||||
console.warn('AI Drawer: Drawer content not found for scrolling');
|
||||
}
|
||||
};
|
||||
|
||||
// Прокручиваем с несколькими задержками для надежности
|
||||
setTimeout(scrollToBottom, 100);
|
||||
setTimeout(scrollToBottom, 300);
|
||||
setTimeout(scrollToBottom, 600);
|
||||
|
||||
console.log('AI Drawer: Chat history restored -', data.history.length, 'messages');
|
||||
} else {
|
||||
console.log('AI Drawer: No chat history found. Response:', data);
|
||||
|
||||
@@ -19,23 +19,23 @@ class Documents_Record_Model extends Vtiger_Record_Model {
|
||||
}
|
||||
|
||||
function getDownloadFileURL() {
|
||||
// Сначала проверяем filelocationtype - это основной индикатор типа хранения
|
||||
// Проверяем наличие S3 метаданных для любого типа файла
|
||||
$db = PearDatabase::getInstance();
|
||||
$result = $db->pquery("SELECT s3_bucket, s3_key FROM vtiger_notes WHERE notesid = ? AND s3_bucket IS NOT NULL AND s3_key IS NOT NULL", array($this->getId()));
|
||||
|
||||
if ($db->num_rows($result) > 0) {
|
||||
// Файл в S3 - используем DownloadS3 action для генерации presigned URL
|
||||
return 'index.php?module='. $this->getModuleName() .'&action=DownloadS3&record='. $this->getId();
|
||||
}
|
||||
|
||||
// Если нет S3 метаданных, обрабатываем по типу хранения
|
||||
if ($this->get('filelocationtype') == 'E') {
|
||||
// Внешняя ссылка (S3 URL уже в filename)
|
||||
// Внешняя ссылка (не S3, а другой внешний источник)
|
||||
return $this->get('filename');
|
||||
} else if ($this->get('filelocationtype') == 'I') {
|
||||
// Проверяем, есть ли S3 метаданные для внутренних файлов
|
||||
$db = PearDatabase::getInstance();
|
||||
$result = $db->pquery("SELECT s3_key FROM vtiger_notes WHERE notesid = ? AND s3_key IS NOT NULL", array($this->getId()));
|
||||
|
||||
if ($db->num_rows($result) > 0) {
|
||||
// Файл в S3 - используем новый DownloadS3 action
|
||||
return 'index.php?module='. $this->getModuleName() .'&action=DownloadS3&record='. $this->getId();
|
||||
} else {
|
||||
// Файл в локальном storage - используем старый метод
|
||||
$fileDetails = $this->getFileDetails();
|
||||
return 'index.php?module='. $this->getModuleName() .'&action=DownloadFile&record='. $this->getId() .'&fileid='. $fileDetails['attachmentsid'].'&name='. $fileDetails['name'];
|
||||
}
|
||||
// Файл в локальном storage - используем старый метод
|
||||
$fileDetails = $this->getFileDetails();
|
||||
return 'index.php?module='. $this->getModuleName() .'&action=DownloadFile&record='. $this->getId() .'&fileid='. $fileDetails['attachmentsid'].'&name='. $fileDetails['name'];
|
||||
} else {
|
||||
// По умолчанию - внешняя ссылка
|
||||
return $this->get('filename');
|
||||
|
||||
@@ -2,6 +2,212 @@
|
||||
|
||||
class Vtiger_Base_Service
|
||||
{
|
||||
private static $s3Client = null;
|
||||
private static $tempFiles = []; // Для очистки временных файлов после архивации
|
||||
|
||||
/**
|
||||
* Инициализация S3 клиента
|
||||
*/
|
||||
private static function initS3Client()
|
||||
{
|
||||
if (self::$s3Client === null) {
|
||||
$configPath = __DIR__ . '/../../crm_extensions/file_storage/config.php';
|
||||
if (file_exists($configPath)) {
|
||||
$config = require $configPath;
|
||||
require_once __DIR__ . '/../../crm_extensions/file_storage/S3Client.php';
|
||||
self::$s3Client = new S3Client($config['s3']);
|
||||
}
|
||||
}
|
||||
return self::$s3Client;
|
||||
}
|
||||
|
||||
/**
|
||||
* Скачивание файла из S3 во временную папку
|
||||
*/
|
||||
private static function downloadS3File($s3Bucket, $s3Key, $fileName)
|
||||
{
|
||||
$debugLog = '/tmp/s3_download_debug.log';
|
||||
@file_put_contents($debugLog, date('Y-m-d H:i:s') . " - downloadS3File: START - bucket={$s3Bucket}, key={$s3Key}\n", FILE_APPEND);
|
||||
|
||||
try {
|
||||
error_log("downloadS3File: Starting download - bucket={$s3Bucket}, key={$s3Key}");
|
||||
|
||||
// Используем нативный AWS SDK для скачивания
|
||||
// Пробуем несколько возможных путей к vendor/autoload.php
|
||||
$possibleVendorPaths = [
|
||||
__DIR__ . '/../../vendor/autoload.php', // От modules/Vtiger/services/
|
||||
__DIR__ . '/../../../vendor/autoload.php', // Альтернативный путь
|
||||
'/var/www/fastuser/data/www/crm.clientright.ru/vendor/autoload.php', // Абсолютный путь
|
||||
];
|
||||
|
||||
$vendorPath = null;
|
||||
foreach ($possibleVendorPaths as $path) {
|
||||
@file_put_contents($debugLog, date('Y-m-d H:i:s') . " - Checking vendor path: {$path}\n", FILE_APPEND);
|
||||
if (file_exists($path)) {
|
||||
$vendorPath = $path;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!$vendorPath) {
|
||||
$errorMsg = "downloadS3File: vendor/autoload.php not found. Tried: " . implode(', ', $possibleVendorPaths);
|
||||
error_log($errorMsg);
|
||||
@file_put_contents($debugLog, date('Y-m-d H:i:s') . " - ERROR: {$errorMsg}\n", FILE_APPEND);
|
||||
return false;
|
||||
}
|
||||
|
||||
require_once $vendorPath;
|
||||
@file_put_contents($debugLog, date('Y-m-d H:i:s') . " - vendor/autoload.php loaded from: {$vendorPath}\n", FILE_APPEND);
|
||||
|
||||
// Пробуем несколько путей к конфигурации
|
||||
$possiblePaths = [
|
||||
__DIR__ . '/../../crm_extensions/file_storage/config.php',
|
||||
dirname(__DIR__) . '/../../crm_extensions/file_storage/config.php',
|
||||
'/var/www/fastuser/data/www/crm.clientright.ru/crm_extensions/file_storage/config.php'
|
||||
];
|
||||
|
||||
$configPath = null;
|
||||
foreach ($possiblePaths as $path) {
|
||||
if (file_exists($path)) {
|
||||
$configPath = $path;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!$configPath) {
|
||||
$errorMsg = "downloadS3File: Config file not found. Tried: " . implode(', ', $possiblePaths);
|
||||
error_log($errorMsg);
|
||||
@file_put_contents($debugLog, date('Y-m-d H:i:s') . " - ERROR: {$errorMsg}\n", FILE_APPEND);
|
||||
return false;
|
||||
}
|
||||
@file_put_contents($debugLog, date('Y-m-d H:i:s') . " - Config found at: {$configPath}\n", FILE_APPEND);
|
||||
|
||||
try {
|
||||
$config = require $configPath;
|
||||
@file_put_contents($debugLog, date('Y-m-d H:i:s') . " - Config loaded successfully\n", FILE_APPEND);
|
||||
} catch (Exception $e) {
|
||||
$errorMsg = "downloadS3File: Error loading config: " . $e->getMessage();
|
||||
error_log($errorMsg);
|
||||
@file_put_contents($debugLog, date('Y-m-d H:i:s') . " - ERROR: {$errorMsg}\n", FILE_APPEND);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!isset($config['s3'])) {
|
||||
$errorMsg = "downloadS3File: S3 config not found in config file";
|
||||
error_log($errorMsg);
|
||||
@file_put_contents($debugLog, date('Y-m-d H:i:s') . " - ERROR: {$errorMsg}\n", FILE_APPEND);
|
||||
return false;
|
||||
}
|
||||
|
||||
$s3Config = $config['s3'];
|
||||
@file_put_contents($debugLog, date('Y-m-d H:i:s') . " - S3 config loaded, endpoint: " . ($s3Config['endpoint'] ?? 'NULL') . "\n", FILE_APPEND);
|
||||
|
||||
// Проверяем наличие обязательных полей
|
||||
if (empty($s3Config['key']) || empty($s3Config['secret']) || empty($s3Config['endpoint'])) {
|
||||
$errorMsg = "downloadS3File: Missing required S3 config fields";
|
||||
error_log($errorMsg);
|
||||
@file_put_contents($debugLog, date('Y-m-d H:i:s') . " - ERROR: {$errorMsg}\n", FILE_APPEND);
|
||||
return false;
|
||||
}
|
||||
|
||||
@file_put_contents($debugLog, date('Y-m-d H:i:s') . " - Creating S3Client...\n", FILE_APPEND);
|
||||
$awsClient = new \Aws\S3\S3Client([
|
||||
'version' => $s3Config['version'],
|
||||
'region' => $s3Config['region'],
|
||||
'endpoint' => $s3Config['endpoint'],
|
||||
'use_path_style_endpoint' => $s3Config['use_path_style_endpoint'],
|
||||
'credentials' => [
|
||||
'key' => $s3Config['key'],
|
||||
'secret' => $s3Config['secret'],
|
||||
],
|
||||
]);
|
||||
@file_put_contents($debugLog, date('Y-m-d H:i:s') . " - S3Client created\n", FILE_APPEND);
|
||||
|
||||
// Используем bucket из параметра, а не из конфига
|
||||
// Используем только расширение файла для имени временного файла, чтобы избежать "File name too long"
|
||||
$extension = '';
|
||||
if (!empty($fileName)) {
|
||||
// Декодируем URL-encoded имя файла, если это URL
|
||||
$decodedFileName = urldecode($fileName);
|
||||
// Извлекаем расширение из оригинального s3_key, если filename - это URL
|
||||
if (strpos($decodedFileName, '/') !== false) {
|
||||
// Если filename содержит путь, используем s3_key для расширения
|
||||
$extension = pathinfo($s3Key, PATHINFO_EXTENSION);
|
||||
} else {
|
||||
$extension = pathinfo($decodedFileName, PATHINFO_EXTENSION);
|
||||
}
|
||||
}
|
||||
if (empty($extension) && !empty($s3Key)) {
|
||||
$extension = pathinfo($s3Key, PATHINFO_EXTENSION);
|
||||
}
|
||||
|
||||
// Создаем короткое имя файла с расширением
|
||||
$tempFileName = uniqid('s3_') . (!empty($extension) ? '.' . $extension : '');
|
||||
$tempFile = sys_get_temp_dir() . '/' . $tempFileName;
|
||||
error_log("downloadS3File: Temp file path: {$tempFile}");
|
||||
@file_put_contents($debugLog, date('Y-m-d H:i:s') . " - Temp file path: {$tempFile}\n", FILE_APPEND);
|
||||
|
||||
// Скачиваем файл
|
||||
@file_put_contents($debugLog, date('Y-m-d H:i:s') . " - Calling getObject() - Bucket: {$s3Bucket}, Key: {$s3Key}\n", FILE_APPEND);
|
||||
$result = $awsClient->getObject([
|
||||
'Bucket' => $s3Bucket,
|
||||
'Key' => $s3Key,
|
||||
'SaveAs' => $tempFile
|
||||
]);
|
||||
error_log("downloadS3File: getObject() completed successfully");
|
||||
@file_put_contents($debugLog, date('Y-m-d H:i:s') . " - getObject() completed successfully\n", FILE_APPEND);
|
||||
|
||||
if (!file_exists($tempFile)) {
|
||||
error_log("downloadS3File: File was not created: {$tempFile}");
|
||||
return false;
|
||||
}
|
||||
|
||||
$fileSize = filesize($tempFile);
|
||||
if ($fileSize == 0) {
|
||||
error_log("downloadS3File: WARNING - File size is 0 bytes: {$tempFile}");
|
||||
// Не возвращаем false для пустого файла - возможно, это нормально
|
||||
}
|
||||
error_log("downloadS3File: Success - file size: {$fileSize} bytes");
|
||||
|
||||
// Сохраняем путь для последующей очистки
|
||||
self::$tempFiles[] = $tempFile;
|
||||
|
||||
return $tempFile;
|
||||
|
||||
} catch (\Aws\Exception\AwsException $e) {
|
||||
$errorMsg = "downloadS3File: AWS Exception - " . $e->getMessage();
|
||||
$errorMsg .= " | Error Code: " . $e->getAwsErrorCode();
|
||||
$errorMsg .= " | Request ID: " . $e->getAwsRequestId();
|
||||
$errorMsg .= " | Bucket: {$s3Bucket} | Key: {$s3Key}";
|
||||
error_log($errorMsg);
|
||||
@file_put_contents($debugLog, date('Y-m-d H:i:s') . " - AWS EXCEPTION: {$errorMsg}\n", FILE_APPEND);
|
||||
@file_put_contents('/tmp/s3_download_errors.log', date('Y-m-d H:i:s') . ' - ' . $errorMsg . "\n", FILE_APPEND);
|
||||
return false;
|
||||
} catch (Exception $e) {
|
||||
$errorMsg = "downloadS3File: Exception - " . $e->getMessage();
|
||||
$errorMsg .= " | Bucket: {$s3Bucket} | Key: {$s3Key}";
|
||||
error_log($errorMsg);
|
||||
error_log("downloadS3File: Stack trace - " . $e->getTraceAsString());
|
||||
@file_put_contents($debugLog, date('Y-m-d H:i:s') . " - EXCEPTION: {$errorMsg}\n", FILE_APPEND);
|
||||
@file_put_contents($debugLog, date('Y-m-d H:i:s') . " - Stack trace: " . $e->getTraceAsString() . "\n", FILE_APPEND);
|
||||
@file_put_contents('/tmp/s3_download_errors.log', date('Y-m-d H:i:s') . ' - ' . $errorMsg . "\n", FILE_APPEND);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Очистка временных файлов
|
||||
*/
|
||||
private static function cleanupTempFiles()
|
||||
{
|
||||
foreach (self::$tempFiles as $tempFile) {
|
||||
if (file_exists($tempFile)) {
|
||||
@unlink($tempFile);
|
||||
}
|
||||
}
|
||||
self::$tempFiles = [];
|
||||
}
|
||||
|
||||
public static function getDocs($record)
|
||||
{
|
||||
$module = 'Documents';
|
||||
@@ -14,33 +220,244 @@ class Vtiger_Base_Service
|
||||
$pager->set('limit', 1000);
|
||||
return $relation->getEntries($pager);
|
||||
}
|
||||
|
||||
/**
|
||||
* Получение документов из связанных сущностей (для проектов)
|
||||
*/
|
||||
public static function getRelatedDocs($projectId)
|
||||
{
|
||||
$adb = PearDatabase::getInstance();
|
||||
$docs = [];
|
||||
|
||||
// Получаем информацию о проекте и связанных контрагентах
|
||||
$query = 'SELECT
|
||||
p.linktoaccountscontacts as contactid,
|
||||
pcf.cf_1994 as accountid,
|
||||
pcf.cf_2274 as acc1,
|
||||
pcf.cf_2276 as acc2
|
||||
FROM vtiger_project p
|
||||
LEFT JOIN vtiger_projectcf pcf ON pcf.projectid = p.projectid
|
||||
LEFT JOIN vtiger_crmentity e ON e.crmid = p.projectid
|
||||
WHERE e.deleted = 0 AND p.projectid = ?';
|
||||
$result = $adb->pquery($query, array($projectId));
|
||||
|
||||
if ($adb->num_rows($result) == 0) {
|
||||
return $docs;
|
||||
}
|
||||
|
||||
$row = $adb->query_result_rowdata($result, 0);
|
||||
$contactId = $row['contactid'];
|
||||
$accountId = $row['accountid'];
|
||||
$acc1 = $row['acc1'];
|
||||
$acc2 = $row['acc2'];
|
||||
|
||||
// Собираем ID всех связанных сущностей
|
||||
$relatedIds = array_filter([$projectId, $contactId, $accountId, $acc1, $acc2]);
|
||||
|
||||
if (empty($relatedIds)) {
|
||||
return $docs;
|
||||
}
|
||||
|
||||
// Получаем все документы из связанных сущностей
|
||||
$placeholders = str_repeat('?,', count($relatedIds) - 1) . '?';
|
||||
$query = "SELECT
|
||||
n.notesid,
|
||||
n.title,
|
||||
n.filename,
|
||||
n.filelocationtype,
|
||||
n.s3_bucket,
|
||||
n.s3_key,
|
||||
r.crmid as related_to_id,
|
||||
CASE
|
||||
WHEN r.crmid = ? THEN 'Project'
|
||||
WHEN r.crmid = ? THEN 'Contact'
|
||||
WHEN r.crmid IN (?, ?, ?) THEN 'Account'
|
||||
ELSE 'Unknown'
|
||||
END as source_type
|
||||
FROM vtiger_senotesrel r
|
||||
LEFT JOIN vtiger_notes n ON n.notesid = r.notesid
|
||||
LEFT JOIN vtiger_crmentity e ON e.crmid = r.notesid
|
||||
WHERE r.crmid IN ($placeholders)
|
||||
AND e.deleted = 0
|
||||
AND n.filename IS NOT NULL
|
||||
ORDER BY r.crmid, n.title";
|
||||
|
||||
$params = array_merge([$projectId, $contactId, $accountId, $acc1, $acc2], $relatedIds);
|
||||
$result = $adb->pquery($query, $params);
|
||||
|
||||
while ($row = $adb->fetchByAssoc($result)) {
|
||||
$docs[] = $row;
|
||||
}
|
||||
|
||||
return $docs;
|
||||
}
|
||||
|
||||
public static function getPaths($docs = [])
|
||||
{
|
||||
$archived = 0;
|
||||
$archived = 0;
|
||||
$errors = [];
|
||||
$files = [];
|
||||
|
||||
// Отладочное логирование
|
||||
error_log("========================================");
|
||||
error_log("getPaths: Processing " . count($docs) . " documents");
|
||||
|
||||
foreach ($docs as $x) {
|
||||
if (empty($x->get('filename'))) {
|
||||
$errors[] = 'skip non-file docs';
|
||||
// Поддержка как Record Model, так и массива (для связанных документов)
|
||||
if (is_object($x)) {
|
||||
$filename = $x->get('filename');
|
||||
$filelocationtype = $x->get('filelocationtype');
|
||||
$title = $x->get('title');
|
||||
$notesid = $x->getId();
|
||||
|
||||
// ВСЕГДА получаем s3_bucket и s3_key напрямую из БД для Record Models,
|
||||
// так как эти поля могут отсутствовать в Record Model
|
||||
$adb = PearDatabase::getInstance();
|
||||
$dbResult = $adb->pquery(
|
||||
"SELECT s3_bucket, s3_key, filelocationtype FROM vtiger_notes WHERE notesid = ?",
|
||||
array($notesid)
|
||||
);
|
||||
if ($adb->num_rows($dbResult) > 0) {
|
||||
$dbRow = $adb->fetchByAssoc($dbResult);
|
||||
$s3Bucket = $dbRow['s3_bucket'] ?? null;
|
||||
$s3Key = $dbRow['s3_key'] ?? null;
|
||||
// Используем filelocationtype из БД, если он есть
|
||||
if (!empty($dbRow['filelocationtype'])) {
|
||||
$filelocationtype = $dbRow['filelocationtype'];
|
||||
}
|
||||
} else {
|
||||
$s3Bucket = null;
|
||||
$s3Key = null;
|
||||
}
|
||||
} else {
|
||||
// Массив из getRelatedDocs
|
||||
$filename = $x['filename'] ?? null;
|
||||
$filelocationtype = $x['filelocationtype'] ?? null;
|
||||
$s3Bucket = $x['s3_bucket'] ?? null;
|
||||
$s3Key = $x['s3_key'] ?? null;
|
||||
$title = $x['title'] ?? '';
|
||||
$notesid = $x['notesid'] ?? null;
|
||||
}
|
||||
|
||||
$logMsg = "getPaths: Processing doc notesid={$notesid}, filename=" . ($filename ?? 'NULL') . ", filelocationtype=" . ($filelocationtype ?? 'NULL') . ", s3_bucket=" . ($s3Bucket ?? 'NULL') . ", s3_key=" . ($s3Key ?? 'NULL');
|
||||
error_log($logMsg);
|
||||
|
||||
// Для S3 файлов filename может быть URL, это нормально
|
||||
// Проверяем только что filename не пустой ИЛИ есть s3_key
|
||||
if (empty($filename) && empty($s3Key)) {
|
||||
$errors[] = 'skip non-file docs (notesid=' . ($notesid ?? 'unknown') . ')';
|
||||
error_log("getPaths: SKIP - empty filename and s3_key for notesid=" . ($notesid ?? 'unknown'));
|
||||
continue;
|
||||
}
|
||||
|
||||
// Проверяем условия для S3
|
||||
$isS3File = ($filelocationtype == 'E' && !empty($s3Bucket) && !empty($s3Key));
|
||||
error_log("getPaths: CHECK S3 - filelocationtype='{$filelocationtype}' == 'E': " . (($filelocationtype == 'E') ? 'YES' : 'NO') . ", s3Bucket empty: " . (empty($s3Bucket) ? 'YES' : 'NO') . ", s3Key empty: " . (empty($s3Key) ? 'YES' : 'NO') . ", isS3File: " . ($isS3File ? 'YES' : 'NO'));
|
||||
|
||||
// Проверяем, файл ли это в S3
|
||||
if ($isS3File) {
|
||||
// Файл в S3 - скачиваем во временную папку
|
||||
// Определяем расширение файла
|
||||
$extension = '';
|
||||
if (!empty($filename)) {
|
||||
$extension = pathinfo($filename, PATHINFO_EXTENSION);
|
||||
}
|
||||
if (empty($extension) && !empty($s3Key)) {
|
||||
$extension = pathinfo($s3Key, PATHINFO_EXTENSION);
|
||||
}
|
||||
|
||||
$displayName = !empty($title)
|
||||
? $title . (!empty($extension) ? '.' . $extension : '')
|
||||
: basename($s3Key);
|
||||
|
||||
$tempPath = self::downloadS3File($s3Bucket, $s3Key, $displayName);
|
||||
|
||||
if ($tempPath && file_exists($tempPath)) {
|
||||
$archived++;
|
||||
$files[] = [
|
||||
'name' => $displayName,
|
||||
'path' => $tempPath,
|
||||
'is_temp' => true
|
||||
];
|
||||
} else {
|
||||
$errors[] = "S3 file download failed: {$s3Key}";
|
||||
}
|
||||
} else {
|
||||
// Локальный файл - используем старую логику
|
||||
// НО: если это массив из getRelatedDocs и у него filelocationtype != 'E',
|
||||
// значит это не S3 файл, но и не локальный (возможно, внешняя ссылка)
|
||||
// Пропускаем такие файлы или пытаемся обработать как локальные
|
||||
if (is_object($x)) {
|
||||
// Record Model - получаем детали файла
|
||||
$details = $x->getFileDetails();
|
||||
if (empty($details) || empty($details['path'])) {
|
||||
$errors[] = "Cannot get file details for Record Model: {$notesid}";
|
||||
error_log("getPaths: Cannot get file details for notesid={$notesid}");
|
||||
continue;
|
||||
}
|
||||
$name = $details['attachmentsid'] . '_' . $details['storedname'];
|
||||
$fullPath = $details['path'] . $name;
|
||||
} else {
|
||||
// Массив из getRelatedDocs - если это не S3, значит локальный файл
|
||||
// Пытаемся создать Record Model для получения пути
|
||||
if (!empty($x['notesid'])) {
|
||||
try {
|
||||
$docRecord = Vtiger_Record_Model::getInstanceById($x['notesid'], 'Documents');
|
||||
if ($docRecord) {
|
||||
$details = $docRecord->getFileDetails();
|
||||
if (empty($details) || empty($details['path'])) {
|
||||
$errors[] = "Cannot get file details for document: {$x['notesid']}";
|
||||
error_log("getPaths: Cannot get file details for notesid={$x['notesid']}");
|
||||
continue;
|
||||
}
|
||||
$name = $details['attachmentsid'] . '_' . $details['storedname'];
|
||||
$fullPath = $details['path'] . $name;
|
||||
} else {
|
||||
$errors[] = "Cannot create Record Model for document: {$x['notesid']}";
|
||||
error_log("getPaths: Cannot create Record Model for notesid={$x['notesid']}");
|
||||
continue;
|
||||
}
|
||||
} catch (Exception $e) {
|
||||
$errors[] = "Error creating Record Model: {$e->getMessage()}";
|
||||
error_log("getPaths: Exception creating Record Model: " . $e->getMessage());
|
||||
continue;
|
||||
}
|
||||
} else {
|
||||
$errors[] = "Local file without Record Model and notesid: {$filename}";
|
||||
error_log("getPaths: Local file without notesid: {$filename}");
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
if (empty($fullPath)) {
|
||||
$errors[] = "Empty file path for notesid: {$notesid}";
|
||||
error_log("getPaths: Empty file path for notesid={$notesid}");
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!file_exists($fullPath)) {
|
||||
$errors[] = "{$fullPath} is missing!";
|
||||
error_log("getPaths: File not found: {$fullPath}");
|
||||
continue;
|
||||
}
|
||||
|
||||
$details = $x->getFileDetails();
|
||||
$name = $details['attachmentsid'] . '_' . $details['storedname'];
|
||||
$fullPath = $details['path'] . $name;
|
||||
if (!file_exists($fullPath)) {
|
||||
$errors[] = "{$fullPath} is missing!";
|
||||
continue;
|
||||
$archived++;
|
||||
$files[] = [
|
||||
'name' => $name,
|
||||
'path' => $fullPath,
|
||||
'is_temp' => false
|
||||
];
|
||||
error_log("getPaths: Added local file: {$name}");
|
||||
}
|
||||
}
|
||||
|
||||
$archived++;
|
||||
$files[] = [
|
||||
'name' => $name,
|
||||
'path' => $fullPath
|
||||
];
|
||||
};
|
||||
|
||||
$resultMsg = "getPaths: Result - archived={$archived}, files=" . count($files) . ", errors=" . count($errors);
|
||||
error_log($resultMsg);
|
||||
if (count($errors) > 0) {
|
||||
$errorsMsg = "getPaths: Errors: " . implode('; ', array_slice($errors, 0, 10));
|
||||
error_log($errorsMsg);
|
||||
}
|
||||
|
||||
return compact(
|
||||
'files',
|
||||
'errors',
|
||||
@@ -62,13 +479,19 @@ class Vtiger_Base_Service
|
||||
|
||||
$files = self::getPaths($docs);
|
||||
if ($files['archived'] == 0) {
|
||||
self::cleanupTempFiles();
|
||||
return false;
|
||||
}
|
||||
|
||||
$ts = date('Ymd_His_') . array_pop(explode('.', microtime(1)));
|
||||
$zipFile = "cache/{$id}_documents_{$ts}.zip";
|
||||
$zip = new ZipArchive();
|
||||
$zip->open($zipFile, ZipArchive::CREATE);
|
||||
$result = $zip->open($zipFile, ZipArchive::CREATE);
|
||||
if (!$result) {
|
||||
self::cleanupTempFiles();
|
||||
return false;
|
||||
}
|
||||
|
||||
foreach ($files['files'] as $x) {
|
||||
$zip->addFile($x['path'], $x['name']);
|
||||
}
|
||||
@@ -76,9 +499,12 @@ class Vtiger_Base_Service
|
||||
|
||||
$size = filesize($zipFile);
|
||||
if ($size == 0) {
|
||||
//exit('Zero file');
|
||||
self::cleanupTempFiles();
|
||||
return false;
|
||||
}
|
||||
|
||||
// Очищаем временные файлы после успешного создания архива
|
||||
self::cleanupTempFiles();
|
||||
|
||||
return [
|
||||
'total' => count($docs),
|
||||
@@ -91,58 +517,131 @@ class Vtiger_Base_Service
|
||||
|
||||
public static function getArchive($id)
|
||||
{
|
||||
$module = 'Documents';
|
||||
$record = Vtiger_Record_Model::getInstanceById($id);
|
||||
if (! $record) {
|
||||
return false;
|
||||
// Логирование через error_log (более надежно)
|
||||
error_log("========================================");
|
||||
error_log("getArchive: START for project ID={$id}");
|
||||
|
||||
try {
|
||||
$record = Vtiger_Record_Model::getInstanceById($id);
|
||||
if (! $record) {
|
||||
error_log("getArchive: Record not found for ID={$id}");
|
||||
return self::response('Record not found');
|
||||
}
|
||||
|
||||
$moduleName = $record->getModuleName();
|
||||
error_log("getArchive: Module name={$moduleName}");
|
||||
$allDocs = [];
|
||||
|
||||
// Получаем документы из самой записи
|
||||
$docs = self::getDocs($record);
|
||||
$docsCount = count($docs);
|
||||
error_log("getArchive: Found {$docsCount} docs from getDocs()");
|
||||
foreach ($docs as $doc) {
|
||||
$allDocs[] = $doc;
|
||||
}
|
||||
|
||||
// Для проектов - добавляем документы из связанных сущностей
|
||||
if ($moduleName == 'Project') {
|
||||
error_log("getArchive: Getting related docs for Project");
|
||||
$relatedDocs = self::getRelatedDocs($id);
|
||||
$relatedCount = count($relatedDocs);
|
||||
error_log("getArchive: Found {$relatedCount} related docs");
|
||||
|
||||
// Собираем notesid уже добавленных документов, чтобы избежать дубликатов
|
||||
$addedNotesIds = [];
|
||||
foreach ($allDocs as $doc) {
|
||||
if (is_object($doc)) {
|
||||
$addedNotesIds[] = $doc->getId();
|
||||
}
|
||||
}
|
||||
|
||||
// Добавляем только те документы, которых еще нет
|
||||
foreach ($relatedDocs as $relatedDoc) {
|
||||
if (!in_array($relatedDoc['notesid'], $addedNotesIds)) {
|
||||
$allDocs[] = $relatedDoc;
|
||||
$addedNotesIds[] = $relatedDoc['notesid'];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$totalDocs = count($allDocs);
|
||||
error_log("getArchive: Total docs to process: {$totalDocs}");
|
||||
|
||||
if ($totalDocs == 0) {
|
||||
error_log("getArchive: No documents found, returning error");
|
||||
return self::response('Record has no documents');
|
||||
}
|
||||
|
||||
error_log("getArchive: Calling getPaths() with {$totalDocs} docs");
|
||||
$files = self::getPaths($allDocs);
|
||||
$archivedCount = $files['archived'];
|
||||
$errorsCount = count($files['errors']);
|
||||
error_log("getArchive: getPaths returned archived={$archivedCount}, errors={$errorsCount}");
|
||||
|
||||
// Выводим первые несколько ошибок
|
||||
if ($errorsCount > 0) {
|
||||
$firstErrors = array_slice($files['errors'], 0, 5);
|
||||
error_log("getArchive: First errors: " . implode('; ', $firstErrors));
|
||||
}
|
||||
|
||||
if ($files['archived'] == 0) {
|
||||
// Очищаем временные файлы перед выходом
|
||||
self::cleanupTempFiles();
|
||||
$errorDetails = implode('; ', array_slice($files['errors'], 0, 10));
|
||||
error_log("getArchive: Nothing to archive - errors: " . $errorDetails);
|
||||
error_log("getArchive: Total docs processed: {$totalDocs}, archived: {$archivedCount}, errors: {$errorsCount}");
|
||||
// Возвращаем детальную информацию об ошибках для отладки
|
||||
return self::response([
|
||||
'message' => 'Nothing to archive',
|
||||
'total_docs' => $totalDocs,
|
||||
'archived' => $archivedCount,
|
||||
'errors_count' => $errorsCount,
|
||||
'errors' => array_slice($files['errors'], 0, 10)
|
||||
]);
|
||||
}
|
||||
|
||||
$ts = date('Ymd_His_') . array_pop(explode('.', microtime(1)));
|
||||
$archive = "{$id}_documents_{$ts}.zip";
|
||||
$zipFile = "cache/{$archive}";
|
||||
$zip = new ZipArchive();
|
||||
$result = $zip->open($zipFile, ZipArchive::CREATE|ZipArchive::OVERWRITE);
|
||||
if (! $result) {
|
||||
self::cleanupTempFiles();
|
||||
return self::response('Unable to create file');
|
||||
}
|
||||
|
||||
foreach ($files['files'] as $x) {
|
||||
$zip->addFile($x['path'], $x['name']);
|
||||
}
|
||||
|
||||
$result = $zip->close();
|
||||
if (! $result) {
|
||||
self::cleanupTempFiles();
|
||||
return self::response('Unable to write file');
|
||||
}
|
||||
|
||||
$size = filesize($zipFile);
|
||||
|
||||
if ($size == 0) {
|
||||
self::cleanupTempFiles();
|
||||
return self::response('Error creating archive');
|
||||
}
|
||||
|
||||
// Очищаем временные файлы после успешного создания архива
|
||||
self::cleanupTempFiles();
|
||||
|
||||
header('Content-disposition: attachment; filename='.$archive);
|
||||
header('Content-type: application/zip');
|
||||
readfile($zipFile);
|
||||
//unlink($zipFile); // Можно оставить для отладки или удалить сразу
|
||||
exit();
|
||||
|
||||
} catch (Exception $e) {
|
||||
error_log("getArchive: Exception - " . $e->getMessage());
|
||||
error_log("getArchive: Stack trace - " . $e->getTraceAsString());
|
||||
self::cleanupTempFiles();
|
||||
return self::response('Error: ' . $e->getMessage());
|
||||
}
|
||||
|
||||
$docs = self::getDocs($record);
|
||||
if (count($docs) == 0) {
|
||||
return self::response('Record has no documents');
|
||||
}
|
||||
|
||||
$files = self::getPaths($docs);
|
||||
if ($files['archived'] == 0) {
|
||||
return self::response('Nothing to archive');
|
||||
}
|
||||
|
||||
$ts = date('Ymd_His_') . array_pop(explode('.', microtime(1)));
|
||||
$archive = "{$id}_documents_{$ts}.zip";
|
||||
$zipFile = "cache/{$archive}";
|
||||
$zip = new ZipArchive();
|
||||
$result = $zip->open($zipFile, ZipArchive::CREATE|ZipArchive::OVERWRITE);
|
||||
if (! $result) {
|
||||
return self::response('Unable to create file');
|
||||
}
|
||||
|
||||
foreach ($files['files'] as $x) {
|
||||
$zip->addFile($x['path'], $x['name']);
|
||||
}
|
||||
|
||||
$result = $zip->close();
|
||||
if (! $result) {
|
||||
return self::response('Unable to write file');
|
||||
}
|
||||
|
||||
$size = filesize($zipFile);
|
||||
|
||||
if ($size == 0) {
|
||||
//exit('Zero file');
|
||||
return self::response('Error creating archive');
|
||||
}
|
||||
|
||||
header('Content-disposition: attachment; filename='.$archive);
|
||||
header('Content-type: application/zip');
|
||||
readfile($zipFile);
|
||||
//unlink($zipFile);
|
||||
exit();
|
||||
|
||||
return self::response([
|
||||
'file' => $zipName,
|
||||
'docsCount' => count($docs),
|
||||
'size' => $size,
|
||||
]);
|
||||
}
|
||||
|
||||
public static function response($data)
|
||||
|
||||
213
move_files_from_temp.php
Normal file
213
move_files_from_temp.php
Normal file
@@ -0,0 +1,213 @@
|
||||
<?php
|
||||
error_reporting(E_ALL);
|
||||
ini_set('display_errors', 1);
|
||||
|
||||
require_once '/var/www/fastuser/data/www/crm.clientright.ru/vendor/autoload.php';
|
||||
require_once '/var/www/fastuser/data/www/crm.clientright.ru/config.inc.php';
|
||||
|
||||
$config = require '/var/www/fastuser/data/www/crm.clientright.ru/crm_extensions/file_storage/config.php';
|
||||
$projectId = 384256;
|
||||
$s3Bucket = $config['s3']['bucket'];
|
||||
|
||||
echo "Перемещение файлов проекта $projectId из temp/ в правильную структуру\n";
|
||||
echo str_repeat("=", 80) . "\n\n";
|
||||
|
||||
try {
|
||||
// Инициализация S3 клиента
|
||||
$s3Client = new \Aws\S3\S3Client([
|
||||
'version' => 'latest',
|
||||
'region' => $config['s3']['region'],
|
||||
'endpoint' => $config['s3']['endpoint'],
|
||||
'use_path_style_endpoint' => true,
|
||||
'credentials' => [
|
||||
'key' => $config['s3']['key'],
|
||||
'secret' => $config['s3']['secret'],
|
||||
],
|
||||
'suppress_php_deprecation_warning' => true
|
||||
]);
|
||||
|
||||
// Подключение к БД
|
||||
$pdo = new PDO(
|
||||
"mysql:host={$dbconfig['db_server']};port=3306;dbname={$dbconfig['db_name']};charset=utf8",
|
||||
$dbconfig['db_username'],
|
||||
$dbconfig['db_password'],
|
||||
[PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION]
|
||||
);
|
||||
|
||||
// Получаем все файлы из temp/384256/
|
||||
echo "1. Поиск файлов в temp/$projectId/...\n";
|
||||
$tempFiles = [];
|
||||
$objects = $s3Client->listObjectsV2([
|
||||
'Bucket' => $s3Bucket,
|
||||
'Prefix' => "temp/$projectId/",
|
||||
'MaxKeys' => 1000
|
||||
]);
|
||||
|
||||
if (isset($objects['Contents'])) {
|
||||
foreach ($objects['Contents'] as $object) {
|
||||
$key = $object['Key'];
|
||||
// Пропускаем папки
|
||||
if (substr($key, -1) !== '/') {
|
||||
$tempFiles[] = $key;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
echo " Найдено файлов в temp/: " . count($tempFiles) . "\n\n";
|
||||
|
||||
if (empty($tempFiles)) {
|
||||
die("Файлы не найдены в temp/$projectId/\n");
|
||||
}
|
||||
|
||||
// Получаем документы проекта из БД
|
||||
echo "2. Получение документов проекта из БД...\n";
|
||||
$stmt = $pdo->prepare('
|
||||
SELECT n.notesid, n.title, n.s3_key, n.filename
|
||||
FROM vtiger_notes n
|
||||
INNER JOIN vtiger_crmentity e ON e.crmid = n.notesid
|
||||
INNER JOIN vtiger_senotesrel snr ON snr.notesid = n.notesid
|
||||
WHERE snr.crmid = ? AND e.deleted = 0 AND n.filelocationtype = "E"
|
||||
ORDER BY n.notesid ASC
|
||||
');
|
||||
$stmt->execute([$projectId]);
|
||||
$dbDocs = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||
|
||||
echo " Найдено документов в БД: " . count($dbDocs) . "\n\n";
|
||||
|
||||
// Сопоставляем файлы из temp/ с документами в БД
|
||||
echo "3. Сопоставление файлов...\n";
|
||||
$mappings = [];
|
||||
|
||||
foreach ($tempFiles as $tempFile) {
|
||||
$basename = basename($tempFile);
|
||||
// Извлекаем ID из имени файла (например, 384260 из 384260_8_Dogovor...)
|
||||
if (preg_match('/^(\d+)_/', $basename, $matches)) {
|
||||
$fileDocId = $matches[1];
|
||||
|
||||
// Ищем соответствующий документ в БД
|
||||
// Обычно файл с ID 384260 соответствует документу 384259 (ID файла = ID документа + 1)
|
||||
// Но лучше искать по ближайшему ID
|
||||
$matchedDoc = null;
|
||||
foreach ($dbDocs as $doc) {
|
||||
// Проверяем разные варианты соответствия
|
||||
if ($doc['notesid'] == $fileDocId - 1 ||
|
||||
$doc['notesid'] == $fileDocId ||
|
||||
abs($doc['notesid'] - $fileDocId) <= 2) {
|
||||
// Дополнительная проверка по названию
|
||||
$docTitleLower = mb_strtolower($doc['title']);
|
||||
$fileNameLower = mb_strtolower($basename);
|
||||
|
||||
// Проверяем совпадение по ключевым словам
|
||||
$keywords = ['dogovor', 'podtverzhdenie', 'skrin', 'zayavlenie', '8', '9', '10', '7'];
|
||||
$foundKeyword = false;
|
||||
foreach ($keywords as $keyword) {
|
||||
if (strpos($docTitleLower, $keyword) !== false && strpos($fileNameLower, $keyword) !== false) {
|
||||
$foundKeyword = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if ($foundKeyword || abs($doc['notesid'] - $fileDocId) <= 1) {
|
||||
$matchedDoc = $doc;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ($matchedDoc) {
|
||||
$mappings[] = [
|
||||
'temp_file' => $tempFile,
|
||||
'doc_id' => $matchedDoc['notesid'],
|
||||
'doc_title' => $matchedDoc['title'],
|
||||
'target_s3_key' => $matchedDoc['s3_key'],
|
||||
'current_s3_key' => $matchedDoc['s3_key']
|
||||
];
|
||||
echo " ✅ {$basename} -> Документ {$matchedDoc['notesid']}: {$matchedDoc['title']}\n";
|
||||
} else {
|
||||
echo " ⚠️ {$basename} -> Не найден соответствующий документ (ID файла: $fileDocId)\n";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
echo "\n Всего сопоставлено: " . count($mappings) . " файлов\n\n";
|
||||
|
||||
if (empty($mappings)) {
|
||||
die("Не удалось сопоставить файлы с документами\n");
|
||||
}
|
||||
|
||||
// Перемещаем файлы
|
||||
echo "4. Перемещение файлов в S3...\n";
|
||||
$moved = 0;
|
||||
$errors = 0;
|
||||
|
||||
foreach ($mappings as $mapping) {
|
||||
$sourceKey = $mapping['temp_file'];
|
||||
$targetKey = $mapping['target_s3_key'];
|
||||
$docId = $mapping['doc_id'];
|
||||
|
||||
echo " Перемещение: " . basename($sourceKey) . "\n";
|
||||
echo " Из: $sourceKey\n";
|
||||
echo " В: $targetKey\n";
|
||||
|
||||
try {
|
||||
// Проверяем, существует ли целевой путь (если да, пропускаем)
|
||||
try {
|
||||
$s3Client->headObject([
|
||||
'Bucket' => $s3Bucket,
|
||||
'Key' => $targetKey
|
||||
]);
|
||||
echo " ⚠️ Целевой файл уже существует, пропускаем\n\n";
|
||||
continue;
|
||||
} catch (\Aws\Exception\AwsException $e) {
|
||||
if ($e->getAwsErrorCode() != 'NotFound') {
|
||||
throw $e;
|
||||
}
|
||||
}
|
||||
|
||||
// Копируем файл в новое место
|
||||
$s3Client->copyObject([
|
||||
'Bucket' => $s3Bucket,
|
||||
'CopySource' => $s3Bucket . '/' . $sourceKey,
|
||||
'Key' => $targetKey,
|
||||
'MetadataDirective' => 'COPY'
|
||||
]);
|
||||
|
||||
echo " ✅ Файл скопирован\n";
|
||||
|
||||
// Удаляем исходный файл из temp/
|
||||
$s3Client->deleteObject([
|
||||
'Bucket' => $s3Bucket,
|
||||
'Key' => $sourceKey
|
||||
]);
|
||||
|
||||
echo " ✅ Исходный файл удален из temp/\n";
|
||||
|
||||
// Обновляем filename в БД (если нужно)
|
||||
$newFilename = "https://s3.twcstorage.ru/{$s3Bucket}/" . rawurlencode($targetKey);
|
||||
$updateStmt = $pdo->prepare('UPDATE vtiger_notes SET filename = ? WHERE notesid = ?');
|
||||
$updateStmt->execute([$newFilename, $docId]);
|
||||
|
||||
echo " ✅ Путь в БД обновлен\n";
|
||||
$moved++;
|
||||
|
||||
} catch (\Aws\Exception\AwsException $e) {
|
||||
echo " ❌ Ошибка: " . $e->getMessage() . " (Code: " . $e->getAwsErrorCode() . ")\n";
|
||||
$errors++;
|
||||
} catch (Exception $e) {
|
||||
echo " ❌ Ошибка: " . $e->getMessage() . "\n";
|
||||
$errors++;
|
||||
}
|
||||
|
||||
echo "\n";
|
||||
}
|
||||
|
||||
echo str_repeat("=", 80) . "\n";
|
||||
echo "РЕЗУЛЬТАТЫ:\n";
|
||||
echo " Перемещено файлов: $moved\n";
|
||||
echo " Ошибок: $errors\n";
|
||||
|
||||
} catch (Exception $e) {
|
||||
echo "КРИТИЧЕСКАЯ ОШИБКА: " . $e->getMessage() . "\n";
|
||||
echo "Trace: " . $e->getTraceAsString() . "\n";
|
||||
}
|
||||
|
||||
165
move_project_394091_files.php
Normal file
165
move_project_394091_files.php
Normal file
@@ -0,0 +1,165 @@
|
||||
<?php
|
||||
/**
|
||||
* Скрипт для перемещения файлов проекта 394091 в правильное место
|
||||
*/
|
||||
|
||||
error_reporting(E_ALL);
|
||||
ini_set('display_errors', 1);
|
||||
|
||||
require_once '/var/www/fastuser/data/www/crm.clientright.ru/vendor/autoload.php';
|
||||
require_once '/var/www/fastuser/data/www/crm.clientright.ru/config.inc.php';
|
||||
|
||||
$config = require '/var/www/fastuser/data/www/crm.clientright.ru/crm_extensions/file_storage/config.php';
|
||||
$s3Bucket = $config['s3']['bucket'];
|
||||
|
||||
$projectId = 394091;
|
||||
|
||||
// Маппинг: текущий путь => правильный путь
|
||||
$filesToMove = [
|
||||
// 394094 - Договор
|
||||
'crm2/CRM_Active_Files/Documents/394094/doc_394094_d583b5d6.pdf' => [
|
||||
'new_path' => 'crm2/CRM_Active_Files/Documents/Project/Згурский_ООО_РЕНТСОФТ_394091/Договор_394094.pdf',
|
||||
'doc_id' => 394094,
|
||||
],
|
||||
// 394096 - Подтверждение оплаты
|
||||
'crm2/CRM_Active_Files/Documents/394096/doc_394096_ce9e6bdc.pdf' => [
|
||||
'new_path' => 'crm2/CRM_Active_Files/Documents/Project/Згурский_ООО_РЕНТСОФТ_394091/Подтверждение_оплаты_394096.pdf',
|
||||
'doc_id' => 394096,
|
||||
],
|
||||
// 394100 - Ответ на претензию
|
||||
'crm2/CRM_Active_Files/Documents/394100/doc_394100_3f15e3c1.pdf' => [
|
||||
'new_path' => 'crm2/CRM_Active_Files/Documents/Project/Згурский_ООО_РЕНТСОФТ_394091/Ответ_на_претензию_394100.pdf',
|
||||
'doc_id' => 394100,
|
||||
],
|
||||
// 394105 - Заявление потребителя
|
||||
'crm2/CRM_Active_Files/Documents/394105/potrebitelya_Zgurskiy_1.pdf' => [
|
||||
'new_path' => 'crm2/CRM_Active_Files/Documents/Project/Згурский_ООО_РЕНТСОФТ_394091/7_заявление_потребителя_394105.pdf',
|
||||
'doc_id' => 394105,
|
||||
],
|
||||
];
|
||||
|
||||
echo "=== ПЕРЕМЕЩЕНИЕ ФАЙЛОВ ПРОЕКТА {$projectId} ===\n";
|
||||
echo str_repeat("=", 80) . "\n\n";
|
||||
|
||||
$dryRun = isset($argv[1]) && $argv[1] === '--dry-run';
|
||||
|
||||
if ($dryRun) {
|
||||
echo "⚠️ РЕЖИМ ПРОВЕРКИ (dry-run) - файлы не будут перемещены\n\n";
|
||||
}
|
||||
|
||||
try {
|
||||
$s3Client = new \Aws\S3\S3Client([
|
||||
'version' => 'latest',
|
||||
'region' => $config['s3']['region'],
|
||||
'endpoint' => $config['s3']['endpoint'],
|
||||
'use_path_style_endpoint' => true,
|
||||
'credentials' => [
|
||||
'key' => $config['s3']['key'],
|
||||
'secret' => $config['s3']['secret'],
|
||||
],
|
||||
'suppress_php_deprecation_warning' => true
|
||||
]);
|
||||
|
||||
$db = PearDatabase::getInstance();
|
||||
|
||||
$stats = [
|
||||
'total' => count($filesToMove),
|
||||
'moved' => 0,
|
||||
'skipped' => 0,
|
||||
'failed' => 0,
|
||||
'errors' => [],
|
||||
];
|
||||
|
||||
foreach ($filesToMove as $oldPath => $info) {
|
||||
$newPath = $info['new_path'];
|
||||
$docId = $info['doc_id'];
|
||||
|
||||
echo "Документ ID: {$docId}\n";
|
||||
echo " Старый путь: {$oldPath}\n";
|
||||
echo " Новый путь: {$newPath}\n";
|
||||
|
||||
// Проверяем существование старого файла
|
||||
if (!$s3Client->doesObjectExist($s3Bucket, $oldPath)) {
|
||||
echo " ⚠️ Старый файл не найден, пропускаем\n\n";
|
||||
$stats['skipped']++;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Проверяем, не существует ли уже новый файл
|
||||
if ($s3Client->doesObjectExist($s3Bucket, $newPath)) {
|
||||
echo " ⚠️ Новый файл уже существует, пропускаем\n\n";
|
||||
$stats['skipped']++;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!$dryRun) {
|
||||
try {
|
||||
// Копируем файл в новое место
|
||||
$s3Client->copyObject([
|
||||
'Bucket' => $s3Bucket,
|
||||
'Key' => $newPath,
|
||||
'CopySource' => "{$s3Bucket}/{$oldPath}",
|
||||
]);
|
||||
echo " ✅ Файл скопирован в новое место\n";
|
||||
|
||||
// Удаляем старый файл
|
||||
$s3Client->deleteObject([
|
||||
'Bucket' => $s3Bucket,
|
||||
'Key' => $oldPath,
|
||||
]);
|
||||
echo " ✅ Старый файл удален\n";
|
||||
|
||||
// Обновляем БД
|
||||
$newFilename = 'https://s3.twcstorage.ru/' . $s3Bucket . '/' . $newPath;
|
||||
$db->pquery("
|
||||
UPDATE vtiger_notes
|
||||
SET s3_key = ?, filename = ?
|
||||
WHERE notesid = ?
|
||||
", array($newPath, $newFilename, $docId));
|
||||
|
||||
echo " ✅ БД обновлена\n";
|
||||
$stats['moved']++;
|
||||
|
||||
sleep(1); // Пауза между операциями
|
||||
|
||||
} catch (Exception $e) {
|
||||
echo " ❌ Ошибка: " . $e->getMessage() . "\n";
|
||||
$stats['failed']++;
|
||||
$stats['errors'][] = "{$oldPath}: " . $e->getMessage();
|
||||
}
|
||||
} else {
|
||||
echo " ⏸️ Будет перемещен (dry-run)\n";
|
||||
$stats['moved']++;
|
||||
}
|
||||
|
||||
echo "\n";
|
||||
}
|
||||
|
||||
// Итоги
|
||||
echo str_repeat("=", 80) . "\n";
|
||||
echo "ИТОГИ:\n";
|
||||
echo "Всего файлов: {$stats['total']}\n";
|
||||
|
||||
if (!$dryRun) {
|
||||
echo "Перемещено: {$stats['moved']}\n";
|
||||
echo "Пропущено: {$stats['skipped']}\n";
|
||||
echo "Ошибок: {$stats['failed']}\n";
|
||||
} else {
|
||||
echo "Будет перемещено: {$stats['moved']}\n";
|
||||
echo "Будет пропущено: {$stats['skipped']}\n";
|
||||
}
|
||||
|
||||
if (!empty($stats['errors'])) {
|
||||
echo "\nОшибки:\n";
|
||||
foreach ($stats['errors'] as $error) {
|
||||
echo " - {$error}\n";
|
||||
}
|
||||
}
|
||||
|
||||
echo "\n=== ГОТОВО ===\n";
|
||||
|
||||
} catch (Exception $e) {
|
||||
echo "❌ Критическая ошибка: " . $e->getMessage() . "\n";
|
||||
exit(1);
|
||||
}
|
||||
|
||||
126
move_project_394091_simple.php
Normal file
126
move_project_394091_simple.php
Normal file
@@ -0,0 +1,126 @@
|
||||
<?php
|
||||
/**
|
||||
* Упрощенный скрипт для перемещения файлов проекта 394091
|
||||
*/
|
||||
|
||||
error_reporting(E_ALL);
|
||||
ini_set('display_errors', 1);
|
||||
|
||||
require_once '/var/www/fastuser/data/www/crm.clientright.ru/vendor/autoload.php';
|
||||
$config = require '/var/www/fastuser/data/www/crm.clientright.ru/crm_extensions/file_storage/config.php';
|
||||
|
||||
$s3Bucket = $config['s3']['bucket'];
|
||||
|
||||
// Маппинг: текущий путь => правильный путь
|
||||
$filesToMove = [
|
||||
'crm2/CRM_Active_Files/Documents/394094/doc_394094_d583b5d6.pdf' => 'crm2/CRM_Active_Files/Documents/Project/Згурский_ООО_РЕНТСОФТ_394091/Договор_394094.pdf',
|
||||
'crm2/CRM_Active_Files/Documents/394096/doc_394096_ce9e6bdc.pdf' => 'crm2/CRM_Active_Files/Documents/Project/Згурский_ООО_РЕНТСОФТ_394091/Подтверждение_оплаты_394096.pdf',
|
||||
'crm2/CRM_Active_Files/Documents/394100/doc_394100_3f15e3c1.pdf' => 'crm2/CRM_Active_Files/Documents/Project/Згурский_ООО_РЕНТСОФТ_394091/Ответ_на_претензию_394100.pdf',
|
||||
'crm2/CRM_Active_Files/Documents/394105/potrebitelya_Zgurskiy_1.pdf' => 'crm2/CRM_Active_Files/Documents/Project/Згурский_ООО_РЕНТСОФТ_394091/7_заявление_потребителя_394105.pdf',
|
||||
];
|
||||
|
||||
echo "=== ПЕРЕМЕЩЕНИЕ ФАЙЛОВ ПРОЕКТА 394091 ===\n";
|
||||
echo str_repeat("=", 80) . "\n\n";
|
||||
|
||||
$dryRun = isset($argv[1]) && $argv[1] === '--dry-run';
|
||||
|
||||
if ($dryRun) {
|
||||
echo "⚠️ РЕЖИМ ПРОВЕРКИ (dry-run)\n\n";
|
||||
}
|
||||
|
||||
try {
|
||||
$s3Client = new \Aws\S3\S3Client([
|
||||
'version' => 'latest',
|
||||
'region' => $config['s3']['region'],
|
||||
'endpoint' => $config['s3']['endpoint'],
|
||||
'use_path_style_endpoint' => true,
|
||||
'credentials' => [
|
||||
'key' => $config['s3']['key'],
|
||||
'secret' => $config['s3']['secret'],
|
||||
],
|
||||
'suppress_php_deprecation_warning' => true
|
||||
]);
|
||||
|
||||
$stats = [
|
||||
'total' => count($filesToMove),
|
||||
'moved' => 0,
|
||||
'skipped' => 0,
|
||||
'failed' => 0,
|
||||
];
|
||||
|
||||
foreach ($filesToMove as $oldPath => $newPath) {
|
||||
$filename = basename($newPath);
|
||||
echo "Файл: {$filename}\n";
|
||||
echo " От: {$oldPath}\n";
|
||||
echo " К: {$newPath}\n";
|
||||
|
||||
// Проверяем существование старого файла
|
||||
if (!$s3Client->doesObjectExist($s3Bucket, $oldPath)) {
|
||||
echo " ⚠️ Старый файл не найден\n\n";
|
||||
$stats['skipped']++;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Проверяем, не существует ли уже новый файл
|
||||
if ($s3Client->doesObjectExist($s3Bucket, $newPath)) {
|
||||
echo " ⚠️ Новый файл уже существует\n\n";
|
||||
$stats['skipped']++;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!$dryRun) {
|
||||
try {
|
||||
// Копируем файл
|
||||
$s3Client->copyObject([
|
||||
'Bucket' => $s3Bucket,
|
||||
'Key' => $newPath,
|
||||
'CopySource' => "{$s3Bucket}/{$oldPath}",
|
||||
]);
|
||||
echo " ✅ Скопирован\n";
|
||||
|
||||
// Удаляем старый
|
||||
$s3Client->deleteObject([
|
||||
'Bucket' => $s3Bucket,
|
||||
'Key' => $oldPath,
|
||||
]);
|
||||
echo " ✅ Старый удален\n";
|
||||
|
||||
$stats['moved']++;
|
||||
sleep(1);
|
||||
|
||||
} catch (Exception $e) {
|
||||
echo " ❌ Ошибка: " . $e->getMessage() . "\n";
|
||||
$stats['failed']++;
|
||||
}
|
||||
} else {
|
||||
echo " ⏸️ Будет перемещен (dry-run)\n";
|
||||
$stats['moved']++;
|
||||
}
|
||||
|
||||
echo "\n";
|
||||
}
|
||||
|
||||
echo str_repeat("=", 80) . "\n";
|
||||
echo "ИТОГИ:\n";
|
||||
echo "Всего: {$stats['total']}\n";
|
||||
|
||||
if (!$dryRun) {
|
||||
echo "Перемещено: {$stats['moved']}\n";
|
||||
echo "Пропущено: {$stats['skipped']}\n";
|
||||
echo "Ошибок: {$stats['failed']}\n";
|
||||
} else {
|
||||
echo "Будет перемещено: {$stats['moved']}\n";
|
||||
}
|
||||
|
||||
if (!$dryRun && $stats['moved'] > 0) {
|
||||
echo "\n⚠️ ВАЖНО: Обновите БД вручную:\n";
|
||||
echo "UPDATE vtiger_notes SET s3_key = '...', filename = '...' WHERE notesid = ...;\n";
|
||||
}
|
||||
|
||||
echo "\n=== ГОТОВО ===\n";
|
||||
|
||||
} catch (Exception $e) {
|
||||
echo "❌ Ошибка: " . $e->getMessage() . "\n";
|
||||
exit(1);
|
||||
}
|
||||
|
||||
165
move_project_398027.php
Normal file
165
move_project_398027.php
Normal file
@@ -0,0 +1,165 @@
|
||||
<?php
|
||||
/**
|
||||
* Перемещение файлов проекта 398027 в правильное место
|
||||
*/
|
||||
|
||||
error_reporting(E_ALL);
|
||||
ini_set('display_errors', 1);
|
||||
|
||||
require_once '/var/www/fastuser/data/www/crm.clientright.ru/vendor/autoload.php';
|
||||
$config = require '/var/www/fastuser/data/www/crm.clientright.ru/crm_extensions/file_storage/config.php';
|
||||
|
||||
$projectId = 398027;
|
||||
$s3Bucket = $config['s3']['bucket'];
|
||||
$projectPrefix = 'crm2/CRM_Active_Files/Documents/Project/Храмов_ООО_НЕТОЛОГИЯ_398027/';
|
||||
|
||||
// Маппинг: текущий путь => правильный путь
|
||||
$filesToMove = [
|
||||
398030 => [
|
||||
'old' => 'crm2/CRM_Active_Files/Documents/398030/8_Договор_на_оказание_услуг_11-14-2025-16-00-51_Храмов_1_CTP#realfile.pdf',
|
||||
'new' => $projectPrefix . '8_Договор_на_оказание_услуг_398030.pdf',
|
||||
],
|
||||
398032 => [
|
||||
'old' => 'crm2/CRM_Active_Files/Documents/398032/9_Подтверждение_оплаты_по_договору_11-14-2025-16-00-03_Храмов_1_CTP#realfile.pdf',
|
||||
'new' => $projectPrefix . '9_Подтверждение_оплаты_по_договору_398032.pdf',
|
||||
],
|
||||
398034 => [
|
||||
'old' => 'crm2/CRM_Active_Files/Documents/398034/10_2_Скрин_личного_кабинета_Истца_и_программа_обучения_11-14-2025-15-47-26_Храмов_41_CTP#realfile.pdf',
|
||||
'new' => $projectPrefix . '10_2_Скрин_личного_кабинета_Истца_и_программа_обучения_398034.pdf',
|
||||
],
|
||||
398036 => [
|
||||
'old' => 'crm2/CRM_Active_Files/Documents/398036/10_1_Скрин_личного_кабинета_Истца_и_программа_обучения_11-14-2025-15-49-59_Храмов_1_CTP#realfile.pdf',
|
||||
'new' => $projectPrefix . '10_1_Скрин_личного_кабинета_Истца_и_программа_обучения_398036.pdf',
|
||||
],
|
||||
398038 => [
|
||||
'old' => 'crm2/CRM_Active_Files/Documents/398038/Прочие_документы_11-14-2025-16-06-07_Храмов_3_CTP#realfile.pdf',
|
||||
'new' => $projectPrefix . 'Прочие_документы_398038.pdf',
|
||||
],
|
||||
398040 => [
|
||||
'old' => 'crm2/CRM_Active_Files/Documents/398040/7_zayavlenie_potrebitelya_Hramov.pdf',
|
||||
'new' => $projectPrefix . '7_заявление_потребителя_398040.pdf',
|
||||
],
|
||||
398063 => [
|
||||
'old' => 'crm2/CRM_Active_Files/Documents/398063/napravleniya_pretenzii.pdf',
|
||||
'new' => $projectPrefix . 'Направление_претензии_398063.pdf',
|
||||
],
|
||||
399067 => [
|
||||
'old' => 'clientright/0/1763997676315.pdf',
|
||||
'new' => $projectPrefix . 'Документ_399067.pdf',
|
||||
],
|
||||
399068 => [
|
||||
'old' => 'clientright/0/1763997790309.pdf',
|
||||
'new' => $projectPrefix . 'Документ_399068.pdf',
|
||||
],
|
||||
];
|
||||
|
||||
echo "=== ПЕРЕМЕЩЕНИЕ ФАЙЛОВ ПРОЕКТА {$projectId} ===\n";
|
||||
echo str_repeat("=", 80) . "\n\n";
|
||||
|
||||
$dryRun = isset($argv[1]) && $argv[1] === '--dry-run';
|
||||
|
||||
if ($dryRun) {
|
||||
echo "⚠️ РЕЖИМ ПРОВЕРКИ (dry-run)\n\n";
|
||||
}
|
||||
|
||||
try {
|
||||
$s3Client = new \Aws\S3\S3Client([
|
||||
'version' => 'latest',
|
||||
'region' => $config['s3']['region'],
|
||||
'endpoint' => $config['s3']['endpoint'],
|
||||
'use_path_style_endpoint' => true,
|
||||
'credentials' => [
|
||||
'key' => $config['s3']['key'],
|
||||
'secret' => $config['s3']['secret'],
|
||||
],
|
||||
'suppress_php_deprecation_warning' => true
|
||||
]);
|
||||
|
||||
$stats = [
|
||||
'total' => count($filesToMove),
|
||||
'moved' => 0,
|
||||
'skipped' => 0,
|
||||
'failed' => 0,
|
||||
];
|
||||
|
||||
foreach ($filesToMove as $docId => $paths) {
|
||||
$oldPath = $paths['old'];
|
||||
$newPath = $paths['new'];
|
||||
$filename = basename($newPath);
|
||||
|
||||
echo "Документ ID: {$docId}\n";
|
||||
echo " Файл: {$filename}\n";
|
||||
echo " От: {$oldPath}\n";
|
||||
echo " К: {$newPath}\n";
|
||||
|
||||
// Проверяем существование старого файла
|
||||
if (!$s3Client->doesObjectExist($s3Bucket, $oldPath)) {
|
||||
echo " ⚠️ Старый файл не найден\n\n";
|
||||
$stats['skipped']++;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Проверяем, не существует ли уже новый файл
|
||||
if ($s3Client->doesObjectExist($s3Bucket, $newPath)) {
|
||||
echo " ⚠️ Новый файл уже существует\n\n";
|
||||
$stats['skipped']++;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!$dryRun) {
|
||||
try {
|
||||
// Копируем файл
|
||||
$s3Client->copyObject([
|
||||
'Bucket' => $s3Bucket,
|
||||
'Key' => $newPath,
|
||||
'CopySource' => "{$s3Bucket}/{$oldPath}",
|
||||
]);
|
||||
echo " ✅ Скопирован\n";
|
||||
|
||||
// Удаляем старый
|
||||
$s3Client->deleteObject([
|
||||
'Bucket' => $s3Bucket,
|
||||
'Key' => $oldPath,
|
||||
]);
|
||||
echo " ✅ Старый удален\n";
|
||||
|
||||
$stats['moved']++;
|
||||
sleep(1);
|
||||
|
||||
} catch (Exception $e) {
|
||||
echo " ❌ Ошибка: " . $e->getMessage() . "\n";
|
||||
$stats['failed']++;
|
||||
}
|
||||
} else {
|
||||
echo " ⏸️ Будет перемещен (dry-run)\n";
|
||||
$stats['moved']++;
|
||||
}
|
||||
|
||||
echo "\n";
|
||||
}
|
||||
|
||||
// Итоги
|
||||
echo str_repeat("=", 80) . "\n";
|
||||
echo "ИТОГИ:\n";
|
||||
echo "Всего файлов: {$stats['total']}\n";
|
||||
|
||||
if (!$dryRun) {
|
||||
echo "Перемещено: {$stats['moved']}\n";
|
||||
echo "Пропущено: {$stats['skipped']}\n";
|
||||
echo "Ошибок: {$stats['failed']}\n";
|
||||
|
||||
if ($stats['moved'] > 0) {
|
||||
echo "\n⚠️ ВАЖНО: Обновите БД с правильными путями!\n";
|
||||
}
|
||||
} else {
|
||||
echo "Будет перемещено: {$stats['moved']}\n";
|
||||
}
|
||||
|
||||
echo "\n=== ГОТОВО ===\n";
|
||||
|
||||
} catch (Exception $e) {
|
||||
echo "❌ Критическая ошибка: " . $e->getMessage() . "\n";
|
||||
exit(1);
|
||||
}
|
||||
|
||||
|
||||
14
nextcloud_scan_files.sh
Executable file
14
nextcloud_scan_files.sh
Executable file
@@ -0,0 +1,14 @@
|
||||
#!/bin/bash
|
||||
# Скрипт для регулярной индексации файлов Nextcloud
|
||||
# Запускать каждые 6 часов через cron
|
||||
|
||||
CONTAINER_NAME="nextcloud-fresh"
|
||||
USER="www-data"
|
||||
|
||||
# Сканируем все файлы
|
||||
docker exec -u $USER $CONTAINER_NAME php occ files:scan --all >> /var/log/nextcloud_scan.log 2>&1
|
||||
|
||||
# Сканируем только внешнее хранилище (быстрее)
|
||||
# docker exec -u $USER $CONTAINER_NAME php occ files:scan --path="/crm" >> /var/log/nextcloud_scan.log 2>&1
|
||||
|
||||
echo "$(date): Nextcloud files scan completed" >> /var/log/nextcloud_scan.log
|
||||
274
restore_all_deleted_files.php
Executable file
274
restore_all_deleted_files.php
Executable file
@@ -0,0 +1,274 @@
|
||||
<?php
|
||||
/**
|
||||
* Скрипт для массового восстановления всех удаленных файлов из S3
|
||||
*
|
||||
* Восстанавливает файлы, удаленные через delete markers, удаляя эти маркеры
|
||||
*/
|
||||
|
||||
error_reporting(E_ALL);
|
||||
ini_set('display_errors', 1);
|
||||
set_time_limit(0); // Без ограничения времени
|
||||
|
||||
require_once '/var/www/fastuser/data/www/crm.clientright.ru/vendor/autoload.php';
|
||||
require_once '/var/www/fastuser/data/www/crm.clientright.ru/config.inc.php';
|
||||
|
||||
$config = require '/var/www/fastuser/data/www/crm.clientright.ru/crm_extensions/file_storage/config.php';
|
||||
$s3Bucket = $config['s3']['bucket'];
|
||||
|
||||
echo "Массовое восстановление удаленных файлов из S3\n";
|
||||
echo str_repeat("=", 80) . "\n\n";
|
||||
|
||||
// Параметры
|
||||
$dryRun = isset($argv[1]) && $argv[1] === '--dry-run';
|
||||
$limit = isset($argv[2]) ? (int)$argv[2] : null; // Ограничение количества файлов
|
||||
$prefix = isset($argv[3]) ? $argv[3] : 'crm2/CRM_Active_Files/Documents/'; // Префикс для поиска
|
||||
|
||||
if ($dryRun) {
|
||||
echo "⚠️ РЕЖИМ ПРОВЕРКИ (dry-run) - файлы не будут восстановлены\n\n";
|
||||
}
|
||||
|
||||
try {
|
||||
$s3Client = new \Aws\S3\S3Client([
|
||||
'version' => 'latest',
|
||||
'region' => $config['s3']['region'],
|
||||
'endpoint' => $config['s3']['endpoint'],
|
||||
'use_path_style_endpoint' => true,
|
||||
'credentials' => [
|
||||
'key' => $config['s3']['key'],
|
||||
'secret' => $config['s3']['secret'],
|
||||
],
|
||||
'suppress_php_deprecation_warning' => true
|
||||
]);
|
||||
|
||||
$stats = [
|
||||
'total_markers' => 0,
|
||||
'restored' => 0,
|
||||
'failed' => 0,
|
||||
'skipped' => 0,
|
||||
'errors' => []
|
||||
];
|
||||
|
||||
$processedKeys = []; // Для отслеживания уже обработанных ключей
|
||||
|
||||
echo "Поиск delete markers в префиксе: $prefix\n";
|
||||
echo "Ограничение: " . ($limit ? "$limit файлов" : "нет") . "\n\n";
|
||||
|
||||
$isTruncated = true;
|
||||
$continuationToken = null;
|
||||
$pageCount = 0;
|
||||
$maxPages = isset($argv[4]) ? (int)$argv[4] : 10; // БЕЗОПАСНО: максимум 10 страниц по умолчанию (можно увеличить через параметр)
|
||||
|
||||
echo "⚠️ ВНИМАНИЕ: Обработка ограничена {$maxPages} страницами для безопасности\n";
|
||||
echo " Для обработки большего количества используйте: php restore_all_deleted_files.php [--dry-run] [limit] [prefix] [maxPages]\n\n";
|
||||
|
||||
while ($isTruncated && $pageCount < $maxPages && (!$limit || $stats['restored'] + $stats['failed'] < $limit)) {
|
||||
$params = [
|
||||
'Bucket' => $s3Bucket,
|
||||
'Prefix' => $prefix,
|
||||
'MaxKeys' => 100 // БЕЗОПАСНО: уменьшено с 1000 до 100 для снижения нагрузки
|
||||
];
|
||||
|
||||
if ($continuationToken) {
|
||||
$params['ContinuationToken'] = $continuationToken;
|
||||
}
|
||||
|
||||
echo "Обработка страницы " . ($pageCount + 1) . "/{$maxPages}...\r";
|
||||
|
||||
try {
|
||||
$versions = $s3Client->listObjectVersions($params);
|
||||
$pageCount++;
|
||||
|
||||
// БЕЗОПАСНОСТЬ: пауза между страницами для снижения нагрузки
|
||||
if ($pageCount < $maxPages && $isTruncated) {
|
||||
usleep(500000); // 0.5 секунды пауза между страницами
|
||||
}
|
||||
|
||||
if (isset($versions['DeleteMarkers']) && !empty($versions['DeleteMarkers'])) {
|
||||
foreach ($versions['DeleteMarkers'] as $marker) {
|
||||
$key = $marker['Key'];
|
||||
$versionId = $marker['VersionId'];
|
||||
$deleteDate = $marker['LastModified'] ?? 'не указана';
|
||||
|
||||
// Пропускаем, если уже обработали этот ключ
|
||||
if (isset($processedKeys[$key])) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$stats['total_markers']++;
|
||||
|
||||
// Проверяем лимит
|
||||
if ($limit && ($stats['restored'] + $stats['failed']) >= $limit) {
|
||||
break 2; // Выходим из обоих циклов
|
||||
}
|
||||
|
||||
// Пропускаем папки (заканчиваются на /)
|
||||
if (substr($key, -1) === '/') {
|
||||
$stats['skipped']++;
|
||||
continue;
|
||||
}
|
||||
|
||||
try {
|
||||
if (!$dryRun) {
|
||||
// Сначала проверяем, есть ли версии файла
|
||||
try {
|
||||
$versionsList = $s3Client->listObjectVersions([
|
||||
'Bucket' => $s3Bucket,
|
||||
'Prefix' => $key,
|
||||
'MaxKeys' => 10
|
||||
]);
|
||||
|
||||
$hasVersions = isset($versionsList['Versions']) && !empty($versionsList['Versions']);
|
||||
|
||||
if ($hasVersions) {
|
||||
// Есть версии - удаляем delete marker и файл восстановится автоматически
|
||||
$s3Client->deleteObject([
|
||||
'Bucket' => $s3Bucket,
|
||||
'Key' => $key,
|
||||
'VersionId' => $versionId
|
||||
]);
|
||||
|
||||
// Проверяем, восстановился ли файл
|
||||
try {
|
||||
$headResult = $s3Client->headObject([
|
||||
'Bucket' => $s3Bucket,
|
||||
'Key' => $key
|
||||
]);
|
||||
|
||||
$stats['restored']++;
|
||||
$processedKeys[$key] = true;
|
||||
|
||||
// БЕЗОПАСНОСТЬ: пауза каждые 10 файлов
|
||||
if ($stats['restored'] % 10 == 0) {
|
||||
echo "Восстановлено: {$stats['restored']} файлов...\r";
|
||||
usleep(200000); // 0.2 секунды пауза
|
||||
}
|
||||
|
||||
} catch (\Aws\Exception\AwsException $e) {
|
||||
if ($e->getAwsErrorCode() == 'NotFound') {
|
||||
// Файл все еще не доступен, пробуем восстановить последнюю версию вручную
|
||||
$latestVersion = $versionsList['Versions'][0];
|
||||
$latestVersionId = $latestVersion['VersionId'];
|
||||
|
||||
try {
|
||||
// Копируем версию в текущий объект
|
||||
$s3Client->copyObject([
|
||||
'Bucket' => $s3Bucket,
|
||||
'CopySource' => urlencode($s3Bucket . '/' . $key) . '?versionId=' . $latestVersionId,
|
||||
'Key' => $key
|
||||
]);
|
||||
|
||||
$stats['restored']++;
|
||||
$processedKeys[$key] = true;
|
||||
|
||||
if ($stats['restored'] % 100 == 0) {
|
||||
echo "Восстановлено: {$stats['restored']} файлов...\r";
|
||||
}
|
||||
} catch (\Aws\Exception\AwsException $e2) {
|
||||
$stats['failed']++;
|
||||
$stats['errors'][] = "Ошибка восстановления версии $key: " . $e2->getMessage();
|
||||
}
|
||||
} else {
|
||||
$stats['failed']++;
|
||||
$stats['errors'][] = "Ошибка проверки $key: " . $e->getMessage();
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Нет версий - файл удален безвозвратно
|
||||
$stats['failed']++;
|
||||
$stats['errors'][] = "Не найдена версия для восстановления: $key (файл удален безвозвратно)";
|
||||
}
|
||||
|
||||
} catch (\Aws\Exception\AwsException $e) {
|
||||
$stats['failed']++;
|
||||
$stats['errors'][] = "Ошибка проверки версий для $key: " . $e->getMessage();
|
||||
}
|
||||
} else {
|
||||
// Dry-run режим - проверяем наличие версий
|
||||
try {
|
||||
$versionsList = $s3Client->listObjectVersions([
|
||||
'Bucket' => $s3Bucket,
|
||||
'Prefix' => $key,
|
||||
'MaxKeys' => 1
|
||||
]);
|
||||
|
||||
if (isset($versionsList['Versions']) && !empty($versionsList['Versions'])) {
|
||||
$stats['restored']++;
|
||||
} else {
|
||||
$stats['failed']++;
|
||||
}
|
||||
$processedKeys[$key] = true;
|
||||
|
||||
if (($stats['restored'] + $stats['failed']) % 100 == 0) {
|
||||
echo "Проверено: " . ($stats['restored'] + $stats['failed']) . " файлов...\r";
|
||||
}
|
||||
} catch (\Aws\Exception\AwsException $e) {
|
||||
$stats['failed']++;
|
||||
$processedKeys[$key] = true;
|
||||
}
|
||||
}
|
||||
|
||||
} catch (\Aws\Exception\AwsException $e) {
|
||||
$stats['failed']++;
|
||||
$stats['errors'][] = "Ошибка удаления delete marker для $key: " . $e->getMessage();
|
||||
|
||||
if (count($stats['errors']) <= 10) {
|
||||
echo "\n Ошибка: {$stats['errors'][count($stats['errors']) - 1]}\n";
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$isTruncated = isset($versions['IsTruncated']) && $versions['IsTruncated'];
|
||||
$continuationToken = isset($versions['NextContinuationToken']) ? $versions['NextContinuationToken'] : null;
|
||||
|
||||
if (!$isTruncated) {
|
||||
break;
|
||||
}
|
||||
|
||||
} catch (\Aws\Exception\AwsException $e) {
|
||||
echo "\n❌ Ошибка при обработке страницы: " . $e->getMessage() . "\n";
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
echo "\n\n";
|
||||
echo str_repeat("=", 80) . "\n";
|
||||
echo "ИТОГОВЫЙ ОТЧЕТ:\n\n";
|
||||
|
||||
echo "Всего найдено delete markers: {$stats['total_markers']}\n";
|
||||
echo "Восстановлено файлов: {$stats['restored']}\n";
|
||||
echo "Ошибок: {$stats['failed']}\n";
|
||||
echo "Пропущено (папки): {$stats['skipped']}\n\n";
|
||||
|
||||
if (!empty($stats['errors']) && count($stats['errors']) <= 20) {
|
||||
echo "Ошибки (первые " . count($stats['errors']) . "):\n";
|
||||
foreach ($stats['errors'] as $error) {
|
||||
echo " - $error\n";
|
||||
}
|
||||
echo "\n";
|
||||
} elseif (!empty($stats['errors'])) {
|
||||
echo "Всего ошибок: " . count($stats['errors']) . " (показаны первые 20)\n";
|
||||
foreach (array_slice($stats['errors'], 0, 20) as $error) {
|
||||
echo " - $error\n";
|
||||
}
|
||||
echo "\n";
|
||||
}
|
||||
|
||||
if ($dryRun) {
|
||||
echo "⚠️ Это был режим проверки. Для реального восстановления запустите:\n";
|
||||
echo " php restore_all_deleted_files.php\n\n";
|
||||
} else {
|
||||
echo "✅ Восстановление завершено!\n";
|
||||
echo " Проверьте доступность файлов в интерфейсе CRM\n\n";
|
||||
}
|
||||
|
||||
// Сохраняем статистику в файл
|
||||
$logFile = '/var/www/fastuser/data/www/crm.clientright.ru/restore_log_' . date('Y-m-d_H-i-s') . '.json';
|
||||
file_put_contents($logFile, json_encode($stats, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE));
|
||||
echo "📝 Лог сохранен в: $logFile\n";
|
||||
|
||||
} catch (Exception $e) {
|
||||
echo "❌ КРИТИЧЕСКАЯ ОШИБКА: " . $e->getMessage() . "\n";
|
||||
echo "Trace: " . $e->getTraceAsString() . "\n";
|
||||
}
|
||||
|
||||
163
restore_deleted_files.php
Normal file
163
restore_deleted_files.php
Normal file
@@ -0,0 +1,163 @@
|
||||
<?php
|
||||
error_reporting(E_ALL);
|
||||
ini_set('display_errors', 1);
|
||||
|
||||
require_once '/var/www/fastuser/data/www/crm.clientright.ru/vendor/autoload.php';
|
||||
require_once '/var/www/fastuser/data/www/crm.clientright.ru/config.inc.php';
|
||||
|
||||
$config = require '/var/www/fastuser/data/www/crm.clientright.ru/crm_extensions/file_storage/config.php';
|
||||
$docIds = [386869, 394973];
|
||||
$s3Bucket = $config['s3']['bucket'];
|
||||
|
||||
echo "Восстановление удаленных файлов из S3\n";
|
||||
echo str_repeat("=", 80) . "\n\n";
|
||||
|
||||
try {
|
||||
// Инициализация S3 клиента
|
||||
$s3Client = new \Aws\S3\S3Client([
|
||||
'version' => 'latest',
|
||||
'region' => $config['s3']['region'],
|
||||
'endpoint' => $config['s3']['endpoint'],
|
||||
'use_path_style_endpoint' => true,
|
||||
'credentials' => [
|
||||
'key' => $config['s3']['key'],
|
||||
'secret' => $config['s3']['secret'],
|
||||
],
|
||||
'suppress_php_deprecation_warning' => true
|
||||
]);
|
||||
|
||||
foreach ($docIds as $docId) {
|
||||
echo "Восстановление файла для документа $docId:\n";
|
||||
echo str_repeat("-", 80) . "\n";
|
||||
|
||||
// Получаем информацию о документе
|
||||
$pdo = new PDO(
|
||||
"mysql:host={$dbconfig['db_server']};port=3306;dbname={$dbconfig['db_name']};charset=utf8",
|
||||
$dbconfig['db_username'],
|
||||
$dbconfig['db_password'],
|
||||
[PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION]
|
||||
);
|
||||
|
||||
$stmt = $pdo->prepare('SELECT notesid, title, s3_key, s3_etag FROM vtiger_notes WHERE notesid = ?');
|
||||
$stmt->execute([$docId]);
|
||||
$doc = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||
|
||||
if (!$doc) {
|
||||
echo " Документ не найден в БД\n\n";
|
||||
continue;
|
||||
}
|
||||
|
||||
$s3Key = $doc['s3_key'];
|
||||
echo " Название: {$doc['title']}\n";
|
||||
echo " Путь: $s3Key\n\n";
|
||||
|
||||
// Получаем delete markers
|
||||
try {
|
||||
$versions = $s3Client->listObjectVersions([
|
||||
'Bucket' => $s3Bucket,
|
||||
'Prefix' => $s3Key,
|
||||
'MaxKeys' => 100
|
||||
]);
|
||||
|
||||
if (isset($versions['DeleteMarkers']) && !empty($versions['DeleteMarkers'])) {
|
||||
echo " Найдено delete markers: " . count($versions['DeleteMarkers']) . "\n";
|
||||
|
||||
// Удаляем все delete markers (самый новый будет удален последним)
|
||||
// Сортируем по дате удаления (от новых к старым)
|
||||
usort($versions['DeleteMarkers'], function($a, $b) {
|
||||
$dateA = isset($a['LastModified']) ? strtotime($a['LastModified']) : 0;
|
||||
$dateB = isset($b['LastModified']) ? strtotime($b['LastModified']) : 0;
|
||||
return $dateB - $dateA; // От новых к старым
|
||||
});
|
||||
|
||||
foreach ($versions['DeleteMarkers'] as $marker) {
|
||||
$versionId = $marker['VersionId'];
|
||||
$deleteDate = $marker['LastModified'] ?? 'не указана';
|
||||
|
||||
echo " Удаление delete marker: VersionId=$versionId (дата удаления: $deleteDate)\n";
|
||||
|
||||
try {
|
||||
$s3Client->deleteObject([
|
||||
'Bucket' => $s3Bucket,
|
||||
'Key' => $s3Key,
|
||||
'VersionId' => $versionId
|
||||
]);
|
||||
|
||||
echo " ✅ Delete marker удален\n";
|
||||
} catch (\Aws\Exception\AwsException $e) {
|
||||
echo " ❌ Ошибка при удалении delete marker: " . $e->getMessage() . "\n";
|
||||
}
|
||||
}
|
||||
|
||||
// Проверяем, восстановился ли файл
|
||||
echo "\n Проверка восстановления файла...\n";
|
||||
try {
|
||||
$headResult = $s3Client->headObject([
|
||||
'Bucket' => $s3Bucket,
|
||||
'Key' => $s3Key
|
||||
]);
|
||||
|
||||
echo " ✅ Файл успешно восстановлен!\n";
|
||||
echo " Размер: " . number_format($headResult['ContentLength'] / 1024, 2) . " KB\n";
|
||||
echo " ETag: " . (isset($headResult['ETag']) ? trim($headResult['ETag'], '"') : 'не указан') . "\n";
|
||||
echo " Дата: " . ($headResult['LastModified'] ?? 'не указана') . "\n";
|
||||
|
||||
} catch (\Aws\Exception\AwsException $e) {
|
||||
if ($e->getAwsErrorCode() == 'NotFound') {
|
||||
echo " ⚠️ Файл все еще не доступен (возможно, нужно восстановить конкретную версию)\n";
|
||||
|
||||
// Пробуем восстановить последнюю версию напрямую
|
||||
if (isset($versions['Versions']) && !empty($versions['Versions'])) {
|
||||
$latestVersion = $versions['Versions'][0]; // Первая версия - самая новая
|
||||
$versionId = $latestVersion['VersionId'];
|
||||
|
||||
echo " Попытка восстановить версию: VersionId=$versionId\n";
|
||||
|
||||
try {
|
||||
// Копируем версию в текущий объект
|
||||
$s3Client->copyObject([
|
||||
'Bucket' => $s3Bucket,
|
||||
'CopySource' => $s3Bucket . '/' . $s3Key . '?versionId=' . $versionId,
|
||||
'Key' => $s3Key
|
||||
]);
|
||||
|
||||
echo " ✅ Версия восстановлена\n";
|
||||
|
||||
// Проверяем еще раз
|
||||
$headResult = $s3Client->headObject([
|
||||
'Bucket' => $s3Bucket,
|
||||
'Key' => $s3Key
|
||||
]);
|
||||
|
||||
echo " ✅ Файл восстановлен и доступен!\n";
|
||||
echo " Размер: " . number_format($headResult['ContentLength'] / 1024, 2) . " KB\n";
|
||||
|
||||
} catch (\Aws\Exception\AwsException $e) {
|
||||
echo " ❌ Ошибка при восстановлении версии: " . $e->getMessage() . "\n";
|
||||
}
|
||||
}
|
||||
} else {
|
||||
echo " ❌ Ошибка при проверке: " . $e->getMessage() . "\n";
|
||||
}
|
||||
}
|
||||
|
||||
} else {
|
||||
echo " ❌ Delete markers не найдены\n";
|
||||
}
|
||||
|
||||
} catch (\Aws\Exception\AwsException $e) {
|
||||
echo " ❌ Ошибка при работе с версиями: " . $e->getMessage() . "\n";
|
||||
}
|
||||
|
||||
echo "\n";
|
||||
}
|
||||
|
||||
echo str_repeat("=", 80) . "\n";
|
||||
echo "Восстановление завершено!\n";
|
||||
echo "Проверьте доступность файлов в интерфейсе CRM\n";
|
||||
|
||||
} catch (Exception $e) {
|
||||
echo "ОШИБКА: " . $e->getMessage() . "\n";
|
||||
echo "Trace: " . $e->getTraceAsString() . "\n";
|
||||
}
|
||||
|
||||
7
restore_log_2025-11-25_10-20-37.json
Normal file
7
restore_log_2025-11-25_10-20-37.json
Normal file
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"total_markers": 11,
|
||||
"restored": 10,
|
||||
"failed": 0,
|
||||
"skipped": 0,
|
||||
"errors": []
|
||||
}
|
||||
7
restore_log_2025-11-25_10-21-27.json
Normal file
7
restore_log_2025-11-25_10-21-27.json
Normal file
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"total_markers": 51,
|
||||
"restored": 50,
|
||||
"failed": 0,
|
||||
"skipped": 0,
|
||||
"errors": []
|
||||
}
|
||||
550
restore_log_2025-11-25_10-23-09.json
Normal file
550
restore_log_2025-11-25_10-23-09.json
Normal file
@@ -0,0 +1,550 @@
|
||||
{
|
||||
"total_markers": 542,
|
||||
"restored": 0,
|
||||
"failed": 542,
|
||||
"skipped": 0,
|
||||
"errors": [
|
||||
"Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/1149\/Оферта_август_2022_(1)_(1).pdf",
|
||||
"Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/1151\/inquiry.pdf",
|
||||
"Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/1153\/Screen.pdf",
|
||||
"Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/117066\/operation_statement_05.04.2024.pdf",
|
||||
"Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/117081\/Заявление_о_выдачи_исполнительного_листа_по_делу_2-1658-2023_1_стр.pdf",
|
||||
"Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/117335\/Договор_04-21-2024-19-49-06_Мустафаев_21_CTP#realfile.pdf",
|
||||
"Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/117337\/Подтверждение_оплаты_04-21-2024-19-41-17_Мустафаев_1_CTP#realfile.pdf",
|
||||
"Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/117339\/Скриншот_прогресса_обучения_04-21-2024-19-48-13_Мустафаев_7_CTP#realfile.pdf",
|
||||
"Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/117341\/Претензия_04-21-2024-19-56-37_Мустафаев_2_CTP#realfile.pdf",
|
||||
"Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/117343\/Ответ_на_претензию_04-21-2024-19-57-51_Мустафаев_1_CTP#realfile.pdf",
|
||||
"Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/117597\/Договор_04-23-2024-12-18-54_Гришакова__1_CTP#realfile.pdf",
|
||||
"Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/117599\/Подтверждение_оплаты_04-23-2024-12-20-02_Гришакова__1_CTP#realfile.pdf",
|
||||
"Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/117601\/Скриншот_прогресса_обучения_04-23-2024-12-23-43_Гришакова__5_CTP#realfile.pdf",
|
||||
"Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/117603\/Претензия_04-23-2024-12-28-08_Гришакова__2_CTP#realfile.pdf",
|
||||
"Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/117605\/Ответ_на_претензию_04-23-2024-12-28-30_Гришакова__2_CTP#realfile.pdf",
|
||||
"Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/117607\/Прочие_документы_04-23-2024-12-34-20_Гришакова__1_CTP#realfile.pdf",
|
||||
"Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/117649\/Договор_04-23-2024-14-32-10_Жидкова__12_CTP#realfile.pdf",
|
||||
"Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/117651\/Подтверждение_оплаты_04-23-2024-14-34-01_Жидкова__1_CTP#realfile.pdf",
|
||||
"Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/117653\/Программа_обучения_04-23-2024-14-34-39_Жидкова__1_CTP#realfile.pdf",
|
||||
"Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/117655\/Скриншот_прогресса_обучения_04-23-2024-14-34-47_Жидкова__1_CTP#realfile.pdf",
|
||||
"Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/117660\/Выписка_из_ГРАФПИЮЛ.pdf",
|
||||
"Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/117824\/от_скилл_бокс.pdf",
|
||||
"Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/117863\/Сканирование_20240425-1902.pdf",
|
||||
"Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/117947\/Ходатайство_по_делу__1_стр.pdf",
|
||||
"Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/118166\/возврат.pdf",
|
||||
"Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/118260\/Договор_04-26-2024-17-05-28_Енин__21_CTP#realfile.pdf",
|
||||
"Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/118262\/Подтверждение_оплаты_04-26-2024-17-07-56_Енин__2_CTP#realfile.pdf",
|
||||
"Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/118264\/Скриншот_прогресса_обучения_04-26-2024-17-12-16_Енин__3_CTP#realfile.pdf",
|
||||
"Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/118266\/Претензия_04-26-2024-17-17-30_Енин__1_CTP#realfile.pdf",
|
||||
"Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/118268\/Ответ_на_претензию_04-26-2024-17-13-49_Енин__1_CTP#realfile.pdf",
|
||||
"Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/118270\/возвраты.pdf",
|
||||
"Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/118328\/Счёт_№9109027.pdf",
|
||||
"Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/118331\/чек_27.04.pdf",
|
||||
"Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/118337\/Договор_04-27-2024-23-53-19_Авдеева__19_CTP#realfile.pdf",
|
||||
"Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/118339\/Подтверждение_оплаты_04-27-2024-23-54-03_Авдеева__3_CTP#realfile.pdf",
|
||||
"Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/118341\/Программа_обучения_04-27-2024-23-55-28_Авдеева__10_CTP#realfile.pdf",
|
||||
"Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/118343\/Скриншот_прогресса_обучения_04-27-2024-23-56-39_Авдеева__10_CTP#realfile.pdf",
|
||||
"Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/118345\/Прочие_документы_04-28-2024-00-00-46_Авдеева__10_CTP#realfile.pdf",
|
||||
"Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/118365\/Договор_04-30-2024-13-16-36_Салиев_14_CTP#realfile.pdf",
|
||||
"Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/118367\/Подтверждение_оплаты_04-30-2024-13-17-19_Салиев_1_CTP#realfile.pdf",
|
||||
"Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/118369\/Программа_обучения_04-30-2024-13-26-10_Салиев_18_CTP#realfile.pdf",
|
||||
"Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/118371\/Скриншот_прогресса_обучения_04-30-2024-13-22-19_Салиев_1_CTP#realfile.pdf",
|
||||
"Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/118469\/Выписка_по_вкладу_(на_русском).pdf",
|
||||
"Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/118586\/Полис_05-02-2024-20-59-43__1_CTP.pdf",
|
||||
"Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/118589\/Подтверждение_бронирования_05-02-2024-21-05-13_Зарубина_19_CTP.pdf",
|
||||
"Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/118592\/Подтверждение_оплаты_бронирования_05-02-2024-21-05-12_Зарубина_1_CTP.pdf",
|
||||
"Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/118595\/Отказ_в_заселении_05-02-2024-21-06-50_Зарубина_2_CTP.pdf",
|
||||
"Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/118598\/Отсутствие_вида_05-02-2024-21-06-30_Зарубина_1_CTP.pdf",
|
||||
"Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/118601\/Документ_удостоверяющий_личность_05-02-2024-21-12-05_Зарубина_1_CTP.pdf",
|
||||
"Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/118612\/Протокол встречи.txt",
|
||||
"Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/118738\/Договор_05-05-2024-16-17-26_Бережной_1_CTP#realfile.pdf",
|
||||
"Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/118740\/Подтверждение_оплаты_05-05-2024-16-13-09_Бережной_15_CTP#realfile.pdf",
|
||||
"Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/118742\/Скриншот_прогресса_обучения_05-05-2024-16-36-04_Бережной_1_CTP#realfile.pdf",
|
||||
"Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/118828\/Акт_за_апрель.pdf",
|
||||
"Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/118831\/Счет_за_май+_четверть_модуля.pdf",
|
||||
"Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/118847\/Binder1_(1).pdf",
|
||||
"Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/118903\/Заявление_в_АО_-ТИНЬКОФФ_БАНК-_об_исполнении_решения_по_делу_2-244-2024_Клиентправ.pdf",
|
||||
"Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/118905\/Бордеро_Апрель_2024.xlsx",
|
||||
"Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/118937\/6_Расчет_исковых_требований_Григоричев__ИП_Айриян_Арам_Ашотович_1_стр.pdf",
|
||||
"Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/118939\/0_Исковое_заявление_по_делу_Григоричев__ИП_Айриян_Арам_Ашотович_4_стр.pdf",
|
||||
"Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/118987\/Договор_05-07-2024-12-48-52_Новичков_3_CTP#realfile.pdf",
|
||||
"Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/118989\/Подтверждение_оплаты_05-07-2024-12-49-12_Новичков_3_CTP#realfile.pdf",
|
||||
"Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/118991\/Скриншот_прогресса_обучения_05-07-2024-12-50-06_Новичков_1_CTP#realfile.pdf",
|
||||
"Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/1190\/Договор.pdf",
|
||||
"Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/119017\/Заявление_о_выдачи_исполнительного_листа_по_делу_2-817-2024_1_стр.pdf",
|
||||
"Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/119032\/Опись_104807.pdf",
|
||||
"Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/119052\/доказательство_направления_иска_2_стр.pdf",
|
||||
"Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/119088\/Договор_05-07-2024-17-27-35_Удовикина_12_CTP#realfile.pdf",
|
||||
"Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/119090\/Подтверждение_оплаты_05-07-2024-17-26-35_Удовикина_1_CTP#realfile.pdf",
|
||||
"Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/119092\/Программа_обучения_05-07-2024-17-25-41_Удовикина_1_CTP#realfile.pdf",
|
||||
"Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/119094\/Скриншот_прогресса_обучения_05-07-2024-17-25-17_Удовикина_1_CTP#realfile.pdf",
|
||||
"Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/119103\/Договор_05-07-2024-17-56-21_Захарова__1_CTP#realfile.pdf",
|
||||
"Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/119105\/Подтверждение_оплаты_05-07-2024-17-58-27_Захарова__1_CTP#realfile.pdf",
|
||||
"Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/119107\/Программа_обучения_05-07-2024-17-52-07_Захарова__13_CTP#realfile.pdf",
|
||||
"Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/119109\/Скриншот_прогресса_обучения_05-07-2024-17-53-11_Захарова__1_CTP#realfile.pdf",
|
||||
"Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/119111\/Претензия_05-07-2024-18-04-05_Захарова__2_CTP#realfile.pdf",
|
||||
"Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/119113\/Ответ_на_претензию_05-07-2024-18-11-10_Захарова__1_CTP#realfile.pdf",
|
||||
"Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/119123\/Ходатайство_по_делу_М-1132-2024_1_стр.pdf",
|
||||
"Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/119126\/0_Исковое_заявление_по_делу_Гусев__ООО_-СКИЛБОКС-_4_стр.pdf",
|
||||
"Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/119128\/6_Расчет_исковых_требований_Гусев__ООО_-СКИЛБОКС-_1_стр.pdf",
|
||||
"Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/119138\/Ходатайство_по_делу_М-538-2024_1_стр.pdf",
|
||||
"Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/1192\/платеж.pdf",
|
||||
"Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/119259\/Договор_05-08-2024-16-49-53_Панина_12_CTP#realfile.pdf",
|
||||
"Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/119261\/Подтверждение_оплаты_05-08-2024-16-50-13_Панина_1_CTP#realfile.pdf",
|
||||
"Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/119263\/Программа_обучения_05-08-2024-16-50-36_Панина_4_CTP#realfile.pdf",
|
||||
"Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/119265\/Скриншот_прогресса_обучения_05-08-2024-16-50-58_Панина_1_CTP#realfile.pdf",
|
||||
"Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/119267\/Претензия_05-08-2024-16-51-31_Панина_2_CTP#realfile.pdf",
|
||||
"Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/119269\/Ответ_на_претензию_05-08-2024-16-52-19_Панина_2_CTP#realfile.pdf",
|
||||
"Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/119271\/Возврат_средств_05-08-2024-16-52-07_Панина_2_CTP#realfile.pdf",
|
||||
"Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/1194\/Новая_папка.7z",
|
||||
"Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/119409\/10_Доказательство_проведение_претензионной_работы__Исаева_Екатерина_2_стр.pdf",
|
||||
"Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/119449\/7_заявление_потребителя_Панина__.pdf",
|
||||
"Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/119465\/7_заявление_потребителя_Бережной__.pdf",
|
||||
"Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/119583\/Заявление_о_выдачи_исполнительного_листа_по_делу_02-11106-2023_1_стр.pdf",
|
||||
"Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/119598\/IMG_3814.pdf",
|
||||
"Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/1196\/Общение_с_представителями_школы.zip",
|
||||
"Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/119613\/Пдф_для_суда.pdf",
|
||||
"Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/119623\/12доказательство_направления_иска.pdf",
|
||||
"Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/119641\/6_Расчет_исковых_требований_Носенко__ООО_-СКИЛБОКС-_1_стр.pdf",
|
||||
"Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/119643\/0_Исковое_заявление_по_делу_Носенко__ООО_-СКИЛБОКС-_4_стр.pdf",
|
||||
"Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/119653\/Опись_104863_merged.pdf",
|
||||
"Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/119719\/Adobe_Scan_23_апр._2024 г..pdf",
|
||||
"Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/119756\/приказ_10.pdf",
|
||||
"Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/119759\/Договор_подряда__ООО_Лира.pdf",
|
||||
"Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/119762\/приказ_9.pdf",
|
||||
"Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/119765\/приказ_Гринчук.pdf",
|
||||
"Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/119768\/ТД_художник_дизайнер.pdf",
|
||||
"Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/119771\/ТД_рук_отд_продаж.pdf",
|
||||
"Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/119774\/ДИ_рук_отд_продаж.pdf",
|
||||
"Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/119819\/6_Расчет_исковых_требований_Кириллов___ИП_Ваняшин_Андрей_Эдуардович_1_стр.pdf",
|
||||
"Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/119821\/0_Исковое_заявление_по_делу_Кириллов___ИП_Ваняшин_Андрей_Эдуардович_4_стр.pdf",
|
||||
"Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/119831\/Опись+квитанция_об_оплате_105029.pdf",
|
||||
"Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/119847\/Заявление_в_АО_-ТИНЬКОФФ_БАНК-_об_исполнении_решения_по_делу_2-1658-2023_Клиентправ.pdf",
|
||||
"Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/119851\/Заявление_в_АО_-ТИНЬКОФФ_БАНК-_об_исполнении_решения_по_делу_2-1419-2023_Клиентправ.pdf",
|
||||
"Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/119958\/10_Доказательство_проведение_претензионной_работы__Коржилова__Наталья__2_стр.pdf",
|
||||
"Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/119970\/10_Доказательство_проведение_претензионной_работы__Мацак_Елена_2_стр.pdf",
|
||||
"Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/119982\/10_Доказательство_проведение_претензионной_работы__Камаш_Александра_2_стр.pdf",
|
||||
"Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/120006\/10_Доказательство_проведение_претензионной_работы__Чубик_Дарья_2_стр.pdf",
|
||||
"Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/120030\/10_Доказательство_проведение_претензионной_работы__Силина_Виктория_2_стр.pdf",
|
||||
"Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/120079\/Опись_(1).pdf",
|
||||
"Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/120087\/6_Расчет_исковых_требований_Мурин__ЧОУ_ДПО_-ОБРАЗОВАТЕЛЬНЫЕ_ТЕХНОЛОГИИ_-СКИЛБОКС_(КОРОБКА_НАВЫКОВ)-_1_стр.pdf",
|
||||
"Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/120089\/0_Исковое_заявление_по_делу_Мурин__ЧОУ_ДПО_-ОБРАЗОВАТЕЛЬНЫЕ_ТЕХНОЛОГИИ_-СКИЛБОКС_(КОРОБКА_НАВЫКОВ)-_4_стр.pdf",
|
||||
"Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/120148\/Письмо_ИП_Будишевский-2.pdf",
|
||||
"Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/120195\/отказ.pdf",
|
||||
"Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/120198\/прогресс.pdf",
|
||||
"Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/120201\/программа_курса.pdf",
|
||||
"Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/120239\/7_заявление_потребителя_Дё__.pdf",
|
||||
"Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/120242\/Договор_05-14-2024-14-12-21_Дё_13_CTP#realfile.pdf",
|
||||
"Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/120245\/Подтверждение_оплаты_05-14-2024-14-22-29_Дё_5_CTP#realfile.pdf",
|
||||
"Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/120248\/Прочие_документы_05-14-2024-14-53-09_Дё_2_CTP#realfile.pdf",
|
||||
"Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/120251\/Скриншот_прогресса_обучения_05-14-2024-14-25-25_Дё_1_CTP#realfile.pdf",
|
||||
"Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/120259\/Договор_05-14-2024-13-17-36_Плохова__12_CTP#realfile.pdf",
|
||||
"Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/120262\/Подтверждение_оплаты_05-14-2024-17-21-09_Плохова__2_CTP#realfile.pdf",
|
||||
"Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/120265\/Скриншот_прогресса_обучения_05-14-2024-14-50-09_Плохова__1_CTP#realfile.pdf",
|
||||
"Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/120269\/7_заявление_потребителя_Плохова___.pdf",
|
||||
"Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/120319\/0_Исковое_заявление_по_делу_Акулов_ООО_-ЭВОТРЕН-_4_стр.pdf",
|
||||
"Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/120321\/6_Расчет_исковых_требований_Акулов_ООО_-ЭВОТРЕН-_1_стр.pdf",
|
||||
"Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/120350\/Договор_05-14-2024-20-27-08_Трубнякова__1_CTP#realfile.pdf",
|
||||
"Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/120352\/Подтверждение_оплаты_05-14-2024-20-34-20_Трубнякова__1_CTP#realfile.pdf",
|
||||
"Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/120354\/Программа_обучения_05-14-2024-20-40-01_Трубнякова__1_CTP#realfile.pdf",
|
||||
"Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/120356\/Скриншот_прогресса_обучения_05-14-2024-20-43-20_Трубнякова__7_CTP#realfile.pdf",
|
||||
"Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/120358\/Претензия_05-14-2024-20-53-35_Трубнякова__3_CTP#realfile.pdf",
|
||||
"Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/120360\/Ответ_на_претензию_05-14-2024-20-56-00_Трубнякова__7_CTP#realfile.pdf",
|
||||
"Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/120368\/7_заявление_потребителя_Трубнякова___.pdf",
|
||||
"Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/120395\/7_заявление_потребителя_Новичков_.pdf",
|
||||
"Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/120397\/11_Доказательство_соблюдения_претензионного_порядка__Новичков_Константин_2_стр.pdf",
|
||||
"Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/120413\/Заявление_в_АО_-ТИНЬКОФФ_БАНК-_об_исполнении_решения_по_делу_02-1320-44-2023_Клиентправ.pdf",
|
||||
"Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/120437\/отправка_претензии_почтой.pdf",
|
||||
"Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/120479\/2024-05-04-2.jpg",
|
||||
"Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/120484\/2024-05-04-1.jpg",
|
||||
"Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/120491\/Договор_05-15-2024-15-11-47_Тупас__17_CTP#realfile.pdf",
|
||||
"Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/120493\/Подтверждение_оплаты_05-15-2024-15-12-30_Тупас__2_CTP#realfile.pdf",
|
||||
"Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/120495\/Прочие_документы_05-15-2024-15-14-02_Тупас__25_CTP#realfile.pdf",
|
||||
"Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/120724\/7_заявление_потребителя_Самуткина__.pdf",
|
||||
"Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/120728\/Договор_05-16-2024-12-02-31_Самуткина__11_CTP#realfile.pdf",
|
||||
"Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/120730\/Подтверждение_оплаты_05-16-2024-12-03-28_Самуткина__8_CTP#realfile.pdf",
|
||||
"Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/120732\/Программа_обучения_05-16-2024-12-04-07_Самуткина__4_CTP#realfile.pdf",
|
||||
"Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/120734\/Скриншот_прогресса_обучения_05-16-2024-12-04-28_Самуткина__1_CTP#realfile.pdf",
|
||||
"Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/120736\/Претензия_05-16-2024-12-07-09_Самуткина__2_CTP#realfile.pdf",
|
||||
"Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/120738\/Ответ_на_претензию_05-16-2024-12-07-19_Самуткина__2_CTP#realfile.pdf",
|
||||
"Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/120740\/Прочие_документы_05-16-2024-13-03-51_Самуткина__1_CTP#realfile.pdf",
|
||||
"Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/120754\/Заявление_истца_на_выдачу_листа_по_делу_02-4298-345-2023_1_стр.pdf",
|
||||
"Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/120787\/7_заявление_потребителя_Мустафаев_.pdf",
|
||||
"Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/120796\/7_заявление_потребителя_Захарова__.pdf",
|
||||
"Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/120803\/Договор_05-16-2024-15-11-45_Кузовлева__6_CTP#realfile.pdf",
|
||||
"Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/120805\/Подтверждение_оплаты_05-16-2024-15-13-53_Кузовлева__3_CTP#realfile.pdf",
|
||||
"Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/120807\/Прочие_документы_05-16-2024-15-14-08_Кузовлева__20_CTP#realfile.pdf",
|
||||
"Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/120809\/Претензия_и_документы_05-16-2024-15-15-45_Кузовлева__2_CTP#realfile.pdf",
|
||||
"Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/120811\/Возврат_средств_контрагентом_05-16-2024-15-17-21_Кузовлева__1_CTP#realfile.pdf",
|
||||
"Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/120865\/11._Подтверждение_проведения_претензионной_работы_Захарова__Наталья__2_стр.pdf",
|
||||
"Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/120877\/11._Подтверждение_проведения_претензионной_работы_Самуткина__Ксения_2_стр.pdf",
|
||||
"Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/120889\/7_заявление_потребителя_Сарина__.pdf",
|
||||
"Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/120895\/document_(2).pdf",
|
||||
"Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/120897\/Скриншот_прогресса_обучения_05-16-2024-22-44-55_Сарина__1_CTP#realfile.pdf",
|
||||
"Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/120899\/Претензия_05-16-2024-22-48-30_Сарина__25_CTP#realfile.pdf",
|
||||
"Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/120901\/Ответ_на_претензию_05-16-2024-22-48-55_Сарина__3_CTP#realfile.pdf",
|
||||
"Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/120924\/11._Подтверждение_проведения_претензионной_работы_Дё_Виктория_2_стр.pdf",
|
||||
"Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/120968\/Договор_05-17-2024-12-03-41_Зозуля__14_CTP#realfile.pdf",
|
||||
"Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/120970\/Подтверждение_оплаты_05-17-2024-12-10-43_Зозуля__1_CTP#realfile.pdf",
|
||||
"Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/120972\/Прочие_документы_05-17-2024-12-02-05_Зозуля__23_CTP#realfile.pdf",
|
||||
"Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/120978\/Почта-суд.pdf",
|
||||
"Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/120996\/11._Подтверждение_проведения_претензионной_работы_Сарина__Людмила__2_стр.pdf",
|
||||
"Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/121032\/11._Подтверждение_проведения_претензионной_работы_Трубнякова__Дарья__2_стр.pdf",
|
||||
"Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/121037\/Ответ_на_претензию_04-24-2024-12-31-08_Окуньков_3_CTP#realfile.pdf",
|
||||
"Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/121057\/11._Подтверждение_проведения_претензионной_работы_Мустафаев_Энвер_2_стр.pdf",
|
||||
"Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/121140\/7_заявление_потребителя_Балашов_.pdf",
|
||||
"Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/121148\/11._Подтверждение_проведения_претензионной_работы_Балашов_Александр_2_стр.pdf",
|
||||
"Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/121160\/7_заявление_потребителя_Жидкова__.pdf",
|
||||
"Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/121177\/11._Подтверждение_проведения_претензионной_работы_Жидкова__Ольга__2_стр.pdf",
|
||||
"Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/1216\/договор.rar",
|
||||
"Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/1218\/оплата.rar",
|
||||
"Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/1220\/личный_кабинет.rar",
|
||||
"Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/1234\/Заявление_(2).pdf",
|
||||
"Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/123567\/7_заявление_потребителя_Матвеев_.pdf",
|
||||
"Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/123571\/Договор_05-18-2024-01-44-40_Матвеев_12_CTP#realfile.pdf",
|
||||
"Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/123573\/Подтверждение_оплаты_05-17-2024-21-06-55_Матвеев_1_CTP#realfile.pdf",
|
||||
"Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/123575\/Скриншот_прогресса_обучения_05-17-2024-18-17-22_Матвеев_1_CTP#realfile.pdf",
|
||||
"Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/123617\/отправка_иска_почтой.pdf",
|
||||
"Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/123620\/Пользовательское-соглашение-в-ред.-от-12.07.22-(1).pdf",
|
||||
"Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/123623\/лк_медведева.pdf",
|
||||
"Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/123626\/подтверждение_оплаты_медведева.pdf",
|
||||
"Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/123629\/доказательство_претензии_медведева.pdf",
|
||||
"Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/123632\/претензия_медведева_2.pdf",
|
||||
"Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/123658\/Опись_и_квитанция.pdf",
|
||||
"Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/123723\/6_Расчет_исковых_требований_Завьялова__ЧОУ_ДПО_-ОБРАЗОВАТЕЛЬНЫЕ_ТЕХНОЛОГИИ_-СКИЛБОКС_(КОРОБКА_НАВЫКОВ)-_1_стр.pdf",
|
||||
"Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/123725\/0_Исковое_заявление_по_делу_Завьялова__ЧОУ_ДПО_-ОБРАЗОВАТЕЛЬНЫЕ_ТЕХНОЛОГИИ_-СКИЛБОКС_(КОРОБКА_НАВЫКОВ)-_4_стр.pdf",
|
||||
"Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/123735\/Опись_104988.pdf",
|
||||
"Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/123786\/7_заявление_потребителя_Ливенцева_.pdf",
|
||||
"Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/123792\/Договор_05-20-2024-16-58-35_Ливенцева_6_CTP#realfile.pdf",
|
||||
"Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/123794\/Подтверждение_оплаты_05-20-2024-16-59-28_Ливенцева_1_CTP#realfile.pdf",
|
||||
"Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/123796\/Программа_обучения_05-20-2024-16-55-35_Ливенцева_3_CTP#realfile.pdf",
|
||||
"Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/123798\/Скриншот_прогресса_обучения_05-20-2024-16-58-01_Ливенцева_1_CTP#realfile.pdf",
|
||||
"Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/123800\/Претензия_05-20-2024-17-04-57_Ливенцева_3_CTP#realfile.pdf",
|
||||
"Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/123847\/7_заявление_потребителя_Гришакова__.pdf",
|
||||
"Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/123858\/6_Расчет_исковых_требований_Юмаев___Turkish_Airlines_(Турецкие_Авиалинии)_1_стр.pdf",
|
||||
"Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/123863\/0_Исковое_заявление_по_делу_Юмаев___Turkish_Airlines_(Турецкие_Авиалинии)_6_стр.pdf",
|
||||
"Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/123870\/доказательство_направления_иска_ответчику_Юмаев___Turkish_Airlines_(Турецкие_Авиалинии)_1_стр.pdf",
|
||||
"Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/123889\/7_заявление_потребителя_Лисовенко_.pdf",
|
||||
"Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/123896\/Договор_05-20-2024-22-33-05_Лисовенко_1_CTP#realfile.pdf",
|
||||
"Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/123898\/Подтверждение_оплаты_05-20-2024-22-33-47_Лисовенко_1_CTP#realfile.pdf",
|
||||
"Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/123900\/Программа_обучения_05-20-2024-22-35-11_Лисовенко_1_CTP#realfile.pdf",
|
||||
"Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/123902\/Скриншот_прогресса_обучения_05-20-2024-22-36-28_Лисовенко_1_CTP#realfile.pdf",
|
||||
"Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/123909\/7_заявление_потребителя_Маклакова__.pdf",
|
||||
"Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/123915\/Договор_05-20-2024-22-40-25_Маклакова__2_CTP#realfile.pdf",
|
||||
"Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/123917\/Подтверждение_оплаты_05-20-2024-22-40-56_Маклакова__3_CTP#realfile.pdf",
|
||||
"Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/123919\/Программа_обучения_05-20-2024-22-41-34_Маклакова__1_CTP#realfile.pdf",
|
||||
"Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/123921\/Скриншот_прогресса_обучения_05-20-2024-22-44-31_Маклакова__2_CTP#realfile.pdf",
|
||||
"Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/123923\/Претензия_05-20-2024-22-58-20_Маклакова__1_CTP#realfile.pdf",
|
||||
"Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/123925\/Прочие_документы_05-20-2024-22-59-48_Маклакова__3_CTP#realfile.pdf",
|
||||
"Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/123931\/7_заявление_потребителя_Енин__.pdf",
|
||||
"Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/123948\/Опись_104985.pdf",
|
||||
"Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/123975\/доказательство_направления_иска_ответчику_Березанский__Turkish_Airlines_1_стр.pdf",
|
||||
"Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/123992\/7_заявление_потребителя_Дорничева_.pdf",
|
||||
"Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/1240\/Пользовательское_соглашение_Гикбрейнс_21.12.2021.pdf",
|
||||
"Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/124000\/Договор_05-21-2024-09-25-07_Дорничева_1_CTP#realfile.pdf",
|
||||
"Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/124002\/Подтверждение_оплаты_05-21-2024-09-22-53_Дорничева_2_CTP#realfile.pdf",
|
||||
"Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/124004\/Скриншот_прогресса_обучения_05-21-2024-09-26-55_Дорничева_1_CTP#realfile.pdf",
|
||||
"Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/124006\/Претензия_05-21-2024-09-31-20_Дорничева_2_CTP#realfile.pdf",
|
||||
"Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/124017\/13_судебная_практика_по_данной_категории_дел_5_стр.pdf",
|
||||
"Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/1242\/Справка_об_оплате_обучения_Петров_Илья_Павлович.pdf",
|
||||
"Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/1244\/ГБ.rar",
|
||||
"Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/12627\/image0.jpeg",
|
||||
"Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/12632\/Доказательство_проведение_претензионной_работы__Чекалин__Матвей__2_стр.pdf",
|
||||
"Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/12635\/Доказательство_проведение_претензионной_работы__Черников_Александр_2_стр.pdf",
|
||||
"Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/126413\/7_заявление_потребителя_Кривенцова_.pdf",
|
||||
"Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/126419\/Договор_05-21-2024-11-40-35_Кривенцова_1_CTP#realfile.pdf",
|
||||
"Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/126421\/Подтверждение_оплаты_05-21-2024-11-38-24_Кривенцова_1_CTP#realfile.pdf",
|
||||
"Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/126423\/Программа_обучения_05-21-2024-11-40-46_Кривенцова_1_CTP#realfile.pdf",
|
||||
"Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/126425\/Скриншот_прогресса_обучения_05-21-2024-11-40-54_Кривенцова_1_CTP#realfile.pdf",
|
||||
"Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/126441\/Скриншот_прогресса_обучения_05-18-2024-13-19-17_Сальников__1_CTP#realfile.pdf",
|
||||
"Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/126444\/Подтверждение_оплаты_05-18-2024-13-16-56_Сальников__1_CTP#realfile.pdf",
|
||||
"Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/126447\/Договор_05-18-2024-13-14-04_Сальников__1_CTP#realfile.pdf",
|
||||
"Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/126450\/7_заявление_потребителя_Сальников__.pdf",
|
||||
"Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/126469\/11._Подтверждение_проведения_претензионной_работы_Сальников__Андрей__2_стр.pdf",
|
||||
"Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/126478\/7_заявление_потребителя_Федорова_.pdf",
|
||||
"Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/12648\/_8.0_9.0_Оферта_Команда_Александра_Никитина_РФ.docx.pdf",
|
||||
"Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/126484\/Договор_05-21-2024-12-13-49_Федорова_1_CTP#realfile.pdf",
|
||||
"Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/126486\/Подтверждение_оплаты_05-21-2024-12-14-49_Федорова_2_CTP#realfile.pdf",
|
||||
"Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/126488\/Скриншот_прогресса_обучения_05-21-2024-12-15-48_Федорова_1_CTP#realfile.pdf",
|
||||
"Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/12650\/0040045670_20230128.pdf",
|
||||
"Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/126510\/11._Подтверждение_проведения_претензионной_работы_Кривенцова_Татьяна_2_стр.pdf",
|
||||
"Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/12652\/2023-06-16_12-26-08.jpg",
|
||||
"Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/126523\/11._Подтверждение_проведения_претензионной_работы_Гришакова__Светлана__2_стр.pdf",
|
||||
"Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/126535\/7_заявление_потребителя_Бекназарова_.pdf",
|
||||
"Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/12654\/Претензия.pdf",
|
||||
"Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/126541\/Договор_05-21-2024-13-17-31_Бекназарова_1_CTP#realfile.pdf",
|
||||
"Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/126543\/Подтверждение_оплаты_05-21-2024-13-14-32_Бекназарова_1_CTP#realfile.pdf",
|
||||
"Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/126545\/Скриншот_прогресса_обучения_05-21-2024-13-16-04_Бекназарова_1_CTP#realfile.pdf",
|
||||
"Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/126564\/7_заявление_потребителя_минеева__.pdf",
|
||||
"Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/126570\/Договор_05-21-2024-14-33-17_минеева__1_CTP#realfile.pdf",
|
||||
"Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/126572\/Подтверждение_оплаты_05-21-2024-14-33-54_минеева__1_CTP#realfile.pdf",
|
||||
"Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/126574\/Программа_обучения_05-21-2024-14-35-31_минеева__1_CTP#realfile.pdf",
|
||||
"Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/126576\/Скриншот_прогресса_обучения_05-21-2024-14-36-26_минеева__1_CTP#realfile.pdf",
|
||||
"Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/126582\/120885_Опись_104925_merged.pdf",
|
||||
"Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/126585\/120959_Ответ_от_2024—05—17_в_11.55.11.pdf",
|
||||
"Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/126616\/7_заявление_потребителя_Еловиков__.pdf",
|
||||
"Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/126621\/Договор_05-21-2024-15-34-42_Еловиков__11_CTP#realfile.pdf",
|
||||
"Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/126623\/Подтверждение_оплаты_05-21-2024-15-43-32_Еловиков__2_CTP#realfile.pdf",
|
||||
"Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/126625\/Скриншот_прогресса_обучения_05-21-2024-15-33-35_Еловиков__1_CTP#realfile.pdf",
|
||||
"Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/126644\/7_заявление_потребителя_Попов__.pdf",
|
||||
"Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/126649\/Договор_05-21-2024-15-58-51_Попов__1_CTP#realfile.pdf",
|
||||
"Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/126651\/Подтверждение_оплаты_05-21-2024-16-10-56_Попов__1_CTP#realfile.pdf",
|
||||
"Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/126653\/Программа_обучения_05-21-2024-16-05-10_Попов__2_CTP#realfile.pdf",
|
||||
"Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/126655\/Скриншот_прогресса_обучения_05-21-2024-15-53-06_Попов__1_CTP#realfile.pdf",
|
||||
"Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/126657\/Претензия_05-21-2024-16-23-23_Попов__1_CTP#realfile.pdf",
|
||||
"Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/126659\/Ответ_на_претензию_05-21-2024-16-22-47_Попов__1_CTP#realfile.pdf",
|
||||
"Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/12685\/Оферта_август_2022_(1).pdf",
|
||||
"Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/12687\/оплата.pdf",
|
||||
"Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/12689\/12_Прогресс_обучения,_на_1_стр.pdf",
|
||||
"Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/12698\/Руслан_договор_(1).pdf",
|
||||
"Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/12700\/photo_1_2023-06-16_22-07-13.jpg",
|
||||
"Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/12702\/photo_3_2023-06-16_22-07-13.jpg",
|
||||
"Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/12709\/Доказательство_проведение_претензионной_работы__Селедцова_Любовь_2_стр.pdf",
|
||||
"Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/12718\/ТАСПИН_ОФЕРТА.pdf",
|
||||
"Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/12720\/check_0006124557045916_7281440500284613_176023_(1).pdf",
|
||||
"Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/12722\/лк.rar",
|
||||
"Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/12724\/таспин_суд.rar",
|
||||
"Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/12726\/kred_dogovor.pdf",
|
||||
"Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/12728\/dosud_pretenziya.pdf",
|
||||
"Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/12737\/оферта.zip",
|
||||
"Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/12739\/оплата_Лайк.pdf",
|
||||
"Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/12741\/Чат_Марафон_инвестиций.PNG",
|
||||
"Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/12743\/переписка_09.2021.jpeg",
|
||||
"Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/12779\/Kreditnyj_dogovor_individualʹnyh_uslovij_po_kreditu_(IUK).pdf",
|
||||
"Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/12781\/Kreditnyj_dogovor_individualʹnyh_uslovij_po_kreditu_(IUK).pdf",
|
||||
"Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/12783\/Kreditnyj_dogovor_individualʹnyh_uslovij_po_kreditu_(IUK).pdf",
|
||||
"Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/12884\/Договор-оферта_Skillbox_+_доп.соглашение.7z",
|
||||
"Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/12886\/operation_statement_16.09.2022.pdf",
|
||||
"Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/12888\/Скрин_личного_кабинета.pdf",
|
||||
"Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/12890\/подтверждение_претензионной_работы.7z",
|
||||
"Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/12892\/Кассовый_чек_(возврат)_88260_рублей_от_02.12.2022.pdf",
|
||||
"Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/12894\/Сообщение_Skillbox_от_14.06.2023.pdf",
|
||||
"Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/12919\/заявление_в_МОО_Клиентправ.jpeg",
|
||||
"Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/12934\/Заявление_в_Клиентправ_Шмелев_ООО_-СКИЛБОКС-.pdf",
|
||||
"Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/1294\/Заявления-Претензия.pdf",
|
||||
"Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/12943\/Договор_оферты_.pdf",
|
||||
"Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/12945\/Заявление_о_предоставлении_кредита_и_открытии_банковского_счёта.pdf",
|
||||
"Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/12947\/Личный_кабинет_1.png",
|
||||
"Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/12949\/Ответ_службы_поддержки.jpg",
|
||||
"Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/12951\/Кассовый_чек_ООО_ГИКБРЕЙНС_95574_руб.jpg",
|
||||
"Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/12964\/Св-во_о_перемене_имени.pdf",
|
||||
"Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/12975\/oferta.pdf",
|
||||
"Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/12977\/check_0004438001022024_9960440503169974_19024.pdf",
|
||||
"Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/12979\/1.rar",
|
||||
"Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/12981\/ответ.rar",
|
||||
"Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/12983\/претензия.rar",
|
||||
"Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/13028\/В_МОО_«Клиентправ»_(1).pdf",
|
||||
"Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/130334\/Прием_РПО_внутреннее.pdf",
|
||||
"Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/13038\/Доказательство_проведение_претензионной_работы__Байкова_Татьяна_2_стр.pdf",
|
||||
"Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/130445\/Опись_104995.pdf",
|
||||
"Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/13047\/Оферта_learnhub_08.02.2023_v1.pdf",
|
||||
"Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/13049\/contract.pdf",
|
||||
"Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/13051\/Снимок_экрана_2023-06-21_114806.pdf",
|
||||
"Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/13053\/Документ_Microsoft_Word_(2).pdf",
|
||||
"Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/13061\/Оферта_learnhub_08.02.2023_v1.pdf",
|
||||
"Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/13063\/contract.pdf",
|
||||
"Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/13065\/Снимок_экрана_2023-06-21_114806.pdf",
|
||||
"Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/13067\/photo_2023-06-21_12-14-28.pdf",
|
||||
"Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/13069\/Документ_Microsoft_Word_(2).pdf",
|
||||
"Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/13082\/SKMBT_C224e23061723020.pdf",
|
||||
"Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/13086\/доказательство_направления_иска_ответчику_Носков_ООО_-ГИКБРЕИНС-_1_стр.pdf",
|
||||
"Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/13121\/oferta.pdf",
|
||||
"Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/13123\/check_0006185384006725_7281440500562238_4211.pdf",
|
||||
"Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/13125\/Мое_обучение_—_Skillbox.pdf",
|
||||
"Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/13190\/лицензия.pdf",
|
||||
"Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/13200\/WhatsApp_Image_2023-06-21_at_20.11.48.jpg",
|
||||
"Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/13202\/WhatsApp_Image_2023-06-21_at_20.12.10.jpg",
|
||||
"Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/13204\/WhatsApp_Image_2023-06-21_at_20.12.04.jpg",
|
||||
"Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/13211\/Оферта_learnhub_08.02.2023_v1_(2).pdf",
|
||||
"Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/13213\/c0e78194-be00-4190-bdc5-d843d43f6ada_(1).pdf",
|
||||
"Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/13215\/скрины_с_платформы.pdf",
|
||||
"Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/13222\/Оферта_learnhub_08.02.2023_v1.pdf",
|
||||
"Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/13224\/sber_statement_21-06-2023_23-52-15.pdf",
|
||||
"Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/13226\/Screenshot_20230621-235424_iSpring_Learn.jpg",
|
||||
"Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/13228\/Screenshot_20230622-000404_Telegram.jpg",
|
||||
"Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/13239\/Доказательство_проведение_претензионной_работы__Хименко_Анна_2_стр.pdf",
|
||||
"Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/13242\/Доказательство_проведение_претензионной_работы__Шмелев_Руслан_2_стр.pdf",
|
||||
"Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/13245\/Доказательство_проведение_претензионной_работы__Сулейменова_Дина_2_стр.pdf",
|
||||
"Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/13348\/доказательство_направления_иска_ответчику_Виноградов_ООО_-СКИЛБОКС-_1_стр.pdf",
|
||||
"Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/13350\/0_Исковое_заявление_по_делу_Виноградов_ООО_-СКИЛБОКС-_4_стр.pdf",
|
||||
"Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/13352\/2_Расчет_исковых_требований_Виноградов_ООО_-СКИЛБОКС-_1_стр.pdf",
|
||||
"Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/13360\/Претензия_в_защиту_интересов_Сопова_Алина_1_стр.pdf",
|
||||
"Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/13368\/Image0082_(1).JPG",
|
||||
"Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/13374\/oferta.pdf",
|
||||
"Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/13376\/Screenshot_20230623_101840_Gmail.jpg",
|
||||
"Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/13378\/Screenshot_20230623_101551_Chrome.jpg",
|
||||
"Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/13380\/Screenshot_20230623_101528_Chrome.jpg",
|
||||
"Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/13387\/Договор_ОТП.zip",
|
||||
"Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/13389\/Договор_ОТП.zip",
|
||||
"Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/13391\/Деньги_под_ключ.pdf",
|
||||
"Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/13398\/Договор_ОТП.zip",
|
||||
"Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/13400\/Договор_ОТП.zip",
|
||||
"Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/13402\/Деньги_под_ключ.pdf",
|
||||
"Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/13412\/doc01781120220725125515.pdf",
|
||||
"Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/13414\/Заявление_о_предоставлении_кредита_и_открытии_банковского_счёта_(экземпляр_банка).PDF",
|
||||
"Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/13416\/ЛК.PNG",
|
||||
"Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/13425\/oferta.pdf",
|
||||
"Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/13427\/photo1687688292.pdf",
|
||||
"Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/13429\/изображение_2023-06-25_162046444.pdf",
|
||||
"Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/13436\/Договор.rar",
|
||||
"Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/13438\/Downloads.rar",
|
||||
"Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/13440\/скрин_.rar",
|
||||
"Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/1348\/Пользовательское_соглашение_в_ред._от_12.07.22.pdf",
|
||||
"Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/1350\/Снимок_экрана_2023-04-07_в_16.49.48.jpg.zip",
|
||||
"Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/1352\/Снимок_экрана_2023-04-07_в_16.46.13.jpg.zip",
|
||||
"Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/135648\/10_Доказательство_проведение_претензионной_работы__Белокуров_Павел_2_стр.pdf",
|
||||
"Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/135779\/11._Подтверждение_проведения_претензионной_работы_Матвеев_Иван_2_стр.pdf",
|
||||
"Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/135797\/11._Подтверждение_проведения_претензионной_работы_Еловиков__Матвей__2_стр.pdf",
|
||||
"Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/135901\/Заявление_о_выдачи_исполнительного_листа_по_делу_2-892-2024_1_стр.pdf",
|
||||
"Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/1361\/Мягкий_Вячеслав_Эдуардович.pdf",
|
||||
"Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/1363\/check_0004438001022024_9287440300779803_69531.pdf",
|
||||
"Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/136455\/7_заявление_потребителя_Виноградова__.pdf",
|
||||
"Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/136460\/Договор_05-22-2024-11-39-20_Виноградова__2_CTP#realfile.pdf",
|
||||
"Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/136462\/Подтверждение_оплаты_05-22-2024-11-39-29_Виноградова__1_CTP#realfile.pdf",
|
||||
"Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/136464\/Скриншот_прогресса_обучения_05-22-2024-11-40-02_Виноградова__3_CTP#realfile.pdf",
|
||||
"Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/136470\/Mail.ru_Письмо_от_oksana.volovikova@skillbox.ru.pdf",
|
||||
"Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/1365\/V3dQu8J3.jpg",
|
||||
"Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/136591\/IMG_3971.pdf",
|
||||
"Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/136606\/0_Исковое_заявление_по_делу_Сулиманов_ЧОУ_ДПО_-ОБРАЗОВАТЕЛЬНЫЕ_ТЕХНОЛОГИИ_-СКИЛБОКС_КОРОБКА_НАВЫКОВ-_4_стр.pdf",
|
||||
"Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/136608\/6_Расчет_исковых_требований_Сулиманов_ЧОУ_ДПО_-ОБРАЗОВАТЕЛЬНЫЕ_ТЕХНОЛОГИИ_-СКИЛБОКС_КОРОБКА_НАВЫКОВ-_1_стр.pdf",
|
||||
"Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/136837\/operation_statement_16.04.2024.pdf",
|
||||
"Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/13797\/5_Доказательство_проведение_претензионной_работы__Галюк_Игорь_2_стр.pdf",
|
||||
"Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/139911\/Опись_105005.pdf",
|
||||
"Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/14044\/0_Исковое_заявление_по_делу_Сопова_ИП_Гратило_Кристина_Сергеевна_4_стр.pdf",
|
||||
"Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/14046\/2_Расчет_исковых_требований_Сопова_ИП_Гратило_Кристина_Сергеевна_1_стр.pdf",
|
||||
"Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/14058\/oferta_май_2021_(1).pdf",
|
||||
"Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/14060\/Zayavlenie_na_oplatu_tovarov_uslug.pdf",
|
||||
"Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/14062\/Снимок_экрана_(3).png",
|
||||
"Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/14064\/oferta_май_2023.pdf",
|
||||
"Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/14107\/Доказательство_проведение_претензионной_работы__Петрова_Ольга_2_стр.pdf",
|
||||
"Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/14109\/Доказательство_проведение_претензионной_работы__Рубашова_Лилия_2_стр.pdf",
|
||||
"Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/14111\/Доказательство_проведение_претензионной_работы__Спасских_Мария_2_стр.pdf",
|
||||
"Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/14113\/Доказательство_проведение_претензионной_работы__Королева_Габриела_2_стр.pdf",
|
||||
"Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/14119\/Претензия_в_защиту_интересов_Имайкина_Ляйсан_1_стр.pdf",
|
||||
"Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/14123\/Доказательство_проведение_претензионной_работы__Имайкина_Ляйсан_2_стр.pdf",
|
||||
"Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/14125\/Доказательство_проведение_претензионной_работы__Бубличенко_Светлана_2_стр.pdf",
|
||||
"Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/1415\/Пользовательское_соглашение_Гикбрейнс_21.12.2021_(1).pdf",
|
||||
"Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/1417\/Справка_об_оплате_обучения.pdf",
|
||||
"Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/14171\/Dogovor.zip",
|
||||
"Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/14173\/Check.zip",
|
||||
"Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/14175\/LK.zip",
|
||||
"Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/14181\/доказательство_направления_иска_ответчику_Фатьянов_ООО_-ГИКБРЕИНС-_1_стр.pdf",
|
||||
"Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/14183\/0_Исковое_заявление_по_делу_Фатьянов_ООО_-ГИКБРЕИНС-_4_стр.pdf",
|
||||
"Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/14185\/2_Расчет_исковых_требований_Фатьянов_ООО_-ГИКБРЕИНС-_1_стр.pdf",
|
||||
"Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/1419\/Screenshot_10.png",
|
||||
"Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/141953\/соглашение_о_юрпомощи_Костоев_ООО_-ГИКБРЕИНС-.pdf",
|
||||
"Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/14199\/oferta.pdf",
|
||||
"Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/14201\/yiBD6HIbPWU.jpg",
|
||||
"Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/14203\/OaCjmcvzmDU.jpg",
|
||||
"Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/14205\/Претензия.docx",
|
||||
"Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/1421\/Письмо_о_смене_номера_лицензии_на_осуществление_образовательной_деятельности_(1).pdf",
|
||||
"Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/14210\/oferta.pdf",
|
||||
"Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/14212\/yiBD6HIbPWU.jpg",
|
||||
"Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/14214\/OaCjmcvzmDU.jpg",
|
||||
"Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/14216\/Претензия.docx",
|
||||
"Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/14225\/oferta.pdf",
|
||||
"Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/14227\/WinRAR_ZIP_archive.zip",
|
||||
"Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/14229\/WinRAR_ZIP_archive.zip",
|
||||
"Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/14231\/WinRAR_ZIP_archive.zip",
|
||||
"Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/14238\/oferta.pdf",
|
||||
"Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/14240\/оплата.pdf",
|
||||
"Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/14242\/прогресс.pdf",
|
||||
"Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/14244\/претензия.pdf",
|
||||
"Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/14246\/возврат.pdf",
|
||||
"Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/1431\/kvk.pdf",
|
||||
"Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/1433\/kvk.pdf",
|
||||
"Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/143394\/Исполнение_заявление_в_банк_Доверитель.pdf",
|
||||
"Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/1435\/Nikita_Ditelev_-_profil_polzovatelya_na_obrazovatelnom_portale_GeekBrains.pdf",
|
||||
"Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/1437\/IMG_0237.pdf",
|
||||
"Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/14372\/wsZBzMxr4H7IHCnhWiYw2po7I1onBJlr.pdf",
|
||||
"Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/14374\/zayavleniye_na_oplatu_tovarov_(uslug)_20.03.2023.pdf",
|
||||
"Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/14376\/прогресссс.pdf",
|
||||
"Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/14378\/претензионка.pdf",
|
||||
"Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/14382\/sber_statement_03-07-2023_17-08-57.pdf",
|
||||
"Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/14389\/kreditnyy_dogovor_individual'nykh_usloviy_po_kreditu_(iuk)_07.05.2023.pdf",
|
||||
"Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/1439\/IMG_0238.pdf",
|
||||
"Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/14391\/image_2023-05-25_20-16-31.png",
|
||||
"Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/14393\/image_2023-05-23_19-06-51.png",
|
||||
"Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/14395\/photo_2023-05-25_20-08-26.jpg",
|
||||
"Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/14397\/ответ_претензия.png",
|
||||
"Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/14404\/oferta_(1).pdf",
|
||||
"Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/14406\/0060235508_20230302.pdf",
|
||||
"Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/14408\/Screenshot_2023-05-26_at_11.17.44.pdf",
|
||||
"Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/14410\/Требование_(претензия)_о_расторжении_договора_и_возврате_денежных_средств.pdf",
|
||||
"Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/14412\/operation_statement_21.04.2023.pdf",
|
||||
"Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/14414\/Прочее.rar",
|
||||
"Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/14422\/Доказательство_проведение_претензионной_работы__Михайловский_Максим_2_стр.pdf",
|
||||
"Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/14435\/00_документы_суд.zip",
|
||||
"Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/14475\/Д16052023.pdf",
|
||||
"Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/14477\/2023-05-15_Хохлов_Максим_Дмитриевич_Приложение_к_оферте_CL-26387_84833.pdf",
|
||||
"Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/14479\/2023-07-04_17-59-33.pdf",
|
||||
"Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/14488\/00_Петров_документы_суд.zip",
|
||||
"Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/14593\/Доказательство_проведение_претензионной_работы__Ширмухамедов_Акбар_2_стр.pdf",
|
||||
"Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/14611\/доказательство_направления_иска_ответчику_Колосов_ООО_-СКИЛБОКС-_1_стр.pdf",
|
||||
"Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/14613\/0_Исковое_заявление_по_делу_Колосов_ООО_-СКИЛБОКС-_4_стр.pdf",
|
||||
"Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/14615\/2_Расчет_исковых_требований_Колосов_ООО_-СКИЛБОКС-_1_стр.pdf",
|
||||
"Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/14624\/Пользовательское_соглашение_в_ред._от_12.07.22.pdf",
|
||||
"Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/14626\/Подтверждение_оплаты_в_СберБанке.pdf",
|
||||
"Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/14628\/2023-07-05_17-15-34.png",
|
||||
"Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/14630\/Требования_поддержке.pdf",
|
||||
"Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/1469\/Доказательство_проведение_претензионной_работы__Плешкова_Алина_2_стр.pdf",
|
||||
"Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/14718\/доказательство_направления_иска_ответчику_Цапенко_ООО_-ГИКБРЕИНС-_1_стр.pdf",
|
||||
"Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/14720\/0_Исковое_заявление_по_делу_Цапенко_ООО_-ГИКБРЕИНС-_4_стр.pdf",
|
||||
"Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/14722\/2_Расчет_исковых_требований_Цапенко_ООО_-ГИКБРЕИНС-_1_стр.pdf",
|
||||
"Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/14737\/Колосов_документы_суд.zip",
|
||||
"Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/14744\/Оферта._Редакция_от_29_августа_2022г.pdf",
|
||||
"Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/14746\/Zaâvlenie_na_oplatu_tovarov_(uslug).pdf",
|
||||
"Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/14748\/IMG_4083.pdf",
|
||||
"Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/14750\/IMG_4084.pdf",
|
||||
"Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/14820\/Цапенко_документы_суд.zip",
|
||||
"Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/14827\/kvk_(34)_(1).pdf",
|
||||
"Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/14829\/3rNhCFnHlOs.pdf",
|
||||
"Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/14831\/onOUwSZWvdU.pdf",
|
||||
"Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/14839\/kvk_(34)_(1).pdf",
|
||||
"Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/14841\/3rNhCFnHlOs.pdf",
|
||||
"Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/14843\/onOUwSZWvdU.pdf",
|
||||
"Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/14846\/доказательство_направления_иска_ответчику_Уварова_ИП_Ложкина_Анжелика_Геннадиевна_1_стр.pdf",
|
||||
"Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/14848\/0_Исковое_заявление_по_делу_Уварова_ИП_Ложкина_Анжелика_Геннадиевна_4_стр.pdf",
|
||||
"Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/14850\/2_Расчет_исковых_требований_Уварова_ИП_Ложкина_Анжелика_Геннадиевна_1_стр.pdf",
|
||||
"Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/14853\/доказательство_направления_иска_ответчику_Гарбузов_АНО_-ЦРП-_1_стр.pdf",
|
||||
"Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/14855\/0_Исковое_заявление_по_делу_Гарбузов_АНО_-ЦРП-_4_стр.pdf",
|
||||
"Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/14857\/2_Расчет_исковых_требований_Гарбузов_АНО_-ЦРП-_1_стр.pdf",
|
||||
"Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/14860\/доказательство_направления_иска_ответчику_Поселянина_ООО_-ГИКБРЕИНС-_1_стр.pdf",
|
||||
"Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/14862\/0_Исковое_заявление_по_делу_Поселянина_ООО_-ГИКБРЕИНС-_4_стр.pdf",
|
||||
"Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/14864\/2_Расчет_исковых_требований_Поселянина_ООО_-ГИКБРЕИНС-_1_стр.pdf",
|
||||
"Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/14867\/доказательство_направления_иска_ответчику_Кукса_ООО_-СКИЛБОКС-_1_стр.pdf",
|
||||
"Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/14869\/0_Исковое_заявление_по_делу_Кукса_ООО_-СКИЛБОКС-_4_стр.pdf",
|
||||
"Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/14874\/2_Расчет_исковых_требований_Кукса_ООО_-СКИЛБОКС-_1_стр.pdf",
|
||||
"Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/14898\/доказательство_направления_иска_ответчику_Жуперина_ООО_-Скилбокс-_1_стр.pdf",
|
||||
"Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/14900\/0_Исковое_заявление_по_делу_Жуперина_ООО_-Скилбокс-_4_стр.pdf",
|
||||
"Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/14902\/2_Расчет_исковых_требований_Жуперина_ООО_-Скилбокс-_1_стр.pdf",
|
||||
"Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/14905\/доказательство_направления_иска_ответчику_Мягкий_ООО_-СКИЛБОКС-_1_стр.pdf",
|
||||
"Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/14907\/0_Исковое_заявление_по_делу_Мягкий_ООО_-СКИЛБОКС-_4_стр.pdf",
|
||||
"Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/14909\/2_Расчет_исковых_требований_Мягкий_ООО_-СКИЛБОКС-_1_стр.pdf",
|
||||
"Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/14912\/доказательство_направления_иска_ответчику_Кузьмина__ООО_-СКИЛБОКС-_1_стр.pdf",
|
||||
"Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/14914\/0_Исковое_заявление_по_делу_Кузьмина__ООО_-СКИЛБОКС-_4_стр_(1).pdf",
|
||||
"Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/14916\/2_Расчет_исковых_требований_Кузьмина__ООО_-СКИЛБОКС-_1_стр.pdf",
|
||||
"Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/14934\/доказательство_направления_иска_ответчику_Дителев_ООО_-ГИКБРЕИНС-_1_стр.pdf",
|
||||
"Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/14936\/0_Исковое_заявление_по_делу_Дителев_ООО_-ГИКБРЕИНС-_4_стр.pdf",
|
||||
"Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/14941\/2_Расчет_исковых_требований_Дителев_ООО_-ГИКБРЕИНС-_1_стр.pdf",
|
||||
"Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/14944\/доказательство_направления_иска_ответчику_Нечаева__ИП_Ширков_Данила_Семёнович_1_стр.pdf",
|
||||
"Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/14946\/0_Исковое_заявление_по_делу_Нечаева__ИП_Ширков_Данила_Семёнович_4_стр.pdf",
|
||||
"Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/14948\/2_Расчет_исковых_требований_Нечаева__ИП_Ширков_Данила_Семёнович_1_стр.pdf",
|
||||
"Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/1516\/Оферта._Редакция_от_29_августа_2022г.pdf",
|
||||
"Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/1518\/КАЮМОВ_ДАНИИЛ_ФЛЮРОВИЧ_(1).pdf",
|
||||
"Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/1520\/Фотографии_личного_кабинета.pdf",
|
||||
"Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/15223\/Договор_Дизайн_цифровых_продуктов_70_первые_полгода_бесплатно_Амбарцумян_Диана_Эдуардовна.pdf",
|
||||
"Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/15225\/Договор_Дизайн_цифровых_продуктов_70_первые_полгода_бесплатно_Амбарцумян_Диана_Эдуардовна.pdf",
|
||||
"Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/15227\/Договор_Дизайн_цифровых_продуктов_70_первые_полгода_бесплатно_Амбарцумян_Диана_Эдуардовна.pdf",
|
||||
"Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/15229\/претензия_ответ_Амбарцумян_Д.Э..pdf",
|
||||
"Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/152984\/7_заявление_потребителя_Пономарева__.pdf",
|
||||
"Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/152989\/Договор_05-23-2024-08-50-46_Пономарева__1_CTP#realfile.pdf",
|
||||
"Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/152991\/Подтверждение_оплаты_05-23-2024-08-54-48_Пономарева__1_CTP#realfile.pdf",
|
||||
"Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/152993\/Программа_обучения_05-23-2024-08-55-21_Пономарева__1_CTP#realfile.pdf",
|
||||
"Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/152995\/Скриншот_прогресса_обучения_05-23-2024-08-58-09_Пономарева__1_CTP#realfile.pdf",
|
||||
"Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/152997\/Претензия_05-23-2024-08-59-05_Пономарева__1_CTP#realfile.pdf",
|
||||
"Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/15309\/Заявление_(1).PDF",
|
||||
"Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/15345\/_.zip",
|
||||
"Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/154432\/0_Исковое_заявление_по_делу_Окуньков__ООО_-ТЕРРА_ЭЙАЙ-_4_стр.pdf",
|
||||
"Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/155051\/7777777_Заявление_о_выдачи_исполнительного_листа_.pdf",
|
||||
"Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/155057\/7777777_Заявление_о_выдачи_исполнительного_листа_.pdf",
|
||||
"Не найдена версия для восстановления: crm2\/CRM_Active_Files\/Documents\/155260\/7777777_Заявление_о_выдачи_исполнительного_листа_.pdf"
|
||||
]
|
||||
}
|
||||
7
restore_log_2025-11-25_10-24-33.json
Normal file
7
restore_log_2025-11-25_10-24-33.json
Normal file
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"total_markers": 0,
|
||||
"restored": 0,
|
||||
"failed": 0,
|
||||
"skipped": 0,
|
||||
"errors": []
|
||||
}
|
||||
7
restore_log_2025-11-25_10-28-14.json
Normal file
7
restore_log_2025-11-25_10-28-14.json
Normal file
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"total_markers": 0,
|
||||
"restored": 0,
|
||||
"failed": 0,
|
||||
"skipped": 0,
|
||||
"errors": []
|
||||
}
|
||||
7
restore_log_2025-11-25_17-08-20.json
Normal file
7
restore_log_2025-11-25_17-08-20.json
Normal file
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"total_markers": 46,
|
||||
"restored": 2,
|
||||
"failed": 43,
|
||||
"skipped": 1,
|
||||
"errors": []
|
||||
}
|
||||
168
restore_project_373977.php
Normal file
168
restore_project_373977.php
Normal file
@@ -0,0 +1,168 @@
|
||||
<?php
|
||||
/**
|
||||
* Восстановление документов проекта 373977
|
||||
*/
|
||||
|
||||
error_reporting(E_ALL);
|
||||
ini_set('display_errors', 1);
|
||||
|
||||
require_once '/var/www/fastuser/data/www/crm.clientright.ru/vendor/autoload.php';
|
||||
$config = require '/var/www/fastuser/data/www/crm.clientright.ru/crm_extensions/file_storage/config.php';
|
||||
|
||||
$projectId = 373977;
|
||||
$s3Bucket = $config['s3']['bucket'];
|
||||
$prefix = 'crm2/CRM_Active_Files/Documents/Project/Полулях_ЧУ_ДПО_ГОРОДСКАЯ_АКАДЕМИЯ_УРБАН_373977/';
|
||||
|
||||
$files = [
|
||||
373981 => '8_Договор_на_оказание_услуг_373981.pdf',
|
||||
373983 => '9_Подтверждение_оплаты_по_договору_373983.pdf',
|
||||
373985 => '10_2_Скрин_личного_кабинета_Истца_и_программа_обуч_373985.pdf',
|
||||
373987 => '10_1_Скрин_личного_кабинета_Истца_и_программа_обуч_373987.pdf',
|
||||
373989 => '11_1_Подтверждение_проведения_претензионной_работы_373989.pdf',
|
||||
373991 => '7_заявление_потребителя_373991.pdf',
|
||||
374017 => '11_Доказательство_соблюдения_претензионного_порядк_374017.pdf',
|
||||
375402 => '11.2_Претензия_в_защиту_интересов_Полулях_Ольга_1_375402.pdf',
|
||||
375404 => '11.3_Доказательство_оплаты_направления_претензии_о_375404.pdf',
|
||||
375406 => '11.4_Доказательство_направления_претензии_ответчик_375406.pdf',
|
||||
376051 => '0_Исковое_заявление_по_делу_Полулях_7_стр_376051.pdf',
|
||||
376054 => '6_Расчет_исковых_требований_Полулях_1_стр_376054.pdf',
|
||||
376080 => '12.1_Доказательство_оплаты_направления_иска_ответч_376080.pdf',
|
||||
376082 => '12.2_Доказательство_направления_иска_ответчику_376082.pdf',
|
||||
];
|
||||
|
||||
echo "=== ВОССТАНОВЛЕНИЕ ДОКУМЕНТОВ ПРОЕКТА {$projectId} ===\n";
|
||||
echo str_repeat("=", 80) . "\n\n";
|
||||
|
||||
$dryRun = isset($argv[1]) && $argv[1] === '--dry-run';
|
||||
|
||||
if ($dryRun) {
|
||||
echo "⚠️ РЕЖИМ ПРОВЕРКИ (dry-run)\n\n";
|
||||
}
|
||||
|
||||
try {
|
||||
$s3Client = new \Aws\S3\S3Client([
|
||||
'version' => 'latest',
|
||||
'region' => $config['s3']['region'],
|
||||
'endpoint' => $config['s3']['endpoint'],
|
||||
'use_path_style_endpoint' => true,
|
||||
'credentials' => [
|
||||
'key' => $config['s3']['key'],
|
||||
'secret' => $config['s3']['secret'],
|
||||
],
|
||||
'suppress_php_deprecation_warning' => true
|
||||
]);
|
||||
|
||||
$stats = [
|
||||
'total' => count($files),
|
||||
'restored' => 0,
|
||||
'already_exists' => 0,
|
||||
'no_versions' => 0,
|
||||
'failed' => 0,
|
||||
];
|
||||
|
||||
foreach ($files as $docId => $filename) {
|
||||
$key = $prefix . $filename;
|
||||
|
||||
echo "Документ ID: {$docId}\n";
|
||||
echo " Файл: {$filename}\n";
|
||||
|
||||
// Проверяем, существует ли файл
|
||||
if ($s3Client->doesObjectExist($s3Bucket, $key)) {
|
||||
echo " ✅ Файл уже существует\n\n";
|
||||
$stats['already_exists']++;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Проверяем версии
|
||||
try {
|
||||
$versions = $s3Client->listObjectVersions([
|
||||
'Bucket' => $s3Bucket,
|
||||
'Prefix' => $key,
|
||||
'MaxKeys' => 10,
|
||||
]);
|
||||
|
||||
$deleteMarkers = $versions['DeleteMarkers'] ?? [];
|
||||
$fileVersions = $versions['Versions'] ?? [];
|
||||
|
||||
if (empty($deleteMarkers) && empty($fileVersions)) {
|
||||
echo " ❌ Нет версий для восстановления\n\n";
|
||||
$stats['no_versions']++;
|
||||
continue;
|
||||
}
|
||||
|
||||
echo " Найдено delete markers: " . count($deleteMarkers) . "\n";
|
||||
echo " Найдено версий: " . count($fileVersions) . "\n";
|
||||
|
||||
if (!$dryRun) {
|
||||
// Удаляем все delete markers
|
||||
foreach ($deleteMarkers as $marker) {
|
||||
try {
|
||||
$s3Client->deleteObject([
|
||||
'Bucket' => $s3Bucket,
|
||||
'Key' => $key,
|
||||
'VersionId' => $marker['VersionId'],
|
||||
]);
|
||||
echo " ✅ Delete marker удален\n";
|
||||
} catch (Exception $e) {
|
||||
echo " ⚠️ Ошибка удаления delete marker: " . $e->getMessage() . "\n";
|
||||
}
|
||||
}
|
||||
|
||||
// Проверяем, появился ли файл после удаления delete marker
|
||||
if (!$s3Client->doesObjectExist($s3Bucket, $key) && !empty($fileVersions)) {
|
||||
// Копируем последнюю версию
|
||||
$latestVersion = $fileVersions[0];
|
||||
try {
|
||||
$s3Client->copyObject([
|
||||
'Bucket' => $s3Bucket,
|
||||
'Key' => $key,
|
||||
'CopySource' => "{$s3Bucket}/{$key}?versionId={$latestVersion['VersionId']}",
|
||||
]);
|
||||
echo " ✅ Файл восстановлен из версии\n";
|
||||
} catch (Exception $e) {
|
||||
echo " ⚠️ Ошибка копирования версии: " . $e->getMessage() . "\n";
|
||||
}
|
||||
} else {
|
||||
echo " ✅ Файл восстановлен\n";
|
||||
}
|
||||
|
||||
$stats['restored']++;
|
||||
sleep(1);
|
||||
|
||||
} else {
|
||||
echo " ⏸️ Будет восстановлен (dry-run)\n";
|
||||
$stats['restored']++;
|
||||
}
|
||||
|
||||
} catch (Exception $e) {
|
||||
echo " ❌ Ошибка: " . $e->getMessage() . "\n";
|
||||
$stats['failed']++;
|
||||
}
|
||||
|
||||
echo "\n";
|
||||
}
|
||||
|
||||
// Итоги
|
||||
echo str_repeat("=", 80) . "\n";
|
||||
echo "ИТОГИ:\n";
|
||||
echo "Всего файлов: {$stats['total']}\n";
|
||||
|
||||
if (!$dryRun) {
|
||||
echo "Восстановлено: {$stats['restored']}\n";
|
||||
echo "Уже существует: {$stats['already_exists']}\n";
|
||||
echo "Нет версий: {$stats['no_versions']}\n";
|
||||
echo "Ошибок: {$stats['failed']}\n";
|
||||
} else {
|
||||
echo "Будет восстановлено: {$stats['restored']}\n";
|
||||
echo "Уже существует: {$stats['already_exists']}\n";
|
||||
echo "Нет версий: {$stats['no_versions']}\n";
|
||||
}
|
||||
|
||||
echo "\n=== ГОТОВО ===\n";
|
||||
|
||||
} catch (Exception $e) {
|
||||
echo "❌ Критическая ошибка: " . $e->getMessage() . "\n";
|
||||
exit(1);
|
||||
}
|
||||
|
||||
|
||||
132
restore_project_391584.php
Normal file
132
restore_project_391584.php
Normal file
@@ -0,0 +1,132 @@
|
||||
<?php
|
||||
/**
|
||||
* Восстановление файлов проекта 391584
|
||||
*/
|
||||
|
||||
error_reporting(E_ALL);
|
||||
ini_set('display_errors', 1);
|
||||
|
||||
require_once '/var/www/fastuser/data/www/crm.clientright.ru/vendor/autoload.php';
|
||||
$config = require '/var/www/fastuser/data/www/crm.clientright.ru/crm_extensions/file_storage/config.php';
|
||||
|
||||
$projectId = 391584;
|
||||
$s3Bucket = $config['s3']['bucket'];
|
||||
$projectPrefix = 'crm2/CRM_Active_Files/Documents/Project/Чужба_ЧОУ_ДПО_ОБРАЗОВАТЕЛЬНЫЕ_ТЕХНОЛОГИИ_СКИЛБОКС_(КОРОБКА_НАВЫКОВ)_391584/';
|
||||
|
||||
// Документы проекта
|
||||
$documents = [
|
||||
391587 => '8_Договор_на_оказание_услуг_391587.pdf',
|
||||
391589 => '9_Подтверждение_оплаты_по_договору_391589.pdf',
|
||||
391591 => '10_1_Скрин_личного_кабинета_Истца_и_программа_обуч_391591.pdf',
|
||||
391593 => '7_заявление_потребителя_391593.pdf',
|
||||
392332 => '11_Доказательство_соблюдения_претензионного_порядк_392332.pdf',
|
||||
392472 => '11.1_Доказательство_соблюдения_претензионного_поря_392472.pdf',
|
||||
392475 => '11.2_Доказательство_соблюдения_претензионного_поря_392475.pdf',
|
||||
395136 => '6_Расчет_иска_Чужба_395136.pdf',
|
||||
395157 => '0_Исковое_заявление_по_делу_Чужба_ЧОУ_ДПО_ОБРАЗОВА_395157.pdf',
|
||||
395744 => '12.1_Доказательство_оплаты_направления_иска_ответч_395744.pdf',
|
||||
];
|
||||
|
||||
echo "=== ВОССТАНОВЛЕНИЕ ФАЙЛОВ ПРОЕКТА {$projectId} ===\n";
|
||||
echo str_repeat("=", 80) . "\n\n";
|
||||
|
||||
try {
|
||||
$s3Client = new \Aws\S3\S3Client([
|
||||
'version' => 'latest',
|
||||
'region' => $config['s3']['region'],
|
||||
'endpoint' => $config['s3']['endpoint'],
|
||||
'use_path_style_endpoint' => true,
|
||||
'credentials' => [
|
||||
'key' => $config['s3']['key'],
|
||||
'secret' => $config['s3']['secret'],
|
||||
],
|
||||
'suppress_php_deprecation_warning' => true
|
||||
]);
|
||||
|
||||
$stats = [
|
||||
'total' => count($documents),
|
||||
'restored' => 0,
|
||||
'not_found' => 0,
|
||||
'already_exists' => 0,
|
||||
];
|
||||
|
||||
foreach ($documents as $docId => $filename) {
|
||||
$s3Key = $projectPrefix . $filename;
|
||||
|
||||
echo "Документ ID: {$docId}\n";
|
||||
echo " Файл: {$filename}\n";
|
||||
|
||||
// Проверяем, существует ли файл
|
||||
if ($s3Client->doesObjectExist($s3Bucket, $s3Key)) {
|
||||
echo " ✅ Файл уже существует\n\n";
|
||||
$stats['already_exists']++;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Проверяем наличие версий и delete markers
|
||||
try {
|
||||
$versions = $s3Client->listObjectVersions([
|
||||
'Bucket' => $s3Bucket,
|
||||
'Prefix' => $s3Key,
|
||||
'MaxKeys' => 10,
|
||||
]);
|
||||
|
||||
$deleteMarkers = $versions['DeleteMarkers'] ?? [];
|
||||
$fileVersions = $versions['Versions'] ?? [];
|
||||
|
||||
if (empty($deleteMarkers) && empty($fileVersions)) {
|
||||
echo " ❌ Файл не найден и нет версий\n\n";
|
||||
$stats['not_found']++;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Удаляем delete markers
|
||||
foreach ($deleteMarkers as $marker) {
|
||||
$s3Client->deleteObject([
|
||||
'Bucket' => $s3Bucket,
|
||||
'Key' => $s3Key,
|
||||
'VersionId' => $marker['VersionId'],
|
||||
]);
|
||||
echo " ✅ Delete marker удален\n";
|
||||
}
|
||||
|
||||
// Если файл все еще не существует, восстанавливаем из версии
|
||||
if (!$s3Client->doesObjectExist($s3Bucket, $s3Key) && !empty($fileVersions)) {
|
||||
$latestVersion = $fileVersions[0];
|
||||
$s3Client->copyObject([
|
||||
'Bucket' => $s3Bucket,
|
||||
'Key' => $s3Key,
|
||||
'CopySource' => "{$s3Bucket}/{$s3Key}?versionId={$latestVersion['VersionId']}",
|
||||
]);
|
||||
echo " ✅ Файл восстановлен из версии\n";
|
||||
} else {
|
||||
echo " ✅ Файл восстановлен\n";
|
||||
}
|
||||
|
||||
$stats['restored']++;
|
||||
sleep(1);
|
||||
|
||||
} catch (Exception $e) {
|
||||
echo " ❌ Ошибка: " . $e->getMessage() . "\n";
|
||||
$stats['not_found']++;
|
||||
}
|
||||
|
||||
echo "\n";
|
||||
}
|
||||
|
||||
// Итоги
|
||||
echo str_repeat("=", 80) . "\n";
|
||||
echo "ИТОГИ:\n";
|
||||
echo "Всего документов: {$stats['total']}\n";
|
||||
echo "✅ Восстановлено: {$stats['restored']}\n";
|
||||
echo "✅ Уже существовало: {$stats['already_exists']}\n";
|
||||
echo "❌ Не найдено: {$stats['not_found']}\n\n";
|
||||
|
||||
echo "=== ГОТОВО ===\n";
|
||||
|
||||
} catch (Exception $e) {
|
||||
echo "❌ Критическая ошибка: " . $e->getMessage() . "\n";
|
||||
exit(1);
|
||||
}
|
||||
|
||||
|
||||
199
restore_project_394091.php
Normal file
199
restore_project_394091.php
Normal file
@@ -0,0 +1,199 @@
|
||||
<?php
|
||||
/**
|
||||
* Скрипт для восстановления файлов проекта 394091
|
||||
*/
|
||||
|
||||
error_reporting(E_ALL);
|
||||
ini_set('display_errors', 1);
|
||||
set_time_limit(0);
|
||||
|
||||
require_once '/var/www/fastuser/data/www/crm.clientright.ru/vendor/autoload.php';
|
||||
require_once '/var/www/fastuser/data/www/crm.clientright.ru/config.inc.php';
|
||||
|
||||
$config = require '/var/www/fastuser/data/www/crm.clientright.ru/crm_extensions/file_storage/config.php';
|
||||
$s3Bucket = $config['s3']['bucket'];
|
||||
|
||||
$projectId = 394091;
|
||||
|
||||
echo "=== ВОССТАНОВЛЕНИЕ ФАЙЛОВ ПРОЕКТА {$projectId} ===\n";
|
||||
echo str_repeat("=", 80) . "\n\n";
|
||||
|
||||
// Параметры
|
||||
$dryRun = isset($argv[1]) && $argv[1] === '--dry-run';
|
||||
|
||||
if ($dryRun) {
|
||||
echo "⚠️ РЕЖИМ ПРОВЕРКИ (dry-run) - файлы не будут восстановлены\n\n";
|
||||
}
|
||||
|
||||
try {
|
||||
$s3Client = new \Aws\S3\S3Client([
|
||||
'version' => 'latest',
|
||||
'region' => $config['s3']['region'],
|
||||
'endpoint' => $config['s3']['endpoint'],
|
||||
'use_path_style_endpoint' => true,
|
||||
'credentials' => [
|
||||
'key' => $config['s3']['key'],
|
||||
'secret' => $config['s3']['secret'],
|
||||
],
|
||||
'suppress_php_deprecation_warning' => true
|
||||
]);
|
||||
|
||||
// Получаем документы проекта из БД
|
||||
echo "Подключение к БД...\n";
|
||||
$db = PearDatabase::getInstance();
|
||||
echo "✅ БД подключена\n";
|
||||
|
||||
echo "Выполнение запроса...\n";
|
||||
$result = $db->pquery("
|
||||
SELECT n.notesid, n.filename, n.filelocationtype, n.s3_bucket, n.s3_key, n.filesize
|
||||
FROM vtiger_notes n
|
||||
INNER JOIN vtiger_senotesrel sn ON sn.notesid = n.notesid
|
||||
WHERE sn.crmid = ?
|
||||
ORDER BY n.notesid
|
||||
", array($projectId));
|
||||
|
||||
echo "✅ Запрос выполнен\n";
|
||||
|
||||
$totalDocs = $db->num_rows($result);
|
||||
echo "Найдено документов в БД: {$totalDocs}\n\n";
|
||||
|
||||
if ($totalDocs == 0) {
|
||||
echo "Документы не найдены!\n";
|
||||
exit(0);
|
||||
}
|
||||
|
||||
$stats = [
|
||||
'total_docs' => $totalDocs,
|
||||
'existing' => 0,
|
||||
'deleted' => 0,
|
||||
'missing' => 0,
|
||||
'restored' => 0,
|
||||
'failed' => 0,
|
||||
'errors' => []
|
||||
];
|
||||
|
||||
// Проверяем каждый документ
|
||||
while ($row = $db->fetch_array($result)) {
|
||||
$s3Key = $row['s3_key'];
|
||||
$docId = $row['notesid'];
|
||||
$filename = $row['filename'];
|
||||
|
||||
echo "Документ ID: {$docId} | Файл: {$filename}\n";
|
||||
|
||||
if (empty($s3Key)) {
|
||||
echo " ⚠️ Нет S3 ключа\n\n";
|
||||
$stats['missing']++;
|
||||
continue;
|
||||
}
|
||||
|
||||
echo " S3 ключ: {$s3Key}\n";
|
||||
|
||||
// Проверяем существование файла
|
||||
$exists = $s3Client->doesObjectExist($s3Bucket, $s3Key);
|
||||
|
||||
if ($exists) {
|
||||
echo " ✅ Файл существует\n\n";
|
||||
$stats['existing']++;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Проверяем версии и delete markers
|
||||
try {
|
||||
$versions = $s3Client->listObjectVersions([
|
||||
'Bucket' => $s3Bucket,
|
||||
'Prefix' => $s3Key,
|
||||
]);
|
||||
|
||||
$deleteMarker = null;
|
||||
$fileVersion = null;
|
||||
|
||||
foreach ($versions['Versions'] ?? [] as $version) {
|
||||
if (isset($version['IsDeleteMarker']) && $version['IsDeleteMarker']) {
|
||||
$deleteMarker = $version;
|
||||
} else {
|
||||
$fileVersion = $version;
|
||||
}
|
||||
}
|
||||
|
||||
if ($deleteMarker) {
|
||||
echo " ❌ Файл удален (delete marker от " . $deleteMarker['LastModified']->format('Y-m-d H:i:s') . ")\n";
|
||||
|
||||
if (!$dryRun) {
|
||||
// Удаляем delete marker
|
||||
try {
|
||||
$s3Client->deleteObject([
|
||||
'Bucket' => $s3Bucket,
|
||||
'Key' => $s3Key,
|
||||
'VersionId' => $deleteMarker['VersionId'],
|
||||
]);
|
||||
|
||||
// Если есть версия файла, копируем её
|
||||
if ($fileVersion) {
|
||||
$s3Client->copyObject([
|
||||
'Bucket' => $s3Bucket,
|
||||
'Key' => $s3Key,
|
||||
'CopySource' => "{$s3Bucket}/{$s3Key}?versionId={$fileVersion['VersionId']}",
|
||||
]);
|
||||
echo " ✅ Файл восстановлен из версии\n";
|
||||
} else {
|
||||
echo " ⚠️ Delete marker удален, но версия файла не найдена\n";
|
||||
}
|
||||
|
||||
$stats['restored']++;
|
||||
sleep(1); // Пауза между запросами
|
||||
|
||||
} catch (Exception $e) {
|
||||
echo " ❌ Ошибка восстановления: " . $e->getMessage() . "\n";
|
||||
$stats['failed']++;
|
||||
$stats['errors'][] = "{$s3Key}: " . $e->getMessage();
|
||||
}
|
||||
} else {
|
||||
echo " ⏸️ Будет восстановлен (dry-run)\n";
|
||||
$stats['restored']++;
|
||||
}
|
||||
|
||||
$stats['deleted']++;
|
||||
|
||||
} else {
|
||||
echo " ⚠️ Файл отсутствует, но delete marker не найден\n";
|
||||
$stats['missing']++;
|
||||
}
|
||||
|
||||
} catch (Exception $e) {
|
||||
echo " ❌ Ошибка проверки версий: " . $e->getMessage() . "\n";
|
||||
$stats['failed']++;
|
||||
$stats['errors'][] = "{$s3Key}: " . $e->getMessage();
|
||||
}
|
||||
|
||||
echo "\n";
|
||||
}
|
||||
|
||||
// Итоговая статистика
|
||||
echo str_repeat("=", 80) . "\n";
|
||||
echo "ИТОГОВАЯ СТАТИСТИКА:\n\n";
|
||||
echo "Всего документов: {$stats['total_docs']}\n";
|
||||
echo "Существующих файлов: {$stats['existing']}\n";
|
||||
echo "Удаленных файлов (delete marker): {$stats['deleted']}\n";
|
||||
echo "Отсутствующих файлов: {$stats['missing']}\n";
|
||||
|
||||
if (!$dryRun) {
|
||||
echo "Восстановлено: {$stats['restored']}\n";
|
||||
echo "Ошибок: {$stats['failed']}\n";
|
||||
} else {
|
||||
echo "Будет восстановлено: {$stats['restored']}\n";
|
||||
}
|
||||
|
||||
if (!empty($stats['errors'])) {
|
||||
echo "\nОшибки:\n";
|
||||
foreach ($stats['errors'] as $error) {
|
||||
echo " - {$error}\n";
|
||||
}
|
||||
}
|
||||
|
||||
echo "\n=== ГОТОВО ===\n";
|
||||
|
||||
} catch (Exception $e) {
|
||||
echo "❌ Критическая ошибка: " . $e->getMessage() . "\n";
|
||||
echo "Stack trace:\n" . $e->getTraceAsString() . "\n";
|
||||
exit(1);
|
||||
}
|
||||
156
restore_project_394091_simple.php
Normal file
156
restore_project_394091_simple.php
Normal file
@@ -0,0 +1,156 @@
|
||||
<?php
|
||||
/**
|
||||
* Упрощенный скрипт для восстановления файлов проекта 394091
|
||||
*/
|
||||
|
||||
error_reporting(E_ALL);
|
||||
ini_set('display_errors', 1);
|
||||
|
||||
require_once '/var/www/fastuser/data/www/crm.clientright.ru/vendor/autoload.php';
|
||||
$config = require '/var/www/fastuser/data/www/crm.clientright.ru/crm_extensions/file_storage/config.php';
|
||||
|
||||
$s3Bucket = $config['s3']['bucket'];
|
||||
$projectId = 394091;
|
||||
|
||||
// Файлы проекта из БД
|
||||
$files = [
|
||||
'crm2/CRM_Active_Files/Documents/Project/Згурский_ООО_РЕНТСОФТ_394091/Договор_394094.pdf',
|
||||
'crm2/CRM_Active_Files/Documents/Project/Згурский_ООО_РЕНТСОФТ_394091/Подтверждение_оплаты_394096.pdf',
|
||||
'crm2/CRM_Active_Files/Documents/Project/Згурский_ООО_РЕНТСОФТ_394091/Претензия_394098.pdf',
|
||||
'crm2/CRM_Active_Files/Documents/Project/Згурский_ООО_РЕНТСОФТ_394091/Ответ_на_претензию_394100.pdf',
|
||||
'crm2/CRM_Active_Files/Documents/Project/Згурский_ООО_РЕНТСОФТ_394091/7_заявление_потребителя_394105.pdf',
|
||||
'crm2/CRM_Active_Files/Documents/Project/Згурский_ООО_РЕНТСОФТ_394091/experimental_report_20251018_095026_395943.xlsx',
|
||||
];
|
||||
|
||||
echo "=== ВОССТАНОВЛЕНИЕ ФАЙЛОВ ПРОЕКТА {$projectId} ===\n";
|
||||
echo str_repeat("=", 80) . "\n\n";
|
||||
|
||||
$dryRun = isset($argv[1]) && $argv[1] === '--dry-run';
|
||||
|
||||
if ($dryRun) {
|
||||
echo "⚠️ РЕЖИМ ПРОВЕРКИ (dry-run)\n\n";
|
||||
}
|
||||
|
||||
try {
|
||||
$s3Client = new \Aws\S3\S3Client([
|
||||
'version' => 'latest',
|
||||
'region' => $config['s3']['region'],
|
||||
'endpoint' => $config['s3']['endpoint'],
|
||||
'use_path_style_endpoint' => true,
|
||||
'credentials' => [
|
||||
'key' => $config['s3']['key'],
|
||||
'secret' => $config['s3']['secret'],
|
||||
],
|
||||
'suppress_php_deprecation_warning' => true
|
||||
]);
|
||||
|
||||
$stats = [
|
||||
'total' => count($files),
|
||||
'existing' => 0,
|
||||
'deleted' => 0,
|
||||
'missing' => 0,
|
||||
'restored' => 0,
|
||||
'failed' => 0,
|
||||
];
|
||||
|
||||
foreach ($files as $s3Key) {
|
||||
$filename = basename($s3Key);
|
||||
echo "Файл: {$filename}\n";
|
||||
echo " Путь: {$s3Key}\n";
|
||||
|
||||
// Проверяем существование
|
||||
$exists = $s3Client->doesObjectExist($s3Bucket, $s3Key);
|
||||
|
||||
if ($exists) {
|
||||
echo " ✅ Файл существует\n\n";
|
||||
$stats['existing']++;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Проверяем версии
|
||||
try {
|
||||
$versions = $s3Client->listObjectVersions([
|
||||
'Bucket' => $s3Bucket,
|
||||
'Prefix' => $s3Key,
|
||||
]);
|
||||
|
||||
$deleteMarker = null;
|
||||
$fileVersion = null;
|
||||
|
||||
foreach ($versions['Versions'] ?? [] as $version) {
|
||||
if (isset($version['IsDeleteMarker']) && $version['IsDeleteMarker']) {
|
||||
$deleteMarker = $version;
|
||||
} else {
|
||||
$fileVersion = $version;
|
||||
}
|
||||
}
|
||||
|
||||
if ($deleteMarker) {
|
||||
echo " ❌ Файл удален (delete marker от " . $deleteMarker['LastModified']->format('Y-m-d H:i:s') . ")\n";
|
||||
|
||||
if (!$dryRun) {
|
||||
// Удаляем delete marker
|
||||
$s3Client->deleteObject([
|
||||
'Bucket' => $s3Bucket,
|
||||
'Key' => $s3Key,
|
||||
'VersionId' => $deleteMarker['VersionId'],
|
||||
]);
|
||||
echo " ✅ Delete marker удален\n";
|
||||
|
||||
// Восстанавливаем файл из версии
|
||||
if ($fileVersion) {
|
||||
$s3Client->copyObject([
|
||||
'Bucket' => $s3Bucket,
|
||||
'Key' => $s3Key,
|
||||
'CopySource' => "{$s3Bucket}/{$s3Key}?versionId={$fileVersion['VersionId']}",
|
||||
]);
|
||||
echo " ✅ Файл восстановлен из версии\n";
|
||||
$stats['restored']++;
|
||||
} else {
|
||||
echo " ⚠️ Версия файла не найдена\n";
|
||||
$stats['missing']++;
|
||||
}
|
||||
|
||||
sleep(1);
|
||||
} else {
|
||||
echo " ⏸️ Будет восстановлен (dry-run)\n";
|
||||
$stats['restored']++;
|
||||
}
|
||||
|
||||
$stats['deleted']++;
|
||||
|
||||
} else {
|
||||
echo " ⚠️ Файл отсутствует, delete marker не найден\n";
|
||||
$stats['missing']++;
|
||||
}
|
||||
|
||||
} catch (Exception $e) {
|
||||
echo " ❌ Ошибка: " . $e->getMessage() . "\n";
|
||||
$stats['failed']++;
|
||||
}
|
||||
|
||||
echo "\n";
|
||||
}
|
||||
|
||||
// Итоги
|
||||
echo str_repeat("=", 80) . "\n";
|
||||
echo "ИТОГИ:\n";
|
||||
echo "Всего файлов: {$stats['total']}\n";
|
||||
echo "Существующих: {$stats['existing']}\n";
|
||||
echo "Удаленных: {$stats['deleted']}\n";
|
||||
echo "Отсутствующих: {$stats['missing']}\n";
|
||||
|
||||
if (!$dryRun) {
|
||||
echo "Восстановлено: {$stats['restored']}\n";
|
||||
echo "Ошибок: {$stats['failed']}\n";
|
||||
} else {
|
||||
echo "Будет восстановлено: {$stats['restored']}\n";
|
||||
}
|
||||
|
||||
echo "\n=== ГОТОВО ===\n";
|
||||
|
||||
} catch (Exception $e) {
|
||||
echo "❌ Ошибка: " . $e->getMessage() . "\n";
|
||||
exit(1);
|
||||
}
|
||||
|
||||
BIN
s3/ERV/398128/398131_Polis.pdf
Normal file
BIN
s3/ERV/398128/398131_Polis.pdf
Normal file
Binary file not shown.
BIN
s3/ERV/398128/398133_Dokument_udostoveryayuschiy_lichnost.pdf
Normal file
BIN
s3/ERV/398128/398133_Dokument_udostoveryayuschiy_lichnost.pdf
Normal file
Binary file not shown.
BIN
s3/ERV/398128/398135/398136_Podtverzhdayuschie_dokumenty.pdf
Normal file
BIN
s3/ERV/398128/398135/398136_Podtverzhdayuschie_dokumenty.pdf
Normal file
Binary file not shown.
Binary file not shown.
BIN
s3/ERV/398235/398238_Polis.pdf
Normal file
BIN
s3/ERV/398235/398238_Polis.pdf
Normal file
Binary file not shown.
BIN
s3/ERV/398235/398240_Dokument_udostoveryayuschiy_lichnost.pdf
Normal file
BIN
s3/ERV/398235/398240_Dokument_udostoveryayuschiy_lichnost.pdf
Normal file
Binary file not shown.
BIN
s3/ERV/398235/398242_Dokument_udostoveryayuschiy_lichnost.pdf
Normal file
BIN
s3/ERV/398235/398242_Dokument_udostoveryayuschiy_lichnost.pdf
Normal file
Binary file not shown.
BIN
s3/ERV/398235/398244/398245_Podtverzhdayuschie_dokumenty.pdf
Normal file
BIN
s3/ERV/398235/398244/398245_Podtverzhdayuschie_dokumenty.pdf
Normal file
Binary file not shown.
Binary file not shown.
BIN
s3/ERV/398253/398256_Polis.pdf
Normal file
BIN
s3/ERV/398253/398256_Polis.pdf
Normal file
Binary file not shown.
BIN
s3/ERV/398253/398258_Dokument_udostoveryayuschiy_lichnost.pdf
Normal file
BIN
s3/ERV/398253/398258_Dokument_udostoveryayuschiy_lichnost.pdf
Normal file
Binary file not shown.
BIN
s3/ERV/398253/398260_Dokument_udostoveryayuschiy_lichnost.pdf
Normal file
BIN
s3/ERV/398253/398260_Dokument_udostoveryayuschiy_lichnost.pdf
Normal file
Binary file not shown.
BIN
s3/ERV/398253/398262/398263_Podtverzhdayuschie_dokumenty.pdf
Normal file
BIN
s3/ERV/398253/398262/398263_Podtverzhdayuschie_dokumenty.pdf
Normal file
Binary file not shown.
Binary file not shown.
BIN
s3/ERV/398275/398278_Polis.pdf
Normal file
BIN
s3/ERV/398275/398278_Polis.pdf
Normal file
Binary file not shown.
BIN
s3/ERV/398275/398280_Dokument_udostoveryayuschiy_lichnost.pdf
Normal file
BIN
s3/ERV/398275/398280_Dokument_udostoveryayuschiy_lichnost.pdf
Normal file
Binary file not shown.
BIN
s3/ERV/398275/398282/398283_Podtverzhdayuschie_dokumenty.pdf
Normal file
BIN
s3/ERV/398275/398282/398283_Podtverzhdayuschie_dokumenty.pdf
Normal file
Binary file not shown.
Binary file not shown.
BIN
s3/ERV/398294/398297_Polis.pdf
Normal file
BIN
s3/ERV/398294/398297_Polis.pdf
Normal file
Binary file not shown.
BIN
s3/ERV/398294/398299_Dokument_udostoveryayuschiy_lichnost.pdf
Normal file
BIN
s3/ERV/398294/398299_Dokument_udostoveryayuschiy_lichnost.pdf
Normal file
Binary file not shown.
BIN
s3/ERV/398294/398301/398302_Podtverzhdayuschie_dokumenty.pdf
Normal file
BIN
s3/ERV/398294/398301/398302_Podtverzhdayuschie_dokumenty.pdf
Normal file
Binary file not shown.
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user