Налаштування автоматичного оновлення фідів товарів за розкладом

Наша компанія займається розробкою, підтримкою та обслуговуванням сайтів будь-якої складності. Від простих односторінкових сайтів до масштабних кластерних систем, побудованих на мікро сервісах. Досвід розробників підтверджено сертифікатами від вендорів.

Розробка та обслуговування будь-яких видів сайтів:

Інформаційні сайти або веб-програми
Сайти візитки, landing page, корпоративні сайти, онлайн каталоги, квіз, промо-сайти, блоги, ресурси новин, інформаційні портали, форуми, агрегатори
Сайти або веб-програми електронної комерції
Інтернет-магазини, B2B-портали, маркетплейси, онлайн-обмінники, кешбек-сайти, біржі, дропшиппінг-платформи, парсери товарів
Веб-програми для управління бізнес-процесами
CRM-системи, ERP-системи, корпоративні портали, системи управління виробництвом, парсери інформації
Сайти або веб-програми електронних послуг
Дошки оголошень, онлайн-школи, онлайн-кінотеатри, конструктори сайтів, портали надання електронних послуг, відеохостинги, тематичні портали

Це лише деякі з технічних типів сайтів, з якими ми працюємо, і кожен із них може мати свої специфічні особливості та функціональність, а також бути адаптованим під конкретні потреби та цілі клієнта.

Пропоновані послуги
Показано 1 з 1 послугУсі 2065 послуг
Налаштування автоматичного оновлення фідів товарів за розкладом
Проста
від 1 робочого дня до 3 робочих днів
Часті питання

Наші компетенції:

Етапи розробки

Останні роботи

  • image_website-b2b-advance_0.png
    Розробка сайту компанії B2B ADVANCE
    1262
  • image_web-applications_feedme_466_0.webp
    Розробка веб-додатків для компанії FEEDME
    1171
  • image_websites_belfingroup_462_0.webp
    Розробка веб-сайту для компанії БЕЛФІНГРУП
    874
  • image_ecommerce_furnoro_435_0.webp
    Розробка інтернет магазину для компанії FURNORO
    1094
  • image_crm_enviok_479_0.webp
    Розробка веб-додатків для компанії Enviok
    831
  • image_bitrix-bitrix-24-1c_fixper_448_0.png
    Розробка веб-сайту для компанії ФІКСПЕР
    851

Налаштування автоматичного обновлення фідів товарів по розкладу

Товарні фіди — це XML або CSV файли, які потребляють Яндекс.Маркет, Google Merchant, Facebook Catalog, партнерські агрегатори. Якщо фід обновляется вручну або раз на день статичним експортом — актуальність цін та остатків під запитанням. Автоматичний розклад вирішує це системно.

Формати фідів

Кожна платформа чекає свій формат:

  • Яндекс.Маркет — YML (Yandex Market Language), розширення XML
  • Google Merchant — RSS 2.0 з розширенням g: namespace або TSV
  • Facebook/Instagram — CSV або XML із конкретними полями
  • Авито — власний XML

Один і той самий каталог потрібно експортувати в кілька форматів. Архітектура повинна це враховувати з самого початку.

Структура генератора

// app/Services/Feed/FeedGenerator.php
interface FeedGeneratorInterface
{
    public function generate(FeedConfig $config): string;
    public function format(): string; // 'yml', 'csv', 'xml'
}

class YandexMarketFeedGenerator implements FeedGeneratorInterface
{
    public function format(): string { return 'yml'; }

    public function generate(FeedConfig $config): string
    {
        $products = Product::query()
            ->where('is_active', true)
            ->whereHas('stock', fn($q) => $q->where('quantity', '>', 0))
            ->when($config->category_ids, fn($q, $ids) => $q->whereIn('category_id', $ids))
            ->with(['category', 'images', 'attributes'])
            ->cursor(); // cursor() — не завантажуємо все в пам'ять

        $xml = new \XMLWriter();
        $xml->openMemory();
        $xml->setIndent(true);
        $xml->startDocument('1.0', 'UTF-8');
        $xml->startElement('yml_catalog');
        $xml->writeAttribute('date', now()->format('Y-m-d H:i'));

        $xml->startElement('shop');
        $this->writeShopInfo($xml, $config);
        $xml->startElement('offers');

        foreach ($products as $product) {
            $this->writeOffer($xml, $product, $config);
        }

        $xml->endElement(); // offers
        $xml->endElement(); // shop
        $xml->endElement(); // yml_catalog

        return $xml->outputMemory();
    }

    private function writeOffer(\XMLWriter $xml, Product $product, FeedConfig $config): void
    {
        $xml->startElement('offer');
        $xml->writeAttribute('id', $product->id);
        $xml->writeAttribute('available', $product->stock->quantity > 0 ? 'true' : 'false');

        $xml->writeElement('url',          route('product.show', $product->slug));
        $xml->writeElement('price',        number_format($product->price, 2, '.', ''));
        $xml->writeElement('currencyId',   $config->currency ?? 'RUB');
        $xml->writeElement('categoryId',   $product->category_id);
        $xml->writeElement('name',         $product->name);
        $xml->writeElement('description',  strip_tags($product->description));

        foreach ($product->images->take(10) as $image) {
            $xml->writeElement('picture', $image->url);
        }

        $xml->endElement(); // offer
    }
}

Модель конфігурації фідів

CREATE TABLE feed_configs (
    id           SERIAL PRIMARY KEY,
    name         VARCHAR(255) NOT NULL,
    type         VARCHAR(32) NOT NULL,   -- 'yandex', 'google', 'facebook'
    schedule     VARCHAR(64) NOT NULL,   -- cron: '*/30 * * * *'
    output_path  VARCHAR(512) NOT NULL,  -- '/public/feeds/yandex.xml'
    is_active    BOOLEAN DEFAULT true,
    last_run_at  TIMESTAMPTZ,
    last_error   TEXT,
    options      JSONB DEFAULT '{}'
);

Artisan-команда генерації

// app/Console/Commands/GenerateFeed.php
class GenerateFeed extends Command
{
    protected $signature   = 'feed:generate {feed_id?} {--all}';
    protected $description = 'Generate product feed files';

    public function handle(): int
    {
        $configs = $this->option('all')
            ? FeedConfig::where('is_active', true)->get()
            : FeedConfig::whereKey($this->argument('feed_id'))->get();

        foreach ($configs as $config) {
            $this->generateOne($config);
        }

        return self::SUCCESS;
    }

    private function generateOne(FeedConfig $config): void
    {
        $start = microtime(true);
        try {
            $generator = FeedGeneratorFactory::make($config->type);
            $content   = $generator->generate($config);

            // Записуємо в tmp, потім атомарно переіменовуємо
            $tmp = $config->output_path . '.tmp';
            file_put_contents(public_path($tmp), $content);
            rename(public_path($tmp), public_path($config->output_path));

            $config->update([
                'last_run_at' => now(),
                'last_error'  => null,
            ]);

            $this->info(sprintf(
                '[%s] %s generated in %.2fs (%s)',
                $config->name,
                basename($config->output_path),
                microtime(true) - $start,
                $this->formatBytes(strlen($content))
            ));

        } catch (\Throwable $e) {
            $config->update(['last_error' => $e->getMessage()]);
            $this->error("[{$config->name}] Failed: " . $e->getMessage());
            report($e);
        }
    }
}

Атомарне переіменування важливо: якщо під час записування агрегатор завантажить фід — отримає стару повну версію, а не обрізаний файл в процесі запису.

Планувальник Laravel

// app/Console/Kernel.php
protected function schedule(Schedule $schedule): void
{
    // Читаємо розклад з БД — гнучко, без деплою при зміні
    FeedConfig::where('is_active', true)->each(function (FeedConfig $config) use ($schedule) {
        $schedule->command("feed:generate {$config->id}")
            ->cron($config->schedule)
            ->withoutOverlapping(10)  // не запускати, якщо попередній ще працює
            ->runInBackground()
            ->onFailure(function () use ($config) {
                // Оповіщення в Slack/Telegram
                Notification::route('slack', config('services.slack.webhook'))
                    ->notify(new FeedGenerationFailed($config));
            });
    });
}

Типові розклади:

  • Ціни та остатки: */15 * * * * (щих 15 хвилин)
  • Основний каталог з описаннями: 0 * * * * (щогодини)
  • Повний експорт з зображеннями: 0 3 * * * (щоночі)

Мониторинг свіжості

Яндекс.Маркет блокує магазини при фіді старіше 24 годин. Перевіряємо свіжесть:

FeedConfig::where('is_active', true)->each(function (FeedConfig $config) {
    $maxAge    = $config->options['max_age_minutes'] ?? 60;
    $isStale   = $config->last_run_at?->diffInMinutes(now()) > $maxAge;

    if ($isStale || $config->last_error) {
        // Алерт в мониторинг
    }
});

Строк реалізації базової системи з двома форматами (YML + Google) та веб-інтерфейсом управління конфігураціями — 3–4 робочі дні.