Update admin_stats.py

This commit is contained in:
Egor
2026-01-19 08:51:48 +03:00
committed by GitHub
parent f8d7b3288c
commit 6775b06b2e

View File

@@ -13,11 +13,14 @@ from sqlalchemy import select, func, and_
from app.database.crud.subscription import get_subscriptions_statistics
from app.database.crud.transaction import get_transactions_statistics, get_revenue_by_period
from app.database.crud.server_squad import get_server_statistics
from app.database.crud.campaign import get_campaigns_list, get_campaign_statistics, get_campaigns_count
from app.services.remnawave_service import RemnaWaveService
from app.config import settings
from ..dependencies import get_cabinet_db, get_current_admin_user
from app.database.models import User, Subscription, Tariff, SubscriptionStatus
from app.database.models import (
User, Subscription, Tariff, SubscriptionStatus,
Transaction, TransactionType, ReferralEarning,
)
logger = logging.getLogger(__name__)
@@ -36,6 +39,15 @@ class NodeStatus(BaseModel):
users_online: int
traffic_used_bytes: Optional[int] = None
uptime: Optional[str] = None
xray_version: Optional[str] = None
node_version: Optional[str] = None
last_status_message: Optional[str] = None
xray_uptime: Optional[str] = None
is_xray_running: Optional[bool] = None
cpu_count: Optional[int] = None
cpu_model: Optional[str] = None
total_ram: Optional[str] = None
country_code: Optional[str] = None
class NodesOverview(BaseModel):
@@ -116,6 +128,81 @@ class DashboardStats(BaseModel):
tariff_stats: Optional[TariffStats] = None
# ============ Extended Stats Schemas ============
class TopReferrerItem(BaseModel):
"""Single referrer in top list."""
user_id: int
telegram_id: int
username: Optional[str] = None
display_name: str
invited_count: int
invited_today: int = 0
invited_week: int = 0
invited_month: int = 0
earnings_today_kopeks: int = 0
earnings_week_kopeks: int = 0
earnings_month_kopeks: int = 0
earnings_total_kopeks: int = 0
class TopReferrersResponse(BaseModel):
"""Top referrers response."""
by_earnings: List[TopReferrerItem]
by_invited: List[TopReferrerItem]
total_referrers: int
total_referrals: int
total_earnings_kopeks: int
class TopCampaignItem(BaseModel):
"""Single campaign in top list."""
id: int
name: str
start_parameter: str
bonus_type: str
is_active: bool
registrations: int
conversions: int
conversion_rate: float
total_revenue_kopeks: int
avg_revenue_per_user_kopeks: int
created_at: Optional[str] = None
class TopCampaignsResponse(BaseModel):
"""Top campaigns response."""
campaigns: List[TopCampaignItem]
total_campaigns: int
total_registrations: int
total_revenue_kopeks: int
class RecentPaymentItem(BaseModel):
"""Single recent payment."""
id: int
user_id: int
telegram_id: int
username: Optional[str] = None
display_name: str
amount_kopeks: int
amount_rubles: float
type: str
type_display: str
payment_method: Optional[str] = None
description: Optional[str] = None
created_at: str
is_completed: bool
class RecentPaymentsResponse(BaseModel):
"""Recent payments response."""
payments: List[RecentPaymentItem]
total_count: int
total_today_kopeks: int
total_week_kopeks: int
# ============ Routes ============
@router.get("/dashboard", response_model=DashboardStats)
@@ -300,6 +387,15 @@ async def _get_nodes_overview() -> NodesOverview:
users_online=n.get("users_online", 0) or 0,
traffic_used_bytes=n.get("traffic_used_bytes"),
uptime=n.get("uptime"),
xray_version=n.get("xray_version"),
node_version=n.get("node_version"),
last_status_message=n.get("last_status_message"),
xray_uptime=n.get("xray_uptime"),
is_xray_running=n.get("is_xray_running"),
cpu_count=n.get("cpu_count"),
cpu_model=n.get("cpu_model"),
total_ram=n.get("total_ram"),
country_code=n.get("country_code"),
)
for n in nodes
]
@@ -426,3 +522,457 @@ async def _get_tariff_stats(db: AsyncSession) -> Optional[TariffStats]:
except Exception as e:
logger.error(f"Failed to get tariff stats: {e}", exc_info=True)
return None
# ============ Extended Stats Routes ============
@router.get("/referrals/top", response_model=TopReferrersResponse)
async def get_top_referrers(
limit: int = 20,
admin: User = Depends(get_current_admin_user),
db: AsyncSession = Depends(get_cabinet_db),
):
"""Get top referrers with earnings breakdown by period."""
try:
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)
# Get all referrers with their stats
referrers_query = await db.execute(
select(
User.referred_by_id.label('referrer_id'),
func.count(User.id).label('total_invited')
)
.where(User.referred_by_id.isnot(None))
.group_by(User.referred_by_id)
)
referrers_data = {row.referrer_id: {'total_invited': row.total_invited} for row in referrers_query}
# Get invited counts by period for each referrer
# Today
today_invited_query = await db.execute(
select(
User.referred_by_id.label('referrer_id'),
func.count(User.id).label('count')
)
.where(
and_(
User.referred_by_id.isnot(None),
User.created_at >= today_start
)
)
.group_by(User.referred_by_id)
)
for row in today_invited_query:
if row.referrer_id in referrers_data:
referrers_data[row.referrer_id]['invited_today'] = row.count
# Week
week_invited_query = await db.execute(
select(
User.referred_by_id.label('referrer_id'),
func.count(User.id).label('count')
)
.where(
and_(
User.referred_by_id.isnot(None),
User.created_at >= week_ago
)
)
.group_by(User.referred_by_id)
)
for row in week_invited_query:
if row.referrer_id in referrers_data:
referrers_data[row.referrer_id]['invited_week'] = row.count
# Month
month_invited_query = await db.execute(
select(
User.referred_by_id.label('referrer_id'),
func.count(User.id).label('count')
)
.where(
and_(
User.referred_by_id.isnot(None),
User.created_at >= month_ago
)
)
.group_by(User.referred_by_id)
)
for row in month_invited_query:
if row.referrer_id in referrers_data:
referrers_data[row.referrer_id]['invited_month'] = row.count
# Get earnings from ReferralEarning table
# Total earnings
total_earnings_query = await db.execute(
select(
ReferralEarning.user_id.label('referrer_id'),
func.sum(ReferralEarning.amount_kopeks).label('total')
)
.group_by(ReferralEarning.user_id)
)
for row in total_earnings_query:
if row.referrer_id in referrers_data:
referrers_data[row.referrer_id]['earnings_total'] = row.total or 0
# Today earnings
today_earnings_query = await db.execute(
select(
ReferralEarning.user_id.label('referrer_id'),
func.sum(ReferralEarning.amount_kopeks).label('total')
)
.where(ReferralEarning.created_at >= today_start)
.group_by(ReferralEarning.user_id)
)
for row in today_earnings_query:
if row.referrer_id in referrers_data:
referrers_data[row.referrer_id]['earnings_today'] = row.total or 0
# Week earnings
week_earnings_query = await db.execute(
select(
ReferralEarning.user_id.label('referrer_id'),
func.sum(ReferralEarning.amount_kopeks).label('total')
)
.where(ReferralEarning.created_at >= week_ago)
.group_by(ReferralEarning.user_id)
)
for row in week_earnings_query:
if row.referrer_id in referrers_data:
referrers_data[row.referrer_id]['earnings_week'] = row.total or 0
# Month earnings
month_earnings_query = await db.execute(
select(
ReferralEarning.user_id.label('referrer_id'),
func.sum(ReferralEarning.amount_kopeks).label('total')
)
.where(ReferralEarning.created_at >= month_ago)
.group_by(ReferralEarning.user_id)
)
for row in month_earnings_query:
if row.referrer_id in referrers_data:
referrers_data[row.referrer_id]['earnings_month'] = row.total or 0
# Also add REFERRAL_REWARD transactions
trans_total_query = await db.execute(
select(
Transaction.user_id.label('referrer_id'),
func.sum(Transaction.amount_kopeks).label('total')
)
.where(Transaction.type == TransactionType.REFERRAL_REWARD.value)
.group_by(Transaction.user_id)
)
for row in trans_total_query:
if row.referrer_id in referrers_data:
referrers_data[row.referrer_id]['earnings_total'] = \
referrers_data[row.referrer_id].get('earnings_total', 0) + (row.total or 0)
trans_today_query = await db.execute(
select(
Transaction.user_id.label('referrer_id'),
func.sum(Transaction.amount_kopeks).label('total')
)
.where(
and_(
Transaction.type == TransactionType.REFERRAL_REWARD.value,
Transaction.created_at >= today_start
)
)
.group_by(Transaction.user_id)
)
for row in trans_today_query:
if row.referrer_id in referrers_data:
referrers_data[row.referrer_id]['earnings_today'] = \
referrers_data[row.referrer_id].get('earnings_today', 0) + (row.total or 0)
trans_week_query = await db.execute(
select(
Transaction.user_id.label('referrer_id'),
func.sum(Transaction.amount_kopeks).label('total')
)
.where(
and_(
Transaction.type == TransactionType.REFERRAL_REWARD.value,
Transaction.created_at >= week_ago
)
)
.group_by(Transaction.user_id)
)
for row in trans_week_query:
if row.referrer_id in referrers_data:
referrers_data[row.referrer_id]['earnings_week'] = \
referrers_data[row.referrer_id].get('earnings_week', 0) + (row.total or 0)
trans_month_query = await db.execute(
select(
Transaction.user_id.label('referrer_id'),
func.sum(Transaction.amount_kopeks).label('total')
)
.where(
and_(
Transaction.type == TransactionType.REFERRAL_REWARD.value,
Transaction.created_at >= month_ago
)
)
.group_by(Transaction.user_id)
)
for row in trans_month_query:
if row.referrer_id in referrers_data:
referrers_data[row.referrer_id]['earnings_month'] = \
referrers_data[row.referrer_id].get('earnings_month', 0) + (row.total or 0)
# Get user info for all referrers
referrer_ids = list(referrers_data.keys())
if referrer_ids:
users_query = await db.execute(
select(User.id, User.telegram_id, User.username, User.first_name, User.last_name)
.where(User.id.in_(referrer_ids))
)
users_info = {u.id: u for u in users_query}
else:
users_info = {}
# Build referrer items
referrer_items = []
for referrer_id, data in referrers_data.items():
user = users_info.get(referrer_id)
if not user:
continue
display_name = ""
if user.first_name:
display_name = user.first_name
if user.last_name:
display_name += f" {user.last_name}"
elif user.username:
display_name = f"@{user.username}"
else:
display_name = f"ID{user.telegram_id}"
referrer_items.append(TopReferrerItem(
user_id=user.id,
telegram_id=user.telegram_id,
username=user.username,
display_name=display_name,
invited_count=data.get('total_invited', 0),
invited_today=data.get('invited_today', 0),
invited_week=data.get('invited_week', 0),
invited_month=data.get('invited_month', 0),
earnings_today_kopeks=data.get('earnings_today', 0),
earnings_week_kopeks=data.get('earnings_week', 0),
earnings_month_kopeks=data.get('earnings_month', 0),
earnings_total_kopeks=data.get('earnings_total', 0),
))
# Sort by earnings and by invited
by_earnings = sorted(referrer_items, key=lambda x: x.earnings_total_kopeks, reverse=True)[:limit]
by_invited = sorted(referrer_items, key=lambda x: x.invited_count, reverse=True)[:limit]
# Calculate totals
total_referrers = len(referrer_items)
total_referrals = sum(r.invited_count for r in referrer_items)
total_earnings = sum(r.earnings_total_kopeks for r in referrer_items)
return TopReferrersResponse(
by_earnings=by_earnings,
by_invited=by_invited,
total_referrers=total_referrers,
total_referrals=total_referrals,
total_earnings_kopeks=total_earnings,
)
except Exception as e:
logger.error(f"Failed to get top referrers: {e}", exc_info=True)
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="Failed to load referrers statistics",
)
@router.get("/campaigns/top", response_model=TopCampaignsResponse)
async def get_top_campaigns(
limit: int = 20,
admin: User = Depends(get_current_admin_user),
db: AsyncSession = Depends(get_cabinet_db),
):
"""Get top advertising campaigns with statistics."""
try:
# Get all campaigns
campaigns = await get_campaigns_list(db, offset=0, limit=100, include_inactive=True)
campaign_items = []
total_registrations = 0
total_revenue = 0
for campaign in campaigns:
stats = await get_campaign_statistics(db, campaign.id)
campaign_items.append(TopCampaignItem(
id=campaign.id,
name=campaign.name,
start_parameter=campaign.start_parameter,
bonus_type=campaign.bonus_type,
is_active=campaign.is_active,
registrations=stats.get("registrations", 0),
conversions=stats.get("conversion_count", 0),
conversion_rate=stats.get("conversion_rate", 0.0),
total_revenue_kopeks=stats.get("total_revenue_kopeks", 0),
avg_revenue_per_user_kopeks=stats.get("avg_revenue_per_user_kopeks", 0),
created_at=campaign.created_at.isoformat() if campaign.created_at else None,
))
total_registrations += stats.get("registrations", 0)
total_revenue += stats.get("total_revenue_kopeks", 0)
# Sort by revenue
campaign_items.sort(key=lambda x: x.total_revenue_kopeks, reverse=True)
total_campaigns = await get_campaigns_count(db)
return TopCampaignsResponse(
campaigns=campaign_items[:limit],
total_campaigns=total_campaigns,
total_registrations=total_registrations,
total_revenue_kopeks=total_revenue,
)
except Exception as e:
logger.error(f"Failed to get top campaigns: {e}", exc_info=True)
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="Failed to load campaigns statistics",
)
@router.get("/payments/recent", response_model=RecentPaymentsResponse)
async def get_recent_payments(
limit: int = 50,
admin: User = Depends(get_current_admin_user),
db: AsyncSession = Depends(get_cabinet_db),
):
"""Get recent payments with user info."""
try:
now = datetime.utcnow()
today_start = now.replace(hour=0, minute=0, second=0, microsecond=0)
week_ago = now - timedelta(days=7)
# Get recent transactions (deposits and subscription payments)
transactions_query = await db.execute(
select(Transaction)
.where(
Transaction.type.in_([
TransactionType.DEPOSIT.value,
TransactionType.SUBSCRIPTION_PAYMENT.value,
])
)
.order_by(Transaction.created_at.desc())
.limit(limit)
)
transactions = transactions_query.scalars().all()
# Get user info for all transactions
user_ids = list(set(t.user_id for t in transactions))
if user_ids:
users_query = await db.execute(
select(User.id, User.telegram_id, User.username, User.first_name, User.last_name)
.where(User.id.in_(user_ids))
)
users_info = {u.id: u for u in users_query}
else:
users_info = {}
# Type display names
type_display = {
TransactionType.DEPOSIT.value: "Пополнение",
TransactionType.SUBSCRIPTION_PAYMENT.value: "Оплата подписки",
TransactionType.WITHDRAWAL.value: "Вывод",
TransactionType.REFUND.value: "Возврат",
TransactionType.REFERRAL_REWARD.value: "Реферальный бонус",
TransactionType.POLL_REWARD.value: "Награда за опрос",
}
payment_items = []
for trans in transactions:
user = users_info.get(trans.user_id)
if not user:
continue
display_name = ""
if user.first_name:
display_name = user.first_name
if user.last_name:
display_name += f" {user.last_name}"
elif user.username:
display_name = f"@{user.username}"
else:
display_name = f"ID{user.telegram_id}"
payment_items.append(RecentPaymentItem(
id=trans.id,
user_id=user.id,
telegram_id=user.telegram_id,
username=user.username,
display_name=display_name,
amount_kopeks=trans.amount_kopeks,
amount_rubles=trans.amount_kopeks / 100,
type=trans.type,
type_display=type_display.get(trans.type, trans.type),
payment_method=trans.payment_method,
description=trans.description,
created_at=trans.created_at.isoformat() if trans.created_at else "",
is_completed=trans.is_completed,
))
# Calculate totals
total_count_result = await db.execute(
select(func.count(Transaction.id))
.where(
Transaction.type.in_([
TransactionType.DEPOSIT.value,
TransactionType.SUBSCRIPTION_PAYMENT.value,
])
)
)
total_count = total_count_result.scalar() or 0
today_total_result = await db.execute(
select(func.coalesce(func.sum(Transaction.amount_kopeks), 0))
.where(
and_(
Transaction.type == TransactionType.DEPOSIT.value,
Transaction.is_completed == True,
Transaction.created_at >= today_start
)
)
)
total_today = today_total_result.scalar() or 0
week_total_result = await db.execute(
select(func.coalesce(func.sum(Transaction.amount_kopeks), 0))
.where(
and_(
Transaction.type == TransactionType.DEPOSIT.value,
Transaction.is_completed == True,
Transaction.created_at >= week_ago
)
)
)
total_week = week_total_result.scalar() or 0
return RecentPaymentsResponse(
payments=payment_items,
total_count=total_count,
total_today_kopeks=total_today,
total_week_kopeks=total_week,
)
except Exception as e:
logger.error(f"Failed to get recent payments: {e}", exc_info=True)
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="Failed to load recent payments",
)