Files
ticket-system/backend/api/routers/tickets.py
openit 1dcecb8d52
All checks were successful
Deploy / deploy (push) Successful in 48s
Update rabbitMQ&paimentsMOK
2026-03-12 09:00:14 +00:00

193 lines
8.0 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import os
import uuid
import httpx
from datetime import datetime, timedelta, timezone
from fastapi import APIRouter, Depends, HTTPException, status
from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy.orm import selectinload
from yookassa import Configuration, Payment
from core.rabbitmq import publish_ticket_task
from api.deps import get_current_user
from database.models import Seat, Ticket, TicketStatus, User
from database.session import get_db
from schemas.ticket import (
TicketResponse,
TicketScanRequest,
TicketScanResponse,
TicketBookRequest,
TicketBookResponse,
YookassaWebhook
)
# Если у тебя есть готовая функция для Раббита, раскомментируй импорт (путь может отличаться)
# from core.rabbitmq import publish_ticket_task
router = APIRouter(prefix="/api/tickets", tags=["tickets"])
@router.get("/me", response_model=list[TicketResponse])
async def get_my_tickets(
current_user: User = Depends(get_current_user),
db: AsyncSession = Depends(get_db),
) -> list[Ticket]:
"""Возвращает все оплаченные билеты текущего пользователя."""
result = await db.execute(
select(Ticket)
.where(Ticket.user_id == current_user.id, Ticket.status == TicketStatus.PAID)
.options(selectinload(Ticket.seat).selectinload(Seat.tournament))
.order_by(Ticket.created_at.desc())
)
return list(result.scalars().all())
@router.post("/scan", response_model=TicketScanResponse)
async def scan_ticket(
body: TicketScanRequest,
current_user: User = Depends(get_current_user),
db: AsyncSession = Depends(get_db),
) -> TicketScanResponse:
"""Сканирует билет по secret_token (QR-код)."""
result = await db.execute(select(Ticket).where(Ticket.secret_token == body.token))
ticket: Ticket | None = result.scalar_one_or_none()
if ticket is None:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail=TicketScanResponse(success=False, message="Билет не найден или подделка").model_dump(),
)
if ticket.status == TicketStatus.SCANNED:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail=TicketScanResponse(success=False, message="Билет уже отсканирован!", ticket_id=ticket.id).model_dump(),
)
if ticket.status != TicketStatus.PAID:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail=TicketScanResponse(success=False, message=f"Проход запрещен: статус билета '{ticket.status.value}'", ticket_id=ticket.id).model_dump(),
)
ticket.status = TicketStatus.SCANNED
await db.commit()
return TicketScanResponse(success=True, message="Проход разрешен", ticket_id=ticket.id)
@router.post("/book", response_model=TicketBookResponse)
async def book_ticket(
body: TicketBookRequest,
current_user: User = Depends(get_current_user),
db: AsyncSession = Depends(get_db),
):
"""Блокирует место на 15 минут и генерирует ссылку на оплату."""
seat_query = await db.execute(
select(Seat).options(selectinload(Seat.tournament)).where(Seat.id == body.seat_id)
)
seat = seat_query.scalar_one_or_none()
if not seat:
raise HTTPException(status_code=404, detail="Место не найдено")
ticket_query = await db.execute(select(Ticket).where(Ticket.seat_id == seat.id))
existing_ticket = ticket_query.scalar_one_or_none()
now = datetime.now(timezone.utc)
# Проверка блокировок
if existing_ticket:
if existing_ticket.status == TicketStatus.PAID:
raise HTTPException(status_code=400, detail="Место уже выкуплено")
if existing_ticket.status == TicketStatus.LOCKED and existing_ticket.expires_at and existing_ticket.expires_at > now:
if existing_ticket.user_id != current_user.id:
raise HTTPException(status_code=400, detail="Место временно забронировано другим пользователем")
description = f"Билет: {seat.tournament.title}, Сектор {seat.sector}, Ряд {seat.row}, Место {seat.number}"[:128]
shop_id = os.getenv("YOOKASSA_SHOP_ID", "dummy")
if shop_id == "test_shop":
# Стучимся в наш мок-сервер
mock_url = os.getenv("MOCK_API_URL", "http://192.168.149.101:8083/v3/payments")
async with httpx.AsyncClient() as client:
response = await client.post(
mock_url,
json={
"amount": {"value": f"{seat.price}.00", "currency": "RUB"},
"confirmation": {"type": "redirect", "return_url": "https://openticket.artifitial.ru/tickets"},
"capture": True,
"description": description
},
headers={"Idempotence-Key": str(uuid.uuid4())}
)
response.raise_for_status()
payment_data = response.json()
payment_id = payment_data["id"]
payment_url = payment_data["confirmation"]["confirmation_url"]
else:
# Реальная ЮKassa
Configuration.account_id = shop_id
Configuration.secret_key = os.getenv("YOOKASSA_SECRET_KEY")
payment = Payment.create({
"amount": {"value": f"{seat.price}.00", "currency": "RUB"},
"confirmation": {"type": "redirect", "return_url": "https://openticket.artifitial.ru/tickets"},
"capture": True,
"description": description
}, str(uuid.uuid4()))
payment_id = payment.id
payment_url = payment.confirmation.confirmation_url
# Фиксируем бронь
if not existing_ticket:
ticket = Ticket(
seat_id=seat.id,
user_id=current_user.id,
status=TicketStatus.LOCKED,
payment_id=payment_id,
payment_url=payment_url,
expires_at=now + timedelta(minutes=15)
)
db.add(ticket)
else:
existing_ticket.user_id = current_user.id
existing_ticket.status = TicketStatus.LOCKED
existing_ticket.payment_id = payment_id
existing_ticket.payment_url = payment_url
existing_ticket.expires_at = now + timedelta(minutes=15)
ticket = existing_ticket
await db.commit()
return TicketBookResponse(ticket_id=ticket.id, payment_url=payment_url)
@router.post("/webhook/yookassa")
async def yookassa_webhook(
payload: YookassaWebhook,
db: AsyncSession = Depends(get_db)
):
"""Принимает уведомления от платежного шлюза."""
query = await db.execute(select(Ticket).where(Ticket.payment_id == payload.object.id))
ticket = query.scalar_one_or_none()
if not ticket:
return {"status": "ignored", "reason": "ticket not found"}
if payload.event == "payment.succeeded" and payload.object.status == "succeeded":
if ticket.status != TicketStatus.PAID:
ticket.status = TicketStatus.PAID
await db.commit()
# ---> ЗДЕСЬ ПУШИМ ЗАДАЧУ В RABBITMQ <---
# Если у тебя есть функция публикации:
# ...
ticket.status = TicketStatus.PAID
await db.commit()
# Отправляем в очередь
await publish_ticket_task(ticket.id)
# ...
print(f"Оплата получена. Задача на PDF для билета {ticket.id} отправлена в очередь.")
elif payload.event == "payment.canceled":
if ticket.status == TicketStatus.LOCKED:
await db.delete(ticket)
await db.commit()
return {"status": "ok"}