Files
new_lottery_bot/src/handlers/admin_account_handlers.py
Andrew K. Choi 505d26f0e9
Some checks reported errors
continuous-integration/drone/push Build encountered an error
feat: Система автоматического подтверждения выигрышей с поддержкой множественных счетов
Основные изменения:

 Новые функции:
- Система регистрации пользователей с множественными счетами
- Автоматическое подтверждение выигрышей через inline-кнопки
- Механизм переигровки для неподтвержденных выигрышей (24 часа)
- Подтверждение на уровне счетов (каждый счет подтверждается отдельно)
- Скрипт полной очистки базы данных

🔧 Технические улучшения:
- Исправлена ошибка MissingGreenlet при lazy loading (добавлен joinedload/selectinload)
- Добавлено поле claimed_at для отслеживания времени подтверждения
- Пакетное добавление счетов с выбором розыгрыша
- Проверка владения конкретным счетом при подтверждении

📚 Документация:
- docs/AUTO_CONFIRM_SYSTEM.md - Полная документация системы подтверждения
- docs/ACCOUNT_BASED_CONFIRMATION.md - Подтверждение на уровне счетов
- docs/REGISTRATION_SYSTEM.md - Система регистрации
- docs/ADMIN_COMMANDS.md - Команды администратора
- docs/CLEAR_DATABASE.md - Очистка БД
- docs/QUICK_GUIDE.md - Быстрое начало
- docs/UPDATE_LOG.md - Журнал обновлений

🗄️ База данных:
- Миграция 003: Таблицы accounts, winner_verifications
- Миграция 004: Поле claimed_at в таблице winners
- Скрипт scripts/clear_database.py для полной очистки

🎮 Новые команды:
Админские:
- /check_unclaimed <lottery_id> - Проверка неподтвержденных выигрышей
- /redraw <lottery_id> - Повторный розыгрыш
- /add_accounts - Пакетное добавление счетов
- /list_accounts <telegram_id> - Список счетов пользователя

Пользовательские:
- /register - Регистрация с вводом данных
- /my_account - Просмотр своих счетов
- Callback confirm_win_{id} - Подтверждение выигрыша

🛠️ Makefile:
- make clear-db - Очистка всех данных из БД (с подтверждением)

🔒 Безопасность:
- Проверка владения счетом при подтверждении
- Защита от подтверждения чужих счетов
- Независимое подтверждение каждого выигрышного счета

📊 Логика работы:
1. Пользователь регистрируется и добавляет счета
2. Счета участвуют в розыгрыше
3. Победители получают уведомление с кнопкой подтверждения
4. Каждый счет подтверждается отдельно (24 часа на подтверждение)
5. Неподтвержденные выигрыши переигрываются через /redraw
2025-11-16 14:01:30 +09:00

586 lines
23 KiB
Python
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""Админские обработчики для управления счетами и верификации"""
from aiogram import Router, F, Bot
from aiogram.types import Message, CallbackQuery, InlineKeyboardButton, InlineKeyboardMarkup
from aiogram.filters import Command
from aiogram.fsm.context import FSMContext
from aiogram.fsm.state import State, StatesGroup
from sqlalchemy import select, and_
from src.core.database import async_session_maker
from src.core.registration_services import AccountService, WinnerNotificationService, RegistrationService
from src.core.services import UserService, LotteryService, ParticipationService
from src.core.models import User, Winner, Account, Participation
from src.core.config import ADMIN_IDS
router = Router()
class AddAccountStates(StatesGroup):
waiting_for_data = State()
choosing_lottery = State()
def is_admin(user_id: int) -> bool:
"""Проверка прав администратора"""
return user_id in ADMIN_IDS
@router.message(Command("add_account"))
async def add_account_command(message: Message, state: FSMContext):
"""
Добавить счет пользователю по клубной карте
Формат: /add_account <club_card> <account_number>
Или: /add_account (затем вводить данные построчно)
"""
if not is_admin(message.from_user.id):
await message.answer("❌ Недостаточно прав")
return
parts = message.text.split(maxsplit=2)
# Если данные указаны в команде
if len(parts) == 3:
club_card = parts[1]
account_number = parts[2]
await process_single_account(message, club_card, account_number, state)
else:
# Запрашиваем данные
await state.set_state(AddAccountStates.waiting_for_data)
await message.answer(
"💳 **Добавление счетов**\n\n"
"Отправьте данные в формате:\n"
"`клубная_карта номер_счета`\n\n"
"**Для одного счета:**\n"
"`2223 11-22-33-44-55-66-77`\n\n"
"**Для нескольких счетов (каждый с новой строки):**\n"
"`2223 11-22-33-44-55-66-77`\n"
"`2223 88-99-00-11-22-33-44`\n"
"`3334 12-34-56-78-90-12-34`\n\n"
"❌ Отправьте /cancel для отмены",
parse_mode="Markdown"
)
async def process_single_account(message: Message, club_card: str, account_number: str, state: FSMContext):
"""Обработка одного счета"""
try:
async with async_session_maker() as session:
# Создаем счет
account = await AccountService.create_account(
session,
club_card_number=club_card,
account_number=account_number
)
# Получаем владельца
owner = await AccountService.get_account_owner(session, account_number)
# Сохраняем данные счета в state для добавления в розыгрыш
await state.update_data(
accounts=[{
'club_card': club_card,
'account_number': account_number,
'account_id': account.id
}]
)
text = f"✅ Счет успешно добавлен!\n\n"
text += f"🎫 Клубная карта: {club_card}\n"
text += f"💳 Счет: {account_number}\n"
if owner:
text += f"👤 Владелец: {owner.first_name}\n\n"
# Отправляем уведомление владельцу
try:
await message.bot.send_message(
owner.telegram_id,
f"К вашему профилю добавлен счет:\n\n"
f"💳 {account_number}\n\n"
f"Теперь вы можете участвовать в розыгрышах с этим счетом!"
)
text += "📨 Владельцу отправлено уведомление\n\n"
except Exception as e:
text += f"⚠️ Не удалось отправить уведомление: {str(e)}\n\n"
# Предлагаем добавить в розыгрыш
await show_lottery_selection(message, text, state)
except ValueError as e:
await message.answer(f"❌ Ошибка: {str(e)}")
await state.clear()
except Exception as e:
await message.answer(f"❌ Произошла ошибка: {str(e)}")
await state.clear()
@router.message(AddAccountStates.waiting_for_data)
async def process_accounts_data(message: Message, state: FSMContext):
"""Обработка данных счетов (один или несколько)"""
if message.text.strip().lower() == '/cancel':
await state.clear()
await message.answer("❌ Операция отменена")
return
lines = message.text.strip().split('\n')
accounts_data = []
errors = []
for i, line in enumerate(lines, 1):
parts = line.strip().split()
if len(parts) != 2:
errors.append(f"Строка {i}: неверный формат (ожидается: клубная_карта номер_счета)")
continue
club_card, account_number = parts
try:
async with async_session_maker() as session:
account = await AccountService.create_account(
session,
club_card_number=club_card,
account_number=account_number
)
owner = await AccountService.get_account_owner(session, account_number)
accounts_data.append({
'club_card': club_card,
'account_number': account_number,
'account_id': account.id,
'owner': owner
})
# Отправляем уведомление владельцу
if owner:
try:
await message.bot.send_message(
owner.telegram_id,
f"К вашему профилю добавлен счет:\n\n"
f"💳 {account_number}\n\n"
f"Теперь вы можете участвовать в розыгрышах!"
)
except:
pass
except ValueError as e:
errors.append(f"Строка {i} ({club_card} {account_number}): {str(e)}")
except Exception as e:
errors.append(f"Строка {i}: {str(e)}")
# Формируем отчет
text = f"📊 **Результаты добавления счетов**\n\n"
if accounts_data:
text += f"✅ **Успешно добавлено: {len(accounts_data)}**\n\n"
for acc in accounts_data:
text += f"{acc['club_card']}{acc['account_number']}\n"
if acc['owner']:
text += f" 👤 {acc['owner'].first_name}\n"
text += "\n"
if errors:
text += f"❌ **Ошибки: {len(errors)}**\n\n"
for error in errors[:5]: # Показываем максимум 5 ошибок
text += f"{error}\n"
if len(errors) > 5:
text += f"\n... и еще {len(errors) - 5} ошибок\n"
if not accounts_data:
await message.answer(text)
await state.clear()
return
# Сохраняем данные и предлагаем добавить в розыгрыш
await state.update_data(accounts=accounts_data)
await show_lottery_selection(message, text, state)
async def show_lottery_selection(message: Message, prev_text: str, state: FSMContext):
"""Показать выбор розыгрыша для добавления счетов"""
async with async_session_maker() as session:
lotteries = await LotteryService.get_active_lotteries(session)
if not lotteries:
await message.answer(
prev_text + " Нет активных розыгрышей для добавления счетов"
)
await state.clear()
return
await state.set_state(AddAccountStates.choosing_lottery)
buttons = []
for lottery in lotteries[:10]: # Максимум 10 розыгрышей
buttons.append([
InlineKeyboardButton(
text=f"🎯 {lottery.title}",
callback_data=f"add_to_lottery_{lottery.id}"
)
])
buttons.append([
InlineKeyboardButton(text="❌ Пропустить", callback_data="skip_lottery_add")
])
await message.answer(
prev_text + " **Добавить счета в розыгрыш?**\n\n"
"Выберите розыгрыш из списка:",
reply_markup=InlineKeyboardMarkup(inline_keyboard=buttons)
)
@router.callback_query(F.data.startswith("add_to_lottery_"))
async def add_accounts_to_lottery(callback: CallbackQuery, state: FSMContext):
"""Добавить счета в выбранный розыгрыш"""
lottery_id = int(callback.data.split("_")[-1])
data = await state.get_data()
accounts = data.get('accounts', [])
if not accounts:
await callback.answer("❌ Нет данных о счетах", show_alert=True)
await state.clear()
return
success_count = 0
errors = []
async with async_session_maker() as session:
lottery = await LotteryService.get_lottery(session, lottery_id)
if not lottery:
await callback.answer("❌ Розыгрыш не найден", show_alert=True)
await state.clear()
return
for acc in accounts:
try:
# Добавляем участие через account_id
# Проверяем, не участвует ли уже
existing = await session.execute(
select(Participation).where(
and_(
Participation.lottery_id == lottery_id,
Participation.account_id == acc['account_id']
)
)
)
if existing.scalar_one_or_none():
errors.append(f"{acc['account_number']}: уже участвует")
continue
# Создаем участие
participation = Participation(
lottery_id=lottery_id,
account_id=acc['account_id'],
account_number=acc['account_number']
)
session.add(participation)
success_count += 1
except Exception as e:
errors.append(f"{acc['account_number']}: {str(e)}")
await session.commit()
text = f"📊 **Добавление в розыгрыш '{lottery.title}'**\n\n"
if success_count:
text += f"✅ Добавлено счетов: {success_count}\n\n"
if errors:
text += f"⚠️ Ошибки: {len(errors)}\n"
for error in errors[:3]:
text += f"{error}\n"
await callback.message.edit_text(text)
await state.clear()
@router.callback_query(F.data == "skip_lottery_add")
async def skip_lottery_add(callback: CallbackQuery, state: FSMContext):
"""Пропустить добавление в розыгрыш"""
await callback.message.edit_text("✅ Счета добавлены без участия в розыгрышах")
await state.clear()
@router.message(Command("remove_account"))
async def remove_account_command(message: Message):
"""
Деактивировать счет
Формат: /remove_account <account_number>
"""
if not is_admin(message.from_user.id):
await message.answer("❌ Недостаточно прав")
return
parts = message.text.split()
if len(parts) != 2:
await message.answer(
"❌ Неверный формат команды\n\n"
"Используйте: /remove_account <account_number>"
)
return
account_number = parts[1]
try:
async with async_session_maker() as session:
success = await AccountService.deactivate_account(session, account_number)
if success:
await message.answer(f"✅ Счет {account_number} деактивирован")
else:
await message.answer(f"❌ Счет {account_number} не найден")
except Exception as e:
await message.answer(f"❌ Ошибка: {str(e)}")
@router.message(Command("verify_winner"))
async def verify_winner_command(message: Message):
"""
Подтвердить выигрыш по коду верификации
Формат: /verify_winner <verification_code> <lottery_id>
Пример: /verify_winner AB12CD34 1
"""
if not is_admin(message.from_user.id):
await message.answer("❌ Недостаточно прав")
return
parts = message.text.split()
if len(parts) != 3:
await message.answer(
"❌ Неверный формат команды\n\n"
"Используйте:\n"
"/verify_winner <verification_code> <lottery_id>\n\n"
"Пример:\n"
"/verify_winner AB12CD34 1"
)
return
verification_code = parts[1].upper()
try:
lottery_id = int(parts[2])
except ValueError:
await message.answer("❌ lottery_id должен быть числом")
return
try:
async with async_session_maker() as session:
# Проверяем существование розыгрыша
lottery = await LotteryService.get_lottery(session, lottery_id)
if not lottery:
await message.answer(f"❌ Розыгрыш #{lottery_id} не найден")
return
# Подтверждаем выигрыш
winner = await WinnerNotificationService.verify_winner(
session,
verification_code=verification_code,
lottery_id=lottery_id
)
if not winner:
await message.answer(
f"❌ Выигрыш не найден\n\n"
f"Возможные причины:\n"
f"• Неверный код верификации\n"
f"• Пользователь не является победителем в розыгрыше #{lottery_id}\n"
f"• Выигрыш уже был подтвержден"
)
return
# Получаем пользователя
user = await RegistrationService.get_user_by_verification_code(session, verification_code)
text = "✅ Выигрыш подтвержден!\n\n"
text += f"🎯 Розыгрыш: {lottery.title}\n"
text += f"🏆 Место: {winner.place}\n"
text += f"🎁 Приз: {winner.prize}\n\n"
if user:
text += f"👤 Победитель: {user.first_name}\n"
text += f"🎫 Клубная карта: {user.club_card_number}\n"
if user.phone:
text += f"📱 Телефон: {user.phone}\n"
# Отправляем уведомление победителю
try:
bot = message.bot
await bot.send_message(
user.telegram_id,
f"✅ Ваш выигрыш подтвержден!\n\n"
f"🎯 Розыгрыш: {lottery.title}\n"
f"🏆 Место: {winner.place}\n"
f"🎁 Приз: {winner.prize}\n\n"
f"Администратор свяжется с вами для получения приза."
)
text += "\n📨 Победителю отправлено уведомление"
except Exception as e:
text += f"\n⚠️ Не удалось отправить уведомление: {str(e)}"
if winner.account_number:
text += f"💳 Счет: {winner.account_number}\n"
await message.answer(text)
except Exception as e:
await message.answer(f"❌ Ошибка: {str(e)}")
@router.message(Command("winner_status"))
async def winner_status_command(message: Message):
"""
Показать статус всех победителей розыгрыша
Формат: /winner_status <lottery_id>
"""
if not is_admin(message.from_user.id):
await message.answer("❌ Недостаточно прав")
return
parts = message.text.split()
if len(parts) != 2:
await message.answer(
"❌ Неверный формат команды\n\n"
"Используйте: /winner_status <lottery_id>"
)
return
try:
lottery_id = int(parts[1])
except ValueError:
await message.answer("❌ lottery_id должен быть числом")
return
try:
async with async_session_maker() as session:
lottery = await LotteryService.get_lottery(session, lottery_id)
if not lottery:
await message.answer(f"❌ Розыгрыш #{lottery_id} не найден")
return
winners = await LotteryService.get_winners(session, lottery_id)
if not winners:
await message.answer(f"В розыгрыше '{lottery.title}' пока нет победителей")
return
text = f"🏆 Победители розыгрыша '{lottery.title}':\n\n"
for winner in winners:
status_icon = "" if winner.is_claimed else ""
notified_icon = "📨" if winner.is_notified else "📭"
text += f"{status_icon} {winner.place} место - {winner.prize}\n"
# Получаем информацию о победителе
async with async_session_maker() as session:
if winner.user_id:
user_result = await session.execute(
select(User).where(User.id == winner.user_id)
)
user = user_result.scalar_one_or_none()
if user:
text += f" 👤 {user.first_name}"
if user.club_card_number:
text += f" (КК: {user.club_card_number})"
text += "\n"
if winner.account_number:
text += f" 💳 {winner.account_number}\n"
# Статус подтверждения
if winner.is_claimed:
text += f" ✅ Подтвержден\n"
else:
text += f" ⏳ Ожидает подтверждения\n"
text += "\n"
await message.answer(text)
except Exception as e:
await message.answer(f"❌ Ошибка: {str(e)}")
@router.message(Command("user_info"))
async def user_info_command(message: Message):
"""
Показать информацию о пользователе
Формат: /user_info <club_card>
"""
if not is_admin(message.from_user.id):
await message.answer("❌ Недостаточно прав")
return
parts = message.text.split()
if len(parts) != 2:
await message.answer(
"❌ Неверный формат команды\n\n"
"Используйте: /user_info <club_card>"
)
return
club_card = parts[1]
try:
async with async_session_maker() as session:
user = await RegistrationService.get_user_by_club_card(session, club_card)
if not user:
await message.answer(f"❌ Пользователь с клубной картой {club_card} не найден")
return
# Получаем счета
accounts = await AccountService.get_user_accounts(session, user.id)
# Получаем выигрыши
winners_result = await session.execute(
select(Winner).where(Winner.user_id == user.id)
)
winners = winners_result.scalars().all()
text = f"👤 Информация о пользователе\n\n"
text += f"🎫 Клубная карта: {user.club_card_number}\n"
text += f"👤 Имя: {user.first_name}"
if user.last_name:
text += f" {user.last_name}"
text += "\n"
if user.username:
text += f"📱 Telegram: @{user.username}\n"
if user.phone:
text += f"📞 Телефон: {user.phone}\n"
text += f"🔑 Код верификации: {user.verification_code}\n"
text += f"📅 Зарегистрирован: {user.created_at.strftime('%d.%m.%Y')}\n\n"
# Счета
text += f"💳 Счета ({len(accounts)}):\n"
if accounts:
for acc in accounts:
status = "" if acc.is_active else ""
text += f" {status} {acc.account_number}\n"
else:
text += " Нет счетов\n"
# Выигрыши
text += f"\n🏆 Выигрыши ({len(winners)}):\n"
if winners:
for w in winners:
status = "" if w.is_claimed else ""
text += f" {status} {w.place} место - {w.prize}\n"
else:
text += " Нет выигрышей\n"
await message.answer(text)
except Exception as e:
await message.answer(f"❌ Ошибка: {str(e)}")