From: fomichev Date: Wed, 26 Nov 2025 14:05:50 +0000 (+0300) Subject: Контроллеры фаза3 X-Git-Url: https://gitweb.erp-flowers.ru/?a=commitdiff_plain;h=cc31e9dc8b351751dc5cf7d3642f829932f12065;p=erp24_rep%2Fyii-erp24%2F.git Контроллеры фаза3 --- diff --git a/erp24/docs/controllers/non-standard/crud/Product1cReplacementController_ACTIONS_TABLE.md b/erp24/docs/controllers/non-standard/crud/Product1cReplacementController_ACTIONS_TABLE.md new file mode 100644 index 00000000..131bc9d4 --- /dev/null +++ b/erp24/docs/controllers/non-standard/crud/Product1cReplacementController_ACTIONS_TABLE.md @@ -0,0 +1,200 @@ +# Product1cReplacementController — Таблица экшенов + +| № | Экшен | HTTP | Назначение | Параметры | Возврат | Особенности | +|---|-------|------|------------|-----------|---------|-------------| +| 1 | `actionIndex()` | GET/POST | Список замен с группировкой по GUID | - | `render('index')` | POST: загрузка XLSX через `Product1cReplacementService::uploadTemplateReplacement()` | +| 2 | `actionView($id)` | GET | Просмотр товара и всех его замен | `$id` (GUID товара) | `render('view')` | Расчёт разницы цен, загрузка последних логов, информация об администраторах | +| 3 | `actionCreate()` | GET/POST | Создание новых замен для товара | - | `render('create')` или `redirect(['index'])` | Множественное создание в транзакции, логирование каждой записи | +| 4 | `actionSearch($q, $exclude, $guid)` | GET | AJAX-поиск товаров для Select2 | `$q`, `$exclude`, `$guid` | JSON: `{items: [{id, text}]}` | Исключение существующих замен и основных товаров | +| 5 | `actionUpdate($id)` | GET/POST | Редактирование замен товара | `$id` (GUID товара) | `render('update')` или `redirect(['view'])` | Обновление/создание в одной транзакции, логирование только изменений | +| 6 | `actionLogHistory($guid)` | GET | История изменений замен для товара | `$guid` | `render('log-history')` | Пагинация 20 записей, сортировка ASC по дате | +| 7 | `actionDelete($id)` | POST | Мягкое удаление замены | `$id` (ID записи) | JSON: `{success, message, redirect}` | Логирование удаления, `redirect: true` если удалена последняя замена | + +## Вспомогательные методы + +| Метод | Область | Назначение | Параметры | Возврат | +|-------|---------|------------|-----------|---------| +| `findModel($id)` | protected | Поиск записи по ID | `$id` | `Product1cReplacement` или 404 | +| `findModelByGuid($id)` | protected | Поиск записи по GUID | `$id` (GUID) | `Product1cReplacement` или 404 | +| `logReplacementAction($replacementId, $stateBefore, $stateAfter)` | public static | Запись изменений в лог | `$replacementId`, `$stateBefore`, `$stateAfter` | void (throws Exception при ошибке) | + +## Маршруты (примеры) + +``` +GET /product1c-replacement/index +POST /product1c-replacement/index (загрузка XLSX) +GET /product1c-replacement/view?id={guid} +GET /product1c-replacement/create +POST /product1c-replacement/create +GET /product1c-replacement/search?q={query}&exclude={ids}&guid={guid} +GET /product1c-replacement/update?id={guid} +POST /product1c-replacement/update?id={guid} +GET /product1c-replacement/log-history?guid={guid} +POST /product1c-replacement/delete?id={id} +``` + +## Формат данных POST-запросов + +### actionCreate / actionUpdate +```php +[ + 'Product1cReplacement' => [ + 'guid' => 'основной-товар-guid', + 'guid_replacement' => [ + 'замена-1-guid', + 'замена-2-guid', + 'замена-3-guid' + ], + 'replacement_ids' => [ // только для update + '42', // ID существующей записи + '', // пустое значение = создать новую + '43' + ] + ] +] +``` + +### actionIndex (загрузка файла) +``` +POST /product1c-replacement/index +Content-Type: multipart/form-data + +myfile: template_replacement.xlsx +``` + +## Формат JSON-ответов + +### actionSearch +```json +{ + "items": [ + { + "id": "123-guid-abc", + "text": "Молоко 3.2% (ART-12345)" + }, + { + "id": "456-guid-def", + "text": "Кефир 2.5% (ART-67890)" + } + ] +} +``` + +### actionDelete +```json +{ + "success": true, + "message": "Запись успешно удалена", + "redirect": false +} +``` + +**При ошибке:** +```json +{ + "success": false, + "message": "Ошибка при удалении записи." +} +``` + +## Особенности работы экшенов + +### actionIndex +- **Двойной режим:** GET (список) / POST (загрузка XLSX) +- **Группировка:** Выбирает уникальные `guid`, затем для каждого загружает все `guid_replacement` +- **Результат группировки:** `['guid-A' => ['guid-B', 'guid-C'], 'guid-D' => ['guid-E']]` + +### actionView +- **Комплексная загрузка:** модель + замены + логи + цены + администраторы +- **Расчёт разницы:** `priceDifference = priceReplacement - priceOriginal` +- **Структура lastLogs:** + ```php + [ + 42 => [ + 'lastLog' => Product1cReplacementLog, + 'adminName' => 'Иван Иванов', + 'productReplacementPrice' => Prices, + 'priceDifference' => 150 + ] + ] + ``` + +### actionCreate +- **Транзакция:** Все замены создаются атомарно +- **Логирование:** Каждая запись получает лог `'Запись создана' → guid_replacement` +- **Rollback:** При ошибке в любой замене откатываются все + +### actionSearch +- **Три режима исключений:** + 1. `$guid = "guid"` → исключить все основные товары + 2. `$guid = {конкретный GUID}` → исключить замены для этого товара + 3. `$exclude = "1,2,3"` → исключить конкретные ID +- **Лимит:** 20 результатов +- **Поиск:** ILIKE по названию товара + +### actionUpdate +- **Гибридная логика:** + - Если `replacement_ids[$index]` заполнен → UPDATE + - Если пуст → INSERT +- **Логирование:** Только при изменении `guid_replacement` +- **Транзакция:** Все изменения атомарны + +### actionLogHistory +- **Агрегация:** Находит все ID замен для товара, затем их логи +- **Сортировка:** ASC (от старых к новым) +- **Дополнительно:** Получает название товара из `Products1c` + +### actionDelete +- **Soft Delete:** Установка `deleted_at` +- **Подсчёт остатка:** Определяет, нужен ли редирект на index +- **JSON ответ:** Для AJAX-обработки на клиенте +- **Логирование:** `guid_replacement → 'Запись удалена'` + +## Зависимости между экшенами + +``` +index → create → index (redirect после создания) +index → view → update → view (redirect после обновления) +view → delete (AJAX) → view/index (по условию) +view → log-history (отдельная страница) +create/update → search (AJAX для Select2) +``` + +## Patterns использования + +### 1. Создание замен +``` +GET /product1c-replacement/create + ↓ (выбор товаров через search) +POST /product1c-replacement/create (с массивом guid_replacement) + ↓ +redirect /product1c-replacement/index +``` + +### 2. Редактирование замен +``` +GET /product1c-replacement/view?id={guid} + ↓ +GET /product1c-replacement/update?id={guid} + ↓ (изменение через search) +POST /product1c-replacement/update?id={guid} + ↓ +redirect /product1c-replacement/view?id={guid} +``` + +### 3. Просмотр истории +``` +GET /product1c-replacement/view?id={guid} + ↓ (клик на "История") +GET /product1c-replacement/log-history?guid={guid} +``` + +### 4. Удаление замены +``` +GET /product1c-replacement/view?id={guid} + ↓ (клик "Удалить" на конкретной замене) +AJAX POST /product1c-replacement/delete?id={replacement_id} + ↓ (ответ JSON) +if (redirect) → redirect /product1c-replacement/index +else → reload view +``` diff --git a/erp24/docs/controllers/non-standard/crud/Product1cReplacementController_ANALYSIS.md b/erp24/docs/controllers/non-standard/crud/Product1cReplacementController_ANALYSIS.md new file mode 100644 index 00000000..7d399b47 --- /dev/null +++ b/erp24/docs/controllers/non-standard/crud/Product1cReplacementController_ANALYSIS.md @@ -0,0 +1,438 @@ +# Product1cReplacementController — Анализ + +## Общая информация + +**Файл:** `erp24/controllers/crud/Product1cReplacementController.php` +**Пространство имён:** `yii_app\controllers\crud` +**Родительский класс:** `yii\web\Controller` +**Размер:** 480 строк + +## Назначение + +CRUD-контроллер для управления **заменами товаров из 1С**. Позволяет связывать товары с их заменителями, отслеживать изменения через систему логирования, загружать шаблоны замен и управлять историей изменений. + +--- + +## Основные особенности + +### 1. **Групповое управление заменами** +- Один товар может иметь несколько заменителей +- Система автоматически группирует замены по GUID +- Поддержка множественного выбора заменителей + +### 2. **Система логирования** +- Все изменения записываются в `Product1cReplacementLog` +- Отслеживание `state_before` → `state_after` +- Привязка к администратору, выполнившему действие +- История изменений с отображением автора + +### 3. **Загрузка шаблонов** +- Импорт замен через XLSX файлы +- Обработка через `Product1cReplacementService::uploadTemplateReplacement()` +- Валидация данных при загрузке + +### 4. **Расчёт разницы цен** +- Автоматический расчёт разницы между основным товаром и заменой +- Интеграция с `Prices` для получения актуальных цен +- Отображение разницы в интерфейсе + +### 5. **Soft Delete** +- Мягкое удаление записей (`deleted_at`) +- Сохранение истории удалений в логах +- Автоматический редирект при удалении последней замены + +--- + +## Структура экшенов + +### 1. `actionIndex()` +**Назначение:** Список всех замен с группировкой по GUID + +**Особенности:** +- Обработка POST-запросов для загрузки XLSX +- Группировка замен по `guid` с выборкой `guid_replacement` +- Поиск через `Product1cReplacementSearch` +- Передача `groupedReplacements` для отображения связей + +**Логика:** +```php +$models = $dataProvider->query->select(['guid'])->distinct()->all(); +$groupedReplacements = []; + +foreach ($models as $model) { + $groupedReplacements[$model->guid] = Product1cReplacement::find() + ->select('guid_replacement') + ->where(['guid' => $model->guid]) + ->andWhere(['deleted_at' => null]) + ->column(); +} +``` + +--- + +### 2. `actionView($id)` +**Назначение:** Просмотр товара и всех его замен + +**Параметры:** +- `$id` — GUID основного товара + +**Функциональность:** +- Получение всех активных замен для товара +- Загрузка последнего лога для каждой замены +- Расчёт разницы цен между основным товаром и заменами +- Получение имени администратора из `Admin` + +**Логика расчёта разницы:** +```php +$priceDifference = null; +if ($productPrice !== null && $productReplacementPrice !== null) { + $priceDifference = (int)($productReplacementPrice->price) - (int)($productPrice->price); +} +``` + +**Передаваемые данные:** +- `model` — основной товар +- `replacements` — ActiveDataProvider с заменами +- `lastLogs` — массив последних логов с информацией об администраторах +- `productPrice` — цена основного товара + +--- + +### 3. `actionCreate()` +**Назначение:** Создание новых замен для товара + +**Логика:** +1. Получение массива `guid_replacement` из формы +2. Начало транзакции +3. Создание отдельной записи для каждого заменителя +4. Логирование создания каждой записи +5. Commit транзакции или rollback при ошибке + +**Код создания:** +```php +foreach ($guidReplacements as $replacementGuid) { + $replacementModel = new Product1cReplacement([ + 'guid' => $model->guid, + 'guid_replacement' => $replacementGuid, + ]); + if (!$replacementModel->save()) { + throw new \Exception('Ошибка сохранения замены.'); + } + self::logReplacementAction( + $replacementModel->id, + 'Запись создана', + $replacementGuid + ); +} +``` + +**Особенности:** +- Использование транзакций для целостности данных +- Автоматическое логирование при создании +- Редирект на index после успеха + +--- + +### 4. `actionSearch($q, $exclude, $guid)` +**Назначение:** AJAX-поиск товаров для Select2 + +**Параметры:** +- `$q` — строка поиска по названию +- `$exclude` — список ID для исключения (через запятую) +- `$guid` — GUID товара для исключения уже добавленных замен + +**Логика исключений:** +```php +if ($guid) { + if ($guid == "guid") { + // Исключить товары, которые уже являются основными + $existingGuids = Product1cReplacement::find() + ->select(['guid']) + ->distinct() + ->column(); + $query->andWhere(['not in', 'id', $existingGuids]); + } else { + // Исключить уже добавленные замены + $existingReplacements = Product1cReplacement::find() + ->select('guid_replacement') + ->where(['guid' => $guid]) + ->column(); + $query->andWhere(['not in', 'id', $existingReplacements]); + } +} +``` + +**Формат ответа:** +```json +{ + "items": [ + { + "id": "guid", + "text": "Название товара (артикул)" + } + ] +} +``` + +--- + +### 5. `actionUpdate($id)` +**Назначение:** Редактирование замен товара + +**Параметры:** +- `$id` — GUID основного товара + +**Логика:** +1. Загрузка всех активных замен +2. Получение массивов `guid_replacement` и `replacement_ids` из POST +3. Для каждой замены: + - Если `replacement_ids[$index]` заполнен → обновление существующей + - Если пуст → создание новой записи +4. Логирование только при изменении данных + +**Код обновления:** +```php +foreach ($replacementData as $index => $replacementGuid) { + if (!empty($replacementIds[$index])) { + // Обновление существующей + $replacementModel = Product1cReplacement::findOne($replacementIds[$index]); + if ($replacementModel && $replacementModel->guid_replacement !== $replacementGuid) { + $stateBefore = $replacementModel->guid_replacement; + $replacementModel->guid_replacement = $replacementGuid; + if ($replacementModel->save()) { + self::logReplacementAction( + $replacementModel->id, + $stateBefore, + $replacementGuid + ); + } + } + } else { + // Создание новой + $replacementModel = new Product1cReplacement([ + 'guid' => $model->guid, + 'guid_replacement' => $replacementGuid, + ]); + if ($replacementModel->save()) { + self::logReplacementAction( + $replacementModel->id, + 'Запись создана', + $replacementGuid + ); + } + } +} +``` + +--- + +### 6. `actionLogHistory($guid)` +**Назначение:** Просмотр истории изменений замен для товара + +**Параметры:** +- `$guid` — GUID основного товара + +**Функциональность:** +- Получение всех ID замен для товара +- Выборка логов из `Product1cReplacementLog` +- Сортировка по дате (ASC) +- Пагинация по 20 записей + +**Передаваемые данные:** +- `dataProvider` — логи изменений +- `guid` — идентификатор товара +- `name` — название товара + +--- + +### 7. `actionDelete($id)` +**Назначение:** Мягкое удаление замены + +**Параметры:** +- `$id` — ID записи замены + +**Формат ответа:** JSON + +**Логика:** +1. Подсчёт оставшихся активных замен для товара +2. Логирование удаления +3. Вызов `softDelete()` +4. Возврат JSON с флагом `redirect` (если удалена последняя замена) + +**Пример ответа:** +```json +{ + "success": true, + "message": "Запись успешно удалена", + "redirect": false +} +``` + +--- + +## Вспомогательные методы + +### 1. `findModel($id)` +Поиск записи по ID с выбросом 404 при отсутствии. + +### 2. `findModelByGuid($id)` +Поиск первой записи по GUID (для получения основного товара). + +### 3. `logReplacementAction($replacementId, $stateBefore, $stateAfter)` (static) +**Назначение:** Запись изменений в лог + +**Логика:** +1. Проверка существования идентичного лога (дедупликация) +2. Создание записи `Product1cReplacementLog` +3. Запись ошибок в лог при неудаче + +**Код:** +```php +$existingLog = Product1cReplacementLog::find() + ->where([ + 'replacement_id' => $replacementId, + 'state_before' => $stateBefore, + 'state_after' => $stateAfter, + 'admin_id' => Yii::$app->user->id, + ]) + ->exists(); + +if ($existingLog) { + return; +} + +$log = new Product1cReplacementLog([ + 'replacement_id' => $replacementId, + 'state_before' => $stateBefore, + 'state_after' => $stateAfter, + 'date' => date('Y-m-d H:i:s'), + 'admin_id' => Yii::$app->user->id, +]); + +if (!$log->save()) { + Yii::error('Ошибка записи в лог: ' . json_encode($log->errors), __METHOD__); + throw new \Exception('Ошибка записи в лог.'); +} +``` + +--- + +## Зависимости + +### Модели +- `Product1cReplacement` — основная модель замен +- `Product1cReplacementSearch` — модель поиска +- `Product1cReplacementLog` — логи изменений +- `Products1c` — справочник товаров 1С +- `Prices` — цены товаров +- `Admin` — администраторы системы + +### Сервисы +- `Product1cReplacementService::uploadTemplateReplacement()` — загрузка XLSX + +### Behaviors +- `TimestampBehavior` — автоматические метки времени +- `VerbFilter` — (не используется, пустой массив actions) + +--- + +## Особенности реализации + +### 1. **Множественные замены** +Система хранит каждую замену как отдельную запись: +``` +guid = "A" → guid_replacement = "B" +guid = "A" → guid_replacement = "C" +guid = "A" → guid_replacement = "D" +``` + +### 2. **Транзакционность** +Все операции создания/обновления обёрнуты в транзакции для целостности данных. + +### 3. **AJAX интеграция** +- `actionSearch` для Select2 +- `actionDelete` возвращает JSON + +### 4. **Дедупликация логов** +Предотвращение повторных идентичных записей в логах. + +### 5. **Soft Delete Pattern** +Использование `deleted_at` вместо физического удаления. + +--- + +## Безопасность + +### Проблемы: +❌ **Отсутствие RBAC-проверок** +❌ **Нет `accessRules` или фильтров доступа** +❌ **Любой авторизованный пользователь может управлять заменами** + +### Рекомендации: +1. Добавить `AccessControl` behavior +2. Реализовать проверку прав для каждого экшена +3. Валидировать права на изменение критичных данных + +--- + +## Производительность + +### Узкие места: +⚠️ **N+1 запросы в `actionView()`** (цикл по заменам + запросы логов и цен) +⚠️ **Множественные запросы в `actionIndex()`** (цикл группировки замен) + +### Оптимизации: +1. Использовать `joinWith()` для загрузки связанных данных +2. Кэшировать результаты группировки +3. Добавить индексы на `guid`, `guid_replacement`, `deleted_at` + +--- + +## Примеры использования + +### 1. Создание замены +``` +POST /product1c-replacement/create +Body: + Product1cReplacement[guid] = "123-guid" + Product1cReplacement[guid_replacement][] = "456-guid" + Product1cReplacement[guid_replacement][] = "789-guid" +``` + +### 2. Поиск товаров +``` +GET /product1c-replacement/search?q=молоко&guid=123-guid +Response: {"items": [{"id": "999-guid", "text": "Молоко 3.2% (ART123)"}]} +``` + +### 3. Удаление замены +``` +POST /product1c-replacement/delete?id=42 +Response: {"success": true, "message": "Запись успешно удалена", "redirect": false} +``` + +--- + +## Связанные компоненты + +- **Модели:** `Product1cReplacement`, `Product1cReplacementLog`, `Products1c`, `Prices` +- **Сервисы:** `Product1cReplacementService` +- **Views:** `index`, `view`, `create`, `update`, `log-history` +- **Функциональность:** CRUD, логирование, поиск, импорт, расчёт цен + +--- + +## Выводы + +**Product1cReplacementController** — сложный CRUD-контроллер с расширенной функциональностью: +- ✅ Множественные связи +- ✅ Полное логирование изменений +- ✅ AJAX-поиск +- ✅ Импорт данных +- ✅ Расчёт разницы цен +- ✅ Транзакционность + +**Требует доработки:** +- ❌ Отсутствие контроля доступа +- ⚠️ Оптимизация запросов +- ⚠️ Добавление валидации прав diff --git a/erp24/docs/controllers/non-standard/crud/Product1cReplacementController_QUICK_REFERENCE.md b/erp24/docs/controllers/non-standard/crud/Product1cReplacementController_QUICK_REFERENCE.md new file mode 100644 index 00000000..895b414d --- /dev/null +++ b/erp24/docs/controllers/non-standard/crud/Product1cReplacementController_QUICK_REFERENCE.md @@ -0,0 +1,373 @@ +# Product1cReplacementController — Быстрый справочник + +## Основная информация + +**Контроллер:** `Product1cReplacementController` +**Namespace:** `yii_app\controllers\crud` +**Путь:** `erp24/controllers/crud/Product1cReplacementController.php` +**Родитель:** `yii\web\Controller` +**Назначение:** CRUD управление заменами товаров из 1С с логированием изменений + +--- + +## Быстрый доступ + +| Действие | URL | Метод | +|----------|-----|-------| +| Список замен | `/product1c-replacement/index` | GET | +| Загрузка XLSX | `/product1c-replacement/index` | POST | +| Просмотр товара | `/product1c-replacement/view?id={guid}` | GET | +| Создание замен | `/product1c-replacement/create` | GET/POST | +| Редактирование | `/product1c-replacement/update?id={guid}` | GET/POST | +| История логов | `/product1c-replacement/log-history?guid={guid}` | GET | +| Поиск товаров | `/product1c-replacement/search?q={query}` | GET | +| Удаление замены | `/product1c-replacement/delete?id={id}` | POST (AJAX) | + +--- + +## Ключевые экшены + +### 1. **actionIndex** — Список и загрузка +- **GET:** Показывает список замен с группировкой по GUID +- **POST:** Загружает XLSX файл с шаблоном замен +- **Особенность:** Группирует замены: `['guid-A' => ['guid-B', 'guid-C']]` + +### 2. **actionView** — Детальный просмотр +- Показывает товар + все замены + историю + разницу цен +- Загружает последний лог для каждой замены +- Отображает имена администраторов, выполнивших изменения + +### 3. **actionCreate** — Создание замен +- Позволяет выбрать несколько заменителей для одного товара +- Использует транзакцию для атомарности +- Логирует каждую созданную запись + +### 4. **actionSearch** — AJAX-поиск +- Для Select2 виджета +- Исключает уже добавленные замены +- Формат: `{items: [{id, text}]}` + +### 5. **actionUpdate** — Редактирование +- Обновляет существующие замены +- Добавляет новые замены +- Логирует только изменения + +### 6. **actionLogHistory** — История изменений +- Показывает все логи для товара +- Пагинация 20 записей +- Сортировка от старых к новым + +### 7. **actionDelete** — Мягкое удаление +- Устанавливает `deleted_at` +- Возвращает JSON с флагом `redirect` +- Логирует удаление + +--- + +## Модели и зависимости + +### Модели +- **Product1cReplacement** — основная модель замен +- **Product1cReplacementLog** — логи изменений +- **Product1cReplacementSearch** — модель поиска +- **Products1c** — справочник товаров 1С +- **Prices** — цены товаров +- **Admin** — администраторы + +### Сервисы +- **Product1cReplacementService::uploadTemplateReplacement()** — загрузка XLSX + +--- + +## Система логирования + +### Метод: `logReplacementAction($replacementId, $stateBefore, $stateAfter)` + +**Записывает в лог:** +- ID замены +- Состояние до изменения +- Состояние после изменения +- Дата и время +- ID администратора + +**Предотвращает дубликаты:** Проверяет существование идентичного лога перед сохранением. + +**События логирования:** +| Событие | state_before | state_after | +|---------|--------------|-------------| +| Создание | `'Запись создана'` | `guid_replacement` | +| Изменение | старый `guid_replacement` | новый `guid_replacement` | +| Удаление | старый `guid_replacement` | `'Запись удалена'` | + +--- + +## Особенности реализации + +### 1. Множественные замены +Один товар может иметь несколько замен, каждая хранится отдельной записью: +``` +guid = "A" → guid_replacement = "B" +guid = "A" → guid_replacement = "C" +guid = "A" → guid_replacement = "D" +``` + +### 2. Транзакционность +Все операции создания/обновления выполняются в транзакциях: +```php +$transaction = Yii::$app->db->beginTransaction(); +try { + // операции + $transaction->commit(); +} catch (\Exception $e) { + $transaction->rollBack(); + throw $e; +} +``` + +### 3. Расчёт разницы цен +Автоматически вычисляется разница между ценой основного товара и замены: +```php +$priceDifference = (int)($productReplacementPrice->price) - (int)($productPrice->price); +``` + +### 4. Soft Delete +Используется мягкое удаление через `deleted_at`: +```php +$model->softDelete(); // вместо $model->delete() +``` + +### 5. AJAX интеграция +- `actionSearch` — для Select2 виджета +- `actionDelete` — возвращает JSON для AJAX-обработки + +--- + +## Примеры использования + +### Создание замены +```php +POST /product1c-replacement/create + +Product1cReplacement[guid] = "123-guid" +Product1cReplacement[guid_replacement][] = "456-guid" +Product1cReplacement[guid_replacement][] = "789-guid" +``` + +### Поиск товаров +```php +GET /product1c-replacement/search?q=молоко&guid=123-guid + +Response: +{ + "items": [ + {"id": "999-guid", "text": "Молоко 3.2% (ART123)"} + ] +} +``` + +### Удаление замены +```javascript +$.post('/product1c-replacement/delete?id=42', function(response) { + if (response.success) { + if (response.redirect) { + window.location.href = '/product1c-replacement/index'; + } else { + location.reload(); + } + } +}); +``` + +### Загрузка XLSX +```html +
+ + +
+``` + +--- + +## Безопасность + +### ⚠️ Проблемы: +- **Нет RBAC-проверок** +- **Нет `AccessControl` behavior** +- **Любой авторизованный пользователь может управлять заменами** + +### ✅ Рекомендации: +1. Добавить `AccessControl` behavior +2. Реализовать проверку прав для каждого экшена +3. Валидировать права на изменение критичных данных + +--- + +## Производительность + +### ⚠️ Узкие места: +- **N+1 запросы в `actionView()`** (цикл по заменам + запросы логов и цен) +- **Множественные запросы в `actionIndex()`** (цикл группировки замен) + +### ✅ Оптимизации: +1. Использовать `joinWith()` для загрузки связанных данных +2. Кэшировать результаты группировки +3. Добавить индексы на `guid`, `guid_replacement`, `deleted_at` + +--- + +## Диаграмма потока данных + +```mermaid +graph TD + A[Index] -->|Создать| B[Create] + A -->|Просмотр| C[View] + A -->|Загрузить XLSX| D[uploadTemplateReplacement] + + B -->|POST| E[Транзакция] + E -->|Создание замен| F[logReplacementAction] + F -->|Redirect| A + + C -->|Редактировать| G[Update] + C -->|История| H[LogHistory] + C -->|Удалить| I[Delete] + + G -->|POST| J[Транзакция] + J -->|Обновление/создание| F + J -->|Redirect| C + + I -->|softDelete| F + I -->|JSON| K[AJAX Response] + K -->|redirect=true| A + K -->|redirect=false| C + + B -.->|AJAX| L[Search] + G -.->|AJAX| L +``` + +--- + +## Связанные компоненты + +### Контроллеры +- `Product1cReplacementController` (текущий) + +### Модели +- `Product1cReplacement` +- `Product1cReplacementLog` +- `Product1cReplacementSearch` +- `Products1c` +- `Prices` +- `Admin` + +### Сервисы +- `Product1cReplacementService` + +### Views +- `views/product1c-replacement/index.php` +- `views/product1c-replacement/view.php` +- `views/product1c-replacement/create.php` +- `views/product1c-replacement/update.php` +- `views/product1c-replacement/log-history.php` + +--- + +## Чеклист разработчика + +### При создании замен: +- [ ] Проверить наличие товара в `Products1c` +- [ ] Убедиться, что заменители существуют +- [ ] Использовать транзакции +- [ ] Логировать каждую операцию + +### При обновлении: +- [ ] Проверить права доступа +- [ ] Логировать только изменения +- [ ] Обрабатывать создание новых и обновление существующих + +### При удалении: +- [ ] Использовать soft delete +- [ ] Логировать удаление +- [ ] Проверить количество оставшихся замен +- [ ] Вернуть корректный JSON + +### При разработке новых функций: +- [ ] Добавить RBAC-проверки +- [ ] Оптимизировать запросы (избегать N+1) +- [ ] Использовать транзакции для критичных операций +- [ ] Логировать все изменения данных + +--- + +## FAQ + +**Q: Как добавить несколько замен одновременно?** +A: В форме создания/редактирования используется Select2 с множественным выбором. Массив `guid_replacement[]` обрабатывается циклом. + +**Q: Почему при удалении последней замены происходит редирект?** +A: Чтобы избежать ошибки 404 на странице `view`, если у товара не осталось замен. + +**Q: Как работает дедупликация логов?** +A: Перед сохранением проверяется существование записи с идентичными `replacement_id`, `state_before`, `state_after`, `admin_id`. + +**Q: Можно ли восстановить удалённую замену?** +A: Да, через SQL: `UPDATE product1c_replacement SET deleted_at = NULL WHERE id = ?` + +**Q: Как посмотреть полную историю изменений?** +A: Перейти в `actionLogHistory` для конкретного товара (GUID). + +--- + +## Полезные SQL-запросы + +### Все замены для товара +```sql +SELECT * FROM product1c_replacement +WHERE guid = 'your-guid' AND deleted_at IS NULL; +``` + +### История изменений замены +```sql +SELECT l.*, a.name as admin_name +FROM product1c_replacement_log l +LEFT JOIN admin a ON l.admin_id = a.id +WHERE l.replacement_id = 42 +ORDER BY l.date ASC; +``` + +### Товары без замен +```sql +SELECT p.* FROM products1c p +LEFT JOIN product1c_replacement r ON p.id = r.guid +WHERE r.id IS NULL AND p.tip = 'products'; +``` + +### Замены с разницей цен > 100 +```sql +SELECT + r.guid, + r.guid_replacement, + p1.price as original_price, + p2.price as replacement_price, + (p2.price - p1.price) as difference +FROM product1c_replacement r +JOIN prices p1 ON r.guid = p1.product_id +JOIN prices p2 ON r.guid_replacement = p2.product_id +WHERE (p2.price - p1.price) > 100 +AND r.deleted_at IS NULL; +``` + +--- + +## Ключевые выводы + +✅ **Сильные стороны:** +- Полное логирование изменений +- Транзакционность операций +- AJAX интеграция +- Импорт данных через XLSX +- Расчёт разницы цен + +❌ **Требует доработки:** +- Отсутствие контроля доступа +- Оптимизация запросов (N+1) +- Валидация прав пользователей