--- /dev/null
+<?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;
+ }
+ }
+}
]);
$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) => [
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,
],
'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) {
$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();
}
}
- 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;
--- /dev/null
+<?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);
+ }
+ }
+}
* @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
[['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],
];
}
--- /dev/null
+<?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++;
+ }
+}
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);