mirror of
https://github.com/BEDOLAGA-DEV/remnawave-bedolaga-telegram-bot.git
synced 2026-01-20 03:40:26 +00:00
Merge pull request #2297 from BEDOLAGA-DEV/dev5
Сброс трафика на тарифах
This commit is contained in:
@@ -218,6 +218,8 @@ async def get_tariff(
|
||||
# Дневной тариф
|
||||
is_daily=tariff.is_daily,
|
||||
daily_price_kopeks=tariff.daily_price_kopeks,
|
||||
# Режим сброса трафика
|
||||
traffic_reset_mode=tariff.traffic_reset_mode,
|
||||
created_at=tariff.created_at,
|
||||
updated_at=tariff.updated_at,
|
||||
)
|
||||
@@ -268,6 +270,8 @@ async def create_new_tariff(
|
||||
# Дневной тариф
|
||||
is_daily=request.is_daily,
|
||||
daily_price_kopeks=request.daily_price_kopeks,
|
||||
# Режим сброса трафика
|
||||
traffic_reset_mode=request.traffic_reset_mode,
|
||||
)
|
||||
|
||||
logger.info(f"Admin {admin.id} created tariff {tariff.id}: {tariff.name}")
|
||||
@@ -354,6 +358,9 @@ async def update_existing_tariff(
|
||||
updates["is_daily"] = request.is_daily
|
||||
if request.daily_price_kopeks is not None:
|
||||
updates["daily_price_kopeks"] = request.daily_price_kopeks
|
||||
# Режим сброса трафика (None допускается как значение для сброса к глобальной настройке)
|
||||
if 'traffic_reset_mode' in request.model_fields_set:
|
||||
updates["traffic_reset_mode"] = request.traffic_reset_mode
|
||||
|
||||
if updates:
|
||||
await update_tariff(db, tariff, **updates)
|
||||
|
||||
@@ -103,6 +103,8 @@ class TariffDetailResponse(BaseModel):
|
||||
# Дневной тариф
|
||||
is_daily: bool = False
|
||||
daily_price_kopeks: int = 0
|
||||
# Режим сброса трафика
|
||||
traffic_reset_mode: Optional[str] = None # DAY, WEEK, MONTH, NO_RESET, None = глобальная настройка
|
||||
created_at: datetime
|
||||
updated_at: Optional[datetime] = None
|
||||
|
||||
@@ -141,6 +143,8 @@ class TariffCreateRequest(BaseModel):
|
||||
# Дневной тариф
|
||||
is_daily: bool = False
|
||||
daily_price_kopeks: int = Field(0, ge=0)
|
||||
# Режим сброса трафика
|
||||
traffic_reset_mode: Optional[str] = None # DAY, WEEK, MONTH, NO_RESET, None = глобальная настройка
|
||||
|
||||
|
||||
class TariffUpdateRequest(BaseModel):
|
||||
@@ -175,6 +179,8 @@ class TariffUpdateRequest(BaseModel):
|
||||
# Дневной тариф
|
||||
is_daily: Optional[bool] = None
|
||||
daily_price_kopeks: Optional[int] = Field(None, ge=0)
|
||||
# Режим сброса трафика
|
||||
traffic_reset_mode: Optional[str] = None # DAY, WEEK, MONTH, NO_RESET, None = глобальная настройка
|
||||
|
||||
|
||||
class TariffToggleResponse(BaseModel):
|
||||
|
||||
@@ -24,10 +24,13 @@ logger = logging.getLogger(__name__)
|
||||
async def get_subscription_by_user_id(db: AsyncSession, user_id: int) -> Optional[Subscription]:
|
||||
result = await db.execute(
|
||||
select(Subscription)
|
||||
.options(selectinload(Subscription.user))
|
||||
.options(
|
||||
selectinload(Subscription.user),
|
||||
selectinload(Subscription.tariff),
|
||||
)
|
||||
.where(Subscription.user_id == user_id)
|
||||
.order_by(Subscription.created_at.desc())
|
||||
.limit(1)
|
||||
.limit(1)
|
||||
)
|
||||
subscription = result.scalar_one_or_none()
|
||||
|
||||
|
||||
@@ -186,6 +186,8 @@ async def create_tariff(
|
||||
traffic_price_per_gb_kopeks: int = 0,
|
||||
min_traffic_gb: int = 1,
|
||||
max_traffic_gb: int = 1000,
|
||||
# Режим сброса трафика
|
||||
traffic_reset_mode: Optional[str] = None, # DAY, WEEK, MONTH, NO_RESET, None = глобальная настройка
|
||||
) -> Tariff:
|
||||
"""Создает новый тариф."""
|
||||
normalized_prices = _normalize_period_prices(period_prices)
|
||||
@@ -220,6 +222,8 @@ async def create_tariff(
|
||||
traffic_price_per_gb_kopeks=max(0, traffic_price_per_gb_kopeks),
|
||||
min_traffic_gb=max(1, min_traffic_gb),
|
||||
max_traffic_gb=max(1, max_traffic_gb),
|
||||
# Режим сброса трафика
|
||||
traffic_reset_mode=traffic_reset_mode,
|
||||
)
|
||||
|
||||
db.add(tariff)
|
||||
@@ -283,6 +287,8 @@ async def update_tariff(
|
||||
traffic_price_per_gb_kopeks: Optional[int] = None,
|
||||
min_traffic_gb: Optional[int] = None,
|
||||
max_traffic_gb: Optional[int] = None,
|
||||
# Режим сброса трафика
|
||||
traffic_reset_mode: Optional[str] = ..., # ... = не передан, None = сбросить к глобальной настройке
|
||||
) -> Tariff:
|
||||
"""Обновляет существующий тариф."""
|
||||
if name is not None:
|
||||
@@ -343,6 +349,9 @@ async def update_tariff(
|
||||
tariff.min_traffic_gb = max(1, min_traffic_gb)
|
||||
if max_traffic_gb is not None:
|
||||
tariff.max_traffic_gb = max(1, max_traffic_gb)
|
||||
# Режим сброса трафика
|
||||
if traffic_reset_mode is not ...:
|
||||
tariff.traffic_reset_mode = traffic_reset_mode
|
||||
|
||||
# Обновляем промогруппы если указаны
|
||||
if promo_group_ids is not None:
|
||||
|
||||
@@ -807,6 +807,9 @@ class Tariff(Base):
|
||||
min_traffic_gb = Column(Integer, default=1, nullable=False) # Минимальный трафик в ГБ
|
||||
max_traffic_gb = Column(Integer, default=1000, nullable=False) # Максимальный трафик в ГБ
|
||||
|
||||
# Режим сброса трафика: DAY, WEEK, MONTH, NO_RESET (по умолчанию берётся из конфига)
|
||||
traffic_reset_mode = Column(String(20), nullable=True, default=None) # None = использовать глобальную настройку
|
||||
|
||||
created_at = Column(DateTime, default=func.now())
|
||||
updated_at = Column(DateTime, default=func.now(), onupdate=func.now())
|
||||
|
||||
|
||||
@@ -5988,6 +5988,28 @@ async def add_tariff_custom_days_traffic_columns() -> bool:
|
||||
return False
|
||||
|
||||
|
||||
async def add_tariff_traffic_reset_mode_column() -> bool:
|
||||
"""Добавляет колонку traffic_reset_mode в tariffs для настройки режима сброса трафика.
|
||||
|
||||
Значения: DAY, WEEK, MONTH, NO_RESET (NULL = использовать глобальную настройку)
|
||||
"""
|
||||
try:
|
||||
if not await check_column_exists('tariffs', 'traffic_reset_mode'):
|
||||
async with engine.begin() as conn:
|
||||
await conn.execute(text(
|
||||
"ALTER TABLE tariffs ADD COLUMN traffic_reset_mode VARCHAR(20) NULL"
|
||||
))
|
||||
logger.info("✅ Колонка traffic_reset_mode добавлена в tariffs")
|
||||
return True
|
||||
else:
|
||||
logger.info("ℹ️ Колонка traffic_reset_mode уже существует в tariffs")
|
||||
return True
|
||||
|
||||
except Exception as error:
|
||||
logger.error(f"❌ Ошибка добавления колонки traffic_reset_mode: {error}")
|
||||
return False
|
||||
|
||||
|
||||
async def add_subscription_daily_columns() -> bool:
|
||||
"""Добавляет колонки для суточных подписок."""
|
||||
try:
|
||||
@@ -6624,6 +6646,13 @@ async def run_universal_migration():
|
||||
else:
|
||||
logger.warning("⚠️ Проблемы с колонками произвольных дней/трафика в tariffs")
|
||||
|
||||
logger.info("=== ДОБАВЛЕНИЕ КОЛОНКИ РЕЖИМА СБРОСА ТРАФИКА В ТАРИФАХ ===")
|
||||
traffic_reset_mode_ready = await add_tariff_traffic_reset_mode_column()
|
||||
if traffic_reset_mode_ready:
|
||||
logger.info("✅ Колонка traffic_reset_mode в tariffs готова")
|
||||
else:
|
||||
logger.warning("⚠️ Проблемы с колонкой traffic_reset_mode в tariffs")
|
||||
|
||||
logger.info("=== ДОБАВЛЕНИЕ КОЛОНОК СУТОЧНЫХ ПОДПИСОК ===")
|
||||
daily_subscription_columns_ready = await add_subscription_daily_columns()
|
||||
if daily_subscription_columns_ready:
|
||||
|
||||
@@ -204,6 +204,9 @@ def get_tariff_view_keyboard(
|
||||
buttons.append([
|
||||
InlineKeyboardButton(text="📈 Докупка трафика", callback_data=f"admin_tariff_edit_traffic_topup:{tariff.id}"),
|
||||
])
|
||||
buttons.append([
|
||||
InlineKeyboardButton(text="🔄 Сброс трафика", callback_data=f"admin_tariff_edit_reset_mode:{tariff.id}"),
|
||||
])
|
||||
buttons.append([
|
||||
InlineKeyboardButton(text="🌐 Серверы", callback_data=f"admin_tariff_edit_squads:{tariff.id}"),
|
||||
InlineKeyboardButton(text="👥 Промогруппы", callback_data=f"admin_tariff_edit_promo:{tariff.id}"),
|
||||
@@ -250,6 +253,19 @@ def get_tariff_view_keyboard(
|
||||
return InlineKeyboardMarkup(inline_keyboard=buttons)
|
||||
|
||||
|
||||
def _format_traffic_reset_mode(mode: Optional[str]) -> str:
|
||||
"""Форматирует режим сброса трафика для отображения."""
|
||||
mode_labels = {
|
||||
'DAY': '📅 Ежедневно',
|
||||
'WEEK': '📆 Еженедельно',
|
||||
'MONTH': '🗓️ Ежемесячно',
|
||||
'NO_RESET': '🚫 Никогда',
|
||||
}
|
||||
if mode is None:
|
||||
return f"🌐 Глобальная настройка ({settings.DEFAULT_TRAFFIC_RESET_STRATEGY})"
|
||||
return mode_labels.get(mode, f"⚠️ Неизвестно ({mode})")
|
||||
|
||||
|
||||
def _format_traffic_topup_packages(tariff: Tariff) -> str:
|
||||
"""Форматирует пакеты докупки трафика для отображения."""
|
||||
if not getattr(tariff, 'traffic_topup_enabled', False):
|
||||
@@ -312,6 +328,10 @@ def format_tariff_info(tariff: Tariff, language: str, subs_count: int = 0) -> st
|
||||
# Форматируем докупку трафика
|
||||
traffic_topup_display = _format_traffic_topup_packages(tariff)
|
||||
|
||||
# Форматируем режим сброса трафика
|
||||
traffic_reset_mode = getattr(tariff, 'traffic_reset_mode', None)
|
||||
traffic_reset_display = _format_traffic_reset_mode(traffic_reset_mode)
|
||||
|
||||
# Форматируем суточный тариф
|
||||
is_daily = getattr(tariff, 'is_daily', False)
|
||||
daily_price_kopeks = getattr(tariff, 'daily_price_kopeks', 0)
|
||||
@@ -341,6 +361,8 @@ def format_tariff_info(tariff: Tariff, language: str, subs_count: int = 0) -> st
|
||||
<b>Докупка трафика:</b>
|
||||
{traffic_topup_display}
|
||||
|
||||
<b>Сброс трафика:</b> {traffic_reset_display}
|
||||
|
||||
{price_block}
|
||||
|
||||
<b>Серверы:</b> {squads_display}
|
||||
@@ -2595,6 +2617,124 @@ async def clear_tariff_promo_groups(
|
||||
pass
|
||||
|
||||
|
||||
# ==================== Режим сброса трафика ====================
|
||||
|
||||
TRAFFIC_RESET_MODES = [
|
||||
('DAY', '📅 Ежедневно', 'Трафик сбрасывается каждый день'),
|
||||
('WEEK', '📆 Еженедельно', 'Трафик сбрасывается каждую неделю'),
|
||||
('MONTH', '🗓️ Ежемесячно', 'Трафик сбрасывается каждый месяц'),
|
||||
('NO_RESET', '🚫 Никогда', 'Трафик не сбрасывается автоматически'),
|
||||
]
|
||||
|
||||
|
||||
def get_traffic_reset_mode_keyboard(tariff_id: int, current_mode: Optional[str], language: str) -> InlineKeyboardMarkup:
|
||||
"""Создает клавиатуру для выбора режима сброса трафика."""
|
||||
texts = get_texts(language)
|
||||
buttons = []
|
||||
|
||||
# Кнопка "Глобальная настройка"
|
||||
global_label = f"{'✅ ' if current_mode is None else ''}🌐 Глобальная настройка ({settings.DEFAULT_TRAFFIC_RESET_STRATEGY})"
|
||||
buttons.append([
|
||||
InlineKeyboardButton(
|
||||
text=global_label,
|
||||
callback_data=f"admin_tariff_set_reset_mode:{tariff_id}:GLOBAL"
|
||||
)
|
||||
])
|
||||
|
||||
# Кнопки для каждого режима
|
||||
for mode_value, mode_label, mode_desc in TRAFFIC_RESET_MODES:
|
||||
is_selected = current_mode == mode_value
|
||||
label = f"{'✅ ' if is_selected else ''}{mode_label}"
|
||||
buttons.append([
|
||||
InlineKeyboardButton(
|
||||
text=label,
|
||||
callback_data=f"admin_tariff_set_reset_mode:{tariff_id}:{mode_value}"
|
||||
)
|
||||
])
|
||||
|
||||
# Кнопка назад
|
||||
buttons.append([
|
||||
InlineKeyboardButton(text=texts.BACK, callback_data=f"admin_tariff_view:{tariff_id}")
|
||||
])
|
||||
|
||||
return InlineKeyboardMarkup(inline_keyboard=buttons)
|
||||
|
||||
|
||||
@admin_required
|
||||
@error_handler
|
||||
async def start_edit_traffic_reset_mode(
|
||||
callback: types.CallbackQuery,
|
||||
db_user: User,
|
||||
db: AsyncSession,
|
||||
):
|
||||
"""Начинает редактирование режима сброса трафика."""
|
||||
tariff_id = int(callback.data.split(":")[1])
|
||||
tariff = await get_tariff_by_id(db, tariff_id)
|
||||
|
||||
if not tariff:
|
||||
await callback.answer("Тариф не найден", show_alert=True)
|
||||
return
|
||||
|
||||
current_mode = getattr(tariff, 'traffic_reset_mode', None)
|
||||
|
||||
await callback.message.edit_text(
|
||||
f"🔄 <b>Режим сброса трафика для тарифа «{tariff.name}»</b>\n\n"
|
||||
f"Текущий режим: {_format_traffic_reset_mode(current_mode)}\n\n"
|
||||
"Выберите, когда сбрасывать использованный трафик у подписчиков этого тарифа:\n\n"
|
||||
"• <b>Глобальная настройка</b> — использовать значение из конфига бота\n"
|
||||
"• <b>Ежедневно</b> — сброс каждый день\n"
|
||||
"• <b>Еженедельно</b> — сброс каждую неделю\n"
|
||||
"• <b>Ежемесячно</b> — сброс каждый месяц\n"
|
||||
"• <b>Никогда</b> — трафик накапливается за весь период подписки",
|
||||
reply_markup=get_traffic_reset_mode_keyboard(tariff_id, current_mode, db_user.language),
|
||||
parse_mode="HTML"
|
||||
)
|
||||
await callback.answer()
|
||||
|
||||
|
||||
@admin_required
|
||||
@error_handler
|
||||
async def set_traffic_reset_mode(
|
||||
callback: types.CallbackQuery,
|
||||
db_user: User,
|
||||
db: AsyncSession,
|
||||
):
|
||||
"""Устанавливает режим сброса трафика для тарифа."""
|
||||
parts = callback.data.split(":")
|
||||
tariff_id = int(parts[1])
|
||||
new_mode = parts[2]
|
||||
|
||||
tariff = await get_tariff_by_id(db, tariff_id)
|
||||
|
||||
if not tariff:
|
||||
await callback.answer("Тариф не найден", show_alert=True)
|
||||
return
|
||||
|
||||
# Преобразуем GLOBAL в None
|
||||
if new_mode == "GLOBAL":
|
||||
new_mode = None
|
||||
|
||||
# Обновляем тариф
|
||||
tariff = await update_tariff(db, tariff, traffic_reset_mode=new_mode)
|
||||
|
||||
mode_display = _format_traffic_reset_mode(new_mode)
|
||||
await callback.answer(f"Режим сброса изменён: {mode_display}", show_alert=True)
|
||||
|
||||
# Обновляем клавиатуру
|
||||
await callback.message.edit_text(
|
||||
f"🔄 <b>Режим сброса трафика для тарифа «{tariff.name}»</b>\n\n"
|
||||
f"Текущий режим: {mode_display}\n\n"
|
||||
"Выберите, когда сбрасывать использованный трафик у подписчиков этого тарифа:\n\n"
|
||||
"• <b>Глобальная настройка</b> — использовать значение из конфига бота\n"
|
||||
"• <b>Ежедневно</b> — сброс каждый день\n"
|
||||
"• <b>Еженедельно</b> — сброс каждую неделю\n"
|
||||
"• <b>Ежемесячно</b> — сброс каждый месяц\n"
|
||||
"• <b>Никогда</b> — трафик накапливается за весь период подписки",
|
||||
reply_markup=get_traffic_reset_mode_keyboard(tariff_id, new_mode, db_user.language),
|
||||
parse_mode="HTML"
|
||||
)
|
||||
|
||||
|
||||
def register_handlers(dp: Dispatcher):
|
||||
"""Регистрирует обработчики для управления тарифами."""
|
||||
# Список тарифов
|
||||
@@ -2681,3 +2821,7 @@ def register_handlers(dp: Dispatcher):
|
||||
dp.callback_query.register(toggle_daily_tariff, F.data.startswith("admin_tariff_toggle_daily:"))
|
||||
dp.callback_query.register(start_edit_daily_price, F.data.startswith("admin_tariff_edit_daily_price:"))
|
||||
dp.message.register(process_daily_price_input, AdminStates.editing_tariff_daily_price)
|
||||
|
||||
# Режим сброса трафика
|
||||
dp.callback_query.register(start_edit_traffic_reset_mode, F.data.startswith("admin_tariff_edit_reset_mode:"))
|
||||
dp.callback_query.register(set_traffic_reset_mode, F.data.startswith("admin_tariff_set_reset_mode:"))
|
||||
|
||||
@@ -531,7 +531,10 @@ class MonitoringService:
|
||||
)
|
||||
result = await db.execute(
|
||||
select(Subscription)
|
||||
.options(selectinload(Subscription.user))
|
||||
.options(
|
||||
selectinload(Subscription.user),
|
||||
selectinload(Subscription.tariff),
|
||||
)
|
||||
.where(
|
||||
and_(
|
||||
Subscription.is_trial.is_(True),
|
||||
|
||||
@@ -62,17 +62,35 @@ def _resolve_addon_discount_percent(
|
||||
period_days=period_days,
|
||||
)
|
||||
|
||||
def get_traffic_reset_strategy():
|
||||
def get_traffic_reset_strategy(tariff=None):
|
||||
"""Получает стратегию сброса трафика.
|
||||
|
||||
Args:
|
||||
tariff: Объект тарифа. Если у тарифа задан traffic_reset_mode,
|
||||
используется он, иначе глобальная настройка из конфига.
|
||||
|
||||
Returns:
|
||||
TrafficLimitStrategy: Стратегия сброса трафика для RemnaWave API.
|
||||
"""
|
||||
from app.config import settings
|
||||
strategy = settings.DEFAULT_TRAFFIC_RESET_STRATEGY.upper()
|
||||
|
||||
|
||||
strategy_mapping = {
|
||||
'NO_RESET': 'NO_RESET',
|
||||
'DAY': 'DAY',
|
||||
'DAY': 'DAY',
|
||||
'WEEK': 'WEEK',
|
||||
'MONTH': 'MONTH'
|
||||
}
|
||||
|
||||
|
||||
# Проверяем настройку тарифа
|
||||
if tariff is not None:
|
||||
tariff_mode = getattr(tariff, 'traffic_reset_mode', None)
|
||||
if tariff_mode is not None:
|
||||
mapped_strategy = strategy_mapping.get(tariff_mode.upper(), 'NO_RESET')
|
||||
logger.info(f"🔄 Стратегия сброса трафика из тарифа '{getattr(tariff, 'name', 'N/A')}': {tariff_mode} -> {mapped_strategy}")
|
||||
return getattr(TrafficLimitStrategy, mapped_strategy)
|
||||
|
||||
# Используем глобальную настройку
|
||||
strategy = settings.DEFAULT_TRAFFIC_RESET_STRATEGY.upper()
|
||||
mapped_strategy = strategy_mapping.get(strategy, 'NO_RESET')
|
||||
logger.info(f"🔄 Стратегия сброса трафика из конфига: {strategy} -> {mapped_strategy}")
|
||||
return getattr(TrafficLimitStrategy, mapped_strategy)
|
||||
@@ -205,7 +223,7 @@ class SubscriptionService:
|
||||
status=UserStatus.ACTIVE,
|
||||
expire_at=subscription.end_date,
|
||||
traffic_limit_bytes=self._gb_to_bytes(subscription.traffic_limit_gb),
|
||||
traffic_limit_strategy=get_traffic_reset_strategy(),
|
||||
traffic_limit_strategy=get_traffic_reset_strategy(subscription.tariff),
|
||||
description=settings.format_remnawave_user_description(
|
||||
full_name=user.full_name,
|
||||
username=user.username,
|
||||
@@ -242,7 +260,7 @@ class SubscriptionService:
|
||||
expire_at=subscription.end_date,
|
||||
status=UserStatus.ACTIVE,
|
||||
traffic_limit_bytes=self._gb_to_bytes(subscription.traffic_limit_gb),
|
||||
traffic_limit_strategy=get_traffic_reset_strategy(),
|
||||
traffic_limit_strategy=get_traffic_reset_strategy(subscription.tariff),
|
||||
telegram_id=user.telegram_id,
|
||||
description=settings.format_remnawave_user_description(
|
||||
full_name=user.full_name,
|
||||
@@ -326,7 +344,7 @@ class SubscriptionService:
|
||||
status=UserStatus.ACTIVE if is_actually_active else UserStatus.EXPIRED,
|
||||
expire_at=subscription.end_date,
|
||||
traffic_limit_bytes=self._gb_to_bytes(subscription.traffic_limit_gb),
|
||||
traffic_limit_strategy=get_traffic_reset_strategy(),
|
||||
traffic_limit_strategy=get_traffic_reset_strategy(subscription.tariff),
|
||||
description=settings.format_remnawave_user_description(
|
||||
full_name=user.full_name,
|
||||
username=user.username,
|
||||
|
||||
Reference in New Issue
Block a user