feat: add warmup images command
This commit is contained in:
@@ -6,6 +6,7 @@ use Console\Commands\PulseSendEventsCommand;
|
|||||||
use Console\Commands\ScheduleListCommand;
|
use Console\Commands\ScheduleListCommand;
|
||||||
use Console\Commands\ScheduleRunCommand;
|
use Console\Commands\ScheduleRunCommand;
|
||||||
use Console\Commands\VersionCommand;
|
use Console\Commands\VersionCommand;
|
||||||
|
use Console\Commands\WarmupImagesCacheCommand;
|
||||||
use Monolog\Handler\RotatingFileHandler;
|
use Monolog\Handler\RotatingFileHandler;
|
||||||
use Monolog\Logger;
|
use Monolog\Logger;
|
||||||
use Openguru\OpenCartFramework\QueryBuilder\Connections\MySqlConnection;
|
use Openguru\OpenCartFramework\QueryBuilder\Connections\MySqlConnection;
|
||||||
@@ -64,7 +65,7 @@ $items = Arr::mergeArraysRecursively($json, [
|
|||||||
],
|
],
|
||||||
'store' => [
|
'store' => [
|
||||||
'oc_store_id' => 0,
|
'oc_store_id' => 0,
|
||||||
'oc_default_currency' => 1,
|
'oc_default_currency' => 'RUB',
|
||||||
'oc_config_tax' => false,
|
'oc_config_tax' => false,
|
||||||
],
|
],
|
||||||
'orders' => [
|
'orders' => [
|
||||||
@@ -94,4 +95,5 @@ $console->add($app->get(VersionCommand::class));
|
|||||||
$console->add($app->get(ScheduleRunCommand::class));
|
$console->add($app->get(ScheduleRunCommand::class));
|
||||||
$console->add($app->get(ScheduleListCommand::class));
|
$console->add($app->get(ScheduleListCommand::class));
|
||||||
$console->add($app->get(PulseSendEventsCommand::class));
|
$console->add($app->get(PulseSendEventsCommand::class));
|
||||||
|
$console->add($app->get(WarmupImagesCacheCommand::class));
|
||||||
$console->run();
|
$console->run();
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ use App\ServiceProviders\AppServiceProvider;
|
|||||||
use App\ServiceProviders\SettingsServiceProvider;
|
use App\ServiceProviders\SettingsServiceProvider;
|
||||||
use Openguru\OpenCartFramework\Application;
|
use Openguru\OpenCartFramework\Application;
|
||||||
use Openguru\OpenCartFramework\Cache\CacheServiceProvider;
|
use Openguru\OpenCartFramework\Cache\CacheServiceProvider;
|
||||||
|
use Openguru\OpenCartFramework\ImageTool\ImageToolServiceProvider;
|
||||||
use Openguru\OpenCartFramework\QueryBuilder\QueryBuilderServiceProvider;
|
use Openguru\OpenCartFramework\QueryBuilder\QueryBuilderServiceProvider;
|
||||||
use Openguru\OpenCartFramework\Scheduler\SchedulerServiceProvider;
|
use Openguru\OpenCartFramework\Scheduler\SchedulerServiceProvider;
|
||||||
use Openguru\OpenCartFramework\Support\Arr;
|
use Openguru\OpenCartFramework\Support\Arr;
|
||||||
@@ -29,6 +30,7 @@ class ApplicationFactory
|
|||||||
TelegramServiceProvider::class,
|
TelegramServiceProvider::class,
|
||||||
TeleCartPulseServiceProvider::class,
|
TeleCartPulseServiceProvider::class,
|
||||||
SchedulerServiceProvider::class,
|
SchedulerServiceProvider::class,
|
||||||
|
ImageToolServiceProvider::class,
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -46,6 +46,7 @@ class ImageFactory
|
|||||||
*/
|
*/
|
||||||
public function make(?string $path = null): self
|
public function make(?string $path = null): self
|
||||||
{
|
{
|
||||||
|
$this->modifications = [];
|
||||||
$this->path = $path;
|
$this->path = $path;
|
||||||
$this->fullPath = $this->imageDir . '/' . $this->path;
|
$this->fullPath = $this->imageDir . '/' . $this->path;
|
||||||
|
|
||||||
@@ -178,7 +179,7 @@ class ImageFactory
|
|||||||
$filename .= '_' . substr(hash('SHA256', json_encode($this->modifications)), 0, 12);
|
$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
|
private function applyModifications(): void
|
||||||
|
|||||||
Reference in New Issue
Block a user