Реалізація обмеження завантажень (за кількістю/часом) для цифрових товарів

Наша компанія займається розробкою, підтримкою та обслуговуванням сайтів будь-якої складності. Від простих односторінкових сайтів до масштабних кластерних систем, побудованих на мікро сервісах. Досвід розробників підтверджено сертифікатами від вендорів.

Розробка та обслуговування будь-яких видів сайтів:

Інформаційні сайти або веб-програми
Сайти візитки, landing page, корпоративні сайти, онлайн каталоги, квіз, промо-сайти, блоги, ресурси новин, інформаційні портали, форуми, агрегатори
Сайти або веб-програми електронної комерції
Інтернет-магазини, B2B-портали, маркетплейси, онлайн-обмінники, кешбек-сайти, біржі, дропшиппінг-платформи, парсери товарів
Веб-програми для управління бізнес-процесами
CRM-системи, ERP-системи, корпоративні портали, системи управління виробництвом, парсери інформації
Сайти або веб-програми електронних послуг
Дошки оголошень, онлайн-школи, онлайн-кінотеатри, конструктори сайтів, портали надання електронних послуг, відеохостинги, тематичні портали

Це лише деякі з технічних типів сайтів, з якими ми працюємо, і кожен із них може мати свої специфічні особливості та функціональність, а також бути адаптованим під конкретні потреби та цілі клієнта.

Пропоновані послуги
Показано 1 з 1 послугУсі 2065 послуг
Реалізація обмеження завантажень (за кількістю/часом) для цифрових товарів
Середня
від 1 робочого дня до 3 робочих днів
Часті питання

Наші компетенції:

Етапи розробки

Останні роботи

  • image_website-b2b-advance_0.png
    Розробка сайту компанії B2B ADVANCE
    1262
  • image_web-applications_feedme_466_0.webp
    Розробка веб-додатків для компанії FEEDME
    1171
  • image_websites_belfingroup_462_0.webp
    Розробка веб-сайту для компанії БЕЛФІНГРУП
    874
  • image_ecommerce_furnoro_435_0.webp
    Розробка інтернет магазину для компанії FURNORO
    1094
  • image_crm_enviok_479_0.webp
    Розробка веб-додатків для компанії Enviok
    831
  • image_bitrix-bitrix-24-1c_fixper_448_0.png
    Розробка веб-сайту для компанії ФІКСПЕР
    851

Реалізація обмежень завантажень (за кількістю/часом) для цифрових товарів

Обмеження на завантаження — не просто захист від нелегального розповсюдження, а й комерційний інструмент: різні тарифи можуть надавати різну кількість завантажень або строк доступу. Реалізація має обробляти граничні випадки: одночасні клики, переконнекти, закритий браузер посередині завантаження.

Типи обмежень

Тип Опис Приклад
За кількістю N завантажень на одну покупку 3 завантаження
За часом Доступ до конкретної дати 30 днів після оплати
Комбінований І те, й інше 5 завантажень або 90 днів
За IP Тільки з зареєстрованої IP Корпоративні ліцензії
За пристроями Прив'язка до fingerprint Для ПО

Модель обмежень

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']);
});

Сервіс перевірки обмежень

class DownloadLimitGuard
{
    /**
     * Перевірити доступність завантаження та повернути причину відмови (якщо є)
     */
    public function check(DigitalOrderDownload $download): DownloadCheckResult
    {
        if ($download->is_revoked) {
            return DownloadCheckResult::denied('revoked', 'Доступ відозваний');
        }

        if ($download->expires_at && $download->expires_at->isPast()) {
            return DownloadCheckResult::denied(
                'expired',
                'Строк дії посилання сплинув ' . $download->expires_at->format('d.m.Y'),
            );
        }

        if ($download->downloads_limit !== null
            && $download->downloads_count >= $download->downloads_limit) {
            return DownloadCheckResult::denied(
                'limit_reached',
                "Ліміт завантажень вичерпаний ({$download->downloads_count} з {$download->downloads_limit})",
            );
        }

        return DownloadCheckResult::allowed(
            remainingDownloads: $download->downloads_limit !== null
                ? $download->downloads_limit - $download->downloads_count
                : null,
            expiresAt: $download->expires_at,
        );
    }
}

Атомарне збільшення лічильника

Проблема: два одночасних запити можуть обидва пройти перевірку ліміту перед оновленням лічильника. Рішення — песимістичне блокування рядка:

class RecordDownloadAction
{
    public function execute(DigitalOrderDownload $download, Request $request): void
    {
        DB::transaction(function () use ($download, $request) {
            // Блокуємо рядок для оновлення — SELECT ... FOR UPDATE
            $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(),
            ]);
        });
    }
}

Обробка переривистих завантажень

HTTP Range запити — механізм докачування. Браузер надсилає заголовок Range: bytes=1048576-, запитуючи продовження. Якщо кожен Range-запит вважати окремим завантаженням, ліміт вичерпається при одному завантаженню великого файлу.

Рішення: вважати завантаженням тільки перший запит без Range-заголовка або запит з Range: bytes=0-:

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);
}

Конфігурація лімітів за тарифом

Різні товари можуть мати різні дефолтні ліміти, а тариф при покупці їх перекриває:

// digital_products.download_limit — дефолтний ліміт для товару
// Перекривається при створенні DigitalOrderDownload залежно від тарифу

class CreateDigitalDownloadAction
{
    public function execute(OrderItem $item): DigitalOrderDownload
    {
        $dp    = $item->product->digitalProduct;
        $plan  = $item->plan_id ? Plan::find($item->plan_id) : null;

        $limit       = $plan?->download_limit ?? $dp->download_limit;
        $validityDays = $plan?->validity_days ?? $dp->validity_days;

        return DigitalOrderDownload::create([
            'order_item_id'      => $item->id,
            'digital_product_id' => $dp->id,
            'token'              => bin2hex(random_bytes(32)),
            'downloads_limit'    => $limit,
            'expires_at'         => $validityDays ? now()->addDays($validityDays) : null,
        ]);
    }
}

Адміністративний контроль

Доступні дії в панелі управління:

  • Скинути лічильник — відновити ліміт при технічній проблемі покупця
  • Продовжити строк — змінити expires_at вручну
  • Відозвати доступ — встановити is_revoked = true
  • Додати завантаження — збільшити downloads_limit
// Приклад: продовження строку через artisan-команду або API
$download->update(['expires_at' => $download->expires_at->addDays(30)]);

Сповіщення про закінчення

За 3 дні до закінчення строку надсилається email-нагадування:

$schedule->command('digital:notify-expiring --days=3')->dailyAt('10:00');

Строки

Базові обмеження (лічильник + строк) з атомарним збільшенням — 2–3 робочих дні. Обробка Range-запитів, обмеження за IP/пристроєм, тарифні плани — ще 2–3 дні.