ORM update and lots of improvements

This commit is contained in:
Vladless
2025-05-24 19:30:23 +03:00
parent d2d6ba1be9
commit d4d2ff1867
106 changed files with 7634 additions and 5279 deletions
+211 -70
View File
@@ -1,9 +1,19 @@
from datetime import datetime
from aiogram import F, Router
from aiogram.fsm.context import FSMContext
from aiogram.fsm.state import State, StatesGroup
from aiogram.types import CallbackQuery, InlineKeyboardButton, InlineKeyboardMarkup, Message
from aiogram.types import (
CallbackQuery,
InlineKeyboardButton,
InlineKeyboardMarkup,
Message,
)
from sqlalchemy import delete, distinct, select, update
from sqlalchemy.ext.asyncio import AsyncSession
from database import create_tariff
from database.models import Key, Server, Tariff
from filters.admin import IsAdminFilter
from ..panel.keyboard import AdminPanelCallback
@@ -17,7 +27,6 @@ from .keyboard import (
build_tariff_menu_kb,
)
router = Router()
@@ -36,23 +45,39 @@ class TariffEditState(StatesGroup):
editing_value = State()
@router.callback_query(AdminPanelCallback.filter(F.action == "tariffs"), IsAdminFilter())
@router.callback_query(
AdminPanelCallback.filter(F.action == "tariffs"), IsAdminFilter()
)
async def handle_tariff_menu(callback_query: CallbackQuery):
text = (
"<b>💸 Управление тарифами</b>\n\n"
"Здесь вы можете:\n"
"🆕 Создать новый тариф (длительность, цена, лимит трафика)\n"
"📋 Просмотреть и редактировать существующие тарифы"
"Вы можете выполнить следующие действия:\n\n"
"<b>🆕 Создать тариф</b>\n"
"Установите длительность (в днях)\n"
"• Задайте цену (в рублях)\n"
"• Задайте лимит устройств (hwid/ip_limit)\n"
"• Укажите лимит трафика (в ГБ)\n\n"
"<b>📋 Редактировать тарифы</b>\n"
"• Просматривайте список текущих тарифов\n"
"• Изменяйте параметры или удаляйте при необходимости"
)
await callback_query.message.edit_text(
text=text, reply_markup=build_tariff_menu_kb()
)
await callback_query.message.edit_text(text=text, reply_markup=build_tariff_menu_kb())
@router.callback_query(AdminTariffCallback.filter(F.action == "create"), IsAdminFilter())
@router.callback_query(
AdminTariffCallback.filter(F.action == "create"), IsAdminFilter()
)
async def start_tariff_creation(callback: CallbackQuery, state: FSMContext):
await state.set_state(TariffCreateState.group)
await callback.message.edit_text(
"📁 Введите <b>код группы</b>, в которую вы хотите добавить тариф.\n\n"
"Например: <code>basic</code>, <code>vip</code>, <code>business</code>",
"Например: <code>basic</code>, <code>vip</code>, <code>business</code>\n\n"
"<b>Специальные группы:</b>\n"
"• <code>discounts</code> — тарифы со скидкой\n"
"• <code>discounts_max</code> — тарифы с максимальной скидкой\n"
"• <code>gifts</code> — тарифы для подарков",
reply_markup=build_cancel_kb(),
)
@@ -75,7 +100,8 @@ async def process_tariff_name(message: Message, state: FSMContext):
await state.update_data(name=message.text.strip())
await state.set_state(TariffCreateState.duration)
await message.answer(
"📅 Введите <b>длительность тарифа в днях</b> (например: <i>30</i>):", reply_markup=build_cancel_kb()
"📅 Введите <b>длительность тарифа в днях</b> (например: <i>30</i>):",
reply_markup=build_cancel_kb(),
)
@@ -86,7 +112,9 @@ async def process_tariff_duration(message: Message, state: FSMContext):
if days <= 0:
raise ValueError
except ValueError:
await message.answer("❌ Введите корректное количество дней (целое число больше 0):")
await message.answer(
"❌ Введите корректное количество дней (целое число больше 0):"
)
return
await state.update_data(duration_days=days)
@@ -111,7 +139,8 @@ async def process_tariff_price(message: Message, state: FSMContext):
await state.update_data(price_rub=price)
await state.set_state(TariffCreateState.traffic)
await message.answer(
"📦 Введите <b>лимит трафика в ГБ</b> (например: <i>100</i>, 0 — безлимит):", reply_markup=build_cancel_kb()
"📦 Введите <b>лимит трафика в ГБ</b> (например: <i>100</i>, 0 — безлимит):",
reply_markup=build_cancel_kb(),
)
@@ -122,7 +151,9 @@ async def process_tariff_traffic(message: Message, state: FSMContext):
if traffic < 0:
raise ValueError
except ValueError:
await message.answer("❌ Введите корректный лимит трафика (целое число 0 или больше):")
await message.answer(
"❌ Введите корректный лимит трафика (целое число 0 или больше):"
)
return
await state.update_data(traffic_limit=traffic * 1024**3 if traffic > 0 else None)
@@ -134,13 +165,17 @@ async def process_tariff_traffic(message: Message, state: FSMContext):
@router.message(TariffCreateState.device_limit, IsAdminFilter())
async def process_tariff_device_limit(message: Message, state: FSMContext, session):
async def process_tariff_device_limit(
message: Message, state: FSMContext, session: AsyncSession
):
try:
device_limit = int(message.text.strip())
if device_limit < 0:
raise ValueError
except ValueError:
await message.answer("❌ Введите корректный лимит устройств (целое число 0 или больше):")
await message.answer(
"❌ Введите корректный лимит устройств (целое число 0 или больше):"
)
return
data = await state.get_data()
@@ -159,13 +194,15 @@ async def process_tariff_device_limit(message: Message, state: FSMContext, sessi
await state.set_state(TariffCreateState.confirm_more)
await message.answer(
f"✅ Тариф <b>{new_tariff['name']}</b> добавлен в группу <code>{data['group_code']}</code>.\n\n"
f"✅ Тариф <b>{new_tariff.name}</b> добавлен в группу <code>{data['group_code']}</code>.\n\n"
"➕ Хотите добавить ещё один тариф в эту группу?",
reply_markup=InlineKeyboardMarkup(
inline_keyboard=[
[
InlineKeyboardButton(text="✅ Да", callback_data="add_more_tariff"),
InlineKeyboardButton(text="❌ Нет", callback_data="done_tariff_group"),
InlineKeyboardButton(
text="❌ Нет", callback_data="done_tariff_group"
),
]
]
),
@@ -175,86 +212,148 @@ async def process_tariff_device_limit(message: Message, state: FSMContext, sessi
@router.callback_query(F.data == "add_more_tariff", IsAdminFilter())
async def handle_add_more_tariff(callback: CallbackQuery, state: FSMContext):
await state.set_state(TariffCreateState.name)
await callback.message.edit_text("📝 Введите <b>название следующего тарифа</b>:", reply_markup=build_cancel_kb())
await callback.message.edit_text(
"📝 Введите <b>название следующего тарифа</b>:", reply_markup=build_cancel_kb()
)
@router.callback_query(F.data == "done_tariff_group", IsAdminFilter())
async def handle_done_tariff_group(callback: CallbackQuery, state: FSMContext):
await state.clear()
await callback.message.edit_text("✅ Группа тарифов успешно завершена.", reply_markup=build_tariff_menu_kb())
await callback.message.edit_text(
"✅ Группа тарифов успешно завершена.", reply_markup=build_tariff_menu_kb()
)
@router.callback_query(F.data == "cancel_tariff_creation", IsAdminFilter())
async def cancel_tariff_creation(callback: CallbackQuery, state: FSMContext):
await state.clear()
await callback.message.edit_text("❌ Создание тарифа отменено.", reply_markup=build_tariff_menu_kb())
await callback.message.edit_text(
"❌ Создание тарифа отменено.", reply_markup=build_tariff_menu_kb()
)
@router.callback_query(AdminTariffCallback.filter(F.action == "list"), IsAdminFilter())
async def show_tariff_groups(callback: CallbackQuery, session):
rows = await session.fetch(
"SELECT DISTINCT group_code FROM tariffs WHERE group_code IS NOT NULL ORDER BY group_code"
async def show_tariff_groups(callback: CallbackQuery, session: AsyncSession):
result = await session.execute(
select(distinct(Tariff.group_code))
.where(Tariff.group_code.isnot(None))
.order_by(Tariff.group_code)
)
groups = [r["group_code"] for r in rows]
groups = [row[0] for row in result.fetchall()]
if not groups:
await callback.message.edit_text("❌ Нет сохранённых тарифов.", reply_markup=build_tariff_menu_kb())
await callback.message.edit_text(
"❌ Нет сохранённых тарифов.", reply_markup=build_tariff_menu_kb()
)
return
await callback.message.edit_text("<b>📋 Выберите тарифную группу:</b>", reply_markup=build_tariff_groups_kb(groups))
special_groups = {
"discounts": "🔻 Скидки",
"discounts_max": "🔻 Макс. скидки",
"gifts": "🎁 Подарки",
}
text = "<b>📋 Выберите тарифную группу:</b>\n\n"
text += "<b>Специальные группы:</b>\n"
for code, label in special_groups.items():
status = "✅ создана" if code in groups else "❌ не создана"
text += f"{label} — <code>{code}</code> — <b>{status}</b>\n"
text += "\n"
await callback.message.edit_text(text, reply_markup=build_tariff_groups_kb(groups))
@router.callback_query(AdminTariffCallback.filter(F.action.startswith("group|")), IsAdminFilter())
async def show_tariffs_in_group(callback: CallbackQuery, callback_data: AdminTariffCallback, session):
def tariff_to_dict(tariff: Tariff) -> dict:
return {
"id": tariff.id,
"name": tariff.name,
"price_rub": tariff.price_rub,
"group_code": tariff.group_code,
}
@router.callback_query(
AdminTariffCallback.filter(F.action.startswith("group|")), IsAdminFilter()
)
async def show_tariffs_in_group(
callback: CallbackQuery, callback_data: AdminTariffCallback, session: AsyncSession
):
group_code = callback_data.action.split("|", 1)[1]
rows = await session.fetch("SELECT * FROM tariffs WHERE group_code = $1 ORDER BY id", group_code)
tariffs = [dict(r) for r in rows]
result = await session.execute(
select(Tariff).where(Tariff.group_code == group_code).order_by(Tariff.id)
)
tariffs = result.scalars().all()
if not tariffs:
await callback.message.edit_text("❌ В этой группе пока нет тарифов.")
return
tariff_dicts = [tariff_to_dict(t) for t in tariffs]
await callback.message.edit_text(
f"<b>📦 Тарифы группы: {group_code}</b>", reply_markup=build_tariff_list_kb(tariffs)
f"<b>📦 Тарифы группы: {group_code}</b>",
reply_markup=build_tariff_list_kb(tariff_dicts),
)
@router.callback_query(AdminTariffCallback.filter(F.action.startswith("view|")), IsAdminFilter())
async def view_tariff(callback: CallbackQuery, callback_data: AdminTariffCallback, session):
@router.callback_query(
AdminTariffCallback.filter(F.action.startswith("view|")), IsAdminFilter()
)
async def view_tariff(
callback: CallbackQuery, callback_data: AdminTariffCallback, session: AsyncSession
):
tariff_id = int(callback_data.action.split("|", 1)[1])
tariff = await session.fetchrow("SELECT * FROM tariffs WHERE id = $1", tariff_id)
result = await session.execute(select(Tariff).where(Tariff.id == tariff_id))
tariff = result.scalar_one_or_none()
if not tariff:
await callback.message.edit_text("❌ Тариф не найден.")
return
t = dict(tariff)
traffic_text = f"{t['traffic_limit'] // 1024**3} ГБ" if t["traffic_limit"] else "Безлимит"
device_text = f"{t['device_limit']}" if t["device_limit"] is not None else "Безлимит"
text = (
f"<b>📄 Тариф: {t['name']}</b>\n\n"
f"📁 Группа: <code>{t['group_code']}</code>\n"
f"📅 Длительность: <b>{t['duration_days']} дней</b>\n"
f"💰 Стоимость: <b>{t['price_rub']}₽</b>\n"
f"📦 Трафик: <b>{traffic_text}</b>\n"
f"📱 Устройств: <b>{device_text}</b>\n"
f"{'✅ Активен' if t['is_active'] else '⛔ Отключен'}"
traffic_text = (
f"{tariff.traffic_limit // 1024**3} ГБ" if tariff.traffic_limit else "Безлимит"
)
device_text = (
f"{tariff.device_limit}" if tariff.device_limit is not None else "Безлимит"
)
await callback.message.edit_text(text, reply_markup=build_single_tariff_kb(tariff_id))
text = (
f"<b>📄 Тариф: {tariff.name}</b>\n\n"
f"📁 Группа: <code>{tariff.group_code}</code>\n"
f"📅 Длительность: <b>{tariff.duration_days} дней</b>\n"
f"💰 Стоимость: <b>{tariff.price_rub}₽</b>\n"
f"📦 Трафик: <b>{traffic_text}</b>\n"
f"📱 Устройств: <b>{device_text}</b>\n"
f"{'✅ Активен' if tariff.is_active else '⛔ Отключен'}"
)
await callback.message.edit_text(
text, reply_markup=build_single_tariff_kb(tariff_id)
)
@router.callback_query(AdminTariffCallback.filter(F.action.startswith("delete|")), IsAdminFilter())
async def confirm_tariff_deletion(callback: CallbackQuery, callback_data: AdminTariffCallback):
@router.callback_query(
AdminTariffCallback.filter(F.action.startswith("delete|")), IsAdminFilter()
)
async def confirm_tariff_deletion(
callback: CallbackQuery, callback_data: AdminTariffCallback
):
tariff_id = int(callback_data.action.split("|", 1)[1])
await callback.message.edit_text(
"⚠️ Вы уверены, что хотите <b>удалить</b> этот тариф?",
reply_markup=InlineKeyboardMarkup(
inline_keyboard=[
[
InlineKeyboardButton(text="✅ Да", callback_data=f"confirm_delete_tariff|{tariff_id}"),
InlineKeyboardButton(text="❌ Отмена", callback_data=f"view|{tariff_id}"),
InlineKeyboardButton(
text="✅ Да", callback_data=f"confirm_delete_tariff|{tariff_id}"
),
InlineKeyboardButton(
text="❌ Отмена", callback_data=f"view|{tariff_id}"
),
]
]
),
@@ -262,32 +361,54 @@ async def confirm_tariff_deletion(callback: CallbackQuery, callback_data: AdminT
@router.callback_query(F.data.startswith("confirm_delete_tariff|"), IsAdminFilter())
async def delete_tariff(callback: CallbackQuery, session):
async def delete_tariff(callback: CallbackQuery, session: AsyncSession):
tariff_id = int(callback.data.split("|", 1)[1])
row = await session.fetchrow("SELECT group_code FROM tariffs WHERE id = $1", tariff_id)
if not row:
result = await session.execute(select(Tariff).where(Tariff.id == tariff_id))
tariff = result.scalar_one_or_none()
if not tariff:
await callback.message.edit_text("❌ Тариф не найден.")
return
group_code = row["group_code"]
group_code = tariff.group_code
await session.execute("UPDATE keys SET tariff_id = NULL WHERE tariff_id = $1", tariff_id)
await session.execute("DELETE FROM tariffs WHERE id = $1", tariff_id)
await session.execute(
update(Key).where(Key.tariff_id == tariff_id).values(tariff_id=None)
)
remaining = await session.fetchval("SELECT COUNT(*) FROM tariffs WHERE group_code = $1", group_code)
if remaining == 0:
await session.execute("UPDATE servers SET tariff_group = NULL WHERE tariff_group = $1", group_code)
await session.execute(delete(Tariff).where(Tariff.id == tariff_id))
await callback.message.edit_text("🗑 Тариф успешно удалён.", reply_markup=build_tariff_menu_kb())
result = await session.execute(
select(Tariff).where(Tariff.group_code == group_code)
)
remaining_tariffs = result.scalars().all()
if not remaining_tariffs:
await session.execute(
update(Server)
.where(Server.tariff_group == group_code)
.values(tariff_group=None)
)
await session.commit()
await callback.message.edit_text(
"🗑 Тариф успешно удалён.", reply_markup=build_tariff_menu_kb()
)
@router.callback_query(AdminTariffCallback.filter(F.action.startswith("edit|")), IsAdminFilter())
async def start_edit_tariff(callback: CallbackQuery, callback_data: AdminTariffCallback, state: FSMContext):
@router.callback_query(
AdminTariffCallback.filter(F.action.startswith("edit|")), IsAdminFilter()
)
async def start_edit_tariff(
callback: CallbackQuery, callback_data: AdminTariffCallback, state: FSMContext
):
tariff_id = int(callback_data.action.split("|")[1])
await state.update_data(tariff_id=tariff_id)
await state.set_state(TariffEditState.choosing_field)
await callback.message.edit_text(
"<b>✏️ Что вы хотите изменить?</b>", reply_markup=build_edit_tariff_fields_kb(tariff_id)
"<b>✏️ Что вы хотите изменить?</b>",
reply_markup=build_edit_tariff_fields_kb(tariff_id),
)
@@ -306,17 +427,26 @@ async def ask_new_value(callback: CallbackQuery, state: FSMContext):
}
await callback.message.edit_text(
f"✏️ Введите новое значение для <b>{field_names.get(field, field)}</b>:", reply_markup=build_cancel_kb()
f"✏️ Введите новое значение для <b>{field_names.get(field, field)}</b>:",
reply_markup=build_cancel_kb(),
)
@router.message(TariffEditState.editing_value, IsAdminFilter())
async def apply_edit(message: Message, state: FSMContext, session):
async def apply_edit(message: Message, state: FSMContext, session: AsyncSession):
data = await state.get_data()
tariff_id = data["tariff_id"]
field = data["field"]
value = message.text.strip()
result = await session.execute(select(Tariff).where(Tariff.id == tariff_id))
tariff = result.scalar_one_or_none()
if not tariff:
await message.answer("❌ Тариф не найден.")
await state.clear()
return
if field in ["duration_days", "price_rub", "traffic_limit", "device_limit"]:
try:
num = int(value)
@@ -332,7 +462,10 @@ async def apply_edit(message: Message, state: FSMContext, session):
await message.answer("❌ Введите корректное число.")
return
await session.execute(f"UPDATE tariffs SET {field} = $1, updated_at = NOW() WHERE id = $2", value, tariff_id)
setattr(tariff, field, value)
tariff.updated_at = datetime.utcnow()
await session.commit()
await state.clear()
await message.answer("✅ Тариф успешно обновлён.")
@@ -340,20 +473,28 @@ async def apply_edit(message: Message, state: FSMContext, session):
@router.callback_query(F.data.startswith("toggle_active|"), IsAdminFilter())
async def toggle_tariff_status(callback: CallbackQuery, session):
tariff_id = int(callback.data.split("|")[1])
row = await session.fetchrow("SELECT is_active FROM tariffs WHERE id = $1", tariff_id)
row = await session.fetchrow(
"SELECT is_active FROM tariffs WHERE id = $1", tariff_id
)
if not row:
await callback.message.edit_text("❌ Тариф не найден.")
return
new_status = not row["is_active"]
await session.execute("UPDATE tariffs SET is_active = $1, updated_at = NOW() WHERE id = $2", new_status, tariff_id)
await session.execute(
"UPDATE tariffs SET is_active = $1, updated_at = NOW() WHERE id = $2",
new_status,
tariff_id,
)
status_text = "✅ Тариф активирован." if new_status else "⛔ Тариф отключён."
await callback.message.edit_text(status_text)
@router.callback_query(AdminTariffCallback.filter(F.action.startswith("create|")), IsAdminFilter())
@router.callback_query(
AdminTariffCallback.filter(F.action.startswith("create|")), IsAdminFilter()
)
async def start_tariff_creation_existing_group(
callback: CallbackQuery, callback_data: AdminTariffCallback, state: FSMContext
):