Реалізація продажу електронних книг (PDF/EPUB) на сайті
Продаж цифрових книг простіше, ніж курси, але все рівно потребує кілька нетривіальних рішень: захист файлів від вільного розповсюдження, швидка доставка після оплати, опціональний DRM та коректна робота з поверненнями.
Схема доставки файлів
Головне правило: файли не повинні лежати в публічно доступній директорії. Ніяких /public/books/my-book.pdf. Сховище — S3-сумісне об'єктне сховище (AWS S3, Cloudflare R2, MinIO) без публічного ACL.
Після підтвердження оплати користувач отримує тимчасове підписане посилання:
// Laravel + AWS S3
$url = Storage::disk('s3')->temporaryUrl(
"books/{$book->file_key}",
now()->addHours(48),
['ResponseContentDisposition' => 'attachment; filename="' . $book->filename . '"']
);
Посилання живе 48 годин. Повторне завантаження через особистий кабінет — кожен раз генерується нове посилання. Кількість завантажень можна обмежити: таблиця download_attempts(purchase_id, downloaded_at), ліміт — наприклад, 5 завантажень на покупку.
Платіжна інтеграція
Для цифрових товарів Stripe рекомендує payment_intent або checkout.session. Ключевий момент — флаг оподаткування: цифрові книги в деяких юрисдикціях облагаються НДС інакше, ніж фізичні товари. Stripe Tax вміє це визначати автоматично за IP/адресою платника.
const session = await stripe.checkout.sessions.create({
mode: 'payment',
line_items: [{
price: book.stripe_price_id, // заздалегідь створений Price в Stripe Dashboard
quantity: 1,
}],
automatic_tax: { enabled: true },
metadata: { book_id: book.id, user_id: user.id },
success_url: `${APP_URL}/library?session_id={CHECKOUT_SESSION_ID}`,
cancel_url: `${APP_URL}/books/${book.slug}`,
});
Webhook checkout.session.completed створює запис purchases і надсилає листа з посиланням на завантаження.
Структура даних
CREATE TABLE purchases (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
user_id BIGINT REFERENCES users(id),
book_id BIGINT REFERENCES books(id),
payment_id VARCHAR(255) UNIQUE,
amount_cents INT,
currency VARCHAR(3),
status VARCHAR(20) DEFAULT 'completed', -- completed | refunded
created_at TIMESTAMP DEFAULT NOW()
);
CREATE TABLE download_attempts (
id BIGSERIAL PRIMARY KEY,
purchase_id UUID REFERENCES purchases(id),
ip_address VARCHAR(45),
user_agent TEXT,
created_at TIMESTAMP DEFAULT NOW()
);
Формати: PDF vs EPUB
Більшість покупців віддають перевагу PDF для читання на десктопі та EPUB для мобільних читалок (Kindle, Apple Books, Kobo). Продавати обидва формати в рамках однієї покупки — добра практика.
Зберігати файли з різними суфіксами в одному бакеті:
books/
{uuid}-original.epub
{uuid}-print.pdf
{uuid}-cover.jpg
Таблиця book_files(book_id, format, file_key, file_size_bytes).
Опціональний watermark
Для дорогих книг має сенс додавати персоналізований водяний знак з email покупця. Це не DRM, але психологічно стримує розповсюдження.
PDF watermark через pypdf (Python) або iTextSharp (.NET). В екосистемі PHP — setasign/fpdi:
use setasign\Fpdi\Fpdi;
$pdf = new Fpdi();
$pageCount = $pdf->setSourceFile($sourcePath);
for ($i = 1; $i <= $pageCount; $i++) {
$pdf->AddPage();
$pdf->useTemplate($pdf->importPage($i));
$pdf->SetFont('Helvetica', '', 8);
$pdf->SetTextColor(180, 180, 180);
$pdf->SetXY(10, 285);
$pdf->Write(0, "Licensed to: {$purchase->user->email}");
}
$pdf->Output($outputPath, 'F');
Генерація відбувається асинхронно в черзі (Laravel Jobs / Bull / Celery), після чого посилання оновлюється. Для книг до 10 МБ це займає 2–5 секунд.
Доставка по email
Після покупки листа з кнопкою завантаження повинна прийти протягом 30–60 секунд. Не робити це синхронно в HTTP-запиті — надсилати через чергу:
// У webhook handler
ProcessPurchase::dispatch($purchase)->onQueue('purchases');
// У Job
class ProcessPurchase implements ShouldQueue {
public function handle() {
// 1. Генеруємо watermark (опціонально)
// 2. Створюємо підписане посилання
// 3. Надсилаємо листа
Mail::to($this->purchase->user)->send(
new BookPurchasedMail($this->purchase, $downloadUrl)
);
}
}
Особиста бібліотека
Користувач повинен мати можливість завантажити книгу повторно через /library. Там список всіх покупок з кнопкою «Завантажити», що викликає ендпоінт генерації нового temporary URL. Це важливо — зберігати постійне посилання в БД нема сенсу, воно протухне.
Строки
| Завдання | Час |
|---|---|
| Завантаження файлів, S3, захист | 1 день |
| Платіжна інтеграція + webhook | 1–2 дні |
| Особиста бібліотека, повторне завантаження | 1 день |
| Email-доставка | 0,5 дня |
| Watermark (PDF) | 1–2 дні |
Базова реалізація без watermark — 3–4 дні.







