diff --git a/database/gifts.py b/database/gifts.py index 68083ce1..530381c2 100644 --- a/database/gifts.py +++ b/database/gifts.py @@ -16,6 +16,8 @@ async def store_gift_link( expiry_time: datetime, gift_link: str, tariff_id: int | None = None, + is_unlimited: bool = False, + max_usages: int | None = None, ): try: stmt = insert(Gift).values( @@ -28,10 +30,12 @@ async def store_gift_link( created_at=datetime.utcnow(), is_used=False, tariff_id=tariff_id, + is_unlimited=is_unlimited, + max_usages=max_usages, ) await session.execute(stmt) await session.commit() - logger.info(f"🎁 Подарок {gift_id} сохранён (tariff_id={tariff_id})") + logger.info(f"🎁 Подарок {gift_id} сохранён (tariff_id={tariff_id}, max_usages={max_usages})") return True except SQLAlchemyError as e: logger.error(f"❌ Ошибка при сохранении подарка {gift_id}: {e}") diff --git a/database/models.py b/database/models.py index f587fb7d..3c6f48df 100644 --- a/database/models.py +++ b/database/models.py @@ -152,9 +152,19 @@ class Gift(DictLikeMixin, Base): gift_link = Column(String) created_at = Column(DateTime, default=datetime.utcnow) is_used = Column(Boolean, default=False) + is_unlimited = Column(Boolean, default=False) + max_usages = Column(Integer, nullable=True) tariff_id: Mapped[int | None] = mapped_column(ForeignKey("tariffs.id")) +class GiftUsage(DictLikeMixin, Base): + __tablename__ = "gift_usages" + + gift_id = Column(String, ForeignKey("gifts.gift_id"), primary_key=True) + tg_id = Column(BigInteger, primary_key=True) + used_at = Column(DateTime, default=datetime.utcnow) + + class ManualBan(DictLikeMixin, Base): __tablename__ = "manual_bans" diff --git a/handlers/admin/__init__.py b/handlers/admin/__init__.py index 3a45164f..9428cd31 100644 --- a/handlers/admin/__init__.py +++ b/handlers/admin/__init__.py @@ -15,6 +15,7 @@ from .servers import router as servers_router from .stats import router as stats_router from .tariffs import router as tariffs_router from .users import router as users_router +from .gifts import router as gifts_router router = Router(name="admins_main_router") @@ -32,4 +33,5 @@ router.include_routers( bans_router, ads_router, tariffs_router, + gifts_router ) diff --git a/handlers/admin/gifts/__init__.py b/handlers/admin/gifts/__init__.py new file mode 100644 index 00000000..8de4fd7f --- /dev/null +++ b/handlers/admin/gifts/__init__.py @@ -0,0 +1,3 @@ +__all__ = ("router",) + +from .gifts_handler import router \ No newline at end of file diff --git a/handlers/admin/gifts/gifts_handler.py b/handlers/admin/gifts/gifts_handler.py new file mode 100644 index 00000000..94d6cfce --- /dev/null +++ b/handlers/admin/gifts/gifts_handler.py @@ -0,0 +1,207 @@ +from aiogram import F, Router, types, Bot +from aiogram.types import CallbackQuery +from aiogram.fsm.context import FSMContext +from aiogram.fsm.state import State, StatesGroup +from sqlalchemy.ext.asyncio import AsyncSession +from sqlalchemy import select, delete, func +from aiogram.utils.keyboard import InlineKeyboardBuilder + +from ..panel.keyboard import AdminPanelCallback +from .keyboard import build_admin_gifts_kb, build_gifts_list_kb +from database.models import Tariff, Gift, GiftUsage +from handlers.payments.gift import finalize_gift + +router = Router() + +class GiftCreationState(StatesGroup): + waiting_for_gift_limit = State() + waiting_for_limit_input_or_unlimited = State() + +@router.callback_query(AdminPanelCallback.filter(F.action == "gifts")) +async def admin_gift_menu(callback: CallbackQuery): + await callback.message.edit_text( + text="🎁 Подарки\nВыберите, что хотите сделать:", + reply_markup=build_admin_gifts_kb() + ) + +@router.callback_query(F.data == "admin_gift_create") +async def admin_create_gift_step1(callback: CallbackQuery, session: AsyncSession): + result = await session.execute( + select(Tariff) + .where(Tariff.group_code == "gifts", Tariff.is_active == True) + .order_by(Tariff.id) + ) + tariffs = result.scalars().all() + + if not tariffs: + builder = InlineKeyboardBuilder() + builder.button(text="🔙 Назад", callback_data=AdminPanelCallback(action="gifts").pack()) + await callback.message.edit_text("❌ Нет активных тарифов в группе 'gifts'.", reply_markup=builder.as_markup()) + return + + kb = InlineKeyboardBuilder() + for t in tariffs: + kb.button(text=f"{t.name} – {t.duration_days // 30} мес", callback_data=f"admin_gift_select|{t.id}") + kb.row(types.InlineKeyboardButton(text="🔙 Назад", callback_data=AdminPanelCallback(action="gifts").pack() +)) + + await callback.message.edit_text( + "🎁 Выберите тариф для подарка:", + reply_markup=kb.as_markup() + ) + + +@router.callback_query(F.data.startswith("admin_gift_select|")) +async def handle_tariff_selection(callback: CallbackQuery, state: FSMContext): + tariff_id = int(callback.data.split("|")[1]) + await state.update_data(tariff_id=tariff_id) + await state.set_state(GiftCreationState.waiting_for_limit_input_or_unlimited) + + kb = InlineKeyboardBuilder() + kb.button(text="🔙 Назад", callback_data="admin_gift_create") + await callback.message.edit_text( + "🔢 Введите максимальное количество активаций подарка:", + reply_markup=kb.as_markup() + ) + + +@router.callback_query(F.data == "gift_limit_unlimited") +async def handle_unlimited_gift(callback: CallbackQuery, state: FSMContext, bot: Bot): + data = await state.get_data() + session: AsyncSession = callback.bot["session"] + await state.clear() + await finalize_gift(callback.message, session, bot, data, is_unlimited=True) + +@router.message(GiftCreationState.waiting_for_limit_input_or_unlimited) +async def handle_limited_gift_input(message: types.Message, session: AsyncSession, state: FSMContext, bot: Bot): + try: + max_usages = int(message.text.strip()) + if max_usages <= 0: + raise ValueError + except ValueError: + await message.answer("❌ Введите корректное положительное число.") + return + + data = await state.get_data() + data["max_usages"] = max_usages + await state.clear() + await finalize_gift(message, session, bot, data, is_unlimited=False) + +@router.callback_query(F.data == "admin_gifts_all") +async def show_gifts_page(callback: CallbackQuery, session: AsyncSession): + await show_gift_list(callback, session, page=1) + +@router.callback_query(F.data.startswith("gifts_page|")) +async def paginate_gifts(callback: CallbackQuery, session: AsyncSession): + page = int(callback.data.split("|")[1]) + await show_gift_list(callback, session, page) + + +async def show_gift_list(callback: CallbackQuery, session: AsyncSession, page: int): + limit = 10 + offset = (page - 1) * limit + + stmt = select(Gift).order_by(Gift.created_at.desc()).offset(offset).limit(limit) + result = await session.execute(stmt) + gifts = result.scalars().all() + + from aiogram.utils.keyboard import InlineKeyboardBuilder + builder = InlineKeyboardBuilder() + + if not gifts: + builder.button(text="🔙 Назад", callback_data=AdminPanelCallback(action="gifts").pack()) + await callback.message.edit_text( + "❌ Подарки не найдены.", + reply_markup=builder.as_markup() + ) + return + + keyboard = build_gifts_list_kb(gifts, page, total=len(gifts)) + + builder.inline_keyboard.extend(keyboard.inline_keyboard) + builder.row(types.InlineKeyboardButton(text="🔙 Назад", callback_data=AdminPanelCallback(action="gifts").pack())) + + await callback.message.edit_text( + f"🎁 Список подарков\nСтраница {page}:", + reply_markup=builder.as_markup() + ) + + +async def show_gift_list(callback: CallbackQuery, session: AsyncSession, page: int): + limit = 10 + offset = (page - 1) * limit + + stmt = select(Gift).order_by(Gift.created_at.desc()).offset(offset).limit(limit) + result = await session.execute(stmt) + gifts = result.scalars().all() + + builder = InlineKeyboardBuilder() + + if not gifts: + builder.button(text="🔙 Назад", callback_data=AdminPanelCallback(action="gifts").pack()) + await callback.message.edit_text( + "❌ Подарки не найдены.", + reply_markup=builder.as_markup() + ) + return + + keyboard = build_gifts_list_kb(gifts, page, total=len(gifts)) + + for row in keyboard.inline_keyboard: + builder.row(*row) + + await callback.message.edit_text( + f"🎁 Список подарков\nСтраница {page}:", + reply_markup=builder.as_markup() + ) + + +@router.callback_query(F.data.startswith("gift_view|")) +async def view_gift(callback: CallbackQuery, session: AsyncSession): + gift_id = callback.data.split("|")[1] + + result = await session.execute(select(Gift).where(Gift.gift_id == gift_id)) + gift = result.scalar_one_or_none() + + if not gift: + await callback.message.edit_text("❌ Подарок не найден.") + return + + usage_result = await session.execute( + select(func.count()).select_from(GiftUsage).where(GiftUsage.gift_id == gift_id) + ) + used_count = usage_result.scalar_one() + + usage_text = f"{used_count}/{gift.max_usages}" if gift.max_usages else "∞" + + text = ( + f"🎁 Подарок\n" + f"ID: {gift.gift_id}\n" + f"Месяцев: {gift.selected_months}\n" + f"Активаций: {usage_text}\n" + f"Срок действия: {gift.expiry_time.strftime('%d.%m.%Y')}\n" + f"Ссылка для активации:\n
{gift.gift_link}
" + ) + + builder = InlineKeyboardBuilder() + builder.button(text="🗑 Удалить", callback_data=f"gift_delete|{gift_id}") + builder.button(text="🔙 Назад", callback_data="admin_gifts_all") + + await callback.message.edit_text(text, reply_markup=builder.as_markup()) + + +@router.callback_query(F.data.startswith("gift_delete|")) +async def delete_gift(callback: CallbackQuery, session: AsyncSession): + gift_id = callback.data.split("|")[1] + + await session.execute(delete(GiftUsage).where(GiftUsage.gift_id == gift_id)) + await session.execute(delete(Gift).where(Gift.gift_id == gift_id)) + await session.commit() + + builder = InlineKeyboardBuilder() + builder.button(text="🔙 Назад к списку", callback_data="admin_gifts_all") + + await callback.message.edit_text( + "✅ Подарок удалён.", + reply_markup=builder.as_markup() + ) diff --git a/handlers/admin/gifts/keyboard.py b/handlers/admin/gifts/keyboard.py new file mode 100644 index 00000000..2be1c4cd --- /dev/null +++ b/handlers/admin/gifts/keyboard.py @@ -0,0 +1,55 @@ +from aiogram.types import InlineKeyboardMarkup, InlineKeyboardButton +from aiogram.utils.keyboard import InlineKeyboardBuilder +from database.models import Tariff, Gift +from ..panel.keyboard import AdminPanelCallback + +from handlers.buttons import BACK + +def build_admin_gifts_kb() -> InlineKeyboardMarkup: + builder = InlineKeyboardBuilder() + + builder.row( + InlineKeyboardButton(text="🎁 Создать подарок", callback_data="admin_gift_create") + ) + builder.row( + InlineKeyboardButton(text="📦 Все подарки", callback_data="admin_gifts_all"), + ) + builder.row( + InlineKeyboardButton(text=BACK, callback_data="admin") + ) + + return builder.as_markup() + + +def build_gift_tariffs_kb(tariffs: list[Tariff]) -> InlineKeyboardMarkup: + builder = InlineKeyboardBuilder() + for tariff in tariffs: + builder.button( + text=f"{tariff.name} — {tariff.duration_days // 30} мес.", + callback_data=f"admin_gift_confirm|{tariff.id}", + ) + builder.button(text="🔙 Назад", callback_data=AdminPanelCallback(action="gifts").pack()) + builder.adjust(1) + return builder.as_markup() + + +def build_gifts_list_kb(gifts: list[Gift], page: int, total: int) -> InlineKeyboardMarkup: + builder = InlineKeyboardBuilder() + + for gift in gifts: + button_text = f"{gift.gift_id[:6]}... — {gift.selected_months} мес." + builder.button( + text=button_text, + callback_data=f"gift_view|{gift.gift_id}", + ) + + nav = [] + if page > 1: + nav.append(InlineKeyboardButton(text="⬅️ Назад", callback_data=f"gifts_page|{page - 1}")) + if len(gifts) == 10: + nav.append(InlineKeyboardButton(text="➡️ Далее", callback_data=f"gifts_page|{page + 1}")) + + builder.row(*nav) + builder.button(text="🔙 Назад", callback_data=AdminPanelCallback(action="gifts").pack()) + + return builder.as_markup() \ No newline at end of file diff --git a/handlers/admin/panel/keyboard.py b/handlers/admin/panel/keyboard.py index b7985acd..f77f0da6 100644 --- a/handlers/admin/panel/keyboard.py +++ b/handlers/admin/panel/keyboard.py @@ -49,6 +49,11 @@ def build_panel_kb() -> InlineKeyboardMarkup: text="💸 Тарифы", callback_data=AdminPanelCallback(action="tariffs").pack() ) ) + builder.row( + InlineKeyboardButton( + text="🎁 Подарки", callback_data=AdminPanelCallback(action="gifts").pack() + ) + ) builder.row( InlineKeyboardButton( text="📊 Статистика", @@ -59,7 +64,7 @@ def build_panel_kb() -> InlineKeyboardMarkup: ), ) builder.button(text=MAIN_MENU, callback_data="profile") - builder.adjust(1, 1, 2, 2, 1, 2, 1) + builder.adjust(1, 1, 2, 2, 1, 1, 2, 1) return builder.as_markup() diff --git a/handlers/admin/panel/panel_handler.py b/handlers/admin/panel/panel_handler.py index 0febdab4..aa8a4e34 100644 --- a/handlers/admin/panel/panel_handler.py +++ b/handlers/admin/panel/panel_handler.py @@ -47,8 +47,8 @@ async def handle_admin_callback_query(callback_query: CallbackQuery, state: FSMC @router.callback_query(F.data == "admin", IsAdminFilter()) -async def handle_admin_callback_query(callback_query: CallbackQuery, state: FSMContext): - await handle_admin_message(callback_query.message, state) +async def handle_admin_callback_query_simple(callback_query: CallbackQuery, state: FSMContext): + await handle_admin_callback_query(callback_query, state) @router.message(Command("admin"), IsAdminFilter()) diff --git a/handlers/payments/gift.cpython-312-x86_64-linux-gnu.so b/handlers/payments/gift.cpython-312-x86_64-linux-gnu.so index b9fed8bd..6d2500c8 100644 Binary files a/handlers/payments/gift.cpython-312-x86_64-linux-gnu.so and b/handlers/payments/gift.cpython-312-x86_64-linux-gnu.so differ