Загрузка
Все статьи

OpenCart: массовое обновление цен через API без падения сервера

Поставили задачу обновить 15 000 цен в OpenCart 3 из внешней ERP-системы. Простой цикл по товарам убил сервер на 3-й минуте. В этой статье рассказываю, почему модель OpenCart требует очистки кэша после каждого сохранения, как использовать batch-запросы к базе, и почему индексы — ваш лучший друг при массовом обновлении.

Привет! Меня зовут Артём, я бэкенд-разработчик. Недавно подружился с 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 пересоздаст кэш из актуальных данных БД.

Поделиться статьёй

Комментарии

0
0 / 2000

Пока нет комментариев

Будьте первым, кто оставит комментарий к этой статье

Читайте также

Сложности с разделами каталога в Битрикс24 через REST API: игра с деревом

Обновляете товар через API, передаете SECTION_ID, но он не появляется в нужной папке. В этой статье я объясняю разницу между простым SECTION_ID и сложным деревом инфоблоков, как правильно выстроить иерархию и почему метод crm.product.update не умеет создавать папки.

5 июля 2026 1 мин

CMake: модульная архитектура для большого C++ проекта — без ad-hoc include путей

Как мы реорганизовали CMake-структуру проекта на 15+ модулей, избавились от хаотичных include_directories и перешли на современный CMake с target-based зависимостями.

20 июня 2026 10 мин

Kotlin + C++: реальный кейс — видеопроцессинг на сервере через JNI

Как мы построили сервер видеотранскодинга на Kotlin REST API + C++ FFmpeg через JNI. Прокачка JNI_OnLoad, передача фреймов через DirectByteBuffer, обработка ошибок и бенчмарки.

19 июня 2026 13 мин