]> gitweb.erp-flowers.ru Git - erp24_rep/yii-erp24/.git/commitdiff
ERP-396: теги каналов перенесены в city_store_params + крон синхронизации StoreDynamic
authorfomichev <vladimir.fomichev@erp-flowers.ru>
Thu, 4 Jun 2026 09:39:58 +0000 (12:39 +0300)
committerfomichev <vladimir.fomichev@erp-flowers.ru>
Thu, 4 Jun 2026 09:39:58 +0000 (12:39 +0300)
- Добавлено поле 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 <noreply@anthropic.com>
erp24/commands/StoreController.php [new file with mode: 0644]
erp24/controllers/CityStoreManagementController.php
erp24/migrations/m260604_100000_add_assortment_label_ids_to_city_store_params.php [new file with mode: 0644]
erp24/records/CityStoreParams.php
erp24/services/StoreDynamicSyncService.php [new file with mode: 0644]
erp24/web/js/city-store-management/city-store-management.js

diff --git a/erp24/commands/StoreController.php b/erp24/commands/StoreController.php
new file mode 100644 (file)
index 0000000..7fee132
--- /dev/null
@@ -0,0 +1,47 @@
+<?php
+
+declare(strict_types=1);
+
+namespace yii_app\commands;
+
+use Yii;
+use yii\console\Controller;
+use yii\console\ExitCode;
+use yii_app\services\StoreDynamicSyncService;
+
+/**
+ * Управление магазинами — консольные утилиты.
+ *
+ * Crontab (03:00 каждый день):
+ *   0 3 * * * cd /www && php yii store/sync-store-dynamic >> /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;
+        }
+    }
+}
index 526becb5ef4df9b3d9591f32b188b9dab6026e58..31c5c252b52c3cad145118d2278d518afac6f66b 100644 (file)
@@ -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 (file)
index 0000000..6c78610
--- /dev/null
@@ -0,0 +1,39 @@
+<?php
+
+declare(strict_types=1);
+
+use yii\db\Migration;
+
+/**
+ * Переносит хранение тегов каналов (assortment labels) из store_dynamic (category 5)
+ * в city_store_params — поле assortment_label_ids, строка вида "1,3,7".
+ */
+class m260604_100000_add_assortment_label_ids_to_city_store_params extends Migration
+{
+    private const TABLE = 'city_store_params';
+    private const COLUMN = 'assortment_label_ids';
+
+    public function safeUp(): void
+    {
+        $schema = $this->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);
+        }
+    }
+}
index 15369230d542cb4048a199c7b83d3c5b1347b6c9..08d65620e0aa6ffe043c160d38889365c12ceaf9 100644 (file)
@@ -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 (file)
index 0000000..e6ea909
--- /dev/null
@@ -0,0 +1,137 @@
+<?php
+
+declare(strict_types=1);
+
+namespace yii_app\services;
+
+use Yii;
+use yii\helpers\ArrayHelper;
+use yii_app\records\CityStore;
+use yii_app\records\CityStoreParams;
+use yii_app\records\StoreDynamic;
+
+/**
+ * Синхронизирует историю StoreDynamic (cat 4 = is_active, cat 5 = assortment_label_ids)
+ * с актуальным состоянием в city_store_params.
+ *
+ * Запускается кроном ~в 03:00, поэтому записывает изменения датой предыдущего дня.
+ */
+class StoreDynamicSyncService
+{
+    private int $created = 0;
+    private int $skipped = 0;
+
+    public function sync(): void
+    {
+        $yesterday  = date('Y-m-d', strtotime('-1 day'));
+        $dayBefore  = date('Y-m-d', strtotime('-2 days'));
+
+        // Batch-загрузка: все store_id → CityStoreParams (только нужные поля)
+        $paramsByStore = ArrayHelper::index(
+            CityStoreParams::find()->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++;
+    }
+}
index 7d981aec345e70383ee72b7eadbfb8f14b0bffe0..451a425904ad23a9d06b804f7a89bfb4dd67850d 100644 (file)
@@ -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);