Реалізація продажу онлайн-курсів (доступ після оплати) на сайті

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

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

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

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

Пропоновані послуги
Показано 1 з 1 послугУсі 2065 послуг
Реалізація продажу онлайн-курсів (доступ після оплати) на сайті
Складна
~1-2 тижні
Часті питання

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

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

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

  • 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

Реалізація продажу онлайн-курсів (доступ після оплати) на сайті

Платформа продажу курсів — це не «додати товар в WooCommerce». Це зв'язка з платіжного шлюзу, системи управління доступом, відеохостингу та прогрес-трекінгу. Кожен шар має власну логіку відмови, а помилка в будь-якому означає або втрату грошей, або витік контенту до неоплачених користувачів.

Архітектурний скелет

Типова схема:

[Користувач] → [Checkout] → [Payment Gateway]
                                  ↓
                          [Webhook Handler]
                                  ↓
                         [Enrollment Service] → [DB: enrollments]
                                  ↓
                         [Access Control Layer]
                                  ↓
                    [LMS / Video Delivery / Downloads]

Webhook handler критичний — саме він створює запис про доступ після підтвердження оплати від шлюзу. Спроба виділити доступ синхронно в момент редиректу після оплати — класична помилка, що призводить до race conditions при збоях мережі.

Платіжні шлюзи та інтеграція

Stripe — предпочтительний варіант для міжнародних платежів. Використовуємо stripe.checkout.sessions.create з mode: 'payment' або mode: 'subscription' для підписочної моделі.

const session = await stripe.checkout.sessions.create({
  mode: 'payment',
  line_items: [{
    price_data: {
      currency: 'usd',
      product_data: { name: course.title },
      unit_amount: course.price_cents,
    },
    quantity: 1,
  }],
  metadata: { course_id: course.id, user_id: user.id },
  success_url: `${BASE_URL}/courses/${course.slug}/success?session_id={CHECKOUT_SESSION_ID}`,
  cancel_url: `${BASE_URL}/courses/${course.slug}`,
});

Після оплати Stripe надсилає подію checkout.session.completed на webhook-ендпоінт. Валідуємо підпис (stripe.webhooks.constructEvent), витягуємо metadata.course_id та metadata.user_id, створюємо enrollment.

Для рунету підключаємо ЮKassa або Robokassa. Обидва працюють через аналогічну схему: сповіщення про платіж → перевірка підпису → виділення доступу.

Idempotency: webhook може прийти двічі. Enrollment-запис створюється з унікальним індексом за (user_id, course_id) або payment_id — другий виклик просто повертає існуючу запис.

Управління доступом

Таблиця enrollments:

Поле Тип Опис
id uuid первинний ключ
user_id bigint FK → users
course_id bigint FK → courses
payment_id varchar ID транзакції від шлюзу
expires_at timestamp NULL = безстроково
status enum active / suspended / refunded
created_at timestamp дата покупки

Middleware на кожному захищеному маршруті перевіряє наявність активної записи:

// Laravel Gate
Gate::define('access-course', function (User $user, Course $course) {
    return $user->enrollments()
        ->where('course_id', $course->id)
        ->where('status', 'active')
        ->where(function ($q) {
            $q->whereNull('expires_at')
              ->orWhere('expires_at', '>', now());
        })
        ->exists();
});

Відеодоставка та захист контенту

Не можна давати користувачам прямі посилання на відео — вони їх поділять. Варіанти:

Signed URLs (AWS S3 / CloudFront):

$url = $s3->createPresignedRequest(
    $s3->getCommand('GetObject', [
        'Bucket' => 'courses-bucket',
        'Key'    => "courses/{$courseId}/lesson-{$lessonId}.mp4",
    ]),
    '+2 hours'
)->getUri();

Посилання живе 2 години та прив'язане до конкретного файлу. Після закінчення — 403.

Vimeo Private / Mux: для продакшену з великим трафіком краще використовувати спеціалізовані відеоплатформи. Mux надає адаптивний стриміинг (HLS), аналітику переглядів та захист через підписані playback tokens:

const playbackId = lesson.mux_playback_id;
const token = await signMuxToken(playbackId, 'video', {
  expiration: '2h',
  params: { user_id: userId },
});
const src = `https://stream.mux.com/${playbackId}.m3u8?token=${token}`;

Для PDF/EPUB — аналогічно: генеруємо підписане посилання на завантаження, логуємо кожен доступ в content_access_logs.

Прогрес та сертифікати

Прогрес по урокам зберігається в lesson_progress(user_id, lesson_id, completed_at, watch_percent). Фронтенд надсилає eventos завершення:

// Кожні 30 секунд або при pause/end
videoPlayer.on('timeupdate', debounce(() => {
  api.post('/progress', {
    lesson_id: lessonId,
    watch_percent: Math.round((player.currentTime / player.duration) * 100),
  });
}, 5000));

Сертифікат генерується автоматично, коли watch_percent >= 80 для всіх уроків курсу. Для генерації PDF використовуємо puppeteer (рендер HTML-шаблону) або PDFKit для простих випадків.

Пробний доступ (preview)

Перші 1-2 урока зазвичай відкриті без оплати. Флаг is_free_preview на рівні урока, перевірка в middleware:

if ($lesson->is_free_preview || Gate::allows('access-course', $course)) {
    return $next($request);
}
return redirect()->route('course.buy', $course);

Повернення

При поверненні платежу Stripe надсилає charge.refunded. Змінюємо enrollments.status = 'refunded', користувач втрачає доступ миттєво. Для ЮKassa — аналогічний webhook payment.canceled.

Строки реалізації

Етап Час
Базова інтеграція Stripe + enrollment 2–3 дні
Захищена відеодоставка (S3 signed URL) 1–2 дні
Прогрес-трекінг + UI прогрес-бара 1–2 дні
Сертифікати (PDF-генерація) 1 день
Інтеграція ЮKassa / другого шлюзу 1–2 дні
Адміністративний дашборд (enrollments, revenue) 2–3 дні

Мінімальна робоча версія — 7–10 робочих днів.

Типові помилки

Виділення доступу до підтвердження від шлюзу. Користувач нажав кнопку оплати → попав на success-сторінку → отримав доступ. Платіж при цьому міг не пройти. Доступ виділяється тільки через webhook.

Зберігання відео на тому ж сервері що й додаток. Bandwidth убиває сервер при 50+ одночасних переглядах. Відео — тільки в об'єктному сховищі або в спеціалізованого провайдера.

Відсутність логів доступу. При спірній ситуації з користувачем («я заплатив, доступ не дали») нема можливості відновити ланцюжок подій. Логувати payment_id, webhook timestamp, enrollment_created_at обов'язково.