diff --git a/app/routes/subscription_routes.py b/app/routes/subscription_routes.py index 127b033..994fb7c 100644 --- a/app/routes/subscription_routes.py +++ b/app/routes/subscription_routes.py @@ -38,17 +38,21 @@ async def buy_subscription( result = await database_manager.buy_sub(request_data.telegram_id, request_data.plan_id) if result == "ERROR": - raise HTTPException(status_code=500, detail="Failed to buy subscription") + raise HTTPException(status_code=500, detail="ERROR") elif result == "INSUFFICIENT_FUNDS": - raise HTTPException(status_code=400, detail="Insufficient funds") + raise HTTPException(status_code=400, detail="INSUFFICIENT_FUNDS") elif result == "TARIFF_NOT_FOUND": - raise HTTPException(status_code=400, detail="Tariff not found") + raise HTTPException(status_code=400, detail="TARIFF_NOT_FOUND") elif result == "ACTIVE_SUBSCRIPTION_EXISTS": - raise HTTPException(status_code=400, detail="User already had subscription",) - - return {"message": "Subscription purchased successfully"} + raise HTTPException(status_code=400, detail="ACTIVE_SUBSCRIPTION_EXISTS") + result = await database_manager.generate_uri(request_data.telegram_id) + return {"message": result} + except HTTPException as http_exc: + # Пропускаем HTTPException, чтобы FastAPI обработал его корректно + raise http_exc except Exception as e: - raise HTTPException(status_code=500, detail=str(e)) + # Обрабатываем остальные исключения + raise HTTPException(status_code=500, detail=f"Unexpected error: {str(e)}") # Эндпоинт для получения последней подписки @@ -59,11 +63,12 @@ async def last_subscription(user_id: UUID, database_manager: DatabaseManager = D """ logger.info(f"Получение последней подписки для пользователя: {user_id}") try: - subscriptions = await database_manager.last_subscriptions(user_id=str(user_id), limit=1) + subscriptions = await database_manager.get_last_subscriptions(user_id=user_id, limit=1) if not subscriptions: logger.warning(f"Подписки для пользователя {user_id} не найдены") raise HTTPException(status_code=404, detail="No subscriptions found") + sub = subscriptions[0] return { @@ -77,9 +82,12 @@ async def last_subscription(user_id: UUID, database_manager: DatabaseManager = D except SQLAlchemyError as e: logger.error(f"Ошибка базы данных при получении подписки для пользователя {user_id}: {e}") raise HTTPException(status_code=500, detail="Database error") + except HTTPException as e: + # Пропускаем HTTPException, чтобы FastAPI обработал её автоматически + raise e except Exception as e: logger.error(f"Неожиданная ошибка: {e}") - raise HTTPException(status_code=500, detail=str(e)) + raise HTTPException(status_code=500, detail="Internal Server Error") @router.get("/subscriptions/{user_id}", response_model=List[SubscriptionResponse]) async def get_subscriptions(user_id: UUID, database_manager: DatabaseManager = Depends(get_database_manager)): @@ -114,3 +122,25 @@ async def get_subscriptions(user_id: UUID, database_manager: DatabaseManager = D logger.error(f"Неожиданная ошибка: {e}") raise HTTPException(status_code=500, detail=str(e)) +@router.get("/uri", response_model=dict) +async def get_uri(telegram_id: int, database_manager: DatabaseManager = Depends(get_database_manager)): + """ + Возвращает список подписок пользователя. + """ + logger.info(f"Получение подписок для пользователя: {telegram_id}") + try: + # Получаем подписки без ограничений или с указанным лимитом + uri = await database_manager.generate_uri(telegram_id) + if uri == "SUB_ERROR": + raise HTTPException(status_code=404, detail="User not found") + if not uri: + logger.warning(f"Не удалось сгенерировать URI для пользователя с telegram_id {telegram_id}") + raise HTTPException(status_code=404, detail="URI not found") + + return {"detail": uri } + except SQLAlchemyError as e: + logger.error(f"Ошибка базы данных при получении подписок для пользователя {telegram_id}: {e}") + raise HTTPException(status_code=500, detail="Database error") + except Exception as e: + logger.error(f"Неожиданная ошибка: {e}") + raise HTTPException(status_code=500, detail=str(e)) \ No newline at end of file diff --git a/app/routes/support_routes.py b/app/routes/support_routes.py index 5404467..2f32f75 100644 --- a/app/routes/support_routes.py +++ b/app/routes/support_routes.py @@ -43,8 +43,6 @@ def handle_exception(e: Exception, message: str): logger.error(f"{message}: {e}") raise HTTPException(status_code=500, detail=f"{message}: {str(e)}") - - @router.post("/support/tickets/{ticket_id}/messages", response_model=TicketMessageResponse, summary="Добавить сообщение") async def add_message( ticket_id: int, diff --git a/app/routes/user_routes.py b/app/routes/user_routes.py index 920c961..edcfb0a 100644 --- a/app/routes/user_routes.py +++ b/app/routes/user_routes.py @@ -1,8 +1,14 @@ from fastapi import APIRouter, Depends, HTTPException +from fastapi.exceptions import HTTPException from app.services.db_manager import DatabaseManager +from sqlalchemy.exc import SQLAlchemyError from instance.configdb import get_database_manager from pydantic import BaseModel from uuid import UUID +import logging + +logger = logging.getLogger(__name__) +router = APIRouter() router = APIRouter() @@ -11,7 +17,7 @@ class CreateUserRequest(BaseModel): telegram_id: int class UserResponse(BaseModel): - id: str + id: UUID telegram_id: int username: str balance: float @@ -44,6 +50,7 @@ async def create_user( raise HTTPException(status_code=500, detail=str(e)) + @router.get("/user/{telegram_id}", response_model=UserResponse, summary="Получить информацию о пользователе") async def get_user( telegram_id: int, @@ -53,11 +60,15 @@ async def get_user( Получение информации о пользователе. """ try: + logger.info(f"Получение пользователя с telegram_id: {telegram_id}") user = await db_manager.get_user_by_telegram_id(telegram_id) if not user: + logger.warning(f"Пользователь с telegram_id {telegram_id} не найден.") raise HTTPException(status_code=404, detail="User not found") - return UserResponse( + logger.info(f"Пользователь найден: ID={user.id}, Username={user.username}") + + user_response = UserResponse( id=user.id, telegram_id=user.telegram_id, username=user.username, @@ -65,9 +76,23 @@ async def get_user( created_at=user.created_at.isoformat(), updated_at=user.updated_at.isoformat() ) + logger.debug(f"Формирование ответа для пользователя: {user_response}") + + return user_response + + except HTTPException as http_ex: # Позволяет обработать HTTPException отдельно + raise http_ex + + except SQLAlchemyError as e: + logger.error(f"Ошибка базы данных при получении пользователя с telegram_id {telegram_id}: {e}") + raise HTTPException(status_code=500, detail="Database error") + except Exception as e: + logger.exception(f"Неожиданная ошибка при получении пользователя с telegram_id {telegram_id}: {e}") raise HTTPException(status_code=500, detail=str(e)) + + @router.post("/user/{telegram_id}/balance/{amount}", summary="Обновить баланс") async def update_balance( telegram_id: int, @@ -77,14 +102,26 @@ async def update_balance( """ Обновляет баланс пользователя. """ + logger.info(f"Получен запрос на обновление баланса: telegram_id={telegram_id}, amount={amount}") try: result = await db_manager.update_balance(telegram_id, amount) if result == "ERROR": + logger.error(f"Ошибка обновления баланса для пользователя {telegram_id}") raise HTTPException(status_code=500, detail="Failed to update balance") + + logger.info(f"Баланс пользователя {telegram_id} успешно обновлен на {amount}") return {"message": "Balance updated successfully"} + except HTTPException as http_ex: + logger.warning(f"HTTP ошибка: {http_ex.detail}") + raise http_ex + except SQLAlchemyError as db_ex: + logger.error(f"Ошибка базы данных при обновлении баланса пользователя {telegram_id}: {db_ex}") + raise HTTPException(status_code=500, detail="Database error") except Exception as e: + logger.exception(f"Неожиданная ошибка при обновлении баланса пользователя {telegram_id}: {e}") raise HTTPException(status_code=500, detail=str(e)) + @router.get("/user/{user_id}/transactions", summary="Последние транзакции пользователя") async def last_transactions( user_id: UUID, @@ -93,11 +130,17 @@ async def last_transactions( """ Возвращает список последних транзакций пользователя. """ + logger.info(f"Получен запрос на транзакции для пользователя: {user_id}") try: - transactions = await db_manager.last_transaction(user_id) + logger.debug(f"Вызов метода get_transaction с user_id={user_id}") + transactions = await db_manager.get_transaction(user_id) + if transactions == "ERROR": + logger.error(f"Ошибка при получении транзакций для пользователя: {user_id}") raise HTTPException(status_code=500, detail="Failed to fetch transactions") - return [ + + logger.debug(f"Транзакции для {user_id}: {transactions}") + response = [ { "id": tx.id, "amount": tx.amount, @@ -105,5 +148,17 @@ async def last_transactions( "transaction_type": tx.transaction_type, } for tx in transactions ] + logger.info(f"Формирование ответа для пользователя {user_id}: {response}") + return response + + except HTTPException as http_ex: + logger.warning(f"HTTP ошибка для {user_id}: {http_ex.detail}") + raise http_ex + + except SQLAlchemyError as db_ex: + logger.error(f"Ошибка базы данных для {user_id}: {db_ex}") + raise HTTPException(status_code=500, detail="Database error") + except Exception as e: + logger.exception(f"Неожиданная ошибка для {user_id}: {e}") raise HTTPException(status_code=500, detail=str(e)) diff --git a/app/services/db_manager.py b/app/services/db_manager.py index 0dd0dfe..107401c 100644 --- a/app/services/db_manager.py +++ b/app/services/db_manager.py @@ -1,168 +1,118 @@ -from instance.model import User, Subscription, Transaction, Administrators, SupportTicket,TicketMessage,TicketStatus -from sqlalchemy.future import select -from sqlalchemy.exc import SQLAlchemyError -from sqlalchemy import desc +from decimal import Decimal +import json +from instance.model import User, Subscription, Transaction, SupportTicket, TicketMessage, TicketStatus +from .xui_rep import PanelInteraction +from .postgres_rep import PostgresRepository +from .mongo_rep import MongoDBRepository +from instance.model import Transaction from dateutil.relativedelta import relativedelta from datetime import datetime -from .xui_rep import PanelInteraction -from .mongo_rep import MongoDBRepository import random import string import logging from uuid import UUID + class DatabaseManager: def __init__(self, session_generator): """ Инициализация с асинхронным генератором сессий (например, get_postgres_session). """ - self.session_generator = session_generator self.logger = logging.getLogger(__name__) self.mongo_repo = MongoDBRepository() + self.postgres_repo = PostgresRepository(session_generator, self.logger) async def create_user(self, telegram_id: int): """ - Создаёт нового пользователя, если его нет. + Создаёт пользователя. """ - async for session in self.session_generator(): - try: - username = self.generate_string(6) - result = await session.execute(select(User).where(User.telegram_id == int(telegram_id))) - user = result.scalars().first() - if not user: - new_user = User(telegram_id=int(telegram_id), username=username) - session.add(new_user) - await session.commit() - return new_user - return user - except SQLAlchemyError as e: - self.logger.error(f"Ошибка при создании пользователя {telegram_id}: {e}") - await session.rollback() - return "ERROR" + username = self.generate_string(6) + return await self.postgres_repo.create_user(telegram_id, username) async def get_user_by_telegram_id(self, telegram_id: int): """ Возвращает пользователя по Telegram ID. """ - async for session in self.session_generator(): - try: - result = await session.execute(select(User).where(User.telegram_id == telegram_id)) - return result.scalars().first() - except SQLAlchemyError as e: - self.logger.error(f"Ошибка при получении пользователя {telegram_id}: {e}") - return None + return await self.postgres_repo.get_user_by_telegram_id(telegram_id) - async def add_transaction(self, user_id: int, amount: float): + async def add_transaction(self, user_id: UUID, amount: float): """ - Добавляет транзакцию для пользователя. + Добавляет транзакцию. """ - async for session in self.session_generator(): - try: - transaction = Transaction(user_id=user_id, amount=amount) - session.add(transaction) - await session.commit() - except SQLAlchemyError as e: - self.logger.error(f"Ошибка добавления транзакции для пользователя {user_id}: {e}") - await session.rollback() + tran = Transaction( + user_id=user_id, + amount=Decimal(amount), + transaction_type="default" + ) + return await self.postgres_repo.add_record(tran) + + async def get_transaction(self, user_id: UUID, limit: int = 10): + """ + Возвращает транзакции. + """ + return await self.postgres_repo.get_last_transactions(user_id, limit) async def update_balance(self, telegram_id: int, amount: float): """ Обновляет баланс пользователя и добавляет транзакцию. """ - async for session in self.session_generator(): - try: - result = await session.execute(select(User).where(User.telegram_id == telegram_id)) - user = result.scalars().first() - if user: - user.balance += int(amount) - await self.add_transaction(user.id, amount) - await session.commit() - else: - self.logger.warning(f"Пользователь с Telegram ID {telegram_id} не найден.") - return "ERROR" - except SQLAlchemyError as e: - self.logger.error(f"Ошибка при обновлении баланса: {e}") - await session.rollback() - return "ERROR" - - async def last_subscriptions(self, user_id: str, limit: int = 10): - """ - Возвращает список последних подписок пользователя, ограниченный заданным количеством. - :param user_id: ID пользователя - :param limit: Максимальное количество подписок для возврата - """ - async for session in self.session_generator(): - try: - result = await session.execute( - select(Subscription) - .where(Subscription.user_id == str(user_id)) - .order_by(desc(Subscription.created_at)) - .limit(limit) # Ограничиваем количество результатов - ) - subscriptions = result.scalars().all() # Получаем все результаты до лимита - if subscriptions: - return subscriptions - else: - self.logger.info(f"Для пользователя {user_id} подписки не найдены.") - return [] # Возвращаем пустой список, если подписок нет - except SQLAlchemyError as e: - self.logger.error(f"Ошибка при получении подписок для пользователя {user_id}: {e}") - return "ERROR" + self.logger.info(f"Попытка обновления баланса: telegram_id={telegram_id}, amount={amount}") + user = await self.get_user_by_telegram_id(telegram_id) + if not user: + self.logger.warning(f"Пользователь с Telegram ID {telegram_id} не найден.") + return "ERROR" + + updated = await self.postgres_repo.update_balance(user, amount) + if not updated: + self.logger.error(f"Не удалось обновить баланс пользователя {telegram_id}") + return "ERROR" + + self.logger.info(f"Баланс пользователя {telegram_id} обновлен на {amount}, добавление транзакции") + await self.add_transaction(user.id, amount) + return "OK" + async def get_active_subscription(self, telegram_id: int): + """ + Проверяет наличие активной подписки. + """ + return await self.postgres_repo.get_active_subscription(telegram_id) - async def last_transaction(self, user_id: UUID): + async def get_last_subscriptions(self, user_id: UUID, limit: int ): """ - Возвращает список транзакций пользователя. + Возвращает список последних подписок. """ - async for session in self.session_generator(): - try: - result = await session.execute( - select(Transaction) - .where(Transaction.user_id == str(user_id)) - .order_by(desc(Transaction.created_at)) - ) - transactions = result.scalars().all() - return transactions - except SQLAlchemyError as e: - self.logger.error(f"Ошибка при получении транзакций пользователя {user_id}: {e}") - return "ERROR" + return await self.postgres_repo.get_last_subscription_by_user_id(user_id, limit) async def buy_sub(self, telegram_id: str, plan_id: str): - async for session in self.session_generator(): - try: - active_subscription = await self._check_active_subscription(telegram_id, session) - if active_subscription: - self.logger.error(f"Пользователь {telegram_id} уже имеет активную подписку.") - return "ACTIVE_SUBSCRIPTION_EXISTS" - result = await self._initialize_user_and_plan(telegram_id, plan_id) - if isinstance(result, str): - return result # Возвращает "ERROR", "TARIFF_NOT_FOUND" или "INSUFFICIENT_FUNDS" + """ + Покупает подписку. + """ + active_subscription = await self.get_active_subscription(telegram_id) + self.logger.info(f"{active_subscription}") + if active_subscription: + self.logger.error(f"Пользователь {telegram_id} уже имеет активную подписку.") + return "ACTIVE_SUBSCRIPTION_EXISTS" - user, plan = result - user.balance -= int(plan["price"]) - session.add(user) + result = await self._initialize_user_and_plan(telegram_id, plan_id) + if isinstance(result, str): + return result # Возвращает "ERROR", "TARIFF_NOT_FOUND" или "INSUFFICIENT_FUNDS" - new_subscription, server = await self._create_subscription_and_add_client(user, plan, session) - if not new_subscription: - await session.rollback() - return "ERROR" + user, plan = result + await self.postgres_repo.update_balance(user,-plan['price']) + new_subscription, server = await self._create_subscription_and_add_client(user, plan) - await session.commit() - self.logger.info(f"Подписка успешно оформлена для пользователя {telegram_id} на план {plan_id} и клиент добавлен на сервер.") - return "OK" + if not new_subscription: + return "ERROR" - except SQLAlchemyError as e: - self.logger.error(f"Ошибка при покупке подписки {plan_id} для пользователя {telegram_id}: {e}") - await session.rollback() - return "ERROR" - except Exception as e: - self.logger.error(f"Непредвиденная ошибка: {e}") - await session.rollback() - return "ERROR" + self.logger.info(f"Подписка успешно оформлена для пользователя {telegram_id}.") + return "OK" async def _initialize_user_and_plan(self, telegram_id, plan_id): - user = await self.create_user(telegram_id) + """ + Инициализирует пользователя и план подписки. + """ + user = await self.get_user_by_telegram_id(telegram_id) if not user: self.logger.error(f"Пользователь с Telegram ID {telegram_id} не найден.") return "ERROR" @@ -179,157 +129,134 @@ class DatabaseManager: return user, plan - async def _create_subscription_and_add_client(self, user, plan, session): + async def _create_subscription_and_add_client(self, user, plan): + """ + Создаёт подписку и добавляет клиента на сервер. + """ expiry_date = datetime.utcnow() + relativedelta(months=plan["duration_months"]) server = await self.mongo_repo.get_server_with_least_clients() - self.logger.info(f"Выбран сервер для подписки: {server}") + if not server: + self.logger.error("Нет доступных серверов для подписки.") + return None, None new_subscription = Subscription( user_id=user.id, - vpn_server_id=str(server['server']["name"]), + vpn_server_id=str(server["server"]["name"]), plan=plan["name"], - expiry_date=expiry_date + expiry_date=expiry_date, ) - session.add(new_subscription) - server_data = await self.mongo_repo.get_server(new_subscription.vpn_server_id) - if not server_data: - self.logger.error(f"Не удалось найти сервер с ID {new_subscription.vpn_server_id}.") - return None, None + panel = PanelInteraction( + base_url=f"https://{server['server']['ip']}:{server['server']['port']}/{server['server']['secretKey']}", + login_data={"username": server["server"]["login"], "password": server["server"]["password"]}, + logger=self.logger, + certificate=server["server"]["certificate"]["data"], + ) - server_info = server_data['server'] - url_base = f"https://{server_info['ip']}:{server_info['port']}/{server_info['secretKey']}" - login_data = { - 'username': server_info['login'], - 'password': server_info['password'], - } - - panel = PanelInteraction(url_base, login_data, self.logger, server_info['certificate']['data']) - expiry_date_iso = new_subscription.expiry_date.isoformat() - - response = await panel.add_client(1, expiry_date_iso, user.username) + response = await panel.add_client( + inbound_id=1, + expiry_date=expiry_date.isoformat(), + email=user.username, + ) if response != "OK": - self.logger.error(f"Ошибка при добавлении клиента {user.telegram_id} на сервер: {response}") + self.logger.error(f"Ошибка при добавлении клиента: {response}") return None, None + await self.postgres_repo.add_record(new_subscription) return new_subscription, server - - async def _check_active_subscription(self, telegram_id, session): + + + async def generate_uri(self, telegram_id: int): """ - Проверяет наличие активной подписки у пользователя. + Генерация URI для пользователя. :param telegram_id: Telegram ID пользователя. - :param session: Текущая сессия базы данных. - :return: Объект подписки или None. + :return: Строка URI или None в случае ошибки. """ try: - result = await session.execute( - select(Subscription) - .join(User, Subscription.user_id == User.id) - .where(User.telegram_id == telegram_id, Subscription.expiry_date > datetime.utcnow()) + # Извлечение данных + subscription = await self.postgres_repo.get_active_subscription(telegram_id) + if not subscription: + self.logger.error(f"Подписки для пользователя {telegram_id} не найдены.") + return "SUB_ERROR" + + server = await self.mongo_repo.get_server(subscription.vpn_server_id) + if not server: + self.logger.error(f"Сервер с ID {subscription.vpn_server_id} не найден в MongoDB.") + return None + + user = await self.postgres_repo.get_user_by_telegram_id(telegram_id) + if not user: + self.logger.error(f"Пользователь с telegram_id {telegram_id} не найден.") + return None + + email = user.username # Используем email из данных пользователя + + panel = PanelInteraction( + base_url=f"https://{server['server']['ip']}:{server['server']['port']}/{server['server']['secretKey']}", + login_data={"username": server["server"]["login"], "password": server["server"]["password"]}, + logger=self.logger, + certificate=server["server"]["certificate"]["data"], ) - return result.scalars().first() + + inbound_info = await panel.get_inbound_info(inbound_id=1) # Используем фиксированный ID + if not inbound_info: + self.logger.error(f"Не удалось получить информацию об инбаунде для ID {subscription.vpn_server_id}.") + return None + + # Логируем полученные данные + self.logger.info(f"Inbound Info: {inbound_info}") + + # Разбор JSON-строк + try: + stream_settings = json.loads(inbound_info["obj"]["streamSettings"]) + except KeyError as e: + self.logger.error(f"Ключ 'streamSettings' отсутствует: {e}") + return None + except json.JSONDecodeError as e: + self.logger.error(f"Ошибка разбора JSON для 'streamSettings': {e}") + return None + + settings = json.loads(inbound_info["obj"]["settings"]) # Разбираем JSON + + # Находим клиента по email + client = next((c for c in settings["clients"] if c["email"] == email), None) + if not client: + self.logger.error(f"Клиент с email {email} не найден среди клиентов.") + return None + + server_info = server["server"] + + # Преобразование данных в формат URI + uri = ( + f"vless://{client['id']}@{server_info['ip']}:443?" + f"type={stream_settings['network']}&security={stream_settings['security']}" + f"&pbk={stream_settings['realitySettings']['settings']['publicKey']}" + f"&fp={stream_settings['realitySettings']['settings']['fingerprint']}" + f"&sni={stream_settings['realitySettings']['serverNames'][0]}" + f"&sid={stream_settings['realitySettings']['shortIds'][0]}" + f"&spx=%2F&flow={client['flow']}" + f"#{inbound_info['obj']['remark']}-{client['email']}" + ) + + self.logger.info(f"Сформирован URI для пользователя {telegram_id}: {uri}") + return uri except Exception as e: - self.logger.error(f"Ошибка проверки активной подписки для пользователя {telegram_id}: {e}") + self.logger.error(f"Ошибка при генерации URI для пользователя {telegram_id}: {e}") return None + + - async def add_ticket_message(self, ticket_id: int, sender: str, message: str): + async def create_ticket(self, user_id: UUID, subject: str, message: str): """ - Добавляет сообщение к тикету. + Создаёт тикет """ - async for session in self.session_generator(): - try: - self.logger.info(f"Попытка добавления сообщения в тикет {ticket_id} от {sender}") - ticket_message = TicketMessage(ticket_id=ticket_id, sender=sender, message=message) - session.add(ticket_message) - await session.commit() - self.logger.info(f"Сообщение добавлено к тикету {ticket_id}: {message}") - return ticket_message - except SQLAlchemyError as e: - self.logger.error(f"Ошибка при добавлении сообщения в тикет {ticket_id}: {e}") - await session.rollback() - return None - - async def get_ticket_messages(self, ticket_id: int): - """ - Возвращает список сообщений для указанного тикета. - """ - async for session in self.session_generator(): - try: - self.logger.info(f"Получение сообщений для тикета {ticket_id}") - result = await session.execute( - select(TicketMessage).where(TicketMessage.ticket_id == ticket_id).order_by(TicketMessage.created_at) - ) - messages = result.scalars().all() - self.logger.info(f"Найдено {len(messages)} сообщений для тикета {ticket_id}") - return messages - except SQLAlchemyError as e: - self.logger.error(f"Ошибка при получении сообщений для тикета {ticket_id}: {e}") - return None - - async def create_ticket(self, user_id: int, subject: str, message: str): - """ - Создаёт новый тикет. - """ - async for session in self.session_generator(): - try: - self.logger.info(f"Создание тикета для пользователя {user_id}: {subject}") - ticket = SupportTicket(user_id=user_id, subject=subject, message=message) - session.add(ticket) - await session.commit() - self.logger.info(f"Тикет создан с ID {ticket.id} для пользователя {user_id}") - return ticket - except SQLAlchemyError as e: - self.logger.error(f"Ошибка при создании тикета: {e}") - await session.rollback() - return None - - async def list_tickets(self, user_id: int): - """ - Возвращает список тикетов пользователя. - """ - async for session in self.session_generator(): - try: - self.logger.info(f"Получение списка тикетов для пользователя {user_id}") - result = await session.execute( - select(SupportTicket).where(SupportTicket.user_id == user_id) - ) - tickets = result.scalars().all() - self.logger.info(f"Найдено {len(tickets)} тикетов для пользователя {user_id}") - return tickets - except SQLAlchemyError as e: - self.logger.error(f"Ошибка при получении тикетов для пользователя {user_id}: {e}") - return None - - async def update_ticket_status(self, ticket_id: int, status: TicketStatus): - """ - Обновляет статус тикета. - """ - async for session in self.session_generator(): - try: - self.logger.info(f"Попытка обновления статуса тикета {ticket_id} на {status}") - result = await session.execute( - select(SupportTicket).where(SupportTicket.id == ticket_id) - ) - ticket = result.scalars().first() - if ticket: - ticket.status = status - await session.commit() - self.logger.info(f"Статус тикета {ticket_id} обновлён на {status}") - return ticket - self.logger.warning(f"Тикет с ID {ticket_id} не найден для обновления статуса") - return None - except SQLAlchemyError as e: - self.logger.error(f"Ошибка при обновлении статуса тикета {ticket_id}: {e}") - await session.rollback() - return None - - + ticket = SupportTicket(user_id=user_id,subject=subject,message=message) + return await self.postgres_repo.add_record(ticket) @staticmethod def generate_string(length): """ Генерирует случайную строку заданной длины. """ - characters = string.ascii_lowercase + string.digits - return ''.join(random.choices(characters, k=length)) + return ''.join(random.choices(string.ascii_lowercase + string.digits, k=length)) diff --git a/app/services/postgres_rep.py b/app/services/postgres_rep.py index c67df1f..c683c24 100644 --- a/app/services/postgres_rep.py +++ b/app/services/postgres_rep.py @@ -1,7 +1,10 @@ +from datetime import datetime +from uuid import UUID from sqlalchemy.future import select from sqlalchemy.exc import SQLAlchemyError +from decimal import Decimal from sqlalchemy import desc -from instance.model import User, Subscription, Transaction +from instance.model import TicketMessage, User, Subscription, Transaction class PostgresRepository: @@ -23,6 +26,23 @@ class PostgresRepository: self.logger.error(f"Ошибка при создании пользователя {telegram_id}: {e}") await session.rollback() return None + async def get_active_subscription(self, telegram_id: int): + """ + Проверяет наличие активной подписки у пользователя. + """ + async for session in self.session_generator(): + try: + result = await session.execute( + select(Subscription) + .join(User, Subscription.user_id == User.id) + .where(User.telegram_id == telegram_id, Subscription.expiry_date > datetime.utcnow()) + ) + result= result.scalars().first() + self.logger.info(f"Пользователь с id {telegram_id}, проверен и имеет {result}") + return result + except SQLAlchemyError as e: + self.logger.error(f"Ошибка проверки активной подписки для пользователя {telegram_id}: {e}") + return None async def get_user_by_telegram_id(self, telegram_id: int): """ @@ -31,61 +51,40 @@ class PostgresRepository: async for session in self.session_generator(): try: result = await session.execute(select(User).where(User.telegram_id == telegram_id)) - return result.scalars().first() + if result: + return result.scalars().first() + return False except SQLAlchemyError as e: self.logger.error(f"Ошибка при получении пользователя {telegram_id}: {e}") - return None + return False + - async def add_transaction(self, user_id: int, amount: float): - """ - Добавляет транзакцию для пользователя. - """ - async for session in self.session_generator(): - try: - transaction = Transaction(user_id=user_id, amount=amount) - session.add(transaction) - await session.commit() - except SQLAlchemyError as e: - self.logger.error(f"Ошибка добавления транзакции для пользователя {user_id}: {e}") - await session.rollback() - - async def update_balance(self, telegram_id: int, amount: float): + async def update_balance(self, user: User, amount: float): """ Обновляет баланс пользователя. + + :param user: Объект пользователя. + :param amount: Сумма для добавления/вычитания. + :return: True, если успешно, иначе False. """ + self.logger.info(f"Обновление баланса пользователя: id={user.id}, current_balance={user.balance}, amount={amount}") async for session in self.session_generator(): try: - result = await session.execute(select(User).where(User.telegram_id == telegram_id)) - user = result.scalars().first() - if user: - user.balance += amount - await session.commit() - return user - else: - self.logger.warning(f"Пользователь с Telegram ID {telegram_id} не найден.") - return None + user = await session.get(User, user.id) # Загружаем пользователя в той же сессии + if not user: + self.logger.warning(f"Пользователь с ID {user.id} не найден.") + return False + # Приведение amount к Decimal + user.balance += Decimal(amount) + await session.commit() + self.logger.info(f"Баланс пользователя id={user.id} успешно обновлен: new_balance={user.balance}") + return True except SQLAlchemyError as e: - self.logger.error(f"Ошибка при обновлении баланса: {e}") + self.logger.error(f"Ошибка при обновлении баланса пользователя id={user.id}: {e}") await session.rollback() - return None + return False - async def last_subscription(self, user_id: int): - """ - Возвращает последние подписки пользователя. - """ - async for session in self.session_generator(): - try: - result = await session.execute( - select(Subscription) - .where(Subscription.user_id == user_id) - .order_by(desc(Subscription.created_at)) - ) - return result.scalars().all() - except SQLAlchemyError as e: - self.logger.error(f"Ошибка при получении последней подписки пользователя {user_id}: {e}") - return None - - async def last_transaction(self, user_id: int): + async def get_last_transactions(self, user_id: UUID, limit: int = 10): """ Возвращает последние транзакции пользователя. """ @@ -95,8 +94,50 @@ class PostgresRepository: select(Transaction) .where(Transaction.user_id == user_id) .order_by(desc(Transaction.created_at)) + .limit(limit) ) return result.scalars().all() except SQLAlchemyError as e: - self.logger.error(f"Ошибка при получении транзакций пользователя {user_id}: {e}") + self.logger.error(f"Ошибка получения транзакций пользователя {user_id}: {e}") return None + + async def get_last_subscription_by_user_id(self, user_id: UUID, limit: int = 1): + """ + Извлекает последнюю подписку пользователя на основании user_id. + + :param user_id: UUID пользователя. + :return: Объект Subscription или None. + """ + async for session in self.session_generator(): + try: + result = await session.execute( + select(Subscription) + .where(Subscription.user_id == user_id) + .order_by(desc(Subscription.created_at)) + .limit(limit) + ) + subscriptions = list(result.scalars()) + self.logger.info(f"Найдены такие подписки: {subscriptions}") + return subscriptions + except SQLAlchemyError as e: + self.logger.error(f"Ошибка при получении подписки для пользователя {user_id}: {e}") + return None + + async def add_record(self, record): + """ + Добавляет запись в базу данных. + + :param record: Объект записи. + :return: Запись или None в случае ошибки. + """ + async for session in self.session_generator(): + try: + session.add(record) + await session.commit() + return record + except SQLAlchemyError as e: + self.logger.error(f"Ошибка при добавлении записи: {record}: {e}") + await session.rollback() + return None + + diff --git a/app/services/xui_rep.py b/app/services/xui_rep.py index 48a1f89..26d5831 100644 --- a/app/services/xui_rep.py +++ b/app/services/xui_rep.py @@ -69,7 +69,7 @@ class PanelInteraction: self.logger.exception(f"Login request failed: {e}") raise - async def get_inbound_info(self, inbound_id): + async def get_inbound_info(self, inbound_id: int = 1): """ Fetch inbound information by ID. @@ -83,6 +83,8 @@ class PanelInteraction: async with session.get( url, headers=self.headers, ssl=self.ssl_context, timeout=10 ) as response: + response_text = await response.text() # Получаем текст ответа + self.logger.info(f"Inbound Info (raw): {response_text}") # Логируем сырой текст if response.status == 200: return await response.json() else: