נעילת הורדות דיגיטליות: טרנזקציות אטומיות ובקשות Range
אתם מוכרים דוחות PDF, מדריכי וידאו או רישיונות תוכנה. לקוח רוכש גישה ל-30 יום עם 5 הורדות מותרות. שבוע לאחר מכן, הוא סוגר את הדפדפן במהלך הורדה—המכסה מוצתה. או שני עובדים לוחצים על "הורדה" בו-זמנית—שניהם עוברים את הבדיקה והקובץ נשלח מעבר למכסה. ללא מנגנון הגבלת הורדות אמין, אתם מאבדים עד 30% מההכנסות מהפצה בלתי מבוקרת. אנחנו צוות מהנדסים עם 6 שנות ניסיון, שיישמנו מעל 50 פתרונות להגבלת גישה למוצרים דיגיטליים. הגישה שלנו היא אמינה פי 10 ממודולי CMS סטנדרטיים, שלעיתים קרובות מתעלמים מתנאי מרוץ (race conditions) ולא מצליחים להבחין בין ההורדה הראשונה לבין חידוש הורדה. בבסיס: טרנזקציות אטומיות עם נעילת שורות SELECT ... FOR UPDATE וטיפול בבקשות HTTP Range. זה מבטיח שכל לקוח מוריד בדיוק כמה פעמים שהתעריף שלו מאפשר.
למה טרנזקציות אטומיות קריטיות למכסות?
ללא נעילה, שתי בקשות בו-זמניות יכולות לעבור את הבדיקה לפני שהמונה מתעדכן. הפתרון הוא נעילת שורות פסימית. בתוך טרנזקציה, אנו מבצעים SELECT ... FOR UPDATE, בודקים שוב את המכסה, ומגדילים את המונה. זה מבטיח שאף בקשה לא תחרוג מהמכסה גם בעומס שיא של 1000 RPS. אנו מיישמים תבנית זו ב-Laravel באמצעות מחלקות Repository ו-Action. בנוסף, אנו מתעדים כל הורדה בטבלת download_events.
סוגי מכסות
| סוג | תיאור | דוגמה |
|---|---|---|
| לפי כמות | N הורדות לכל רכישה | 3 הורדות |
| לפי זמן | גישה עד תאריך מסוים | 30 יום לאחר תשלום |
| משולב | שניהם | 5 הורדות או 90 יום |
| לפי IP | רק מ-IP רשום | רישיונות ארגוניים |
| לפי מכשיר | מקושר לטביעת אצבע | לתוכנה |
דוגמת תצורה לפי תעריף
| תעריף | מכסת הורדות | תקופת תוקף |
|---|---|---|
| בסיסי | 5 | 30 יום |
| סטנדרטי | 20 | 90 יום |
| פרימיום | ללא הגבלה | שנה |
איך למנוע תנאי מרוץ?
כדי לאחסן נתונים, צרו את הטבלה digital_order_downloads:
Schema::create('digital_order_downloads', function (Blueprint $table) { $table->id(); $table->foreignId('order_item_id')->constrained(); $table->foreignId('digital_product_id')->constrained(); $table->string('token', 64)->unique(); $table->integer('downloads_count')->default(0); $table->integer('downloads_limit')->nullable(); // NULL = без лимита $table->timestamp('expires_at')->nullable(); // NULL = бессрочно $table->boolean('is_revoked')->default(false); // ручная блокировка $table->timestamps(); $table->index(['token', 'is_revoked']); }); המחלקה Schema::create('digital_order_downloads', function (Blueprint $table) { $table->id(); $table->foreignId('order_item_id')->constrained(); $table->foreignId('digital_product_id')->constrained(); $table->string('token', 64)->unique(); $table->integer('downloads_count')->default(0); $table->integer('downloads_limit')->nullable(); // NULL = без лимита $table->timestamp('expires_at')->nullable(); // NULL = бессрочно $table->boolean('is_revoked')->default(false); // ручная блокировка $table->timestamps(); $table->index(['token', 'is_revoked']); }); בודקת זמינות הורדה. בעיה: שתי בקשות בו-זמניות יכולות לעבור את הבדיקה לפני שהמונה מתעדכן. פתרון—נעילת שורות פסימית. בתוך טרנזקציה, אנו מבצעים DownloadLimitGuard, בודקים שוב את המכסה, ומגדילים את המונה.
class RecordDownloadAction { public function execute(DigitalOrderDownload $download, Request $request): void { DB::transaction(function () use ($download, $request) { $locked = DigitalOrderDownload::lockForUpdate()->findOrFail($download->id); if ($locked->downloads_limit !== null && $locked->downloads_count >= $locked->downloads_limit) { throw new DownloadLimitExceededException(); } $locked->increment('downloads_count'); DownloadEvent::create([ 'digital_order_download_id' => $locked->id, 'ip_address' => $request->ip(), 'user_agent' => $request->userAgent(), 'referer' => $request->header('Referer'), 'downloaded_at' => now(), ]); }); } } טיפול בבקשות Range: איך לא לאבד מכסה בחידוש הורדה
לפי מפרט HTTP, בקשות Range מיועדות לחידוש הורדות ואין לספור אותן כהורדה חדשה (ראו MDN Web Docs על HTTP Range). אם כל בקשת Range הייתה נספרת כהורדה, המכסה הייתה מוצתה על קובץ גדול אחד. אנו סופרים רק את הבקשה הראשונה ללא SELECT ... FOR UPDATE או עם class RecordDownloadAction { public function execute(DigitalOrderDownload $download, Request $request): void { DB::transaction(function () use ($download, $request) { $locked = DigitalOrderDownload::lockForUpdate()->findOrFail($download->id); if ($locked->downloads_limit !== null && $locked->downloads_count >= $locked->downloads_limit) { throw new DownloadLimitExceededException(); } $locked->increment('downloads_count'); DownloadEvent::create([ 'digital_order_download_id' => $locked->id, 'ip_address' => $request->ip(), 'user_agent' => $request->userAgent(), 'referer' => $request->header('Referer'), 'downloaded_at' => now(), ]); }); } } . הנה דוגמה:
public function download(string $token, Request $request): Response { $download = DigitalOrderDownload::where('token', $token)->firstOrFail(); $guard = app(DownloadLimitGuard::class); $result = $guard->check($download); if (!$result->isAllowed()) { return response()->view('digital.download-denied', ['reason' => $result->reason], 403); } $rangeHeader = $request->header('Range'); $isFirstRequest = !$rangeHeader || $rangeHeader === 'bytes=0-'; if ($isFirstRequest) { app(RecordDownloadAction::class)->execute($download, $request); } return $this->streamFile($download->digitalProduct); } איך להגדיר ולנהל מכסות?
למוצרים שונים יש מכסות ברירת מחדל, והתעריף עוקף אותן ברכישה. בלוח הניהול, מנהל יכול לאפס את המונה, להאריך את התפוגה, או לבטל גישה. כל הפעולות מתועדות. שלושה ימים לפני התפוגה, נשלחת תזכורת דוא"ל על ידי הפקודה Range, המתוזמנת יומית באמצעות cron. אפשר גם אינטגרציה עם מערכות חיוב לעדכון מכסות אוטומטי עם התשלום.
תהליך העבודה
- ניתוח—אנו לומדים את המוצרים הדיגיטליים שלכם, התעריפים ודרישות המכסה. אורך 1-2 ימים.
- עיצוב—אנו בוחרים אסטרטגיה (כמות/זמן/IP), מתכננים טבלאות ושירותים.
- יישום—אנו כותבים קוד, מכסים בבדיקות יחידה (>80% כיסוי).
- אינטגרציה—אנו מטמיעים ב-CMS או בפריימוורק שלכם.
- בדיקות—בדיקות עומס המדמות 5000 בקשות בו-זמניות, בדיקות חידוש הורדות.
- פריסה—אנו פורסים על האחסון שלכם, מגדירים ניטור.
לוחות זמנים ועלות
יישום בסיסי (מונה + תפוגה + הגדלה אטומית) — מ-$500 ו-2 ימי עבודה. תרחישים מורכבים (הגבלות IP/מכשיר, אינטגרציה עם ERP) — מ-$1500 ו-5 ימי עבודה. העלות מחושבת באופן אישי לאחר ניתוח דרישות. קבלו ייעוץ מהנדס—נעריך את הפרויקט שלכם בחינם.
טעויות נפוצות
- התעלמות מבקשות Range—כל חידוש צורך את המכסה, משתמשים מתלוננים.
- חוסר בטרנזקציות—בקשות בו-זמניות חורגות מהמכסה פי 2-3.
- קישור קשיח ל-IP—משתמשים ניידים מחליפים רשתות, הגישה נחסמת.
- טוקן קצר מדי—32 תווים לא מספיקים; השתמשו ב-64+.
- התראות רק באנגלית—משתמשים דוברי רוסית זקוקים ללוקליזציה.
מכסות תעריף מפורטות (לחצו להרחבה)
| תעריף | מכסת הורדות | תקופת תוקף |
|---|---|---|
| בסיסי | 5 | 30 יום |
| סטנדרטי | 20 | 90 יום |
| פרימיום | ללא הגבלה | שנה |
מה כלול
- תיעוד API (נקודות קצה, דוגמאות בקשות).
- הוראות הפעלה למנהל.
- גישה למאגר פרטי עם הקוד.
- 3 חודשי תמיכה טכנית לאחר ההשקה.
- המלצות לניטור והרחבה.
צרו קשר כדי לדון בפרויקט שלכם. בקשו ייעוץ מהנדס—אנו נעזור לאבטח את המוצרים הדיגיטליים שלכם.







