Clear referral edit state when returning to list

This commit is contained in:
Egor
2025-11-01 01:58:08 +03:00
parent ac43f17a43
commit 165691412b
6 changed files with 532 additions and 5 deletions
+26 -2
View File
@@ -62,10 +62,34 @@ async def get_user_by_telegram_id(db: AsyncSession, telegram_id: int) -> Optiona
.where(User.telegram_id == telegram_id)
)
user = result.scalar_one_or_none()
if user and user.subscription:
_ = user.subscription.is_active
return user
async def get_user_by_username(db: AsyncSession, username: str) -> Optional[User]:
if not username:
return None
normalized = username.lower()
result = await db.execute(
select(User)
.options(
selectinload(User.subscription),
selectinload(User.promo_group),
selectinload(User.referrer),
)
.where(func.lower(User.username) == normalized)
)
user = result.scalar_one_or_none()
if user and user.subscription:
_ = user.subscription.is_active
return user
+412 -2
View File
@@ -1,6 +1,7 @@
import logging
import re
from datetime import datetime, timedelta
from typing import Optional
from typing import Optional, List, Tuple
from aiogram import Dispatcher, types, F
from aiogram.exceptions import TelegramForbiddenError, TelegramBadRequest
from aiogram.types import InlineKeyboardMarkup, InlineKeyboardButton
@@ -10,7 +11,12 @@ from sqlalchemy.ext.asyncio import AsyncSession
from app.config import settings
from app.states import AdminStates
from app.database.models import User, UserStatus, Subscription, SubscriptionStatus, TransactionType
from app.database.crud.user import get_user_by_id
from app.database.crud.user import (
get_user_by_id,
get_user_by_telegram_id,
get_user_by_username,
get_referrals,
)
from app.database.crud.campaign import (
get_campaign_registration_by_user,
get_campaign_statistics,
@@ -1489,6 +1495,395 @@ async def show_user_management(
await callback.answer()
async def _build_user_referrals_view(
db: AsyncSession,
language: str,
user_id: int,
limit: int = 30,
) -> Optional[Tuple[str, InlineKeyboardMarkup]]:
texts = get_texts(language)
user = await get_user_by_id(db, user_id)
if not user:
return None
referrals = await get_referrals(db, user_id)
header = texts.t(
"ADMIN_USER_REFERRALS_TITLE",
"🤝 <b>Рефералы пользователя</b>",
)
summary = texts.t(
"ADMIN_USER_REFERRALS_SUMMARY",
"👤 {name} (ID: <code>{telegram_id}</code>)\n👥 Всего рефералов: {count}",
).format(
name=user.full_name,
telegram_id=user.telegram_id,
count=len(referrals),
)
lines: List[str] = [header, summary]
if referrals:
lines.append(
texts.t(
"ADMIN_USER_REFERRALS_LIST_HEADER",
"<b>Список рефералов:</b>",
)
)
items = []
for referral in referrals[:limit]:
username_part = (
f", @{referral.username}"
if referral.username
else ""
)
items.append(
texts.t(
"ADMIN_USER_REFERRALS_LIST_ITEM",
"{name} (ID: <code>{telegram_id}</code>{username_part})",
).format(
name=referral.full_name,
telegram_id=referral.telegram_id,
username_part=username_part,
)
)
lines.append("\n".join(items))
if len(referrals) > limit:
remaining = len(referrals) - limit
lines.append(
texts.t(
"ADMIN_USER_REFERRALS_LIST_TRUNCATED",
"• … и ещё {count} рефералов",
).format(count=remaining)
)
else:
lines.append(
texts.t(
"ADMIN_USER_REFERRALS_EMPTY",
"Рефералов пока нет.",
)
)
lines.append(
texts.t(
"ADMIN_USER_REFERRALS_EDIT_HINT",
"✏️ Чтобы изменить список, нажмите «✏️ Редактировать» ниже.",
)
)
keyboard = InlineKeyboardMarkup(
inline_keyboard=[
[
InlineKeyboardButton(
text=texts.t(
"ADMIN_USER_REFERRALS_EDIT_BUTTON",
"✏️ Редактировать",
),
callback_data=f"admin_user_referrals_edit_{user_id}",
)
],
[
InlineKeyboardButton(
text=texts.BACK,
callback_data=f"admin_user_manage_{user_id}",
)
],
]
)
return "\n\n".join(lines), keyboard
@admin_required
@error_handler
async def show_user_referrals(
callback: types.CallbackQuery,
db_user: User,
db: AsyncSession,
state: FSMContext,
):
user_id = int(callback.data.split('_')[-1])
current_state = await state.get_state()
if current_state == AdminStates.editing_user_referrals:
data = await state.get_data()
preserved_data = {
key: value
for key, value in data.items()
if key not in {"editing_referrals_user_id", "referrals_message_id"}
}
await state.clear()
if preserved_data:
await state.update_data(**preserved_data)
view = await _build_user_referrals_view(db, db_user.language, user_id)
if not view:
await callback.answer("❌ Пользователь не найден", show_alert=True)
return
text, keyboard = view
await callback.message.edit_text(
text,
reply_markup=keyboard,
)
await callback.answer()
@admin_required
@error_handler
async def start_edit_user_referrals(
callback: types.CallbackQuery,
db_user: User,
state: FSMContext,
db: AsyncSession,
):
user_id = int(callback.data.split('_')[-1])
user = await get_user_by_id(db, user_id)
if not user:
await callback.answer("❌ Пользователь не найден", show_alert=True)
return
texts = get_texts(db_user.language)
prompt = texts.t(
"ADMIN_USER_REFERRALS_EDIT_PROMPT",
(
"✏️ <b>Редактирование рефералов</b>\n\n"
"Отправьте список рефералов для пользователя <b>{name}</b> (ID: <code>{telegram_id}</code>):\n"
"• Используйте TG ID или @username\n"
"• Значения можно указывать через запятую, пробел или с новой строки\n"
"• Чтобы очистить список, отправьте 0 или слово 'нет'\n\n"
"Или нажмите кнопку ниже, чтобы отменить."
),
).format(
name=user.full_name,
telegram_id=user.telegram_id,
)
await state.update_data(
editing_referrals_user_id=user_id,
referrals_message_id=callback.message.message_id,
)
await callback.message.edit_text(
prompt,
reply_markup=InlineKeyboardMarkup(
inline_keyboard=[
[
InlineKeyboardButton(
text=texts.BACK,
callback_data=f"admin_user_referrals_{user_id}",
)
]
]
),
)
await state.set_state(AdminStates.editing_user_referrals)
await callback.answer()
@admin_required
@error_handler
async def process_edit_user_referrals(
message: types.Message,
db_user: User,
state: FSMContext,
db: AsyncSession,
):
texts = get_texts(db_user.language)
data = await state.get_data()
user_id = data.get("editing_referrals_user_id")
if not user_id:
await message.answer(
texts.t(
"ADMIN_USER_REFERRALS_STATE_LOST",
"❌ Не удалось определить пользователя. Попробуйте начать сначала.",
)
)
await state.clear()
return
raw_text = message.text.strip()
lower_text = raw_text.lower()
clear_keywords = {"0", "нет", "none", "пусто", "clear"}
clear_requested = lower_text in clear_keywords
tokens: List[str] = []
if not clear_requested:
parts = re.split(r"[,\n]+", raw_text)
for part in parts:
for token in part.split():
cleaned = token.strip()
if cleaned and cleaned not in tokens:
tokens.append(cleaned)
found_users: List[User] = []
not_found: List[str] = []
skipped_self: List[str] = []
duplicate_tokens: List[str] = []
seen_ids = set()
for token in tokens:
normalized = token.strip()
if not normalized:
continue
if normalized.startswith("@"):
normalized = normalized[1:]
user = None
if normalized.isdigit():
try:
user = await get_user_by_telegram_id(db, int(normalized))
except ValueError:
user = None
else:
user = await get_user_by_username(db, normalized)
if not user:
not_found.append(token)
continue
if user.id == user_id:
skipped_self.append(token)
continue
if user.id in seen_ids:
duplicate_tokens.append(token)
continue
seen_ids.add(user.id)
found_users.append(user)
if not found_users and not clear_requested:
error_lines = [
texts.t(
"ADMIN_USER_REFERRALS_NO_VALID",
"❌ Не удалось найти ни одного пользователя по введённым данным.",
)
]
if not_found:
error_lines.append(
texts.t(
"ADMIN_USER_REFERRALS_INVALID_ENTRIES",
"Не найдены: {values}",
).format(values=", ".join(not_found))
)
if skipped_self:
error_lines.append(
texts.t(
"ADMIN_USER_REFERRALS_SELF_SKIPPED",
"Пропущены значения пользователя: {values}",
).format(values=", ".join(skipped_self))
)
await message.answer("\n".join(error_lines))
return
user_service = UserService()
new_referral_ids = [user.id for user in found_users] if not clear_requested else []
success, details = await user_service.update_user_referrals(
db,
user_id,
new_referral_ids,
db_user.id,
)
if not success:
await message.answer(
texts.t(
"ADMIN_USER_REFERRALS_UPDATE_ERROR",
"❌ Не удалось обновить рефералов. Попробуйте позже.",
)
)
return
response_lines = [
texts.t(
"ADMIN_USER_REFERRALS_UPDATED",
"✅ Список рефералов обновлён.",
)
]
total_referrals = details.get("total", len(new_referral_ids))
added = details.get("added", 0)
removed = details.get("removed", 0)
response_lines.append(
texts.t(
"ADMIN_USER_REFERRALS_UPDATED_TOTAL",
"• Текущий список: {total}",
).format(total=total_referrals)
)
if added > 0:
response_lines.append(
texts.t(
"ADMIN_USER_REFERRALS_UPDATED_ADDED",
"• Добавлено: {count}",
).format(count=added)
)
if removed > 0:
response_lines.append(
texts.t(
"ADMIN_USER_REFERRALS_UPDATED_REMOVED",
"• Удалено: {count}",
).format(count=removed)
)
if not_found:
response_lines.append(
texts.t(
"ADMIN_USER_REFERRALS_INVALID_ENTRIES",
"Не найдены: {values}",
).format(values=", ".join(not_found))
)
if skipped_self:
response_lines.append(
texts.t(
"ADMIN_USER_REFERRALS_SELF_SKIPPED",
"Пропущены значения пользователя: {values}",
).format(values=", ".join(skipped_self))
)
if duplicate_tokens:
response_lines.append(
texts.t(
"ADMIN_USER_REFERRALS_DUPLICATES",
"Игнорированы дубли: {values}",
).format(values=", ".join(duplicate_tokens))
)
view = await _build_user_referrals_view(db, db_user.language, user_id)
message_id = data.get("referrals_message_id")
if view and message_id:
try:
await message.bot.edit_message_text(
view[0],
chat_id=message.chat.id,
message_id=message_id,
reply_markup=view[1],
)
except TelegramBadRequest:
await message.answer(view[0], reply_markup=view[1])
elif view:
await message.answer(view[0], reply_markup=view[1])
await message.answer("\n".join(response_lines))
await state.clear()
async def _render_user_promo_group(
message: types.Message,
language: str,
@@ -4159,6 +4554,21 @@ def register_handlers(dp: Dispatcher):
AdminStates.editing_user_balance
)
dp.callback_query.register(
show_user_referrals,
F.data.startswith("admin_user_referrals_") & ~F.data.contains("_edit")
)
dp.callback_query.register(
start_edit_user_referrals,
F.data.startswith("admin_user_referrals_edit_")
)
dp.message.register(
process_edit_user_referrals,
AdminStates.editing_user_referrals
)
dp.callback_query.register(
start_send_user_message,
F.data.startswith("admin_user_send_message_")
+6
View File
@@ -760,6 +760,12 @@ def get_user_management_keyboard(user_id: int, user_status: str, language: str =
callback_data=f"admin_user_promo_group_{user_id}"
)
],
[
InlineKeyboardButton(
text=_t(texts, "ADMIN_USER_REFERRALS_BUTTON", "🤝 Рефералы"),
callback_data=f"admin_user_referrals_{user_id}"
)
],
[
InlineKeyboardButton(
text=_t(texts, "ADMIN_USER_STATISTICS", "📊 Статистика"),
+20
View File
@@ -713,6 +713,26 @@
"ADMIN_USER_PROMO_GROUP_ALREADY": "ℹ️ Пользователь уже состоит в этой промогруппе.",
"ADMIN_USER_PROMO_GROUP_BACK": "⬅️ К пользователю",
"ADMIN_USER_PROMO_GROUP_BUTTON": "👥 Промогруппа",
"ADMIN_USER_REFERRALS_BUTTON": "🤝 Рефералы",
"ADMIN_USER_REFERRALS_TITLE": "🤝 <b>Рефералы пользователя</b>",
"ADMIN_USER_REFERRALS_SUMMARY": "👤 {name} (ID: <code>{telegram_id}</code>)\n👥 Всего рефералов: {count}",
"ADMIN_USER_REFERRALS_LIST_HEADER": "<b>Список рефералов:</b>",
"ADMIN_USER_REFERRALS_LIST_ITEM": "• {name} (ID: <code>{telegram_id}</code>{username_part})",
"ADMIN_USER_REFERRALS_LIST_TRUNCATED": "• … и ещё {count} рефералов",
"ADMIN_USER_REFERRALS_EMPTY": "Рефералов пока нет.",
"ADMIN_USER_REFERRALS_EDIT_HINT": "✏️ Чтобы изменить список, нажмите «✏️ Редактировать» ниже.",
"ADMIN_USER_REFERRALS_EDIT_BUTTON": "✏️ Редактировать",
"ADMIN_USER_REFERRALS_EDIT_PROMPT": "✏️ <b>Редактирование рефералов</b>\n\nОтправьте список рефералов для пользователя <b>{name}</b> (ID: <code>{telegram_id}</code>):\n• Используйте TG ID или @username\n• Значения можно указывать через запятую, пробел или с новой строки\n• Чтобы очистить список, отправьте 0 или слово 'нет'\n\nИли нажмите кнопку ниже, чтобы отменить.",
"ADMIN_USER_REFERRALS_STATE_LOST": "❌ Не удалось определить пользователя. Попробуйте начать сначала.",
"ADMIN_USER_REFERRALS_NO_VALID": "❌ Не удалось найти ни одного пользователя по введённым данным.",
"ADMIN_USER_REFERRALS_INVALID_ENTRIES": "Не найдены: {values}",
"ADMIN_USER_REFERRALS_SELF_SKIPPED": "Пропущены значения пользователя: {values}",
"ADMIN_USER_REFERRALS_DUPLICATES": "Игнорированы дубли: {values}",
"ADMIN_USER_REFERRALS_UPDATE_ERROR": "❌ Не удалось обновить рефералов. Попробуйте позже.",
"ADMIN_USER_REFERRALS_UPDATED": "✅ Список рефералов обновлён.",
"ADMIN_USER_REFERRALS_UPDATED_TOTAL": "• Текущий список: {total}",
"ADMIN_USER_REFERRALS_UPDATED_ADDED": "• Добавлено: {count}",
"ADMIN_USER_REFERRALS_UPDATED_REMOVED": "• Удалено: {count}",
"ADMIN_USER_PROMO_GROUP_CURRENT": "Текущая группа: {name}",
"ADMIN_USER_PROMO_GROUP_CURRENT_NONE": "Текущая группа: не назначена",
"ADMIN_USER_PROMO_GROUP_DISCOUNTS": "Скидки — серверы: {servers}%, трафик: {traffic}%, устройства: {devices}%",
+67 -1
View File
@@ -9,7 +9,7 @@ from app.database.crud.user import (
get_user_by_id, get_user_by_telegram_id, get_users_list,
get_users_count, get_users_statistics, get_inactive_users,
add_user_balance, subtract_user_balance, update_user, delete_user,
get_users_spending_stats
get_users_spending_stats, get_referrals
)
from app.database.crud.promo_group import get_promo_group_by_id
from app.database.crud.transaction import get_user_transactions_count
@@ -411,6 +411,72 @@ class UserService:
logger.error(f"Ошибка обновления промогруппы пользователя {user_id}: {e}")
return False, None, None, None
async def update_user_referrals(
self,
db: AsyncSession,
user_id: int,
referral_user_ids: List[int],
admin_id: int,
) -> Tuple[bool, Dict[str, int]]:
try:
user = await get_user_by_id(db, user_id)
if not user:
return False, {"error": "user_not_found"}
unique_ids: List[int] = []
for referral_id in referral_user_ids:
if referral_id == user_id:
continue
if referral_id not in unique_ids:
unique_ids.append(referral_id)
current_referrals = await get_referrals(db, user_id)
current_ids = {ref.id for ref in current_referrals}
to_assign = unique_ids
to_remove = [rid for rid in current_ids if rid not in unique_ids]
to_add = [rid for rid in unique_ids if rid not in current_ids]
if to_assign:
await db.execute(
update(User)
.where(User.id.in_(to_assign))
.values(referred_by_id=user_id)
)
if to_remove:
await db.execute(
update(User)
.where(User.id.in_(to_remove))
.values(referred_by_id=None)
)
await db.commit()
logger.info(
"Админ %s обновил рефералов пользователя %s: добавлено %s, удалено %s, всего %s",
admin_id,
user_id,
len(to_add),
len(to_remove),
len(unique_ids),
)
return True, {
"added": len(to_add),
"removed": len(to_remove),
"total": len(unique_ids),
}
except Exception as e:
await db.rollback()
logger.error(
"Ошибка обновления рефералов пользователя %s: %s",
user_id,
e,
)
return False, {"error": "update_failed"}
async def block_user(
self,
db: AsyncSession,
+1
View File
@@ -90,6 +90,7 @@ class AdminStates(StatesGroup):
editing_device_price = State()
editing_user_devices = State()
editing_user_traffic = State()
editing_user_referrals = State()
editing_rules_page = State()
editing_privacy_policy = State()