Встроил марзбан в бекенд, исправил бывшие проблемы с получением активной подписки
This commit is contained in:
@@ -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 обработал её автоматически
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -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")
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -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)
|
||||||
|
|||||||
5
main.py
5
main.py
@@ -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():
|
||||||
|
|||||||
Reference in New Issue
Block a user