פיתוח בוט תמחור מחדש לשווקים מקוונים
אתה מתעורר בבוקר ורואה שהמוצר שלך ב-Ozon ירד לעמוד 100 בתוצאות החיפוש — מתחרים הורידו את המחיר שלך בן לילה. הורדת מחירים ידנית מובילה לאפס רווח ומלאי שלא נמכר. אתה צריך אוטומציה: בוט שמנטר מחירי מתחרים, מיישם אסטרטגיות גמישות, ומפרסם עדכונים דרך ממשקי ה-API של השוק. כך אנחנו עושים את זה.
כאשר ניהול ידני לא מספיק: בעיות אופייניות של מוכרים
מתחרים משנים מחירים מספר פעמים ביום — אי אפשר לעמוד בקצב ידנית: או שאתה מפסיד רווח או שמוכרים בהפסד. אסטרטגיות רבות מבוססות Excel נכשלות כי זמן התגובה נמדד בדקות, לא בשעות. אין הגנה מפני מלחמות מחירים — שני מוכרים מורידים את המחיר לאפס בעוד שלישי זורק במחיר עלות. בדיקת השערות קשה — כלל כמו "הפחת ב-5% אם מתחרה הפחית" ללא בוט תמחור מחדש הופך לסיוט אנלוגי.
מערכת תמחור מחדש מוגדרת כראוי מבטיחה צמיחה יציבה בהמרות תוך שמירה על מרווחים. מקדם אחד שגוי ואתה באדום. לכן הבוט חייב: לאכוף רצפת מרווח, לנתח את מיקום ה-Buy Box, ולהשהות בכוח כאשר יש חשד למלחמת מחירים.
איך אנחנו בונים את מערכת התמחור מחדש
הטכנולוגיות שלנו: PHP 8.3 + Laravel 11 לשרת, PostgreSQL לאחסון כללים, לוגים וסטטיסטיקות. מנוע התמחור מחדש מורכב ממחלקות מופרדות בבירור: RepricingRule, RepricingEngine, PriceWarDetector, ו-OzonPricePublisher / WbPricePublisher.
סכמת נתונים של כלל תמחור מחדש
CREATE TABLE repricing_rules ( id BIGSERIAL PRIMARY KEY, name VARCHAR(255) NOT NULL, marketplace VARCHAR(50) NOT NULL, -- 'ozon', 'wildberries', 'yandex_market' scope_type VARCHAR(20) NOT NULL, -- 'global', 'category', 'product' scope_id BIGINT, strategy VARCHAR(30) NOT NULL, -- 'min_price', 'buy_box', 'rule_based' min_price_mode VARCHAR(20) DEFAULT 'margin_floor', min_price_value NUMERIC(12,2), min_margin_pct NUMERIC(5,2) DEFAULT 10, max_price NUMERIC(12,2), step_pct NUMERIC(5,2) DEFAULT 1.0, step_abs NUMERIC(10,2), cooldown_minutes INT DEFAULT 60, is_active BOOLEAN DEFAULT TRUE ); CREATE TABLE repricing_log ( id BIGSERIAL PRIMARY KEY, product_id BIGINT REFERENCES products(id), marketplace VARCHAR(50), old_price NUMERIC(12,2), new_price NUMERIC(12,2), reason TEXT, rule_id BIGINT REFERENCES repricing_rules(id), triggered_at TIMESTAMP DEFAULT NOW() ); המבנה הזה מאפשר הגדרת כללים גמישה: ברמת החנות (גלובלית), ברמת קטגוריה, או עבור מוצר ספציפי. הכללים מכילים מגבלות חובה — מחיר מינימלי (קבוע או אחוז מהעלות), מחיר מקסימלי, גודל צעד, וזמן המתנה בין פרסומים. הלוג משמש לניפוי באגים ולאנליטיקה.
קבלת מחירי מתחרים: Ozon ו-Wildberries
עבור Ozon, אנו משתמשים בנקודת הקצה CREATE TABLE repricing_rules ( id BIGSERIAL PRIMARY KEY, name VARCHAR(255) NOT NULL, marketplace VARCHAR(50) NOT NULL, -- 'ozon', 'wildberries', 'yandex_market' scope_type VARCHAR(20) NOT NULL, -- 'global', 'category', 'product' scope_id BIGINT, strategy VARCHAR(30) NOT NULL, -- 'min_price', 'buy_box', 'rule_based' min_price_mode VARCHAR(20) DEFAULT 'margin_floor', min_price_value NUMERIC(12,2), min_margin_pct NUMERIC(5,2) DEFAULT 10, max_price NUMERIC(12,2), step_pct NUMERIC(5,2) DEFAULT 1.0, step_abs NUMERIC(10,2), cooldown_minutes INT DEFAULT 60, is_active BOOLEAN DEFAULT TRUE ); CREATE TABLE repricing_log ( id BIGSERIAL PRIMARY KEY, product_id BIGINT REFERENCES products(id), marketplace VARCHAR(50), old_price NUMERIC(12,2), new_price NUMERIC(12,2), reason TEXT, rule_id BIGINT REFERENCES repricing_rules(id), triggered_at TIMESTAMP DEFAULT NOW() ); . היא מחזירה מחירי מתחרים ב-Buy Box יחד עם הדירוג שלהם. דוגמת קריאה ב-PHP:
class OzonCompetitorPriceClient { public function getCompetitorPrices(string $offerId): array { $response = Http::withHeaders([ 'Client-Id' => $this->clientId, 'Api-Key' => $this->apiKey, ])->post('https://api-seller.ozon.ru/v1/product/info/competitor-price', [ 'offer_id' => $offerId, ]); return $response->json('result', []); } } עבור Wildberries, אנו ניגשים לכרטיס המוצר ומנתחים את /v1/product/info/competitor-price (או משתמשים ב-API v2 במקרים מסוימים). אנו מקבלים מערך של class OzonCompetitorPriceClient { public function getCompetitorPrices(string $offerId): array { $response = Http::withHeaders([ 'Client-Id' => $this->clientId, 'Api-Key' => $this->apiKey, ])->post('https://api-seller.ozon.ru/v1/product/info/competitor-price', [ 'offer_id' => $offerId, ]); return $response->json('result', []); } } עם מחירים לכל מידה, כולל מחירי מתחרים.
מנוע התמחור מחדש: איך מתקבלות החלטות
class RepricingEngine { public function calculateNewPrice( Product $product, string $marketplace, RepricingRule $rule, ): ?PriceDecision { $competitorData = $this->getCompetitorData($product, $marketplace); $costPrice = $product->cost_price ?? 0; $currentPrice = $this->getCurrentMarketplacePrice($product, $marketplace); $decision = match ($rule->strategy) { 'min_price' => $this->strategyMinPrice($currentPrice, $competitorData, $rule, $costPrice), 'buy_box' => $this->strategyBuyBox($currentPrice, $competitorData, $rule, $costPrice), 'rule_based' => $this->strategyRuleBased($currentPrice, $competitorData, $rule, $costPrice), default => null, }; if (!$decision) return null; // Проверка cooldown $lastChange = RepricingLog::where('product_id', $product->id) ->where('marketplace', $marketplace) ->where('triggered_at', '>=', now()->subMinutes($rule->cooldown_minutes)) ->exists(); if ($lastChange) return null; return $decision; } private function strategyMinPrice( float $current, array $competitors, RepricingRule $rule, float $costPrice ): ?PriceDecision { $competitorMin = collect($competitors)->min('price'); if (!$competitorMin) return null; $floor = $this->calculateFloor($rule, $costPrice); if ($competitorMin < $current) { $newPrice = max($competitorMin, $floor); if ($newPrice >= $current) return null; return new PriceDecision( newPrice: $newPrice, reason: "Конкурент снизил цену до {$competitorMin}", ); } if ($competitorMin > $current && $rule->max_price && $current < $rule->max_price) { $newPrice = min($competitorMin - 1, $rule->max_price); return new PriceDecision( newPrice: $newPrice, reason: "Конкурент поднял цену до {$competitorMin}", ); } return null; } private function calculateFloor(RepricingRule $rule, float $costPrice): float { if ($rule->min_price_mode === 'fixed' && $rule->min_price_value) { return $rule->min_price_value; } if ($rule->min_margin_pct && $costPrice > 0) { return $costPrice * (1 + $rule->min_margin_pct / 100); } return 0; } } אסטרטגיית מפתח: מחיר מינימלי. היא משווה את מחיר המתחרה הנמוך ביותר עם המחיר הנוכחי: אם המתחרה זול יותר, אנו מורידים למחיר הזה (אך לא מתחת ל-card.wb.ru המחושב); אם המתחרה יקר יותר וקיים תקרה, אנו מעלים. זמן ההמתנה מונע תנודות תכופות.
הגנה מפני מלחמת מחירים
מלחמת מחירים מתרחשת כאשר שני מוכרים מורידים מחירים זה מול זה ברציפות. הגלאי שלנו:
class PriceWarDetector { public function isWarring(int $productId, string $marketplace): bool { $changes = RepricingLog::where('product_id', $productId) ->where('marketplace', $marketplace) ->where('triggered_at', '>=', now()->subDay()) ->count(); if ($changes >= 5) { Cache::put("repricing.paused.{$productId}.{$marketplace}", true, now()->addHours(6)); Notification::send($this->admins, new PriceWarAlert($productId, $marketplace)); return true; } return false; } } הסף (5 שינויים ב-24 שעות) ניתן להגדרה לפי קטגוריית מוצר. בזמן שהבוט קפוא, אתה יכול לבדוק ידנית או להעלות את המחיר.
ניטור ודוחות: SQL לאנליטיקה
SELECT p.name, rl.marketplace, COUNT(*) AS changes_count, MIN(rl.new_price) AS min_price_today, MAX(rl.new_price) AS max_price_today, ROUND(AVG(rl.new_price), 2) AS avg_price_today FROM repricing_log rl JOIN products p ON p.id = rl.product_id WHERE rl.triggered_at >= NOW() - INTERVAL '24 hours' GROUP BY p.name, rl.marketplace ORDER BY changes_count DESC; השאילתה הזו נותנת תמונת מצב יומית: אילו מוצרים השתנו לעתים קרובות וטווח המחירים שלהם. ניתן להוסיף פילוח לפי אסטרטגיה או קטגוריה.
תזמון משימות
$schedule->job(new RunRepricingJob)->everyThirtyMinutes(); $schedule->job(new ResetRepricingCountersJob)->dailyAt('03:00'); התמחור מחדש רץ כל 30 דקות עבור כללים פעילים. המונים מתאפסים בלילה. ניתן להתאים את התזמון לעומס השיא של השוק הספציפי.
השוואת אסטרטגיות תמחור מחדש
| אסטרטגיה | תיאור | מתי להשתמש |
|---|---|---|
| מחיר מינימלי | שמירה על המחיר ברמת המתחרה הנמוך ביותר | תחרות גבוהה, מוצרים עם בידול נמוך |
| Buy Box | מטרה לזכות ב-Buy Box (מחיר + דירוג) | מוצרים שבהם ה-Buy Box מניע 80% מהמכירות |
| מבוסס כללים | תנאים גמישים (למשל, הפחת ב-5% כאשר מתחרה מפחית) | תרחישים מורכבים עם עונתיות או מבצעים |
| רצפת מרווח | הגנה על מרווח מינימלי (מחיר >= עלות + % מרווח) | מוצרים עם מרווח נמוך |
מה כלול בפיתוח בוט התמחור מחדש
אנו מיישמים את הפרויקט מקצה לקצה תוך 6–8 ימי עסקים. ההיקף כולל:
- עיצוב סכמת נתונים ומנוע לאסטרטגיות ליבה (יומיים)
- אינטגרציית Ozon API (מחירי מתחרים + פרסום) (1–2 ימים)
- אינטגרציית Wildberries API (+יום אחד)
- מנגנון PriceWarDetector עם זמן המתנה והתראות (יום אחד)
- ממשק ניהול כללים ויומן שינויים (1–2 ימים)
- תיעוד API מלא (סכמות בקשה/תגובה), מדריך הגדרת כללים, הדרכת צוות לניהול בסיסי, ותמיכה בהשקה.
אנו מבטיחים אינטגרציה נכונה עם ממשקי API רשמיים — הניסיון שלנו כולל מעל 50 פרויקטי אוטומציה לשווקים. המחיר קבוע בחוזה ואינו משתנה במהלך הפיתוח.
רשימת בדיקה: טעויות נפוצות ביישום תמחור מחדש
- אין זמן המתנה — מחירים משתנים כל 5 דקות; Ozon חוסם את ה-API.
- חסר min_price — אתה מוכר בהפסד.
- שכחת היפוך — אם המחיר יורד מתחת לעלות, הבוט חייב להפסיק מיד את ההתאמות.
- אין ניטור שגיאות — כשלים שקטים בפרסום מחיר משאירים את המוצר במחיר הישן.
צור קשר כדי לדון בתרחיש שלך — נשלח דוגמת מפרט טכני ונענה על שאלותיך תוך יום.







