mirror of
https://github.com/BEDOLAGA-DEV/remnawave-bedolaga-telegram-bot.git
synced 2026-01-20 11:50:27 +00:00
626 lines
24 KiB
Python
626 lines
24 KiB
Python
"""Сервис расширенной статистики партнёров (рефереров)."""
|
||
|
||
from __future__ import annotations
|
||
|
||
import logging
|
||
from datetime import datetime, timedelta
|
||
from typing import Any, Dict, List, Optional
|
||
|
||
from sqlalchemy import and_, case, desc, func, select
|
||
from sqlalchemy.ext.asyncio import AsyncSession
|
||
|
||
from app.database.models import (
|
||
ReferralEarning,
|
||
Subscription,
|
||
SubscriptionStatus,
|
||
User,
|
||
)
|
||
|
||
logger = logging.getLogger(__name__)
|
||
|
||
|
||
class PartnerStatsService:
|
||
"""Сервис для детальной статистики партнёров."""
|
||
|
||
@classmethod
|
||
async def get_referrer_detailed_stats(
|
||
cls,
|
||
db: AsyncSession,
|
||
user_id: int,
|
||
) -> Dict[str, Any]:
|
||
"""Получить детальную статистику реферера."""
|
||
now = datetime.utcnow()
|
||
today_start = now.replace(hour=0, minute=0, second=0, microsecond=0)
|
||
week_ago = now - timedelta(days=7)
|
||
month_ago = now - timedelta(days=30)
|
||
year_ago = now - timedelta(days=365)
|
||
|
||
# Базовые данные о рефералах
|
||
referrals_query = select(User).where(User.referred_by_id == user_id)
|
||
referrals_result = await db.execute(referrals_query)
|
||
referrals = referrals_result.scalars().all()
|
||
referral_ids = [r.id for r in referrals]
|
||
|
||
total_referrals = len(referrals)
|
||
|
||
# Сколько сделали первое пополнение (has_made_first_topup)
|
||
paid_referrals = sum(1 for r in referrals if r.has_made_first_topup)
|
||
|
||
# Активные рефералы (с активной подпиской)
|
||
if referral_ids:
|
||
active_result = await db.execute(
|
||
select(func.count(func.distinct(User.id)))
|
||
.join(Subscription, User.id == Subscription.user_id)
|
||
.where(
|
||
and_(
|
||
User.id.in_(referral_ids),
|
||
Subscription.status == SubscriptionStatus.ACTIVE.value,
|
||
Subscription.end_date > now,
|
||
)
|
||
)
|
||
)
|
||
active_referrals = active_result.scalar() or 0
|
||
else:
|
||
active_referrals = 0
|
||
|
||
# Заработки по периодам - один запрос с CASE WHEN
|
||
earnings_result = await db.execute(
|
||
select(
|
||
func.coalesce(func.sum(ReferralEarning.amount_kopeks), 0).label("all_time"),
|
||
func.coalesce(func.sum(
|
||
case((ReferralEarning.created_at >= today_start, ReferralEarning.amount_kopeks), else_=0)
|
||
), 0).label("today"),
|
||
func.coalesce(func.sum(
|
||
case((ReferralEarning.created_at >= week_ago, ReferralEarning.amount_kopeks), else_=0)
|
||
), 0).label("week"),
|
||
func.coalesce(func.sum(
|
||
case((ReferralEarning.created_at >= month_ago, ReferralEarning.amount_kopeks), else_=0)
|
||
), 0).label("month"),
|
||
func.coalesce(func.sum(
|
||
case((ReferralEarning.created_at >= year_ago, ReferralEarning.amount_kopeks), else_=0)
|
||
), 0).label("year"),
|
||
).where(ReferralEarning.user_id == user_id)
|
||
)
|
||
earnings_row = earnings_result.one()
|
||
earnings_all_time = int(earnings_row.all_time)
|
||
earnings_today = int(earnings_row.today)
|
||
earnings_week = int(earnings_row.week)
|
||
earnings_month = int(earnings_row.month)
|
||
earnings_year = int(earnings_row.year)
|
||
|
||
# Рефералы по периодам
|
||
referrals_today = sum(1 for r in referrals if r.created_at >= today_start)
|
||
referrals_week = sum(1 for r in referrals if r.created_at >= week_ago)
|
||
referrals_month = sum(1 for r in referrals if r.created_at >= month_ago)
|
||
referrals_year = sum(1 for r in referrals if r.created_at >= year_ago)
|
||
|
||
# Конверсии
|
||
conversion_to_paid = round((paid_referrals / total_referrals * 100), 2) if total_referrals > 0 else 0
|
||
conversion_to_active = round((active_referrals / total_referrals * 100), 2) if total_referrals > 0 else 0
|
||
|
||
# Средний доход с реферала
|
||
avg_earnings_per_referral = round(earnings_all_time / paid_referrals, 2) if paid_referrals > 0 else 0
|
||
|
||
return {
|
||
"user_id": user_id,
|
||
"summary": {
|
||
"total_referrals": total_referrals,
|
||
"paid_referrals": paid_referrals,
|
||
"active_referrals": active_referrals,
|
||
"conversion_to_paid_percent": conversion_to_paid,
|
||
"conversion_to_active_percent": conversion_to_active,
|
||
"avg_earnings_per_referral_kopeks": avg_earnings_per_referral,
|
||
},
|
||
"earnings": {
|
||
"all_time_kopeks": earnings_all_time,
|
||
"year_kopeks": earnings_year,
|
||
"month_kopeks": earnings_month,
|
||
"week_kopeks": earnings_week,
|
||
"today_kopeks": earnings_today,
|
||
},
|
||
"referrals_count": {
|
||
"all_time": total_referrals,
|
||
"year": referrals_year,
|
||
"month": referrals_month,
|
||
"week": referrals_week,
|
||
"today": referrals_today,
|
||
},
|
||
}
|
||
|
||
@classmethod
|
||
async def get_referrer_daily_stats(
|
||
cls,
|
||
db: AsyncSession,
|
||
user_id: int,
|
||
days: int = 30,
|
||
) -> List[Dict[str, Any]]:
|
||
"""Получить статистику реферера по дням."""
|
||
now = datetime.utcnow()
|
||
start_date = now - timedelta(days=days)
|
||
|
||
# Рефералы по дням
|
||
referrals_by_day = await db.execute(
|
||
select(
|
||
func.date(User.created_at).label("date"),
|
||
func.count(User.id).label("referrals_count"),
|
||
)
|
||
.where(
|
||
and_(
|
||
User.referred_by_id == user_id,
|
||
User.created_at >= start_date,
|
||
)
|
||
)
|
||
.group_by(func.date(User.created_at))
|
||
.order_by(func.date(User.created_at))
|
||
)
|
||
referrals_dict = {str(row.date): row.referrals_count for row in referrals_by_day.all()}
|
||
|
||
# Заработки по дням (из ReferralEarning)
|
||
earnings_by_day = await db.execute(
|
||
select(
|
||
func.date(ReferralEarning.created_at).label("date"),
|
||
func.sum(ReferralEarning.amount_kopeks).label("earnings"),
|
||
)
|
||
.where(
|
||
and_(
|
||
ReferralEarning.user_id == user_id,
|
||
ReferralEarning.created_at >= start_date,
|
||
)
|
||
)
|
||
.group_by(func.date(ReferralEarning.created_at))
|
||
)
|
||
earnings_dict = {str(row.date): int(row.earnings or 0) for row in earnings_by_day.all()}
|
||
|
||
# Формируем массив за все дни
|
||
result = []
|
||
for i in range(days):
|
||
date = (start_date + timedelta(days=i)).date()
|
||
date_str = str(date)
|
||
result.append({
|
||
"date": date_str,
|
||
"referrals_count": referrals_dict.get(date_str, 0),
|
||
"earnings_kopeks": earnings_dict.get(date_str, 0),
|
||
})
|
||
|
||
return result
|
||
|
||
@classmethod
|
||
async def get_referrer_top_referrals(
|
||
cls,
|
||
db: AsyncSession,
|
||
user_id: int,
|
||
limit: int = 10,
|
||
) -> List[Dict[str, Any]]:
|
||
"""Получить топ рефералов по доходу для реферера."""
|
||
now = datetime.utcnow()
|
||
|
||
# Получаем рефералов с их доходами
|
||
result = await db.execute(
|
||
select(
|
||
User.id,
|
||
User.telegram_id,
|
||
User.username,
|
||
User.first_name,
|
||
User.last_name,
|
||
User.created_at,
|
||
User.has_made_first_topup,
|
||
func.coalesce(func.sum(ReferralEarning.amount_kopeks), 0).label("total_earnings"),
|
||
)
|
||
.outerjoin(ReferralEarning, ReferralEarning.referral_id == User.id)
|
||
.where(User.referred_by_id == user_id)
|
||
.group_by(User.id)
|
||
.order_by(desc(func.coalesce(func.sum(ReferralEarning.amount_kopeks), 0)))
|
||
.limit(limit)
|
||
)
|
||
rows = result.all()
|
||
|
||
if not rows:
|
||
return []
|
||
|
||
# Собираем user_id для проверки активных подписок одним запросом
|
||
user_ids = [row.id for row in rows]
|
||
|
||
# Получаем все активные подписки для этих пользователей одним запросом
|
||
active_subs_result = await db.execute(
|
||
select(Subscription.user_id)
|
||
.where(
|
||
and_(
|
||
Subscription.user_id.in_(user_ids),
|
||
Subscription.status == SubscriptionStatus.ACTIVE.value,
|
||
Subscription.end_date > now,
|
||
)
|
||
)
|
||
)
|
||
active_user_ids = {row.user_id for row in active_subs_result.all()}
|
||
|
||
referrals = []
|
||
for row in rows:
|
||
referrals.append({
|
||
"id": row.id,
|
||
"telegram_id": row.telegram_id,
|
||
"username": row.username,
|
||
"first_name": row.first_name,
|
||
"last_name": row.last_name,
|
||
"full_name": f"{row.first_name or ''} {row.last_name or ''}".strip() or f"User {row.telegram_id}",
|
||
"created_at": row.created_at,
|
||
"has_made_first_topup": row.has_made_first_topup,
|
||
"is_active": row.id in active_user_ids,
|
||
"total_earnings_kopeks": int(row.total_earnings),
|
||
})
|
||
|
||
return referrals
|
||
|
||
@classmethod
|
||
async def get_referrer_period_comparison(
|
||
cls,
|
||
db: AsyncSession,
|
||
user_id: int,
|
||
current_days: int = 7,
|
||
previous_days: int = 7,
|
||
) -> Dict[str, Any]:
|
||
"""Сравнить текущий и предыдущий период."""
|
||
now = datetime.utcnow()
|
||
current_start = now - timedelta(days=current_days)
|
||
previous_start = current_start - timedelta(days=previous_days)
|
||
previous_end = current_start
|
||
|
||
# Рефералы за текущий период
|
||
current_referrals = await db.execute(
|
||
select(func.count(User.id))
|
||
.where(
|
||
and_(
|
||
User.referred_by_id == user_id,
|
||
User.created_at >= current_start,
|
||
)
|
||
)
|
||
)
|
||
current_referrals_count = current_referrals.scalar() or 0
|
||
|
||
# Рефералы за предыдущий период
|
||
previous_referrals = await db.execute(
|
||
select(func.count(User.id))
|
||
.where(
|
||
and_(
|
||
User.referred_by_id == user_id,
|
||
User.created_at >= previous_start,
|
||
User.created_at < previous_end,
|
||
)
|
||
)
|
||
)
|
||
previous_referrals_count = previous_referrals.scalar() or 0
|
||
|
||
# Заработки за текущий период
|
||
current_earnings = await cls._get_earnings_for_period(db, user_id, current_start)
|
||
|
||
# Заработки за предыдущий период
|
||
previous_earnings = await cls._get_earnings_for_period(
|
||
db, user_id, previous_start, previous_end
|
||
)
|
||
|
||
# Расчёт изменений
|
||
referrals_change = current_referrals_count - previous_referrals_count
|
||
referrals_change_percent = (
|
||
round((referrals_change / previous_referrals_count * 100), 2)
|
||
if previous_referrals_count > 0
|
||
else 0
|
||
)
|
||
|
||
earnings_change = current_earnings - previous_earnings
|
||
earnings_change_percent = (
|
||
round((earnings_change / previous_earnings * 100), 2)
|
||
if previous_earnings > 0
|
||
else 0
|
||
)
|
||
|
||
return {
|
||
"current_period": {
|
||
"days": current_days,
|
||
"start": current_start.isoformat(),
|
||
"end": now.isoformat(),
|
||
"referrals_count": current_referrals_count,
|
||
"earnings_kopeks": current_earnings,
|
||
},
|
||
"previous_period": {
|
||
"days": previous_days,
|
||
"start": previous_start.isoformat(),
|
||
"end": previous_end.isoformat(),
|
||
"referrals_count": previous_referrals_count,
|
||
"earnings_kopeks": previous_earnings,
|
||
},
|
||
"change": {
|
||
"referrals": {
|
||
"absolute": referrals_change,
|
||
"percent": referrals_change_percent,
|
||
"trend": "up" if referrals_change > 0 else "down" if referrals_change < 0 else "stable",
|
||
},
|
||
"earnings": {
|
||
"absolute": earnings_change,
|
||
"percent": earnings_change_percent,
|
||
"trend": "up" if earnings_change > 0 else "down" if earnings_change < 0 else "stable",
|
||
},
|
||
},
|
||
}
|
||
|
||
@classmethod
|
||
async def get_global_partner_stats(
|
||
cls,
|
||
db: AsyncSession,
|
||
days: int = 30,
|
||
) -> Dict[str, Any]:
|
||
"""Глобальная статистика партнёрской программы."""
|
||
now = datetime.utcnow()
|
||
today_start = now.replace(hour=0, minute=0, second=0, microsecond=0)
|
||
week_ago = now - timedelta(days=7)
|
||
month_ago = now - timedelta(days=30)
|
||
year_ago = now - timedelta(days=365)
|
||
start_date = now - timedelta(days=days)
|
||
|
||
# Всего рефереров (у кого есть рефералы)
|
||
total_referrers = await db.execute(
|
||
select(func.count(func.distinct(User.referred_by_id)))
|
||
.where(User.referred_by_id.isnot(None))
|
||
)
|
||
total_referrers_count = total_referrers.scalar() or 0
|
||
|
||
# Всего рефералов
|
||
total_referrals = await db.execute(
|
||
select(func.count(User.id))
|
||
.where(User.referred_by_id.isnot(None))
|
||
)
|
||
total_referrals_count = total_referrals.scalar() or 0
|
||
|
||
# Рефералы которые заплатили
|
||
paid_referrals = await db.execute(
|
||
select(func.count(User.id))
|
||
.where(
|
||
and_(
|
||
User.referred_by_id.isnot(None),
|
||
User.has_made_first_topup.is_(True),
|
||
)
|
||
)
|
||
)
|
||
paid_referrals_count = paid_referrals.scalar() or 0
|
||
|
||
# Всего выплачено - один запрос с CASE WHEN
|
||
payouts_result = await db.execute(
|
||
select(
|
||
func.coalesce(func.sum(ReferralEarning.amount_kopeks), 0).label("all_time"),
|
||
func.coalesce(func.sum(
|
||
case((ReferralEarning.created_at >= today_start, ReferralEarning.amount_kopeks), else_=0)
|
||
), 0).label("today"),
|
||
func.coalesce(func.sum(
|
||
case((ReferralEarning.created_at >= week_ago, ReferralEarning.amount_kopeks), else_=0)
|
||
), 0).label("week"),
|
||
func.coalesce(func.sum(
|
||
case((ReferralEarning.created_at >= month_ago, ReferralEarning.amount_kopeks), else_=0)
|
||
), 0).label("month"),
|
||
func.coalesce(func.sum(
|
||
case((ReferralEarning.created_at >= year_ago, ReferralEarning.amount_kopeks), else_=0)
|
||
), 0).label("year"),
|
||
)
|
||
)
|
||
payouts_row = payouts_result.one()
|
||
total_paid = int(payouts_row.all_time)
|
||
today_paid = int(payouts_row.today)
|
||
week_paid = int(payouts_row.week)
|
||
month_paid = int(payouts_row.month)
|
||
year_paid = int(payouts_row.year)
|
||
|
||
# Новые рефералы по периодам - один запрос с CASE WHEN
|
||
new_referrals_result = await db.execute(
|
||
select(
|
||
func.sum(case((User.created_at >= today_start, 1), else_=0)).label("today"),
|
||
func.sum(case((User.created_at >= week_ago, 1), else_=0)).label("week"),
|
||
func.sum(case((User.created_at >= month_ago, 1), else_=0)).label("month"),
|
||
).where(User.referred_by_id.isnot(None))
|
||
)
|
||
new_referrals_row = new_referrals_result.one()
|
||
new_referrals_today_count = int(new_referrals_row.today or 0)
|
||
new_referrals_week_count = int(new_referrals_row.week or 0)
|
||
new_referrals_month_count = int(new_referrals_row.month or 0)
|
||
|
||
# Конверсия
|
||
conversion_rate = (
|
||
round((paid_referrals_count / total_referrals_count * 100), 2)
|
||
if total_referrals_count > 0
|
||
else 0
|
||
)
|
||
|
||
# Средний доход с реферала
|
||
avg_per_referral = (
|
||
round(total_paid / paid_referrals_count, 2)
|
||
if paid_referrals_count > 0
|
||
else 0
|
||
)
|
||
|
||
return {
|
||
"summary": {
|
||
"total_referrers": total_referrers_count,
|
||
"total_referrals": total_referrals_count,
|
||
"paid_referrals": paid_referrals_count,
|
||
"conversion_rate_percent": conversion_rate,
|
||
"avg_earnings_per_referral_kopeks": avg_per_referral,
|
||
},
|
||
"payouts": {
|
||
"all_time_kopeks": total_paid,
|
||
"year_kopeks": year_paid,
|
||
"month_kopeks": month_paid,
|
||
"week_kopeks": week_paid,
|
||
"today_kopeks": today_paid,
|
||
},
|
||
"new_referrals": {
|
||
"today": new_referrals_today_count,
|
||
"week": new_referrals_week_count,
|
||
"month": new_referrals_month_count,
|
||
},
|
||
}
|
||
|
||
@classmethod
|
||
async def get_global_daily_stats(
|
||
cls,
|
||
db: AsyncSession,
|
||
days: int = 30,
|
||
) -> List[Dict[str, Any]]:
|
||
"""Глобальная статистика по дням."""
|
||
now = datetime.utcnow()
|
||
start_date = now - timedelta(days=days)
|
||
|
||
# Рефералы по дням
|
||
referrals_by_day = await db.execute(
|
||
select(
|
||
func.date(User.created_at).label("date"),
|
||
func.count(User.id).label("referrals_count"),
|
||
)
|
||
.where(
|
||
and_(
|
||
User.referred_by_id.isnot(None),
|
||
User.created_at >= start_date,
|
||
)
|
||
)
|
||
.group_by(func.date(User.created_at))
|
||
)
|
||
referrals_dict = {str(row.date): row.referrals_count for row in referrals_by_day.all()}
|
||
|
||
# Выплаты по дням
|
||
earnings_by_day = await db.execute(
|
||
select(
|
||
func.date(ReferralEarning.created_at).label("date"),
|
||
func.sum(ReferralEarning.amount_kopeks).label("earnings"),
|
||
)
|
||
.where(ReferralEarning.created_at >= start_date)
|
||
.group_by(func.date(ReferralEarning.created_at))
|
||
)
|
||
earnings_dict = {str(row.date): int(row.earnings or 0) for row in earnings_by_day.all()}
|
||
|
||
result = []
|
||
for i in range(days):
|
||
date = (start_date + timedelta(days=i)).date()
|
||
date_str = str(date)
|
||
result.append({
|
||
"date": date_str,
|
||
"referrals_count": referrals_dict.get(date_str, 0),
|
||
"earnings_kopeks": earnings_dict.get(date_str, 0),
|
||
})
|
||
|
||
return result
|
||
|
||
@classmethod
|
||
async def get_top_referrers(
|
||
cls,
|
||
db: AsyncSession,
|
||
limit: int = 10,
|
||
days: Optional[int] = None,
|
||
) -> List[Dict[str, Any]]:
|
||
"""Получить топ рефереров."""
|
||
now = datetime.utcnow()
|
||
start_date = now - timedelta(days=days) if days else None
|
||
|
||
# Подсчёт рефералов и заработков
|
||
earnings_query = (
|
||
select(
|
||
ReferralEarning.user_id,
|
||
func.sum(ReferralEarning.amount_kopeks).label("total_earnings"),
|
||
)
|
||
.group_by(ReferralEarning.user_id)
|
||
)
|
||
if start_date:
|
||
earnings_query = earnings_query.where(ReferralEarning.created_at >= start_date)
|
||
|
||
earnings_result = await db.execute(earnings_query)
|
||
earnings_dict = {row.user_id: int(row.total_earnings or 0) for row in earnings_result.all()}
|
||
|
||
# Подсчёт рефералов
|
||
referrals_query = (
|
||
select(
|
||
User.referred_by_id,
|
||
func.count(User.id).label("referrals_count"),
|
||
)
|
||
.where(User.referred_by_id.isnot(None))
|
||
.group_by(User.referred_by_id)
|
||
)
|
||
if start_date:
|
||
referrals_query = referrals_query.where(User.created_at >= start_date)
|
||
|
||
referrals_result = await db.execute(referrals_query)
|
||
referrals_dict = {row.referred_by_id: row.referrals_count for row in referrals_result.all()}
|
||
|
||
# Объединяем данные
|
||
all_referrer_ids = set(earnings_dict.keys()) | set(referrals_dict.keys())
|
||
referrers_data = []
|
||
|
||
for referrer_id in all_referrer_ids:
|
||
referrers_data.append({
|
||
"user_id": referrer_id,
|
||
"referrals_count": referrals_dict.get(referrer_id, 0),
|
||
"total_earnings": earnings_dict.get(referrer_id, 0),
|
||
})
|
||
|
||
# Сортируем по заработку
|
||
referrers_data.sort(key=lambda x: x["total_earnings"], reverse=True)
|
||
top_referrers = referrers_data[:limit]
|
||
|
||
if not top_referrers:
|
||
return []
|
||
|
||
# Получаем данные всех пользователей одним запросом
|
||
top_user_ids = [data["user_id"] for data in top_referrers]
|
||
users_result = await db.execute(
|
||
select(User).where(User.id.in_(top_user_ids))
|
||
)
|
||
users_dict = {user.id: user for user in users_result.scalars().all()}
|
||
|
||
# Формируем результат с сохранением порядка сортировки
|
||
result = []
|
||
for data in top_referrers:
|
||
user = users_dict.get(data["user_id"])
|
||
if user:
|
||
result.append({
|
||
"id": user.id,
|
||
"telegram_id": user.telegram_id,
|
||
"username": user.username,
|
||
"first_name": user.first_name,
|
||
"last_name": user.last_name,
|
||
"full_name": f"{user.first_name or ''} {user.last_name or ''}".strip() or f"User {user.telegram_id}",
|
||
"referral_code": user.referral_code,
|
||
"referrals_count": data["referrals_count"],
|
||
"total_earnings_kopeks": data["total_earnings"],
|
||
})
|
||
|
||
return result
|
||
|
||
@classmethod
|
||
async def _get_earnings_for_period(
|
||
cls,
|
||
db: AsyncSession,
|
||
user_id: int,
|
||
start_date: Optional[datetime],
|
||
end_date: Optional[datetime] = None,
|
||
) -> int:
|
||
"""Получить заработки за период."""
|
||
query = select(func.coalesce(func.sum(ReferralEarning.amount_kopeks), 0)).where(
|
||
ReferralEarning.user_id == user_id
|
||
)
|
||
|
||
if start_date:
|
||
query = query.where(ReferralEarning.created_at >= start_date)
|
||
if end_date:
|
||
query = query.where(ReferralEarning.created_at < end_date)
|
||
|
||
result = await db.execute(query)
|
||
return int(result.scalar() or 0)
|
||
|
||
@classmethod
|
||
async def _get_total_earnings(
|
||
cls,
|
||
db: AsyncSession,
|
||
start_date: Optional[datetime],
|
||
) -> int:
|
||
"""Получить общие выплаты за период."""
|
||
query = select(func.coalesce(func.sum(ReferralEarning.amount_kopeks), 0))
|
||
|
||
if start_date:
|
||
query = query.where(ReferralEarning.created_at >= start_date)
|
||
|
||
result = await db.execute(query)
|
||
return int(result.scalar() or 0)
|