mirror of
https://github.com/BEDOLAGA-DEV/remnawave-bedolaga-telegram-bot.git
synced 2026-01-19 19:32:10 +00:00
Update admin_stats.py
This commit is contained in:
@@ -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",
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user