feat: add warmup images command

This commit is contained in:
2025-12-09 00:13:03 +03:00
parent 359395b7e8
commit ecd372dad3
4 changed files with 249 additions and 2 deletions

View File

@@ -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();

View File

@@ -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,
]);
}
}

View File

@@ -0,0 +1,242 @@
<?php
namespace Console\Commands;
use App\Services\SettingsService;
use Exception;
use Openguru\OpenCartFramework\ImageTool\ImageFactory;
use Openguru\OpenCartFramework\ImageTool\ImageUtils;
use Openguru\OpenCartFramework\QueryBuilder\Builder;
use Openguru\OpenCartFramework\QueryBuilder\JoinClause;
use Openguru\OpenCartFramework\Support\Arr;
use Psr\Log\LoggerInterface;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Style\SymfonyStyle;
class WarmupImagesCacheCommand extends TeleCartCommand
{
protected static $defaultName = 'images:warmup';
protected static $defaultDescription = 'Прогрев кеша изображений товаров';
private Builder $queryBuilder;
private ImageFactory $image;
private SettingsService $settings;
private LoggerInterface $logger;
public function __construct(
Builder $queryBuilder,
ImageFactory $image,
SettingsService $settings,
LoggerInterface $logger
) {
parent::__construct();
$this->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');
}
}

View File

@@ -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