Files
remnawave-bedolaga-telegram…/app/services/campaign_service.py
2026-01-17 05:02:54 +03:00

340 lines
12 KiB
Python
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import 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,
)