mirror of
https://github.com/BEDOLAGA-DEV/remnawave-bedolaga-telegram-bot.git
synced 2026-02-26 22:31:44 +00:00
Новый фильтр и кричиеский баг
Теперь при подписке на канал: - ✅ Обычные пользователи — подписка реактивируется - 🚫 Заблокированные — пропуск с логом, подписка НЕ активируется
This commit is contained in:
@@ -70,6 +70,7 @@ class UserFilterType(Enum):
|
||||
SPENDING = 'spending'
|
||||
PURCHASES = 'purchases'
|
||||
CAMPAIGN = 'campaign'
|
||||
POTENTIAL_CUSTOMERS = 'potential_customers'
|
||||
|
||||
|
||||
@dataclass
|
||||
@@ -127,6 +128,13 @@ USER_FILTER_CONFIGS: dict[UserFilterType, UserFilterConfig] = {
|
||||
pagination_prefix='admin_users_campaign_list',
|
||||
order_param='', # использует специальный метод
|
||||
),
|
||||
UserFilterType.POTENTIAL_CUSTOMERS: UserFilterConfig(
|
||||
fsm_state=AdminStates.viewing_user_from_potential_customers_list,
|
||||
title='👥 <b>Потенциальные клиенты</b>',
|
||||
empty_message='💰 Потенциальные клиенты не найдены',
|
||||
pagination_prefix='admin_users_potential_customers_list',
|
||||
order_param='', # использует специальный метод
|
||||
),
|
||||
}
|
||||
|
||||
|
||||
@@ -574,6 +582,125 @@ async def show_users_ready_to_renew(
|
||||
await callback.answer()
|
||||
|
||||
|
||||
@admin_required
|
||||
@error_handler
|
||||
async def show_potential_customers(
|
||||
callback: types.CallbackQuery, db_user: User, db: AsyncSession, state: FSMContext, page: int = 1
|
||||
):
|
||||
"""Показывает пользователей без активной подписки с балансом >= месячной цены."""
|
||||
await state.set_state(AdminStates.viewing_user_from_potential_customers_list)
|
||||
|
||||
texts = get_texts(db_user.language)
|
||||
from app.config import PERIOD_PRICES
|
||||
|
||||
monthly_price = PERIOD_PRICES.get(30, 99000)
|
||||
|
||||
user_service = UserService()
|
||||
users_data = await user_service.get_potential_customers(
|
||||
db,
|
||||
min_balance_kopeks=monthly_price,
|
||||
page=page,
|
||||
limit=10,
|
||||
)
|
||||
|
||||
amount_text = settings.format_price(monthly_price)
|
||||
header = texts.t(
|
||||
'ADMIN_USERS_FILTER_POTENTIAL_CUSTOMERS_TITLE',
|
||||
'💰 Потенциальные клиенты',
|
||||
)
|
||||
description = texts.t(
|
||||
'ADMIN_USERS_FILTER_POTENTIAL_CUSTOMERS_DESC',
|
||||
'Пользователи без активной подписки с балансом {amount} или больше.',
|
||||
).format(amount=amount_text)
|
||||
|
||||
if not users_data['users']:
|
||||
empty_text = texts.t(
|
||||
'ADMIN_USERS_FILTER_POTENTIAL_CUSTOMERS_EMPTY',
|
||||
'Сейчас нет пользователей, которые подходят под этот фильтр.',
|
||||
)
|
||||
await callback.message.edit_text(
|
||||
f'{header}\n\n{description}\n\n{empty_text}',
|
||||
reply_markup=get_admin_users_keyboard(db_user.language),
|
||||
)
|
||||
await callback.answer()
|
||||
return
|
||||
|
||||
text = f'{header}\n\n{description}\n\n'
|
||||
text += 'Нажмите на пользователя для управления:'
|
||||
|
||||
keyboard = []
|
||||
|
||||
for user in users_data['users']:
|
||||
subscription = user.subscription
|
||||
status_emoji = '✅' if user.status == UserStatus.ACTIVE.value else '🚫'
|
||||
subscription_emoji = '❌'
|
||||
|
||||
if subscription:
|
||||
if subscription.is_trial:
|
||||
subscription_emoji = '🎁'
|
||||
elif subscription.is_active:
|
||||
subscription_emoji = '💎'
|
||||
else:
|
||||
subscription_emoji = '⏰'
|
||||
|
||||
button_text = (
|
||||
f'{status_emoji} {subscription_emoji} {user.full_name}'
|
||||
f' | 💰 {settings.format_price(user.balance_kopeks)}'
|
||||
)
|
||||
|
||||
if len(button_text) > 60:
|
||||
short_name = user.full_name
|
||||
if len(short_name) > 20:
|
||||
short_name = short_name[:17] + '...'
|
||||
button_text = f'{status_emoji} {subscription_emoji} {short_name} | 💰 {settings.format_price(user.balance_kopeks)}'
|
||||
|
||||
keyboard.append(
|
||||
[
|
||||
types.InlineKeyboardButton(
|
||||
text=button_text,
|
||||
callback_data=f'admin_user_manage_{user.id}',
|
||||
)
|
||||
]
|
||||
)
|
||||
|
||||
if users_data['total_pages'] > 1:
|
||||
pagination_row = get_admin_pagination_keyboard(
|
||||
users_data['current_page'],
|
||||
users_data['total_pages'],
|
||||
'admin_users_potential_customers_list',
|
||||
'admin_users_potential_customers_filter',
|
||||
db_user.language,
|
||||
).inline_keyboard[0]
|
||||
keyboard.append(pagination_row)
|
||||
|
||||
keyboard.extend(
|
||||
[
|
||||
[
|
||||
types.InlineKeyboardButton(
|
||||
text='🔍 Поиск',
|
||||
callback_data='admin_users_search',
|
||||
),
|
||||
types.InlineKeyboardButton(
|
||||
text='📊 Статистика',
|
||||
callback_data='admin_users_stats',
|
||||
),
|
||||
],
|
||||
[
|
||||
types.InlineKeyboardButton(
|
||||
text='⬅️ Назад',
|
||||
callback_data='admin_users',
|
||||
)
|
||||
],
|
||||
]
|
||||
)
|
||||
|
||||
await callback.message.edit_text(
|
||||
text,
|
||||
reply_markup=types.InlineKeyboardMarkup(inline_keyboard=keyboard),
|
||||
)
|
||||
await callback.answer()
|
||||
|
||||
|
||||
@admin_required
|
||||
@error_handler
|
||||
async def show_users_list_by_traffic(
|
||||
@@ -716,6 +843,19 @@ async def handle_users_ready_to_renew_pagination(
|
||||
await show_users_ready_to_renew(callback, db_user, db, state, 1)
|
||||
|
||||
|
||||
@admin_required
|
||||
@error_handler
|
||||
async def handle_potential_customers_pagination(
|
||||
callback: types.CallbackQuery, db_user: User, db: AsyncSession, state: FSMContext
|
||||
):
|
||||
try:
|
||||
page = int(callback.data.split('_')[-1])
|
||||
await show_potential_customers(callback, db_user, db, state, page)
|
||||
except (ValueError, IndexError) as e:
|
||||
logger.error(f'Ошибка парсинга номера страницы: {e}')
|
||||
await show_potential_customers(callback, db_user, db, state, 1)
|
||||
|
||||
|
||||
@admin_required
|
||||
@error_handler
|
||||
async def handle_users_campaign_list_pagination(
|
||||
@@ -1322,6 +1462,8 @@ async def show_user_management(callback: types.CallbackQuery, db_user: User, db:
|
||||
back_callback = 'admin_users_campaign_filter'
|
||||
elif current_state == AdminStates.viewing_user_from_ready_to_renew_list:
|
||||
back_callback = 'admin_users_ready_to_renew_filter'
|
||||
elif current_state == AdminStates.viewing_user_from_potential_customers_list:
|
||||
back_callback = 'admin_users_potential_customers_filter'
|
||||
|
||||
# Базовая клавиатура профиля
|
||||
kb = get_user_management_keyboard(user.id, user.status, db_user.language, back_callback)
|
||||
@@ -5503,6 +5645,10 @@ def register_handlers(dp: Dispatcher):
|
||||
handle_users_ready_to_renew_pagination, F.data.startswith('admin_users_ready_to_renew_list_page_')
|
||||
)
|
||||
|
||||
dp.callback_query.register(
|
||||
handle_potential_customers_pagination, F.data.startswith('admin_users_potential_customers_list_page_')
|
||||
)
|
||||
|
||||
dp.callback_query.register(
|
||||
handle_users_campaign_list_pagination, F.data.startswith('admin_users_campaign_list_page_')
|
||||
)
|
||||
@@ -5669,4 +5815,6 @@ def register_handlers(dp: Dispatcher):
|
||||
|
||||
dp.callback_query.register(show_users_ready_to_renew, F.data == 'admin_users_ready_to_renew_filter')
|
||||
|
||||
dp.callback_query.register(show_potential_customers, F.data == 'admin_users_potential_customers_filter')
|
||||
|
||||
dp.callback_query.register(show_users_list_by_campaign, F.data == 'admin_users_campaign_filter')
|
||||
|
||||
@@ -372,6 +372,12 @@ def get_admin_users_filters_keyboard(language: str = 'ru') -> InlineKeyboardMark
|
||||
callback_data='admin_users_ready_to_renew_filter',
|
||||
)
|
||||
],
|
||||
[
|
||||
InlineKeyboardButton(
|
||||
text=_t(texts, 'ADMIN_USERS_FILTER_POTENTIAL_CUSTOMERS', '💰 Потенциальные клиенты'),
|
||||
callback_data='admin_users_potential_customers_filter',
|
||||
)
|
||||
],
|
||||
[
|
||||
InlineKeyboardButton(
|
||||
text=_t(texts, 'ADMIN_USERS_FILTER_CAMPAIGN', '📢 По кампании'),
|
||||
|
||||
@@ -15,7 +15,7 @@ from app.database.crud.campaign import get_campaign_by_start_parameter
|
||||
from app.database.crud.subscription import deactivate_subscription, reactivate_subscription
|
||||
from app.database.crud.user import get_user_by_telegram_id
|
||||
from app.database.database import AsyncSessionLocal
|
||||
from app.database.models import SubscriptionStatus
|
||||
from app.database.models import SubscriptionStatus, UserStatus
|
||||
from app.keyboards.inline import get_channel_sub_keyboard
|
||||
from app.localization.loader import DEFAULT_LANGUAGE
|
||||
from app.localization.texts import get_texts
|
||||
@@ -396,6 +396,14 @@ class ChannelCheckerMiddleware(BaseMiddleware):
|
||||
if not user or not user.subscription:
|
||||
return
|
||||
|
||||
# НЕ реактивируем подписку заблокированным пользователям
|
||||
if user.status == UserStatus.BLOCKED.value:
|
||||
logger.info(
|
||||
'🚫 Пропуск реактивации для заблокированного пользователя %s',
|
||||
telegram_id,
|
||||
)
|
||||
return
|
||||
|
||||
subscription = user.subscription
|
||||
|
||||
# Реактивируем только DISABLED подписки
|
||||
|
||||
@@ -344,6 +344,72 @@ class UserService:
|
||||
'total_count': 0,
|
||||
}
|
||||
|
||||
async def get_potential_customers(
|
||||
self,
|
||||
db: AsyncSession,
|
||||
min_balance_kopeks: int,
|
||||
page: int = 1,
|
||||
limit: int = 10,
|
||||
) -> dict[str, Any]:
|
||||
"""Возвращает пользователей без активной подписки с достаточным балансом."""
|
||||
try:
|
||||
offset = (page - 1) * limit
|
||||
|
||||
# Фильтры: нет активной подписки И баланс >= порога
|
||||
base_filters = [
|
||||
User.balance_kopeks >= min_balance_kopeks,
|
||||
]
|
||||
|
||||
# Основной запрос с LEFT JOIN для поддержки пользователей без подписки
|
||||
query = (
|
||||
select(User)
|
||||
.options(selectinload(User.subscription))
|
||||
.outerjoin(Subscription, Subscription.user_id == User.id)
|
||||
.where(
|
||||
*base_filters,
|
||||
or_(
|
||||
User.subscription == None,
|
||||
~Subscription.status.in_(['active', 'trial']),
|
||||
),
|
||||
)
|
||||
.order_by(User.balance_kopeks.desc(), User.created_at.desc())
|
||||
.offset(offset)
|
||||
.limit(limit)
|
||||
)
|
||||
result = await db.execute(query)
|
||||
users = result.scalars().unique().all()
|
||||
|
||||
# Запрос для подсчета общего количества
|
||||
count_query = (
|
||||
select(func.count(User.id))
|
||||
.outerjoin(Subscription, Subscription.user_id == User.id)
|
||||
.where(
|
||||
*base_filters,
|
||||
or_(
|
||||
User.subscription == None,
|
||||
~Subscription.status.in_(['active', 'trial']),
|
||||
),
|
||||
)
|
||||
)
|
||||
total_count = (await db.execute(count_query)).scalar() or 0
|
||||
total_pages = (total_count + limit - 1) // limit if total_count else 0
|
||||
|
||||
return {
|
||||
'users': users,
|
||||
'current_page': page,
|
||||
'total_pages': total_pages,
|
||||
'total_count': total_count,
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f'Ошибка получения потенциальных клиентов: {e}')
|
||||
return {
|
||||
'users': [],
|
||||
'current_page': 1,
|
||||
'total_pages': 1,
|
||||
'total_count': 0,
|
||||
}
|
||||
|
||||
async def get_user_spending_stats_map(self, db: AsyncSession, user_ids: list[int]) -> dict[int, dict[str, int]]:
|
||||
try:
|
||||
return await get_users_spending_stats(db, user_ids)
|
||||
|
||||
@@ -186,6 +186,7 @@ class AdminStates(StatesGroup):
|
||||
viewing_user_from_purchases_list = State()
|
||||
viewing_user_from_campaign_list = State()
|
||||
viewing_user_from_ready_to_renew_list = State()
|
||||
viewing_user_from_potential_customers_list = State()
|
||||
|
||||
# Состояния для управления тарифами
|
||||
creating_tariff_name = State()
|
||||
|
||||
Reference in New Issue
Block a user