תעודות מתנה הן כלי נאמנות פופולרי, אבל היישום שלהן במסחר אלקטרוני מלא במלכודות. מערכת טיפוסית שנכתבה עצמאית סובלת מתנאי מרוץ: כאשר שני משתמשים מנסים להפעיל את אותו קוד בו-זמנית, היתרה עלולה לרדת לשלילי. או שהיתרה הנותרת אובדת לאחר חיוב חלקי, וקלים לפענוח קודים בכוח גס. אנחנו פותרים בעיות אלה ברמת הארכיטקטורה באמצעות נעילת שורות ב-PostgreSQL ויצירה קריפטוגרפית חזקה. ב-10+ שנים במסחר אלקטרוני, יישמנו מעל 50 מערכות תעודות—מערכים קבועים פשוטים ועד למערכות מורכבות לשימוש חוזר עם החזרים וקמפיינים מבצעיים. במאמר זה, נפרק את הפתרונות הטכניים המרכזיים שמבטיחים שלמות יתרה ונוחות משתמש.
סוגי תעודות נתמכים
| סוג | ערך נקוב | מקור | שימוש |
|---|---|---|---|
| קבוע | 500, 1000, 2000 RUB | נמכר או מבצעי | חד-פעמי או רב-פעמי |
| מותאם אישית | כל סכום | רכישה או הנפקה ידנית | רב-פעמי עם יתרה נותרת |
| מבצעי | נקבע על ידי החנות | אוטומטי (יום הולדת, נאמנות) | תפוגה מתכווננת |
למה חיוב אטומי הוא יסודי
ללא נעילת שורות, שתי בקשות עלולות לקרוא בו-זמנית יתרה של 1000 RUB ולחייב 900 RUB כל אחת, ולהשאיר -800 RUB. הקוד שלנו מונע זאת באמצעות lockForUpdate() בתוך טרנזקציה:
public function apply(string $code, Order $order, float $maxAmount): CertificateApplication
{
return DB::transaction(function () use ($code, $order, $maxAmount) {
$cert = GiftCertificate::lockForUpdate()
->where('code', $code)
->where('is_active', true)
->where(fn($q) => $q->whereNull('expires_at')->orWhere('expires_at', '>', now()))
->firstOrFail();
if ($cert->balance <= 0) {
throw new CertificateExhaustedException($code);
}
$amountToUse = min($cert->balance, $maxAmount);
$balanceBefore = $cert->balance;
$cert->decrement('balance', $amountToUse);
if ($cert->balance == 0) {
$cert->update(['is_active' => false]);
}
GiftCertificateUsage::create([
'certificate_id' => $cert->id,
'order_id' => $order->id,
'amount_used' => $amountToUse,
'balance_before' => $balanceBefore,
'balance_after' => $cert->balance,
]);
return new CertificateApplication($cert, $amountToUse);
});
}כל קריאה למתודת public function apply(string $code, Order $order, float $maxAmount): CertificateApplication { return DB::transaction(function () use ($code, $order, $maxAmount) { $cert = GiftCertificate::lockForUpdate() ->where('code', $code) ->where('is_active', true) ->where(fn($q) => $q->whereNull('expires_at')->orWhere('expires_at', '>', now())) ->firstOrFail(); if ($cert->balance <= 0) { throw new CertificateExhaustedException($code); } $amountToUse = min($cert->balance, $maxAmount); $balanceBefore = $cert->balance; $cert->decrement('balance', $amountToUse); if ($cert->balance == 0) { $cert->update(['is_active' => false]); } GiftCertificateUsage::create([ 'certificate_id' => $cert->id, 'order_id' => $order->id, 'amount_used' => $amountToUse, 'balance_before' => $balanceBefore, 'balance_after' => $cert->balance, ]); return new CertificateApplication($cert, $amountToUse); }); } היא פעולה אטומית. אם שתי ניסיונות מגיעים בו-זמנית, השני ממתין עד שהראשון מסתיים. זו הדרך היחידה להבטיח שלמות יתרה בחנות בעומס גבוה. לפי תיעוד PostgreSQL, apply נועל את השורות שנבחרו משינויים על ידי טרנזקציות אחרות עד שהנוכחית מסתיימת (ראה FOR UPDATE). בפועל, עם 3000+ בקשות תעודות ביום, לא תיעדנו אף מקרה של חוסר סנכרון יתרה.
למה לתעד כל חיוב
טבלת SELECT ... FOR UPDATE היא יומן בלתי ניתן לשינוי. היתרה הנוכחית מנורמלת למהירות אבל תמיד אפשר לשחזר אותה מהיומן. זה מגן מפני שגיאות מטמון ומאפשר ביקורת היסטוריה. לדוגמה, כאשר מזמין מתבטל, אנחנו משחזרים את היתרה על סמך הרשומה האחרונה. ב-99% מהמקרים, השחזור אורך פחות מ-10 אלפיות השנייה, אפילו עבור תעודות עם מאות חיובים חלקיים.
יצירת קודים ללא התנגשויות
אנחנו משתמשים באלפבית ללא תווים דומים במראה (0/O, 1/I/l)—הקוד קל להקלדה ידנית. 32 תווים, 4 מקטעים של 4 תווים נותנים 32^16 ≈ 10^24 צירופים. זה מיליארדי פעמים בטוח יותר ממזהים רציפים: פענוח בכוח גס בלתי אפשרי, התנגשויות מבוטלות.
class GiftCertificateCodeGenerator {
private const ALPHABET = 'ABCDEFGHJKLMNPQRSTUVWXYZ23456789';
private const SEGMENT_LENGTH = 4;
private const SEGMENTS = 4;
public function generate(): string {
do {
$code = $this->makeCode();
} while (GiftCertificate::where('code', $code)->exists());
return $code;
}
private function makeCode(): string {
$segments = [];
for ($i = 0; $i < self::SEGMENTS; $i++) {
$segment = '';
for ($j = 0; $j < self::SEGMENT_LENGTH; $j++) {
$segment .= self::ALPHABET[random_int(0, strlen(self::ALPHABET) - 1)];
}
$segments[] = $segment;
}
return implode('-', $segments); // ABCD-EF3H-K7MN-PQRT
}
}המחולל בודק ייחודיות במסד הנתונים, אבל ההסתברות להתנגשות זניחה. קוד נראה כמו gift_certificate_usages. אנחנו בדרך כלל מייצרים עד 10,000 קודים בקבוצה אחת—בדיקת הייחודיות מסתיימת בשברירי שנייה.
מידע נוסף על תהליך היצירה
אנחנו משתמשים בספריית `random_int` לאקראיות קריפטוגרפית חזקה. מקטעים מופרדים במקפים לנוחות ההקלדה. האורך של 16 תווים (4x4) נבחר כאיזון אופטימלי בין אבטחה לקריאות. למכשירים ניידים, זמין קוד QR.החזר על ביטול הזמנה
כאשר הזמנה מוחזרת, היתרה משוחזרת בתוך הערך הנקוב הראשוני. התעודה הופכת לפעילה שוב אם היתרה הנותרת שלה הייתה אפס. אם התעודה פגה—מדיניות ההחזר היא לפי שיקול דעת החנות (הארכה או החזר במזומן). יישמנו זאת באמצעות אותו מנגנון נעילה כמו לחיוב—עקביות הנתונים מובטחת.
רכישת תעודה כמוצר
תעודת מתנה היא פריט שורה מיוחד בהזמנה. כאשר ההזמנה משולמת, מאזין class GiftCertificateCodeGenerator { private const ALPHABET = 'ABCDEFGHJKLMNPQRSTUVWXYZ23456789'; private const SEGMENT_LENGTH = 4; private const SEGMENTS = 4; public function generate(): string { do { $code = $this->makeCode(); } while (GiftCertificate::where('code', $code)->exists()); return $code; } private function makeCode(): string { $segments = []; for ($i = 0; $i < self::SEGMENTS; $i++) { $segment = ''; for ($j = 0; $j < self::SEGMENT_LENGTH; $j++) { $segment .= self::ALPHABET[random_int(0, strlen(self::ALPHABET) - 1)]; } $segments[] = $segment; } return implode('-', $segments); // ABCD-EF3H-K7MN-PQRT } } יוצר את התעודה ושולח אותה לנמען. האימייל מכיל תבנית HTML יפה, קוד QR ליישום מהיר, ו-PDF להדפסה. ההודעה האישית של השולח מתווספת אוטומטית. 95% מהקונים משאירים משוב חיובי על שיטת מתנה זו.
השוואה: מערכת עצמאית מול המערכת שלנו
| קריטריון | מערכת עצמאית | היישום שלנו |
|---|---|---|
| הגנה מתנאי מרוץ | אין | נעילת שורות ב-PostgreSQL |
| יצירת קודים | מזהים רציפים | קריפטוגרפית חזקה, 10^24 צירופים |
| חיוב חלקי | לא | אטומי עם תיעוד |
| החזר | ידני | אוטומטי בתוך טרנזקציה |
| מדרגיות | מוגבלת | עד 10,000 תעודות/יום |
מה כלול
- ניתוח תהליכים עסקיים של החנות והגדרת חוקים (קטגוריות, מגבלות, תפוגה)
- עיצוב סכמת מסד נתונים ולוגיקה (יצירה, יישום, החזרים)
- יישום צד לקוח: ווידג'ט בחירת תעודה, אזור אישי עם יתרה
- אינטגרציה עם שער תשלום ושירות דוא"ל
- כיסוי בדיקות יחידה (כולל תרחישי תנאי מרוץ)
- תיעוד ופרטי גישה
קבלו ייעוץ מהנדס: נעריך לוחות זמנים ועלות לפרויקט שלכם.
שלבי עבודה
- ניתוח—הבהרת דרישות, הגדרת חוקים עסקיים
- עיצוב—סכמת מסד נתונים, ארכיטקטורת שירות
- יישום—כתיבת קוד, בדיקות על סביבת staging מבודדת
- בדיקות—בדיקות עומס, אימות אטומיות והחזרים
- פריסה—העלאה לייצור, ניטור
לוחות זמנים ליישום
מערכת מלאה אורכת בין 1.5 ל-2 שבועות. לוחות הזמנים מעודנים לאחר ניתוח הדרישות. הזמינו פיתוח מערכת תעודות מתנה—קבלו ייעוץ מהנדס. נעריך את הפרויקט שלכם. צרו קשר כדי לדון בפרויקט שלכם.







