mirror of
https://github.com/BEDOLAGA-DEV/remnawave-bedolaga-telegram-bot.git
synced 2026-02-11 14:40:30 +00:00
340 lines
12 KiB
Python
340 lines
12 KiB
Python
import logging
|
||
from dataclasses import dataclass
|
||
from typing import List, Optional
|
||
|
||
from sqlalchemy.ext.asyncio import AsyncSession
|
||
|
||
from app.config import settings
|
||
from app.database.crud.campaign import record_campaign_registration
|
||
from app.database.crud.subscription import (
|
||
create_paid_subscription,
|
||
get_subscription_by_user_id,
|
||
)
|
||
from app.database.crud.user import add_user_balance
|
||
from app.database.crud.tariff import get_tariff_by_id
|
||
from app.database.models import AdvertisingCampaign, User
|
||
from app.services.subscription_service import SubscriptionService
|
||
|
||
logger = logging.getLogger(__name__)
|
||
|
||
|
||
@dataclass
|
||
class CampaignBonusResult:
|
||
success: bool
|
||
bonus_type: Optional[str] = None
|
||
balance_kopeks: int = 0
|
||
subscription_days: Optional[int] = None
|
||
subscription_traffic_gb: Optional[int] = None
|
||
subscription_device_limit: Optional[int] = None
|
||
subscription_squads: Optional[List[str]] = None
|
||
# Поля для tariff
|
||
tariff_id: Optional[int] = None
|
||
tariff_name: Optional[str] = None
|
||
tariff_duration_days: Optional[int] = None
|
||
|
||
|
||
class AdvertisingCampaignService:
|
||
def __init__(self) -> None:
|
||
self.subscription_service = SubscriptionService()
|
||
|
||
async def apply_campaign_bonus(
|
||
self,
|
||
db: AsyncSession,
|
||
user: User,
|
||
campaign: AdvertisingCampaign,
|
||
) -> CampaignBonusResult:
|
||
if not campaign.is_active:
|
||
logger.warning(
|
||
"⚠️ Попытка выдать бонус по неактивной кампании %s", campaign.id
|
||
)
|
||
return CampaignBonusResult(success=False)
|
||
|
||
if campaign.is_balance_bonus:
|
||
return await self._apply_balance_bonus(db, user, campaign)
|
||
|
||
if campaign.is_subscription_bonus:
|
||
return await self._apply_subscription_bonus(db, user, campaign)
|
||
|
||
if campaign.is_none_bonus:
|
||
return await self._apply_none_bonus(db, user, campaign)
|
||
|
||
if campaign.is_tariff_bonus:
|
||
return await self._apply_tariff_bonus(db, user, campaign)
|
||
|
||
logger.error("❌ Неизвестный тип бонуса кампании: %s", campaign.bonus_type)
|
||
return CampaignBonusResult(success=False)
|
||
|
||
async def _apply_balance_bonus(
|
||
self,
|
||
db: AsyncSession,
|
||
user: User,
|
||
campaign: AdvertisingCampaign,
|
||
) -> CampaignBonusResult:
|
||
amount = campaign.balance_bonus_kopeks or 0
|
||
if amount <= 0:
|
||
logger.info("ℹ️ Кампания %s не имеет бонуса на баланс", campaign.id)
|
||
return CampaignBonusResult(success=False)
|
||
|
||
description = f"Бонус за регистрацию по кампании '{campaign.name}'"
|
||
success = await add_user_balance(
|
||
db,
|
||
user,
|
||
amount,
|
||
description=description,
|
||
)
|
||
|
||
if not success:
|
||
return CampaignBonusResult(success=False)
|
||
|
||
await record_campaign_registration(
|
||
db,
|
||
campaign_id=campaign.id,
|
||
user_id=user.id,
|
||
bonus_type="balance",
|
||
balance_bonus_kopeks=amount,
|
||
)
|
||
|
||
logger.info(
|
||
"💰 Пользователю %s начислен бонус %s₽ по кампании %s",
|
||
user.telegram_id,
|
||
amount / 100,
|
||
campaign.id,
|
||
)
|
||
|
||
return CampaignBonusResult(
|
||
success=True,
|
||
bonus_type="balance",
|
||
balance_kopeks=amount,
|
||
)
|
||
|
||
async def _apply_subscription_bonus(
|
||
self,
|
||
db: AsyncSession,
|
||
user: User,
|
||
campaign: AdvertisingCampaign,
|
||
) -> CampaignBonusResult:
|
||
existing_subscription = await get_subscription_by_user_id(db, user.id)
|
||
if existing_subscription:
|
||
logger.warning(
|
||
"⚠️ У пользователя %s уже есть подписка, бонус кампании %s пропущен",
|
||
user.telegram_id,
|
||
campaign.id,
|
||
)
|
||
return CampaignBonusResult(success=False)
|
||
|
||
duration_days = campaign.subscription_duration_days or 0
|
||
if duration_days <= 0:
|
||
logger.info(
|
||
"ℹ️ Кампания %s не содержит корректной длительности подписки",
|
||
campaign.id,
|
||
)
|
||
return CampaignBonusResult(success=False)
|
||
|
||
traffic_limit = campaign.subscription_traffic_gb
|
||
device_limit = campaign.subscription_device_limit
|
||
if device_limit is None:
|
||
device_limit = settings.DEFAULT_DEVICE_LIMIT
|
||
squads = list(campaign.subscription_squads or [])
|
||
|
||
if not squads:
|
||
try:
|
||
from app.database.crud.server_squad import get_random_trial_squad_uuid
|
||
|
||
trial_uuid = await get_random_trial_squad_uuid(db)
|
||
if trial_uuid:
|
||
squads = [trial_uuid]
|
||
except Exception as error:
|
||
logger.error(
|
||
"Не удалось подобрать сквад для кампании %s: %s",
|
||
campaign.id,
|
||
error,
|
||
)
|
||
|
||
squad_uuid = squads[0] if squads else None
|
||
|
||
new_subscription = await create_paid_subscription(
|
||
db=db,
|
||
user_id=user.id,
|
||
duration_days=duration_days,
|
||
traffic_limit_gb=traffic_limit or 0,
|
||
device_limit=device_limit,
|
||
connected_squads=squads,
|
||
update_server_counters=True,
|
||
is_trial=True,
|
||
)
|
||
|
||
try:
|
||
await self.subscription_service.create_remnawave_user(db, new_subscription)
|
||
except Exception as error:
|
||
logger.error(
|
||
"❌ Ошибка синхронизации RemnaWave для кампании %s: %s",
|
||
campaign.id,
|
||
error,
|
||
)
|
||
|
||
await record_campaign_registration(
|
||
db,
|
||
campaign_id=campaign.id,
|
||
user_id=user.id,
|
||
bonus_type="subscription",
|
||
subscription_duration_days=duration_days,
|
||
)
|
||
|
||
logger.info(
|
||
"🎁 Пользователю %s выдана подписка по кампании %s на %s дней",
|
||
user.telegram_id,
|
||
campaign.id,
|
||
duration_days,
|
||
)
|
||
|
||
return CampaignBonusResult(
|
||
success=True,
|
||
bonus_type="subscription",
|
||
subscription_days=duration_days,
|
||
subscription_traffic_gb=traffic_limit or 0,
|
||
subscription_device_limit=device_limit,
|
||
subscription_squads=squads,
|
||
)
|
||
|
||
async def _apply_none_bonus(
|
||
self,
|
||
db: AsyncSession,
|
||
user: User,
|
||
campaign: AdvertisingCampaign,
|
||
) -> CampaignBonusResult:
|
||
"""Обычная ссылка без награды - только регистрация для отслеживания."""
|
||
await record_campaign_registration(
|
||
db,
|
||
campaign_id=campaign.id,
|
||
user_id=user.id,
|
||
bonus_type="none",
|
||
)
|
||
|
||
logger.info(
|
||
"📊 Пользователь %s зарегистрирован по ссылке кампании %s (без награды)",
|
||
user.telegram_id,
|
||
campaign.id,
|
||
)
|
||
|
||
return CampaignBonusResult(
|
||
success=True,
|
||
bonus_type="none",
|
||
)
|
||
|
||
async def _apply_tariff_bonus(
|
||
self,
|
||
db: AsyncSession,
|
||
user: User,
|
||
campaign: AdvertisingCampaign,
|
||
) -> CampaignBonusResult:
|
||
"""Выдача тарифа на определённое время."""
|
||
existing_subscription = await get_subscription_by_user_id(db, user.id)
|
||
if existing_subscription:
|
||
logger.warning(
|
||
"⚠️ У пользователя %s уже есть подписка, бонус тарифа кампании %s пропущен",
|
||
user.telegram_id,
|
||
campaign.id,
|
||
)
|
||
return CampaignBonusResult(success=False)
|
||
|
||
if not campaign.tariff_id:
|
||
logger.error(
|
||
"❌ Кампания %s не имеет указанного тарифа для выдачи",
|
||
campaign.id,
|
||
)
|
||
return CampaignBonusResult(success=False)
|
||
|
||
duration_days = campaign.tariff_duration_days or 0
|
||
if duration_days <= 0:
|
||
logger.error(
|
||
"❌ Кампания %s не имеет указанной длительности тарифа",
|
||
campaign.id,
|
||
)
|
||
return CampaignBonusResult(success=False)
|
||
|
||
# Получаем тариф для извлечения параметров
|
||
tariff = await get_tariff_by_id(db, campaign.tariff_id)
|
||
if not tariff:
|
||
logger.error(
|
||
"❌ Тариф %s не найден для кампании %s",
|
||
campaign.tariff_id,
|
||
campaign.id,
|
||
)
|
||
return CampaignBonusResult(success=False)
|
||
|
||
if not tariff.is_active:
|
||
logger.warning(
|
||
"⚠️ Тариф %s неактивен, бонус кампании %s пропущен",
|
||
tariff.id,
|
||
campaign.id,
|
||
)
|
||
return CampaignBonusResult(success=False)
|
||
|
||
traffic_limit = tariff.traffic_limit_gb
|
||
device_limit = tariff.device_limit
|
||
squads = list(tariff.allowed_squads or [])
|
||
|
||
if not squads:
|
||
try:
|
||
from app.database.crud.server_squad import get_random_trial_squad_uuid
|
||
|
||
trial_uuid = await get_random_trial_squad_uuid(db)
|
||
if trial_uuid:
|
||
squads = [trial_uuid]
|
||
except Exception as error:
|
||
logger.error(
|
||
"Не удалось подобрать сквад для тарифа кампании %s: %s",
|
||
campaign.id,
|
||
error,
|
||
)
|
||
|
||
# Создаём подписку как платную (не trial) с привязкой к тарифу
|
||
new_subscription = await create_paid_subscription(
|
||
db=db,
|
||
user_id=user.id,
|
||
duration_days=duration_days,
|
||
traffic_limit_gb=traffic_limit or 0,
|
||
device_limit=device_limit,
|
||
connected_squads=squads,
|
||
update_server_counters=True,
|
||
is_trial=False, # Это полноценная подписка, не пробная
|
||
tariff_id=tariff.id,
|
||
)
|
||
|
||
try:
|
||
await self.subscription_service.create_remnawave_user(db, new_subscription)
|
||
except Exception as error:
|
||
logger.error(
|
||
"❌ Ошибка синхронизации RemnaWave для тарифа кампании %s: %s",
|
||
campaign.id,
|
||
error,
|
||
)
|
||
|
||
await record_campaign_registration(
|
||
db,
|
||
campaign_id=campaign.id,
|
||
user_id=user.id,
|
||
bonus_type="tariff",
|
||
tariff_id=tariff.id,
|
||
tariff_duration_days=duration_days,
|
||
)
|
||
|
||
logger.info(
|
||
"🎁 Пользователю %s выдан тариф '%s' по кампании %s на %s дней",
|
||
user.telegram_id,
|
||
tariff.name,
|
||
campaign.id,
|
||
duration_days,
|
||
)
|
||
|
||
return CampaignBonusResult(
|
||
success=True,
|
||
bonus_type="tariff",
|
||
tariff_id=tariff.id,
|
||
tariff_name=tariff.name,
|
||
tariff_duration_days=duration_days,
|
||
subscription_traffic_gb=traffic_limit or 0,
|
||
subscription_device_limit=device_limit,
|
||
subscription_squads=squads,
|
||
)
|