Compare commits

...

55 Commits

Author SHA1 Message Date
Fedor
d7982931cd feat: добавлен telegram_replay.php для публикации ответов поддержки в CRM
Новый endpoint для записи ответов поддержки как комментариев в CRM:
- Принимает JSON с полями: answer, contact_id, project_id (опц.), support_user_id (опц.), channel (опц.)
- Использует прямые INSERT запросы в vtiger_crmentity, vtiger_modcomments, vtiger_modcommentscf
- Обязательно создаёт запись в vtiger_modcommentscf (иначе комментарий не отображается)
- Устанавливает deleted=0 (иначе фильтруется при выборке)
- Полная проверка ошибок БД с детальным логированием
- Логи: logs/tg_replay_inbound.log

Исправлены проблемы:
- vtws_create падал без выброса исключения — заменён на прямой SQL
- Убраны несуществующие колонки (from_mailconverter, customer_email, from_mailroom)
- Добавлена обязательная запись в vtiger_modcommentscf
2026-02-03 14:02:12 +03:00
Fedor
ea0edafba5 Добавлена обработка бинарных данных и форматирование email для n8n workflow
- Добавлен рабочий код для подготовки бинарных данных и вложений для узла Send email
- Реализовано форматирование form_data в читаемый текст с разделами:
  * Личные данные (с автоматическим расчетом возраста, код документа, серия и номер)
  * Контактная информация
  * Страховщик (с телефоном страховой компании)
  * Банковские реквизиты (с получателем платежа)
  * Информация о страховом случае
  * Описание ситуации
- Добавлена документация по использованию и устранению неполадок
- Созданы альтернативные версии кода (простая версия, Function Node версия)
2025-12-26 14:51:58 +03:00
Fedor
1fdb244fd4 fix: исправлены ошибки clipboard и добавлена передача ticket_number в webhook поддержки
- Исправлена ошибка 'Cannot read properties of undefined (reading writeText)' в Step1Phone, StepClaimConfirmation, Step3Payment
  - Добавлена проверка на существование navigator.clipboard
  - Добавлен fallback для старых браузеров (document.execCommand)
  - Добавлена обработка ошибок с try/catch

- Добавлена передача ticket_number и ticket_id в webhook поддержки
  - При обработке события out_of_scope сохраняются claim_id, ticket_number, ticket_id из payload
  - Эти поля теперь передаются в webhook при отправке в поддержку

- Обновлён n8n код для обработки ошибок и out_of_scope
  - Добавлено явное сохранение ticket_number в data объекта события
  - ticket_number теперь гарантированно попадает в событие для Redis
2025-12-05 12:39:08 +03:00
Fedor
ab54530500 feat: добавлено детальное логирование статусов заявлений для диагностики
- Добавлено логирование всех заявлений с их статусами, если запрос вернул 0 строк
- Помогает понять, почему заявление не возвращается
2025-12-04 10:33:29 +03:00
Fedor
ffe30033da fix: временно убран фильтр is_confirmed для диагностики
- Убран фильтр is_confirmed для выяснения, почему заявление не возвращается
- После диагностики нужно вернуть фильтры с правильной логикой
2025-12-04 10:31:46 +03:00
Fedor
35ca11552d fix: убран фильтр по status_code='approved' - теперь approved заявления тоже возвращаются
- Заявления со статусом 'approved' теперь показываются в списке
- Исключаются только заявления с is_confirmed = true
- Исправляет проблему, когда заявление не возвращалось из-за статуса 'approved'
2025-12-04 10:30:36 +03:00
Fedor
b28bb4b636 fix: временно убраны фильтры для диагностики заявлений
- Убраны фильтры is_confirmed и status_code для диагностики
- Нужно выяснить, почему заявление не возвращается даже без фильтров
2025-12-04 10:27:35 +03:00
Fedor
37472d612a feat: добавлена диагностика статусов заявлений в debug
- Добавлены test_count_approved и test_count_confirmed для диагностики
- Показывает, сколько заявлений со статусом approved и is_confirmed = true
- Помогает понять, почему заявления не возвращаются в списке
2025-12-04 10:23:02 +03:00
Fedor
e0097a164d fix: улучшен поиск заявлений по телефону - поддержка разных форматов
- Поиск теперь работает по разным форматам телефона:
  * 71234543212 (оригинальный)
  * +71234543212 (с плюсом)
  * 81234543212 (с 8 вместо 7)
- Убрано условие unified_id IS NULL - теперь ищет в любом случае
- Поиск в clpr_user_accounts и напрямую в payload->>'phone'

Исправляет проблему поиска заявлений для контакта 71234543212
2025-12-04 10:18:04 +03:00
Fedor
6a783deba1 fix: улучшен поиск заявлений по телефону - добавлен fallback на прямой поиск по payload
- Теперь поиск работает двумя способами:
  1. Через unified_id (если есть запись в clpr_user_accounts)
  2. Напрямую по телефону в payload (если unified_id не установлен)

Исправляет проблему, когда заявления не находятся для контакта 71234543212
2025-12-04 10:15:41 +03:00
Fedor
920eb53660 fix: исправлена вставка номера телефона через копирование
- Установка значения напрямую в input элемент
- Триггер события input для синхронизации с формой
- Использование requestAnimationFrame вместо setTimeout для более надёжной работы

Теперь можно вставлять номер телефона через Ctrl+V / Cmd+V
2025-12-04 10:10:19 +03:00
Fedor
1f88e156b7 fix: исправлена проблема с label for атрибутом в createCheckbox
- Добавлена генерация безопасного id (убираются специальные символы)
- Добавлено экранирование id при использовании в атрибуте for
- Убеждаемся, что id и for всегда совпадают

Исправляет предупреждение: 'Incorrect use of <label for=FORM_ELEMENT>'
2025-12-04 09:45:40 +03:00
Fedor
003210dcfc fix: исправлены проблемы с формами и циклическими ссылками
- Исправлена проблема с циклическими ссылками в Step1Phone.tsx при onPaste (добавлен setTimeout)
- Добавлены name атрибуты во все поля формы для правильной работы форм и автозаполнения
- Исправлены проблемы с label/input связями (все label имеют правильные for/id)
- Все поля теперь имеют id и name атрибуты (исправляет предупреждения в консоли)

Исправленные функции:
- createField: добавлен name атрибут
- createReadonlyField: добавлен name атрибут
- createDateField: добавлен name атрибут
- createMoneyField: добавлен name атрибут
- createTextarea: добавлен name атрибут
- createBankSelect: добавлен name атрибут для основного поля и скрытого bank_id
- createCheckbox: добавлен name атрибут, проверена связь label/input
2025-12-04 09:21:12 +03:00
Fedor
346d9a77d2 feat: добавлена визуальная подсветка обязательных полей
- Добавлены звёздочки (*) рядом с обязательными полями
- Незаполненные обязательные поля подсвечиваются жёлтой рамкой
- Добавлен блок с предупреждением о незаполненных полях перед кнопкой отправки
- Улучшена валидация с визуальной обратной связью
- Пользователю теперь понятно, какие поля нужно заполнить
2025-12-03 18:59:58 +03:00
Fedor
38457394c1 fix: исправлена ошибка setSelectionRange для email полей и добавлено логирование
- Исправлена ошибка 'setSelectionRange not supported for email field' - добавлен try-catch и проверка типа поля
- Добавлено логирование наличия contact_data_confirmed, cf_2624, bank_id, bank_name в body запроса
- Это поможет отследить, передаются ли поля из frontend в backend
2025-12-03 18:51:33 +03:00
Fedor
e7915df634 fix: исправлено поле email для контрагента - добавлен type='email'
- Добавлен type='email' и inputmode='email' для полей email
- Это исправляет проблему, когда в поле email контрагента можно было вводить только цифры
- Обработчики событий уже были настроены правильно, но не хватало правильного типа поля
2025-12-03 18:47:08 +03:00
Fedor
b2433f38d8 feat: добавлено логирование для отладки передачи cf_2624 и bank_name
- Добавлено логирование извлечения contact_data_confirmed, cf_2624, bank_id, bank_name из body
- Добавлено логирование наличия этих полей в event_data перед отправкой в Redis
- Поможет отследить, передаются ли поля из frontend в backend
2025-12-03 18:44:07 +03:00
Fedor
8e116c76a4 feat: после отправки заявления сразу показывается сообщение об успехе
- Добавлен callback onSubmitted в StepClaimConfirmation
- После успешной отправки (SMS-верификации) сразу показывается сообщение об успехе
- Убрана форма редактирования после отправки
- Пользователь видит только сообщение: 'Поздравляем! Ваше обращение направлено в Клиентправ...'
2025-12-03 18:41:18 +03:00
Fedor
bf3fb5fef0 feat: добавлены cf_2624 и bank_name в payload при отправке заявления
Frontend (StepClaimConfirmation):
- Добавлен contact_data_confirmed в payload для Redis канала
- Добавлен cf_2624 (значение для CRM: 1 или 0)
- Добавлены bank_id и bank_name в payload
- bank_name сохраняется в state при выборе банка

Backend (claims.py):
- Добавлены contact_data_confirmed и cf_2624 в event_data
- Добавлены bank_id и bank_name в event_data для Redis канала clientright:webform:approve

Обновлён статус заявления 226564ce-d7cf-48ee-a820-690e8f5ec8e5 на draft_claim_ready для тестирования
2025-12-03 18:35:02 +03:00
Fedor
1fc64c035e feat: обновлён текст успешной отправки заявления
- Заменён текст на главной странице после отправки:
  'Поздравляем! Ваше обращение направлено в Клиентправ.'
  'В ближайшее время на указанную Вами электронную почту поступит письмо, подтверждающее регистрацию вашего обращения.'

- Обновлены toast-уведомления при отправке заявления
- Убрано старое сообщение 'Мы изучаем ваш вопрос и документы'
2025-12-03 18:29:36 +03:00
Fedor
b93bb9e8ad feat: выбор банка для СБП выплат теперь обязательное поле
- Добавлен bank_id в список обязательных полей в validateAllFields()
- Обновлён placeholder: "Начните вводить название банка (обязательно)"
- Обновлён заголовок: "Банк для получения выплаты (обязательно)"
- При отправке формы проверяется заполнение банка

Теперь форма не отправится без выбора банка для получения выплаты по СБП.
2025-12-03 18:18:53 +03:00
Fedor
35adcb3043 feat: добавлены ИНН и E-mail как обязательные поля для контрагентов
- ИНН организации теперь обязательное поле
- E-mail организации теперь обязательное поле
- Обновлены placeholder'ы с указанием (обязательно)
- Обновлена валидация validateAllFields()

Обязательные поля для контрагентов:
1. Наименование (accountname)
2. ИНН (inn)
3. Адрес (address)
4. E-mail (email)

Необязательные:
- Телефон (phone)
- Сайт (website)
2025-12-03 18:13:58 +03:00
Fedor
3d9669dd8e fix: настройка подключения MySQL CRM из Docker контейнера
- Изменён docker-compose.yml: добавлен network_mode: host для доступа к MySQL на хосте
- Обновлён config.py: mysql_crm_host = localhost (работает в режиме host)
- MySQL CRM теперь успешно подключается из контейнера
2025-12-03 18:08:05 +03:00
Fedor
6a9f8b5465 docs: обновлён лог сессии 2025-12-03
- Получение cf_2624 из MySQL при загрузке черновика
- Исправление подключения MySQL CRM из Docker контейнера
- Блокировка редактирования полей при contact_data_confirmed=true
- Выбор банка для СБП выплат
- Все проблемы решены, система работает корректно
2025-12-03 18:07:53 +03:00
Fedor
a86120dd53 fix: передача contact_data_confirmed в StepClaimConfirmation для блокировки полей
- Добавлен prop contact_data_confirmed в StepClaimConfirmation
- Передача флага из formData.contact_data_confirmed в компонент
- Исправлена логика получения флага (приоритет: props > claimPlanData > false)
- Поля должны блокироваться при contact_data_confirmed=true
2025-12-03 18:04:08 +03:00
Fedor
e114231541 feat: Получение cf_2624 из MySQL при загрузке черновика
- Добавлен сервис CrmMySQLService для подключения к MySQL БД vtiger CRM
- Обновлён get_draft() для прямого SQL запроса к MySQL вместо webservice API
- Получение cf_2624 и всех данных контакта из MySQL
- Обновлена документация и SQL файлы для n8n
- Добавлено логирование для отладки

Преимущества:
- Проще: один SQL запрос вместо цепочки HTTP запросов
- Быстрее: прямой запрос к БД
- Надёжнее: не зависит от webservice API
- Актуальнее: всегда свежие данные из БД
2025-12-03 16:04:25 +03:00
Fedor
b7197e0da5 feat(forms): автоподстановка банков и улучшенная обработка телефона
- Step1Phone: добавлена обработка вставки телефона с автоматической очисткой от +7 и обрезкой до 10 цифр
- Step3Payment: заменён Select на AutoComplete для выбора банка с автоподстановкой
- generateConfirmationFormHTML: заменён select на input с datalist для автоподстановки банков в форме подтверждения
- Добавлены скрытые поля bank_id для сохранения ID банка отдельно от названия
- Добавлены файлы для проверки заявки 226564ce

Улучшения UX:
- Пользователь может вводить название банка вместо прокрутки длинного списка
- Автоматическая фильтрация списка банков при вводе
- Предупреждение при обрезке номера телефона при вставке
2025-12-02 17:12:25 +03:00
Fedor
ee1c4af5c3 feat: добавлен выбор банка для СБП выплат
Frontend:
- Динамическая загрузка 226 банков из NSPK API
- Выбор банка добавлен в Step3Payment (новая заявка)
- Выбор банка добавлен в generateConfirmationFormHTML (редактирование)
- Поля: bank_id (ID из NSPK) и bank_name (название для отображения)

Backend:
- Добавлено поле bank_id в ClaimCreateRequest

API:
- http://212.193.27.93/api/payouts/dictionaries/nspk-banks

Изменения:
- ticket_form/frontend/src/components/form/Step3Payment.tsx
- ticket_form/frontend/src/components/form/generateConfirmationFormHTML.ts
- ticket_form/frontend/src/pages/ClaimForm.tsx
- ticket_form/backend/app/api/models.py
2025-12-02 11:06:15 +03:00
Fedor
834520a045 docs: add session log 2025-12-01 2025-12-01 22:19:31 +03:00
Fedor
da82100b60 feat: UI/UX improvements + CRM integration methods + documents_meta deduplication
Frontend:
- Changed main title to 'Подать обращение о защите прав потребителя'
- Changed browser title to 'Clientright — защита прав потребителей'
- Enhanced draft cards: show problem_description (250 chars), category tag, document progress bar
- Fixed 'Назад' button to always return to draft selection
- Added SSE connection for OCR status updates
- Renamed steps: Вход, Обращение, Документы, Заявление
- Skip 'Проверка полиса' and 'Тип события' steps for new claim flow

Backend:
- Fixed client IP extraction (X-Forwarded-For, X-Real-IP)
- Added problem_title, category, documents_required_list to draft list API
- Fixed documents_uploaded count to count unique field_labels

CRM Webservices:
- Added UpsertContact.php - create/update contacts with tgid support
- Added UpsertAccounts.php - batch upsert offenders by INN
- Added UpsertProject.php - create/update projects with offender mapping

Database:
- Fixed documents_meta duplicates in existing claims
- SQL query for deduplication by field_name provided
2025-12-01 22:18:21 +03:00
Fedor
81acd49fd9 feat(frontend): использование form_draft для формы подтверждения
- StepDraftSelection: кнопка 'Продолжить' активна для draft_docs_complete
- ClaimForm: проверка form_draft при загрузке черновика
- Если есть form_draft — преобразуем в propertyName и показываем форму подтверждения
- Поддержка статуса draft_docs_complete для перехода к подтверждению
2025-11-30 11:45:35 +03:00
Fedor
0f8631bf20 fix(backend): исправлен алгоритм hash для совместимости с JS
- Использован ctypes.c_int32 для эмуляции JS 32-битного сдвига
- Теперь Python hash совпадает с JS hash в n8n
- Кэширование form_draft работает корректно
2025-11-30 11:37:16 +03:00
Fedor
3801bc4949 feat(backend): кэширование form_draft в check-ocr-status
- Добавлена проверка наличия черновика в БД перед запуском RAG
- Если documents_hash совпадает — возвращаем черновик из кэша
- Если черновика нет или он устарел — запускаем RAG workflow
- Добавлен параметр force_refresh для принудительного обновления
- Импортирован db сервис для работы с PostgreSQL
2025-11-30 11:30:36 +03:00
Fedor
985ee23810 feat(n8n): RAG workflow для извлечения данных из документов
- Обновлён Code1: нормализация данных из give_data1 с поддержкой payload.applicant, ai_analysis, wizard_plan
- Обновлён Code6: генерация промптов для RAG (user, project, offenders)
- Добавлена документация по настройке n8n нод для OCR статуса
- Добавлен эндпоинт check-ocr-status в documents.py
- Добавлен лог сессии с полным описанием workflow

Workflow itX62h38faB51y9J успешно извлекает:
- Данные пользователя (ФИО, контакты, адрес)
- Данные проекта (сумма, предмет, даты договора)
- Несколько контрагентов с разными ролями (seller, service_provider)
2025-11-29 19:29:14 +03:00
Fedor
840acca51a feat(documents): дедупликация documents_meta и исправление field_label
- Исправлен N8N_CODE_PROCESS_UPLOADED_FILES_FIXED.js: использовать uploads_field_labels[0] вместо [grp]
- Создан SQL_CLAIMSAVE_FIXED_NEW_FLOW_DEDUP.sql с дедупликацией documents_meta
- Создан SQL_CLEANUP_DOCUMENTS_META_DUPLICATES.sql для очистки существующих дубликатов
- Создан полный уникальный индекс idx_document_texts_hash_unique на document_texts(file_hash)
- Добавлен SESSION_LOG_2025-11-28_documents_dedup.md с описанием всех изменений

Fixes:
- field_label теперь корректно отображает 'Переписка' вместо 'group-2'
- documents_meta не накапливает дубликаты при повторных сохранениях
- ON CONFLICT (file_hash) теперь работает для document_texts
2025-11-28 18:16:53 +03:00
Fedor
6c770f0a87 feat(ticket_form): Новая архитектура загрузки документов
- StepDocumentsNew.tsx: поэкранная загрузка документов
- StepWaitingClaim.tsx: ожидание формирования заявления с SSE
- StepDraftSelection.tsx: поддержка новых статусов черновиков
- documents.py: API для загрузки документов
- NEW_FLOW_ARCHITECTURE.md: документация новой архитектуры

Флоу: Description → Documents → Waiting → Claim Review → SMS
Статусы: draft_new, draft_docs_progress, draft_docs_complete, draft_claim_ready
2025-11-26 12:52:54 +03:00
Fedor
0868d37484 Сессия 26.11.2025: Исправления UI Wizard - блоки загрузки, retry аутентификации 2025-11-25 23:42:32 +03:00
Fedor
55c1402d99 Исправлен React.useRef → useRef 2025-11-25 23:38:24 +03:00
Fedor
18fcdecae8 Исправлено дублирование блоков документов (ref для отслеживания созданных) 2025-11-25 23:36:32 +03:00
Fedor
f058ca91ad Убран дублирующий useEffect для создания блоков документов 2025-11-25 23:33:59 +03:00
Fedor
796316d969 Убраны лишние поля для предустановленных документов (description, кнопка Удалить) 2025-11-25 23:31:40 +03:00
Fedor
f3b5771c09 Блоки загрузки документов сразу развёрнуты при загрузке плана 2025-11-25 23:28:36 +03:00
Fedor
2ce0c585ff Чекбокс 'нет документа' перенесён под блок загрузки 2025-11-25 23:26:20 +03:00
Fedor
6cc07b0ba6 Исправлен JSX Fragment для блоков загрузки документов 2025-11-25 23:22:34 +03:00
Fedor
b5478c143f Заменены чекбоксы docs_exist на блоки загрузки файлов 2025-11-25 23:20:33 +03:00
Fedor
be1ac2ed49 Добавлен retry механизм для webservice аутентификации (race condition fix) 2025-11-25 23:02:52 +03:00
Fedor
2b1dca9e92 Автогенерация уникального описания для дополнительных блоков документов 2025-11-25 21:22:11 +03:00
Fedor
99ef902a31 Показываем поле описания для всех блоков если блоков > 1 2025-11-25 21:20:48 +03:00
Fedor
8626c9aff4 Автоматическая нумерация дубликатов field_label при отправке визарда 2025-11-25 21:12:30 +03:00
Fedor
444e5d2b91 Добавлен лог отправляемых метаданных документов (field_label) 2025-11-25 20:53:19 +03:00
Fedor
8f4cff55e9 Добавлен полный лог диалога за 22 ноября 2025 2025-11-25 20:46:59 +03:00
Fedor
52fe013375 feat(ticket_form): unified_id/contact_id передача, исправлен мерж сессии, новая сессия для жалобы
- Добавлены unified_id и contact_id в TicketFormDescriptionRequest
- Исправлен CODE_MERGE_PROJECT_TO_SESSION.js - теперь сохраняются ВСЕ данные из body.other
- Добавлен fallback на получение other из Webhook напрямую
- Генерация новой session_id при создании новой жалобы (сохраняя авторизацию)
- Добавлен SQL_SELECT_CONTACT_WITH_CUSTOM_FIELDS.sql для CRM контактов
- Создан SESSION_LOG_2025-11-25.md с документацией сессии
2025-11-25 20:02:21 +03:00
Fedor
a20a4d0e09 Добавлен лог сессии 2025-11-22 2025-11-22 09:40:14 +03:00
Fedor
486f3619ff Добавлен field_label в результат переименования файлов, исправлена загрузка черновиков, обновлен формат пути S3 с project_name 2025-11-22 09:38:38 +03:00
Fedor
d3ba054027 Исправление архивации проектов: поддержка S3 файлов и документов из связанных сущностей
- Добавлен метод getRelatedDocs() для получения документов из связанных сущностей (контакты, контрагенты)
- Добавлен метод downloadS3File() для скачивания файлов из S3 во временную папку
- Добавлен метод cleanupTempFiles() для очистки временных файлов
- Исправлен getPaths() для корректной обработки S3 файлов (всегда запрашивает s3_bucket/s3_key из БД)
- Исправлен getArchive() для проектов: собирает документы из основной записи и связанных сущностей
- Исправлен путь к vendor/autoload.php (поиск по нескольким путям)
- Исправлено имя временного файла (короткое имя вместо полного пути для избежания 'File name too long')

Результат: архив успешно создается с документами из проекта и связанных сущностей (25 документов для проекта 396447)
2025-11-21 10:23:52 +03:00
421 changed files with 44889 additions and 682 deletions

View File

@@ -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"
]
}
}
}
}

View File

@@ -211,3 +211,9 @@ composer require phpoffice/phpword
Все компоненты реализованы, протестированы и готовы к использованию в n8n workflow.

146
RESTORE_INSTRUCTIONS.md Normal file
View 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
View 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 с описанием исправлений.

View 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
View 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";
}

View 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
View 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";
}

View 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
View 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
View 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
View 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);
}

View 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
View 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
View 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
View 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 "✅ Очистка завершена"

View File

@@ -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()
];
}
}

View File

@@ -432,3 +432,9 @@ PUBLISH ai:response:task-xxx {"success": true, "documentUrl": "..."}
- Ошибки изолированы
- Легко отлаживать

View File

@@ -197,3 +197,9 @@ https://crm.clientright.ru/crm_extensions/file_storage/api/create_document_with_
- Документ сразу доступен для редактирования в OnlyOffice
- Путь формируется автоматически: `{module}/{recordName}_{recordId}/{fileName}.{ext}`

View File

@@ -244,3 +244,9 @@ _курсив_
3. **Гибкость** — можно комбинировать элементы
4. **Автоматическое форматирование** — документ получается красивым без ручной правки

View File

@@ -152,3 +152,9 @@ curl -X POST "https://crm.clientright.ru/crm_extensions/file_storage/api/create_
}'
```

View File

@@ -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/)

View File

@@ -246,3 +246,9 @@ echo json_encode(['success' => true, 'templates' => $templates]);
3. **Решение:** Использовать WebDAV PROPFIND для получения списка файлов из папки Templates
4. **Статус:** Наш текущий подход (WebDAV + PHPWord) является правильным и оптимальным решением

View File

@@ -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`

View File

@@ -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

View 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
View 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
View 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);
}

View 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
View 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
View 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";

View File

@@ -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'
];

View 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;
}
}

View File

@@ -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,

View 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;
}

View File

@@ -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;

View File

@@ -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)

View 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);
}

View 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);
}

View 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);
}

View File

@@ -53,4 +53,5 @@ $languageStrings = array(
'заявление на лист' => 'заявление на лист',
'урегулирование' => 'урегулирование',
'заключение мирового соглашения' => 'заключение мирового соглашения',
'Черновик' => 'Черновик',
);

View File

@@ -53,4 +53,5 @@ $languageStrings = array(
'заявление на лист' => 'заявление на лист',
'урегулирование' => 'урегулирование',
'заключение мирового соглашения' => 'заключение мирового соглашения',
'Черновик' => 'Черновик',
);

View File

@@ -53,4 +53,5 @@ $languageStrings = array(
'заявление на лист' => 'заявление на лист',
'урегулирование' => 'урегулирование',
'заключение мирового соглашения' => 'заключение мирового соглашения',
'Черновик' => 'Черновик',
);

View File

@@ -53,4 +53,5 @@ $languageStrings = array(
'заявление на лист' => 'заявление на лист',
'урегулирование' => 'урегулирование',
'заключение мирового соглашения' => 'заключение мирового соглашения',
'Черновик' => 'Черновик',
);

View File

@@ -53,4 +53,5 @@ $languageStrings = array(
'заявление на лист' => 'заявление на лист',
'урегулирование' => 'урегулирование',
'заключение мирового соглашения' => 'заключение мирового соглашения',
'Черновик' => 'Черновик',
);

View File

@@ -53,4 +53,5 @@ $languageStrings = array(
'заявление на лист' => 'заявление на лист',
'урегулирование' => 'урегулирование',
'заключение мирового соглашения' => 'заключение мирового соглашения',
'Черновик' => 'Черновик',
);

View File

@@ -53,4 +53,5 @@ $languageStrings = array(
'заявление на лист' => 'заявление на лист',
'урегулирование' => 'урегулирование',
'заключение мирового соглашения' => 'заключение мирового соглашения',
'Черновик' => 'Черновик',
);

View File

@@ -53,4 +53,5 @@ $languageStrings = array(
'заявление на лист' => 'заявление на лист',
'урегулирование' => 'урегулирование',
'заключение мирового соглашения' => 'заключение мирового соглашения',
'Черновик' => 'Черновик',
);

View File

@@ -53,4 +53,5 @@ $languageStrings = array(
'заявление на лист' => 'заявление на лист',
'урегулирование' => 'урегулирование',
'заключение мирового соглашения' => 'заключение мирового соглашения',
'Черновик' => 'Черновик',
);

View File

@@ -53,4 +53,5 @@ $languageStrings = array(
'заявление на лист' => 'заявление на лист',
'урегулирование' => 'урегулирование',
'заключение мирового соглашения' => 'заключение мирового соглашения',
'Черновик' => 'Черновик',
);

View File

@@ -53,4 +53,5 @@ $languageStrings = array(
'заявление на лист' => 'заявление на лист',
'урегулирование' => 'урегулирование',
'заключение мирового соглашения' => 'заключение мирового соглашения',
'Черновик' => 'Черновик',
);

View File

@@ -53,4 +53,5 @@ $languageStrings = array(
'заявление на лист' => 'заявление на лист',
'урегулирование' => 'урегулирование',
'заключение мирового соглашения' => 'заключение мирового соглашения',
'Черновик' => 'Черновик',
);

View File

@@ -53,4 +53,5 @@ $languageStrings = array(
'заявление на лист' => 'заявление на лист',
'урегулирование' => 'урегулирование',
'заключение мирового соглашения' => 'заключение мирового соглашения',
'Черновик' => 'Черновик',
);

View File

@@ -53,4 +53,5 @@ $languageStrings = array(
'заявление на лист' => 'заявление на лист',
'урегулирование' => 'урегулирование',
'заключение мирового соглашения' => 'заключение мирового соглашения',
'Черновик' => 'Черновик',
);

View File

@@ -53,4 +53,5 @@ $languageStrings = array(
'заявление на лист' => 'заявление на лист',
'урегулирование' => 'урегулирование',
'заключение мирового соглашения' => 'заключение мирового соглашения',
'Черновик' => 'Черновик',
);

View File

@@ -53,4 +53,5 @@ $languageStrings = array(
'заявление на лист' => 'заявление на лист',
'урегулирование' => 'урегулирование',
'заключение мирового соглашения' => 'заключение мирового соглашения',
'Черновик' => 'Черновик',
);

View File

@@ -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

View File

@@ -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

View File

@@ -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;

View File

@@ -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, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&#039;');
// ШАГ 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);

View File

@@ -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');

View File

@@ -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
View 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";
}

View 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);
}

View 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
View 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
View 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
View 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
View 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";
}

View File

@@ -0,0 +1,7 @@
{
"total_markers": 11,
"restored": 10,
"failed": 0,
"skipped": 0,
"errors": []
}

View File

@@ -0,0 +1,7 @@
{
"total_markers": 51,
"restored": 50,
"failed": 0,
"skipped": 0,
"errors": []
}

View 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"
]
}

View File

@@ -0,0 +1,7 @@
{
"total_markers": 0,
"restored": 0,
"failed": 0,
"skipped": 0,
"errors": []
}

View File

@@ -0,0 +1,7 @@
{
"total_markers": 0,
"restored": 0,
"failed": 0,
"skipped": 0,
"errors": []
}

View File

@@ -0,0 +1,7 @@
{
"total_markers": 46,
"restored": 2,
"failed": 43,
"skipped": 1,
"errors": []
}

168
restore_project_373977.php Normal file
View 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
View 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
View 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);
}

View 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);
}

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Some files were not shown because too many files have changed in this diff Show More