diff --git a/backend/core/pdf_generator.py b/backend/core/pdf_generator.py new file mode 100644 index 0000000..3a6afa6 --- /dev/null +++ b/backend/core/pdf_generator.py @@ -0,0 +1,53 @@ +import io +import qrcode +from reportlab.lib.pagesizes import landscape, A5 +from reportlab.pdfgen import canvas +from reportlab.lib import colors +from reportlab.lib.utils import ImageReader + +def generate_qr_ticket(ticket_id: int, user_id: int, seat_id: int = 0) -> bytes: + buffer = io.BytesIO() + width, height = landscape(A5) + c = canvas.Canvas(buffer, pagesize=(width, height)) + + # Темный фон (под дизайн фронтенда) + c.setFillColor(colors.HexColor("#121212")) + c.rect(0, 0, width, height, fill=1, stroke=0) + + # Белая карточка билета + margin = 20 + c.setFillColor(colors.white) + c.roundRect(margin, margin, width - 2*margin, height - 2*margin, 10, fill=1, stroke=0) + + # Текст + c.setFillColor(colors.black) + c.setFont("Helvetica-Bold", 28) + c.drawString(margin + 30, height - margin - 50, "EVENT TICKET") + + c.setFont("Helvetica", 14) + c.drawString(margin + 30, height - margin - 100, f"Ticket ID: {ticket_id}") + c.drawString(margin + 30, height - margin - 130, f"User ID: {user_id}") + if seat_id: + c.drawString(margin + 30, height - margin - 160, f"Seat ID: {seat_id}") + + c.setFont("Helvetica-Oblique", 10) + c.setFillColor(colors.gray) + c.drawString(margin + 30, margin + 30, "Scan this QR code at the entrance.") + + # QR-код + qr = qrcode.QRCode(box_size=5, border=1) + qr.add_data(f"TICKET:{ticket_id}|USER:{user_id}|SEAT:{seat_id}") + qr.make(fit=True) + img = qr.make_image(fill_color="black", back_color="white") + + # Буферизация картинки для ReportLab + img_buffer = io.BytesIO() + img.save(img_buffer, format="PNG") + img_buffer.seek(0) + + c.drawImage(ImageReader(img_buffer), width - margin - 160, margin + 30, width=130, height=130) + + c.showPage() + c.save() + buffer.seek(0) + return buffer.read() diff --git a/backend/requirements.txt b/backend/requirements.txt index ba7d555..a1b762d 100644 --- a/backend/requirements.txt +++ b/backend/requirements.txt @@ -12,3 +12,4 @@ aio-pika reportlab aioboto3 pydantic[email]>=2.5.0 +qrcode[pil]==7.4.2 diff --git a/backend/worker.py b/backend/worker.py index c3b32b7..60b49ba 100644 --- a/backend/worker.py +++ b/backend/worker.py @@ -1,22 +1,20 @@ """ Standalone worker: слушает очередь RabbitMQ 'ticket_events', -генерирует PDF-билет через reportlab, загружает в MinIO, +генерирует PDF-билет через core.pdf_generator, загружает в MinIO, сохраняет pdf_url в PostgreSQL. """ import asyncio -import io import json import logging import os from typing import Any import aio_pika -from reportlab.lib.pagesizes import A4 -from reportlab.pdfgen import canvas from sqlalchemy import select from sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker, create_async_engine from core.minio import ensure_bucket_exists, upload_pdf +from core.pdf_generator import generate_qr_ticket from database.models import Ticket logging.basicConfig( @@ -33,27 +31,6 @@ DATABASE_URL: str = os.getenv( QUEUE_NAME: str = "ticket_events" -def _build_pdf(ticket_id: int, seat_id: int, user_id: int) -> bytes: - """Генерирует PDF-билет в памяти и возвращает байты.""" - buffer = io.BytesIO() - pdf = canvas.Canvas(buffer, pagesize=A4) - width, height = A4 - - pdf.setFont("Helvetica-Bold", 24) - pdf.drawCentredString(width / 2, height - 80, "TICKET") - - pdf.setFont("Helvetica", 14) - pdf.drawCentredString(width / 2, height - 130, f"Ticket ID: {ticket_id}") - pdf.drawCentredString(width / 2, height - 160, f"Seat ID: {seat_id}") - pdf.drawCentredString(width / 2, height - 190, f"User ID: {user_id}") - - pdf.setFont("Helvetica-Oblique", 10) - pdf.drawCentredString(width / 2, 40, "Thank you for your purchase!") - - pdf.save() - return buffer.getvalue() - - async def _handle_ticket_paid( payload: dict[str, Any], db_session: AsyncSession, @@ -75,10 +52,10 @@ async def _handle_ticket_paid( return log.info("Generating PDF for ticket %s …", ticket_id) - pdf_bytes = _build_pdf( + pdf_bytes = generate_qr_ticket( ticket_id=ticket.id, - seat_id=ticket.seat_id, user_id=ticket.user_id or 0, + seat_id=ticket.seat_id, ) object_name = f"tickets/ticket_{ticket_id}.pdf" diff --git a/frontend-client/FRONTEND_ARCHITECTURE.md b/frontend-client/FRONTEND_ARCHITECTURE.md index c7184eb..7e532c1 100644 --- a/frontend-client/FRONTEND_ARCHITECTURE.md +++ b/frontend-client/FRONTEND_ARCHITECTURE.md @@ -25,4 +25,17 @@ ## 5. Интерактивность и Навигация (КРИТИЧЕСКИ ВАЖНО) - ЗАПРЕЩЕНО создавать "мертвые" кнопки навигации. - Любой элемент UI, который визуально выглядит как кнопка перехода на другую страницу, ОБЯЗАН быть обернут в компонент `` из `next/link` или иметь обработчик `onClick={() => router.push('...')}`. -- Если для кнопки пока нет API (например, Apple Wallet), она должна выводить `toast("Функция в разработке")`. \ No newline at end of file +- Если для кнопки пока нет API (например, Apple Wallet), она должна выводить `toast("Функция в разработке")`. + +## 6. Карта маршрутизации (Screen Flow) +[ Каталог турниров : / ] (Главная) + ├── Клик по турниру ──> [ Схема зала : /events/[id]/seats ] + │ ├── Клик "Назад" ──> (возврат на /) + │ └── Клик "Оплатить" ──> [ Оформление заказа : /checkout ] + │ └── Успех ──> [ Мои билеты : /tickets ] + │ + ├── Клик в TabBar "Билеты" ──> [ Мои билеты : /tickets ] (Требует авторизации) + │ └── Клик по карточке ──> [ Электронный билет (QR) : /tickets/[ticket_id] ] + │ └── Клик "Назад" ──> (возврат на /tickets) + │ + └── Клик в TabBar "Профиль" ──> [ Профиль : /profile ] (Если нет токена -> редирект на /login) \ No newline at end of file diff --git a/frontend-client/src/app/events/[id]/seats/page.tsx b/frontend-client/src/app/events/[id]/seats/page.tsx index 3f46fcb..ac7e4dc 100644 --- a/frontend-client/src/app/events/[id]/seats/page.tsx +++ b/frontend-client/src/app/events/[id]/seats/page.tsx @@ -49,6 +49,7 @@ export default function SeatsPage() {