Реализация продажи электронных книг (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. Создать signed URL
// 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 дня.







