mirror of
https://github.com/BEDOLAGA-DEV/remnawave-bedolaga-telegram-bot.git
synced 2026-03-01 15:52:30 +00:00
Merge pull request #208 from Fr1ngg/revert-207-sha36e-bedolaga/add-referral-system-enhancements-and-analytics
Revert "Enhance campaign analytics and registration visibility"
This commit is contained in:
@@ -9,10 +9,6 @@ from sqlalchemy.orm import selectinload
|
||||
from app.database.models import (
|
||||
AdvertisingCampaign,
|
||||
AdvertisingCampaignRegistration,
|
||||
Transaction,
|
||||
TransactionType,
|
||||
Subscription,
|
||||
User,
|
||||
)
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
@@ -201,7 +197,7 @@ async def get_campaign_statistics(
|
||||
db: AsyncSession,
|
||||
campaign_id: int,
|
||||
) -> Dict[str, Optional[int]]:
|
||||
aggregate_result = await db.execute(
|
||||
result = await db.execute(
|
||||
select(
|
||||
func.count(AdvertisingCampaignRegistration.id),
|
||||
func.coalesce(
|
||||
@@ -210,7 +206,7 @@ async def get_campaign_statistics(
|
||||
func.max(AdvertisingCampaignRegistration.created_at),
|
||||
).where(AdvertisingCampaignRegistration.campaign_id == campaign_id)
|
||||
)
|
||||
registrations_count, total_balance, last_registration = aggregate_result.one()
|
||||
count, total_balance, last_registration = result.one()
|
||||
|
||||
subscription_count_result = await db.execute(
|
||||
select(func.count(AdvertisingCampaignRegistration.id)).where(
|
||||
@@ -221,134 +217,14 @@ async def get_campaign_statistics(
|
||||
)
|
||||
)
|
||||
|
||||
campaign_users_subquery = (
|
||||
select(AdvertisingCampaignRegistration.user_id)
|
||||
.where(AdvertisingCampaignRegistration.campaign_id == campaign_id)
|
||||
)
|
||||
|
||||
revenue_filter = and_(
|
||||
Transaction.user_id.in_(campaign_users_subquery),
|
||||
Transaction.is_completed.is_(True),
|
||||
Transaction.amount_kopeks > 0,
|
||||
Transaction.type.in_(
|
||||
[
|
||||
TransactionType.DEPOSIT.value,
|
||||
TransactionType.SUBSCRIPTION_PAYMENT.value,
|
||||
]
|
||||
),
|
||||
)
|
||||
|
||||
revenue_result = await db.execute(
|
||||
select(func.coalesce(func.sum(Transaction.amount_kopeks), 0)).where(
|
||||
revenue_filter
|
||||
)
|
||||
)
|
||||
revenue_kopeks = revenue_result.scalar() or 0
|
||||
|
||||
transactions_count_result = await db.execute(
|
||||
select(func.count(Transaction.id)).where(revenue_filter)
|
||||
)
|
||||
transactions_count = transactions_count_result.scalar() or 0
|
||||
|
||||
paying_users_result = await db.execute(
|
||||
select(func.count(func.distinct(Transaction.user_id))).where(
|
||||
revenue_filter
|
||||
)
|
||||
)
|
||||
paying_users = paying_users_result.scalar() or 0
|
||||
|
||||
trial_users_result = await db.execute(
|
||||
select(func.count(func.distinct(Subscription.user_id))).where(
|
||||
and_(
|
||||
Subscription.user_id.in_(campaign_users_subquery),
|
||||
Subscription.is_trial.is_(True),
|
||||
)
|
||||
)
|
||||
)
|
||||
trial_users = trial_users_result.scalar() or 0
|
||||
|
||||
paid_flag_users_result = await db.execute(
|
||||
select(func.count(func.distinct(User.id))).where(
|
||||
and_(
|
||||
User.id.in_(campaign_users_subquery),
|
||||
User.has_had_paid_subscription.is_(True),
|
||||
)
|
||||
)
|
||||
)
|
||||
paid_flag_users = paid_flag_users_result.scalar() or 0
|
||||
|
||||
registrations = registrations_count or 0
|
||||
conversion_base = registrations if registrations > 0 else None
|
||||
effective_paying_users = max(paying_users, paid_flag_users or 0)
|
||||
|
||||
conversion_rate = (
|
||||
round((effective_paying_users / registrations) * 100, 1)
|
||||
if conversion_base
|
||||
else 0.0
|
||||
)
|
||||
|
||||
trial_conversion_rate = (
|
||||
round((effective_paying_users / trial_users) * 100, 1)
|
||||
if trial_users
|
||||
else 0.0
|
||||
)
|
||||
|
||||
avg_revenue_per_user = (
|
||||
int(round(revenue_kopeks / registrations)) if registrations else 0
|
||||
)
|
||||
avg_revenue_per_paying_user = (
|
||||
int(round(revenue_kopeks / effective_paying_users))
|
||||
if effective_paying_users
|
||||
else 0
|
||||
)
|
||||
|
||||
return {
|
||||
"registrations": registrations,
|
||||
"registrations": count or 0,
|
||||
"balance_issued": total_balance or 0,
|
||||
"subscription_issued": subscription_count_result.scalar() or 0,
|
||||
"last_registration": last_registration,
|
||||
"revenue_kopeks": revenue_kopeks,
|
||||
"transactions_count": transactions_count,
|
||||
"paying_users": effective_paying_users,
|
||||
"trial_users": trial_users,
|
||||
"conversion_rate": conversion_rate,
|
||||
"trial_conversion_rate": trial_conversion_rate,
|
||||
"avg_revenue_per_user": avg_revenue_per_user,
|
||||
"avg_revenue_per_paying_user": avg_revenue_per_paying_user,
|
||||
}
|
||||
|
||||
|
||||
async def get_campaign_registration_by_user(
|
||||
db: AsyncSession,
|
||||
user_id: int,
|
||||
) -> Optional[AdvertisingCampaignRegistration]:
|
||||
result = await db.execute(
|
||||
select(AdvertisingCampaignRegistration)
|
||||
.options(selectinload(AdvertisingCampaignRegistration.campaign))
|
||||
.where(AdvertisingCampaignRegistration.user_id == user_id)
|
||||
.order_by(AdvertisingCampaignRegistration.created_at.desc())
|
||||
.limit(1)
|
||||
)
|
||||
return result.scalar_one_or_none()
|
||||
|
||||
|
||||
async def get_campaign_registrations_for_users(
|
||||
db: AsyncSession,
|
||||
user_ids: List[int],
|
||||
) -> Dict[int, AdvertisingCampaignRegistration]:
|
||||
if not user_ids:
|
||||
return {}
|
||||
|
||||
result = await db.execute(
|
||||
select(AdvertisingCampaignRegistration)
|
||||
.options(selectinload(AdvertisingCampaignRegistration.campaign))
|
||||
.where(AdvertisingCampaignRegistration.user_id.in_(user_ids))
|
||||
)
|
||||
|
||||
registrations = result.scalars().all()
|
||||
return {registration.user_id: registration for registration in registrations}
|
||||
|
||||
|
||||
async def get_campaigns_overview(db: AsyncSession) -> Dict[str, int]:
|
||||
total = await get_campaigns_count(db)
|
||||
active = await get_campaigns_count(db, is_active=True)
|
||||
|
||||
@@ -333,24 +333,6 @@ async def show_campaign_detail(
|
||||
f"• Выдано баланса: <b>{texts.format_price(stats['balance_issued'])}</b>"
|
||||
)
|
||||
text.append(f"• Выдано подписок: <b>{stats['subscription_issued']}</b>")
|
||||
text.append(f"• Транзакций: <b>{stats['transactions_count']}</b>")
|
||||
text.append(
|
||||
f"• Доход кампании: <b>{texts.format_price(stats['revenue_kopeks'])}</b>"
|
||||
)
|
||||
text.append(
|
||||
f"• Платящих пользователей: <b>{stats['paying_users']}</b>"
|
||||
)
|
||||
text.append(f"• Взяли триал: <b>{stats['trial_users']}</b>")
|
||||
text.append(f"• Конверсия в оплату: <b>{stats['conversion_rate']}%</b>")
|
||||
text.append(
|
||||
f"• Конверсия триала: <b>{stats['trial_conversion_rate']}%</b>"
|
||||
)
|
||||
text.append(
|
||||
f"• Средний доход на регистрацию: <b>{texts.format_price(stats['avg_revenue_per_user'])}</b>"
|
||||
)
|
||||
text.append(
|
||||
f"• Средний доход на платящего: <b>{texts.format_price(stats['avg_revenue_per_paying_user'])}</b>"
|
||||
)
|
||||
if stats["last_registration"]:
|
||||
text.append(
|
||||
f"• Последняя: {stats['last_registration'].strftime('%d.%m.%Y %H:%M')}"
|
||||
@@ -1194,18 +1176,6 @@ async def show_campaign_stats(
|
||||
text.append(f"Регистраций: <b>{stats['registrations']}</b>")
|
||||
text.append(f"Выдано баланса: <b>{texts.format_price(stats['balance_issued'])}</b>")
|
||||
text.append(f"Выдано подписок: <b>{stats['subscription_issued']}</b>")
|
||||
text.append(f"Транзакций: <b>{stats['transactions_count']}</b>")
|
||||
text.append(f"Доход кампании: <b>{texts.format_price(stats['revenue_kopeks'])}</b>")
|
||||
text.append(f"Платящих пользователей: <b>{stats['paying_users']}</b>")
|
||||
text.append(f"Взяли триал: <b>{stats['trial_users']}</b>")
|
||||
text.append(f"Конверсия в оплату: <b>{stats['conversion_rate']}%</b>")
|
||||
text.append(f"Конверсия триала: <b>{stats['trial_conversion_rate']}%</b>")
|
||||
text.append(
|
||||
f"Средний доход на регистрацию: <b>{texts.format_price(stats['avg_revenue_per_user'])}</b>"
|
||||
)
|
||||
text.append(
|
||||
f"Средний доход на платящего: <b>{texts.format_price(stats['avg_revenue_per_paying_user'])}</b>"
|
||||
)
|
||||
if stats["last_registration"]:
|
||||
text.append(
|
||||
f"Последняя регистрация: {stats['last_registration'].strftime('%d.%m.%Y %H:%M')}"
|
||||
|
||||
@@ -17,7 +17,6 @@ from app.keyboards.admin import (
|
||||
from app.localization.texts import get_texts
|
||||
from app.services.user_service import UserService
|
||||
from app.database.crud.promo_group import get_promo_groups_with_counts
|
||||
from app.database.crud.campaign import get_campaign_registrations_for_users
|
||||
from app.utils.decorators import admin_required, error_handler
|
||||
from app.utils.formatters import format_datetime, format_time_ago
|
||||
from app.services.remnawave_service import RemnaWaveService
|
||||
@@ -428,7 +427,6 @@ async def _render_user_subscription_overview(
|
||||
|
||||
user = profile["user"]
|
||||
subscription = profile["subscription"]
|
||||
campaign_registration = profile.get("campaign_registration")
|
||||
|
||||
text = "📱 <b>Подписка и настройки пользователя</b>\n\n"
|
||||
text += f"👤 {user.full_name} (ID: <code>{user.telegram_id}</code>)\n\n"
|
||||
@@ -1238,49 +1236,16 @@ async def show_user_statistics(
|
||||
text += f"• Отсутствует\n"
|
||||
|
||||
text += f"\n<b>Реферальная программа:</b>\n"
|
||||
|
||||
registration_lines = []
|
||||
|
||||
|
||||
if user.referred_by_id:
|
||||
referrer = await get_user_by_id(db, user.referred_by_id)
|
||||
if referrer:
|
||||
registration_lines.append(
|
||||
f"• Пришел по реферальной ссылке от <b>{referrer.full_name}</b>"
|
||||
)
|
||||
text += f"• Пришел по реферальной ссылке от <b>{referrer.full_name}</b>\n"
|
||||
else:
|
||||
registration_lines.append(
|
||||
"• Пришел по реферальной ссылке (реферер не найден)"
|
||||
)
|
||||
|
||||
if campaign_registration and campaign_registration.campaign:
|
||||
campaign = campaign_registration.campaign
|
||||
registration_lines.append(
|
||||
f"• Пришел через рекламную кампанию <b>{campaign.name}</b> (не прямая регистрация)"
|
||||
)
|
||||
registration_lines.append(
|
||||
f"• Участие в кампании: {format_datetime(campaign_registration.created_at)}"
|
||||
)
|
||||
|
||||
if campaign_registration.bonus_type == "balance":
|
||||
registration_lines.append(
|
||||
f"• Бонус кампании: {settings.format_price(campaign_registration.balance_bonus_kopeks)} на баланс"
|
||||
)
|
||||
elif campaign_registration.bonus_type == "subscription":
|
||||
bonus_parts = []
|
||||
if campaign_registration.subscription_duration_days:
|
||||
bonus_parts.append(
|
||||
f"{campaign_registration.subscription_duration_days} дн."
|
||||
)
|
||||
registration_lines.append(
|
||||
"• Бонус кампании: Подписка"
|
||||
+ (f" ({', '.join(bonus_parts)})" if bonus_parts else "")
|
||||
)
|
||||
|
||||
if not registration_lines:
|
||||
registration_lines.append("• Прямая регистрация")
|
||||
|
||||
text += "\n".join(registration_lines) + "\n"
|
||||
|
||||
text += f"• Пришел по реферальной ссылке (реферер не найден)\n"
|
||||
else:
|
||||
text += f"• Прямая регистрация\n"
|
||||
|
||||
text += f"• Реферальный код: <code>{user.referral_code}</code>\n\n"
|
||||
|
||||
if referral_stats['invited_count'] > 0:
|
||||
@@ -1292,15 +1257,11 @@ async def show_user_statistics(
|
||||
|
||||
if referral_stats['referrals_detail']:
|
||||
text += f"\n<b>Детали по рефералам:</b>\n"
|
||||
for detail in referral_stats['referrals_detail'][:5]:
|
||||
for detail in referral_stats['referrals_detail'][:5]:
|
||||
referral_name = detail['referral_name']
|
||||
earned = settings.format_price(detail['total_earned_kopeks'])
|
||||
status = "🟢" if detail['is_active'] else "🔴"
|
||||
text += f"• {status} {referral_name}: {earned}\n"
|
||||
if detail.get('campaign_name'):
|
||||
text += (
|
||||
f" 📣 Кампания: {detail['campaign_name']} (не прямая регистрация)\n"
|
||||
)
|
||||
|
||||
if len(referral_stats['referrals_detail']) > 5:
|
||||
text += f"• ... и еще {len(referral_stats['referrals_detail']) - 5} рефералов\n"
|
||||
@@ -1331,9 +1292,6 @@ async def get_detailed_referral_stats(db: AsyncSession, user_id: int) -> dict:
|
||||
|
||||
referrals_result = await db.execute(referrals_query)
|
||||
referrals = referrals_result.scalars().all()
|
||||
|
||||
referral_ids = [referral.id for referral in referrals]
|
||||
campaign_registrations = await get_campaign_registrations_for_users(db, referral_ids)
|
||||
|
||||
earnings_by_referral = {}
|
||||
all_earnings = await get_referral_earnings_by_user(db, user_id)
|
||||
@@ -1358,11 +1316,6 @@ async def get_detailed_referral_stats(db: AsyncSession, user_id: int) -> dict:
|
||||
referral.subscription.end_date > current_time
|
||||
)
|
||||
|
||||
registration = campaign_registrations.get(referral.id)
|
||||
campaign_name = None
|
||||
if registration and registration.campaign:
|
||||
campaign_name = registration.campaign.name
|
||||
|
||||
referrals_detail.append({
|
||||
'referral_id': referral.id,
|
||||
'referral_name': referral.full_name,
|
||||
@@ -1370,9 +1323,7 @@ async def get_detailed_referral_stats(db: AsyncSession, user_id: int) -> dict:
|
||||
'total_earned_kopeks': earned,
|
||||
'is_active': is_active,
|
||||
'registration_date': referral.created_at,
|
||||
'has_subscription': bool(referral.subscription),
|
||||
'campaign_name': campaign_name,
|
||||
'registration_source': 'campaign' if campaign_name else 'referral'
|
||||
'has_subscription': bool(referral.subscription)
|
||||
})
|
||||
|
||||
referrals_detail.sort(key=lambda x: x['total_earned_kopeks'], reverse=True)
|
||||
|
||||
@@ -297,16 +297,7 @@ async def show_detailed_referral_list(
|
||||
"REFERRAL_LIST_ITEM_ACTIVITY_LONG_AGO",
|
||||
" 🕐 Активность: давно",
|
||||
) + "\n"
|
||||
|
||||
if (
|
||||
referral.get('registration_source') == 'campaign'
|
||||
and referral.get('campaign_name')
|
||||
):
|
||||
text += texts.t(
|
||||
"REFERRAL_LIST_ITEM_SOURCE_CAMPAIGN",
|
||||
" 📣 Источник: рекламная кампания «{name}» (не прямой реферал)",
|
||||
).format(name=referral['campaign_name']) + "\n"
|
||||
|
||||
|
||||
text += "\n"
|
||||
|
||||
keyboard = []
|
||||
|
||||
@@ -368,7 +368,6 @@
|
||||
"REFERRAL_LIST_ITEM_REGISTERED": " 📅 Registered: {days} days ago",
|
||||
"REFERRAL_LIST_ITEM_ACTIVITY": " 🕐 Activity: {days} days ago",
|
||||
"REFERRAL_LIST_ITEM_ACTIVITY_LONG_AGO": " 🕐 Activity: long ago",
|
||||
"REFERRAL_LIST_ITEM_SOURCE_CAMPAIGN": " 📣 Source: advertising campaign “{name}” (not a direct referral)",
|
||||
"REFERRAL_LIST_PREV_PAGE": "⬅️ Back",
|
||||
"REFERRAL_LIST_NEXT_PAGE": "Next ➡️",
|
||||
"REFERRAL_ANALYTICS_TITLE": "📊 <b>Referral analytics</b>",
|
||||
|
||||
@@ -368,7 +368,6 @@
|
||||
"REFERRAL_LIST_ITEM_REGISTERED": " 📅 Регистрация: {days} дн. назад",
|
||||
"REFERRAL_LIST_ITEM_ACTIVITY": " 🕐 Активность: {days} дн. назад",
|
||||
"REFERRAL_LIST_ITEM_ACTIVITY_LONG_AGO": " 🕐 Активность: давно",
|
||||
"REFERRAL_LIST_ITEM_SOURCE_CAMPAIGN": " 📣 Источник: рекламная кампания «{name}» (не прямой реферал)",
|
||||
"REFERRAL_LIST_PREV_PAGE": "⬅️ Назад",
|
||||
"REFERRAL_LIST_NEXT_PAGE": "Вперед ➡️",
|
||||
"REFERRAL_ANALYTICS_TITLE": "📊 <b>Аналитика рефералов</b>",
|
||||
|
||||
@@ -10,7 +10,6 @@ from app.database.crud.user import (
|
||||
get_users_count, get_users_statistics, get_inactive_users,
|
||||
add_user_balance, subtract_user_balance, update_user, delete_user
|
||||
)
|
||||
from app.database.crud.campaign import get_campaign_registration_by_user
|
||||
from app.database.crud.promo_group import get_promo_group_by_id
|
||||
from app.database.crud.transaction import get_user_transactions_count
|
||||
from app.database.crud.subscription import get_subscription_by_user_id
|
||||
@@ -92,15 +91,13 @@ class UserService:
|
||||
|
||||
subscription = await get_subscription_by_user_id(db, user_id)
|
||||
transactions_count = await get_user_transactions_count(db, user_id)
|
||||
campaign_registration = await get_campaign_registration_by_user(db, user_id)
|
||||
|
||||
|
||||
return {
|
||||
"user": user,
|
||||
"subscription": subscription,
|
||||
"transactions_count": transactions_count,
|
||||
"is_admin": settings.is_admin(user.telegram_id),
|
||||
"registration_days": (datetime.utcnow() - user.created_at).days,
|
||||
"campaign_registration": campaign_registration,
|
||||
"registration_days": (datetime.utcnow() - user.created_at).days
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
|
||||
@@ -8,7 +8,6 @@ from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from sqlalchemy.orm import selectinload
|
||||
|
||||
from app.database.models import User, ReferralEarning, Transaction, TransactionType
|
||||
from app.database.crud.campaign import get_campaign_registrations_for_users
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -165,9 +164,6 @@ async def get_detailed_referral_list(db: AsyncSession, user_id: int, limit: int
|
||||
.limit(limit)
|
||||
)
|
||||
referrals = referrals_result.scalars().all()
|
||||
|
||||
referral_ids = [referral.id for referral in referrals]
|
||||
campaign_registrations = await get_campaign_registrations_for_users(db, referral_ids)
|
||||
|
||||
total_count_result = await db.execute(
|
||||
select(func.count(User.id)).where(User.referred_by_id == user_id)
|
||||
@@ -205,11 +201,6 @@ async def get_detailed_referral_list(db: AsyncSession, user_id: int, limit: int
|
||||
if referral.last_activity:
|
||||
days_since_activity = (datetime.utcnow() - referral.last_activity).days
|
||||
|
||||
registration = campaign_registrations.get(referral.id)
|
||||
campaign_name = None
|
||||
if registration and registration.campaign:
|
||||
campaign_name = registration.campaign.name
|
||||
|
||||
detailed_referrals.append({
|
||||
'id': referral.id,
|
||||
'telegram_id': referral.telegram_id,
|
||||
@@ -223,9 +214,7 @@ async def get_detailed_referral_list(db: AsyncSession, user_id: int, limit: int
|
||||
'topups_count': topups_count,
|
||||
'days_since_registration': days_since_registration,
|
||||
'days_since_activity': days_since_activity,
|
||||
'status': 'active' if days_since_activity is not None and days_since_activity <= 30 else 'inactive',
|
||||
'campaign_name': campaign_name,
|
||||
'registration_source': 'campaign' if campaign_name else 'referral'
|
||||
'status': 'active' if days_since_activity is not None and days_since_activity <= 30 else 'inactive'
|
||||
})
|
||||
|
||||
return {
|
||||
|
||||
@@ -414,7 +414,6 @@
|
||||
"REFERRAL_LIST_ITEM_REGISTERED": " 📅 Registered: {days} days ago",
|
||||
"REFERRAL_LIST_ITEM_ACTIVITY": " 🕐 Activity: {days} days ago",
|
||||
"REFERRAL_LIST_ITEM_ACTIVITY_LONG_AGO": " 🕐 Activity: long ago",
|
||||
"REFERRAL_LIST_ITEM_SOURCE_CAMPAIGN": " 📣 Source: advertising campaign “{name}” (not a direct referral)",
|
||||
"REFERRAL_LIST_PREV_PAGE": "⬅️ Back",
|
||||
"REFERRAL_LIST_NEXT_PAGE": "Next ➡️",
|
||||
"REFERRAL_ANALYTICS_TITLE": "📊 <b>Referral analytics</b>",
|
||||
|
||||
@@ -414,7 +414,6 @@
|
||||
"REFERRAL_LIST_ITEM_REGISTERED": " 📅 Регистрация: {days} дн. назад",
|
||||
"REFERRAL_LIST_ITEM_ACTIVITY": " 🕐 Активность: {days} дн. назад",
|
||||
"REFERRAL_LIST_ITEM_ACTIVITY_LONG_AGO": " 🕐 Активность: давно",
|
||||
"REFERRAL_LIST_ITEM_SOURCE_CAMPAIGN": " 📣 Источник: рекламная кампания «{name}» (не прямой реферал)",
|
||||
"REFERRAL_LIST_PREV_PAGE": "⬅️ Назад",
|
||||
"REFERRAL_LIST_NEXT_PAGE": "Вперед ➡️",
|
||||
"REFERRAL_ANALYTICS_TITLE": "📊 <b>Аналитика рефералов</b>",
|
||||
|
||||
Reference in New Issue
Block a user