עדכון אוטומטי של פיד מוצרים לפי לוח זמנים ב-Laravel
השקתם חנות מסחר אלקטרוני ב-Laravel, שילבתם את Yandex.Market ו-Google Merchant, אבל אחרי שבוע שמתם לב: המחירים בפיד מיושנים, מוצרים שאזלו מהמלאי עדיין מופיעים במרקטפלייס, והפלטפורמה שולחת אזהרות. ייצוא ידני כל שעה אינו אופציה; קובץ CSV סטטי פעם ביום לא יכול לעמוד בקצב הדינמיקה של המלאי. עדכונים אוטומטיים לפי לוח זמנים הם הדרך היחידה לשמור על פידים עדכניים. אנו מיישמים זאת באמצעות מתזמן המובנה של Laravel וארכיטקטורת גנרטור גמישה.
ייצור פיד אוטומטי מהיר פי 100 מייצוא ידני עבור קטלוג של 10,000 מוצרים. שגיאות במהלך ייצוא ידני מתרחשות ב-3% מהמקרים; עם אוטומציה, זה 0.01% – שיפור של 99.7%. חיסכון חודשי בזמן עולה על 20 שעות – שווה ערך לכ-$200 משכורת מנהל. מניעת חסימות מרקטפלייס שומרת על תקציב פרסום ממוצע של $500 בחודש.
מדוע עדכוני פיד אוטומטיים קריטיים למכירות?
קונה רואה מוצר מיושן ועוזב למתחרה. Yandex.Market חוסם חנויות עם פידים ישנים מ-24 שעות. Google Merchant משביתה מוצרים עם מחירים שגויים. אוטומציה מבטלת סיכונים אלה: פידים נוצרים בתדירות הנדרשת, שגיאות מתועדות, וטריות מנוטרת על ידי ניטור נפרד. השוו: ייצוא ידני לוקח 30 דקות עבור 10,000 מוצרים; ייצור אוטומטי לוקח פחות מדקה. לפי תיעוד Laravel, המתזמן יכול להריץ משימות בדיוק של דקה.
פורמטי פיד: טבלת השוואה
| פלטפורמה | פורמט | סיומת | מאפיינים |
|---|---|---|---|
| Yandex.Market | YML (Yandex Market Language) | .xml | תג available נדרש, תומך ב-market_category |
| Google Merchant | RSS 2.0 + מרחב שמות g: | .xml, .tsv | דורש g:price, g:availability, g:mpn |
| Facebook/Instagram | CSV או XML | .csv, .xml | שדות: id, title, description, image_link, price |
| Avito | XML מותאם אישית | .xml | פורמט ייחודי עם AdId, Category, Price |
אותו קטלוג חייב להיות מיוצא במספר פורמטים. הארכיטקטורה חייבת לקחת זאת בחשבון מההתחלה.
ארכיטקטורת גנרטור הפיד
אנו מתכננים את המערכת על בסיס FeedGeneratorInterface ו-FeedGeneratorFactory. כל גנרטור מיישם את מתודת generate(FeedConfig $config) המחזירה תוכן פיד. החלטות מפתח:
- שאילתות Cursor – אין לטעון את כל המוצרים לזיכרון, לטפל באלפי פריטים ללא חריגה ממגבלות.
- שינוי שם אטומי – כתיבה לקובץ זמני, ולאחר מכן
rename(): אם התהליך מופרע, האגרגטור מקבל את הגרסה המלאה הישנה, לא קובץ קטוע. - הממשק מאפשר הוספת פורמט חדש ללא שינוי קוד קיים.
// 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(); $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(); $xml->endElement(); $xml->endElement(); 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 ?? 'USD'); $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(); } } כיצד להגדיר לוחות זמנים לסוגי פיד שונים?
אנו משתמשים במתזמן של Laravel עם ביטויי cron דינמיים ממסד הנתונים. זה מאפשר שינוי לוחות זמנים ללא פריסה. תדירויות אופייניות:
| סוג נתונים | תדירות | דוגמת cron |
|---|---|---|
| מחירים ומלאי | כל 15 דקות | // 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(); $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(); $xml->endElement(); $xml->endElement(); 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 ?? 'USD'); $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(); } } |
| תיאורים ומאפיינים | כל שעה | // 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 = $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); } } } |
| ייצוא מלא עם תמונות | פעם ביום בלילה | */15 * * * * |
// 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 = $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); } } } בשרת, יש צורך רק ברישום crontab אחד:
// 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) { Notification::route('slack', config('services.slack.webhook')) ->notify(new FeedGenerationFailed($config)); }); }); } ניטור טריות וגישה ציבורית
פידים מאוחסנים ב-0 * * * *. להגנה, השתמשו באימות Basic של HTTP או בכתובות URL חתומות:
* * * * * php /path/to/artisan schedule:run >> /dev/null 2>&1 פקודה נוספת בודקת אם פיד מיושן (מעל 60 דקות) ושולחת התראה לניטור. זה מונע חסימות פלטפורמה.
כיצד להימנע מחסימות מרקטפלייס?
השתמשו בשינוי שם אטומי: הפיד המוצג לפלטפורמה אף פעם לא שבור. אם הייצור מופרע עקב timeout במסד הנתונים או כשל רשת, הגרסה המלאה הקודמת נשארת על הדיסק. בנוסף, נשלחות התראות ל-Telegram כאשר זמן הייצור עולה על פי שניים מהממוצע.
רשימת בדיקה של טעויות נפוצות בהגדרת פיד:
- שאילתת N+1 בבחירת מוצרים – השתמשו ב-eager loading או בשאילתות cursor.
- תווים XML לא חוקיים בתיאורים – בריחה באמצעות
0 3 * * *. - תג
// 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) { Notification::route('slack', config('services.slack.webhook')) ->notify(new FeedGenerationFailed($config)); }); }); }חסר עבור YML – המוצר לא יופיע בתוצאות. - גודל פיד גדול מדי (>100 MB) – חלקו לחלקים או השתמשו ב-gzip.
מה כלול בעבודה
- ניתוח – סקירת הקטלוג שלכם, דרישות המרקטפלייס, תשתית קיימת.
- עיצוב – ארכיטקטורת גנרטור, תצורה, סכמת DB, לוח זמנים.
- יישום – כתיבת גנרטורים לפורמטים הנדרשים, אינטגרציה עם המתזמן, הגדרת ניטור.
- בדיקות – אימות עם פיד בדיקה, אימות בפלטפורמות, בדיקות עומס.
- פריסה ותיעוד – פריסה על השרת שלכם, הוראות ניהול מלוח הבקרה.
מעל 7+ שנות ניסיון בפיתוח Laravel, 50+ אינטגרציות מוצלחות עם מרקטפלייס. אנו מבטיחים פעילות מערכת יציבה ותמיכה מהירה לאחר הפריסה. צרו קשר לייעוץ חינם – ננתח את הקטלוג שלכם ונציע לוח זמנים אופטימלי. הזמינו יישום מערכת וקבלו אינטגרציה מוכנה עם התראות מוגדרות תוך 3-4 ימים.







