From ecd372dad30e05c5913fa489e561475584b89079 Mon Sep 17 00:00:00 2001 From: Nikita Kiselev Date: Tue, 9 Dec 2025 00:13:03 +0300 Subject: [PATCH] feat: add warmup images command --- module/oc_telegram_shop/upload/cli.php | 4 +- .../console/ApplicationFactory.php | 2 + .../Commands/WarmupImagesCacheCommand.php | 242 ++++++++++++++++++ .../framework/ImageTool/ImageFactory.php | 3 +- 4 files changed, 249 insertions(+), 2 deletions(-) create mode 100644 module/oc_telegram_shop/upload/oc_telegram_shop/console/Commands/WarmupImagesCacheCommand.php diff --git a/module/oc_telegram_shop/upload/cli.php b/module/oc_telegram_shop/upload/cli.php index a536c7d..e7b3d55 100755 --- a/module/oc_telegram_shop/upload/cli.php +++ b/module/oc_telegram_shop/upload/cli.php @@ -6,6 +6,7 @@ use Console\Commands\PulseSendEventsCommand; use Console\Commands\ScheduleListCommand; use Console\Commands\ScheduleRunCommand; use Console\Commands\VersionCommand; +use Console\Commands\WarmupImagesCacheCommand; use Monolog\Handler\RotatingFileHandler; use Monolog\Logger; use Openguru\OpenCartFramework\QueryBuilder\Connections\MySqlConnection; @@ -64,7 +65,7 @@ $items = Arr::mergeArraysRecursively($json, [ ], 'store' => [ 'oc_store_id' => 0, - 'oc_default_currency' => 1, + 'oc_default_currency' => 'RUB', 'oc_config_tax' => false, ], 'orders' => [ @@ -94,4 +95,5 @@ $console->add($app->get(VersionCommand::class)); $console->add($app->get(ScheduleRunCommand::class)); $console->add($app->get(ScheduleListCommand::class)); $console->add($app->get(PulseSendEventsCommand::class)); +$console->add($app->get(WarmupImagesCacheCommand::class)); $console->run(); diff --git a/module/oc_telegram_shop/upload/oc_telegram_shop/console/ApplicationFactory.php b/module/oc_telegram_shop/upload/oc_telegram_shop/console/ApplicationFactory.php index 355c862..31130c6 100755 --- a/module/oc_telegram_shop/upload/oc_telegram_shop/console/ApplicationFactory.php +++ b/module/oc_telegram_shop/upload/oc_telegram_shop/console/ApplicationFactory.php @@ -6,6 +6,7 @@ use App\ServiceProviders\AppServiceProvider; use App\ServiceProviders\SettingsServiceProvider; use Openguru\OpenCartFramework\Application; use Openguru\OpenCartFramework\Cache\CacheServiceProvider; +use Openguru\OpenCartFramework\ImageTool\ImageToolServiceProvider; use Openguru\OpenCartFramework\QueryBuilder\QueryBuilderServiceProvider; use Openguru\OpenCartFramework\Scheduler\SchedulerServiceProvider; use Openguru\OpenCartFramework\Support\Arr; @@ -29,6 +30,7 @@ class ApplicationFactory TelegramServiceProvider::class, TeleCartPulseServiceProvider::class, SchedulerServiceProvider::class, + ImageToolServiceProvider::class, ]); } } diff --git a/module/oc_telegram_shop/upload/oc_telegram_shop/console/Commands/WarmupImagesCacheCommand.php b/module/oc_telegram_shop/upload/oc_telegram_shop/console/Commands/WarmupImagesCacheCommand.php new file mode 100644 index 0000000..30655fa --- /dev/null +++ b/module/oc_telegram_shop/upload/oc_telegram_shop/console/Commands/WarmupImagesCacheCommand.php @@ -0,0 +1,242 @@ +queryBuilder = $queryBuilder; + $this->image = $image; + $this->settings = $settings; + $this->logger = $logger; + } + + protected function configure(): void + { + $this->addArgument( + 'product_id', + InputArgument::OPTIONAL, + 'ID товара для прогрева кеша (если не указан, прогреваются все товары)' + ); + } + + protected function execute(InputInterface $input, OutputInterface $output): int + { + $io = new SymfonyStyle($input, $output); + $productId = $input->getArgument('product_id'); + + $io->title('Прогрев кеша изображений товаров'); + + // Получаем настройки + $aspectRatio = $this->settings->get('app.image_aspect_ratio', '1:1'); + $cropAlgorithm = $this->settings->get('app.image_crop_algorithm', 'cover'); + [$imageWidth, $imageHeight] = ImageUtils::aspectRatioToSize($aspectRatio); + $languageId = $this->settings->config()->getApp()->getLanguageId(); + + $io->section('Настройки'); + $io->listing([ + "Соотношение сторон: {$aspectRatio}", + "Алгоритм обрезки: {$cropAlgorithm}", + "Размер изображения: {$imageWidth}x{$imageHeight}", + "Размер миниатюры: 500x500", + "Размер большого изображения: 1000x1000", + ]); + + // Получаем список товаров + $products = $this->getProducts($productId, $languageId); + + if (empty($products)) { + $io->warning('Товары не найдены'); + return Command::SUCCESS; + } + + $totalProducts = count($products); + $io->section("Найдено товаров: {$totalProducts}"); + + $stats = [ + 'products' => 0, + 'main_images' => 0, + 'additional_images' => 0, + 'thumbnails' => 0, + 'large_images' => 0, + 'errors' => 0, + ]; + + $progressBar = $io->createProgressBar($totalProducts); + $progressBar->setFormat(' %current%/%max% [%bar%] %percent:3s%% %message%'); + $progressBar->setMessage('Обработка товаров...'); + $progressBar->start(); + + foreach ($products as $product) { + $productId = $product['product_id']; + $productName = $product['product_name'] ?? "ID: {$productId}"; + $progressBar->setMessage("Товар: {$productName}"); + + try { + // Прогреваем основное изображение товара + if (!empty($product['product_image'])) { + try { + $this->image->make($product['product_image']) + ->crop($cropAlgorithm, $imageWidth, $imageHeight) + ->url(); + $stats['main_images']++; + } catch (Exception $e) { + $this->logger->error("Ошибка при прогреве основного изображения товара {$productId}: " . $e->getMessage()); + $stats['errors']++; + } + } + + // Получаем дополнительные изображения товара + $additionalImages = $this->getProductAdditionalImages($productId); + $processedAdditional = 0; + foreach ($additionalImages as $imagePath) { + if ($processedAdditional >= 2) { + break; // Ограничиваем до 2 дополнительных изображений, как в ProductsService + } + try { + $this->image->make($imagePath) + ->crop($cropAlgorithm, $imageWidth, $imageHeight) + ->url(); + $stats['additional_images']++; + $processedAdditional++; + } catch (Exception $e) { + $this->logger->error("Ошибка при прогреве дополнительного изображения товара {$productId}: " . $e->getMessage()); + $stats['errors']++; + } + } + + // Прогреваем изображения для детальной страницы (миниатюры и большие) + $allImages = []; + if (!empty($product['product_image'])) { + $allImages[] = $product['product_image']; + } + $allImages = array_merge($allImages, $additionalImages); + + foreach ($allImages as $imagePath) { + try { + // Миниатюра + $this->image->make($imagePath) + ->contain(500, 500) + ->url(); + $stats['thumbnails']++; + + // Большое изображение + $this->image->make($imagePath) + ->resize(1000, 1000) + ->url(); + $stats['large_images']++; + } catch (Exception $e) { + $this->logger->error("Ошибка при прогреве изображений для детальной страницы товара {$productId}: " . $e->getMessage()); + $stats['errors']++; + } + } + + $stats['products']++; + } catch (Exception $e) { + $this->logger->error("Ошибка при обработке товара {$productId}: " . $e->getMessage()); + $stats['errors']++; + } + + $progressBar->advance(); + } + + $progressBar->setMessage('Завершено'); + $progressBar->finish(); + $io->newLine(2); + + // Выводим статистику + $io->section('Статистика'); + $io->table( + ['Метрика', 'Значение'], + [ + ['Обработано товаров', $stats['products']], + ['Основных изображений', $stats['main_images']], + ['Дополнительных изображений', $stats['additional_images']], + ['Миниатюр (500x500)', $stats['thumbnails']], + ['Больших изображений (1000x1000)', $stats['large_images']], + ['Ошибок', $stats['errors']], + ] + ); + + if ($stats['errors'] > 0) { + $io->warning("Обнаружено {$stats['errors']} ошибок. Проверьте логи для подробностей."); + } else { + $io->success('Кеш изображений успешно прогрет!'); + } + + return Command::SUCCESS; + } + + /** + * Получает список товаров для прогрева кеша + */ + private function getProducts(?string $productId, int $languageId): array + { + $query = $this->queryBuilder->newQuery() + ->select([ + 'products.product_id' => 'product_id', + 'products.image' => 'product_image', + 'product_description.name' => 'product_name', + ]) + ->from(db_table('product'), 'products') + ->join( + db_table('product_description') . ' AS product_description', + function (JoinClause $join) use ($languageId) { + $join->on('products.product_id', '=', 'product_description.product_id') + ->where('product_description.language_id', '=', $languageId); + } + ) + ->where('products.status', '=', 1) + ->whereRaw('products.date_available < NOW()'); + + if ($productId !== null) { + $query->where('products.product_id', '=', (int) $productId); + } + + return $query->orderBy('products.product_id', 'ASC')->get(); + } + + /** + * Получает дополнительные изображения товара + */ + private function getProductAdditionalImages(int $productId): array + { + $images = $this->queryBuilder->newQuery() + ->select(['products_images.image' => 'image']) + ->from(db_table('product_image'), 'products_images') + ->where('products_images.product_id', '=', $productId) + ->orderBy('products_images.sort_order') + ->get(); + + return Arr::pluck($images, 'image'); + } +} + diff --git a/module/oc_telegram_shop/upload/oc_telegram_shop/framework/ImageTool/ImageFactory.php b/module/oc_telegram_shop/upload/oc_telegram_shop/framework/ImageTool/ImageFactory.php index 7f9c47c..fece8be 100755 --- a/module/oc_telegram_shop/upload/oc_telegram_shop/framework/ImageTool/ImageFactory.php +++ b/module/oc_telegram_shop/upload/oc_telegram_shop/framework/ImageTool/ImageFactory.php @@ -46,6 +46,7 @@ class ImageFactory */ public function make(?string $path = null): self { + $this->modifications = []; $this->path = $path; $this->fullPath = $this->imageDir . '/' . $this->path; @@ -178,7 +179,7 @@ class ImageFactory $filename .= '_' . substr(hash('SHA256', json_encode($this->modifications)), 0, 12); } - return $this->imageDir . '/cache/' . $imagePath . $filename . '.' . $format; + return $this->imageDir . '/cache/telecart/' . $imagePath . $filename . '.' . $format; } private function applyModifications(): void