תארו לעצמכם: הוספתם מוצר לרשימת המשאלות, אבל המחיר גבוה מדי. שבוע לאחר מכן, יש הנחה של 20%, ואתם מגלים על כך במקרה. זו מכירה אבודה. אנחנו פותרים את הבעיה הזו: התראות אוטומטיות מחזירות קונים מהססנים לתוך המשפך. הסטACK שלנו: Laravel 11 (PHP 8.3) לשרת, React 18 לצד הלקוח, ו-PostgreSQL לאחסון מנויים. בניית הלוגיקה המרכזית אורכת 1-2 ימי עבודה, בתנאי שרשימת המשאלות כבר קיימת. לפי הסטטיסטיקות שלנואנליטיקת פרויקטים פנימית למסחר אלקטרוני 2022-2024, התראות על ירידת מחיר מגדילות את ההמרה מרכישות נדחות ב-15-25%, ואחד מכל חמישה מנויים מבצע רכישה, ומייצר הכנסה ממוצעת של $20–50. עם ניסיון של למעלה מ-5 שנים בפיתוח מסחר אלקטרוני ו-50+ פרויקטים מוצלחים, אנחנו מבטיחים אספקת התראות אמינה.
בעיות שהתראות על ירידת מחיר פותרות
בעיה 1: משתמשים עוזבים בלי לקנות. הנתונים שלנו מראים ש-60-70% מהמבקרים עוזבים את דף המוצר ללא ביצוע רכישה. התראה על ירידת מחיר היא טריגר עדין להחזיר אותם. קמפיין דוא"ל אוטומטי זה מחזיר לקוחות ביעילות.
בעיה 2: עומס גבוה במהלך שליחות המוניות. ללא תורים ו-chunkById, 10,000 מנויים למוצר אחד יפילו את השרת. אנחנו משתמשים ב-Laravel Horizon עם Redis — מוכח על 50+ פרויקטים.
בעיה 3: התראות חוזרות ללא הגבלה. אנחנו מיישמים אסטרטגיה: להודיע רק כאשר המחיר יורד בעוד 10% יחסית למחיר בהתראה האחרונה.
פרטי יישום
סכמת נתונים
המנויים מאוחסנים בטבלה נפרדת עם אינדקס ייחודי על (user_id, product_id, variant_id). זה מונע כפילויות. השדה price_at_subscription מתעד את המחיר בזמן ההרשמה — ההתראה מופעלת כאשר המחיר יורד יחסית לזה, לא למחיר האחרון.
CREATE TABLE price_drop_subscriptions (
id BIGSERIAL PRIMARY KEY,
product_id BIGINT NOT NULL REFERENCES products(id) ON DELETE CASCADE,
variant_id BIGINT REFERENCES product_variants(id) ON DELETE CASCADE,
user_id BIGINT REFERENCES users(id) ON DELETE CASCADE,
email VARCHAR(255) NOT NULL,
price_at_subscription NUMERIC(12,2) NOT NULL,
target_price NUMERIC(12,2),
notified_at TIMESTAMP,
notified_price NUMERIC(12,2),
created_at TIMESTAMP DEFAULT NOW()
);
CREATE UNIQUE INDEX idx_pds_user_product ON price_drop_subscriptions(user_id, product_id, COALESCE(variant_id, 0));
תהליך המנוי שלב אחר שלב
- בדף המוצר, המשתמש רואה כפתור "מעקב אחר מחיר". אם לא מחובר, הוא מופנה להתחברות ואז חזרה לדף המוצר.
- לאחר התחברות, לחיצה על הכפתור שולחת בקשת POST ל-API, שיוצרת רשומה בטבלת
CREATE TABLE price_drop_subscriptions ( id BIGSERIAL PRIMARY KEY, product_id BIGINT NOT NULL REFERENCES products(id) ON DELETE CASCADE, variant_id BIGINT REFERENCES product_variants(id) ON DELETE CASCADE, user_id BIGINT REFERENCES users(id) ON DELETE CASCADE, email VARCHAR(255) NOT NULL, price_at_subscription NUMERIC(12,2) NOT NULL, target_price NUMERIC(12,2), notified_at TIMESTAMP, notified_price NUMERIC(12,2), created_at TIMESTAMP DEFAULT NOW() ); CREATE UNIQUE INDEX idx_pds_user_product ON price_drop_subscriptions(user_id, product_id, COALESCE(variant_id, 0));עם מחיר המוצר הנוכחי וכתובת הדוא"ל של המשתמש. - כאשר מחיר המוצר משתנה (דרך לוח הניהול או סנכרון), Observer של Laravel מופעל. אם המחיר החדש נמוך יותר, הוא שולח Job מסוג
price_drop_subscriptionsלתורCheckPriceDropSubscriptions. - ה-Job מעבד מנויים בקבוצות של 100 רשומות. עבור כל אחת, הוא בודק את אחוז הירידה (סף של 5%). אם התנאים מתקיימים, נשלחת התראה במייל.
- לאחר השליחה, המנוי מסומן כמשומש (השדות
notificationsו-notified_atמוגדרים) כדי למנוע התראות כפולות באותו מחיר.
גישת עיבוד אסינכרונית זו מבטיחה שהשרת לא יהיה עמוס במהלך שליחות המוניות, והמשתמשים מקבלים התראות בזמן.
כפתור המנוי בדף המוצר
הכפתור "מעקב אחר מחיר" מופיע ליד "הוסף לסל". למשתמשים לא מחוברים, הוא מפנה להתחברות עם כתובת חזרה. אנחנו משתמשים ב-notified_price ו-useState (Axios) — ללא ספריות צד שלישי.
const PriceDropButton = ({ product, variant }: PriceDropProps) => {
const { user } = useAuth();
const [subscribed, setSubscribed] = useState(product.is_price_drop_subscribed ?? false);
const toggle = async () => {
if (!user) {
router.push(`/login?redirect=${encodeURIComponent(window.location.pathname)}`);
return;
}
if (subscribed) {
await api.delete(`/price-drop-subscriptions/${product.id}`);
setSubscribed(false);
toast.success('Подписка отменена');
} else {
await api.post('/price-drop-subscriptions', {
product_id: product.id,
variant_id: variant?.id ?? null,
});
setSubscribed(true);
toast.success('Уведомим при снижении цены');
}
};
return (
<button
onClick={toggle}
className={cn('flex items-center gap-1.5 text-sm', {
'text-blue-600': subscribed,
'text-gray-500 hover:text-gray-700': !subscribed,
})}
>
<BellIcon className={cn('w-4 h-4', { 'fill-blue-600': subscribed })} />
{subscribed ? 'Слежу за ценой' : 'Следить за ценой'}
</button>
);
}; API ו-Job לשליחה (קוד משולב)
כאשר המחיר משתנה, Observer שולח Job. ה-Job משתמש ב-api כדי להתמודד עם כמויות גדולות ללא עומס זיכרון. סף הירידה ניתן להגדרה, וה-const PriceDropButton = ({ product, variant }: PriceDropProps) => { const { user } = useAuth(); const [subscribed, setSubscribed] = useState(product.is_price_drop_subscribed ?? false); const toggle = async () => { if (!user) { router.push(`/login?redirect=${encodeURIComponent(window.location.pathname)}`); return; } if (subscribed) { await api.delete(`/price-drop-subscriptions/${product.id}`); setSubscribed(false); toast.success('Подписка отменена'); } else { await api.post('/price-drop-subscriptions', { product_id: product.id, variant_id: variant?.id ?? null, }); setSubscribed(true); toast.success('Уведомим при снижении цены'); } }; return ( <button onClick={toggle} className={cn('flex items-center gap-1.5 text-sm', { 'text-blue-600': subscribed, 'text-gray-500 hover:text-gray-700': !subscribed, })} > <BellIcon className={cn('w-4 h-4', { 'fill-blue-600': subscribed })} /> {subscribed ? 'Слежу за ценой' : 'Следить за ценой'} </button> ); }; של המשתמש נחשב אם הוגדר.
class CheckPriceDropSubscriptions implements ShouldQueue {
public function __construct(
private Product $product,
private float $newPrice,
) {}
public function handle(): void
{
$threshold = 0.05;
PriceDropSubscription::where('product_id', $this->product->id)
->whereNull('notified_at')
->chunkById(100, function ($subscriptions) use ($threshold) {
foreach ($subscriptions as $sub) {
$dropPercent = ($sub->price_at_subscription - $this->newPrice) / $sub->price_at_subscription;
if ($dropPercent < $threshold) continue;
if ($sub->target_price && $this->newPrice > $sub->target_price) continue;
Mail::to($sub->email)->queue(new PriceDropNotification($sub, $this->product, $this->newPrice));
$sub->update([
'notified_at' => now(),
'notified_price' => $this->newPrice,
]);
}
});
}
} הגדרת התראות חוזרות
לאחר ההתראה הראשונה, המנוי מסומן כמשומש. כדי לשלוח התראות חוזרות על ירידות נוספות, הוסיפו תנאי: ירידה של עוד 10% מה-chunkById. זה מיושם בקלות על ידי הרחבת target_price — בדקו את class CheckPriceDropSubscriptions implements ShouldQueue { public function __construct( private Product $product, private float $newPrice, ) {} public function handle(): void { $threshold = 0.05; PriceDropSubscription::where('product_id', $this->product->id) ->whereNull('notified_at') ->chunkById(100, function ($subscriptions) use ($threshold) { foreach ($subscriptions as $sub) { $dropPercent = ($sub->price_at_subscription - $this->newPrice) / $sub->price_at_subscription; if ($dropPercent < $threshold) continue; if ($sub->target_price && $this->newPrice > $sub->target_price) continue; Mail::to($sub->email)->queue(new PriceDropNotification($sub, $this->product, $this->newPrice)); $sub->update([ 'notified_at' => now(), 'notified_price' => $this->newPrice, ]); } }); } } והשוו. זה מגדיל את ערך ההתראה: המשתמש לא מקבל ספאם אלא נשאר מעודכן לגבי הנחות משתלמות.
מה כלול בהגדרת ההתראות?
- עיצוב סכמת מסד נתונים ומיגרציות
- REST API להרשמה/ביטול הרשמה
- רכיב כפתור בצד הלקוח ורשימת מעקב
- Job לבדיקת מחיר ושליחת מייל
- הגדרת תור (Redis + Horizon)
- תבנית מייל עם UTM וקישור לביטול הרשמה
- אינטגרציה עם רשימת משאלות קיימת (אם יש)
- תיעוד API והוראות פריסה
לוחות זמנים משוערים
| שלב | משך |
|---|---|
| ניתוח ועיצוב | 0.5–1 יום |
| פיתוח סכמה ו-API | 0.5–1 יום |
| צד לקוח ואינטגרציה | 0.5–1 יום |
| בדיקות ופריסה | 0.5–1 יום |
| סה"כ | 2–4 ימים |
העלות מחושבת באופן אישי — החל מ-$500 להגדרה בסיסית. צרו קשר כדי להעריך את הפרויקט שלכם.
השוואת אסטרטגיות התראות
בחירת אסטרטגיית ההתראות תלויה ביעדים העסקיים. התראה חד-פעמית היא הפשוטה ביותר: הלקוח מקבל מייל בירידת המחיר הראשונה מתחת לסף. קל ליישום, אבל המשתמש עלול לפספס הנחה עמוקה יותר. התראה חוזרת מודיעה על כל ירידה חדשה של 10% ממחיר ההתראה הקודמת — לוגיקה מורכבת יותר אבל שומרת על עניין ללא ספאם. אם הלקוח מציין מחיר רצוי, ההתראה מופעלת רק כאשר המחיר הזה מושג: רלוונטיות מקסימלית, אבל ההמרה עשויה להיות נמוכה יותר בגלל טריגרים נדירים.
רשימת מעקב בחשבון המשתמש
הסעיף notified_price מציג את כל המנויים של המשתמש: מוצר, מחיר בזמן ההרשמה, מחיר נוכחי, אחוז שינוי. הרכיב משתמש בטבלה עם כפתור ביטול הרשמה.
const PriceTrackingList = ({ subscriptions }: { subscriptions: PriceDropSubscription[] }) => (
<table className="w-full text-sm">
<thead>
<tr className="border-b">
<th className="text-left py-2">Товар</th>
<th>Цена при подписке</th>
<th>Текущая цена</th>
<th>Изменение</th>
<th></th>
</tr>
</thead>
<tbody>
{subscriptions.map(sub => {
const change = ((sub.current_price - sub.price_at_subscription) / sub.price_at_subscription) * 100;
return (
<tr key={sub.id} className="border-b">
<td className="py-3">
<a href={`/products/${sub.product.slug}`} className="font-medium hover:underline">
{sub.product.name}
</a>
</td>
<td className="text-center">{formatPrice(sub.price_at_subscription)}</td>
<td className="text-center font-medium">{formatPrice(sub.current_price)}</td>
<td className={cn('text-center text-sm', change < 0 ? 'text-green-600' : 'text-gray-400')}>
{change < 0 ? `−${Math.abs(change).toFixed(1)}%` : `+${change.toFixed(1)}%`}
</td>
<td>
<button onClick={() => unsubscribe(sub.id)} className="text-gray-400 hover:text-red-500">
Отписаться
</button>
</td>
</tr>
);
})}
</tbody>
</table>
); כיצד ההתראה משפיעה על ההמרה?
התראות על ירידת מחיר מגדילות את ההמרה מרכישות נדחות ב-15-25% (בהתבסס על נתוני הפרויקטים שלנו). המשתמש מקבל הצעה מותאמת אישית — טוב יותר משליחות המוניות. אחד מכל חמישה מנויים מבצע רכישה, ומייצר בממוצע $20–50 בהכנסות לחנות. אסטרטגיית החזרת לקוחות זו גם מסייעת בשחזור רשימות משאלות על ידי מיקוד למשתמשים שכבר גילו עניין.
מוכנים ליישם? צרו קשר, ואנחנו נספק פתרון התראות סוהר. נדון בפרטי הפרויקט שלכם ונציע את הפתרון האופטימלי.







