Реалізація автоматичного оновлення цін товарів із зовнішніх джерел
Інтернет-магазини, які працюють з кількома постачальниками або використовують динамічне ціноутворення, стикаються з однією проблемою: ціни в каталозі застарівають швидше, ніж їх встигають оновити вручну. Рішення — побудувати автоматичний конвеєр, який тягне актуальні дані зі зовнішніх джерел та оновлює ціни у БД без участі оператора.
Джерела цін та способи їх отримання
Зовнішніми джерелами цін можуть бути:
- Прайс-листи постачальників — CSV/Excel-файли через HTTP або FTP
- API постачальників — REST або SOAP з авторизацією за токеном
-
YML-фіди — формат Яндекс.Маркету, містить
<price>та<oldprice> -
Google Merchant Feed — XML з полем
g:price - Парсинг сторінок — крайній випадок, коли немає ні API, ні фіду
Для кожного типу джерела потрібен окремий адаптер, що реалізує спільний інтерфейс:
interface PriceSourceInterface
{
/** @return array<string, float> [sku => price] */
public function fetch(): array;
}
Адаптер для CSV через HTTP
class CsvHttpPriceSource implements PriceSourceInterface
{
public function __construct(
private string $url,
private int $skuColumn,
private int $priceColumn,
private string $delimiter = ';',
) {}
public function fetch(): array
{
$stream = fopen($this->url, 'r');
$prices = [];
$header = fgetcsv($stream, 0, $this->delimiter); // пропускаємо заголовок
while ($row = fgetcsv($stream, 0, $this->delimiter)) {
$sku = trim($row[$this->skuColumn]);
$price = (float) str_replace(',', '.', $row[$this->priceColumn]);
if ($sku && $price > 0) {
$prices[$sku] = $price;
}
}
fclose($stream);
return $prices;
}
}
Архітектура планувальника
Оновлення цін — це фонова задача. Стандартний підхід у Laravel: Artisan-команда + scheduler + Queue.
Cron (кожні N хвилин)
└─> SchedulePriceUpdateCommand
└─> PriceUpdateJob (queued)
└─> PriceSourceFactory::make($source)
└─> PriceUpdater::apply($prices)
Команда-диспетчер:
class SchedulePriceUpdateCommand extends Command
{
protected $signature = 'prices:update {--source=all}';
public function handle(PriceSourceRepository $repo): void
{
$sources = $this->option('source') === 'all'
? $repo->getActive()
: [$repo->find($this->option('source'))];
foreach ($sources as $source) {
PriceUpdateJob::dispatch($source)->onQueue('prices');
}
}
}
У app/Console/Kernel.php:
$schedule->command('prices:update')->everyThirtyMinutes();
Логіка оновлення зі захистом від мусору
Неможливо сліпо писати будь-яку ціну з фіду. Потрібні перевірки:
| Перевірка | Причина |
|---|---|
price > 0 |
Постачальник може прислати 0 при помилці |
abs(new - old) / old < 0.5 |
Зміна >50% — скоріше всього збій |
| SKU існує в каталозі | Не створювати "привидів" товарів |
| Джерело не застаріле (TTL) | Фід міг не оновитися |
class PriceUpdater
{
private const MAX_CHANGE_RATIO = 0.5;
public function apply(array $prices, PriceSource $source): UpdateResult
{
$updated = $skipped = $errors = 0;
foreach ($prices as $sku => $newPrice) {
$product = Product::where('sku', $sku)->first();
if (!$product) { $skipped++; continue; }
$oldPrice = $product->price;
if ($oldPrice > 0) {
$ratio = abs($newPrice - $oldPrice) / $oldPrice;
if ($ratio > self::MAX_CHANGE_RATIO) {
Log::warning("Price anomaly: $sku $oldPrice -> $newPrice");
$errors++;
continue;
}
}
$product->update([
'price' => $newPrice,
'price_updated_at' => now(),
'price_source_id' => $source->id,
]);
$updated++;
}
return new UpdateResult($updated, $skipped, $errors);
}
}
Декілька джерел та пріоритети
Коли товар присутній у кількох фідах, потрібна стратегія розв'язання конфліктів:
- MIN — брати мінімальну ціну (агресивне ціноутворення)
- PRIMARY — перше джерело має пріоритет, решта як резервні
- LAST_UPDATED — ціна з останнього оновленого фіду
Конфігурація джерела у БД:
CREATE TABLE price_sources (
id serial PRIMARY KEY,
name varchar(100),
type varchar(30), -- csv_http | api | yml | merchant
config jsonb, -- url, credentials, column mapping
priority smallint DEFAULT 10,
strategy varchar(20) DEFAULT 'primary',
active boolean DEFAULT true,
updated_at timestamptz
);
Оновлення через API постачальника
Якщо постачальник надає REST API з пагінацією:
class ApiPriceSource implements PriceSourceInterface
{
public function fetch(): array
{
$client = new \GuzzleHttp\Client(['base_uri' => $this->baseUrl]);
$prices = [];
$page = 1;
do {
$response = $client->get('/v2/prices', [
'headers' => ['Authorization' => 'Bearer ' . $this->token],
'query' => ['page' => $page, 'per_page' => 500],
]);
$data = json_decode($response->getBody(), true);
foreach ($data['items'] as $item) {
$prices[$item['article']] = (float) $item['price_rub'];
}
$page++;
} while ($data['has_more']);
return $prices;
}
}
Тривалість реалізації
- Базовий конвеєр (одне CSV-джерело, scheduler, оновлення у БД) — 2–3 дні
- Підтримка кількох типів джерел + пріоритети — +2 дні
- Панель з історією оновлень та сигналами про аномалії — +2 дні
Моніторинг та сигнали
Після кожного циклу оновлення записуємо у таблицю price_update_logs:
source_id | total_fetched | updated | skipped | errors | duration_ms | created_at
При errors / total_fetched > 0.05 (більш ніж 5% аномалій) — надсилаємо сповіщення у Slack або на email через Laravel Notification. Це дозволяє виявити зламаний фід до того, як покупці побачать неправильні ціни.







