mirror of
https://github.com/BEDOLAGA-DEV/remnawave-bedolaga-telegram-bot.git
synced 2026-03-02 00:03:05 +00:00
Merge pull request #86 from Fr1ngg/dev
Правка бекапов/восстановления, удаления юзеров, исправление вывода курса за звезды+динамическая инфа в способах оплаты
This commit is contained in:
6
app/external/telegram_stars.py
vendored
6
app/external/telegram_stars.py
vendored
@@ -33,6 +33,7 @@ class TelegramStarsService:
|
||||
try:
|
||||
amount_rubles = amount_kopeks / 100
|
||||
stars_amount = self.calculate_stars_from_rubles(amount_rubles)
|
||||
stars_rate = settings.get_stars_rate()
|
||||
|
||||
invoice_link = await self.bot.create_invoice_link(
|
||||
title=title,
|
||||
@@ -46,7 +47,7 @@ class TelegramStarsService:
|
||||
|
||||
logger.info(
|
||||
f"Создан Stars invoice на {stars_amount} звезд (~{int(amount_rubles)}₽) "
|
||||
f"для {chat_id}, курс: {int(settings.get_stars_rate())}₽/⭐"
|
||||
f"для {chat_id}, курс: {stars_rate}₽/⭐"
|
||||
)
|
||||
return invoice_link
|
||||
|
||||
@@ -66,6 +67,7 @@ class TelegramStarsService:
|
||||
try:
|
||||
amount_rubles = amount_kopeks / 100
|
||||
stars_amount = self.calculate_stars_from_rubles(amount_rubles)
|
||||
stars_rate = settings.get_stars_rate()
|
||||
|
||||
message = await self.bot.send_invoice(
|
||||
chat_id=chat_id,
|
||||
@@ -80,7 +82,7 @@ class TelegramStarsService:
|
||||
|
||||
logger.info(
|
||||
f"Отправлен Stars invoice {message.message_id} на {stars_amount} звезд "
|
||||
f"(~{int(amount_rubles)}₽), курс: {int(settings.get_stars_rate())}₽/⭐"
|
||||
f"(~{int(amount_rubles)}₽), курс: {stars_rate}₽/⭐"
|
||||
)
|
||||
return {
|
||||
"message_id": message.message_id,
|
||||
|
||||
@@ -149,19 +149,10 @@ async def show_payment_methods(
|
||||
db_user: User,
|
||||
state: FSMContext
|
||||
):
|
||||
texts = get_texts(db_user.language)
|
||||
from app.utils.payment_utils import get_payment_methods_text
|
||||
|
||||
payment_text = """
|
||||
💳 <b>Способы пополнения баланса</b>
|
||||
|
||||
Выберите удобный для вас способ оплаты:
|
||||
|
||||
⭐ <b>Telegram Stars</b> - быстро и удобно
|
||||
💳 <b>Банковская карта</b> - через YooKassa/Tribute
|
||||
🛠️ <b>Через поддержку</b> - другие способы
|
||||
|
||||
Выберите способ пополнения:
|
||||
"""
|
||||
texts = get_texts(db_user.language)
|
||||
payment_text = get_payment_methods_text()
|
||||
|
||||
await callback.message.edit_text(
|
||||
payment_text,
|
||||
@@ -171,6 +162,20 @@ async def show_payment_methods(
|
||||
await callback.answer()
|
||||
|
||||
|
||||
@error_handler
|
||||
async def handle_payment_methods_unavailable(
|
||||
callback: types.CallbackQuery,
|
||||
db_user: User
|
||||
):
|
||||
texts = get_texts(db_user.language)
|
||||
|
||||
await callback.answer(
|
||||
"⚠️ В данный момент автоматические способы оплаты временно недоступны. "
|
||||
"Для пополнения баланса обратитесь в техподдержку.",
|
||||
show_alert=True
|
||||
)
|
||||
|
||||
|
||||
@error_handler
|
||||
async def start_stars_payment(
|
||||
callback: types.CallbackQuery,
|
||||
@@ -360,7 +365,7 @@ async def process_stars_payment_amount(
|
||||
texts = get_texts(db_user.language)
|
||||
|
||||
if not settings.TELEGRAM_STARS_ENABLED:
|
||||
await message.answer("⚠ Оплата Stars временно недоступна")
|
||||
await message.answer("⚠️ Оплата Stars временно недоступна")
|
||||
return
|
||||
|
||||
try:
|
||||
@@ -368,6 +373,7 @@ async def process_stars_payment_amount(
|
||||
|
||||
amount_rubles = amount_kopeks / 100
|
||||
stars_amount = TelegramStarsService.calculate_stars_from_rubles(amount_rubles)
|
||||
stars_rate = settings.get_stars_rate()
|
||||
|
||||
payment_service = PaymentService(message.bot)
|
||||
invoice_link = await payment_service.create_stars_invoice(
|
||||
@@ -385,7 +391,7 @@ async def process_stars_payment_amount(
|
||||
f"⭐ <b>Оплата через Telegram Stars</b>\n\n"
|
||||
f"💰 Сумма: {texts.format_price(amount_kopeks)}\n"
|
||||
f"⭐ К оплате: {stars_amount} звезд\n"
|
||||
f"📊 Курс: {int(settings.get_stars_rate())}₽ за звезду\n\n"
|
||||
f"📊 Курс: {stars_rate}₽ за звезду\n\n"
|
||||
f"Нажмите кнопку ниже для оплаты:",
|
||||
reply_markup=keyboard,
|
||||
parse_mode="HTML"
|
||||
@@ -395,7 +401,8 @@ async def process_stars_payment_amount(
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка создания Stars invoice: {e}")
|
||||
await message.answer("⚠ Ошибка создания платежа")
|
||||
await message.answer("⚠️ Ошибка создания платежа")
|
||||
|
||||
|
||||
|
||||
@error_handler
|
||||
@@ -790,3 +797,8 @@ def register_handlers(dp: Dispatcher):
|
||||
check_cryptobot_payment_status,
|
||||
F.data.startswith("check_cryptobot_")
|
||||
)
|
||||
|
||||
dp.callback_query.register(
|
||||
handle_payment_methods_unavailable,
|
||||
F.data == "payment_methods_unavailable"
|
||||
)
|
||||
|
||||
@@ -458,7 +458,16 @@ def get_balance_keyboard(language: str = "ru") -> InlineKeyboardMarkup:
|
||||
def get_payment_methods_keyboard(amount_kopeks: int, language: str = "ru") -> InlineKeyboardMarkup:
|
||||
texts = get_texts(language)
|
||||
keyboard = []
|
||||
|
||||
|
||||
|
||||
if settings.TELEGRAM_STARS_ENABLED:
|
||||
keyboard.append([
|
||||
InlineKeyboardButton(
|
||||
text="⭐ Telegram Stars",
|
||||
callback_data="topup_stars"
|
||||
)
|
||||
])
|
||||
|
||||
if settings.is_yookassa_enabled():
|
||||
keyboard.append([
|
||||
InlineKeyboardButton(
|
||||
@@ -483,14 +492,6 @@ def get_payment_methods_keyboard(amount_kopeks: int, language: str = "ru") -> In
|
||||
)
|
||||
])
|
||||
|
||||
if settings.TELEGRAM_STARS_ENABLED:
|
||||
keyboard.append([
|
||||
InlineKeyboardButton(
|
||||
text="⭐ Telegram Stars",
|
||||
callback_data="topup_stars"
|
||||
)
|
||||
])
|
||||
|
||||
keyboard.append([
|
||||
InlineKeyboardButton(
|
||||
text="🛠️ Через поддержку",
|
||||
@@ -498,6 +499,14 @@ def get_payment_methods_keyboard(amount_kopeks: int, language: str = "ru") -> In
|
||||
)
|
||||
])
|
||||
|
||||
if len(keyboard) == 1:
|
||||
keyboard.insert(0, [
|
||||
InlineKeyboardButton(
|
||||
text="⚠️ Способы оплаты временно недоступны",
|
||||
callback_data="payment_methods_unavailable"
|
||||
)
|
||||
])
|
||||
|
||||
keyboard.append([
|
||||
InlineKeyboardButton(text=texts.BACK, callback_data="menu_balance")
|
||||
])
|
||||
|
||||
@@ -21,7 +21,7 @@ from app.database.models import (
|
||||
ReferralEarning, Squad, ServiceRule, SystemSetting, MonitoringLog,
|
||||
SubscriptionConversion, SentNotification, BroadcastHistory,
|
||||
ServerSquad, SubscriptionServer, UserMessage, YooKassaPayment,
|
||||
CryptoBotPayment, Base
|
||||
CryptoBotPayment, WelcomeText, Base
|
||||
)
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
@@ -30,7 +30,7 @@ logger = logging.getLogger(__name__)
|
||||
@dataclass
|
||||
class BackupMetadata:
|
||||
timestamp: str
|
||||
version: str = "1.0"
|
||||
version: str = "1.1"
|
||||
database_type: str = "postgresql"
|
||||
backup_type: str = "full"
|
||||
tables_count: int = 0
|
||||
@@ -60,16 +60,32 @@ class BackupService:
|
||||
self._auto_backup_task = None
|
||||
self._settings = self._load_settings()
|
||||
|
||||
self.backup_models = [
|
||||
User, Subscription, Transaction, PromoCode, PromoCodeUse,
|
||||
ReferralEarning, ServiceRule, SystemSetting,
|
||||
SubscriptionConversion, SentNotification, BroadcastHistory,
|
||||
ServerSquad, SubscriptionServer, UserMessage,
|
||||
YooKassaPayment, CryptoBotPayment
|
||||
self.backup_models_ordered = [
|
||||
ServiceRule,
|
||||
SystemSetting,
|
||||
Squad,
|
||||
PromoCode,
|
||||
ServerSquad,
|
||||
|
||||
User,
|
||||
|
||||
WelcomeText,
|
||||
Subscription,
|
||||
Transaction,
|
||||
YooKassaPayment,
|
||||
CryptoBotPayment,
|
||||
PromoCodeUse,
|
||||
ReferralEarning,
|
||||
SubscriptionConversion,
|
||||
BroadcastHistory,
|
||||
UserMessage,
|
||||
|
||||
SentNotification,
|
||||
SubscriptionServer,
|
||||
]
|
||||
|
||||
if self._settings.include_logs:
|
||||
self.backup_models.append(MonitoringLog)
|
||||
self.backup_models_ordered.append(MonitoringLog)
|
||||
|
||||
def _load_settings(self) -> BackupSettings:
|
||||
return BackupSettings(
|
||||
@@ -83,7 +99,6 @@ class BackupService:
|
||||
)
|
||||
|
||||
def _parse_backup_time(self) -> Tuple[int, int]:
|
||||
"""Возвращает часы и минуты для запланированного времени бекапа."""
|
||||
time_str = (self._settings.backup_time or "").strip()
|
||||
|
||||
try:
|
||||
@@ -137,12 +152,12 @@ class BackupService:
|
||||
include_logs: bool = None
|
||||
) -> Tuple[bool, str, Optional[str]]:
|
||||
try:
|
||||
logger.info("🔄 Начинаем создание бекапа...")
|
||||
logger.info("📄 Начинаем создание бекапа...")
|
||||
|
||||
if include_logs is None:
|
||||
include_logs = self._settings.include_logs
|
||||
|
||||
models_to_backup = self.backup_models.copy()
|
||||
models_to_backup = self.backup_models_ordered.copy()
|
||||
if not include_logs and MonitoringLog in models_to_backup:
|
||||
models_to_backup.remove(MonitoringLog)
|
||||
elif include_logs and MonitoringLog not in models_to_backup:
|
||||
@@ -157,7 +172,16 @@ class BackupService:
|
||||
table_name = model.__tablename__
|
||||
logger.info(f"📊 Экспортируем таблицу: {table_name}")
|
||||
|
||||
result = await db.execute(select(model))
|
||||
query = select(model)
|
||||
|
||||
if model == User:
|
||||
query = query.options(selectinload(User.subscription))
|
||||
elif model == Subscription:
|
||||
query = query.options(selectinload(Subscription.user))
|
||||
elif model == Transaction:
|
||||
query = query.options(selectinload(Transaction.user))
|
||||
|
||||
result = await db.execute(query)
|
||||
records = result.scalars().all()
|
||||
|
||||
table_data = []
|
||||
@@ -166,8 +190,12 @@ class BackupService:
|
||||
for column in model.__table__.columns:
|
||||
value = getattr(record, column.name)
|
||||
|
||||
if isinstance(value, datetime):
|
||||
if value is None:
|
||||
record_dict[column.name] = None
|
||||
elif isinstance(value, datetime):
|
||||
record_dict[column.name] = value.isoformat()
|
||||
elif isinstance(value, (list, dict)):
|
||||
record_dict[column.name] = json_lib.dumps(value) if value else None
|
||||
elif hasattr(value, '__dict__'):
|
||||
record_dict[column.name] = str(value)
|
||||
else:
|
||||
@@ -266,7 +294,7 @@ class BackupService:
|
||||
clear_existing: bool = False
|
||||
) -> Tuple[bool, str]:
|
||||
try:
|
||||
logger.info(f"🔄 Начинаем восстановление из {backup_file_path}")
|
||||
logger.info(f"📄 Начинаем восстановление из {backup_file_path}")
|
||||
|
||||
backup_path = Path(backup_file_path)
|
||||
if not backup_path.exists():
|
||||
@@ -300,21 +328,25 @@ class BackupService:
|
||||
logger.warning("🗑️ Очищаем существующие данные...")
|
||||
await self._clear_database_tables(db)
|
||||
|
||||
for table_name, records in backup_data.items():
|
||||
models_by_table = {model.__tablename__: model for model in self.backup_models_ordered}
|
||||
|
||||
restore_order = []
|
||||
for model in self.backup_models_ordered:
|
||||
table_name = model.__tablename__
|
||||
if table_name in backup_data and backup_data[table_name]:
|
||||
restore_order.append(table_name)
|
||||
|
||||
for table_name in restore_order:
|
||||
records = backup_data[table_name]
|
||||
if not records:
|
||||
continue
|
||||
|
||||
model = None
|
||||
for m in self.backup_models:
|
||||
if m.__tablename__ == table_name:
|
||||
model = m
|
||||
break
|
||||
|
||||
model = models_by_table.get(table_name)
|
||||
if not model:
|
||||
logger.warning(f"⚠️ Модель для таблицы {table_name} не найдена, пропускаем")
|
||||
continue
|
||||
|
||||
logger.info(f"📥 Восстанавливаем таблицу {table_name} ({len(records)} записей)")
|
||||
logger.info(f"🔥 Восстанавливаем таблицу {table_name} ({len(records)} записей)")
|
||||
|
||||
for record_data in records:
|
||||
try:
|
||||
@@ -326,9 +358,11 @@ class BackupService:
|
||||
|
||||
column = getattr(model.__table__.columns, key, None)
|
||||
if column is None:
|
||||
logger.warning(f"Колонка {key} не найдена в модели {table_name}")
|
||||
continue
|
||||
|
||||
column_type_str = str(column.type).upper()
|
||||
|
||||
if ('DATETIME' in column_type_str or 'TIMESTAMP' in column_type_str) and isinstance(value, str):
|
||||
try:
|
||||
if 'T' in value:
|
||||
@@ -340,7 +374,7 @@ class BackupService:
|
||||
processed_data[key] = datetime.utcnow()
|
||||
elif ('BOOLEAN' in column_type_str or 'BOOL' in column_type_str) and isinstance(value, str):
|
||||
processed_data[key] = value.lower() in ('true', '1', 'yes', 'on')
|
||||
elif ('INTEGER' in column_type_str or 'INT' in column_type_str) and isinstance(value, str):
|
||||
elif ('INTEGER' in column_type_str or 'INT' in column_type_str or 'BIGINT' in column_type_str) and isinstance(value, str):
|
||||
try:
|
||||
processed_data[key] = int(value)
|
||||
except ValueError:
|
||||
@@ -350,15 +384,19 @@ class BackupService:
|
||||
processed_data[key] = float(value)
|
||||
except ValueError:
|
||||
processed_data[key] = 0.0
|
||||
elif 'JSON' in column_type_str and isinstance(value, str):
|
||||
try:
|
||||
processed_data[key] = json_lib.loads(value)
|
||||
except (ValueError, TypeError):
|
||||
elif 'JSON' in column_type_str:
|
||||
if isinstance(value, str) and value.strip():
|
||||
try:
|
||||
processed_data[key] = json_lib.loads(value)
|
||||
except (ValueError, TypeError):
|
||||
processed_data[key] = value
|
||||
elif isinstance(value, (list, dict)):
|
||||
processed_data[key] = value
|
||||
else:
|
||||
processed_data[key] = None
|
||||
else:
|
||||
processed_data[key] = value
|
||||
|
||||
# Проверяем существует ли запись с таким ID
|
||||
primary_key_col = None
|
||||
for col in model.__table__.columns:
|
||||
if col.primary_key:
|
||||
@@ -366,7 +404,6 @@ class BackupService:
|
||||
break
|
||||
|
||||
if primary_key_col and primary_key_col in processed_data:
|
||||
# Проверяем существование записи
|
||||
existing_record = await db.execute(
|
||||
select(model).where(
|
||||
getattr(model, primary_key_col) == processed_data[primary_key_col]
|
||||
@@ -374,18 +411,15 @@ class BackupService:
|
||||
)
|
||||
existing = existing_record.scalar_one_or_none()
|
||||
|
||||
if existing:
|
||||
# Обновляем существующую запись
|
||||
if existing and not clear_existing:
|
||||
for key, value in processed_data.items():
|
||||
if key != primary_key_col: # Не обновляем primary key
|
||||
if key != primary_key_col:
|
||||
setattr(existing, key, value)
|
||||
logger.debug(f"Обновлена существующая запись {primary_key_col}={processed_data[primary_key_col]} в {table_name}")
|
||||
else:
|
||||
# Создаем новую запись
|
||||
instance = model(**processed_data)
|
||||
db.add(instance)
|
||||
else:
|
||||
# Если нет primary key или он не в данных, просто добавляем
|
||||
instance = model(**processed_data)
|
||||
db.add(instance)
|
||||
|
||||
@@ -393,6 +427,7 @@ class BackupService:
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка восстановления записи в {table_name}: {e}")
|
||||
logger.error(f"Проблемные данные: {record_data}")
|
||||
continue
|
||||
|
||||
restored_tables += 1
|
||||
@@ -432,11 +467,12 @@ class BackupService:
|
||||
|
||||
async def _clear_database_tables(self, db: AsyncSession):
|
||||
tables_order = [
|
||||
"subscription_servers", "sent_notifications", "broadcast_history",
|
||||
"subscription_conversions", "referral_earnings", "promocode_uses",
|
||||
"transactions", "yookassa_payments", "cryptobot_payments",
|
||||
"subscriptions", "users", "promocodes", "server_squads",
|
||||
"service_rules", "system_settings", "monitoring_logs", "user_messages"
|
||||
"subscription_servers", "sent_notifications",
|
||||
"user_messages", "broadcast_history", "subscription_conversions",
|
||||
"referral_earnings", "promocode_uses", "transactions",
|
||||
"yookassa_payments", "cryptobot_payments", "welcome_texts",
|
||||
"subscriptions", "users", "promocodes", "server_squads",
|
||||
"squads", "service_rules", "system_settings", "monitoring_logs"
|
||||
]
|
||||
|
||||
for table_name in tables_order:
|
||||
@@ -472,7 +508,8 @@ class BackupService:
|
||||
"file_size_bytes": file_stats.st_size,
|
||||
"file_size_mb": round(file_stats.st_size / 1024 / 1024, 2),
|
||||
"created_by": metadata.get("created_by"),
|
||||
"database_type": metadata.get("database_type", "unknown")
|
||||
"database_type": metadata.get("database_type", "unknown"),
|
||||
"version": metadata.get("version", "1.0")
|
||||
}
|
||||
|
||||
backups.append(backup_info)
|
||||
@@ -491,6 +528,7 @@ class BackupService:
|
||||
"file_size_mb": round(file_stats.st_size / 1024 / 1024, 2),
|
||||
"created_by": None,
|
||||
"database_type": "unknown",
|
||||
"version": "unknown",
|
||||
"error": f"Ошибка чтения: {str(e)}"
|
||||
})
|
||||
|
||||
@@ -563,7 +601,7 @@ class BackupService:
|
||||
interval = self._get_backup_interval()
|
||||
self._auto_backup_task = asyncio.create_task(self._auto_backup_loop(next_run))
|
||||
logger.info(
|
||||
"🔄 Автобекапы включены, интервал: %.2fч, ближайший запуск: %s",
|
||||
"📄 Автобекапы включены, интервал: %.2fч, ближайший запуск: %s",
|
||||
interval.total_seconds() / 3600,
|
||||
next_run.strftime("%d.%m.%Y %H:%M:%S")
|
||||
)
|
||||
@@ -571,7 +609,7 @@ class BackupService:
|
||||
async def stop_auto_backup(self):
|
||||
if self._auto_backup_task and not self._auto_backup_task.done():
|
||||
self._auto_backup_task.cancel()
|
||||
logger.info("⏹️ Автобекапы остановлены")
|
||||
logger.info("ℹ️ Автобекапы остановлены")
|
||||
|
||||
async def _auto_backup_loop(self, next_run: Optional[datetime] = None):
|
||||
next_run = next_run or self._calculate_next_backup_datetime()
|
||||
@@ -595,7 +633,7 @@ class BackupService:
|
||||
next_run.strftime("%d.%m.%Y %H:%M:%S")
|
||||
)
|
||||
|
||||
logger.info("🔄 Запуск автоматического бекапа...")
|
||||
logger.info("📄 Запуск автоматического бекапа...")
|
||||
success, message, _ = await self.create_backup()
|
||||
|
||||
if success:
|
||||
@@ -624,7 +662,7 @@ class BackupService:
|
||||
icons = {
|
||||
"success": "✅",
|
||||
"error": "❌",
|
||||
"restore_success": "📥",
|
||||
"restore_success": "🔥",
|
||||
"restore_error": "❌"
|
||||
}
|
||||
|
||||
|
||||
@@ -3,7 +3,6 @@ from datetime import datetime, timedelta
|
||||
from typing import Optional, List, Dict, Any
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from sqlalchemy import delete, select, update
|
||||
|
||||
from app.database.crud.user import (
|
||||
get_user_by_id, get_user_by_telegram_id, get_users_list,
|
||||
get_users_count, get_users_statistics, get_inactive_users,
|
||||
@@ -12,8 +11,10 @@ from app.database.crud.user import (
|
||||
from app.database.crud.transaction import get_user_transactions_count
|
||||
from app.database.crud.subscription import get_subscription_by_user_id
|
||||
from app.database.models import (
|
||||
User, UserStatus, Subscription, Transaction, PromoCodeUse,
|
||||
ReferralEarning, SubscriptionServer, YooKassaPayment, BroadcastHistory, CryptoBotPayment
|
||||
User, UserStatus, Subscription, Transaction, PromoCode, PromoCodeUse,
|
||||
ReferralEarning, SubscriptionServer, YooKassaPayment, BroadcastHistory,
|
||||
CryptoBotPayment, SubscriptionConversion, UserMessage, WelcomeText,
|
||||
SentNotification
|
||||
)
|
||||
from app.config import settings
|
||||
|
||||
@@ -246,55 +247,103 @@ class UserService:
|
||||
except Exception as e:
|
||||
logger.warning(f"⚠️ Ошибка деактивации RemnaWave: {e}")
|
||||
|
||||
|
||||
try:
|
||||
from app.database.models import UserMessage
|
||||
from sqlalchemy import update
|
||||
|
||||
result = await db.execute(
|
||||
update(UserMessage)
|
||||
.where(UserMessage.created_by == user_id)
|
||||
.values(created_by=None)
|
||||
sent_notifications_result = await db.execute(
|
||||
select(SentNotification).where(SentNotification.user_id == user_id)
|
||||
)
|
||||
if result.rowcount > 0:
|
||||
logger.info(f"🔄 Обновлено {result.rowcount} пользовательских сообщений")
|
||||
await db.flush()
|
||||
except Exception as e:
|
||||
logger.error(f"❌ Ошибка обновления пользовательских сообщений: {e}")
|
||||
|
||||
try:
|
||||
from app.database.models import PromoCode
|
||||
from sqlalchemy import update
|
||||
sent_notifications = sent_notifications_result.scalars().all()
|
||||
|
||||
result = await db.execute(
|
||||
update(PromoCode)
|
||||
.where(PromoCode.created_by == user_id)
|
||||
.values(created_by=None)
|
||||
)
|
||||
if result.rowcount > 0:
|
||||
logger.info(f"🔄 Обновлено {result.rowcount} промокодов")
|
||||
await db.flush()
|
||||
if sent_notifications:
|
||||
logger.info(f"🔄 Удаляем {len(sent_notifications)} уведомлений")
|
||||
await db.execute(
|
||||
delete(SentNotification).where(SentNotification.user_id == user_id)
|
||||
)
|
||||
await db.flush()
|
||||
except Exception as e:
|
||||
logger.error(f"❌ Ошибка обновления промокодов: {e}")
|
||||
logger.error(f"❌ Ошибка удаления уведомлений: {e}")
|
||||
|
||||
try:
|
||||
from app.database.models import WelcomeText
|
||||
from sqlalchemy import update
|
||||
|
||||
result = await db.execute(
|
||||
update(WelcomeText)
|
||||
.where(WelcomeText.created_by == user_id)
|
||||
.values(created_by=None)
|
||||
)
|
||||
if result.rowcount > 0:
|
||||
logger.info(f"🔄 Обновлено {result.rowcount} приветственных текстов")
|
||||
await db.flush()
|
||||
if user.subscription:
|
||||
subscription_servers_result = await db.execute(
|
||||
select(SubscriptionServer).where(
|
||||
SubscriptionServer.subscription_id == user.subscription.id
|
||||
)
|
||||
)
|
||||
subscription_servers = subscription_servers_result.scalars().all()
|
||||
|
||||
if subscription_servers:
|
||||
logger.info(f"🔄 Удаляем {len(subscription_servers)} связей подписка-сервер")
|
||||
await db.execute(
|
||||
delete(SubscriptionServer).where(
|
||||
SubscriptionServer.subscription_id == user.subscription.id
|
||||
)
|
||||
)
|
||||
await db.flush()
|
||||
except Exception as e:
|
||||
logger.error(f"❌ Ошибка обновления приветственных текстов: {e}")
|
||||
logger.error(f"❌ Ошибка удаления связей подписка-сервер: {e}")
|
||||
|
||||
try:
|
||||
from app.database.models import YooKassaPayment
|
||||
from sqlalchemy import select
|
||||
conversions_result = await db.execute(
|
||||
select(SubscriptionConversion).where(SubscriptionConversion.user_id == user_id)
|
||||
)
|
||||
conversions = conversions_result.scalars().all()
|
||||
|
||||
if conversions:
|
||||
logger.info(f"🔄 Удаляем {len(conversions)} записей конверсий")
|
||||
await db.execute(
|
||||
delete(SubscriptionConversion).where(SubscriptionConversion.user_id == user_id)
|
||||
)
|
||||
await db.flush()
|
||||
except Exception as e:
|
||||
logger.error(f"❌ Ошибка удаления записей конверсий: {e}")
|
||||
|
||||
try:
|
||||
referral_earnings_result = await db.execute(
|
||||
select(ReferralEarning).where(ReferralEarning.user_id == user_id)
|
||||
)
|
||||
referral_earnings = referral_earnings_result.scalars().all()
|
||||
|
||||
if referral_earnings:
|
||||
logger.info(f"🔄 Удаляем {len(referral_earnings)} реферальных доходов")
|
||||
await db.execute(
|
||||
delete(ReferralEarning).where(ReferralEarning.user_id == user_id)
|
||||
)
|
||||
await db.flush()
|
||||
except Exception as e:
|
||||
logger.error(f"❌ Ошибка удаления реферальных доходов: {e}")
|
||||
|
||||
try:
|
||||
referral_records_result = await db.execute(
|
||||
select(ReferralEarning).where(ReferralEarning.referral_id == user_id)
|
||||
)
|
||||
referral_records = referral_records_result.scalars().all()
|
||||
|
||||
if referral_records:
|
||||
logger.info(f"🔄 Удаляем {len(referral_records)} записей о рефералах")
|
||||
await db.execute(
|
||||
delete(ReferralEarning).where(ReferralEarning.referral_id == user_id)
|
||||
)
|
||||
await db.flush()
|
||||
except Exception as e:
|
||||
logger.error(f"❌ Ошибка удаления записей о рефералах: {e}")
|
||||
|
||||
try:
|
||||
promocode_uses_result = await db.execute(
|
||||
select(PromoCodeUse).where(PromoCodeUse.user_id == user_id)
|
||||
)
|
||||
promocode_uses = promocode_uses_result.scalars().all()
|
||||
|
||||
if promocode_uses:
|
||||
logger.info(f"🔄 Удаляем {len(promocode_uses)} использований промокодов")
|
||||
await db.execute(
|
||||
delete(PromoCodeUse).where(PromoCodeUse.user_id == user_id)
|
||||
)
|
||||
await db.flush()
|
||||
except Exception as e:
|
||||
logger.error(f"❌ Ошибка удаления использований промокодов: {e}")
|
||||
|
||||
try:
|
||||
yookassa_result = await db.execute(
|
||||
select(YooKassaPayment).where(YooKassaPayment.user_id == user_id)
|
||||
)
|
||||
@@ -306,14 +355,10 @@ class UserService:
|
||||
delete(YooKassaPayment).where(YooKassaPayment.user_id == user_id)
|
||||
)
|
||||
await db.flush()
|
||||
logger.info(f"✅ YooKassa платежи удалены")
|
||||
except Exception as e:
|
||||
logger.error(f"❌ Ошибка удаления YooKassa платежей: {e}")
|
||||
|
||||
try:
|
||||
from app.database.models import CryptoBotPayment
|
||||
from sqlalchemy import select, delete
|
||||
|
||||
cryptobot_result = await db.execute(
|
||||
select(CryptoBotPayment).where(CryptoBotPayment.user_id == user_id)
|
||||
)
|
||||
@@ -325,10 +370,9 @@ class UserService:
|
||||
delete(CryptoBotPayment).where(CryptoBotPayment.user_id == user_id)
|
||||
)
|
||||
await db.flush()
|
||||
logger.info(f"✅ CryptoBot платежи удалены")
|
||||
except Exception as e:
|
||||
logger.error(f"❌ Ошибка удаления CryptoBot платежей: {e}")
|
||||
|
||||
|
||||
try:
|
||||
transactions_result = await db.execute(
|
||||
select(Transaction).where(Transaction.user_id == user_id)
|
||||
@@ -341,89 +385,72 @@ class UserService:
|
||||
delete(Transaction).where(Transaction.user_id == user_id)
|
||||
)
|
||||
await db.flush()
|
||||
logger.info(f"✅ Транзакции удалены")
|
||||
except Exception as e:
|
||||
logger.error(f"❌ Ошибка удаления транзакций: {e}")
|
||||
|
||||
|
||||
try:
|
||||
await db.execute(
|
||||
delete(PromoCodeUse).where(PromoCodeUse.user_id == user_id)
|
||||
)
|
||||
await db.flush()
|
||||
logger.info(f"🗑️ Удалены использования промокодов пользователя {user_id}")
|
||||
except Exception as e:
|
||||
logger.error(f"❌ Ошибка удаления использований промокодов: {e}")
|
||||
|
||||
try:
|
||||
await db.execute(
|
||||
delete(ReferralEarning).where(ReferralEarning.user_id == user_id)
|
||||
)
|
||||
await db.flush()
|
||||
logger.info(f"🗑️ Удалены реферальные доходы пользователя {user_id}")
|
||||
except Exception as e:
|
||||
logger.error(f"❌ Ошибка удаления реферальных доходов: {e}")
|
||||
|
||||
try:
|
||||
await db.execute(
|
||||
delete(ReferralEarning).where(ReferralEarning.referral_id == user_id)
|
||||
)
|
||||
await db.flush()
|
||||
logger.info(f"🗑️ Удалены реферальные записи о пользователе {user_id}")
|
||||
except Exception as e:
|
||||
logger.error(f"❌ Ошибка удаления реферальных записей: {e}")
|
||||
|
||||
try:
|
||||
from app.database.models import BroadcastHistory
|
||||
await db.execute(
|
||||
delete(BroadcastHistory).where(BroadcastHistory.admin_id == user_id)
|
||||
)
|
||||
await db.flush()
|
||||
logger.info(f"🗑️ Удалена история рассылок админа {user_id}")
|
||||
except Exception as e:
|
||||
logger.error(f"❌ Ошибка удаления истории рассылок: {e}")
|
||||
|
||||
try:
|
||||
from app.database.models import SubscriptionConversion
|
||||
conversions_result = await db.execute(
|
||||
select(SubscriptionConversion).where(SubscriptionConversion.user_id == user_id)
|
||||
)
|
||||
conversions = conversions_result.scalars().all()
|
||||
|
||||
if conversions:
|
||||
logger.info(f"🔄 Удаляем {len(conversions)} записей конверсий")
|
||||
await db.execute(
|
||||
delete(SubscriptionConversion).where(SubscriptionConversion.user_id == user_id)
|
||||
)
|
||||
await db.flush()
|
||||
logger.info(f"✅ Записи конверсий удалены")
|
||||
except Exception as e:
|
||||
logger.error(f"❌ Ошибка удаления записей конверсий: {e}")
|
||||
|
||||
if user.subscription:
|
||||
try:
|
||||
await db.execute(
|
||||
delete(SubscriptionServer).where(
|
||||
SubscriptionServer.subscription_id == user.subscription.id
|
||||
)
|
||||
)
|
||||
await db.flush()
|
||||
logger.info(f"🗑️ Удалены записи SubscriptionServer для подписки {user.subscription.id}")
|
||||
except Exception as e:
|
||||
logger.error(f"❌ Ошибка удаления SubscriptionServer: {e}")
|
||||
|
||||
if user.subscription:
|
||||
try:
|
||||
from app.database.models import Subscription
|
||||
if user.subscription:
|
||||
logger.info(f"🔄 Удаляем подписку {user.subscription.id}")
|
||||
await db.execute(
|
||||
delete(Subscription).where(Subscription.user_id == user_id)
|
||||
)
|
||||
await db.flush()
|
||||
logger.info(f"🗑️ Удалена подписка пользователя {user_id}")
|
||||
except Exception as e:
|
||||
logger.error(f"❌ Ошибка удаления подписки: {e}")
|
||||
except Exception as e:
|
||||
logger.error(f"❌ Ошибка удаления подписки: {e}")
|
||||
|
||||
|
||||
try:
|
||||
from sqlalchemy import update
|
||||
user_messages_result = await db.execute(
|
||||
update(UserMessage)
|
||||
.where(UserMessage.created_by == user_id)
|
||||
.values(created_by=None)
|
||||
)
|
||||
if user_messages_result.rowcount > 0:
|
||||
logger.info(f"🔄 Обновлено {user_messages_result.rowcount} пользовательских сообщений")
|
||||
await db.flush()
|
||||
except Exception as e:
|
||||
logger.error(f"❌ Ошибка обновления пользовательских сообщений: {e}")
|
||||
|
||||
try:
|
||||
promocodes_result = await db.execute(
|
||||
update(PromoCode)
|
||||
.where(PromoCode.created_by == user_id)
|
||||
.values(created_by=None)
|
||||
)
|
||||
if promocodes_result.rowcount > 0:
|
||||
logger.info(f"🔄 Обновлено {promocodes_result.rowcount} промокодов")
|
||||
await db.flush()
|
||||
except Exception as e:
|
||||
logger.error(f"❌ Ошибка обновления промокодов: {e}")
|
||||
|
||||
try:
|
||||
welcome_texts_result = await db.execute(
|
||||
update(WelcomeText)
|
||||
.where(WelcomeText.created_by == user_id)
|
||||
.values(created_by=None)
|
||||
)
|
||||
if welcome_texts_result.rowcount > 0:
|
||||
logger.info(f"🔄 Обновлено {welcome_texts_result.rowcount} приветственных текстов")
|
||||
await db.flush()
|
||||
except Exception as e:
|
||||
logger.error(f"❌ Ошибка обновления приветственных текстов: {e}")
|
||||
|
||||
try:
|
||||
broadcast_history_result = await db.execute(
|
||||
select(BroadcastHistory).where(BroadcastHistory.admin_id == user_id)
|
||||
)
|
||||
broadcast_history = broadcast_history_result.scalars().all()
|
||||
|
||||
if broadcast_history:
|
||||
logger.info(f"🔄 Удаляем {len(broadcast_history)} записей истории рассылок")
|
||||
await db.execute(
|
||||
delete(BroadcastHistory).where(BroadcastHistory.admin_id == user_id)
|
||||
)
|
||||
await db.flush()
|
||||
except Exception as e:
|
||||
logger.error(f"❌ Ошибка удаления истории рассылок: {e}")
|
||||
|
||||
try:
|
||||
referrals_result = await db.execute(
|
||||
update(User)
|
||||
.where(User.referred_by_id == user_id)
|
||||
@@ -434,7 +461,7 @@ class UserService:
|
||||
await db.flush()
|
||||
except Exception as e:
|
||||
logger.error(f"❌ Ошибка очистки реферальных ссылок: {e}")
|
||||
|
||||
|
||||
try:
|
||||
await db.execute(
|
||||
delete(User).where(User.id == user_id)
|
||||
|
||||
123
app/utils/payment_utils.py
Normal file
123
app/utils/payment_utils.py
Normal file
@@ -0,0 +1,123 @@
|
||||
from typing import List, Dict, Tuple
|
||||
from app.config import settings
|
||||
|
||||
def get_available_payment_methods() -> List[Dict[str, str]]:
|
||||
"""
|
||||
Возвращает список доступных способов оплаты с их настройками
|
||||
"""
|
||||
methods = []
|
||||
|
||||
if settings.TELEGRAM_STARS_ENABLED:
|
||||
methods.append({
|
||||
"id": "stars",
|
||||
"name": "Telegram Stars",
|
||||
"icon": "⭐",
|
||||
"description": "быстро и удобно",
|
||||
"callback": "topup_stars"
|
||||
})
|
||||
|
||||
if settings.is_yookassa_enabled():
|
||||
methods.append({
|
||||
"id": "yookassa",
|
||||
"name": "Банковская карта",
|
||||
"icon": "💳",
|
||||
"description": "через YooKassa",
|
||||
"callback": "topup_yookassa"
|
||||
})
|
||||
|
||||
if settings.TRIBUTE_ENABLED:
|
||||
methods.append({
|
||||
"id": "tribute",
|
||||
"name": "Банковская карта",
|
||||
"icon": "💳",
|
||||
"description": "через Tribute",
|
||||
"callback": "topup_tribute"
|
||||
})
|
||||
|
||||
if settings.is_cryptobot_enabled():
|
||||
methods.append({
|
||||
"id": "cryptobot",
|
||||
"name": "Криптовалюта",
|
||||
"icon": "🪙",
|
||||
"description": "через CryptoBot",
|
||||
"callback": "topup_cryptobot"
|
||||
})
|
||||
|
||||
# Поддержка всегда доступна
|
||||
methods.append({
|
||||
"id": "support",
|
||||
"name": "Через поддержку",
|
||||
"icon": "🛠️",
|
||||
"description": "другие способы",
|
||||
"callback": "topup_support"
|
||||
})
|
||||
|
||||
return methods
|
||||
|
||||
def get_payment_methods_text() -> str:
|
||||
"""
|
||||
Генерирует текст с описанием доступных способов оплаты
|
||||
"""
|
||||
methods = get_available_payment_methods()
|
||||
|
||||
if len(methods) <= 1: # Только поддержка
|
||||
return """💳 <b>Способы пополнения баланса</b>
|
||||
|
||||
⚠️ В данный момент автоматические способы оплаты временно недоступны.
|
||||
Обратитесь в техподдержку для пополнения баланса.
|
||||
|
||||
Выберите способ пополнения:"""
|
||||
|
||||
text = "💳 <b>Способы пополнения баланса</b>\n\n"
|
||||
text += "Выберите удобный для вас способ оплаты:\n\n"
|
||||
|
||||
for method in methods:
|
||||
text += f"{method['icon']} <b>{method['name']}</b> - {method['description']}\n"
|
||||
|
||||
text += "\nВыберите способ пополнения:"
|
||||
|
||||
return text
|
||||
|
||||
def is_payment_method_available(method_id: str) -> bool:
|
||||
"""
|
||||
Проверяет, доступен ли конкретный способ оплаты
|
||||
"""
|
||||
if method_id == "stars":
|
||||
return settings.TELEGRAM_STARS_ENABLED
|
||||
elif method_id == "yookassa":
|
||||
return settings.is_yookassa_enabled()
|
||||
elif method_id == "tribute":
|
||||
return settings.TRIBUTE_ENABLED
|
||||
elif method_id == "cryptobot":
|
||||
return settings.is_cryptobot_enabled()
|
||||
elif method_id == "support":
|
||||
return True # Поддержка всегда доступна
|
||||
else:
|
||||
return False
|
||||
|
||||
def get_payment_method_status() -> Dict[str, bool]:
|
||||
"""
|
||||
Возвращает статус всех способов оплаты
|
||||
"""
|
||||
return {
|
||||
"stars": settings.TELEGRAM_STARS_ENABLED,
|
||||
"yookassa": settings.is_yookassa_enabled(),
|
||||
"tribute": settings.TRIBUTE_ENABLED,
|
||||
"cryptobot": settings.is_cryptobot_enabled(),
|
||||
"support": True
|
||||
}
|
||||
|
||||
def get_enabled_payment_methods_count() -> int:
|
||||
"""
|
||||
Возвращает количество включенных способов оплаты (не считая поддержку)
|
||||
"""
|
||||
count = 0
|
||||
if settings.TELEGRAM_STARS_ENABLED:
|
||||
count += 1
|
||||
if settings.is_yookassa_enabled():
|
||||
count += 1
|
||||
if settings.TRIBUTE_ENABLED:
|
||||
count += 1
|
||||
if settings.is_cryptobot_enabled():
|
||||
count += 1
|
||||
return count
|
||||
Reference in New Issue
Block a user