From: fomichev Date: Thu, 4 Jun 2026 09:39:58 +0000 (+0300) Subject: ERP-396: теги каналов перенесены в city_store_params + крон синхронизации StoreDynamic X-Git-Url: https://gitweb.erp-flowers.ru/?a=commitdiff_plain;h=1dd57f58aa8528df18f242b323f9f1c863d843f8;p=erp24_rep%2Fyii-erp24%2F.git ERP-396: теги каналов перенесены в city_store_params + крон синхронизации StoreDynamic - Добавлено поле assortment_label_ids (varchar 255) в city_store_params - Контроллер читает/пишет теги из city_store_params вместо store_dynamic - JS: label_ids → assortment_label_ids в POST-запросе - StoreDynamicSyncService: крон сравнивает is_active (cat 4) и assortment_label_ids (cat 5) с активной записью store_dynamic, при изменении закрывает старую (позавчера 23:59:59) и создаёт новую (вчера 00:00:00) — запуск 0 3 * * * - StoreController: php yii store/sync-store-dynamic Co-Authored-By: Claude Sonnet 4.6 --- diff --git a/erp24/commands/StoreController.php b/erp24/commands/StoreController.php new file mode 100644 index 00000000..7fee1322 --- /dev/null +++ b/erp24/commands/StoreController.php @@ -0,0 +1,47 @@ +> /var/log/store-sync.log 2>&1 + */ +class StoreController extends Controller +{ + /** + * Синхронизирует историю StoreDynamic (cat 4 = is_active, cat 5 = assortment_label_ids) + * с актуальным состоянием city_store_params. + * + * Записи создаются датой предыдущего дня, поскольку команда работает в ~03:00. + */ + public function actionSyncStoreDynamic(): int + { + $this->stdout("StoreDynamic sync: start\n"); + + try { + $service = new StoreDynamicSyncService(); + $service->sync(); + + $this->stdout(sprintf( + "Done: created=%d skipped=%d\n", + $service->getCreated(), + $service->getSkipped() + )); + + return ExitCode::OK; + } catch (\Throwable $e) { + $this->stderr("FATAL: {$e->getMessage()}\n"); + Yii::error('StoreDynamic sync fatal: ' . $e->getMessage(), 'store-sync'); + return ExitCode::SOFTWARE; + } + } +} diff --git a/erp24/controllers/CityStoreManagementController.php b/erp24/controllers/CityStoreManagementController.php index 526becb5..31c5c252 100644 --- a/erp24/controllers/CityStoreManagementController.php +++ b/erp24/controllers/CityStoreManagementController.php @@ -79,14 +79,9 @@ class CityStoreManagementController extends Controller ]); $terrManager = $terrManagerDyn ? Admin::findOne($terrManagerDyn->value_int) : null; - $labelsDyn = StoreDynamic::findOne([ - 'store_id' => $id, - 'active' => 1, - 'category' => StoreDynamic::CATEGORY_ASSORTMENT_LABELS, - ]); $labelIds = []; - if ($labelsDyn && $labelsDyn->value_string !== null && $labelsDyn->value_string !== '') { - $labelIds = array_values(array_filter(array_map('intval', explode(',', $labelsDyn->value_string)))); + if ($params && $params->assortment_label_ids !== null && $params->assortment_label_ids !== '') { + $labelIds = array_values(array_filter(array_map('intval', explode(',', $params->assortment_label_ids)))); } $allLabels = AssortmentLabel::findActive(); $labelsArray = array_map(static fn(AssortmentLabel $l) => [ @@ -187,7 +182,7 @@ class CityStoreManagementController extends Controller return ArrayHelper::map($q->orderBy(['name_full' => SORT_ASC])->all(), 'id', 'name_full'); })(), - // assortment labels (StoreDynamic category 5) + // assortment labels (city_store_params.assortment_label_ids) 'labelIds' => $labelIds, 'labelsArray' => $labelsArray, ], @@ -283,7 +278,7 @@ class CityStoreManagementController extends Controller 'store_type', 'store_area', 'showcase_volume', 'freeze_area', 'freeze_volume', 'address_region', 'address_city', 'address_district', 'is_active', 'camera_model', 'camera_count', 'microphone_model', 'router_model', - 'internet_provider', 'cash_register_info', + 'internet_provider', 'cash_register_info', 'assortment_label_ids', ]; foreach ($allowed as $field) { @@ -320,10 +315,6 @@ class CityStoreManagementController extends Controller $this->recordStoreDynamic($storeId, StoreDynamic::CATEGORY_TERRITORIAL_MANAGER, (int)$post['territorial_manager']); } - if (array_key_exists('label_ids', $post)) { - $this->recordStoreDynamicStr($storeId, StoreDynamic::CATEGORY_ASSORTMENT_LABELS, (string)$post['label_ids']); - } - $transaction->commit(); } catch (\Throwable $e) { $transaction->rollBack(); @@ -374,28 +365,6 @@ class CityStoreManagementController extends Controller } } - private function recordStoreDynamicStr(int $storeId, int $category, string $value): void - { - StoreDynamic::updateAll( - ['active' => 0, 'date_to' => date('Y-m-d H:i:s')], - ['active' => 1, 'category' => $category, 'store_id' => $storeId] - ); - - $record = new StoreDynamic([ - 'store_id' => $storeId, - 'value_type' => 'string', - 'value_string' => $value, - 'date_from' => date('Y-m-d H:i:s'), - 'date_to' => '2100-01-01 00:00:00', - 'active' => 1, - 'category' => $category, - ]); - - if (!$record->save()) { - Yii::error('StoreDynamic (str) save error: ' . json_encode($record->getErrors()), __CLASS__); - } - } - public function actionUploadStorePhoto(): array { Yii::$app->response->format = Response::FORMAT_JSON; diff --git a/erp24/migrations/m260604_100000_add_assortment_label_ids_to_city_store_params.php b/erp24/migrations/m260604_100000_add_assortment_label_ids_to_city_store_params.php new file mode 100644 index 00000000..6c78610a --- /dev/null +++ b/erp24/migrations/m260604_100000_add_assortment_label_ids_to_city_store_params.php @@ -0,0 +1,39 @@ +db->getTableSchema(self::TABLE, true); + if ($schema === null) { + return; + } + + if ($schema->getColumn(self::COLUMN) === null) { + $this->addColumn(self::TABLE, self::COLUMN, 'varchar(255) DEFAULT NULL'); + } + } + + public function safeDown(): void + { + $schema = $this->db->getTableSchema(self::TABLE, true); + if ($schema === null) { + return; + } + + if ($schema->getColumn(self::COLUMN) !== null) { + $this->dropColumn(self::TABLE, self::COLUMN); + } + } +} diff --git a/erp24/records/CityStoreParams.php b/erp24/records/CityStoreParams.php index 15369230..08d65620 100644 --- a/erp24/records/CityStoreParams.php +++ b/erp24/records/CityStoreParams.php @@ -28,6 +28,7 @@ use yii\db\Expression; * @property string|null $router_model * @property string|null $internet_provider * @property string|null $cash_register_info + * @property string|null $assortment_label_ids * @property int $created_by * @property string $created_at * @property int|null $updated_by @@ -76,7 +77,7 @@ class CityStoreParams extends ActiveRecord [['is_active'], 'boolean'], [['is_active'], 'default', 'value' => true], [['camera_model', 'microphone_model', 'router_model', 'internet_provider'], 'string', 'max' => 150], - [['cash_register_info'], 'string', 'max' => 200], + [['cash_register_info', 'assortment_label_ids'], 'string', 'max' => 255], [['camera_count'], 'integer', 'min' => 0], ]; } diff --git a/erp24/services/StoreDynamicSyncService.php b/erp24/services/StoreDynamicSyncService.php new file mode 100644 index 00000000..e6ea909d --- /dev/null +++ b/erp24/services/StoreDynamicSyncService.php @@ -0,0 +1,137 @@ +select(['store_id', 'is_active', 'assortment_label_ids'])->asArray()->all(), + 'store_id' + ); + + // Batch-загрузка активных записей store_dynamic для категорий 4 и 5 + $dynamics = StoreDynamic::find() + ->where(['active' => 1, 'category' => [StoreDynamic::CATEGORY_IS_ACTIVE, StoreDynamic::CATEGORY_ASSORTMENT_LABELS]]) + ->all(); + + // Индекс: store_id => [category => StoreDynamic] + $dynIndex = []; + foreach ($dynamics as $d) { + $dynIndex[$d->store_id][$d->category] = $d; + } + + $storeIds = ArrayHelper::getColumn(CityStore::find()->select('id')->asArray()->all(), 'id'); + + $db = Yii::$app->db; + foreach ($storeIds as $storeId) { + $params = $paramsByStore[$storeId] ?? null; + + $isActive = $params !== null ? (int)(bool)$params['is_active'] : 1; + $labelIds = $params !== null ? (string)($params['assortment_label_ids'] ?? '') : ''; + + $txn = $db->beginTransaction(); + try { + $this->syncCategory( + $storeId, + StoreDynamic::CATEGORY_IS_ACTIVE, + 'int', + $isActive, + null, + $dynIndex[$storeId][StoreDynamic::CATEGORY_IS_ACTIVE] ?? null, + $yesterday, + $dayBefore + ); + + $this->syncCategory( + $storeId, + StoreDynamic::CATEGORY_ASSORTMENT_LABELS, + 'string', + null, + $labelIds, + $dynIndex[$storeId][StoreDynamic::CATEGORY_ASSORTMENT_LABELS] ?? null, + $yesterday, + $dayBefore + ); + + $txn->commit(); + } catch (\Throwable $e) { + $txn->rollBack(); + Yii::error("StoreDynamicSync storeId={$storeId}: " . $e->getMessage(), __CLASS__); + } + } + } + + public function getCreated(): int { return $this->created; } + public function getSkipped(): int { return $this->skipped; } + + private function syncCategory( + int $storeId, + int $category, + string $valueType, + ?int $valueInt, + ?string $valueString, + ?StoreDynamic $active, + string $yesterday, + string $dayBefore + ): void { + $unchanged = $active !== null && ( + ($valueType === 'int' && (int)$active->value_int === $valueInt) || + ($valueType === 'string' && $active->value_string === $valueString) + ); + + if ($unchanged) { + $this->skipped++; + return; + } + + // Деактивируем текущую запись — закрываем позавчерашним днём, + // чтобы новая запись (date_from = вчера 00:00:00) не перекрывалась со старой + if ($active !== null) { + StoreDynamic::updateAll( + ['active' => 0, 'date_to' => $dayBefore . ' 23:59:59'], + ['id' => $active->id] + ); + } + + $record = new StoreDynamic(); + $record->store_id = $storeId; + $record->value_type = $valueType; + $record->value_int = $valueInt; + $record->value_string = $valueString; + $record->date_from = $yesterday . ' 00:00:00'; + $record->date_to = '2100-01-01 00:00:00'; + $record->active = 1; + $record->category = $category; + + if (!$record->save()) { + throw new \RuntimeException( + "Save failed for storeId={$storeId} cat={$category}: " . json_encode($record->getErrors()) + ); + } + + $this->created++; + } +} diff --git a/erp24/web/js/city-store-management/city-store-management.js b/erp24/web/js/city-store-management/city-store-management.js index 7d981aec..451a4259 100644 --- a/erp24/web/js/city-store-management/city-store-management.js +++ b/erp24/web/js/city-store-management/city-store-management.js @@ -558,7 +558,7 @@ function saveChanges() { router_model: val('fRouterModel'), internet_provider: val('fInternetProvider'), cash_register_info: val('fCashRegisterInfo'), - label_ids: (D.labelIds || []).join(','), + assortment_label_ids: (D.labelIds || []).join(','), }; var p1 = postJson(CSM_URLS.saveCityStore, csStore);