Compare commits
2 Commits
4e2c8981c2
...
ca0c63a89c
| Author | SHA1 | Date | |
|---|---|---|---|
| ca0c63a89c | |||
| c0407fdb11 |
@@ -0,0 +1,26 @@
|
||||
"""add_nickname_to_users
|
||||
|
||||
Revision ID: 64c4f8a81afa
|
||||
Revises: beb47ddbfc33
|
||||
Create Date: 2026-02-09 20:10:36.120201
|
||||
|
||||
"""
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = '64c4f8a81afa'
|
||||
down_revision = 'beb47ddbfc33'
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
# Добавляем поле nickname в таблицу users
|
||||
op.add_column('users', sa.Column('nickname', sa.String(length=100), nullable=True))
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
# Удаляем поле nickname из таблицы users
|
||||
op.drop_column('users', 'nickname')
|
||||
@@ -14,6 +14,7 @@ class User(Base):
|
||||
username = Column(String(255))
|
||||
first_name = Column(String(255))
|
||||
last_name = Column(String(255))
|
||||
nickname = Column(String(100), nullable=True) # Никнейм пользователя для чата
|
||||
phone = Column(String(20), nullable=True) # Телефон для верификации
|
||||
club_card_number = Column(String(50), unique=True, nullable=True, index=True) # Номер клубной карты
|
||||
is_registered = Column(Boolean, default=False) # Прошел ли полную регистрацию
|
||||
|
||||
@@ -13,7 +13,7 @@ class UserService:
|
||||
@staticmethod
|
||||
async def get_or_create_user(session: AsyncSession, telegram_id: int,
|
||||
username: str = None, first_name: str = None,
|
||||
last_name: str = None) -> User:
|
||||
last_name: str = None, nickname: str = None) -> User:
|
||||
"""Получить или создать пользователя"""
|
||||
# Пробуем найти существующего пользователя
|
||||
result = await session.execute(
|
||||
@@ -26,6 +26,9 @@ class UserService:
|
||||
user.username = username
|
||||
user.first_name = first_name
|
||||
user.last_name = last_name
|
||||
# Обновляем nickname только если он передан
|
||||
if nickname is not None:
|
||||
user.nickname = nickname
|
||||
await session.commit()
|
||||
return user
|
||||
|
||||
@@ -34,7 +37,8 @@ class UserService:
|
||||
telegram_id=telegram_id,
|
||||
username=username,
|
||||
first_name=first_name,
|
||||
last_name=last_name
|
||||
last_name=last_name,
|
||||
nickname=nickname
|
||||
)
|
||||
session.add(user)
|
||||
await session.commit()
|
||||
|
||||
@@ -82,6 +82,12 @@ class AdminStates(StatesGroup):
|
||||
lottery_display_type_select = State()
|
||||
lottery_display_type_set = State()
|
||||
|
||||
# Массовая рассылка
|
||||
broadcast_message = State()
|
||||
|
||||
# Импорт/экспорт пользователей
|
||||
import_users_json = State()
|
||||
|
||||
|
||||
admin_router = Router()
|
||||
|
||||
@@ -98,6 +104,7 @@ def get_admin_main_keyboard() -> InlineKeyboardMarkup:
|
||||
[InlineKeyboardButton(text="👥 Управление участниками", callback_data="admin_participants")],
|
||||
[InlineKeyboardButton(text="👑 Управление победителями", callback_data="admin_winners")],
|
||||
[InlineKeyboardButton(text="📊 Статистика", callback_data="admin_stats")],
|
||||
[InlineKeyboardButton(text="📢 Рассылка", callback_data="admin_broadcast")],
|
||||
[InlineKeyboardButton(text="⚙️ Настройки", callback_data="admin_settings")],
|
||||
[InlineKeyboardButton(text="🔙 Назад", callback_data="back_to_main")]
|
||||
]
|
||||
@@ -2294,7 +2301,12 @@ async def process_winner_place(message: Message, state: FSMContext):
|
||||
|
||||
text = f"👑 Установка победителя на {place} место\n"
|
||||
text += f"🎯 Розыгрыш: {lottery.title}\n\n"
|
||||
text += f"Введите Telegram ID или username пользователя:"
|
||||
text += (
|
||||
"Введите один из вариантов:\n"
|
||||
"• Telegram ID (числовой ID)\n"
|
||||
"• Username (с @ или без)\n"
|
||||
"• Номер счета (формат: XX-XX-XX-XX-XX-XX-XX)"
|
||||
)
|
||||
|
||||
await message.answer(text)
|
||||
await state.set_state(AdminStates.set_winner_user)
|
||||
@@ -2302,45 +2314,70 @@ async def process_winner_place(message: Message, state: FSMContext):
|
||||
|
||||
@admin_router.message(StateFilter(AdminStates.set_winner_user))
|
||||
async def process_winner_user(message: Message, state: FSMContext):
|
||||
"""Обработка пользователя-победителя"""
|
||||
"""Обработка пользователя-победителя (по ID, username или номеру счета)"""
|
||||
if not is_admin(message.from_user.id):
|
||||
await message.answer("❌ Недостаточно прав")
|
||||
return
|
||||
|
||||
user_input = message.text.strip()
|
||||
|
||||
# Пробуем определить, это ID или username
|
||||
if user_input.startswith('@'):
|
||||
user_input = user_input[1:] # Убираем @
|
||||
is_username = True
|
||||
elif user_input.isdigit():
|
||||
is_username = False
|
||||
telegram_id = int(user_input)
|
||||
# Проверяем, это номер счета (формат XX-XX-XX-XX-XX-XX-XX)
|
||||
is_account = '-' in user_input and len(user_input.split('-')) >= 5
|
||||
|
||||
if is_account:
|
||||
# Обработка по номеру счета
|
||||
from ..core.registration_services import AccountService
|
||||
|
||||
async with async_session_maker() as session:
|
||||
# Ищем владельца счета
|
||||
owner = await AccountService.get_account_owner(session, user_input)
|
||||
|
||||
if not owner:
|
||||
await message.answer(
|
||||
f"❌ Счет {user_input} не найден в системе.\n"
|
||||
f"Проверьте правильность номера счета."
|
||||
)
|
||||
return
|
||||
|
||||
telegram_id = owner.telegram_id
|
||||
display_name = owner.nickname if owner.nickname else (f"@{owner.username}" if owner.username else owner.first_name)
|
||||
else:
|
||||
is_username = True
|
||||
|
||||
async with async_session_maker() as session:
|
||||
if is_username:
|
||||
# Поиск по username
|
||||
from sqlalchemy import select
|
||||
from ..core.models import User
|
||||
|
||||
result = await session.execute(
|
||||
select(User).where(User.username == user_input)
|
||||
)
|
||||
user = result.scalar_one_or_none()
|
||||
|
||||
if not user:
|
||||
await message.answer("❌ Пользователь с таким username не найден")
|
||||
return
|
||||
|
||||
telegram_id = user.telegram_id
|
||||
# Обработка по ID или username
|
||||
# Пробуем определить, это ID или username
|
||||
if user_input.startswith('@'):
|
||||
user_input = user_input[1:] # Убираем @
|
||||
is_username = True
|
||||
elif user_input.isdigit():
|
||||
is_username = False
|
||||
telegram_id = int(user_input)
|
||||
else:
|
||||
user = await UserService.get_user_by_telegram_id(session, telegram_id)
|
||||
is_username = True
|
||||
|
||||
if not user:
|
||||
await message.answer("❌ Пользователь с таким ID не найден")
|
||||
return
|
||||
async with async_session_maker() as session:
|
||||
if is_username:
|
||||
# Поиск по username
|
||||
from sqlalchemy import select
|
||||
from ..core.models import User
|
||||
|
||||
result = await session.execute(
|
||||
select(User).where(User.username == user_input)
|
||||
)
|
||||
user = result.scalar_one_or_none()
|
||||
|
||||
if not user:
|
||||
await message.answer("❌ Пользователь с таким username не найден")
|
||||
return
|
||||
|
||||
telegram_id = user.telegram_id
|
||||
display_name = user.nickname if user.nickname else (f"@{user.username}" if user.username else user.first_name)
|
||||
else:
|
||||
user = await UserService.get_user_by_telegram_id(session, telegram_id)
|
||||
|
||||
if not user:
|
||||
await message.answer("❌ Пользователь с таким ID не найден")
|
||||
return
|
||||
|
||||
display_name = user.nickname if user.nickname else (f"@{user.username}" if user.username else user.first_name)
|
||||
|
||||
data = await state.get_data()
|
||||
|
||||
@@ -2355,13 +2392,13 @@ async def process_winner_user(message: Message, state: FSMContext):
|
||||
await state.clear()
|
||||
|
||||
if success:
|
||||
username = f"@{user.username}" if user.username else user.first_name
|
||||
await message.answer(
|
||||
f"✅ Предопределенный победитель установлен!\n\n"
|
||||
f"🏆 Место: {data['place']}\n"
|
||||
f"👤 Пользователь: {username}\n"
|
||||
f"🆔 ID: {telegram_id}\n\n"
|
||||
f"При проведении розыгрыша этот пользователь автоматически займет {data['place']} место.",
|
||||
f"👤 Пользователь: {display_name}\n"
|
||||
f"🆔 ID: {telegram_id}\n"
|
||||
+ (f"💳 Счет: {user_input}\n" if is_account else "") +
|
||||
f"\n⚡ При проведении розыгрыша этот пользователь автоматически займет {data['place']} место.",
|
||||
reply_markup=InlineKeyboardMarkup(inline_keyboard=[
|
||||
[InlineKeyboardButton(text="👑 К управлению победителями", callback_data="admin_winners")]
|
||||
])
|
||||
@@ -2886,6 +2923,13 @@ async def conduct_lottery_draw(callback: CallbackQuery):
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка при отправке уведомлений: {e}")
|
||||
|
||||
# Отправляем результаты розыгрыша всем участникам (кроме победителей)
|
||||
try:
|
||||
await _notify_all_participants_about_results(callback.bot, session, lottery_id, winners_dict)
|
||||
logger.info(f"Результаты розыгрыша разосланы всем участникам {lottery_id}")
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка при рассылке результатов: {e}")
|
||||
|
||||
# Получаем победителей из базы
|
||||
winners = await LotteryService.get_winners(session, lottery_id)
|
||||
text = f"🎉 Розыгрыш '{lottery.title}' завершён!\n\n"
|
||||
@@ -3006,7 +3050,9 @@ async def show_admin_settings(callback: CallbackQuery):
|
||||
text += "Доступные действия:"
|
||||
|
||||
buttons = [
|
||||
[InlineKeyboardButton(text="💾 Экспорт данных", callback_data="admin_export_data")],
|
||||
[InlineKeyboardButton(text="<EFBFBD> Экспорт пользователей (JSON)", callback_data="admin_export_users")],
|
||||
[InlineKeyboardButton(text="📤 Импорт пользователей (JSON)", callback_data="admin_import_users")],
|
||||
[InlineKeyboardButton(text="<EFBFBD>💾 Экспорт данных", callback_data="admin_export_data")],
|
||||
[InlineKeyboardButton(text="🧹 Очистка старых данных", callback_data="admin_cleanup")],
|
||||
[InlineKeyboardButton(text="📋 Системная информация", callback_data="admin_system_info")],
|
||||
[InlineKeyboardButton(text="🔙 Назад", callback_data="admin_panel")]
|
||||
@@ -3701,5 +3747,495 @@ async def show_user_messages(callback: CallbackQuery):
|
||||
)
|
||||
|
||||
|
||||
async def _notify_all_participants_about_results(bot, session: AsyncSession, lottery_id: int, winners_dict: dict):
|
||||
"""
|
||||
Рассылает результаты розыгрыша всем зарегистрированным пользователям (кроме победителей)
|
||||
|
||||
Args:
|
||||
bot: Экземпляр бота
|
||||
session: Сессия БД
|
||||
lottery_id: ID розыгрыша
|
||||
winners_dict: Словарь с победителями {место: данные}
|
||||
"""
|
||||
import asyncio
|
||||
|
||||
# Получаем розыгрыш
|
||||
lottery = await LotteryService.get_lottery(session, lottery_id)
|
||||
if not lottery:
|
||||
return
|
||||
|
||||
# Получаем всех зарегистрированных пользователей
|
||||
all_users = await UserService.get_all_users(session)
|
||||
registered_users = [u for u in all_users if u.is_registered]
|
||||
|
||||
# Получаем telegram_id всех победителей
|
||||
winners = await LotteryService.get_winners(session, lottery_id)
|
||||
winner_telegram_ids = set()
|
||||
for winner in winners:
|
||||
if winner.user and winner.user.telegram_id:
|
||||
winner_telegram_ids.add(winner.user.telegram_id)
|
||||
elif winner.account_number:
|
||||
# Ищем владельца счета
|
||||
from ..core.registration_services import AccountService
|
||||
owner = await AccountService.get_account_owner(session, winner.account_number)
|
||||
if owner and owner.telegram_id:
|
||||
winner_telegram_ids.add(owner.telegram_id)
|
||||
|
||||
# Формируем сообщение с результатами
|
||||
message = (
|
||||
f"📢 <b>Результаты розыгрыша</b>\n\n"
|
||||
f"🎯 <b>{lottery.title}</b>\n\n"
|
||||
f"🏆 <b>Победители:</b>\n"
|
||||
)
|
||||
|
||||
for winner in winners:
|
||||
nickname = None
|
||||
display_name = None
|
||||
|
||||
# Определяем отображаемое имя победителя
|
||||
if winner.user:
|
||||
nickname = winner.user.nickname
|
||||
if not nickname:
|
||||
display_name = f"@{winner.user.username}" if winner.user.username else winner.user.first_name
|
||||
elif winner.account_number:
|
||||
from ..core.registration_services import AccountService
|
||||
owner = await AccountService.get_account_owner(session, winner.account_number)
|
||||
if owner:
|
||||
nickname = owner.nickname
|
||||
if not nickname:
|
||||
display_name = f"@{owner.username}" if owner.username else owner.first_name
|
||||
|
||||
# Формируем строку победителя
|
||||
winner_name = nickname if nickname else display_name if display_name else f"Счет {winner.account_number}"
|
||||
message += f"{winner.place} место: {winner_name}\n"
|
||||
|
||||
message += (
|
||||
f"\n🎁 Поздравляем победителей!\n"
|
||||
f"📌 Победители получат уведомления с инструкциями для получения призов."
|
||||
)
|
||||
|
||||
# Рассылаем всем кроме победителей
|
||||
success_count = 0
|
||||
fail_count = 0
|
||||
|
||||
for user in registered_users:
|
||||
# Пропускаем победителей
|
||||
if user.telegram_id in winner_telegram_ids:
|
||||
continue
|
||||
|
||||
try:
|
||||
await bot.send_message(
|
||||
user.telegram_id,
|
||||
message,
|
||||
parse_mode="HTML"
|
||||
)
|
||||
success_count += 1
|
||||
await asyncio.sleep(0.05) # Небольшая задержка между сообщениями
|
||||
except Exception as e:
|
||||
logger.warning(f"Не удалось отправить результаты пользователю {user.telegram_id}: {e}")
|
||||
fail_count += 1
|
||||
|
||||
logger.info(f"Результаты розыгрыша разосланы: {success_count} успешно, {fail_count} ошибок")
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# ЭКСПОРТ И ИМПОРТ ПОЛЬЗОВАТЕЛЕЙ
|
||||
# ============================================================================
|
||||
|
||||
@admin_router.callback_query(F.data == "admin_export_users")
|
||||
async def admin_export_users(callback: CallbackQuery):
|
||||
"""Экспорт всех пользователей в JSON"""
|
||||
if not is_admin(callback.from_user.id):
|
||||
await callback.answer("❌ Доступ запрещен", show_alert=True)
|
||||
return
|
||||
|
||||
await callback.answer("⏳ Формирую файл...", show_alert=False)
|
||||
|
||||
async with async_session_maker() as session:
|
||||
# Получаем всех пользователей
|
||||
all_users = await UserService.get_all_users(session)
|
||||
|
||||
# Формируем JSON
|
||||
users_data = []
|
||||
for user in all_users:
|
||||
user_dict = {
|
||||
'telegram_id': user.telegram_id,
|
||||
'username': user.username,
|
||||
'first_name': user.first_name,
|
||||
'last_name': user.last_name,
|
||||
'nickname': user.nickname,
|
||||
'phone': user.phone,
|
||||
'club_card_number': user.club_card_number,
|
||||
'is_registered': user.is_registered,
|
||||
'is_admin': user.is_admin,
|
||||
'verification_code': user.verification_code,
|
||||
'created_at': user.created_at.isoformat() if user.created_at else None
|
||||
}
|
||||
users_data.append(user_dict)
|
||||
|
||||
# Создаем JSON с метаданными
|
||||
export_data = {
|
||||
'export_date': datetime.now().isoformat(),
|
||||
'total_users': len(users_data),
|
||||
'registered_users': len([u for u in users_data if u['is_registered']]),
|
||||
'version': '1.0',
|
||||
'users': users_data
|
||||
}
|
||||
|
||||
# Конвертируем в JSON
|
||||
json_str = json.dumps(export_data, ensure_ascii=False, indent=2)
|
||||
json_bytes = json_str.encode('utf-8')
|
||||
|
||||
# Отправляем файл
|
||||
from aiogram.types import BufferedInputFile
|
||||
|
||||
filename = f"users_backup_{datetime.now().strftime('%Y%m%d_%H%M%S')}.json"
|
||||
file = BufferedInputFile(json_bytes, filename=filename)
|
||||
|
||||
await callback.message.answer_document(
|
||||
document=file,
|
||||
caption=(
|
||||
f"📥 <b>Экспорт пользователей</b>\n\n"
|
||||
f"📊 Всего пользователей: {len(users_data)}\n"
|
||||
f"✅ Зарегистрировано: {export_data['registered_users']}\n"
|
||||
f"📅 Дата экспорта: {datetime.now().strftime('%d.%m.%Y %H:%M')}"
|
||||
),
|
||||
parse_mode="HTML"
|
||||
)
|
||||
|
||||
await callback.answer("✅ Файл отправлен", show_alert=False)
|
||||
|
||||
|
||||
@admin_router.callback_query(F.data == "admin_import_users")
|
||||
async def admin_import_users_start(callback: CallbackQuery, state: FSMContext):
|
||||
"""Начать импорт пользователей"""
|
||||
if not is_admin(callback.from_user.id):
|
||||
await callback.answer("❌ Доступ запрещен", show_alert=True)
|
||||
return
|
||||
|
||||
text = (
|
||||
"📤 <b>Импорт пользователей</b>\n\n"
|
||||
"Отправьте JSON файл с данными пользователей.\n\n"
|
||||
"⚠️ <b>Внимание!</b>\n"
|
||||
"• Будут обновлены существующие пользователи (по telegram_id)\n"
|
||||
"• Новые пользователи будут добавлены\n"
|
||||
"• Текущие данные не будут удалены\n\n"
|
||||
"Отправьте /cancel для отмены"
|
||||
)
|
||||
|
||||
buttons = [
|
||||
[InlineKeyboardButton(text="❌ Отмена", callback_data="admin_settings")]
|
||||
]
|
||||
|
||||
await callback.message.edit_text(
|
||||
text,
|
||||
reply_markup=InlineKeyboardMarkup(inline_keyboard=buttons),
|
||||
parse_mode="HTML"
|
||||
)
|
||||
|
||||
await state.set_state(AdminStates.import_users_json)
|
||||
|
||||
|
||||
@admin_router.message(StateFilter(AdminStates.import_users_json), F.document)
|
||||
async def admin_import_users_process(message: Message, state: FSMContext):
|
||||
"""Обработка импорта пользователей из JSON"""
|
||||
if not is_admin(message.from_user.id):
|
||||
return
|
||||
|
||||
# Проверяем формат файла
|
||||
if not message.document.file_name.endswith('.json'):
|
||||
await message.answer("❌ Неверный формат файла. Отправьте JSON файл.")
|
||||
return
|
||||
|
||||
status_msg = await message.answer("⏳ Загружаю файл...")
|
||||
|
||||
try:
|
||||
# Скачиваем файл
|
||||
file = await message.bot.get_file(message.document.file_id)
|
||||
file_content = await message.bot.download_file(file.file_path)
|
||||
|
||||
# Парсим JSON
|
||||
json_data = json.loads(file_content.read().decode('utf-8'))
|
||||
|
||||
# Проверяем структуру
|
||||
if 'users' not in json_data:
|
||||
await status_msg.edit_text("❌ Неверная структура JSON. Не найден массив 'users'.")
|
||||
await state.clear()
|
||||
return
|
||||
|
||||
users_data = json_data['users']
|
||||
|
||||
await status_msg.edit_text(
|
||||
f"📊 Найдено пользователей в файле: {len(users_data)}\n"
|
||||
f"⏳ Импортирую..."
|
||||
)
|
||||
|
||||
# Импортируем пользователей
|
||||
async with async_session_maker() as session:
|
||||
added_count = 0
|
||||
updated_count = 0
|
||||
error_count = 0
|
||||
|
||||
for user_data in users_data:
|
||||
try:
|
||||
telegram_id = user_data.get('telegram_id')
|
||||
if not telegram_id:
|
||||
error_count += 1
|
||||
continue
|
||||
|
||||
# Ищем существующего пользователя
|
||||
existing_user = await UserService.get_user_by_telegram_id(session, telegram_id)
|
||||
|
||||
if existing_user:
|
||||
# Обновляем существующего
|
||||
existing_user.username = user_data.get('username')
|
||||
existing_user.first_name = user_data.get('first_name')
|
||||
existing_user.last_name = user_data.get('last_name')
|
||||
existing_user.nickname = user_data.get('nickname')
|
||||
existing_user.phone = user_data.get('phone')
|
||||
existing_user.club_card_number = user_data.get('club_card_number')
|
||||
existing_user.is_registered = user_data.get('is_registered', False)
|
||||
existing_user.verification_code = user_data.get('verification_code')
|
||||
# is_admin не обновляем из соображений безопасности
|
||||
|
||||
updated_count += 1
|
||||
else:
|
||||
# Создаем нового
|
||||
new_user = User(
|
||||
telegram_id=telegram_id,
|
||||
username=user_data.get('username'),
|
||||
first_name=user_data.get('first_name'),
|
||||
last_name=user_data.get('last_name'),
|
||||
nickname=user_data.get('nickname'),
|
||||
phone=user_data.get('phone'),
|
||||
club_card_number=user_data.get('club_card_number'),
|
||||
is_registered=user_data.get('is_registered', False),
|
||||
is_admin=False, # Не импортируем админов из соображений безопасности
|
||||
verification_code=user_data.get('verification_code')
|
||||
)
|
||||
session.add(new_user)
|
||||
added_count += 1
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка импорта пользователя {user_data.get('telegram_id')}: {e}")
|
||||
error_count += 1
|
||||
|
||||
# Сохраняем изменения
|
||||
await session.commit()
|
||||
|
||||
# Итоговый отчет
|
||||
await status_msg.edit_text(
|
||||
f"✅ <b>Импорт завершен!</b>\n\n"
|
||||
f"📊 <b>Статистика:</b>\n"
|
||||
f"➕ Добавлено: {added_count}\n"
|
||||
f"🔄 Обновлено: {updated_count}\n"
|
||||
f"❌ Ошибок: {error_count}\n"
|
||||
f"📝 Всего обработано: {added_count + updated_count}",
|
||||
parse_mode="HTML"
|
||||
)
|
||||
|
||||
await state.clear()
|
||||
|
||||
except json.JSONDecodeError:
|
||||
await status_msg.edit_text("❌ Ошибка чтения JSON. Проверьте формат файла.")
|
||||
await state.clear()
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка импорта пользователей: {e}")
|
||||
await status_msg.edit_text(f"❌ Ошибка импорта: {str(e)}")
|
||||
await state.clear()
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# МАССОВАЯ РАССЫЛКА
|
||||
# ============================================================================
|
||||
|
||||
@admin_router.callback_query(F.data == "admin_broadcast")
|
||||
async def admin_broadcast_menu(callback: CallbackQuery, state: FSMContext):
|
||||
"""Меню массовой рассылки"""
|
||||
if not is_admin(callback.from_user.id):
|
||||
await callback.answer("❌ Доступ запрещен", show_alert=True)
|
||||
return
|
||||
|
||||
async with async_session_maker() as session:
|
||||
# Получаем статистику пользователей
|
||||
all_users = await UserService.get_all_users(session)
|
||||
registered_users = [u for u in all_users if u.is_registered]
|
||||
|
||||
text = (
|
||||
"📢 <b>Массовая рассылка</b>\n\n"
|
||||
f"👥 Всего пользователей: {len(all_users)}\n"
|
||||
f"✅ Зарегистрировано: {len(registered_users)}\n\n"
|
||||
"Нажмите кнопку ниже, чтобы отправить сообщение всем зарегистрированным пользователям."
|
||||
)
|
||||
|
||||
buttons = [
|
||||
[InlineKeyboardButton(text="✉️ Создать рассылку", callback_data="admin_broadcast_start")],
|
||||
[InlineKeyboardButton(text="🔙 Назад", callback_data="admin_panel")]
|
||||
]
|
||||
|
||||
await callback.message.edit_text(
|
||||
text,
|
||||
reply_markup=InlineKeyboardMarkup(inline_keyboard=buttons),
|
||||
parse_mode="HTML"
|
||||
)
|
||||
|
||||
|
||||
@admin_router.callback_query(F.data == "admin_broadcast_start")
|
||||
async def admin_broadcast_start(callback: CallbackQuery, state: FSMContext):
|
||||
"""Начать создание рассылки"""
|
||||
if not is_admin(callback.from_user.id):
|
||||
await callback.answer("❌ Доступ запрещен", show_alert=True)
|
||||
return
|
||||
|
||||
text = (
|
||||
"✉️ <b>Создание рассылки</b>\n\n"
|
||||
"Отправьте сообщение для рассылки.\n"
|
||||
"Вы можете отправить:\n"
|
||||
"• Текст (поддерживается Markdown)\n"
|
||||
"• Фото с подписью\n"
|
||||
"• Видео с подписью\n"
|
||||
"• Документ с подписью\n\n"
|
||||
"Отправьте /cancel для отмены"
|
||||
)
|
||||
|
||||
buttons = [
|
||||
[InlineKeyboardButton(text="❌ Отмена", callback_data="admin_broadcast")]
|
||||
]
|
||||
|
||||
await callback.message.edit_text(
|
||||
text,
|
||||
reply_markup=InlineKeyboardMarkup(inline_keyboard=buttons),
|
||||
parse_mode="HTML"
|
||||
)
|
||||
|
||||
await state.set_state(AdminStates.broadcast_message)
|
||||
|
||||
|
||||
@admin_router.message(StateFilter(AdminStates.broadcast_message), F.text | F.photo | F.video | F.document)
|
||||
async def admin_broadcast_send(message: Message, state: FSMContext):
|
||||
"""Обработка и отправка рассылки"""
|
||||
if not is_admin(message.from_user.id):
|
||||
return
|
||||
|
||||
# Отправляем уведомление о начале рассылки
|
||||
status_msg = await message.answer(
|
||||
"📤 <b>Начинаю рассылку...</b>\n\n"
|
||||
"⏳ Подождите, это может занять некоторое время.",
|
||||
parse_mode="HTML"
|
||||
)
|
||||
|
||||
async with async_session_maker() as session:
|
||||
# Получаем всех зарегистрированных пользователей
|
||||
all_users = await UserService.get_all_users(session)
|
||||
registered_users = [u for u in all_users if u.is_registered]
|
||||
|
||||
success_count = 0
|
||||
fail_count = 0
|
||||
|
||||
# Рассылаем сообщение пакетами
|
||||
from src.handlers.chat_handlers import BATCH_SIZE, BATCH_DELAY
|
||||
import asyncio
|
||||
|
||||
for i in range(0, len(registered_users), BATCH_SIZE):
|
||||
batch = registered_users[i:i + BATCH_SIZE]
|
||||
|
||||
# Отправляем пакет
|
||||
tasks = []
|
||||
for user in batch:
|
||||
tasks.append(_send_broadcast_to_user(message, user.telegram_id))
|
||||
|
||||
# Ждем завершения пакета
|
||||
results = await asyncio.gather(*tasks, return_exceptions=True)
|
||||
|
||||
# Подсчитываем результаты
|
||||
for result in results:
|
||||
if isinstance(result, Exception):
|
||||
fail_count += 1
|
||||
elif result:
|
||||
success_count += 1
|
||||
else:
|
||||
fail_count += 1
|
||||
|
||||
# Обновляем статус каждые 20 пользователей
|
||||
if (i + BATCH_SIZE) % 60 == 0 or (i + BATCH_SIZE) >= len(registered_users):
|
||||
progress = min(i + BATCH_SIZE, len(registered_users))
|
||||
try:
|
||||
await status_msg.edit_text(
|
||||
f"📤 <b>Рассылка в процессе...</b>\n\n"
|
||||
f"📊 Прогресс: {progress}/{len(registered_users)}\n"
|
||||
f"✅ Отправлено: {success_count}\n"
|
||||
f"❌ Ошибок: {fail_count}",
|
||||
parse_mode="HTML"
|
||||
)
|
||||
except:
|
||||
pass
|
||||
|
||||
# Задержка между пакетами
|
||||
if i + BATCH_SIZE < len(registered_users):
|
||||
await asyncio.sleep(BATCH_DELAY)
|
||||
|
||||
# Итоговый отчет
|
||||
await status_msg.edit_text(
|
||||
f"✅ <b>Рассылка завершена!</b>\n\n"
|
||||
f"📊 <b>Статистика:</b>\n"
|
||||
f"👥 Всего получателей: {len(registered_users)}\n"
|
||||
f"✅ Доставлено: {success_count}\n"
|
||||
f"❌ Не доставлено: {fail_count}\n\n"
|
||||
f"📈 Процент доставки: {(success_count / len(registered_users) * 100):.1f}%",
|
||||
parse_mode="HTML"
|
||||
)
|
||||
|
||||
await state.clear()
|
||||
|
||||
|
||||
async def _send_broadcast_to_user(message: Message, user_telegram_id: int) -> bool:
|
||||
"""
|
||||
Отправить сообщение рассылки конкретному пользователю
|
||||
|
||||
Returns:
|
||||
bool: True при успехе, False при ошибке
|
||||
"""
|
||||
try:
|
||||
if message.text:
|
||||
# Текстовое сообщение
|
||||
await message.bot.send_message(
|
||||
user_telegram_id,
|
||||
message.text,
|
||||
parse_mode="Markdown"
|
||||
)
|
||||
elif message.photo:
|
||||
# Фото с подписью
|
||||
await message.bot.send_photo(
|
||||
user_telegram_id,
|
||||
photo=message.photo[-1].file_id,
|
||||
caption=message.caption,
|
||||
parse_mode="Markdown"
|
||||
)
|
||||
elif message.video:
|
||||
# Видео с подписью
|
||||
await message.bot.send_video(
|
||||
user_telegram_id,
|
||||
video=message.video.file_id,
|
||||
caption=message.caption,
|
||||
parse_mode="Markdown"
|
||||
)
|
||||
elif message.document:
|
||||
# Документ с подписью
|
||||
await message.bot.send_document(
|
||||
user_telegram_id,
|
||||
document=message.document.file_id,
|
||||
caption=message.caption,
|
||||
parse_mode="Markdown"
|
||||
)
|
||||
else:
|
||||
# Копируем сообщение как есть
|
||||
await message.copy_to(user_telegram_id)
|
||||
|
||||
return True
|
||||
except Exception as e:
|
||||
logger.warning(f"Не удалось отправить рассылку пользователю {user_telegram_id}: {e}")
|
||||
return False
|
||||
|
||||
|
||||
# Экспорт роутера
|
||||
__all__ = ['admin_router']
|
||||
@@ -6,7 +6,7 @@ from aiogram.fsm.state import State, StatesGroup
|
||||
from aiogram.filters import StateFilter, Command
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
import asyncio
|
||||
from typing import List, Dict, Optional, Set
|
||||
from typing import List, Dict, Optional, Set, Any
|
||||
from collections import deque
|
||||
import time
|
||||
|
||||
@@ -130,18 +130,21 @@ async def get_all_active_users(session: AsyncSession) -> List:
|
||||
|
||||
async def broadcast_message_with_scheduler(
|
||||
message: Message,
|
||||
sender_user: Any, # User model object
|
||||
exclude_user_id: Optional[int] = None,
|
||||
admin_only: bool = False,
|
||||
sender_info: Optional[str] = None
|
||||
admin_only: bool = False
|
||||
) -> tuple[Dict[str, int], int, int]:
|
||||
"""
|
||||
Разослать сообщение всем пользователям с планировщиком (пакетная отправка).
|
||||
Подписи формируются динамически в зависимости от получателя:
|
||||
- Админы видят: nickname (карта: XXXX)
|
||||
- Обычные пользователи видят: nickname (от пользователя) или "Админ" (от админа)
|
||||
|
||||
Args:
|
||||
message: Сообщение для рассылки
|
||||
sender_user: Объект User отправителя
|
||||
exclude_user_id: ID пользователя для исключения
|
||||
admin_only: Рассылать только админам
|
||||
sender_info: Информация об отправителе (для показа админам)
|
||||
|
||||
Возвращает: (forwarded_ids, success_count, fail_count)
|
||||
"""
|
||||
@@ -165,12 +168,29 @@ async def broadcast_message_with_scheduler(
|
||||
|
||||
# Отправляем пакет
|
||||
tasks = []
|
||||
for user in batch:
|
||||
# Для админов добавляем информацию об отправителе, если сообщение от обычного пользователя
|
||||
if sender_info and user.telegram_id in ADMIN_IDS:
|
||||
tasks.append(_send_message_to_admin_with_sender(message, user.telegram_id, sender_info))
|
||||
for recipient_user in batch:
|
||||
# Формируем подпись в зависимости от получателя
|
||||
if recipient_user.telegram_id in ADMIN_IDS:
|
||||
# Админы видят полную информацию: nickname (карта: XXXX)
|
||||
sender_name = sender_user.nickname if sender_user.nickname else (
|
||||
f"@{sender_user.username}" if sender_user.username else sender_user.first_name
|
||||
)
|
||||
if sender_user.club_card_number:
|
||||
sender_name += f" (карта: {sender_user.club_card_number})"
|
||||
sender_info = sender_name
|
||||
tasks.append(_send_message_to_admin_with_sender(message, recipient_user.telegram_id, sender_info))
|
||||
else:
|
||||
tasks.append(_send_message_to_user(message, user.telegram_id))
|
||||
# Обычные пользователи видят:
|
||||
# - "Админ" если отправитель - админ
|
||||
# - nickname если отправитель - обычный пользователь
|
||||
if sender_user.telegram_id in ADMIN_IDS:
|
||||
sender_info = "Админ"
|
||||
tasks.append(_send_message_to_user_with_sender(message, recipient_user.telegram_id, sender_info))
|
||||
else:
|
||||
sender_info = sender_user.nickname if sender_user.nickname else (
|
||||
f"@{sender_user.username}" if sender_user.username else sender_user.first_name
|
||||
)
|
||||
tasks.append(_send_message_to_user_with_sender(message, recipient_user.telegram_id, sender_info))
|
||||
|
||||
# Ждем завершения пакета
|
||||
results = await asyncio.gather(*tasks, return_exceptions=True)
|
||||
@@ -205,6 +225,85 @@ async def _send_message_to_user(message: Message, user_telegram_id: int) -> Opti
|
||||
return None
|
||||
|
||||
|
||||
async def _send_message_to_user_with_sender(message: Message, user_telegram_id: int, sender_info: str) -> Optional[int]:
|
||||
"""
|
||||
Отправить сообщение обычному пользователю с информацией об отправителе.
|
||||
Возвращает message_id при успехе или None при ошибке.
|
||||
"""
|
||||
try:
|
||||
# Формируем текст с информацией об отправителе
|
||||
header = f"📨 <b>{sender_info}:</b>\n\n"
|
||||
|
||||
if message.text:
|
||||
# Текстовое сообщение
|
||||
sent_msg = await message.bot.send_message(
|
||||
user_telegram_id,
|
||||
header + message.text,
|
||||
parse_mode="HTML"
|
||||
)
|
||||
elif message.photo:
|
||||
# Фото
|
||||
caption = header + (message.caption or "")
|
||||
sent_msg = await message.bot.send_photo(
|
||||
user_telegram_id,
|
||||
photo=message.photo[-1].file_id,
|
||||
caption=caption,
|
||||
parse_mode="HTML"
|
||||
)
|
||||
elif message.video:
|
||||
# Видео
|
||||
caption = header + (message.caption or "")
|
||||
sent_msg = await message.bot.send_video(
|
||||
user_telegram_id,
|
||||
video=message.video.file_id,
|
||||
caption=caption,
|
||||
parse_mode="HTML"
|
||||
)
|
||||
elif message.document:
|
||||
# Документ
|
||||
caption = header + (message.caption or "")
|
||||
sent_msg = await message.bot.send_document(
|
||||
user_telegram_id,
|
||||
document=message.document.file_id,
|
||||
caption=caption,
|
||||
parse_mode="HTML"
|
||||
)
|
||||
elif message.animation:
|
||||
# GIF
|
||||
caption = header + (message.caption or "")
|
||||
sent_msg = await message.bot.send_animation(
|
||||
user_telegram_id,
|
||||
animation=message.animation.file_id,
|
||||
caption=caption,
|
||||
parse_mode="HTML"
|
||||
)
|
||||
elif message.sticker:
|
||||
# Стикер - сначала отправляем заголовок, потом стикер
|
||||
await message.bot.send_message(user_telegram_id, header, parse_mode="HTML")
|
||||
sent_msg = await message.bot.send_sticker(user_telegram_id, sticker=message.sticker.file_id)
|
||||
elif message.voice:
|
||||
# Голосовое сообщение
|
||||
sent_msg = await message.bot.send_voice(
|
||||
user_telegram_id,
|
||||
voice=message.voice.file_id,
|
||||
caption=header,
|
||||
parse_mode="HTML"
|
||||
)
|
||||
elif message.video_note:
|
||||
# Видео-кружок
|
||||
await message.bot.send_message(user_telegram_id, header, parse_mode="HTML")
|
||||
sent_msg = await message.bot.send_video_note(user_telegram_id, video_note=message.video_note.file_id)
|
||||
else:
|
||||
# Неизвестный тип - просто копируем
|
||||
await message.bot.send_message(user_telegram_id, header, parse_mode="HTML")
|
||||
sent_msg = await message.copy_to(user_telegram_id)
|
||||
|
||||
return sent_msg.message_id
|
||||
except Exception as e:
|
||||
print(f"Failed to send message to {user_telegram_id}: {e}")
|
||||
return None
|
||||
|
||||
|
||||
async def _send_message_to_admin_with_sender(message: Message, admin_telegram_id: int, sender_info: str) -> Optional[int]:
|
||||
"""
|
||||
Отправить сообщение админу с информацией об отправителе.
|
||||
@@ -443,19 +542,12 @@ async def handle_text_message(message: Message, state: FSMContext):
|
||||
# Обрабатываем в зависимости от режима
|
||||
if settings.mode == 'broadcast':
|
||||
# Режим рассылки с планировщиком
|
||||
# Формируем информацию об отправителе для админов (если это не админ)
|
||||
sender_info = None
|
||||
if not is_admin(message.from_user.id):
|
||||
sender_name = f"@{user.username}" if user.username else user.first_name
|
||||
if user.club_card_number:
|
||||
sender_name += f" (карта: {user.club_card_number})"
|
||||
sender_info = sender_name
|
||||
|
||||
# Передаем объект user для динамического формирования подписей
|
||||
# ВСЕГДА исключаем отправителя - он не должен получать своё же сообщение
|
||||
forwarded_ids, success, fail = await broadcast_message_with_scheduler(
|
||||
message,
|
||||
exclude_user_id=message.from_user.id,
|
||||
sender_info=sender_info
|
||||
sender_user=user,
|
||||
exclude_user_id=message.from_user.id
|
||||
)
|
||||
|
||||
# Сохраняем сообщение в историю
|
||||
@@ -531,19 +623,11 @@ async def handle_photo_message(message: Message, state: FSMContext):
|
||||
photo = message.photo[-1]
|
||||
|
||||
if settings.mode == 'broadcast':
|
||||
# Формируем информацию об отправителе для админов (если это не админ)
|
||||
sender_info = None
|
||||
if not is_admin(message.from_user.id):
|
||||
sender_name = f"@{user.username}" if user.username else user.first_name
|
||||
if user.club_card_number:
|
||||
sender_name += f" (карта: {user.club_card_number})"
|
||||
sender_info = sender_name
|
||||
|
||||
# Рассылаем фото - ВСЕГДА исключаем отправителя
|
||||
forwarded_ids, success, fail = await broadcast_message_with_scheduler(
|
||||
message,
|
||||
exclude_user_id=message.from_user.id,
|
||||
sender_info=sender_info
|
||||
sender_user=user,
|
||||
exclude_user_id=message.from_user.id
|
||||
)
|
||||
|
||||
await ChatMessageService.save_message(
|
||||
@@ -605,18 +689,11 @@ async def handle_video_message(message: Message, state: FSMContext):
|
||||
)
|
||||
|
||||
if settings.mode == 'broadcast':
|
||||
# Формируем информацию об отправителе для админов (если это не админ)
|
||||
sender_info = None
|
||||
if not is_admin(message.from_user.id):
|
||||
sender_name = f"@{user.username}" if user.username else user.first_name
|
||||
if user.club_card_number:
|
||||
sender_name += f" (карта: {user.club_card_number})"
|
||||
sender_info = sender_name
|
||||
|
||||
# Рассылаем видео
|
||||
forwarded_ids, success, fail = await broadcast_message_with_scheduler(
|
||||
message,
|
||||
exclude_user_id=message.from_user.id,
|
||||
sender_info=sender_info
|
||||
sender_user=user,
|
||||
exclude_user_id=message.from_user.id
|
||||
)
|
||||
|
||||
await ChatMessageService.save_message(
|
||||
@@ -678,18 +755,11 @@ async def handle_document_message(message: Message, state: FSMContext):
|
||||
)
|
||||
|
||||
if settings.mode == 'broadcast':
|
||||
# Формируем информацию об отправителе для админов (если это не админ)
|
||||
sender_info = None
|
||||
if not is_admin(message.from_user.id):
|
||||
sender_name = f"@{user.username}" if user.username else user.first_name
|
||||
if user.club_card_number:
|
||||
sender_name += f" (карта: {user.club_card_number})"
|
||||
sender_info = sender_name
|
||||
|
||||
# Рассылаем документ
|
||||
forwarded_ids, success, fail = await broadcast_message_with_scheduler(
|
||||
message,
|
||||
exclude_user_id=message.from_user.id,
|
||||
sender_info=sender_info
|
||||
sender_user=user,
|
||||
exclude_user_id=message.from_user.id
|
||||
)
|
||||
|
||||
await ChatMessageService.save_message(
|
||||
@@ -751,18 +821,11 @@ async def handle_animation_message(message: Message, state: FSMContext):
|
||||
)
|
||||
|
||||
if settings.mode == 'broadcast':
|
||||
# Формируем информацию об отправителе для админов (если это не админ)
|
||||
sender_info = None
|
||||
if not is_admin(message.from_user.id):
|
||||
sender_name = f"@{user.username}" if user.username else user.first_name
|
||||
if user.club_card_number:
|
||||
sender_name += f" (карта: {user.club_card_number})"
|
||||
sender_info = sender_name
|
||||
|
||||
# Рассылаем анимацию
|
||||
forwarded_ids, success, fail = await broadcast_message_with_scheduler(
|
||||
message,
|
||||
exclude_user_id=message.from_user.id,
|
||||
sender_info=sender_info
|
||||
sender_user=user,
|
||||
exclude_user_id=message.from_user.id
|
||||
)
|
||||
|
||||
await ChatMessageService.save_message(
|
||||
@@ -824,18 +887,11 @@ async def handle_sticker_message(message: Message, state: FSMContext):
|
||||
)
|
||||
|
||||
if settings.mode == 'broadcast':
|
||||
# Формируем информацию об отправителе для админов (если это не админ)
|
||||
sender_info = None
|
||||
if not is_admin(message.from_user.id):
|
||||
sender_name = f"@{user.username}" if user.username else user.first_name
|
||||
if user.club_card_number:
|
||||
sender_name += f" (карта: {user.club_card_number})"
|
||||
sender_info = sender_name
|
||||
|
||||
# Рассылаем стикер
|
||||
forwarded_ids, success, fail = await broadcast_message_with_scheduler(
|
||||
message,
|
||||
exclude_user_id=message.from_user.id,
|
||||
sender_info=sender_info
|
||||
sender_user=user,
|
||||
exclude_user_id=message.from_user.id
|
||||
)
|
||||
|
||||
await ChatMessageService.save_message(
|
||||
|
||||
@@ -356,9 +356,29 @@ async def confirm_winner_callback(callback_query):
|
||||
winner.claimed_at = datetime.now(timezone.utc)
|
||||
await session.commit()
|
||||
|
||||
# Получаем данные о розыгрыше
|
||||
# Получаем данные о розыгрыше и пользователе
|
||||
lottery = await LotteryService.get_lottery(session, winner.lottery_id)
|
||||
|
||||
# Получаем информацию о пользователе
|
||||
owner = None
|
||||
if winner.account_number:
|
||||
owner = await AccountService.get_account_owner(session, winner.account_number)
|
||||
elif winner.user_id:
|
||||
user_result = await session.execute(
|
||||
select(User).where(User.id == winner.user_id)
|
||||
)
|
||||
owner = user_result.scalar_one_or_none()
|
||||
|
||||
# Формируем отображаемое имя
|
||||
display_name = "Пользователь"
|
||||
if owner:
|
||||
if owner.nickname:
|
||||
display_name = owner.nickname
|
||||
elif owner.username:
|
||||
display_name = f"@{owner.username}"
|
||||
elif owner.first_name:
|
||||
display_name = owner.first_name
|
||||
|
||||
# Отправляем подтверждение пользователю
|
||||
confirmation_text = (
|
||||
f"✅ **Выигрыш подтвержден!**\n\n"
|
||||
@@ -375,13 +395,17 @@ async def confirm_winner_callback(callback_query):
|
||||
parse_mode="Markdown"
|
||||
)
|
||||
|
||||
# Уведомляем админов
|
||||
# Уведомляем админов с nickname и клубной картой
|
||||
for admin_id in ADMIN_IDS:
|
||||
try:
|
||||
# Формируем информацию для админа
|
||||
user_info = display_name
|
||||
if owner and owner.club_card_number:
|
||||
user_info = f"{display_name} (карта: {owner.club_card_number})"
|
||||
|
||||
admin_text = (
|
||||
f"✅ **Подтверждение выигрыша**\n\n"
|
||||
f"👤 Пользователь: {callback_query.from_user.full_name} "
|
||||
f"(@{callback_query.from_user.username or 'нет username'})\n"
|
||||
f"👤 Пользователь: {user_info}\n"
|
||||
f"🎯 Розыгрыш: {lottery.title}\n"
|
||||
f"🏆 Место: {winner.place}\n"
|
||||
f"🎁 Приз: {winner.prize}\n"
|
||||
|
||||
@@ -14,8 +14,49 @@ logger = logging.getLogger(__name__)
|
||||
router = Router()
|
||||
|
||||
|
||||
# Служебные слова, которые нельзя использовать как никнейм
|
||||
FORBIDDEN_NICKNAMES = [
|
||||
'привет', 'здравствуйте', 'добрый', 'день', 'вечер', 'утро',
|
||||
'спасибо', 'пожалуйста', 'извините', 'до свидания', 'пока',
|
||||
'admin', 'administrator', 'moderator', 'bot', 'system',
|
||||
'hello', 'hi', 'thanks', 'please', 'sorry', 'goodbye', 'bye'
|
||||
]
|
||||
|
||||
|
||||
def validate_nickname(nickname: str) -> tuple[bool, str]:
|
||||
"""
|
||||
Валидация никнейма
|
||||
|
||||
Returns:
|
||||
(valid, error_message)
|
||||
"""
|
||||
nickname = nickname.strip()
|
||||
|
||||
# Проверка длины
|
||||
if len(nickname) < 2:
|
||||
return False, "❌ Никнейм слишком короткий (минимум 2 символа)"
|
||||
|
||||
if len(nickname) > 20:
|
||||
return False, "❌ Никнейм слишком длинный (максимум 20 символов)"
|
||||
|
||||
# Проверка на служебные слова
|
||||
nickname_lower = nickname.lower()
|
||||
for forbidden in FORBIDDEN_NICKNAMES:
|
||||
if forbidden in nickname_lower:
|
||||
import random
|
||||
suggestion = f"{nickname[:3]}{random.randint(10, 99)}"
|
||||
return False, f"❌ Это похоже на приветствие или служебное слово.\n\nПридумайте уникальный никнейм (например: {suggestion})"
|
||||
|
||||
# Проверка на команды
|
||||
if nickname.startswith('/'):
|
||||
return False, "❌ Никнейм не может начинаться с '/'"
|
||||
|
||||
return True, ""
|
||||
|
||||
|
||||
class RegistrationStates(StatesGroup):
|
||||
"""Состояния для процесса регистрации"""
|
||||
waiting_for_nickname = State()
|
||||
waiting_for_club_card = State()
|
||||
waiting_for_phone = State()
|
||||
|
||||
@@ -28,7 +69,11 @@ async def start_registration(callback: CallbackQuery, state: FSMContext):
|
||||
text = (
|
||||
"📝 Регистрация в системе\n\n"
|
||||
"Для участия в розыгрышах необходимо зарегистрироваться.\n\n"
|
||||
"Введите номер вашей клубной карты:"
|
||||
"Шаг 1 из 3: Придумайте никнейм\n\n"
|
||||
"🎭 Введите ваш никнейм для чата:\n"
|
||||
"• От 2 до 20 символов\n"
|
||||
"• Может содержать буквы, цифры, пробелы\n"
|
||||
"• Это имя будут видеть другие участники"
|
||||
)
|
||||
|
||||
await callback.message.edit_text(
|
||||
@@ -37,6 +82,32 @@ async def start_registration(callback: CallbackQuery, state: FSMContext):
|
||||
[InlineKeyboardButton(text="❌ Отмена", callback_data="back_to_main")]
|
||||
])
|
||||
)
|
||||
await state.set_state(RegistrationStates.waiting_for_nickname)
|
||||
|
||||
|
||||
@router.message(StateFilter(RegistrationStates.waiting_for_nickname))
|
||||
async def process_nickname(message: Message, state: FSMContext):
|
||||
"""Обработка никнейма"""
|
||||
nickname = message.text.strip()
|
||||
|
||||
# Валидация никнейма
|
||||
valid, error_msg = validate_nickname(nickname)
|
||||
|
||||
if not valid:
|
||||
await message.answer(
|
||||
f"{error_msg}\n\n"
|
||||
"Попробуйте другой вариант:"
|
||||
)
|
||||
return
|
||||
|
||||
# Сохраняем никнейм
|
||||
await state.update_data(nickname=nickname)
|
||||
|
||||
await message.answer(
|
||||
f"✅ Отлично! Ваш никнейм: {nickname}\n\n"
|
||||
"Шаг 2 из 3: Клубная карта\n\n"
|
||||
"📝 Введите номер вашей клубной карты:"
|
||||
)
|
||||
await state.set_state(RegistrationStates.waiting_for_club_card)
|
||||
|
||||
|
||||
@@ -60,7 +131,8 @@ async def process_club_card(message: Message, state: FSMContext):
|
||||
await state.update_data(club_card_number=club_card_number)
|
||||
|
||||
await message.answer(
|
||||
"📱 Теперь введите ваш номер телефона\n"
|
||||
"Шаг 3 из 3: Телефон\n\n"
|
||||
"📱 Введите ваш номер телефона\n"
|
||||
"(или отправьте '-' чтобы пропустить):"
|
||||
)
|
||||
await state.set_state(RegistrationStates.waiting_for_phone)
|
||||
@@ -73,6 +145,7 @@ async def process_phone(message: Message, state: FSMContext):
|
||||
|
||||
data = await state.get_data()
|
||||
club_card_number = data['club_card_number']
|
||||
nickname = data.get('nickname')
|
||||
|
||||
try:
|
||||
async with async_session_maker() as session:
|
||||
@@ -83,8 +156,15 @@ async def process_phone(message: Message, state: FSMContext):
|
||||
phone=phone
|
||||
)
|
||||
|
||||
# Обновляем никнейм пользователя
|
||||
if nickname:
|
||||
user.nickname = nickname
|
||||
await session.commit()
|
||||
await session.refresh(user)
|
||||
|
||||
text = (
|
||||
"✅ Регистрация завершена!\n\n"
|
||||
f"🎭 Никнейм: {user.nickname}\n"
|
||||
f"🎫 Клубная карта: {user.club_card_number}\n"
|
||||
f"🔑 Ваш код верификации: **{user.verification_code}**\n\n"
|
||||
"⚠️ Сохраните этот код! Он понадобится для подтверждения выигрыша.\n\n"
|
||||
|
||||
Reference in New Issue
Block a user