Реализованы все улучшения функционала бота

Блок 1: Система никнеймов
-  Добавлено поле nickname в модель User
-  Создана миграция для nickname
-  Обновлена регистрация (3 шага: nickname → карта → телефон)
-  Валидация nickname (длина 2-20, проверка служебных слов)
-  Подписи в чате используют nickname

Блок 2: Админские функции
-  Массовая рассылка (кнопка в админке, поддержка текста/фото/видео/документов)
-  Экспорт пользователей в JSON (бэкап с метаданными)
-  Импорт пользователей из JSON (восстановление с обновлением)

Блок 3: Улучшения розыгрышей
-  Рассылка результатов розыгрыша всем участникам (кроме победителей)
-  Сообщения подтверждения показывают nickname + клубную карту
-  Ручное назначение победителя по номеру счета/telegram ID/username
This commit is contained in:
2026-02-09 20:22:32 +09:00
parent 4e2c8981c2
commit c0407fdb11
7 changed files with 725 additions and 48 deletions

View File

@@ -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')

View File

@@ -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) # Прошел ли полную регистрацию

View File

@@ -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()

View File

@@ -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']

View File

@@ -446,7 +446,8 @@ async def handle_text_message(message: Message, state: FSMContext):
# Формируем информацию об отправителе для админов (если это не админ)
sender_info = None
if not is_admin(message.from_user.id):
sender_name = f"@{user.username}" if user.username else user.first_name
# Используем nickname, если есть, иначе fallback на username или first_name
sender_name = user.nickname if user.nickname else (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
@@ -534,7 +535,8 @@ async def handle_photo_message(message: Message, state: FSMContext):
# Формируем информацию об отправителе для админов (если это не админ)
sender_info = None
if not is_admin(message.from_user.id):
sender_name = f"@{user.username}" if user.username else user.first_name
# Используем nickname, если есть, иначе fallback на username или first_name
sender_name = user.nickname if user.nickname else (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
@@ -608,7 +610,8 @@ async def handle_video_message(message: Message, state: FSMContext):
# Формируем информацию об отправителе для админов (если это не админ)
sender_info = None
if not is_admin(message.from_user.id):
sender_name = f"@{user.username}" if user.username else user.first_name
# Используем nickname, если есть, иначе fallback на username или first_name
sender_name = user.nickname if user.nickname else (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
@@ -681,7 +684,8 @@ async def handle_document_message(message: Message, state: FSMContext):
# Формируем информацию об отправителе для админов (если это не админ)
sender_info = None
if not is_admin(message.from_user.id):
sender_name = f"@{user.username}" if user.username else user.first_name
# Используем nickname, если есть, иначе fallback на username или first_name
sender_name = user.nickname if user.nickname else (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
@@ -754,7 +758,8 @@ async def handle_animation_message(message: Message, state: FSMContext):
# Формируем информацию об отправителе для админов (если это не админ)
sender_info = None
if not is_admin(message.from_user.id):
sender_name = f"@{user.username}" if user.username else user.first_name
# Используем nickname, если есть, иначе fallback на username или first_name
sender_name = user.nickname if user.nickname else (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
@@ -827,7 +832,8 @@ async def handle_sticker_message(message: Message, state: FSMContext):
# Формируем информацию об отправителе для админов (если это не админ)
sender_info = None
if not is_admin(message.from_user.id):
sender_name = f"@{user.username}" if user.username else user.first_name
# Используем nickname, если есть, иначе fallback на username или first_name
sender_name = user.nickname if user.nickname else (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

View File

@@ -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"

View File

@@ -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"