Tweak server edit menu promo group display

This commit is contained in:
Egor
2025-09-24 17:18:12 +03:00
parent 68452974f1
commit a6465b0b0b
8 changed files with 540 additions and 72 deletions

View File

@@ -44,6 +44,13 @@ async def get_promo_group_by_id(db: AsyncSession, group_id: int) -> Optional[Pro
return await db.get(PromoGroup, group_id)
async def get_all_promo_groups(db: AsyncSession) -> List[PromoGroup]:
result = await db.execute(
select(PromoGroup).order_by(PromoGroup.is_default.desc(), PromoGroup.name)
)
return result.scalars().all()
async def get_default_promo_group(db: AsyncSession) -> Optional[PromoGroup]:
result = await db.execute(
select(PromoGroup).where(PromoGroup.is_default.is_(True))

View File

@@ -4,7 +4,13 @@ from sqlalchemy import select, and_, func, update, delete, text
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy.orm import selectinload
from app.database.models import ServerSquad, SubscriptionServer, Subscription
from app.database.models import (
ServerSquad,
SubscriptionServer,
Subscription,
PromoGroup,
server_squad_promo_groups,
)
logger = logging.getLogger(__name__)
@@ -18,9 +24,43 @@ async def create_server_squad(
price_kopeks: int = 0,
description: str = None,
max_users: int = None,
is_available: bool = True
is_available: bool = True,
promo_group_ids: Optional[List[int]] = None,
) -> ServerSquad:
result = await db.execute(
select(PromoGroup.id)
.where(PromoGroup.is_default.is_(True))
.limit(1)
)
default_group_id = result.scalar_one_or_none()
if promo_group_ids is None:
promo_group_ids = []
if default_group_id is not None:
promo_group_ids.append(default_group_id)
else:
fallback_group_result = await db.execute(
select(PromoGroup.id)
.order_by(PromoGroup.is_default.desc(), PromoGroup.id)
.limit(1)
)
fallback_group_id = fallback_group_result.scalar_one_or_none()
if fallback_group_id is not None:
promo_group_ids.append(fallback_group_id)
unique_group_ids = list(dict.fromkeys(promo_group_ids or []))
if not unique_group_ids:
raise ValueError("Server squad must have at least one promo group")
groups_result = await db.execute(
select(PromoGroup).where(PromoGroup.id.in_(unique_group_ids))
)
groups = groups_result.scalars().all()
if len(groups) != len(unique_group_ids):
raise ValueError("One or more promo groups not found")
server_squad = ServerSquad(
squad_uuid=squad_uuid,
display_name=display_name,
@@ -31,12 +71,21 @@ async def create_server_squad(
max_users=max_users,
is_available=is_available
)
db.add(server_squad)
await db.flush()
server_squad.promo_groups = groups
await db.commit()
await db.refresh(server_squad)
logger.info(f"✅ Создан сервер {display_name} (UUID: {squad_uuid})")
logger.info(
"✅ Создан сервер %s (UUID: %s) с промогруппами: %s",
display_name,
squad_uuid,
", ".join(group.name for group in groups),
)
return server_squad
@@ -46,7 +95,9 @@ async def get_server_squad_by_uuid(
) -> Optional[ServerSquad]:
result = await db.execute(
select(ServerSquad).where(ServerSquad.squad_uuid == squad_uuid)
select(ServerSquad)
.options(selectinload(ServerSquad.promo_groups))
.where(ServerSquad.squad_uuid == squad_uuid)
)
return result.scalar_one_or_none()
@@ -57,7 +108,9 @@ async def get_server_squad_by_id(
) -> Optional[ServerSquad]:
result = await db.execute(
select(ServerSquad).where(ServerSquad.id == server_id)
select(ServerSquad)
.options(selectinload(ServerSquad.promo_groups))
.where(ServerSquad.id == server_id)
)
return result.scalar_one_or_none()
@@ -69,7 +122,7 @@ async def get_all_server_squads(
limit: int = 50
) -> Tuple[List[ServerSquad], int]:
query = select(ServerSquad)
query = select(ServerSquad).options(selectinload(ServerSquad.promo_groups))
if available_only:
query = query.where(ServerSquad.is_available == True)
@@ -91,16 +144,75 @@ async def get_all_server_squads(
return servers, total_count
async def get_available_server_squads(db: AsyncSession) -> List[ServerSquad]:
async def get_available_server_squads(
db: AsyncSession,
promo_group_id: Optional[int] = None,
) -> List[ServerSquad]:
result = await db.execute(
query = (
select(ServerSquad)
.options(selectinload(ServerSquad.promo_groups))
.where(ServerSquad.is_available == True)
.order_by(ServerSquad.sort_order, ServerSquad.display_name)
)
if promo_group_id is not None:
query = (
query.join(
server_squad_promo_groups,
server_squad_promo_groups.c.server_squad_id == ServerSquad.id,
)
.where(server_squad_promo_groups.c.promo_group_id == promo_group_id)
.distinct()
)
query = query.order_by(ServerSquad.sort_order, ServerSquad.display_name)
result = await db.execute(query)
return result.scalars().all()
async def set_server_squad_promo_groups(
db: AsyncSession,
server_id: int,
promo_group_ids: List[int],
) -> Optional[ServerSquad]:
unique_group_ids = list(dict.fromkeys(promo_group_ids or []))
if not unique_group_ids:
logger.warning("Попытка оставить сервер без промогрупп (id=%s)", server_id)
return None
server = await get_server_squad_by_id(db, server_id)
if not server:
logger.warning("Сервер %s не найден при обновлении промогрупп", server_id)
return None
groups_result = await db.execute(
select(PromoGroup).where(PromoGroup.id.in_(unique_group_ids))
)
groups = groups_result.scalars().all()
if len(groups) != len(unique_group_ids):
logger.warning(
"Не все промогруппы найдены для сервера %s: %s",
server_id,
unique_group_ids,
)
return None
server.promo_groups = groups
await db.commit()
await db.refresh(server)
logger.info(
"✅ Обновлены промогруппы сервера %s: %s",
server.display_name,
", ".join(group.name for group in groups),
)
return server
async def update_server_squad(
db: AsyncSession,
server_id: int,

View File

@@ -15,6 +15,7 @@ from sqlalchemy import (
BigInteger,
UniqueConstraint,
Index,
Table,
)
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import relationship, Mapped, mapped_column
@@ -24,6 +25,22 @@ from sqlalchemy.sql import func
Base = declarative_base()
server_squad_promo_groups = Table(
"server_squad_promo_groups",
Base.metadata,
Column(
"server_squad_id",
ForeignKey("server_squads.id", ondelete="CASCADE"),
primary_key=True,
),
Column(
"promo_group_id",
ForeignKey("promo_groups.id", ondelete="CASCADE"),
primary_key=True,
),
)
class UserStatus(Enum):
ACTIVE = "active"
BLOCKED = "blocked"
@@ -278,6 +295,11 @@ class PromoGroup(Base):
updated_at = Column(DateTime, default=func.now(), onupdate=func.now())
users = relationship("User", back_populates="promo_group")
server_squads = relationship(
"ServerSquad",
secondary="server_squad_promo_groups",
back_populates="promo_groups",
)
def _get_period_discounts_map(self) -> Dict[int, int]:
raw_discounts = self.period_discounts or {}
@@ -832,10 +854,16 @@ class ServerSquad(Base):
sort_order = Column(Integer, default=0)
max_users = Column(Integer, nullable=True)
current_users = Column(Integer, default=0)
current_users = Column(Integer, default=0)
created_at = Column(DateTime, default=func.now())
updated_at = Column(DateTime, default=func.now(), onupdate=func.now())
promo_groups = relationship(
"PromoGroup",
secondary="server_squad_promo_groups",
back_populates="server_squads",
)
@property
def price_rubles(self) -> float:

View File

@@ -608,6 +608,96 @@ async def create_discount_offers_table():
logger.error(f"Ошибка создания таблицы discount_offers: {e}")
return False
async def create_server_squad_promo_groups_table():
table_exists = await check_table_exists('server_squad_promo_groups')
if table_exists:
logger.info("Таблица server_squad_promo_groups уже существует")
return True
try:
async with engine.begin() as conn:
db_type = await get_database_type()
if db_type == 'sqlite':
await conn.execute(text("""
CREATE TABLE server_squad_promo_groups (
server_squad_id INTEGER NOT NULL,
promo_group_id INTEGER NOT NULL,
PRIMARY KEY (server_squad_id, promo_group_id),
FOREIGN KEY(server_squad_id) REFERENCES server_squads(id) ON DELETE CASCADE,
FOREIGN KEY(promo_group_id) REFERENCES promo_groups(id) ON DELETE CASCADE
)
"""))
elif db_type == 'postgresql':
await conn.execute(text("""
CREATE TABLE IF NOT EXISTS server_squad_promo_groups (
server_squad_id INTEGER NOT NULL REFERENCES server_squads(id) ON DELETE CASCADE,
promo_group_id INTEGER NOT NULL REFERENCES promo_groups(id) ON DELETE CASCADE,
PRIMARY KEY (server_squad_id, promo_group_id)
)
"""))
elif db_type == 'mysql':
await conn.execute(text("""
CREATE TABLE IF NOT EXISTS server_squad_promo_groups (
server_squad_id INTEGER NOT NULL,
promo_group_id INTEGER NOT NULL,
PRIMARY KEY (server_squad_id, promo_group_id),
CONSTRAINT fk_sspg_server FOREIGN KEY(server_squad_id) REFERENCES server_squads(id) ON DELETE CASCADE,
CONSTRAINT fk_sspg_group FOREIGN KEY(promo_group_id) REFERENCES promo_groups(id) ON DELETE CASCADE
)
"""))
else:
raise ValueError(f"Unsupported database type: {db_type}")
logger.info("✅ Таблица server_squad_promo_groups успешно создана")
return True
except Exception as e:
logger.error(f"Ошибка создания таблицы server_squad_promo_groups: {e}")
return False
async def ensure_server_squads_have_default_promo_group():
try:
async with engine.begin() as conn:
db_type = await get_database_type()
default_group_sql = "SELECT id FROM promo_groups WHERE is_default IS TRUE LIMIT 1"
if db_type in {'sqlite', 'mysql'}:
default_group_sql = "SELECT id FROM promo_groups WHERE is_default = 1 LIMIT 1"
result = await conn.execute(text(default_group_sql))
row = result.fetchone()
if not row:
logger.warning("⚠️ Базовая промогруппа не найдена, пропускаем привязку серверов")
return False
default_group_id = row[0]
await conn.execute(
text("""
INSERT INTO server_squad_promo_groups (server_squad_id, promo_group_id)
SELECT ss.id, :group_id
FROM server_squads ss
LEFT JOIN server_squad_promo_groups spg
ON spg.server_squad_id = ss.id
WHERE spg.server_squad_id IS NULL
"""),
{"group_id": default_group_id},
)
logger.info("Все серверы без привязки получили базовую промогруппу")
return True
except Exception as e:
logger.error(f"Ошибка назначения базовой промогруппы серверам: {e}")
return False
async def create_user_messages_table():
table_exists = await check_table_exists('user_messages')
if table_exists:
@@ -1562,6 +1652,13 @@ async def run_universal_migration():
else:
logger.warning("⚠️ Проблемы с таблицей discount_offers")
logger.info("=== СОЗДАНИЕ СВЯЗИ SERVER_SQUAD_PROMO_GROUPS ===")
promo_link_created = await create_server_squad_promo_groups_table()
if promo_link_created:
logger.info("✅ Таблица server_squad_promo_groups готова")
else:
logger.warning("⚠️ Проблемы с таблицей server_squad_promo_groups")
logger.info("=== СОЗДАНИЕ ТАБЛИЦЫ USER_MESSAGES ===")
user_messages_created = await create_user_messages_table()
if user_messages_created:
@@ -1569,6 +1666,13 @@ async def run_universal_migration():
else:
logger.warning("⚠️ Проблемы с таблицей user_messages")
logger.info("=== НАЗНАЧЕНИЕ БАЗОВОЙ ПРОМОГРУППЫ СЕРВЕРАМ ===")
default_assignment_done = await ensure_server_squads_have_default_promo_group()
if default_assignment_done:
logger.info("✅ Базовая промогруппа назначена серверам без связей")
else:
logger.warning("⚠️ Не удалось назначить базовую промогруппу серверам")
logger.info("=== СОЗДАНИЕ/ОБНОВЛЕНИЕ ТАБЛИЦЫ WELCOME_TEXTS ===")
welcome_texts_created = await create_welcome_texts_table()
if welcome_texts_created:

View File

@@ -1,4 +1,6 @@
import logging
from typing import List, Set
from aiogram import Dispatcher, types, F
from aiogram.fsm.context import FSMContext
from sqlalchemy.ext.asyncio import AsyncSession
@@ -6,17 +8,61 @@ from sqlalchemy.ext.asyncio import AsyncSession
from app.states import AdminStates
from app.database.models import User
from app.database.crud.server_squad import (
get_all_server_squads, get_server_squad_by_id, update_server_squad,
delete_server_squad, sync_with_remnawave, get_server_statistics,
create_server_squad, get_available_server_squads
get_all_server_squads,
get_server_squad_by_id,
update_server_squad,
delete_server_squad,
sync_with_remnawave,
get_server_statistics,
create_server_squad,
get_available_server_squads,
set_server_squad_promo_groups,
)
from app.database.crud.promo_group import get_all_promo_groups
from app.services.remnawave_service import RemnaWaveService
from app.utils.decorators import admin_required, error_handler
from app.utils.cache import cache
from app.utils.cache import invalidate_available_countries_cache
logger = logging.getLogger(__name__)
def _format_promo_group_list(promo_groups: List, selected_ids: Set[int]) -> str:
names = [group.name for group in promo_groups if group.id in selected_ids]
return ", ".join(names) if names else "Не выбраны"
def _build_server_promo_groups_keyboard(
server_id: int,
promo_groups: List,
selected_ids: Set[int],
):
rows = []
for group in promo_groups:
emoji = "" if group.id in selected_ids else ""
rows.append([
types.InlineKeyboardButton(
text=f"{emoji} {group.name}",
callback_data=f"admin_server_group_toggle_{server_id}_{group.id}",
)
])
rows.append([
types.InlineKeyboardButton(
text="💾 Сохранить",
callback_data=f"admin_server_group_save_{server_id}",
)
])
rows.append([
types.InlineKeyboardButton(
text="⬅️ Назад",
callback_data=f"admin_server_edit_{server_id}",
)
])
return types.InlineKeyboardMarkup(inline_keyboard=rows)
@admin_required
@error_handler
async def show_servers_menu(
@@ -166,7 +212,7 @@ async def sync_servers_with_remnawave(
created, updated, disabled = await sync_with_remnawave(db, squads)
await cache.delete("available_countries")
await invalidate_available_countries_cache()
text = f"""
✅ <b>Синхронизация завершена</b>
@@ -223,6 +269,7 @@ async def show_server_edit_menu(
status_emoji = "✅ Доступен" if server.is_available else "❌ Недоступен"
price_text = f"{int(server.price_rubles)}" if server.price_kopeks > 0 else "Бесплатно"
promo_group_names = ", ".join(group.name for group in server.promo_groups) if server.promo_groups else "Не назначены"
text = f"""
🌐 <b>Редактирование сервера</b>
@@ -239,13 +286,14 @@ async def show_server_edit_menu(
• Код страны: {server.country_code or 'Не указан'}
• Лимит пользователей: {server.max_users or 'Без лимита'}
• Текущих пользователей: {server.current_users}
• Промогруппы: {promo_group_names}
<b>Описание:</b>
{server.description or 'Не указано'}
Выберите что изменить:
"""
keyboard = [
[
types.InlineKeyboardButton(text="✏️ Название", callback_data=f"admin_server_edit_name_{server.id}"),
@@ -258,6 +306,9 @@ async def show_server_edit_menu(
[
types.InlineKeyboardButton(text="📝 Описание", callback_data=f"admin_server_edit_desc_{server.id}")
],
[
types.InlineKeyboardButton(text="🎯 Промогруппы", callback_data=f"admin_server_edit_groups_{server.id}")
],
[
types.InlineKeyboardButton(
text="❌ Отключить" if server.is_available else "✅ Включить",
@@ -296,7 +347,7 @@ async def toggle_server_availability(
new_status = not server.is_available
await update_server_squad(db, server_id, is_available=new_status)
await cache.delete("available_countries")
await invalidate_available_countries_cache()
status_text = "включен" if new_status else "отключен"
await callback.answer(f"✅ Сервер {status_text}!")
@@ -321,6 +372,7 @@ async def toggle_server_availability(
• Код страны: {server.country_code or 'Не указан'}
• Лимит пользователей: {server.max_users or 'Без лимита'}
• Текущих пользователей: {server.current_users}
• Промогруппы: {promo_group_names}
<b>Описание:</b>
{server.description or 'Не указано'}
@@ -340,6 +392,9 @@ async def toggle_server_availability(
[
types.InlineKeyboardButton(text="📝 Описание", callback_data=f"admin_server_edit_desc_{server.id}")
],
[
types.InlineKeyboardButton(text="🎯 Промогруппы", callback_data=f"admin_server_edit_groups_{server.id}")
],
[
types.InlineKeyboardButton(
text="❌ Отключить" if server.is_available else "✅ Включить",
@@ -422,7 +477,7 @@ async def process_server_price_edit(
if server:
await state.clear()
await cache.delete("available_countries")
await invalidate_available_countries_cache()
price_text = f"{int(price_rubles)}" if price_kopeks > 0 else "Бесплатно"
await message.answer(
@@ -497,7 +552,7 @@ async def process_server_name_edit(
if server:
await state.clear()
await cache.delete("available_countries")
await invalidate_available_countries_cache()
await message.answer(
f"✅ Название сервера изменено на: <b>{new_name}</b>",
@@ -570,7 +625,7 @@ async def delete_server_execute(
success = await delete_server_squad(db, server_id)
if success:
await cache.delete("available_countries")
await invalidate_available_countries_cache()
await callback.message.edit_text(
f"✅ Сервер <b>{server.display_name}</b> успешно удален!",
@@ -701,7 +756,7 @@ async def process_server_country_edit(
if server:
await state.clear()
await cache.delete("available_countries")
await invalidate_available_countries_cache()
country_text = new_country or "Удален"
await message.answer(
@@ -862,6 +917,137 @@ async def process_server_description_edit(
else:
await message.answer("❌ Ошибка при обновлении сервера")
@admin_required
@error_handler
async def start_server_edit_promo_groups(
callback: types.CallbackQuery,
state: FSMContext,
db_user: User,
db: AsyncSession,
):
server_id = int(callback.data.split('_')[-1])
server = await get_server_squad_by_id(db, server_id)
if not server:
await callback.answer("❌ Сервер не найден!", show_alert=True)
return
promo_groups = await get_all_promo_groups(db)
if not promo_groups:
await callback.answer("⚠️ Нет доступных промогрупп", show_alert=True)
return
selected_ids: Set[int] = {group.id for group in server.promo_groups}
await state.set_data({
'server_id': server_id,
'server_name': server.display_name,
'selected_promo_groups': list(selected_ids),
})
await state.set_state(AdminStates.editing_server_promo_groups)
selected_text = _format_promo_group_list(promo_groups, selected_ids)
text = (
f"🎯 <b>Промогруппы сервера</b>\n\n"
f"Сервер: <b>{server.display_name}</b>\n"
f"Текущие промогруппы: <b>{selected_text}</b>\n\n"
"Выберите промогруппы, которым будет доступен сервер."
)
await callback.message.edit_text(
text,
reply_markup=_build_server_promo_groups_keyboard(server_id, promo_groups, selected_ids),
parse_mode="HTML",
)
await callback.answer()
@admin_required
@error_handler
async def toggle_server_promo_group(
callback: types.CallbackQuery,
state: FSMContext,
db_user: User,
db: AsyncSession,
):
parts = callback.data.split('_')
server_id = int(parts[-2])
group_id = int(parts[-1])
data = await state.get_data()
selected_ids: Set[int] = set(data.get('selected_promo_groups', []))
if group_id in selected_ids:
if len(selected_ids) == 1:
await callback.answer("⚠️ Должна быть выбрана хотя бы одна промогруппа", show_alert=True)
return
selected_ids.remove(group_id)
else:
selected_ids.add(group_id)
data['selected_promo_groups'] = list(selected_ids)
await state.set_data(data)
promo_groups = await get_all_promo_groups(db)
selected_text = _format_promo_group_list(promo_groups, selected_ids)
await callback.message.edit_text(
f"🎯 <b>Промогруппы сервера</b>\n\n"
f"Сервер: <b>{data.get('server_name', 'Неизвестно')}</b>\n"
f"Текущие промогруппы: <b>{selected_text}</b>\n\n"
"Выберите промогруппы, которым будет доступен сервер.",
reply_markup=_build_server_promo_groups_keyboard(server_id, promo_groups, selected_ids),
parse_mode="HTML",
)
await callback.answer()
@admin_required
@error_handler
async def save_server_promo_groups(
callback: types.CallbackQuery,
state: FSMContext,
db_user: User,
db: AsyncSession,
):
data = await state.get_data()
server_id_value = data.get('server_id')
if server_id_value is None:
await callback.answer("Не удалось определить сервер", show_alert=True)
return
server_id = int(server_id_value)
selected_ids: List[int] = data.get('selected_promo_groups', [])
if not selected_ids:
await callback.answer("⚠️ Выберите хотя бы одну промогруппу", show_alert=True)
return
server = await set_server_squad_promo_groups(db, server_id, selected_ids)
if not server:
await callback.answer("Не удалось сохранить промогруппы", show_alert=True)
return
await state.clear()
await invalidate_available_countries_cache()
promo_group_names = ", ".join(group.name for group in server.promo_groups) if server.promo_groups else "Не назначены"
await callback.message.edit_text(
f"✅ Промогруппы сервера обновлены:\n<b>{promo_group_names}</b>",
reply_markup=types.InlineKeyboardMarkup(inline_keyboard=[
[types.InlineKeyboardButton(text="🔙 К серверу", callback_data=f"admin_server_edit_{server_id}")]
]),
parse_mode="HTML",
)
await callback.answer()
@admin_required
@error_handler
async def sync_server_user_counts_handler(
@@ -940,13 +1126,16 @@ def register_handlers(dp: Dispatcher):
dp.callback_query.register(start_server_edit_name, F.data.startswith("admin_server_edit_name_"))
dp.callback_query.register(start_server_edit_price, F.data.startswith("admin_server_edit_price_"))
dp.callback_query.register(start_server_edit_country, F.data.startswith("admin_server_edit_country_"))
dp.callback_query.register(start_server_edit_limit, F.data.startswith("admin_server_edit_limit_"))
dp.callback_query.register(start_server_edit_description, F.data.startswith("admin_server_edit_desc_"))
dp.callback_query.register(start_server_edit_limit, F.data.startswith("admin_server_edit_limit_"))
dp.callback_query.register(start_server_edit_description, F.data.startswith("admin_server_edit_desc_"))
dp.callback_query.register(start_server_edit_promo_groups, F.data.startswith("admin_server_edit_groups_"))
dp.callback_query.register(toggle_server_promo_group, F.data.startswith("admin_server_group_toggle_"))
dp.callback_query.register(save_server_promo_groups, F.data.startswith("admin_server_group_save_"))
dp.message.register(process_server_name_edit, AdminStates.editing_server_name)
dp.message.register(process_server_price_edit, AdminStates.editing_server_price)
dp.message.register(process_server_country_edit, AdminStates.editing_server_country)
dp.message.register(process_server_limit_edit, AdminStates.editing_server_limit)
dp.message.register(process_server_country_edit, AdminStates.editing_server_country)
dp.message.register(process_server_limit_edit, AdminStates.editing_server_limit)
dp.message.register(process_server_description_edit, AdminStates.editing_server_description)
dp.callback_query.register(delete_server_confirm, F.data.startswith("admin_server_delete_") & ~F.data.contains("confirm"))

View File

@@ -99,7 +99,7 @@ async def _prepare_subscription_summary(
)
summary_data = dict(data)
countries = await _get_available_countries()
countries = await _get_available_countries(db_user.promo_group_id)
months_in_period = calculate_months_from_days(summary_data['period_days'])
period_display = format_period_description(summary_data['period_days'], db_user.language)
@@ -1003,7 +1003,7 @@ async def return_to_saved_cart(
from app.utils.pricing_utils import calculate_months_from_days, format_period_description
countries = await _get_available_countries()
countries = await _get_available_countries(db_user.promo_group_id)
selected_countries_names = []
months_in_period = calculate_months_from_days(data['period_days'])
@@ -1043,7 +1043,7 @@ async def handle_add_countries(
db: AsyncSession,
state: FSMContext
):
if not await _should_show_countries_management():
if not await _should_show_countries_management(db_user.promo_group_id):
await callback.answer(" Управление серверами недоступно - доступен только один сервер", show_alert=True)
return
@@ -1054,7 +1054,7 @@ async def handle_add_countries(
await callback.answer("⚠ Эта функция доступна только для платных подписок", show_alert=True)
return
countries = await _get_available_countries()
countries = await _get_available_countries(db_user.promo_group_id)
current_countries = subscription.connected_squads
current_countries_names = []
@@ -1138,21 +1138,26 @@ async def handle_manage_country(
return
data = await state.get_data()
countries = await _get_available_countries(db_user.promo_group_id)
available_ids = {country['uuid'] for country in countries}
if country_uuid not in available_ids:
await callback.answer("❌ Эта страна недоступна для вашей промогруппы", show_alert=True)
return
current_selected = data.get('countries', subscription.connected_squads.copy())
if country_uuid in current_selected:
current_selected.remove(country_uuid)
action = "removed"
else:
current_selected.append(country_uuid)
action = "added"
logger.info(f"🔍 Страна {country_uuid} {action}")
await state.update_data(countries=current_selected)
countries = await _get_available_countries()
try:
await callback.message.edit_reply_markup(
reply_markup=get_manage_countries_keyboard(
@@ -1203,7 +1208,7 @@ async def apply_countries_changes(
logger.info(f"🔧 Добавлено: {added}, Удалено: {removed}")
countries = await _get_available_countries()
countries = await _get_available_countries(db_user.promo_group_id)
months_to_pay = get_remaining_months(subscription.end_date)
@@ -2527,15 +2532,15 @@ async def select_period(
)
await state.set_state(SubscriptionStates.selecting_traffic)
else:
if await _should_show_countries_management():
countries = await _get_available_countries()
if await _should_show_countries_management(db_user.promo_group_id):
countries = await _get_available_countries(db_user.promo_group_id)
await callback.message.edit_text(
texts.SELECT_COUNTRIES,
reply_markup=get_countries_keyboard(countries, [], db_user.language)
)
await state.set_state(SubscriptionStates.selecting_countries)
else:
countries = await _get_available_countries()
countries = await _get_available_countries(db_user.promo_group_id)
available_countries = [c for c in countries if c.get('is_available', True)]
data['countries'] = [available_countries[0]['uuid']] if available_countries else []
await state.set_data(data)
@@ -2603,7 +2608,7 @@ async def get_traffic_packages_info() -> str:
async def get_subscription_info_text(subscription, texts, db_user, db: AsyncSession):
devices_used = await get_current_devices_count(db_user)
countries_info = await _get_countries_info(subscription.connected_squads)
countries_info = await _get_countries_info(subscription.connected_squads, db_user.promo_group_id)
countries_text = ", ".join([c['name'] for c in countries_info]) if countries_info else "Нет"
subscription_url = getattr(subscription, 'subscription_url', None) or "Генерируется..."
@@ -2683,15 +2688,15 @@ async def select_traffic(
await state.set_data(data)
if await _should_show_countries_management():
countries = await _get_available_countries()
if await _should_show_countries_management(db_user.promo_group_id):
countries = await _get_available_countries(db_user.promo_group_id)
await callback.message.edit_text(
texts.SELECT_COUNTRIES,
reply_markup=get_countries_keyboard(countries, [], db_user.language)
)
await state.set_state(SubscriptionStates.selecting_countries)
else:
countries = await _get_available_countries()
countries = await _get_available_countries(db_user.promo_group_id)
available_countries = [c for c in countries if c.get('is_available', True)]
data['countries'] = [available_countries[0]['uuid']] if available_countries else []
await state.set_data(data)
@@ -2717,13 +2722,19 @@ async def select_country(
data = await state.get_data()
selected_countries = data.get('countries', [])
countries = await _get_available_countries(db_user.promo_group_id)
available_ids = {country['uuid'] for country in countries}
if country_uuid not in available_ids:
await callback.answer("❌ Эта страна недоступна для вашей промогруппы", show_alert=True)
return
if country_uuid in selected_countries:
selected_countries.remove(country_uuid)
else:
selected_countries.append(country_uuid)
countries = await _get_available_countries()
period_base_price = PERIOD_PRICES[data['period_days']]
from app.utils.pricing_utils import apply_percentage_discount
@@ -2797,7 +2808,7 @@ async def select_devices(
settings.get_traffic_price(data['traffic_gb'])
)
countries = await _get_available_countries()
countries = await _get_available_countries(db_user.promo_group_id)
countries_price = sum(
c['price_kopeks'] for c in countries
if c['uuid'] in data['countries']
@@ -2866,7 +2877,7 @@ async def confirm_purchase(
else None
)
countries = await _get_available_countries()
countries = await _get_available_countries(db_user.promo_group_id)
months_in_period = data.get(
'months_in_period', calculate_months_from_days(data['period_days'])
@@ -3523,7 +3534,7 @@ async def handle_subscription_settings(
Выберите что хотите изменить:
"""
show_countries = await _should_show_countries_management()
show_countries = await _should_show_countries_management(db_user.promo_group_id)
await callback.message.edit_text(
settings_text,
@@ -3636,8 +3647,8 @@ async def handle_subscription_config_back(
await state.set_state(SubscriptionStates.selecting_period)
elif current_state == SubscriptionStates.selecting_devices.state:
if await _should_show_countries_management():
countries = await _get_available_countries()
if await _should_show_countries_management(db_user.promo_group_id):
countries = await _get_available_countries(db_user.promo_group_id)
data = await state.get_data()
selected_countries = data.get('countries', [])
@@ -3683,19 +3694,24 @@ async def handle_subscription_cancel(
await callback.answer("❌ Покупка отменена")
async def _get_available_countries():
from app.utils.cache import cache
async def _get_available_countries(promo_group_id: Optional[int] = None):
from app.utils.cache import cache, cache_key
from app.database.database import AsyncSessionLocal
from app.database.crud.server_squad import get_available_server_squads
cached_countries = await cache.get("available_countries")
cache_key_name = cache_key("available_countries", promo_group_id or "all")
cached_countries = await cache.get(cache_key_name)
if cached_countries:
return cached_countries
try:
async with AsyncSessionLocal() as db:
available_servers = await get_available_server_squads(db)
available_servers = await get_available_server_squads(
db,
promo_group_id=promo_group_id,
)
countries = []
for server in available_servers:
countries.append({
@@ -3734,20 +3750,20 @@ async def _get_available_countries():
"is_available": True
})
await cache.set("available_countries", countries, 300)
await cache.set(cache_key_name, countries, 300)
return countries
except Exception as e:
logger.error(f"Ошибка получения списка стран: {e}")
fallback_countries = [
{"uuid": "default-free", "name": "🆓 Бесплатный сервер", "price_kopeks": 0, "is_available": True},
]
await cache.set("available_countries", fallback_countries, 60)
await cache.set(cache_key_name, fallback_countries, 60)
return fallback_countries
async def _get_countries_info(squad_uuids):
countries = await _get_available_countries()
async def _get_countries_info(squad_uuids, promo_group_id: Optional[int] = None):
countries = await _get_available_countries(promo_group_id)
return [c for c in countries if c['uuid'] in squad_uuids]
async def handle_reset_devices(
@@ -3776,8 +3792,13 @@ async def handle_add_country_to_subscription(
logger.info(f"🔍 Данные состояния: {data}")
selected_countries = data.get('countries', [])
countries = await _get_available_countries()
countries = await _get_available_countries(db_user.promo_group_id)
available_ids = {country['uuid'] for country in countries}
if country_uuid not in available_ids:
await callback.answer("❌ Эта страна недоступна для вашей промогруппы", show_alert=True)
return
if country_uuid in selected_countries:
selected_countries.remove(country_uuid)
logger.info(f"🔍 Удалена страна: {country_uuid}")
@@ -3808,9 +3829,9 @@ async def handle_add_country_to_subscription(
await callback.answer()
async def _should_show_countries_management() -> bool:
async def _should_show_countries_management(promo_group_id: Optional[int] = None) -> bool:
try:
countries = await _get_available_countries()
countries = await _get_available_countries(promo_group_id)
available_countries = [c for c in countries if c.get('is_available', True)]
return len(available_countries) > 1
except Exception as e:
@@ -3839,7 +3860,7 @@ async def confirm_add_countries_to_subscription(
await callback.answer("⚠️ Изменения не обнаружены", show_alert=True)
return
countries = await _get_available_countries()
countries = await _get_available_countries(db_user.promo_group_id)
total_price = 0
new_countries_names = []
removed_countries_names = []

View File

@@ -95,6 +95,7 @@ class AdminStates(StatesGroup):
editing_server_country = State()
editing_server_limit = State()
editing_server_description = State()
editing_server_promo_groups = State()
creating_server_uuid = State()
creating_server_name = State()

View File

@@ -179,6 +179,12 @@ async def cached_function(key: str, expire: int = 300):
return decorator
async def invalidate_available_countries_cache() -> None:
keys = await cache.get_keys("available_countries*")
for key in keys:
await cache.delete(key)
class UserCache:
@staticmethod