Пересборка клавиатур, рефакторинг чата
All checks were successful
continuous-integration/drone/push Build is passing
All checks were successful
continuous-integration/drone/push Build is passing
This commit is contained in:
@@ -93,12 +93,10 @@ if not owner or owner.telegram_id != callback.from_user.id:
|
|||||||
### Что НЕ может сделать пользователь:
|
### Что НЕ может сделать пользователь:
|
||||||
|
|
||||||
❌ Подтвердить чужой счет
|
❌ Подтвердить чужой счет
|
||||||
❌ Подтвердить счет, который ему не принадлежит
|
|
||||||
❌ Подтвердить один счет дважды
|
❌ Подтвердить один счет дважды
|
||||||
|
|
||||||
### Что может сделать пользователь:
|
### Что может сделать пользователь:
|
||||||
|
|
||||||
✅ Подтвердить только свои счета
|
|
||||||
✅ Подтвердить каждый свой выигрышный счет отдельно
|
✅ Подтвердить каждый свой выигрышный счет отдельно
|
||||||
✅ Видеть номер счета на каждой кнопке
|
✅ Видеть номер счета на каждой кнопке
|
||||||
|
|
||||||
|
|||||||
28
migrations/versions/20260213_1812_12_41aae82e631b_.py
Normal file
28
migrations/versions/20260213_1812_12_41aae82e631b_.py
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
"""
|
||||||
|
|
||||||
|
Revision ID: 41aae82e631b
|
||||||
|
Revises: 64c4f8a81afa
|
||||||
|
Create Date: 2026-02-13 18:12:12.031589
|
||||||
|
|
||||||
|
"""
|
||||||
|
from alembic import op
|
||||||
|
import sqlalchemy as sa
|
||||||
|
|
||||||
|
|
||||||
|
# revision identifiers, used by Alembic.
|
||||||
|
revision = '41aae82e631b'
|
||||||
|
down_revision = '64c4f8a81afa'
|
||||||
|
branch_labels = None
|
||||||
|
depends_on = None
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade() -> None:
|
||||||
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
|
pass
|
||||||
|
# ### end Alembic commands ###
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade() -> None:
|
||||||
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
|
pass
|
||||||
|
# ### end Alembic commands ###
|
||||||
@@ -68,6 +68,10 @@ class AdminStates(StatesGroup):
|
|||||||
remove_participant_bulk_accounts = State()
|
remove_participant_bulk_accounts = State()
|
||||||
participant_search = State()
|
participant_search = State()
|
||||||
|
|
||||||
|
# Добавление/удаление участников в конкретном розыгрыше
|
||||||
|
add_to_lottery_user = State()
|
||||||
|
remove_from_lottery_user = State()
|
||||||
|
|
||||||
# Установка победителей
|
# Установка победителей
|
||||||
set_winner_lottery = State()
|
set_winner_lottery = State()
|
||||||
set_winner_place = State()
|
set_winner_place = State()
|
||||||
@@ -115,11 +119,8 @@ def get_lottery_management_keyboard() -> InlineKeyboardMarkup:
|
|||||||
"""Клавиатура управления розыгрышами"""
|
"""Клавиатура управления розыгрышами"""
|
||||||
buttons = [
|
buttons = [
|
||||||
[InlineKeyboardButton(text="➕ Создать розыгрыш", callback_data="admin_create_lottery")],
|
[InlineKeyboardButton(text="➕ Создать розыгрыш", callback_data="admin_create_lottery")],
|
||||||
[InlineKeyboardButton(text="📝 Редактировать розыгрыш", callback_data="admin_edit_lottery")],
|
[InlineKeyboardButton(text="<EFBFBD> Список всех розыгрышей", callback_data="admin_list_all_lotteries")],
|
||||||
[InlineKeyboardButton(text="🎭 Настройка отображения победителей", callback_data="admin_winner_display_settings")],
|
[InlineKeyboardButton(text="🎭 Настройка отображения победителей", callback_data="admin_winner_display_settings")],
|
||||||
[InlineKeyboardButton(text="📋 Список всех розыгрышей", callback_data="admin_list_all_lotteries")],
|
|
||||||
[InlineKeyboardButton(text="🏁 Завершить розыгрыш", callback_data="admin_finish_lottery")],
|
|
||||||
[InlineKeyboardButton(text="🗑️ Удалить розыгрыш", callback_data="admin_delete_lottery")],
|
|
||||||
[InlineKeyboardButton(text="🔙 Назад", callback_data="admin_panel")]
|
[InlineKeyboardButton(text="🔙 Назад", callback_data="admin_panel")]
|
||||||
]
|
]
|
||||||
return InlineKeyboardMarkup(inline_keyboard=buttons)
|
return InlineKeyboardMarkup(inline_keyboard=buttons)
|
||||||
@@ -153,9 +154,7 @@ def get_winner_management_keyboard() -> InlineKeyboardMarkup:
|
|||||||
[InlineKeyboardButton(text="👑 Установить победителя", callback_data="admin_set_manual_winner")],
|
[InlineKeyboardButton(text="👑 Установить победителя", callback_data="admin_set_manual_winner")],
|
||||||
[InlineKeyboardButton(text="📝 Изменить победителя", callback_data="admin_edit_winner")],
|
[InlineKeyboardButton(text="📝 Изменить победителя", callback_data="admin_edit_winner")],
|
||||||
[InlineKeyboardButton(text="❌ Удалить победителя", callback_data="admin_remove_winner")],
|
[InlineKeyboardButton(text="❌ Удалить победителя", callback_data="admin_remove_winner")],
|
||||||
[InlineKeyboardButton(text="📋 Список победителей", callback_data="admin_list_winners")],
|
[InlineKeyboardButton(text=" Назад", callback_data="admin_panel")]
|
||||||
[InlineKeyboardButton(text="🎲 Провести розыгрыш", callback_data="admin_conduct_draw")],
|
|
||||||
[InlineKeyboardButton(text="🔙 Назад", callback_data="admin_panel")]
|
|
||||||
]
|
]
|
||||||
return InlineKeyboardMarkup(inline_keyboard=buttons)
|
return InlineKeyboardMarkup(inline_keyboard=buttons)
|
||||||
|
|
||||||
@@ -568,14 +567,22 @@ async def show_lottery_detail(callback: CallbackQuery):
|
|||||||
buttons = []
|
buttons = []
|
||||||
|
|
||||||
if not lottery.is_completed:
|
if not lottery.is_completed:
|
||||||
|
# Розыгрыш ещё не проведён
|
||||||
buttons.extend([
|
buttons.extend([
|
||||||
[InlineKeyboardButton(text="👑 Установить победителя", callback_data=f"admin_set_winner_{lottery_id}")],
|
|
||||||
[InlineKeyboardButton(text="🎲 Провести розыгрыш", callback_data=f"admin_conduct_{lottery_id}")],
|
[InlineKeyboardButton(text="🎲 Провести розыгрыш", callback_data=f"admin_conduct_{lottery_id}")],
|
||||||
|
[InlineKeyboardButton(text="👑 Установить победителя", callback_data=f"admin_set_winner_{lottery_id}")],
|
||||||
|
])
|
||||||
|
else:
|
||||||
|
# Розыгрыш завершён - показываем управление победителями
|
||||||
|
buttons.extend([
|
||||||
|
[InlineKeyboardButton(text="✅ Проверка победителей", callback_data=f"admin_check_winners_{lottery_id}")],
|
||||||
|
[InlineKeyboardButton(text="🔄 Провести повторно", callback_data=f"admin_redraw_{lottery_id}")],
|
||||||
])
|
])
|
||||||
|
|
||||||
buttons.extend([
|
buttons.extend([
|
||||||
[InlineKeyboardButton(text="📝 Редактировать", callback_data=f"admin_edit_{lottery_id}")],
|
[InlineKeyboardButton(text="📝 Редактировать", callback_data=f"admin_edit_{lottery_id}")],
|
||||||
[InlineKeyboardButton(text="👥 Участники", callback_data=f"admin_participants_{lottery_id}")],
|
[InlineKeyboardButton(text="👥 Участники", callback_data=f"admin_participants_{lottery_id}")],
|
||||||
|
[InlineKeyboardButton(text="🗑️ Удалить", callback_data=f"admin_del_lottery_{lottery_id}")],
|
||||||
[InlineKeyboardButton(text="🔙 К списку", callback_data="admin_list_all_lotteries")]
|
[InlineKeyboardButton(text="🔙 К списку", callback_data="admin_list_all_lotteries")]
|
||||||
])
|
])
|
||||||
|
|
||||||
@@ -1417,6 +1424,579 @@ async def process_bulk_remove_participant(message: Message, state: FSMContext):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# ======================
|
||||||
|
# ДОБАВЛЕНИЕ/УДАЛЕНИЕ УЧАСТНИКОВ В КОНКРЕТНОМ РОЗЫГРЫШЕ
|
||||||
|
# ======================
|
||||||
|
|
||||||
|
@admin_router.callback_query(F.data.startswith("admin_add_to_"))
|
||||||
|
async def add_participant_to_lottery(callback: CallbackQuery, state: FSMContext):
|
||||||
|
"""Добавление участника в конкретный розыгрыш"""
|
||||||
|
if not is_admin(callback.from_user.id):
|
||||||
|
await callback.answer("❌ Недостаточно прав", show_alert=True)
|
||||||
|
return
|
||||||
|
|
||||||
|
lottery_id = int(callback.data.split("_")[-1])
|
||||||
|
await state.update_data(add_to_lottery_id=lottery_id)
|
||||||
|
|
||||||
|
async with async_session_maker() as session:
|
||||||
|
lottery = await LotteryService.get_lottery(session, lottery_id)
|
||||||
|
|
||||||
|
if not lottery:
|
||||||
|
await callback.answer("❌ Розыгрыш не найден", show_alert=True)
|
||||||
|
return
|
||||||
|
|
||||||
|
text = f"➕ Добавление участника\n\n"
|
||||||
|
text += f"🎯 Розыгрыш: {lottery.title}\n\n"
|
||||||
|
text += "Введите данные участника:\n"
|
||||||
|
text += "• Telegram ID (число)\n"
|
||||||
|
text += "• @username\n"
|
||||||
|
text += "• Номер счета (XX-XX-XX-XX-XX-XX-XX)"
|
||||||
|
|
||||||
|
await callback.message.edit_text(
|
||||||
|
text,
|
||||||
|
reply_markup=InlineKeyboardMarkup(inline_keyboard=[
|
||||||
|
[InlineKeyboardButton(text="❌ Отмена", callback_data=f"admin_participants_{lottery_id}")]
|
||||||
|
])
|
||||||
|
)
|
||||||
|
await state.set_state(AdminStates.add_to_lottery_user)
|
||||||
|
|
||||||
|
|
||||||
|
@admin_router.message(StateFilter(AdminStates.add_to_lottery_user))
|
||||||
|
async def process_add_to_lottery(message: Message, state: FSMContext):
|
||||||
|
"""Обработка добавления участника в конкретный розыгрыш"""
|
||||||
|
if not is_admin(message.from_user.id):
|
||||||
|
await message.answer("❌ Недостаточно прав")
|
||||||
|
return
|
||||||
|
|
||||||
|
data = await state.get_data()
|
||||||
|
lottery_id = data.get('add_to_lottery_id')
|
||||||
|
user_input = message.text.strip()
|
||||||
|
|
||||||
|
async with async_session_maker() as session:
|
||||||
|
lottery = await LotteryService.get_lottery(session, lottery_id)
|
||||||
|
if not lottery:
|
||||||
|
await message.answer("❌ Розыгрыш не найден")
|
||||||
|
await state.clear()
|
||||||
|
return
|
||||||
|
|
||||||
|
# Определяем тип ввода
|
||||||
|
user = None
|
||||||
|
account_number = None
|
||||||
|
|
||||||
|
if user_input.startswith('@'):
|
||||||
|
# Username
|
||||||
|
username = user_input[1:]
|
||||||
|
user = await UserService.get_user_by_username(session, username)
|
||||||
|
elif user_input.isdigit():
|
||||||
|
# Telegram ID
|
||||||
|
telegram_id = int(user_input)
|
||||||
|
user = await UserService.get_user_by_telegram_id(session, telegram_id)
|
||||||
|
elif '-' in user_input:
|
||||||
|
# Номер счета
|
||||||
|
from src.utils.account_utils import parse_accounts_from_message
|
||||||
|
accounts = parse_accounts_from_message(user_input)
|
||||||
|
if accounts:
|
||||||
|
account_number = accounts[0]
|
||||||
|
|
||||||
|
if not user and not account_number:
|
||||||
|
await message.answer(
|
||||||
|
"❌ Не удалось найти пользователя или распознать счет.\n"
|
||||||
|
"Пользователь должен запустить бота командой /start",
|
||||||
|
reply_markup=InlineKeyboardMarkup(inline_keyboard=[
|
||||||
|
[InlineKeyboardButton(text="🔙 Назад", callback_data=f"admin_participants_{lottery_id}")]
|
||||||
|
])
|
||||||
|
)
|
||||||
|
await state.clear()
|
||||||
|
return
|
||||||
|
|
||||||
|
# Добавляем участника
|
||||||
|
if user:
|
||||||
|
success = await ParticipationService.add_participant(session, lottery_id, user.id)
|
||||||
|
name = f"@{user.username}" if user.username else f"{user.first_name} (ID: {user.telegram_id})"
|
||||||
|
else:
|
||||||
|
# Добавление по номеру счета
|
||||||
|
from sqlalchemy import select
|
||||||
|
from ..core.models import Participation
|
||||||
|
|
||||||
|
# Проверяем, не добавлен ли уже этот счет
|
||||||
|
existing = await session.execute(
|
||||||
|
select(Participation).where(
|
||||||
|
Participation.lottery_id == lottery_id,
|
||||||
|
Participation.account_number == account_number
|
||||||
|
)
|
||||||
|
)
|
||||||
|
if existing.scalar_one_or_none():
|
||||||
|
await message.answer(
|
||||||
|
f"⚠️ Счет {account_number} уже участвует в этом розыгрыше",
|
||||||
|
reply_markup=InlineKeyboardMarkup(inline_keyboard=[
|
||||||
|
[InlineKeyboardButton(text="🔙 Назад", callback_data=f"admin_participants_{lottery_id}")]
|
||||||
|
])
|
||||||
|
)
|
||||||
|
await state.clear()
|
||||||
|
return
|
||||||
|
|
||||||
|
participation = Participation(lottery_id=lottery_id, account_number=account_number)
|
||||||
|
session.add(participation)
|
||||||
|
await session.commit()
|
||||||
|
success = True
|
||||||
|
name = f"Счет: {account_number}"
|
||||||
|
|
||||||
|
await state.clear()
|
||||||
|
|
||||||
|
if success:
|
||||||
|
await message.answer(
|
||||||
|
f"✅ Участник добавлен!\n\n"
|
||||||
|
f"👤 {name}\n"
|
||||||
|
f"🎯 Розыгрыш: {lottery.title}",
|
||||||
|
reply_markup=InlineKeyboardMarkup(inline_keyboard=[
|
||||||
|
[InlineKeyboardButton(text="➕ Добавить ещё", callback_data=f"admin_add_to_{lottery_id}")],
|
||||||
|
[InlineKeyboardButton(text="👥 К участникам", callback_data=f"admin_participants_{lottery_id}")]
|
||||||
|
])
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
await message.answer(
|
||||||
|
f"⚠️ Участник уже добавлен в этот розыгрыш",
|
||||||
|
reply_markup=InlineKeyboardMarkup(inline_keyboard=[
|
||||||
|
[InlineKeyboardButton(text="🔙 Назад", callback_data=f"admin_participants_{lottery_id}")]
|
||||||
|
])
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@admin_router.callback_query(F.data.startswith("admin_remove_from_"))
|
||||||
|
async def remove_participant_from_lottery(callback: CallbackQuery, state: FSMContext):
|
||||||
|
"""Удаление участника из конкретного розыгрыша"""
|
||||||
|
if not is_admin(callback.from_user.id):
|
||||||
|
await callback.answer("❌ Недостаточно прав", show_alert=True)
|
||||||
|
return
|
||||||
|
|
||||||
|
lottery_id = int(callback.data.split("_")[-1])
|
||||||
|
await state.update_data(remove_from_lottery_id=lottery_id)
|
||||||
|
|
||||||
|
async with async_session_maker() as session:
|
||||||
|
lottery = await LotteryService.get_lottery(session, lottery_id)
|
||||||
|
participants_count = await ParticipationService.get_participants_count(session, lottery_id)
|
||||||
|
|
||||||
|
if not lottery:
|
||||||
|
await callback.answer("❌ Розыгрыш не найден", show_alert=True)
|
||||||
|
return
|
||||||
|
|
||||||
|
text = f"➖ Удаление участника\n\n"
|
||||||
|
text += f"🎯 Розыгрыш: {lottery.title}\n"
|
||||||
|
text += f"👥 Участников: {participants_count}\n\n"
|
||||||
|
text += "Введите данные участника для удаления:\n"
|
||||||
|
text += "• Telegram ID (число)\n"
|
||||||
|
text += "• @username\n"
|
||||||
|
text += "• Номер счета (XX-XX-XX-XX-XX-XX-XX)"
|
||||||
|
|
||||||
|
await callback.message.edit_text(
|
||||||
|
text,
|
||||||
|
reply_markup=InlineKeyboardMarkup(inline_keyboard=[
|
||||||
|
[InlineKeyboardButton(text="❌ Отмена", callback_data=f"admin_participants_{lottery_id}")]
|
||||||
|
])
|
||||||
|
)
|
||||||
|
await state.set_state(AdminStates.remove_from_lottery_user)
|
||||||
|
|
||||||
|
|
||||||
|
@admin_router.message(StateFilter(AdminStates.remove_from_lottery_user))
|
||||||
|
async def process_remove_from_lottery(message: Message, state: FSMContext):
|
||||||
|
"""Обработка удаления участника из конкретного розыгрыша"""
|
||||||
|
if not is_admin(message.from_user.id):
|
||||||
|
await message.answer("❌ Недостаточно прав")
|
||||||
|
return
|
||||||
|
|
||||||
|
data = await state.get_data()
|
||||||
|
lottery_id = data.get('remove_from_lottery_id')
|
||||||
|
user_input = message.text.strip()
|
||||||
|
|
||||||
|
async with async_session_maker() as session:
|
||||||
|
lottery = await LotteryService.get_lottery(session, lottery_id)
|
||||||
|
if not lottery:
|
||||||
|
await message.answer("❌ Розыгрыш не найден")
|
||||||
|
await state.clear()
|
||||||
|
return
|
||||||
|
|
||||||
|
removed = False
|
||||||
|
name = user_input
|
||||||
|
|
||||||
|
if user_input.startswith('@'):
|
||||||
|
# Username
|
||||||
|
username = user_input[1:]
|
||||||
|
user = await UserService.get_user_by_username(session, username)
|
||||||
|
if user:
|
||||||
|
removed = await ParticipationService.remove_participant(session, lottery_id, user.id)
|
||||||
|
name = f"@{user.username}" if user.username else f"{user.first_name}"
|
||||||
|
elif user_input.isdigit():
|
||||||
|
# Telegram ID
|
||||||
|
telegram_id = int(user_input)
|
||||||
|
user = await UserService.get_user_by_telegram_id(session, telegram_id)
|
||||||
|
if user:
|
||||||
|
removed = await ParticipationService.remove_participant(session, lottery_id, user.id)
|
||||||
|
name = f"@{user.username}" if user and user.username else f"ID: {telegram_id}"
|
||||||
|
elif '-' in user_input:
|
||||||
|
# Номер счета
|
||||||
|
from sqlalchemy import select, delete
|
||||||
|
from ..core.models import Participation
|
||||||
|
|
||||||
|
from src.utils.account_utils import parse_accounts_from_message
|
||||||
|
accounts = parse_accounts_from_message(user_input)
|
||||||
|
if accounts:
|
||||||
|
account_number = accounts[0]
|
||||||
|
result = await session.execute(
|
||||||
|
select(Participation).where(
|
||||||
|
Participation.lottery_id == lottery_id,
|
||||||
|
Participation.account_number == account_number
|
||||||
|
)
|
||||||
|
)
|
||||||
|
participation = result.scalar_one_or_none()
|
||||||
|
if participation:
|
||||||
|
await session.delete(participation)
|
||||||
|
await session.commit()
|
||||||
|
removed = True
|
||||||
|
name = f"Счет: {account_number}"
|
||||||
|
|
||||||
|
await state.clear()
|
||||||
|
|
||||||
|
if removed:
|
||||||
|
await message.answer(
|
||||||
|
f"✅ Участник удалён!\n\n"
|
||||||
|
f"👤 {name}\n"
|
||||||
|
f"🎯 Розыгрыш: {lottery.title}",
|
||||||
|
reply_markup=InlineKeyboardMarkup(inline_keyboard=[
|
||||||
|
[InlineKeyboardButton(text="➖ Удалить ещё", callback_data=f"admin_remove_from_{lottery_id}")],
|
||||||
|
[InlineKeyboardButton(text="👥 К участникам", callback_data=f"admin_participants_{lottery_id}")]
|
||||||
|
])
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
await message.answer(
|
||||||
|
f"⚠️ Участник не найден в этом розыгрыше",
|
||||||
|
reply_markup=InlineKeyboardMarkup(inline_keyboard=[
|
||||||
|
[InlineKeyboardButton(text="🔙 Назад", callback_data=f"admin_participants_{lottery_id}")]
|
||||||
|
])
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# ======================
|
||||||
|
# ПРОВЕРКА ПОБЕДИТЕЛЕЙ И ПОВТОРНЫЙ РОЗЫГРЫШ
|
||||||
|
# ======================
|
||||||
|
|
||||||
|
@admin_router.callback_query(F.data.startswith("admin_check_winners_"))
|
||||||
|
async def check_winners(callback: CallbackQuery):
|
||||||
|
"""Проверка подтверждения победителей"""
|
||||||
|
if not is_admin(callback.from_user.id):
|
||||||
|
await callback.answer("❌ Недостаточно прав", show_alert=True)
|
||||||
|
return
|
||||||
|
|
||||||
|
lottery_id = int(callback.data.split("_")[-1])
|
||||||
|
|
||||||
|
async with async_session_maker() as session:
|
||||||
|
lottery = await LotteryService.get_lottery(session, lottery_id)
|
||||||
|
if not lottery:
|
||||||
|
await callback.answer("❌ Розыгрыш не найден", show_alert=True)
|
||||||
|
return
|
||||||
|
|
||||||
|
winners = await LotteryService.get_winners(session, lottery_id)
|
||||||
|
|
||||||
|
if not winners:
|
||||||
|
await callback.message.edit_text(
|
||||||
|
f"🏆 Проверка победителей\n\n"
|
||||||
|
f"🎯 Розыгрыш: {lottery.title}\n\n"
|
||||||
|
f"❌ Победители не определены. Сначала проведите розыгрыш.",
|
||||||
|
reply_markup=InlineKeyboardMarkup(inline_keyboard=[
|
||||||
|
[InlineKeyboardButton(text="🔙 Назад", callback_data=f"admin_lottery_detail_{lottery_id}")]
|
||||||
|
])
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
|
text = f"🏆 Проверка победителей\n\n"
|
||||||
|
text += f"🎯 Розыгрыш: {lottery.title}\n\n"
|
||||||
|
|
||||||
|
confirmed_count = 0
|
||||||
|
unconfirmed_count = 0
|
||||||
|
|
||||||
|
for winner in winners:
|
||||||
|
status = "✅" if winner.is_claimed else "⏳"
|
||||||
|
|
||||||
|
if winner.is_claimed:
|
||||||
|
confirmed_count += 1
|
||||||
|
else:
|
||||||
|
unconfirmed_count += 1
|
||||||
|
|
||||||
|
# Определяем имя победителя
|
||||||
|
if winner.account_number:
|
||||||
|
name = f"Счет: {winner.account_number}"
|
||||||
|
elif winner.user:
|
||||||
|
name = f"@{winner.user.username}" if winner.user.username else winner.user.first_name
|
||||||
|
else:
|
||||||
|
name = f"ID: {winner.user_id}"
|
||||||
|
|
||||||
|
# Приз
|
||||||
|
prize = lottery.prizes[winner.place - 1] if lottery.prizes and len(lottery.prizes) >= winner.place else "Не указан"
|
||||||
|
|
||||||
|
text += f"{status} {winner.place} место: {name}\n"
|
||||||
|
text += f" 🎁 Приз: {prize}\n"
|
||||||
|
if winner.is_claimed and winner.claimed_at:
|
||||||
|
text += f" 📅 Подтверждено: {winner.claimed_at.strftime('%d.%m.%Y %H:%M')}\n"
|
||||||
|
text += "\n"
|
||||||
|
|
||||||
|
text += f"📊 Итого: {confirmed_count} подтверждено, {unconfirmed_count} ожидает\n"
|
||||||
|
|
||||||
|
buttons = []
|
||||||
|
if unconfirmed_count > 0:
|
||||||
|
buttons.append([InlineKeyboardButton(text="🔄 Переиграть неподтверждённые", callback_data=f"admin_redraw_{lottery_id}")])
|
||||||
|
buttons.append([InlineKeyboardButton(text="🔙 Назад", callback_data=f"admin_lottery_detail_{lottery_id}")])
|
||||||
|
|
||||||
|
await callback.message.edit_text(text, reply_markup=InlineKeyboardMarkup(inline_keyboard=buttons))
|
||||||
|
|
||||||
|
|
||||||
|
@admin_router.callback_query(F.data.startswith("admin_redraw_"))
|
||||||
|
async def redraw_lottery(callback: CallbackQuery):
|
||||||
|
"""Повторный розыгрыш для неподтверждённых призов"""
|
||||||
|
if not is_admin(callback.from_user.id):
|
||||||
|
await callback.answer("❌ Недостаточно прав", show_alert=True)
|
||||||
|
return
|
||||||
|
|
||||||
|
lottery_id = int(callback.data.split("_")[-1])
|
||||||
|
|
||||||
|
async with async_session_maker() as session:
|
||||||
|
lottery = await LotteryService.get_lottery(session, lottery_id)
|
||||||
|
if not lottery:
|
||||||
|
await callback.answer("❌ Розыгрыш не найден", show_alert=True)
|
||||||
|
return
|
||||||
|
|
||||||
|
winners = await LotteryService.get_winners(session, lottery_id)
|
||||||
|
|
||||||
|
# Находим неподтверждённых победителей
|
||||||
|
unconfirmed = [w for w in winners if not w.is_claimed]
|
||||||
|
|
||||||
|
if not unconfirmed:
|
||||||
|
await callback.answer("✅ Все победители подтверждены!", show_alert=True)
|
||||||
|
return
|
||||||
|
|
||||||
|
# Показываем подтверждение
|
||||||
|
text = f"⚠️ Повторный розыгрыш\n\n"
|
||||||
|
text += f"🎯 Розыгрыш: {lottery.title}\n\n"
|
||||||
|
text += f"Будут переиграны {len(unconfirmed)} неподтверждённых мест:\n\n"
|
||||||
|
|
||||||
|
for winner in unconfirmed:
|
||||||
|
if winner.account_number:
|
||||||
|
name = f"Счет: {winner.account_number}"
|
||||||
|
elif winner.user:
|
||||||
|
name = f"@{winner.user.username}" if winner.user.username else winner.user.first_name
|
||||||
|
else:
|
||||||
|
name = f"ID: {winner.user_id}"
|
||||||
|
|
||||||
|
prize = lottery.prizes[winner.place - 1] if lottery.prizes and len(lottery.prizes) >= winner.place else "Не указан"
|
||||||
|
text += f"• {winner.place} место: {name} → {prize}\n"
|
||||||
|
|
||||||
|
text += "\n❗️ Эти счета будут исключены из повторного розыгрыша."
|
||||||
|
|
||||||
|
await callback.message.edit_text(
|
||||||
|
text,
|
||||||
|
reply_markup=InlineKeyboardMarkup(inline_keyboard=[
|
||||||
|
[InlineKeyboardButton(text="✅ Подтвердить переигровку", callback_data=f"admin_redraw_confirm_{lottery_id}")],
|
||||||
|
[InlineKeyboardButton(text="❌ Отмена", callback_data=f"admin_check_winners_{lottery_id}")]
|
||||||
|
])
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@admin_router.callback_query(F.data.startswith("admin_redraw_confirm_"))
|
||||||
|
async def confirm_redraw(callback: CallbackQuery):
|
||||||
|
"""Подтверждение и выполнение повторного розыгрыша"""
|
||||||
|
if not is_admin(callback.from_user.id):
|
||||||
|
await callback.answer("❌ Недостаточно прав", show_alert=True)
|
||||||
|
return
|
||||||
|
|
||||||
|
lottery_id = int(callback.data.split("_")[-1])
|
||||||
|
|
||||||
|
await callback.answer("⏳ Проводится повторный розыгрыш...", show_alert=True)
|
||||||
|
|
||||||
|
async with async_session_maker() as session:
|
||||||
|
from sqlalchemy import select, delete
|
||||||
|
from ..core.models import Winner, Participation
|
||||||
|
import random
|
||||||
|
|
||||||
|
lottery = await LotteryService.get_lottery(session, lottery_id)
|
||||||
|
if not lottery:
|
||||||
|
await callback.message.edit_text("❌ Розыгрыш не найден")
|
||||||
|
return
|
||||||
|
|
||||||
|
winners = await LotteryService.get_winners(session, lottery_id)
|
||||||
|
|
||||||
|
# Собираем подтверждённые и неподтверждённые
|
||||||
|
confirmed_winners = [w for w in winners if w.is_claimed]
|
||||||
|
unconfirmed_winners = [w for w in winners if not w.is_claimed]
|
||||||
|
|
||||||
|
if not unconfirmed_winners:
|
||||||
|
await callback.message.edit_text(
|
||||||
|
"✅ Все победители уже подтверждены!",
|
||||||
|
reply_markup=InlineKeyboardMarkup(inline_keyboard=[
|
||||||
|
[InlineKeyboardButton(text="🔙 Назад", callback_data=f"admin_lottery_detail_{lottery_id}")]
|
||||||
|
])
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
|
# Собираем исключённые счета (подтверждённые победители + бывшие неподтверждённые)
|
||||||
|
excluded_accounts = set()
|
||||||
|
for w in winners:
|
||||||
|
if w.account_number:
|
||||||
|
excluded_accounts.add(w.account_number)
|
||||||
|
|
||||||
|
# Получаем всех участников, исключая уже выигравших
|
||||||
|
result = await session.execute(
|
||||||
|
select(Participation)
|
||||||
|
.where(Participation.lottery_id == lottery_id)
|
||||||
|
)
|
||||||
|
all_participations = result.scalars().all()
|
||||||
|
|
||||||
|
# Фильтруем участников
|
||||||
|
available_participations = [
|
||||||
|
p for p in all_participations
|
||||||
|
if p.account_number not in excluded_accounts
|
||||||
|
]
|
||||||
|
|
||||||
|
if not available_participations:
|
||||||
|
await callback.message.edit_text(
|
||||||
|
"❌ Нет доступных участников для переигровки.\n"
|
||||||
|
"Все участники уже выиграли или были исключены.",
|
||||||
|
reply_markup=InlineKeyboardMarkup(inline_keyboard=[
|
||||||
|
[InlineKeyboardButton(text="🔙 Назад", callback_data=f"admin_lottery_detail_{lottery_id}")]
|
||||||
|
])
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
|
# Удаляем неподтверждённых победителей
|
||||||
|
for winner in unconfirmed_winners:
|
||||||
|
await session.delete(winner)
|
||||||
|
|
||||||
|
# Проводим розыгрыш для неподтверждённых мест
|
||||||
|
new_winners_text = ""
|
||||||
|
random.shuffle(available_participations)
|
||||||
|
|
||||||
|
for i, old_winner in enumerate(unconfirmed_winners):
|
||||||
|
if i >= len(available_participations):
|
||||||
|
break
|
||||||
|
|
||||||
|
new_participation = available_participations[i]
|
||||||
|
|
||||||
|
# Создаём нового победителя
|
||||||
|
new_winner = Winner(
|
||||||
|
lottery_id=lottery_id,
|
||||||
|
user_id=new_participation.user_id,
|
||||||
|
account_number=new_participation.account_number,
|
||||||
|
place=old_winner.place,
|
||||||
|
is_manual=False,
|
||||||
|
is_claimed=False
|
||||||
|
)
|
||||||
|
session.add(new_winner)
|
||||||
|
|
||||||
|
# Исключаем из следующих итераций
|
||||||
|
if new_participation.account_number:
|
||||||
|
excluded_accounts.add(new_participation.account_number)
|
||||||
|
|
||||||
|
prize = lottery.prizes[old_winner.place - 1] if lottery.prizes and len(lottery.prizes) >= old_winner.place else "Приз"
|
||||||
|
name = new_participation.account_number or f"ID: {new_participation.user_id}"
|
||||||
|
new_winners_text += f"🏆 {old_winner.place} место: {name} → {prize}\n"
|
||||||
|
|
||||||
|
await session.commit()
|
||||||
|
|
||||||
|
# Отправляем уведомления новым победителям
|
||||||
|
from ..utils.notifications import notify_winners_async
|
||||||
|
try:
|
||||||
|
await notify_winners_async(callback.bot, session, lottery_id)
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Ошибка при отправке уведомлений: {e}")
|
||||||
|
|
||||||
|
text = f"🎉 Повторный розыгрыш завершён!\n\n"
|
||||||
|
text += f"🎯 Розыгрыш: {lottery.title}\n\n"
|
||||||
|
text += f"Новые победители:\n{new_winners_text}\n"
|
||||||
|
text += "✅ Уведомления отправлены новым победителям"
|
||||||
|
|
||||||
|
await callback.message.edit_text(
|
||||||
|
text,
|
||||||
|
reply_markup=InlineKeyboardMarkup(inline_keyboard=[
|
||||||
|
[InlineKeyboardButton(text="✅ Проверить победителей", callback_data=f"admin_check_winners_{lottery_id}")],
|
||||||
|
[InlineKeyboardButton(text="🔙 К розыгрышу", callback_data=f"admin_lottery_detail_{lottery_id}")]
|
||||||
|
])
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# ======================
|
||||||
|
# УДАЛЕНИЕ РОЗЫГРЫША
|
||||||
|
# ======================
|
||||||
|
|
||||||
|
@admin_router.callback_query(F.data.startswith("admin_del_lottery_"))
|
||||||
|
async def delete_lottery_confirm(callback: CallbackQuery):
|
||||||
|
"""Подтверждение удаления розыгрыша"""
|
||||||
|
if not is_admin(callback.from_user.id):
|
||||||
|
await callback.answer("❌ Недостаточно прав", show_alert=True)
|
||||||
|
return
|
||||||
|
|
||||||
|
lottery_id = int(callback.data.split("_")[-1])
|
||||||
|
|
||||||
|
async with async_session_maker() as session:
|
||||||
|
lottery = await LotteryService.get_lottery(session, lottery_id)
|
||||||
|
if not lottery:
|
||||||
|
await callback.answer("❌ Розыгрыш не найден", show_alert=True)
|
||||||
|
return
|
||||||
|
|
||||||
|
participants_count = await ParticipationService.get_participants_count(session, lottery_id)
|
||||||
|
winners = await LotteryService.get_winners(session, lottery_id)
|
||||||
|
|
||||||
|
text = f"⚠️ Удаление розыгрыша\n\n"
|
||||||
|
text += f"🎯 Название: {lottery.title}\n"
|
||||||
|
text += f"👥 Участников: {participants_count}\n"
|
||||||
|
text += f"🏆 Победителей: {len(winners)}\n\n"
|
||||||
|
text += "❗️ Это действие необратимо!\n"
|
||||||
|
text += "Все данные об участниках и победителях будут удалены."
|
||||||
|
|
||||||
|
await callback.message.edit_text(
|
||||||
|
text,
|
||||||
|
reply_markup=InlineKeyboardMarkup(inline_keyboard=[
|
||||||
|
[InlineKeyboardButton(text="🗑️ Да, удалить", callback_data=f"admin_del_lottery_yes_{lottery_id}")],
|
||||||
|
[InlineKeyboardButton(text="❌ Отмена", callback_data=f"admin_lottery_detail_{lottery_id}")]
|
||||||
|
])
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@admin_router.callback_query(F.data.startswith("admin_del_lottery_yes_"))
|
||||||
|
async def delete_lottery_execute(callback: CallbackQuery):
|
||||||
|
"""Выполнение удаления розыгрыша"""
|
||||||
|
if not is_admin(callback.from_user.id):
|
||||||
|
await callback.answer("❌ Недостаточно прав", show_alert=True)
|
||||||
|
return
|
||||||
|
|
||||||
|
lottery_id = int(callback.data.split("_")[-1])
|
||||||
|
|
||||||
|
async with async_session_maker() as session:
|
||||||
|
from sqlalchemy import delete as sql_delete
|
||||||
|
from ..core.models import Winner, Participation
|
||||||
|
|
||||||
|
lottery = await LotteryService.get_lottery(session, lottery_id)
|
||||||
|
if not lottery:
|
||||||
|
await callback.answer("❌ Розыгрыш не найден", show_alert=True)
|
||||||
|
return
|
||||||
|
|
||||||
|
lottery_title = lottery.title
|
||||||
|
|
||||||
|
# Удаляем победителей
|
||||||
|
await session.execute(sql_delete(Winner).where(Winner.lottery_id == lottery_id))
|
||||||
|
|
||||||
|
# Удаляем участников
|
||||||
|
await session.execute(sql_delete(Participation).where(Participation.lottery_id == lottery_id))
|
||||||
|
|
||||||
|
# Удаляем розыгрыш
|
||||||
|
await session.delete(lottery)
|
||||||
|
await session.commit()
|
||||||
|
|
||||||
|
await callback.message.edit_text(
|
||||||
|
f"✅ Розыгрыш удалён\n\n"
|
||||||
|
f"🗑️ {lottery_title}",
|
||||||
|
reply_markup=InlineKeyboardMarkup(inline_keyboard=[
|
||||||
|
[InlineKeyboardButton(text="📋 К списку розыгрышей", callback_data="admin_list_all_lotteries")]
|
||||||
|
])
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
# ======================
|
# ======================
|
||||||
# МАССОВОЕ УПРАВЛЕНИЕ УЧАСТНИКАМИ ПО СЧЕТАМ
|
# МАССОВОЕ УПРАВЛЕНИЕ УЧАСТНИКАМИ ПО СЧЕТАМ
|
||||||
# ======================
|
# ======================
|
||||||
|
|||||||
Reference in New Issue
Block a user