Встроил марзбан в бекенд, исправил бывшие проблемы с получением активной подписки

This commit is contained in:
root
2025-11-26 18:04:22 +03:00
parent e975bf4774
commit a001694608
6 changed files with 271 additions and 209 deletions

View File

@@ -1,4 +1,4 @@
from typing import List from typing import List, Optional
from fastapi import APIRouter, HTTPException, Depends from fastapi import APIRouter, HTTPException, Depends
from pydantic import BaseModel from pydantic import BaseModel
from app.services import DatabaseManager from app.services import DatabaseManager
@@ -15,15 +15,17 @@ router = APIRouter()
class BuySubscriptionRequest(BaseModel): class BuySubscriptionRequest(BaseModel):
telegram_id: int telegram_id: int
plan_id: str plan_name: str
class SubscriptionResponse(BaseModel): class SubscriptionResponse(BaseModel):
id: str id: str
plan: str user_id: int
vpn_server_id: str plan_name: str
expiry_date: str vpn_server_id: Optional[str]
status: str
start_date: str
end_date: str
created_at: str created_at: str
updated_at: str
# Эндпоинт для покупки подписки # Эндпоинт для покупки подписки
@router.post("/subscription/buy", response_model=dict) @router.post("/subscription/buy", response_model=dict)
@@ -35,23 +37,34 @@ async def buy_subscription(
Покупка подписки. Покупка подписки.
""" """
try: try:
result = await database_manager.buy_sub(request_data.telegram_id, request_data.plan_id) logger.info(f"Получен запрос на покупку подписки: {request_data.dict()}")
if result == "ERROR": result = await database_manager.buy_sub(request_data.telegram_id, request_data.plan_name)
raise HTTPException(status_code=500, detail="ERROR")
logger.info(f"Результат buy_sub: {result}")
if result == "ERROR" or result is None:
raise HTTPException(status_code=500, detail="Internal server error")
elif result == "INSUFFICIENT_FUNDS": 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": 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": elif result == "ACTIVE_SUBSCRIPTION_EXISTS":
raise HTTPException(status_code=400, detail="ACTIVE_SUBSCRIPTION_EXISTS") raise HTTPException(status_code=400, detail="ACTIVE_SUBSCRIPTION_EXISTS")
result = await database_manager.generate_uri(request_data.telegram_id)
return {"message": result} # Если успешно, генерируем URI
if isinstance(result, dict) and result.get('status') == 'OK':
uri_result = await database_manager.generate_uri(request_data.telegram_id)
logger.info(f"Результат генерации URI: {uri_result}")
return {"status": "success", "subscription_id": result.get('subscription_id'), "uri": uri_result[0]}
else:
return {"status": "success", "message": "Subscription created"}
except HTTPException as http_exc: except HTTPException as http_exc:
# Пропускаем HTTPException, чтобы FastAPI обработал его корректно logger.error(f"HTTPException в buy_subscription: {http_exc.detail}")
raise http_exc raise http_exc
except Exception as e: except Exception as e:
# Обрабатываем остальные исключения logger.error(f"Неожиданная ошибка в buy_subscription: {str(e)}")
raise HTTPException(status_code=500, detail=f"Unexpected error: {str(e)}") raise HTTPException(status_code=500, detail=f"Unexpected error: {str(e)}")
@@ -63,21 +76,24 @@ async def last_subscription(telegram_id: int, database_manager: DatabaseManager
""" """
logger.info(f"Получение последней подписки для пользователя: {telegram_id}") logger.info(f"Получение последней подписки для пользователя: {telegram_id}")
try: try:
subscriptions = await database_manager.get_last_subscriptions(telegram_id=telegram_id, limit=1) subscription = await database_manager.get_last_subscriptions(telegram_id=telegram_id)
if not subscriptions: plan = await database_manager.get_plan_by_id(subscription.plan_id)
if not subscription or not plan:
logger.warning(f"Подписки для пользователя {telegram_id} не найдены") logger.warning(f"Подписки для пользователя {telegram_id} не найдены")
raise HTTPException(status_code=404, detail="No subscriptions found") raise HTTPException(status_code=404, detail="No subscriptions found")
sub = subscriptions[0]
return { return {
"id": sub.id, "id": str(subscription.id),
"plan": sub.plan, "user_id": subscription.user_id,
"vpn_server_id": sub.vpn_server_id, "plan_name": plan.name,
"expiry_date": sub.expiry_date.isoformat(), "vpn_server_id": subscription.vpn_server_id,
"created_at": sub.created_at.isoformat(), "status": subscription.status.value,
"updated_at": sub.updated_at.isoformat(), "start_date": subscription.start_date.isoformat(),
"end_date": subscription.end_date.isoformat(),
"created_at": subscription.created_at.isoformat(),
} }
except SQLAlchemyError as e: except SQLAlchemyError as e:
logger.error(f"Ошибка базы данных при получении подписки для пользователя {telegram_id}: {e}") logger.error(f"Ошибка базы данных при получении подписки для пользователя {telegram_id}: {e}")
@@ -135,10 +151,10 @@ async def get_uri(telegram_id: int, database_manager: DatabaseManager = Depends(
if uri == "SUB_ERROR": if uri == "SUB_ERROR":
raise HTTPException(status_code=404, detail="SUB_ERROR") raise HTTPException(status_code=404, detail="SUB_ERROR")
if not uri: if not uri:
logger.warning(f"Не удалось сгенерировать URI для пользователя с telegram_id {telegram_id}") logger.warning(f"Не удалось сгенерировать URI для пользователя с telegram_id {telegram_id}, данные -> {uri}")
raise HTTPException(status_code=404, detail="URI not found") raise HTTPException(status_code=404, detail="URI not found")
return {"detail": uri} return {"detail": uri[0]}
except HTTPException as e: except HTTPException as e:
# Пропускаем HTTPException, чтобы FastAPI обработал её автоматически # Пропускаем HTTPException, чтобы FastAPI обработал её автоматически

View File

@@ -1,11 +1,11 @@
from decimal import Decimal from decimal import Decimal
import json import json
from instance.model import User, Subscription, Transaction from instance.model import User, Subscription, Transaction
from app.services.marzban import MarzbanService from app.services.marzban import MarzbanService, MarzbanUser
from .postgres_rep import PostgresRepository from .postgres_rep import PostgresRepository
from instance.model import Transaction,TransactionType from instance.model import Transaction,TransactionType, Plan
from dateutil.relativedelta import relativedelta from dateutil.relativedelta import relativedelta
from datetime import datetime from datetime import datetime, timezone
import random import random
import string import string
from typing import Optional from typing import Optional
@@ -14,12 +14,13 @@ from uuid import UUID
class DatabaseManager: class DatabaseManager:
def __init__(self, session_generator): def __init__(self, session_generator,marzban_username,marzban_password,marzban_url):
""" """
Инициализация с асинхронным генератором сессий (например, get_postgres_session). Инициализация с асинхронным генератором сессий (например, get_postgres_session).
""" """
self.logger = logging.getLogger(__name__) self.logger = logging.getLogger(__name__)
self.postgres_repo = PostgresRepository(session_generator, self.logger) self.postgres_repo = PostgresRepository(session_generator, self.logger)
self.marzban_service = MarzbanService(marzban_url,marzban_username,marzban_password)
async def create_user(self, telegram_id: int, invented_by: Optional[int]= None): async def create_user(self, telegram_id: int, invented_by: Optional[int]= None):
""" """
@@ -82,98 +83,131 @@ class DatabaseManager:
""" """
Проверяет наличие активной подписки. Проверяет наличие активной подписки.
""" """
try:
return await self.postgres_repo.get_active_subscription(telegram_id) return await self.postgres_repo.get_active_subscription(telegram_id)
except Exception as e:
self.logger.error(f"Неожиданная ошибка в get_active_subscription: {str(e)}")
return "ERROR"
async def get_plan_by_id(self, plan_id):
"""
Ищет по названию плана.
"""
try:
return await self.postgres_repo.get_plan_by_id(plan_id)
except Exception as e:
self.logger.error(f"Неожиданная ошибка в get_plan_by_name: {str(e)}")
return None
async def get_last_subscriptions(self, telegram_id: int, limit: int = 1): async def get_last_subscriptions(self, telegram_id: int, limit: int = 1):
""" """
Возвращает список последних подписок. Возвращает список последних подписок.
""" """
return await self.postgres_repo.get_last_subscription_by_user_id(telegram_id, limit) return await self.postgres_repo.get_last_subscription_by_user_id(telegram_id)
async def buy_sub(self, telegram_id: int, plan_id: str): async def buy_sub(self, telegram_id: int, plan_name: str):
""" """
Покупает подписку. Покупает подписку.
""" """
# active_subscription = await self.get_active_subscription(telegram_id) try:
# self.logger.info(f"{active_subscription}") self.logger.info(f"Начало покупки подписки для пользователя {telegram_id}, план: {plan_name}")
# if active_subscription: active_subscription = await self.get_active_subscription(telegram_id)
# self.logger.error(f"Пользователь {telegram_id} уже имеет активную подписку.") self.logger.info(f"Активная подписка: {active_subscription}")
# return "ACTIVE_SUBSCRIPTION_EXISTS"
# result = await self._initialize_user_and_plan(telegram_id, plan_id) if active_subscription:
# if isinstance(result, str): self.logger.error(f"Пользователь {telegram_id} уже имеет активную подписку.")
# return result # Возвращает "ERROR", "TARIFF_NOT_FOUND" или "INSUFFICIENT_FUNDS" return "ACTIVE_SUBSCRIPTION_EXISTS"
# user, plan = result result = await self._initialize_user_and_plan(telegram_id, plan_name)
# await self.postgres_repo.update_balance(user,-plan['price']) if isinstance(result, str):
# new_subscription, server = await self._create_subscription_and_add_client(user, plan) return result # Возвращает "ERROR", "TARIFF_NOT_FOUND" или "INSUFFICIENT_FUNDS"
# if not new_subscription: user, plan = result
# return "ERROR" self.logger.info(f"Пользователь и план найдены: user_id={user.telegram_id}, plan_price={plan.price}")
# self.logger.info(f"Подписка успешно оформлена для пользователя {telegram_id}.") new_subscription = await self._create_subscription_and_add_client(user, plan)
# return "OK" if not new_subscription:
pass self.logger.error(f"Не удалось создать подписку для пользователя {telegram_id}")
return "ERROR"
async def _initialize_user_and_plan(self, telegram_id, plan_id): updated = await self.postgres_repo.update_balance(user,-plan.price)
if updated == False:
self.logger.error(f"Не удалось обновить баланс для пользователя {telegram_id}")
return "ERROR"
self.logger.info(f"Подписка успешно оформлена для пользователя {telegram_id}.")
return {"status": "OK", "subscription_id": str(new_subscription.id)}
except Exception as e:
self.logger.error(f"Неожиданная ошибка в buy_sub: {str(e)}")
return "ERROR"
async def _initialize_user_and_plan(self, telegram_id, plan_name):
""" """
Инициализирует пользователя и план подписки. Инициализирует пользователя и план подписки.
""" """
# user = await self.get_user_by_telegram_id(telegram_id) try:
# if not user:
# self.logger.error(f"Пользователь с Telegram ID {telegram_id} не найден.")
# return "ERROR"
# plan = await self.mongo_repo.get_subscription_plan(plan_id) user = await self.get_user_by_telegram_id(telegram_id)
# if not plan: if not user:
# self.logger.error(f"Тарифный план {plan_id} не найден.") self.logger.error(f"Пользователь с Telegram ID {telegram_id} не найден.")
# return "TARIFF_NOT_FOUND" return "ERROR"
# cost = int(plan["price"]) plan = await self.postgres_repo.get_subscription_plan(plan_name)
# if user.balance < cost: if not plan:
# self.logger.error(f"Недостаточно средств у пользователя {telegram_id} для покупки плана {plan_id}.") self.logger.error(f"Тарифный план {plan_name} не найден.")
# return "INSUFFICIENT_FUNDS" return "TARIFF_NOT_FOUND"
# return user, plan cost = plan.price
pass if user.balance < cost:
self.logger.error(f"Недостаточно средств у пользователя {telegram_id} для покупки плана {plan_name}.")
return "INSUFFICIENT_FUNDS"
async def _create_subscription_and_add_client(self, user, plan): return user, plan
""" except Exception as e:
Создаёт подписку и добавляет клиента на сервер. self.logger.error(f"Неожиданная ошибка в _initialize_user_and_plan: {str(e)}")
""" return "ERROR"
# expiry_date = datetime.utcnow() + relativedelta(months=plan["duration_months"])
# server = await self.mongo_repo.get_server_with_least_clients()
# if not server:
# self.logger.error("Нет доступных серверов для подписки.")
# return None, None
# new_subscription = Subscription( async def _create_subscription_and_add_client(self, user: User, plan: Plan):
# user_id=user.id, """Создаёт подписку и добавляет клиента на сервер."""
# vpn_server_id=str(server["server"]["name"]), try:
# plan=plan["name"], self.logger.info(f"Создание подписки для user_id={user.telegram_id}, plan={plan.name}")
# expiry_date=expiry_date,
# )
# panel = PanelInteraction( # Проверяем типы объектов
# base_url=f"https://{server['server']['ip']}:{server['server']['port']}/{server['server']['secretKey']}", self.logger.info(f"Тип user: {type(user)}, тип plan: {type(plan)}")
# login_data={"username": server["server"]["login"], "password": server["server"]["password"]},
# logger=self.logger,
# certificate=server["server"]["certificate"]["data"],
# )
# response = await panel.add_client( expiry_date = datetime.utcnow() + relativedelta(days=plan.duration_days)
# inbound_id=1,
# expiry_date=expiry_date.isoformat(),
# email=user.username,
# )
# if response != "OK":
# self.logger.error(f"Ошибка при добавлении клиента: {response}")
# return None, None
# await self.postgres_repo.add_record(new_subscription)
# return new_subscription, server new_subscription = Subscription(
pass user_id=user.telegram_id,
vpn_server_id="BASE SERVER NEED TO UPDATE",
plan_id=plan.id,
end_date=expiry_date,
start_date=datetime.utcnow()
)
self.logger.info(f"Создан объект подписки: {new_subscription}")
response = await self.marzban_service.create_user(user, new_subscription)
self.logger.info(f"Ответ от Marzban: {response}")
if response == "USER_ALREADY_EXISTS":
response = await self.marzban_service.get_user_status(user)
result = await self.marzban_service.update_user(user, new_subscription)
# if not isinstance(response,MarzbanUser) or not isinstance(result,MarzbanUser):
# self.logger.error(f"Ошибка при добавлении клиента: {response}, {result}")
# return None
await self.postgres_repo.add_record(new_subscription)
self.logger.info(f"Подписка сохранена в БД с ID: {new_subscription.id}")
return new_subscription
except Exception as e:
self.logger.error(f"Неожиданная ошибка в _create_subscription_and_add_client: {str(e)}")
import traceback
self.logger.error(f"Трассировка: {traceback.format_exc()}")
return None
async def generate_uri(self, telegram_id: int): async def generate_uri(self, telegram_id: int):
""" """
@@ -182,78 +216,24 @@ class DatabaseManager:
:param telegram_id: Telegram ID пользователя. :param telegram_id: Telegram ID пользователя.
:return: Строка URI или None в случае ошибки. :return: Строка URI или None в случае ошибки.
""" """
# try: try:
# # Извлечение данных user = await self.get_user_by_telegram_id(telegram_id)
# subscription = await self.postgres_repo.get_active_subscription(telegram_id) if user == False or user == None:
# if not subscription: self.logger.error(f"Ошибка при получении клиента: user = {user}")
# self.logger.error(f"Подписки для пользователя {telegram_id} не найдены.") return "ERROR"
# return "SUB_ERROR"
# server = await self.mongo_repo.get_server(subscription.vpn_server_id) result = await self.marzban_service.get_config_links(user)
# if not server: if result == None:
# self.logger.error(f"Сервер с ID {subscription.vpn_server_id} не найден в MongoDB.") self.logger.error(f"Ошибка при получении ссылки клиента: result = {user}")
# return None return "ERROR"
# user = await self.postgres_repo.get_user_by_telegram_id(telegram_id) self.logger.info(f"Итог generate_uri: result = {result}")
# if not user:
# self.logger.error(f"Пользователь с telegram_id {telegram_id} не найден.")
# return None
# email = user.username # Используем email из данных пользователя return result
except Exception as e:
self.logger.error(f"Неожиданная ошибка в generate_uri: {str(e)}")
return "ERROR"
# 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"],
# )
# 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"Ошибка при генерации URI для пользователя {telegram_id}: {e}")
# return None
pass
@staticmethod @staticmethod
def generate_string(length): def generate_string(length):
@@ -261,3 +241,11 @@ class DatabaseManager:
Генерирует случайную строку заданной длины. Генерирует случайную строку заданной длины.
""" """
return ''.join(random.choices(string.ascii_lowercase + string.digits, k=length)) return ''.join(random.choices(string.ascii_lowercase + string.digits, k=length))
def _is_subscription_expired(self, expire_timestamp: int) -> bool:
"""Проверяет, истекла ли подписка"""
current_time = datetime.now(timezone.utc)
expire_time = datetime.fromtimestamp(expire_timestamp, tz=timezone.utc)
return expire_time < current_time

View File

@@ -5,6 +5,10 @@ from datetime import date, datetime, time, timezone
import logging import logging
from instance import User, Subscription from instance import User, Subscription
class UserAlreadyExistsError(Exception):
"""Пользователь уже существует в системе"""
pass
class MarzbanUser: class MarzbanUser:
"""Модель пользователя Marzban""" """Модель пользователя Marzban"""
def __init__(self, data: Dict[str, Any]): def __init__(self, data: Dict[str, Any]):
@@ -78,12 +82,14 @@ class MarzbanService:
) as response: ) as response:
response_data = await response.json() if response.content_length else {} response_data = await response.json() if response.content_length else {}
if response.status == 409:
raise UserAlreadyExistsError(f"User already exists: {response_data}")
if response.status not in (200, 201): if response.status not in (200, 201):
raise Exception(f"HTTP {response.status}: {response_data}") raise Exception(f"HTTP {response.status}: {response_data}")
return response_data return response_data
except UserAlreadyExistsError:
raise # Пробрасываем наверх
except aiohttp.ClientError as e: except aiohttp.ClientError as e:
logging.error(f"Network error during {method.upper()} to {url}: {e}") logging.error(f"Network error during {method.upper()} to {url}: {e}")
raise raise
@@ -91,9 +97,9 @@ class MarzbanService:
logging.error(f"Unexpected error during request to {url}: {e}") logging.error(f"Unexpected error during request to {url}: {e}")
raise raise
async def create_user(self, user: User, subscription: Subscription) -> MarzbanUser: async def create_user(self, user: User, subscription: Subscription) -> str | MarzbanUser:
"""Создает нового пользователя в Marzban""" """Создает нового пользователя в Marzban"""
username = f"user_{user.telegram_id}" logging.info(f"Конец подписки пользователя {user.telegram_id} {subscription.end_date}")
if subscription.end_date: if subscription.end_date:
if isinstance(subscription.end_date, datetime): if isinstance(subscription.end_date, datetime):
if subscription.end_date.tzinfo is None: if subscription.end_date.tzinfo is None:
@@ -114,7 +120,7 @@ class MarzbanService:
expire_timestamp = 0 expire_timestamp = 0
data = { data = {
"username": username, "username": user.username,
"status": "active", "status": "active",
"expire": expire_timestamp, "expire": expire_timestamp,
"data_limit": 100 * 1073741824, # Конвертируем GB в bytes "data_limit": 100 * 1073741824, # Конвертируем GB в bytes
@@ -139,15 +145,18 @@ class MarzbanService:
try: try:
response_data = await self._make_request("/api/user", "post", data) response_data = await self._make_request("/api/user", "post", data)
marzban_user = MarzbanUser(response_data) marzban_user = MarzbanUser(response_data)
logging.info(f"User {username} created successfully") logging.info(f"Пользователь {user.username} успешно создан в Marzban")
return marzban_user return marzban_user
except UserAlreadyExistsError:
logging.warning(f"Пользователь {user.telegram_id} уже существует в Marzban")
return "USER_ALREADY_EXISTS"
except Exception as e: except Exception as e:
logging.error(f"Failed to create user {username}: {e}") logging.error(f"Failed to create user {user.username}: {e}")
raise Exception(f"Failed to create user: {e}") raise Exception(f"Failed to create user: {e}")
async def update_user(self, user: User, subscription: Subscription) -> MarzbanUser: async def update_user(self, user: User, subscription: Subscription) -> MarzbanUser:
"""Обновляет существующего пользователя""" """Обновляет существующего пользователя"""
username = f"user_{user.telegram_id}" username = user.username
if subscription.end_date: if subscription.end_date:
if isinstance(subscription.end_date, datetime): if isinstance(subscription.end_date, datetime):
@@ -186,7 +195,7 @@ class MarzbanService:
async def disable_user(self, user: User) -> bool: async def disable_user(self, user: User) -> bool:
"""Отключает пользователя""" """Отключает пользователя"""
username = f"user_{user.telegram_id}" username = user.username
data = { data = {
"status": "disabled" "status": "disabled"
@@ -202,7 +211,7 @@ class MarzbanService:
async def enable_user(self, user: User) -> bool: async def enable_user(self, user: User) -> bool:
"""Включает пользователя""" """Включает пользователя"""
username = f"user_{user.telegram_id}" username = user.username
data = { data = {
"status": "active" "status": "active"
@@ -218,41 +227,38 @@ class MarzbanService:
async def delete_user(self, user: User) -> bool: async def delete_user(self, user: User) -> bool:
"""Полностью удаляет пользователя из Marzban""" """Полностью удаляет пользователя из Marzban"""
username = f"user_{user.telegram_id}"
try: try:
await self._make_request(f"/api/user/{username}", "delete") await self._make_request(f"/api/user/{user.username}", "delete")
logging.info(f"User {username} deleted successfully") logging.info(f"User {user.username} deleted successfully")
return True return True
except Exception as e: except Exception as e:
logging.error(f"Failed to delete user {username}: {e}") logging.error(f"Failed to delete user {user.username}: {e}")
return False return False
async def get_user_status(self, user: User) -> UserStatus: async def get_user_status(self, user: User) -> UserStatus:
"""Получает текущий статус пользователя""" """Получает текущий статус пользователя"""
username = f"user_{user.telegram_id}"
try: try:
response_data = await self._make_request(f"/api/user/{username}", "get") response_data = await self._make_request(f"/api/user/{user.username}", "get")
return UserStatus(response_data) return UserStatus(response_data)
except Exception as e: except Exception as e:
logging.error(f"Failed to get status for user {username}: {e}") logging.error(f"Failed to get status for user {user.username}: {e}")
raise Exception(f"Failed to get user status: {e}") raise Exception(f"Failed to get user status: {e}")
async def get_subscription_url(self, user: User) -> str: async def get_subscription_url(self, user: User) -> str | None:
"""Возвращает готовую subscription_url для подключения""" """Возвращает готовую subscription_url для подключения"""
username = f"user_{user.telegram_id}"
try: try:
response_data = await self._make_request(f"/api/user/{username}", "get") response_data = await self._make_request(f"/api/user/{user.username}", "get")
return response_data.get('subscription_url', '') return response_data.get('subscription_url', '')
except Exception as e: except Exception as e:
logging.error(f"Failed to get subscription URL for user {username}: {e}") logging.error(f"Failed to get subscription URL for user {user.username}: {e}")
return "" return None
async def get_config_links(self, user: User) -> str: async def get_config_links(self, user: User) -> str:
"""Возвращает конфигурации для подключения""" """Возвращает конфигурации для подключения"""
username = f"user_{user.telegram_id}" username = user.username
try: try:
response_data = await self._make_request(f"/api/user/{username}", "get") response_data = await self._make_request(f"/api/user/{username}", "get")

View File

@@ -6,7 +6,7 @@ from sqlalchemy.exc import SQLAlchemyError
from decimal import Decimal from decimal import Decimal
from sqlalchemy import asc, desc, update from sqlalchemy import asc, desc, update
from sqlalchemy.orm import joinedload from sqlalchemy.orm import joinedload
from instance.model import Referral, User, Subscription, Transaction from instance.model import Referral, User, Subscription, Transaction, Plan
class PostgresRepository: class PostgresRepository:
@@ -37,12 +37,18 @@ class PostgresRepository:
try: try:
result = await session.execute( result = await session.execute(
select(Subscription) select(Subscription)
.join(User, Subscription.user_id == User.id) .join(User, Subscription.user_id == User.telegram_id)
.where(User.telegram_id == telegram_id, Subscription.expiry_date > datetime.utcnow()) .where(User.telegram_id == telegram_id, Subscription.end_date > datetime.utcnow())
) )
result= result.scalars().first() subscription = result.scalars().first()
self.logger.info(f"Пользователь с id {telegram_id}, проверен и имеет {result}") if subscription:
return result # Отделяем объект от сессии
session.expunge(subscription)
self.logger.info(f"Пользователь с id {telegram_id}, проверен и имеет подписку ID: {subscription.id}")
else:
self.logger.info(f"Пользователь с id {telegram_id}, проверен и имеет None")
return subscription
except SQLAlchemyError as e: except SQLAlchemyError as e:
self.logger.error(f"Ошибка проверки активной подписки для пользователя {telegram_id}: {e}") self.logger.error(f"Ошибка проверки активной подписки для пользователя {telegram_id}: {e}")
return None return None
@@ -104,7 +110,7 @@ class PostgresRepository:
self.logger.error(f"Ошибка получения транзакций пользователя {user_telegram_id}: {e}") self.logger.error(f"Ошибка получения транзакций пользователя {user_telegram_id}: {e}")
return None return None
async def get_last_subscription_by_user_id(self, user_telegram_id: int, limit: int = 1): async def get_last_subscription_by_user_id(self, user_telegram_id: int):
""" """
Извлекает последнюю подписку пользователя на основании user_id. Извлекает последнюю подписку пользователя на основании user_id.
@@ -117,12 +123,19 @@ class PostgresRepository:
select(Subscription) select(Subscription)
.where(Subscription.user_id == user_telegram_id) .where(Subscription.user_id == user_telegram_id)
.order_by(desc(Subscription.created_at)) .order_by(desc(Subscription.created_at))
.limit(limit) .limit(1)
) )
subscriptions = list(result.scalars()) subscription = result.scalars().first()
result.scalars() self.logger.info(f"Найдены такие подписки: {subscription}")
self.logger.info(f"Найдены такие подписки: {subscriptions}")
return subscriptions if subscription:
session.expunge(subscription)
self.logger.info(f"Найдена подписка ID: {subscription.id} для пользователя {user_telegram_id}")
return subscription
else:
return None
except SQLAlchemyError as e: except SQLAlchemyError as e:
self.logger.error(f"Ошибка при получении подписки для пользователя {user_telegram_id}: {e}") self.logger.error(f"Ошибка при получении подписки для пользователя {user_telegram_id}: {e}")
return None return None
@@ -142,7 +155,7 @@ class PostgresRepository:
except SQLAlchemyError as e: except SQLAlchemyError as e:
self.logger.error(f"Ошибка при добавлении записи: {record}: {e}") self.logger.error(f"Ошибка при добавлении записи: {record}: {e}")
await session.rollback() await session.rollback()
return None raise Exception
async def add_referral(self, referrer_id: int, referral_id: int): async def add_referral(self, referrer_id: int, referral_id: int):
""" """
@@ -199,3 +212,41 @@ class PostgresRepository:
await session.rollback() await session.rollback()
self.logger.error(f"Ошибка при добавлении реферальной связи: {str(e)}") self.logger.error(f"Ошибка при добавлении реферальной связи: {str(e)}")
raise raise
async def get_subscription_plan(self, plan_name:str) -> Plan | None:
"""
Поиск плана для подписки
:param plan_name: Объект записи.
:return: Запись или None в случае ошибки.
"""
async for session in self.session_generator():
try:
result = await session.execute(
select(Plan)
.where(Plan.name == plan_name)
)
return result.scalar_one_or_none()
except SQLAlchemyError as e:
self.logger.error(f"Ошибка при поиске плана: {plan_name}: {e}")
await session.rollback()
return Noneэ
async def get_plan_by_id(self, plan_id: int) -> Plan | None:
"""
Поиск плана для подписки
:param plan_name: Объект записи.
:return: Запись или None в случае ошибки.
"""
async for session in self.session_generator():
try:
result = await session.execute(
select(Plan)
.where(Plan.id == plan_id)
)
return result.scalar_one_or_none()
except SQLAlchemyError as e:
self.logger.error(f"Ошибка при поиске плана: {plan_id}: {e}")
await session.rollback()
return None

View File

@@ -6,9 +6,11 @@ from .model import Base
try: try:
# Настройки PostgreSQL из переменных окружения # Настройки PostgreSQL из переменных окружения
POSTGRES_DSN = os.getenv("POSTGRES_URL") POSTGRES_DSN = os.getenv("POSTGRES_URL")
BASE_URL_MARZBAN = os.getenv("BASE_URL_MARZBAN")
USERNAME_MARZBA = os.getenv('USERNAME_MARZBAN')
PASSWORD_MARZBAN = os.getenv('PASSWORD_MARZBAN')
# Создание движка для PostgreSQL # Создание движка для PostgreSQL
if POSTGRES_DSN is None: if POSTGRES_DSN is None or BASE_URL_MARZBAN is None or USERNAME_MARZBA is None or PASSWORD_MARZBAN is None:
raise Exception raise Exception
postgres_engine = create_async_engine(POSTGRES_DSN, echo=False) postgres_engine = create_async_engine(POSTGRES_DSN, echo=False)
except Exception as e: except Exception as e:
@@ -49,4 +51,4 @@ def get_database_manager() -> DatabaseManager:
""" """
Функция-зависимость для получения экземпляра DatabaseManager. Функция-зависимость для получения экземпляра DatabaseManager.
""" """
return DatabaseManager(get_postgres_session) return DatabaseManager(get_postgres_session, USERNAME_MARZBA,PASSWORD_MARZBAN,BASE_URL_MARZBAN)

View File

@@ -1,3 +1,4 @@
import os
import sys import sys
from fastapi import FastAPI from fastapi import FastAPI
from instance import setup_logging from instance import setup_logging
@@ -10,15 +11,13 @@ setup_logging()
# force=True # force=True
# ) # )
from instance import init_postgresql, close_connections, get_postgres_session from instance import init_postgresql, close_connections
from app.routes import router, subscription_router from app.routes import router, subscription_router
from app.services import DatabaseManager
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
app = FastAPI() app = FastAPI()
database_manager = DatabaseManager(session_generator=get_postgres_session)
@app.on_event("startup") @app.on_event("startup")
async def startup(): async def startup():