mirror of
https://github.com/BEDOLAGA-DEV/remnawave-bedolaga-telegram-bot.git
synced 2026-01-19 19:32:10 +00:00
Новый фильтр Готовы к продлению
This commit is contained in:
@@ -172,6 +172,7 @@ class Settings(BaseSettings):
|
||||
DEFAULT_AUTOPAY_ENABLED: bool = False
|
||||
DEFAULT_AUTOPAY_DAYS_BEFORE: int = 3
|
||||
MIN_BALANCE_FOR_AUTOPAY_KOPEKS: int = 10000
|
||||
SUBSCRIPTION_RENEWAL_BALANCE_THRESHOLD_KOPEKS: int = 20000
|
||||
|
||||
MONITORING_INTERVAL: int = 60
|
||||
INACTIVE_USER_DELETE_MONTHS: int = 3
|
||||
|
||||
@@ -299,6 +299,137 @@ async def show_users_list_by_balance(
|
||||
await callback.answer()
|
||||
|
||||
|
||||
@admin_required
|
||||
@error_handler
|
||||
async def show_users_ready_to_renew(
|
||||
callback: types.CallbackQuery,
|
||||
db_user: User,
|
||||
db: AsyncSession,
|
||||
state: FSMContext,
|
||||
page: int = 1
|
||||
):
|
||||
"""Показывает пользователей с истекшей подпиской и балансом >= порога."""
|
||||
await state.set_state(AdminStates.viewing_user_from_ready_to_renew_list)
|
||||
|
||||
texts = get_texts(db_user.language)
|
||||
threshold = getattr(
|
||||
settings,
|
||||
"SUBSCRIPTION_RENEWAL_BALANCE_THRESHOLD_KOPEKS",
|
||||
20000,
|
||||
)
|
||||
|
||||
user_service = UserService()
|
||||
users_data = await user_service.get_users_ready_to_renew(
|
||||
db,
|
||||
min_balance_kopeks=threshold,
|
||||
page=page,
|
||||
limit=10,
|
||||
)
|
||||
|
||||
amount_text = settings.format_price(threshold)
|
||||
header = texts.t(
|
||||
"ADMIN_USERS_FILTER_RENEW_READY_TITLE",
|
||||
"♻️ Пользователи готовы к продлению",
|
||||
)
|
||||
description = texts.t(
|
||||
"ADMIN_USERS_FILTER_RENEW_READY_DESC",
|
||||
"Подписка истекла, а на балансе осталось {amount} или больше.",
|
||||
).format(amount=amount_text)
|
||||
|
||||
if not users_data["users"]:
|
||||
empty_text = texts.t(
|
||||
"ADMIN_USERS_FILTER_RENEW_READY_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 = []
|
||||
current_time = datetime.utcnow()
|
||||
|
||||
for user in users_data["users"]:
|
||||
subscription = user.subscription
|
||||
status_emoji = "✅" if user.status == UserStatus.ACTIVE.value else "🚫"
|
||||
subscription_emoji = "❌"
|
||||
expired_days = "?"
|
||||
|
||||
if subscription:
|
||||
if subscription.is_trial:
|
||||
subscription_emoji = "🎁"
|
||||
elif subscription.is_active:
|
||||
subscription_emoji = "💎"
|
||||
else:
|
||||
subscription_emoji = "⏰"
|
||||
|
||||
if subscription.end_date:
|
||||
delta = current_time - subscription.end_date
|
||||
expired_days = delta.days
|
||||
|
||||
button_text = (
|
||||
f"{status_emoji} {subscription_emoji} {user.full_name}"
|
||||
f" | 💰 {settings.format_price(user.balance_kopeks)}"
|
||||
f" | ⏰ {expired_days}д ист."
|
||||
)
|
||||
|
||||
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}"
|
||||
f" | 💰 {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_ready_to_renew_list",
|
||||
"admin_users_ready_to_renew_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(
|
||||
@@ -859,6 +990,22 @@ async def handle_users_purchases_list_pagination(
|
||||
await show_users_list_by_purchases(callback, db_user, db, state, 1)
|
||||
|
||||
|
||||
@admin_required
|
||||
@error_handler
|
||||
async def handle_users_ready_to_renew_pagination(
|
||||
callback: types.CallbackQuery,
|
||||
db_user: User,
|
||||
db: AsyncSession,
|
||||
state: FSMContext
|
||||
):
|
||||
try:
|
||||
page = int(callback.data.split('_')[-1])
|
||||
await show_users_ready_to_renew(callback, db_user, db, state, page)
|
||||
except (ValueError, IndexError) as e:
|
||||
logger.error(f"Ошибка парсинга номера страницы: {e}")
|
||||
await show_users_ready_to_renew(callback, db_user, db, state, 1)
|
||||
|
||||
|
||||
@admin_required
|
||||
@error_handler
|
||||
async def handle_users_campaign_list_pagination(
|
||||
@@ -1502,6 +1649,8 @@ async def show_user_management(
|
||||
back_callback = "admin_users_purchases_filter"
|
||||
elif current_state == AdminStates.viewing_user_from_campaign_list:
|
||||
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"
|
||||
|
||||
# Базовая клавиатура профиля
|
||||
kb = get_user_management_keyboard(user.id, user.status, db_user.language, back_callback)
|
||||
@@ -4860,6 +5009,11 @@ def register_handlers(dp: Dispatcher):
|
||||
F.data.startswith("admin_users_purchases_list_page_")
|
||||
)
|
||||
|
||||
dp.callback_query.register(
|
||||
handle_users_ready_to_renew_pagination,
|
||||
F.data.startswith("admin_users_ready_to_renew_list_page_")
|
||||
)
|
||||
|
||||
dp.callback_query.register(
|
||||
handle_users_campaign_list_pagination,
|
||||
F.data.startswith("admin_users_campaign_list_page_")
|
||||
@@ -5131,6 +5285,11 @@ def register_handlers(dp: Dispatcher):
|
||||
show_users_list_by_purchases,
|
||||
F.data == "admin_users_purchases_filter"
|
||||
)
|
||||
|
||||
dp.callback_query.register(
|
||||
show_users_ready_to_renew,
|
||||
F.data == "admin_users_ready_to_renew_filter"
|
||||
)
|
||||
|
||||
dp.callback_query.register(
|
||||
show_users_list_by_campaign,
|
||||
|
||||
@@ -372,6 +372,12 @@ def get_admin_users_filters_keyboard(language: str = "ru") -> InlineKeyboardMark
|
||||
callback_data="admin_users_purchases_filter"
|
||||
)
|
||||
],
|
||||
[
|
||||
InlineKeyboardButton(
|
||||
text=_t(texts, "ADMIN_USERS_FILTER_RENEW_READY", "♻️ Готовы к продлению"),
|
||||
callback_data="admin_users_ready_to_renew_filter"
|
||||
)
|
||||
],
|
||||
[
|
||||
InlineKeyboardButton(
|
||||
text=_t(texts, "ADMIN_USERS_FILTER_CAMPAIGN", "📢 По кампании"),
|
||||
|
||||
@@ -711,6 +711,10 @@
|
||||
"ADMIN_USERS_FILTERS": "⚙️ Filters",
|
||||
"ADMIN_USERS_FILTER_ACTIVITY": "🕒 By activity",
|
||||
"ADMIN_USERS_FILTER_BALANCE": "💰 By balance",
|
||||
"ADMIN_USERS_FILTER_RENEW_READY": "♻️ Ready to renew",
|
||||
"ADMIN_USERS_FILTER_RENEW_READY_TITLE": "♻️ Users ready to renew",
|
||||
"ADMIN_USERS_FILTER_RENEW_READY_DESC": "Their subscription expired and the balance still has {amount} or more.",
|
||||
"ADMIN_USERS_FILTER_RENEW_READY_EMPTY": "No users match this filter right now.",
|
||||
"ADMIN_USERS_FILTER_CAMPAIGN": "📢 By campaign",
|
||||
"ADMIN_USERS_FILTER_PURCHASES": "🛒 By purchases",
|
||||
"ADMIN_USERS_FILTER_SPENDING": "💳 By spending",
|
||||
|
||||
@@ -712,6 +712,10 @@
|
||||
"ADMIN_USERS_FILTERS": "⚙️ Фильтры",
|
||||
"ADMIN_USERS_FILTER_ACTIVITY": "🕒 По активности",
|
||||
"ADMIN_USERS_FILTER_BALANCE": "💰 По балансу",
|
||||
"ADMIN_USERS_FILTER_RENEW_READY": "♻️ Готовы к продлению",
|
||||
"ADMIN_USERS_FILTER_RENEW_READY_TITLE": "♻️ Пользователи готовы к продлению",
|
||||
"ADMIN_USERS_FILTER_RENEW_READY_DESC": "Подписка истекла, а на балансе осталось {amount} или больше.",
|
||||
"ADMIN_USERS_FILTER_RENEW_READY_EMPTY": "Сейчас нет пользователей, которые подходят под этот фильтр.",
|
||||
"ADMIN_USERS_FILTER_CAMPAIGN": "📢 По кампании",
|
||||
"ADMIN_USERS_FILTER_PURCHASES": "🛒 По количеству покупок",
|
||||
"ADMIN_USERS_FILTER_SPENDING": "💳 По сумме трат",
|
||||
|
||||
@@ -710,8 +710,12 @@
|
||||
"ADMIN_USERS_ALL": "👥 Всі користувачі",
|
||||
"ADMIN_USERS_FILTERS": "⚙️ Фільтри",
|
||||
"ADMIN_USERS_FILTER_ACTIVITY": "🕒 За активністю",
|
||||
"ADMIN_USERS_FILTER_BALANCE": "💰 За балансом",
|
||||
"ADMIN_USERS_FILTER_CAMPAIGN": "📢 За кампанією",
|
||||
"ADMIN_USERS_FILTER_BALANCE": "💰 За балансом",
|
||||
"ADMIN_USERS_FILTER_RENEW_READY": "♻️ Готові до продовження",
|
||||
"ADMIN_USERS_FILTER_RENEW_READY_TITLE": "♻️ Користувачі, готові до продовження",
|
||||
"ADMIN_USERS_FILTER_RENEW_READY_DESC": "Підписка вже закінчилась, а на балансі залишилось {amount} або більше.",
|
||||
"ADMIN_USERS_FILTER_RENEW_READY_EMPTY": "Наразі немає користувачів, які підходять під цей фільтр.",
|
||||
"ADMIN_USERS_FILTER_CAMPAIGN": "📢 За кампанією",
|
||||
"ADMIN_USERS_FILTER_PURCHASES": "🛒 За кількістю покупок",
|
||||
"ADMIN_USERS_FILTER_SPENDING": "💳 За сумою витрат",
|
||||
"ADMIN_USERS_FILTER_TRAFFIC": "📶 За трафіком",
|
||||
@@ -1532,4 +1536,4 @@
|
||||
"POLL_ERROR": "Не вдалося обробити опитування. Спробуйте пізніше.",
|
||||
"POLL_COMPLETED": "🙏 Дякуємо за участь в опитуванні!",
|
||||
"POLL_REWARD_GRANTED": "Нагороду {amount} зараховано на ваш баланс."
|
||||
}
|
||||
}
|
||||
|
||||
@@ -710,6 +710,10 @@
|
||||
"ADMIN_USERS_FILTERS":"⚙️筛选器",
|
||||
"ADMIN_USERS_FILTER_ACTIVITY":"🕒按活跃度",
|
||||
"ADMIN_USERS_FILTER_BALANCE":"💰按余额",
|
||||
"ADMIN_USERS_FILTER_RENEW_READY":"♻️准备续费",
|
||||
"ADMIN_USERS_FILTER_RENEW_READY_TITLE":"♻️准备续费的用户",
|
||||
"ADMIN_USERS_FILTER_RENEW_READY_DESC":"订阅已到期,但余额仍不少于{amount}。",
|
||||
"ADMIN_USERS_FILTER_RENEW_READY_EMPTY":"目前没有符合条件的用户。",
|
||||
"ADMIN_USERS_FILTER_CAMPAIGN":"📢按活动",
|
||||
"ADMIN_USERS_FILTER_PURCHASES":"🛒按购买次数",
|
||||
"ADMIN_USERS_FILTER_SPENDING":"💳按消费金额",
|
||||
@@ -1860,4 +1864,4 @@
|
||||
"POLL_REWARD_GRANTED":"奖励{amount}已存入您的余额。",
|
||||
"DEVICE_GUIDE_WINDOWS":"💻Windows",
|
||||
"REFERRAL_LIST_ITEM_ACTIVITY_LONG_AGO":"🕐活跃:很久以前"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ from datetime import datetime, timedelta
|
||||
from typing import Optional, List, Dict, Any, Tuple
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from sqlalchemy import delete, select, update, func
|
||||
from sqlalchemy.orm import selectinload
|
||||
from aiogram import Bot, types
|
||||
from aiogram.exceptions import TelegramBadRequest, TelegramForbiddenError
|
||||
from app.database.crud.user import (
|
||||
@@ -218,6 +219,60 @@ class UserService:
|
||||
"has_prev": False
|
||||
}
|
||||
|
||||
async def get_users_ready_to_renew(
|
||||
self,
|
||||
db: AsyncSession,
|
||||
min_balance_kopeks: int,
|
||||
page: int = 1,
|
||||
limit: int = 20,
|
||||
) -> Dict[str, Any]:
|
||||
"""Возвращает пользователей с истекшей подпиской и достаточным балансом."""
|
||||
try:
|
||||
offset = (page - 1) * limit
|
||||
now = datetime.utcnow()
|
||||
|
||||
base_filters = [
|
||||
User.balance_kopeks >= min_balance_kopeks,
|
||||
Subscription.end_date.isnot(None),
|
||||
Subscription.end_date <= now,
|
||||
]
|
||||
|
||||
query = (
|
||||
select(User)
|
||||
.options(selectinload(User.subscription))
|
||||
.join(Subscription, Subscription.user_id == User.id)
|
||||
.where(*base_filters)
|
||||
.order_by(User.balance_kopeks.desc(), Subscription.end_date.asc())
|
||||
.offset(offset)
|
||||
.limit(limit)
|
||||
)
|
||||
result = await db.execute(query)
|
||||
users = result.scalars().all()
|
||||
|
||||
count_query = (
|
||||
select(func.count(User.id))
|
||||
.join(Subscription, Subscription.user_id == User.id)
|
||||
.where(*base_filters)
|
||||
)
|
||||
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,
|
||||
|
||||
@@ -142,6 +142,7 @@ class AdminStates(StatesGroup):
|
||||
viewing_user_from_spending_list = State()
|
||||
viewing_user_from_purchases_list = State()
|
||||
viewing_user_from_campaign_list = State()
|
||||
viewing_user_from_ready_to_renew_list = State()
|
||||
|
||||
class SupportStates(StatesGroup):
|
||||
waiting_for_message = State()
|
||||
|
||||
Reference in New Issue
Block a user