Compare commits
19 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| f307ad03bf | |||
| f150bfad37 | |||
| 8c22df1f52 | |||
| f396ca7df4 | |||
| 0b1a2b7f68 | |||
| d92b9559ac | |||
| fb3942e29d | |||
| 6009d15a51 | |||
| e2469a14c9 | |||
| 4daacae0d9 | |||
| 47dbced347 | |||
| 49b129a8f4 | |||
| f8315ba18e | |||
| a9809e74c9 | |||
| 4fe61ec870 | |||
| 172ee773ee | |||
| b8edc4ec0d | |||
| d8f2f413a8 | |||
| 360a04fbe8 |
@@ -7,6 +7,11 @@ BOT_TOKEN=
|
||||
ADMIN_IDS=
|
||||
SUPPORT_USERNAME=@support
|
||||
|
||||
# Уведомления администраторов
|
||||
ADMIN_NOTIFICATIONS_ENABLED=true
|
||||
ADMIN_NOTIFICATIONS_CHAT_ID=-1001234567890 # Замени на ID твоего канала (-100) - ПРЕФИКС ЗАКРЫТОГО КАНАЛА! ВСТАВИТЬ СВОЙ ID СРАЗУ ПОСЛЕ (-100) БЕЗ ПРОБЕЛОВ!
|
||||
ADMIN_NOTIFICATIONS_TOPIC_ID=123 # Опционально: ID топика
|
||||
|
||||
# ===== DATABASE =====
|
||||
# Для Docker используйте PostgreSQL:
|
||||
DATABASE_URL=postgresql+asyncpg://remnawave_user:secure_password_123@postgres:5432/remnawave_bot
|
||||
@@ -85,6 +90,10 @@ REFERRAL_FIRST_TOPUP_BONUS_KOPEKS=10000
|
||||
REFERRAL_INVITER_BONUS_KOPEKS=10000
|
||||
REFERRAL_COMMISSION_PERCENT=25
|
||||
|
||||
# Уведомления
|
||||
REFERRAL_NOTIFICATIONS_ENABLED=true
|
||||
REFERRAL_NOTIFICATION_RETRY_ATTEMPTS=3
|
||||
|
||||
# ===== АВТОПРОДЛЕНИЕ =====
|
||||
AUTOPAY_WARNING_DAYS=3,1
|
||||
DEFAULT_AUTOPAY_DAYS_BEFORE=3
|
||||
|
||||
@@ -38,6 +38,7 @@
|
||||
- 💰 **Многоканальные платежи** - Telegram Stars + Tribute + ЮKassa
|
||||
- 🔄 **Автоматизация 99%** - от регистрации до продления подписок
|
||||
- 📊 **Детальная аналитика** - полная картина вашего бизнеса
|
||||
- 💬 **Уведомления в топики** - Уведомление в топик канала об: Активация триала 💎 Покупка подписки 🔄 Конверсия из триала в платную ⏰ Продление подписки 💰 Пополнение баланса
|
||||
|
||||
### 🎛️ **Гибкость конфигурации**
|
||||
- 🌍 **Умный выбор серверов** - автоматический пропуск при одном сервере, мультивыбор при нескольких
|
||||
@@ -167,6 +168,26 @@ DEFAULT_DEVICE_LIMIT=3
|
||||
MAX_DEVICES_LIMIT=15
|
||||
```
|
||||
|
||||
### 👥 Реферальная система
|
||||
|
||||
```env
|
||||
# Минимальная сумма пополнения для активации бонусов
|
||||
REFERRAL_MINIMUM_TOPUP_KOPEKS=10000
|
||||
|
||||
# Бонус новому пользователю при первом пополнении
|
||||
REFERRAL_FIRST_TOPUP_BONUS_KOPEKS=10000
|
||||
|
||||
# Бонус пригласившему при первом пополнении реферала
|
||||
REFERRAL_INVITER_BONUS_KOPEKS=10000
|
||||
|
||||
# Процент комиссии с последующих пополнений
|
||||
REFERRAL_COMMISSION_PERCENT=25
|
||||
|
||||
# Уведомления
|
||||
REFERRAL_NOTIFICATIONS_ENABLED=true
|
||||
REFERRAL_NOTIFICATION_RETRY_ATTEMPTS=3
|
||||
```
|
||||
|
||||
### 🛡️ Мониторинг и техническое обслуживание
|
||||
|
||||
```env
|
||||
@@ -195,6 +216,11 @@ BOT_TOKEN=
|
||||
ADMIN_IDS=
|
||||
SUPPORT_USERNAME=@support
|
||||
|
||||
# Уведомления администраторов
|
||||
ADMIN_NOTIFICATIONS_ENABLED=true
|
||||
ADMIN_NOTIFICATIONS_CHAT_ID=-1001234567890 # Замени на ID твоего канала (-100) - ПРЕФИКС ЗАКРЫТОГО КАНАЛА! ВСТАВИТЬ СВОЙ ID СРАЗУ ПОСЛЕ (-100) БЕЗ ПРОБЕЛОВ!
|
||||
ADMIN_NOTIFICATIONS_TOPIC_ID=123 # Опционально: ID топика
|
||||
|
||||
# ===== DATABASE =====
|
||||
# Для Docker используйте PostgreSQL:
|
||||
DATABASE_URL=postgresql+asyncpg://remnawave_user:secure_password_123@postgres:5432/remnawave_bot
|
||||
@@ -272,6 +298,10 @@ REFERRAL_FIRST_TOPUP_BONUS_KOPEKS=10000
|
||||
REFERRAL_INVITER_BONUS_KOPEKS=10000
|
||||
REFERRAL_COMMISSION_PERCENT=25
|
||||
|
||||
# Уведомления
|
||||
REFERRAL_NOTIFICATIONS_ENABLED=true
|
||||
REFERRAL_NOTIFICATION_RETRY_ATTEMPTS=3
|
||||
|
||||
# ===== АВТОПРОДЛЕНИЕ =====
|
||||
AUTOPAY_WARNING_DAYS=3,1
|
||||
DEFAULT_AUTOPAY_DAYS_BEFORE=3
|
||||
@@ -378,7 +408,7 @@ APP_CONFIG_CACHE_TTL=3600
|
||||
🛒 **Умная покупка подписок**
|
||||
- 📅 Гибкие периоды (14-360 дней)
|
||||
- 📊 Выбор трафика или фиксированный лимит
|
||||
- 🌍 Автоматический выбор серверов
|
||||
- 🌍 Автоматический выбор серверов(для одного сквада) или ручной выбор из множества
|
||||
- 📱 Настройка количества устройств
|
||||
|
||||
🧪 **Тестовая подписка**
|
||||
@@ -394,7 +424,7 @@ APP_CONFIG_CACHE_TTL=3600
|
||||
- Детальная история транзакций
|
||||
|
||||
📱 **Управление подписками**
|
||||
- 📈 Статистика использования в реальном времени
|
||||
- 📈 Статистика использования в реальном времени (Используемый трафик, подключенные устройства)
|
||||
- 🔄 Автопродление с баланса
|
||||
- 🔄 Управление трафиком
|
||||
- 🌍 Переключение серверов на лету
|
||||
@@ -402,8 +432,9 @@ APP_CONFIG_CACHE_TTL=3600
|
||||
|
||||
🎁 **Бонусная система**
|
||||
- 🎫 Промокоды на деньги/дни/триал
|
||||
- 👥 Реферальная программа с комиссиями
|
||||
- 👥 Защищенная реферальная программа с комиссиями
|
||||
- 🔔 Своевременные уведомления
|
||||
- 📊 Детальная статистика рефералов
|
||||
|
||||
</td>
|
||||
<td width="50%" valign="top">
|
||||
@@ -423,6 +454,7 @@ APP_CONFIG_CACHE_TTL=3600
|
||||
- 📊 **НОВОЕ**: Настройка лимитов трафика (0-10000 ГБ)
|
||||
- 🌍 **НОВОЕ**: Мультивыбор серверов
|
||||
- 🔄 **НОВОЕ**: Сброс HWID устройств
|
||||
- 👥 **НОВОЕ**: Детальная реферальная статистика в профиле пользователя
|
||||
- 🚫 Блокировка/разблокировка/удаление
|
||||
|
||||
🎫 **Промо-система**
|
||||
@@ -443,12 +475,51 @@ APP_CONFIG_CACHE_TTL=3600
|
||||
- 💬 Система поддержки с HTML разметкой
|
||||
- 📝 Настройка правил сервиса
|
||||
|
||||
📨 **Уведомления в закрытый канал**
|
||||
- 🎯 Активация триала
|
||||
- 💎 Покупка подписки
|
||||
- 🔄 Конверсия из триала в платную
|
||||
- ⏰ Продление подписки
|
||||
- 💰 Пополнение баланса
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
---
|
||||
|
||||
## 🆕 Недавние обновления (v2.2.1)
|
||||
|
||||
### 🛡️ **Защищенная реферальная система**
|
||||
- **Защита от абуза**: Бонусы выдаются только при реальном пополнении от 100₽
|
||||
- **Новая логика наград**: При первом пополнении ≥100₽ - новый пользователь и пригласивший получают по 100₽
|
||||
- **Комиссионная система**: Пригласивший получает 25% с последующих пополнений
|
||||
- **Автоматическая миграция**: Обновление статуса существующих пользователей с историей пополнений
|
||||
|
||||
### 🔔 **Умные уведомления**
|
||||
- **Реферальные уведомления**: Автоматические сообщения при переходе по ссылке, первом пополнении и получении комиссий
|
||||
- **Контроль повторов**: Защита от дублирования уведомлений и повторного получения бонусов
|
||||
- **Настраиваемые попытки**: Конфигурируемое количество попыток отправки уведомлений
|
||||
|
||||
### 📊 **Расширенная статистика рефералов**
|
||||
- **Детальная аналитика**: Количество приглашенных, активных рефералов, конверсия и доходы
|
||||
- **Список рефералов**: Просмотр всех приглашенных пользователей с пагинацией
|
||||
- **Топ-рефералы**: Самые прибыльные рефералы с индикацией активности
|
||||
- **Доходы по периодам**: Разбивка заработка по типам начислений
|
||||
|
||||
### 🔧 **Исправления админ-панели**
|
||||
- **Статистика**: Исправлена неработающая кнопка "Обновить" в статистике
|
||||
- **Промокоды**: Исправлена кнопка "Статистика" в разделе управления промокодами
|
||||
- **Навигация**: Синхронизированы callback_data между клавиатурами и обработчиками
|
||||
- **Реферальная статистика**: Добавлена детальная реферальная информация в профилях пользователей
|
||||
|
||||
### 📱 **Улучшения пользовательского интерфейса**
|
||||
- **User-friendly**: Улучшен интерфейс страницы подписки для лучшего UX
|
||||
- **Производительность**: Оптимизированы SQL-запросы для работы с реферальной статистикой
|
||||
- **Обработка ошибок**: Улучшена обработка ошибок при обновлении данных
|
||||
|
||||
---
|
||||
|
||||
## 🆕 Недавние обновления
|
||||
|
||||
### 🛠️ **Управление серверами**
|
||||
@@ -597,6 +668,41 @@ docker compose down -v --remove-orphans
|
||||
- **Tribute**: Настрой webhook на `https://your-domain.com/tribute-webhook`
|
||||
- **YooKassa**: Настрой webhook на `https://your-domain.com/yookassa-webhook`
|
||||
|
||||
|
||||
### 🛠️ Настройка Уведомлений в топик группы
|
||||
|
||||
#### 1. Переменные окружения
|
||||
|
||||
Добавьте в файл `.env`:
|
||||
|
||||
```env
|
||||
# Уведомления администраторов
|
||||
ADMIN_NOTIFICATIONS_ENABLED=true
|
||||
ADMIN_NOTIFICATIONS_CHAT_ID=-1001234567890 # ID канала/группы
|
||||
ADMIN_NOTIFICATIONS_TOPIC_ID=123 # ID топика (опционально)
|
||||
```
|
||||
|
||||
#### 2. Создание канала
|
||||
|
||||
1. **Создайте приватный канал** или группу для уведомлений
|
||||
2. **Добавьте бота** как администратора с правами:
|
||||
- Отправка сообщений
|
||||
- Использование встроенного режима (если нужно)
|
||||
3. **Получите ID канала**:
|
||||
- Отправьте любое сообщение в канал
|
||||
- Перешлите его боту @userinfobot
|
||||
- Скопируйте Chat ID (например: `-1001234567890`)
|
||||
|
||||
#### 3. Настройка топиков (опционально)
|
||||
|
||||
Если используете супергруппу с топиками:
|
||||
|
||||
1. **Включите топики** в настройках группы
|
||||
2. **Создайте топик** для уведомлений (например, "Уведомления")
|
||||
3. **Получите ID топика**:
|
||||
- Откройте топик в веб-версии Telegram
|
||||
- Скопируйте число после последнего `/` в URL
|
||||
- Или используйте бота для получения message_thread_id
|
||||
---
|
||||
|
||||
## 🚀 Производительность
|
||||
@@ -667,6 +773,7 @@ bedolaga_bot/
|
||||
│ │ ├── 📋 subscription.py # Операции с подписками
|
||||
│ │ ├── 💰 transaction.py # Операции с транзакциями
|
||||
│ │ ├── 📜 rules.py # Правила сервиса
|
||||
│ │ ├── 📜 subscription_conversion.py # Правила сервиса
|
||||
│ │ ├── 💳 yookassa.py # YooKassa операции
|
||||
│ │ ├── 🌐 server_squad.py # Серверы и сквады
|
||||
│ │ ├── 🎁 promocode.py # Промокоды
|
||||
@@ -934,6 +1041,20 @@ REMNAWAVE_SECRET_KEY=XXXXXXX:DDDDDDDD
|
||||
|
||||
<tr>
|
||||
<td>5</td>
|
||||
<td><strong>@edward_forix</strong></td>
|
||||
<td>₽1,000</td>
|
||||
<td>За поддержку и доверие</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td>6</td>
|
||||
<td><strong>@SmartSystemCompany</strong></td>
|
||||
<td>₽600</td>
|
||||
<td>За поддержку и доверие</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td>7</td>
|
||||
<td><strong>@PhiLin58</strong></td>
|
||||
<td>₽300</td>
|
||||
<td>За участие в развитии</td>
|
||||
@@ -1008,4 +1129,3 @@ REMNAWAVE_SECRET_KEY=XXXXXXX:DDDDDDDD
|
||||
**Made with ❤️ by [@fringg](https://t.me/fringg)**
|
||||
|
||||
</div>
|
||||
│
|
||||
|
||||
@@ -10,6 +10,10 @@ class Settings(BaseSettings):
|
||||
BOT_TOKEN: str
|
||||
ADMIN_IDS: str = ""
|
||||
SUPPORT_USERNAME: str = "@support"
|
||||
|
||||
ADMIN_NOTIFICATIONS_ENABLED: bool = False
|
||||
ADMIN_NOTIFICATIONS_CHAT_ID: Optional[str] = None
|
||||
ADMIN_NOTIFICATIONS_TOPIC_ID: Optional[int] = None
|
||||
|
||||
DATABASE_URL: str
|
||||
REDIS_URL: str = "redis://localhost:6379/0"
|
||||
@@ -302,6 +306,19 @@ class Settings(BaseSettings):
|
||||
def rubles_to_stars(self, rubles: float) -> int:
|
||||
return max(1, int(rubles / self.get_stars_rate()))
|
||||
|
||||
def get_admin_notifications_chat_id(self) -> Optional[int]:
|
||||
if not self.ADMIN_NOTIFICATIONS_CHAT_ID:
|
||||
return None
|
||||
|
||||
try:
|
||||
return int(self.ADMIN_NOTIFICATIONS_CHAT_ID)
|
||||
except (ValueError, TypeError):
|
||||
return None
|
||||
|
||||
def is_admin_notifications_enabled(self) -> bool:
|
||||
return (self.ADMIN_NOTIFICATIONS_ENABLED and
|
||||
self.get_admin_notifications_chat_id() is not None)
|
||||
|
||||
def get_referral_settings(self) -> Dict:
|
||||
return {
|
||||
"minimum_topup_kopeks": self.REFERRAL_MINIMUM_TOPUP_KOPEKS,
|
||||
|
||||
@@ -356,3 +356,16 @@ async def create_unique_tribute_transaction(
|
||||
external_id=external_id,
|
||||
is_completed=True
|
||||
)
|
||||
|
||||
async def get_transaction_by_id(
|
||||
db: AsyncSession,
|
||||
transaction_id: int
|
||||
) -> Optional[Transaction]:
|
||||
try:
|
||||
result = await db.execute(
|
||||
select(Transaction).where(Transaction.id == transaction_id)
|
||||
)
|
||||
return result.scalar_one_or_none()
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка получения транзакции {transaction_id}: {e}")
|
||||
return None
|
||||
|
||||
@@ -526,43 +526,6 @@ async def check_yookassa_payment_status(
|
||||
logger.error(f"Ошибка проверки статуса платежа: {e}")
|
||||
await callback.answer("❌ Ошибка проверки статуса", show_alert=True)
|
||||
|
||||
@error_handler
|
||||
async def add_user_balance(
|
||||
db: AsyncSession,
|
||||
user: User,
|
||||
amount_kopeks: int,
|
||||
description: str = "Пополнение баланса"
|
||||
) -> bool:
|
||||
try:
|
||||
old_balance = user.balance_kopeks
|
||||
user.balance_kopeks += amount_kopeks
|
||||
user.updated_at = datetime.utcnow()
|
||||
|
||||
from app.database.crud.transaction import create_transaction
|
||||
from app.database.models import TransactionType
|
||||
|
||||
await create_transaction(
|
||||
db=db,
|
||||
user_id=user.id,
|
||||
type=TransactionType.DEPOSIT,
|
||||
amount_kopeks=amount_kopeks,
|
||||
description=description
|
||||
)
|
||||
|
||||
await db.commit()
|
||||
await db.refresh(user)
|
||||
|
||||
if "пополнение" in description.lower() or "topup" in description.lower():
|
||||
from app.services.referral_service import process_referral_topup
|
||||
await process_referral_topup(db, user.id, amount_kopeks)
|
||||
|
||||
logger.info(f"💰 Баланс пользователя {user.telegram_id} изменен: {old_balance} → {user.balance_kopeks} (изменение: +{amount_kopeks})")
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка изменения баланса пользователя {user.id}: {e}")
|
||||
await db.rollback()
|
||||
return False
|
||||
|
||||
|
||||
def register_handlers(dp: Dispatcher):
|
||||
|
||||
@@ -21,7 +21,7 @@ from app.database.crud.user import subtract_user_balance
|
||||
from app.database.crud.transaction import create_transaction, get_user_transactions
|
||||
from app.database.models import (
|
||||
User, TransactionType, SubscriptionStatus,
|
||||
SubscriptionServer
|
||||
SubscriptionServer, Subscription
|
||||
)
|
||||
from app.keyboards.inline import (
|
||||
get_subscription_keyboard, get_trial_keyboard,
|
||||
@@ -38,6 +38,7 @@ from app.keyboards.inline import (
|
||||
)
|
||||
from app.localization.texts import get_texts
|
||||
from app.services.remnawave_service import RemnaWaveService
|
||||
from app.services.admin_notification_service import AdminNotificationService
|
||||
from app.services.subscription_service import SubscriptionService
|
||||
from app.utils.pricing_utils import (
|
||||
calculate_months_from_days,
|
||||
@@ -349,6 +350,8 @@ async def activate_trial(
|
||||
db_user: User,
|
||||
db: AsyncSession
|
||||
):
|
||||
from app.services.admin_notification_service import AdminNotificationService
|
||||
|
||||
texts = get_texts(db_user.language)
|
||||
|
||||
if db_user.subscription or db_user.has_had_paid_subscription:
|
||||
@@ -371,6 +374,12 @@ async def activate_trial(
|
||||
|
||||
await db.refresh(db_user)
|
||||
|
||||
try:
|
||||
notification_service = AdminNotificationService(callback.bot)
|
||||
await notification_service.send_trial_activation_notification(db_user, subscription)
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка отправки уведомления о триале: {e}")
|
||||
|
||||
if remnawave_user and hasattr(subscription, 'subscription_url') and subscription.subscription_url:
|
||||
trial_success_text = f"{texts.TRIAL_ACTIVATED}\n\n"
|
||||
trial_success_text += f"🔗 <b>Ваша ссылка для импорта в VPN приложениe:</b>\n"
|
||||
@@ -412,8 +421,6 @@ async def activate_trial(
|
||||
[InlineKeyboardButton(text="⬅️ В главное меню", callback_data="back_to_menu")],
|
||||
])
|
||||
|
||||
|
||||
|
||||
await callback.message.edit_text(
|
||||
trial_success_text,
|
||||
reply_markup=connect_keyboard,
|
||||
@@ -1083,6 +1090,7 @@ async def confirm_extend_subscription(
|
||||
db: AsyncSession
|
||||
):
|
||||
from app.utils.pricing_utils import calculate_months_from_days, validate_pricing_calculation
|
||||
from app.services.admin_notification_service import AdminNotificationService
|
||||
|
||||
days = int(callback.data.split('_')[2])
|
||||
texts = get_texts(db_user.language)
|
||||
@@ -1094,6 +1102,8 @@ async def confirm_extend_subscription(
|
||||
|
||||
months_in_period = calculate_months_from_days(days)
|
||||
|
||||
old_end_date = subscription.end_date
|
||||
|
||||
try:
|
||||
from app.config import PERIOD_PRICES
|
||||
|
||||
@@ -1124,7 +1134,7 @@ async def confirm_extend_subscription(
|
||||
|
||||
logger.info(f"💰 Расчет продления подписки {subscription.id} на {days} дней ({months_in_period} мес):")
|
||||
logger.info(f" 📅 Период {days} дней: {base_price/100}₽")
|
||||
logger.info(f" 🌍 Серверы: {servers_price_per_month/100}₽/мес × {months_in_period} = {total_servers_price/100}₽")
|
||||
logger.info(f" 🌐 Серверы: {servers_price_per_month/100}₽/мес × {months_in_period} = {total_servers_price/100}₽")
|
||||
logger.info(f" 📱 Устройства: {devices_price_per_month/100}₽/мес × {months_in_period} = {total_devices_price/100}₽")
|
||||
logger.info(f" 📊 Трафик: {traffic_price_per_month/100}₽/мес × {months_in_period} = {total_traffic_price/100}₽")
|
||||
logger.info(f" 💎 ИТОГО: {price/100}₽")
|
||||
@@ -1179,7 +1189,7 @@ async def confirm_extend_subscription(
|
||||
except Exception as e:
|
||||
logger.error(f"⚠ ИСКЛЮЧЕНИЕ ПРИ ОБНОВЛЕНИИ REMNAWAVE: {e}")
|
||||
|
||||
await create_transaction(
|
||||
transaction = await create_transaction(
|
||||
db=db,
|
||||
user_id=db_user.id,
|
||||
type=TransactionType.SUBSCRIPTION_PAYMENT,
|
||||
@@ -1187,6 +1197,13 @@ async def confirm_extend_subscription(
|
||||
description=f"Продление подписки на {days} дней ({months_in_period} мес)"
|
||||
)
|
||||
|
||||
try:
|
||||
notification_service = AdminNotificationService(callback.bot)
|
||||
await notification_service.send_subscription_extension_notification(
|
||||
db_user, subscription, transaction, days, old_end_date
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка отправки уведомления о продлении: {e}")
|
||||
|
||||
await callback.message.edit_text(
|
||||
f"✅ Подписка успешно продлена!\n\n"
|
||||
@@ -1726,6 +1743,7 @@ async def confirm_purchase(
|
||||
db: AsyncSession
|
||||
):
|
||||
from app.utils.pricing_utils import calculate_months_from_days, validate_pricing_calculation
|
||||
from app.services.admin_notification_service import AdminNotificationService
|
||||
|
||||
data = await state.get_data()
|
||||
texts = get_texts(db_user.language)
|
||||
@@ -1800,12 +1818,14 @@ async def confirm_purchase(
|
||||
return
|
||||
|
||||
existing_subscription = db_user.subscription
|
||||
was_trial_conversion = False
|
||||
|
||||
if existing_subscription:
|
||||
logger.info(f"Обновляем существующую подписку пользователя {db_user.telegram_id}")
|
||||
|
||||
if existing_subscription.is_trial:
|
||||
logger.info(f"Конверсия из триала в платную для пользователя {db_user.telegram_id}")
|
||||
was_trial_conversion = True
|
||||
|
||||
trial_duration = (datetime.utcnow() - existing_subscription.start_date).days
|
||||
|
||||
@@ -1877,7 +1897,7 @@ async def confirm_purchase(
|
||||
logger.error(f"Не удалось создать/обновить RemnaWave пользователя для {db_user.telegram_id}")
|
||||
remnawave_user = await subscription_service.create_remnawave_user(db, subscription)
|
||||
|
||||
await create_transaction(
|
||||
transaction = await create_transaction(
|
||||
db=db,
|
||||
user_id=db_user.id,
|
||||
type=TransactionType.SUBSCRIPTION_PAYMENT,
|
||||
@@ -1885,6 +1905,14 @@ async def confirm_purchase(
|
||||
description=f"Подписка на {data['period_days']} дней ({months_in_period} мес)"
|
||||
)
|
||||
|
||||
try:
|
||||
notification_service = AdminNotificationService(callback.bot)
|
||||
await notification_service.send_subscription_purchase_notification(
|
||||
db_user, subscription, transaction, data['period_days'], was_trial_conversion
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка отправки уведомления о покупке: {e}")
|
||||
|
||||
await db.refresh(db_user)
|
||||
await db.refresh(subscription)
|
||||
|
||||
@@ -2966,6 +2994,57 @@ def get_reset_devices_confirm_keyboard(language: str = "ru") -> InlineKeyboardMa
|
||||
]
|
||||
])
|
||||
|
||||
async def send_trial_notification(callback: types.CallbackQuery, db_user: User, subscription: Subscription):
|
||||
try:
|
||||
notification_service = AdminNotificationService(callback.bot)
|
||||
await notification_service.send_trial_activation_notification(db_user, subscription)
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка отправки уведомления о триале: {e}")
|
||||
|
||||
async def send_purchase_notification(
|
||||
callback: types.CallbackQuery,
|
||||
db_user: User,
|
||||
subscription: Subscription,
|
||||
transaction_id: int,
|
||||
period_days: int,
|
||||
was_trial_conversion: bool = False
|
||||
):
|
||||
try:
|
||||
from app.database.crud.transaction import get_transaction_by_id
|
||||
from app.database.database import AsyncSessionLocal
|
||||
|
||||
async with AsyncSessionLocal() as db:
|
||||
transaction = await get_transaction_by_id(db, transaction_id)
|
||||
if transaction:
|
||||
notification_service = AdminNotificationService(callback.bot)
|
||||
await notification_service.send_subscription_purchase_notification(
|
||||
db_user, subscription, transaction, period_days, was_trial_conversion
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка отправки уведомления о покупке: {e}")
|
||||
|
||||
async def send_extension_notification(
|
||||
callback: types.CallbackQuery,
|
||||
db_user: User,
|
||||
subscription: Subscription,
|
||||
transaction_id: int,
|
||||
extended_days: int,
|
||||
old_end_date: datetime
|
||||
):
|
||||
try:
|
||||
from app.database.crud.transaction import get_transaction_by_id
|
||||
from app.database.database import AsyncSessionLocal
|
||||
|
||||
async with AsyncSessionLocal() as db:
|
||||
transaction = await get_transaction_by_id(db, transaction_id)
|
||||
if transaction:
|
||||
notification_service = AdminNotificationService(callback.bot)
|
||||
await notification_service.send_subscription_extension_notification(
|
||||
db_user, subscription, transaction, extended_days, old_end_date
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка отправки уведомления о продлении: {e}")
|
||||
|
||||
|
||||
def register_handlers(dp: Dispatcher):
|
||||
update_traffic_prices()
|
||||
|
||||
@@ -0,0 +1,277 @@
|
||||
import logging
|
||||
from typing import Optional, Dict, Any
|
||||
from datetime import datetime
|
||||
from aiogram import Bot
|
||||
from aiogram.exceptions import TelegramBadRequest, TelegramForbiddenError
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from app.config import settings
|
||||
from app.database.models import User, Subscription, Transaction
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class AdminNotificationService:
|
||||
|
||||
def __init__(self, bot: Bot):
|
||||
self.bot = bot
|
||||
self.chat_id = getattr(settings, 'ADMIN_NOTIFICATIONS_CHAT_ID', None)
|
||||
self.topic_id = getattr(settings, 'ADMIN_NOTIFICATIONS_TOPIC_ID', None)
|
||||
self.enabled = getattr(settings, 'ADMIN_NOTIFICATIONS_ENABLED', False)
|
||||
|
||||
async def send_trial_activation_notification(
|
||||
self,
|
||||
user: User,
|
||||
subscription: Subscription
|
||||
) -> bool:
|
||||
if not self._is_enabled():
|
||||
return False
|
||||
|
||||
try:
|
||||
user_status = "🆕 Новый" if not user.has_had_paid_subscription else "🔄 Существующий"
|
||||
|
||||
message = f"""🎯 <b>АКТИВАЦИЯ ТРИАЛА</b>
|
||||
|
||||
👤 <b>Пользователь:</b> {user.full_name}
|
||||
🆔 <b>Telegram ID:</b> <code>{user.telegram_id}</code>
|
||||
📱 <b>Username:</b> @{user.username or 'отсутствует'}
|
||||
👥 <b>Статус:</b> {user_status}
|
||||
|
||||
⏰ <b>Параметры триала:</b>
|
||||
📅 Период: {settings.TRIAL_DURATION_DAYS} дней
|
||||
📊 Трафик: {settings.TRIAL_TRAFFIC_LIMIT_GB} ГБ
|
||||
📱 Устройства: {settings.TRIAL_DEVICE_LIMIT}
|
||||
🌐 Сервер: {subscription.connected_squads[0] if subscription.connected_squads else 'По умолчанию'}
|
||||
|
||||
📆 <b>Действует до:</b> {subscription.end_date.strftime('%d.%m.%Y %H:%M')}
|
||||
🔗 <b>Реферер:</b> {f'ID {user.referred_by_id}' if user.referred_by_id else 'Нет'}
|
||||
|
||||
⏰ <i>{datetime.now().strftime('%d.%m.%Y %H:%M:%S')}</i>"""
|
||||
|
||||
return await self._send_message(message)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка отправки уведомления о триале: {e}")
|
||||
return False
|
||||
|
||||
async def send_subscription_purchase_notification(
|
||||
self,
|
||||
user: User,
|
||||
subscription: Subscription,
|
||||
transaction: Transaction,
|
||||
period_days: int,
|
||||
was_trial_conversion: bool = False
|
||||
) -> bool:
|
||||
if not self._is_enabled():
|
||||
return False
|
||||
|
||||
try:
|
||||
event_type = "🔄 КОНВЕРСИЯ ИЗ ТРИАЛА" if was_trial_conversion else "💎 ПОКУПКА ПОДПИСКИ"
|
||||
|
||||
if was_trial_conversion:
|
||||
user_status = "🎯 Конверсия из триала"
|
||||
elif user.has_had_paid_subscription:
|
||||
user_status = "🔄 Продление/Обновление"
|
||||
else:
|
||||
user_status = "🆕 Первая покупка"
|
||||
|
||||
servers_info = await self._get_servers_info(subscription.connected_squads)
|
||||
|
||||
payment_method = self._get_payment_method_display(transaction.payment_method)
|
||||
|
||||
message = f"""💎 <b>{event_type}</b>
|
||||
|
||||
👤 <b>Пользователь:</b> {user.full_name}
|
||||
🆔 <b>Telegram ID:</b> <code>{user.telegram_id}</code>
|
||||
📱 <b>Username:</b> @{user.username or 'отсутствует'}
|
||||
👥 <b>Статус:</b> {user_status}
|
||||
|
||||
💰 <b>Платеж:</b>
|
||||
💵 Сумма: {settings.format_price(transaction.amount_kopeks)}
|
||||
💳 Способ: {payment_method}
|
||||
🆔 ID транзакции: {transaction.id}
|
||||
|
||||
📱 <b>Параметры подписки:</b>
|
||||
📅 Период: {period_days} дней
|
||||
📊 Трафик: {self._format_traffic(subscription.traffic_limit_gb)}
|
||||
📱 Устройства: {subscription.device_limit}
|
||||
🌐 Серверы: {servers_info}
|
||||
|
||||
📆 <b>Действует до:</b> {subscription.end_date.strftime('%d.%m.%Y %H:%M')}
|
||||
💰 <b>Баланс после покупки:</b> {settings.format_price(user.balance_kopeks)}
|
||||
🔗 <b>Реферер:</b> {f'ID {user.referred_by_id}' if user.referred_by_id else 'Нет'}
|
||||
|
||||
⏰ <i>{datetime.now().strftime('%d.%m.%Y %H:%M:%S')}</i>"""
|
||||
|
||||
return await self._send_message(message)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка отправки уведомления о покупке: {e}")
|
||||
return False
|
||||
|
||||
async def send_balance_topup_notification(
|
||||
self,
|
||||
user: User,
|
||||
transaction: Transaction,
|
||||
old_balance: int
|
||||
) -> bool:
|
||||
if not self._is_enabled():
|
||||
return False
|
||||
|
||||
try:
|
||||
topup_status = "🆕 Первое пополнение" if not user.has_made_first_topup else "🔄 Пополнение"
|
||||
|
||||
payment_method = self._get_payment_method_display(transaction.payment_method)
|
||||
|
||||
balance_change = user.balance_kopeks - old_balance
|
||||
|
||||
message = f"""💰 <b>ПОПОЛНЕНИЕ БАЛАНСА</b>
|
||||
|
||||
👤 <b>Пользователь:</b> {user.full_name}
|
||||
🆔 <b>Telegram ID:</b> <code>{user.telegram_id}</code>
|
||||
📱 <b>Username:</b> @{user.username or 'отсутствует'}
|
||||
💳 <b>Статус:</b> {topup_status}
|
||||
|
||||
💰 <b>Детали пополнения:</b>
|
||||
💵 Сумма: {settings.format_price(transaction.amount_kopeks)}
|
||||
💳 Способ: {payment_method}
|
||||
🆔 ID транзакции: {transaction.id}
|
||||
|
||||
💰 <b>Баланс:</b>
|
||||
📉 Было: {settings.format_price(old_balance)}
|
||||
📈 Стало: {settings.format_price(user.balance_kopeks)}
|
||||
➕ Изменение: +{settings.format_price(balance_change)}
|
||||
|
||||
🔗 <b>Реферер:</b> {f'ID {user.referred_by_id}' if user.referred_by_id else 'Нет'}
|
||||
📱 <b>Подписка:</b> {self._get_subscription_status(user)}
|
||||
|
||||
⏰ <i>{datetime.now().strftime('%d.%m.%Y %H:%M:%S')}</i>"""
|
||||
|
||||
return await self._send_message(message)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка отправки уведомления о пополнении: {e}")
|
||||
return False
|
||||
|
||||
async def send_subscription_extension_notification(
|
||||
self,
|
||||
user: User,
|
||||
subscription: Subscription,
|
||||
transaction: Transaction,
|
||||
extended_days: int,
|
||||
old_end_date: datetime
|
||||
) -> bool:
|
||||
if not self._is_enabled():
|
||||
return False
|
||||
|
||||
try:
|
||||
payment_method = self._get_payment_method_display(transaction.payment_method)
|
||||
|
||||
servers_info = await self._get_servers_info(subscription.connected_squads)
|
||||
|
||||
message = f"""⏰ <b>ПРОДЛЕНИЕ ПОДПИСКИ</b>
|
||||
|
||||
👤 <b>Пользователь:</b> {user.full_name}
|
||||
🆔 <b>Telegram ID:</b> <code>{user.telegram_id}</code>
|
||||
📱 <b>Username:</b> @{user.username or 'отсутствует'}
|
||||
|
||||
💰 <b>Платеж:</b>
|
||||
💵 Сумма: {settings.format_price(transaction.amount_kopeks)}
|
||||
💳 Способ: {payment_method}
|
||||
🆔 ID транзакции: {transaction.id}
|
||||
|
||||
📅 <b>Продление:</b>
|
||||
➕ Добавлено дней: {extended_days}
|
||||
📆 Было до: {old_end_date.strftime('%d.%m.%Y %H:%M')}
|
||||
📆 Стало до: {subscription.end_date.strftime('%d.%m.%Y %H:%M')}
|
||||
|
||||
📱 <b>Текущие параметры:</b>
|
||||
📊 Трафик: {self._format_traffic(subscription.traffic_limit_gb)}
|
||||
📱 Устройства: {subscription.device_limit}
|
||||
🌐 Серверы: {servers_info}
|
||||
|
||||
💰 <b>Баланс после операции:</b> {settings.format_price(user.balance_kopeks)}
|
||||
|
||||
⏰ <i>{datetime.now().strftime('%d.%m.%Y %H:%M:%S')}</i>"""
|
||||
|
||||
return await self._send_message(message)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка отправки уведомления о продлении: {e}")
|
||||
return False
|
||||
|
||||
async def _send_message(self, text: str) -> bool:
|
||||
if not self.chat_id:
|
||||
logger.warning("ADMIN_NOTIFICATIONS_CHAT_ID не настроен")
|
||||
return False
|
||||
|
||||
try:
|
||||
message_kwargs = {
|
||||
'chat_id': self.chat_id,
|
||||
'text': text,
|
||||
'parse_mode': 'HTML',
|
||||
'disable_web_page_preview': True
|
||||
}
|
||||
|
||||
if self.topic_id:
|
||||
message_kwargs['message_thread_id'] = self.topic_id
|
||||
|
||||
await self.bot.send_message(**message_kwargs)
|
||||
logger.info(f"Уведомление отправлено в чат {self.chat_id}")
|
||||
return True
|
||||
|
||||
except TelegramForbiddenError:
|
||||
logger.error(f"Бот не имеет прав для отправки в чат {self.chat_id}")
|
||||
return False
|
||||
except TelegramBadRequest as e:
|
||||
logger.error(f"Ошибка отправки уведомления: {e}")
|
||||
return False
|
||||
except Exception as e:
|
||||
logger.error(f"Неожиданная ошибка при отправке уведомления: {e}")
|
||||
return False
|
||||
|
||||
def _is_enabled(self) -> bool:
|
||||
return self.enabled and bool(self.chat_id)
|
||||
|
||||
def _get_payment_method_display(self, payment_method: Optional[str]) -> str:
|
||||
method_names = {
|
||||
'telegram_stars': '⭐ Telegram Stars',
|
||||
'yookassa': '💳 YooKassa (карта)',
|
||||
'tribute': '💎 Tribute (карта)',
|
||||
'manual': '🛠️ Вручную (админ)',
|
||||
'balance': '💰 С баланса'
|
||||
}
|
||||
|
||||
if not payment_method:
|
||||
return '💰 С баланса'
|
||||
|
||||
return method_names.get(payment_method, f'💰 С баланса')
|
||||
|
||||
def _format_traffic(self, traffic_gb: int) -> str:
|
||||
if traffic_gb == 0:
|
||||
return "∞ Безлимит"
|
||||
return f"{traffic_gb} ГБ"
|
||||
|
||||
def _get_subscription_status(self, user: User) -> str:
|
||||
if not user.subscription:
|
||||
return "❌ Нет подписки"
|
||||
|
||||
sub = user.subscription
|
||||
if sub.is_trial:
|
||||
return f"🎯 Триал (до {sub.end_date.strftime('%d.%m')})"
|
||||
elif sub.is_active:
|
||||
return f"✅ Активна (до {sub.end_date.strftime('%d.%m')})"
|
||||
else:
|
||||
return "❌ Неактивна"
|
||||
|
||||
async def _get_servers_info(self, squad_uuids: list) -> str:
|
||||
if not squad_uuids:
|
||||
return "❌ Нет серверов"
|
||||
|
||||
try:
|
||||
from app.handlers.subscription import get_servers_display_names
|
||||
servers_names = await get_servers_display_names(squad_uuids)
|
||||
return f"{len(squad_uuids)} шт. ({servers_names})"
|
||||
except Exception as e:
|
||||
logger.warning(f"Не удалось получить названия серверов: {e}")
|
||||
return f"{len(squad_uuids)} шт."
|
||||
@@ -81,6 +81,7 @@ class PaymentService:
|
||||
user = await get_user_by_id(db, user_id)
|
||||
if user:
|
||||
old_balance = user.balance_kopeks
|
||||
|
||||
user.balance_kopeks += amount_kopeks
|
||||
user.updated_at = datetime.utcnow()
|
||||
|
||||
@@ -93,7 +94,7 @@ class PaymentService:
|
||||
logger.info(f"🔍 Проверка реферальной логики для описания: '{description_for_referral}'")
|
||||
|
||||
if any(word in description_for_referral.lower() for word in ["пополнение", "stars", "yookassa", "topup"]) and not any(word in description_for_referral.lower() for word in ["комиссия", "бонус"]):
|
||||
logger.info(f"📞 Вызов process_referral_topup для пользователя {user_id}")
|
||||
logger.info(f"🔞 Вызов process_referral_topup для пользователя {user_id}")
|
||||
try:
|
||||
from app.services.referral_service import process_referral_topup
|
||||
await process_referral_topup(db, user_id, amount_kopeks, self.bot)
|
||||
@@ -102,6 +103,16 @@ class PaymentService:
|
||||
else:
|
||||
logger.info(f"❌ Описание '{description_for_referral}' не подходит для реферальной логики")
|
||||
|
||||
if self.bot:
|
||||
try:
|
||||
from app.services.admin_notification_service import AdminNotificationService
|
||||
notification_service = AdminNotificationService(self.bot)
|
||||
await notification_service.send_balance_topup_notification(
|
||||
user, transaction, old_balance
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка отправки уведомления о пополнении Stars: {e}")
|
||||
|
||||
if self.bot:
|
||||
try:
|
||||
await self.bot.send_message(
|
||||
@@ -109,7 +120,7 @@ class PaymentService:
|
||||
f"✅ <b>Пополнение успешно!</b>\n\n"
|
||||
f"⭐ Звезд: {stars_amount}\n"
|
||||
f"💰 Сумма: {settings.format_price(amount_kopeks)}\n"
|
||||
f"🏦 Способ: Telegram Stars\n"
|
||||
f"🦊 Способ: Telegram Stars\n"
|
||||
f"🆔 Транзакция: {telegram_payment_charge_id[:8]}...\n\n"
|
||||
f"Баланс пополнен автоматически!",
|
||||
parse_mode="HTML"
|
||||
@@ -268,6 +279,7 @@ class PaymentService:
|
||||
user = await get_user_by_id(db, updated_payment.user_id)
|
||||
if user:
|
||||
old_balance = user.balance_kopeks
|
||||
|
||||
user.balance_kopeks += updated_payment.amount_kopeks
|
||||
user.updated_at = datetime.utcnow()
|
||||
|
||||
@@ -280,13 +292,23 @@ class PaymentService:
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка обработки реферального пополнения YooKassa: {e}")
|
||||
|
||||
if self.bot:
|
||||
try:
|
||||
from app.services.admin_notification_service import AdminNotificationService
|
||||
notification_service = AdminNotificationService(self.bot)
|
||||
await notification_service.send_balance_topup_notification(
|
||||
user, transaction, old_balance
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка отправки уведомления о пополнении YooKassa: {e}")
|
||||
|
||||
if self.bot:
|
||||
try:
|
||||
await self.bot.send_message(
|
||||
user.telegram_id,
|
||||
f"✅ <b>Пополнение успешно!</b>\n\n"
|
||||
f"💰 Сумма: {settings.format_price(updated_payment.amount_kopeks)}\n"
|
||||
f"🏦 Способ: Банковская карта\n"
|
||||
f"🦊 Способ: Банковская карта\n"
|
||||
f"🆔 Транзакция: {yookassa_payment_id[:8]}...\n\n"
|
||||
f"Баланс пополнен автоматически!",
|
||||
parse_mode="HTML"
|
||||
|
||||
@@ -242,9 +242,9 @@ class UserService:
|
||||
from app.services.subscription_service import SubscriptionService
|
||||
subscription_service = SubscriptionService()
|
||||
await subscription_service.disable_remnawave_user(user.remnawave_uuid)
|
||||
logger.info(f" ✅ RemnaWave пользователь {user.remnawave_uuid} деактивирован")
|
||||
logger.info(f"✅ RemnaWave пользователь {user.remnawave_uuid} деактивирован")
|
||||
except Exception as e:
|
||||
logger.warning(f" ⚠️ Ошибка деактивации RemnaWave: {e}")
|
||||
logger.warning(f"⚠️ Ошибка деактивации RemnaWave: {e}")
|
||||
|
||||
try:
|
||||
from app.database.models import YooKassaPayment
|
||||
@@ -256,14 +256,14 @@ class UserService:
|
||||
yookassa_payments = yookassa_result.scalars().all()
|
||||
|
||||
if yookassa_payments:
|
||||
logger.info(f" 🔄 Удаляем {len(yookassa_payments)} YooKassa платежей")
|
||||
logger.info(f"🔄 Удаляем {len(yookassa_payments)} YooKassa платежей")
|
||||
await db.execute(
|
||||
delete(YooKassaPayment).where(YooKassaPayment.user_id == user_id)
|
||||
)
|
||||
await db.flush()
|
||||
logger.info(f" ✅ YooKassa платежи удалены")
|
||||
logger.info(f"✅ YooKassa платежи удалены")
|
||||
except Exception as e:
|
||||
logger.error(f" ❌ Ошибка удаления YooKassa платежей: {e}")
|
||||
logger.error(f"❌ Ошибка удаления YooKassa платежей: {e}")
|
||||
|
||||
try:
|
||||
transactions_result = await db.execute(
|
||||
@@ -272,41 +272,41 @@ class UserService:
|
||||
transactions = transactions_result.scalars().all()
|
||||
|
||||
if transactions:
|
||||
logger.info(f" 🔄 Удаляем {len(transactions)} транзакций")
|
||||
logger.info(f"🔄 Удаляем {len(transactions)} транзакций")
|
||||
await db.execute(
|
||||
delete(Transaction).where(Transaction.user_id == user_id)
|
||||
)
|
||||
await db.flush()
|
||||
logger.info(f" ✅ Транзакции удалены")
|
||||
logger.info(f"✅ Транзакции удалены")
|
||||
except Exception as e:
|
||||
logger.error(f" ❌ Ошибка удаления транзакций: {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}")
|
||||
logger.info(f"🗑️ Удалены использования промокодов пользователя {user_id}")
|
||||
except Exception as e:
|
||||
logger.error(f" ❌ Ошибка удаления использований промокодов: {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}")
|
||||
logger.info(f"🗑️ Удалены реферальные доходы пользователя {user_id}")
|
||||
except Exception as e:
|
||||
logger.error(f" ❌ Ошибка удаления реферальных доходов: {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}")
|
||||
logger.info(f"🗑️ Удалены реферальные записи о пользователе {user_id}")
|
||||
except Exception as e:
|
||||
logger.error(f" ❌ Ошибка удаления реферальных записей: {e}")
|
||||
logger.error(f"❌ Ошибка удаления реферальных записей: {e}")
|
||||
|
||||
try:
|
||||
from app.database.models import BroadcastHistory
|
||||
@@ -314,9 +314,26 @@ class UserService:
|
||||
delete(BroadcastHistory).where(BroadcastHistory.admin_id == user_id)
|
||||
)
|
||||
await db.flush()
|
||||
logger.info(f" 🗑️ Удалена история рассылок админа {user_id}")
|
||||
logger.info(f"🗑️ Удалена история рассылок админа {user_id}")
|
||||
except Exception as e:
|
||||
logger.error(f" ❌ Ошибка удаления истории рассылок: {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:
|
||||
@@ -326,9 +343,9 @@ class UserService:
|
||||
)
|
||||
)
|
||||
await db.flush()
|
||||
logger.info(f" 🗑️ Удалены записи SubscriptionServer для подписки {user.subscription.id}")
|
||||
logger.info(f"🗑️ Удалены записи SubscriptionServer для подписки {user.subscription.id}")
|
||||
except Exception as e:
|
||||
logger.error(f" ❌ Ошибка удаления SubscriptionServer: {e}")
|
||||
logger.error(f"❌ Ошибка удаления SubscriptionServer: {e}")
|
||||
|
||||
if user.subscription:
|
||||
try:
|
||||
@@ -337,9 +354,9 @@ class UserService:
|
||||
delete(Subscription).where(Subscription.user_id == user_id)
|
||||
)
|
||||
await db.flush()
|
||||
logger.info(f" 🗑️ Удалена подписка пользователя {user_id}")
|
||||
logger.info(f"🗑️ Удалена подписка пользователя {user_id}")
|
||||
except Exception as e:
|
||||
logger.error(f" ❌ Ошибка удаления подписки: {e}")
|
||||
logger.error(f"❌ Ошибка удаления подписки: {e}")
|
||||
|
||||
try:
|
||||
from sqlalchemy import update
|
||||
@@ -349,19 +366,19 @@ class UserService:
|
||||
.values(referred_by_id=None)
|
||||
)
|
||||
if referrals_result.rowcount > 0:
|
||||
logger.info(f" 🔗 Очищены реферальные ссылки у {referrals_result.rowcount} рефералов")
|
||||
logger.info(f"🔗 Очищены реферальные ссылки у {referrals_result.rowcount} рефералов")
|
||||
await db.flush()
|
||||
except Exception as e:
|
||||
logger.error(f" ❌ Ошибка очистки реферальных ссылок: {e}")
|
||||
logger.error(f"❌ Ошибка очистки реферальных ссылок: {e}")
|
||||
|
||||
try:
|
||||
await db.execute(
|
||||
delete(User).where(User.id == user_id)
|
||||
)
|
||||
await db.commit()
|
||||
logger.info(f" ✅ Пользователь {user_id} окончательно удален из базы")
|
||||
logger.info(f"✅ Пользователь {user_id} окончательно удален из базы")
|
||||
except Exception as e:
|
||||
logger.error(f" ❌ Ошибка финального удаления пользователя: {e}")
|
||||
logger.error(f"❌ Ошибка финального удаления пользователя: {e}")
|
||||
await db.rollback()
|
||||
return False
|
||||
|
||||
|
||||
Reference in New Issue
Block a user