Update project 11 FRONT MVP phase 2 complete
This commit is contained in:
53
backend/core/pdf_generator.py
Normal file
53
backend/core/pdf_generator.py
Normal file
@@ -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()
|
||||
@@ -12,3 +12,4 @@ aio-pika
|
||||
reportlab
|
||||
aioboto3
|
||||
pydantic[email]>=2.5.0
|
||||
qrcode[pil]==7.4.2
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -26,3 +26,16 @@
|
||||
- ЗАПРЕЩЕНО создавать "мертвые" кнопки навигации.
|
||||
- Любой элемент UI, который визуально выглядит как кнопка перехода на другую страницу, ОБЯЗАН быть обернут в компонент `<Link href="...">` из `next/link` или иметь обработчик `onClick={() => router.push('...')}`.
|
||||
- Если для кнопки пока нет API (например, Apple Wallet), она должна выводить `toast("Функция в разработке")`.
|
||||
|
||||
## 6. Карта маршрутизации (Screen Flow)
|
||||
[ Каталог турниров : / ] (Главная)
|
||||
├── Клик по турниру ──> [ Схема зала : /events/[id]/seats ]
|
||||
│ ├── Клик "Назад" ──> (возврат на /)
|
||||
│ └── Клик "Оплатить" ──> [ Оформление заказа : /checkout ]
|
||||
│ └── Успех ──> [ Мои билеты : /tickets ]
|
||||
│
|
||||
├── Клик в TabBar "Билеты" ──> [ Мои билеты : /tickets ] (Требует авторизации)
|
||||
│ └── Клик по карточке ──> [ Электронный билет (QR) : /tickets/[ticket_id] ]
|
||||
│ └── Клик "Назад" ──> (возврат на /tickets)
|
||||
│
|
||||
└── Клик в TabBar "Профиль" ──> [ Профиль : /profile ] (Если нет токена -> редирект на /login)
|
||||
@@ -49,6 +49,7 @@ export default function SeatsPage() {
|
||||
<div className="flex items-center justify-between px-5 pt-12 pb-3">
|
||||
<button
|
||||
aria-label="Назад"
|
||||
onClick={() => router.back()}
|
||||
className="w-9 h-9 flex items-center justify-center rounded-full bg-[#1C1C1E] text-white"
|
||||
>
|
||||
<ArrowLeft size={18} strokeWidth={2.5} />
|
||||
|
||||
@@ -97,7 +97,9 @@ export default function HomePage() {
|
||||
{/* ── Event list ── */}
|
||||
<div className="flex flex-col gap-3 px-4 flex-1 pb-24">
|
||||
{MOCK_EVENTS.map((event) => (
|
||||
<EventCard key={event.id} event={event} />
|
||||
<Link key={event.id} href={`/events/${event.id}/seats`}>
|
||||
<EventCard event={event} />
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user