Настройка автоматического обновления фидов товаров по расписанию

Наша компания занимается разработкой, поддержкой и обслуживанием сайтов любой сложности. От простых одностраничных сайтов до масштабных кластерных систем построенных на микро сервисах. Опыт разработчиков подтвержден сертификатами от вендоров.

Разработка и обслуживание любых видов сайтов:

Информационные сайты или веб-приложения
Сайты визитки, 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);
        }
    }
}

Атомарное переименование (rename) важно: если во время записи агрегатор скачает фид — он получит старую полную версию, а не обрезанный файл в процессе записи.

Планировщик 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 * * * (раз в сутки ночью)

Cron на сервере

# crontab -e
* * * * * www-data php /var/www/project/artisan schedule:run >> /dev/null 2>&1

Публичный доступ к фидам

Фиды хранятся в public/feeds/ и доступны по URL. Для защиты от несанкционированного скачивания добавляют HTTP Basic Auth или signed URL:

Route::get('/feeds/{name}', function (string $name) {
    $path = public_path("feeds/{$name}");
    abort_unless(file_exists($path), 404);

    return response()->file($path, [
        'Content-Type' => 'application/xml; charset=utf-8',
        'X-Generated'  => filemtime($path),
    ]);
})->middleware('feed.auth'); // опциональная basic auth

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

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

// Команда в мониторинге или scheduled job
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 рабочих дня.