Привет! Меня зовут Артём, я бэкенд-разработчик. Недавно подружился с OpenCart 3 на реальном коммерческом проекте — и если честно, архитектура этого движка заставила меня попотеть. Сегодня поделюсь опытом массового обновления цен, когда мы интегрировали OpenCart с 1С:Предприятие.
Задача звучала просто: раз в сутки 1С выгружает прайс-лист, наш скрипт забирает его и обновляет цены в OpenCart. Объём — около 15 000 товаров. Первый блин вышел комом — сервер падал через 3 минуты после старта скрипта.
Почему стандартный подход убивает сервер
Самая очевидная реализация — использовать встроенную модель OpenCart:
$this->load->model('catalog/product');
foreach ($products as $product) {
$this->model_catalog_product->editPrice($product['id'], $product['price']);
}
Проблема в том, что метод editPrice внутри модели делает следующее: выполняет UPDATE-запрос, затем вызывает $this->cache->delete('product'), который сбрасывает весь кэш продуктов. При 15 000 товаров вы получаете 15 000 UPDATE-запросов и 15 000 операций очистки кэша. На стандартном shared-хостинге с 512MB RAM это неизбежно приводит к исчерпанию памяти.
Кроме того, OpenCart для каждого товара генерирует SEO URL, обновляет индексы и записывает лог изменений. Всё это — лишние операции, которые при массовом обновлении не нужны.
Решение: прямые batch-запросы к БД
Я отказался от модели OpenCart и пошёл напрямую в базу данных. Ключевая идея — собрать все обновления в один batch-запрос:
// Шаг 1: Подготовка данных для batch-обновления
$updateData = [];
foreach ($products as $product) {
$productId = (int) $product['id'];
$price = (float) $product['price'];
$updateData[] = "WHEN {$productId} THEN {$price}";
}
// Шаг 2: Один запрос вместо 15000
$caseExpression = implode(' ', $updateData);
$sql = "UPDATE " . DB_PREFIX . "product
SET price = CASE product_id {$caseExpression} END
WHERE product_id IN (" . implode(',', array_column($products, 'id')) . ")";
$this->db->query($sql);
Этот подход выполняет один UPDATE-запрос вместо 15 000. Время выполнения упало с «сервер умер» до 2-3 секунд. Но тут обнаружилась вторая проблема: special_price и скидки.
Учитываем акционные цены
В OpenCart финальная цена товара зависит не только от price, но и от special (акционная цена), а также от discount-групп и налогов. Если обновить только price, а товар участвовал в акции, финальная цена для покупателя может не измениться — потому что OpenCart берёт минимальную из price и special.
// Проверяем, есть ли акционная цена
$sql = "SELECT product_id, price FROM " . DB_PREFIX . "product_special
WHERE product_id IN (" . $ids . ")
AND (date_start = '0000-00-00' OR date_start < NOW())
AND (date_end = '0000-00-00' OR date_end > NOW())";
$specials = $this->db->query($sql)->rows;
$specialMap = array_column($specials, 'price', 'product_id');
foreach ($products as &$product) {
if (isset($specialMap[$product['id']])) {
$specialPrice = (float) $specialMap[$product['id']];
// Обновляем акционную цену пропорционально
$ratio = $product['price'] / $product['old_price'];
$product['special_price'] = round($specialPrice * $ratio, 2);
}
}
Этот подход сохраняет пропорцию скидок при изменении базовой цены. Клиент магазина требовал именно такого поведения — если товар стоил 1000 рублей с акцией 800, а новая цена 1200, то акция должна стать 960 (80% от новой цены).
Кэш: чистить один раз в конце
После batch-обновления нужно очистить кэш — но только один раз, в конце:
// Очистка кэша после ВСЕХ обновлений
$this->cache->delete('product');
$this->cache->delete('product_special');
$this->cache->delete('seo_url'); // В OpenCart 3 SEO URL тоже кэшируются
В OpenCart 3 система кэширования по умолчанию использует файловый кэш. При 15 000 товаров каталог уже содержит сотни кэш-файлов. Очистка кэша через cache->delete('product') удаляет все файлы с префиксом product. Если у вас установлен Redis или Memcached, операция мгновенная. На файловом кэше — может занять несколько секунд, но это всё равно в сотни раз быстрее, чем 15 000 отдельных очисток.
Индексы: проверьте их наличие
Перед запуском массового обновления убедитесь, что в таблице oc_product есть индекс на product_id. Звучит банально, но я видел установленные OpenCart, где после импорта товаров кто-то удалил первичный ключ «для ускорения вставки». Без индекса каждый batch-UPDATE будет делать full table scan, что при 15 000 строк может занять минуты.
-- Проверьте индексы
SHOW INDEX FROM oc_product;
-- Если индекса нет, создайте
ALTER TABLE oc_product ADD PRIMARY KEY (product_id);
Ещё один важный индекс — oc_product_special(product_id, date_start, date_end). Без него запрос проверки акционных цен будет сканировать всю таблицу спеццен, а она при активных акциях может быть больше основной таблицы товаров.
Транзакции и откаты
Никогда не делайте массовое обновление без транзакции. Если скрипт упадёт на середине, вы получите частично обновлённые цены — и это хуже, чем ничего. Оберните всё в транзакцию:
$this->db->query("START TRANSACTION");
try {
// Batch update products
$this->db->query($updateSql);
// Batch update specials
$this->db->query($specialSql);
// Clear cache
$this->cache->delete('product');
$this->db->query("COMMIT");
} catch (\Exception $e) {
$this->db->query("ROLLBACK");
$this->log->write('Price update failed: ' . $e->getMessage());
throw $e;
}
Обратите внимание: очистку кэша я делаю внутри транзакции. Если COMMIT не пройдёт, кэш будет очищен зря, но это безопаснее, чем оставить в кэше устаревшие данные. При следующем запросе к каталогу OpenCart пересоздаст кэш из актуальных данных БД.
Пока нет комментариев
Будьте первым, кто оставит комментарий к этой статье