feat: Система автоматического подтверждения выигрышей с поддержкой множественных счетов
Some checks reported errors
continuous-integration/drone/push Build encountered an error

Основные изменения:

 Новые функции:
- Система регистрации пользователей с множественными счетами
- Автоматическое подтверждение выигрышей через 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
This commit is contained in:
2025-11-16 14:01:30 +09:00
parent 31c4c5382a
commit 505d26f0e9
21 changed files with 4217 additions and 68 deletions

View File

@@ -0,0 +1,585 @@
"""Админские обработчики для управления счетами и верификации"""
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)}")

View File

@@ -471,9 +471,13 @@ async def show_lottery_participants(callback: CallbackQuery):
for i, participation in enumerate(lottery.participations[:20], 1): # Показываем первых 20
user = participation.user
username = f"@{user.username}" if user.username else "Нет username"
text += f"{i}. {user.first_name} {user.last_name or ''}\n"
text += f" {username} | ID: {user.telegram_id}\n"
if user:
username = f"@{user.username}" if user.username else "Нет username"
text += f"{i}. {user.first_name} {user.last_name or ''}\n"
text += f" {username} | ID: {user.telegram_id}\n"
else:
# Если пользователя нет, показываем номер счета
text += f"{i}. Счет: {participation.account_number or 'Не указан'}\n"
text += f" Участвует с: {participation.created_at.strftime('%d.%m %H:%M')}\n\n"
if len(lottery.participations) > 20:
@@ -1683,11 +1687,13 @@ async def redirect_to_edit_lottery(callback: CallbackQuery, state: FSMContext):
parts = callback.data.split("_")
if len(parts) == 3: # admin_edit_123
lottery_id = int(parts[2])
# Подменяем callback_data для обработки существующим хэндлером
callback.data = f"admin_edit_lottery_select_{lottery_id}"
# Напрямую вызываем обработчик вместо подмены callback_data
await state.update_data(edit_lottery_id=lottery_id)
await choose_edit_field(callback, state)
else:
# Если формат другой, то это уже правильный callback
lottery_id = int(callback.data.split("_")[-1])
await state.update_data(edit_lottery_id=lottery_id)
await choose_edit_field(callback, state)
@@ -2027,8 +2033,8 @@ async def handle_set_winner_from_lottery(callback: CallbackQuery, state: FSMCont
lottery_id = int(callback.data.split("_")[-1])
# Перенаправляем на стандартный обработчик
callback.data = f"admin_choose_winner_lottery_{lottery_id}"
# Напрямую вызываем обработчик вместо подмены callback_data
await state.update_data(winner_lottery_id=lottery_id)
await choose_winner_place(callback, state)
@@ -2610,11 +2616,12 @@ async def conduct_lottery_draw(callback: CallbackQuery):
await callback.answer("Нет участников для розыгрыша", show_alert=True)
return
# Проводим розыгрыш
from ..display.conduct_draw import conduct_draw
winners = await conduct_draw(lottery_id)
# Проводим розыгрыш через сервис
winners_dict = await LotteryService.conduct_draw(session, lottery_id)
if winners:
if winners_dict:
# Получаем победителей из базы
winners = await LotteryService.get_winners(session, lottery_id)
text = f"🎉 Розыгрыш '{lottery.title}' завершён!\n\n"
text += "🏆 Победители:\n"
for winner in winners:

View File

@@ -0,0 +1,314 @@
"""Команды для повторного розыгрыша неподтвержденных выигрышей"""
from aiogram import Router, F
from aiogram.types import Message, InlineKeyboardButton, InlineKeyboardMarkup
from aiogram.filters import Command
from sqlalchemy import select, and_
from datetime import datetime, timezone, timedelta
import random
from src.core.database import async_session_maker
from src.core.registration_services import AccountService, WinnerNotificationService
from src.core.services import LotteryService
from src.core.models import User, Winner
from src.core.config import ADMIN_IDS
router = Router()
def is_admin(user_id: int) -> bool:
"""Проверка прав администратора"""
return user_id in ADMIN_IDS
@router.message(Command("check_unclaimed"))
async def check_unclaimed_winners(message: Message):
"""
Проверить неподтвержденные выигрыши (более 24 часов)
Формат: /check_unclaimed <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"
"Используйте: /check_unclaimed <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:
from sqlalchemy.orm import selectinload
from src.core.models import Lottery
# Загружаем розыгрыш с участниками
lottery_result = await session.execute(
select(Lottery)
.options(selectinload(Lottery.participations))
.where(Lottery.id == lottery_id)
)
lottery = lottery_result.scalar_one_or_none()
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
# Находим неподтвержденные выигрыши старше 24 часов
now = datetime.now(timezone.utc)
unclaimed = []
for winner in winners:
if not winner.is_claimed and winner.is_notified:
# Проверяем, прошло ли 24 часа
time_passed = now - winner.created_at
if time_passed.total_seconds() > 24 * 3600: # 24 часа
unclaimed.append({
'winner': winner,
'hours_passed': int(time_passed.total_seconds() / 3600)
})
if not unclaimed:
await message.answer(
f"Все победители розыгрыша '{lottery.title}' подтвердили выигрыш\n"
f"или срок подтверждения еще не истек."
)
return
text = f"⚠️ **Неподтвержденные выигрыши в розыгрыше '{lottery.title}':**\n\n"
for item in unclaimed:
winner = item['winner']
hours = item['hours_passed']
text += f"🏆 {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"
text += f" ⏰ Прошло: {hours} часов\n\n"
text += f"\n📊 Всего неподтвержденных: {len(unclaimed)}\n\n"
text += f"Используйте /redraw {lottery_id} для повторного розыгрыша"
await message.answer(text, parse_mode="Markdown")
except Exception as e:
await message.answer(f"❌ Ошибка: {str(e)}")
@router.message(Command("redraw"))
async def redraw_lottery(message: Message):
"""
Переиграть розыгрыш для неподтвержденных выигрышей
Формат: /redraw <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"
"Используйте: /redraw <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:
from sqlalchemy.orm import selectinload
from src.core.models import Lottery
# Загружаем розыгрыш с участниками
lottery_result = await session.execute(
select(Lottery)
.options(selectinload(Lottery.participations))
.where(Lottery.id == lottery_id)
)
lottery = lottery_result.scalar_one_or_none()
if not lottery:
await message.answer(f"❌ Розыгрыш #{lottery_id} не найден")
return
winners = await LotteryService.get_winners(session, lottery_id)
# Находим неподтвержденные выигрыши старше 24 часов
now = datetime.now(timezone.utc)
unclaimed_winners = []
for winner in winners:
if not winner.is_claimed and winner.is_notified:
time_passed = now - winner.created_at
if time_passed.total_seconds() > 24 * 3600:
unclaimed_winners.append(winner)
if not unclaimed_winners:
await message.answer(
"✅ Нет неподтвержденных выигрышей старше 24 часов.\n"
"Повторный розыгрыш не требуется."
)
return
# Получаем всех участников, исключая текущих победителей
all_participants = []
current_winner_accounts = set()
for winner in winners:
if winner.account_number:
current_winner_accounts.add(winner.account_number)
for p in lottery.participations:
if p.account_number and p.account_number not in current_winner_accounts:
all_participants.append(p)
if not all_participants:
await message.answer(
"❌ Нет доступных участников для повторного розыгрыша.\n"
"Все участники уже являются победителями."
)
return
# Переигрываем каждое неподтвержденное место
redraw_results = []
for old_winner in unclaimed_winners:
if not all_participants:
break
# Выбираем нового победителя
new_participant = random.choice(all_participants)
all_participants.remove(new_participant)
# Удаляем старого победителя
await session.delete(old_winner)
# Создаем нового победителя
new_winner = Winner(
lottery_id=lottery_id,
user_id=None,
account_number=new_participant.account_number,
account_id=new_participant.account_id,
place=old_winner.place,
prize=old_winner.prize,
is_manual=False,
is_notified=False,
is_claimed=False
)
session.add(new_winner)
redraw_results.append({
'place': old_winner.place,
'prize': old_winner.prize,
'old_account': old_winner.account_number,
'new_account': new_participant.account_number
})
await session.commit()
# Отправляем уведомления новым победителям
for result in redraw_results:
# Находим нового победителя
new_winner_result = await session.execute(
select(Winner).where(
and_(
Winner.lottery_id == lottery_id,
Winner.place == result['place'],
Winner.account_number == result['new_account']
)
)
)
new_winner = new_winner_result.scalar_one_or_none()
if new_winner:
# Отправляем уведомление новому победителю
owner = await AccountService.get_account_owner(session, new_winner.account_number)
if owner and owner.telegram_id:
# Создаем токен верификации
await WinnerNotificationService.create_verification_token(
session,
new_winner.id
)
# Формируем сообщение
notification_message = (
f"🎉 Поздравляем! Ваш счет выиграл!\n\n"
f"🎯 Розыгрыш: {lottery.title}\n"
f"🏆 Место: {new_winner.place}\n"
f"🎁 Приз: {new_winner.prize}\n"
f"💳 Счет: {new_winner.account_number}\n\n"
f"⏰ **У вас есть 24 часа для подтверждения!**\n\n"
f"Нажмите кнопку ниже, чтобы подтвердить получение приза."
)
keyboard = InlineKeyboardMarkup(inline_keyboard=[
[InlineKeyboardButton(
text="✅ Подтвердить получение приза",
callback_data=f"confirm_win_{new_winner.id}"
)]
])
try:
await message.bot.send_message(
owner.telegram_id,
notification_message,
reply_markup=keyboard,
parse_mode="Markdown"
)
new_winner.is_notified = True
await session.commit()
except:
pass
# Формируем отчет для админа
text = f"🔄 **Повторный розыгрыш завершен!**\n\n"
text += f"🎯 Розыгрыш: {lottery.title}\n"
text += f"📊 Переиграно мест: {len(redraw_results)}\n\n"
for result in redraw_results:
text += f"🏆 {result['place']} место - {result['prize']}\n"
text += f" ❌ Было: {result['old_account']}\n"
text += f" ✅ Стало: {result['new_account']}\n\n"
text += "📨 Новым победителям отправлены уведомления"
await message.answer(text, parse_mode="Markdown")
except Exception as e:
await message.answer(f"❌ Ошибка: {str(e)}")

View File

@@ -0,0 +1,150 @@
"""Обработчики для регистрации пользователей"""
from aiogram import Router, F
from aiogram.types import Message, CallbackQuery, InlineKeyboardButton, InlineKeyboardMarkup
from aiogram.filters import Command, StateFilter
from aiogram.fsm.context import FSMContext
from aiogram.fsm.state import State, StatesGroup
from src.core.database import async_session_maker
from src.core.registration_services import RegistrationService, AccountService
from src.core.services import UserService
router = Router()
class RegistrationStates(StatesGroup):
"""Состояния для процесса регистрации"""
waiting_for_club_card = State()
waiting_for_phone = State()
@router.callback_query(F.data == "start_registration")
async def start_registration(callback: CallbackQuery, state: FSMContext):
"""Начать процесс регистрации"""
text = (
"📝 Регистрация в системе\n\n"
"Для участия в розыгрышах необходимо зарегистрироваться.\n\n"
"Введите номер вашей клубной карты:"
)
await callback.message.edit_text(
text,
reply_markup=InlineKeyboardMarkup(inline_keyboard=[
[InlineKeyboardButton(text="❌ Отмена", callback_data="back_to_main")]
])
)
await state.set_state(RegistrationStates.waiting_for_club_card)
@router.message(StateFilter(RegistrationStates.waiting_for_club_card))
async def process_club_card(message: Message, state: FSMContext):
"""Обработка номера клубной карты"""
club_card_number = message.text.strip()
# Проверяем, не занята ли карта
async with async_session_maker() as session:
existing_user = await RegistrationService.get_user_by_club_card(session, club_card_number)
if existing_user:
await message.answer(
f"❌ Клубная карта {club_card_number} уже зарегистрирована.\n\n"
"Если это ваша карта, обратитесь к администратору."
)
await state.clear()
return
await state.update_data(club_card_number=club_card_number)
await message.answer(
"📱 Теперь введите ваш номер телефона\n"
"(или отправьте '-' чтобы пропустить):"
)
await state.set_state(RegistrationStates.waiting_for_phone)
@router.message(StateFilter(RegistrationStates.waiting_for_phone))
async def process_phone(message: Message, state: FSMContext):
"""Обработка номера телефона"""
phone = None if message.text.strip() == "-" else message.text.strip()
data = await state.get_data()
club_card_number = data['club_card_number']
try:
async with async_session_maker() as session:
user = await RegistrationService.register_user(
session,
telegram_id=message.from_user.id,
club_card_number=club_card_number,
phone=phone
)
text = (
"✅ Регистрация завершена!\n\n"
f"🎫 Клубная карта: {user.club_card_number}\n"
f"🔑 Ваш код верификации: **{user.verification_code}**\n\n"
"⚠️ Сохраните этот код! Он понадобится для подтверждения выигрыша.\n\n"
"Теперь вы можете участвовать в розыгрышах!"
)
await message.answer(text, parse_mode="Markdown")
await state.clear()
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(Command("my_code"))
async def show_verification_code(message: Message):
"""Показать код верификации пользователя"""
async with async_session_maker() as session:
user = await UserService.get_user_by_telegram_id(session, message.from_user.id)
if not user or not user.is_registered:
await message.answer(
"❌ Вы не зарегистрированы в системе.\n\n"
"Для регистрации отправьте /start и выберите 'Регистрация'"
)
return
text = (
"🔑 Ваш код верификации:\n\n"
f"**{user.verification_code}**\n\n"
"Этот код используется для подтверждения выигрыша.\n"
"Сообщите его администратору при получении приза."
)
await message.answer(text, parse_mode="Markdown")
@router.message(Command("my_accounts"))
async def show_user_accounts(message: Message):
"""Показать счета пользователя"""
async with async_session_maker() as session:
user = await UserService.get_user_by_telegram_id(session, message.from_user.id)
if not user or not user.is_registered:
await message.answer("❌ Вы не зарегистрированы в системе")
return
accounts = await AccountService.get_user_accounts(session, user.id)
if not accounts:
await message.answer(
"У вас пока нет привязанных счетов.\n\n"
"Счета добавляются администратором."
)
return
text = f"💳 Ваши счета (Клубная карта: {user.club_card_number}):\n\n"
for i, account in enumerate(accounts, 1):
status = "" if account.is_active else ""
text += f"{i}. {status} {account.account_number}\n"
await message.answer(text)