Compare commits
10 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| be00256618 | |||
| 7f693f2b58 | |||
| c3bf0dc0fd | |||
| d651a6c02f | |||
| d7039d75a4 | |||
| 6409b0c023 | |||
| af31c551d2 | |||
| a38dfcb75a | |||
| b7b83abb72 | |||
| f076269c32 |
@@ -1,3 +1,3 @@
|
||||
{
|
||||
".": "3.16.2"
|
||||
".": "3.16.3"
|
||||
}
|
||||
|
||||
@@ -1,5 +1,18 @@
|
||||
# Changelog
|
||||
|
||||
## [3.16.3](https://github.com/BEDOLAGA-DEV/remnawave-bedolaga-telegram-bot/compare/v3.16.2...v3.16.3) (2026-02-18)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* 3 user deletion bugs — type cast, inner savepoint, lazy load ([af31c55](https://github.com/BEDOLAGA-DEV/remnawave-bedolaga-telegram-bot/commit/af31c551d2f23ef01425bdb2db8f255dbc3047e2))
|
||||
* auth middleware catches all commit errors, not just connection errors ([6409b0c](https://github.com/BEDOLAGA-DEV/remnawave-bedolaga-telegram-bot/commit/6409b0c023cd7957c43d5c1c3d83e671ccaf959c))
|
||||
* connected_squads stores UUIDs, not int IDs — use get_server_ids_by_uuids ([d7039d7](https://github.com/BEDOLAGA-DEV/remnawave-bedolaga-telegram-bot/commit/d7039d75a47fbf67436a9d39f2cd9f65f2646544))
|
||||
* deadlock on user deletion + robust migration 0002 ([b7b83ab](https://github.com/BEDOLAGA-DEV/remnawave-bedolaga-telegram-bot/commit/b7b83abb723913b3167e7462ff592a374c3f421b))
|
||||
* eliminate deadlock by matching lock order with webhook ([d651a6c](https://github.com/BEDOLAGA-DEV/remnawave-bedolaga-telegram-bot/commit/d651a6c02f501b7a0ded570f2db6addcc16173a9))
|
||||
* make migration 0002 robust with table existence checks ([f076269](https://github.com/BEDOLAGA-DEV/remnawave-bedolaga-telegram-bot/commit/f076269c323726c683a38db092d907591a26e647))
|
||||
* wrap user deletion steps in savepoints to prevent transaction cascade abort ([a38dfcb](https://github.com/BEDOLAGA-DEV/remnawave-bedolaga-telegram-bot/commit/a38dfcb75a47a185d979a8202f637d8b79812e67))
|
||||
|
||||
## [3.16.2](https://github.com/BEDOLAGA-DEV/remnawave-bedolaga-telegram-bot/compare/v3.16.1...v3.16.2) (2026-02-18)
|
||||
|
||||
|
||||
|
||||
+1
-1
@@ -14,7 +14,7 @@ RUN pip install --no-cache-dir --upgrade pip && \
|
||||
|
||||
FROM python:3.13-slim
|
||||
|
||||
ARG VERSION="v3.16.2" # x-release-please-version
|
||||
ARG VERSION="v3.16.3" # x-release-please-version
|
||||
ARG BUILD_DATE
|
||||
ARG VCS_REF
|
||||
|
||||
|
||||
@@ -1771,15 +1771,18 @@ async def full_delete_user(
|
||||
panel_error: str | None = None
|
||||
deleted_from_panel = False
|
||||
|
||||
# Pre-fetch admin.id to avoid MissingGreenlet after transaction rollback
|
||||
admin_id_val = admin.id
|
||||
|
||||
# UserService.delete_user_account handles both bot DB and Remnawave panel
|
||||
user_service = UserService()
|
||||
success = await user_service.delete_user_account(db, user_id, admin.id)
|
||||
success = await user_service.delete_user_account(db, user_id, admin_id_val)
|
||||
|
||||
if success:
|
||||
deleted_from_panel = request.delete_from_panel and user.remnawave_uuid is not None
|
||||
|
||||
reason_text = f' (reason: {request.reason})' if request.reason else ''
|
||||
logger.info('Admin fully deleted user', admin_id=admin.id, user_id=user_id, reason_text=reason_text)
|
||||
logger.info('Admin fully deleted user', admin_id=admin_id_val, user_id=user_id, reason_text=reason_text)
|
||||
|
||||
return FullDeleteUserResponse(
|
||||
success=success,
|
||||
|
||||
@@ -222,6 +222,13 @@ class AuthMiddleware(BaseMiddleware):
|
||||
except (InterfaceError, OperationalError) as conn_err:
|
||||
# Соединение закрылось (таймаут после долгой операции) - просто логируем
|
||||
logger.warning('⚠️ Соединение с БД закрыто после обработки, пропускаем commit', conn_err=conn_err)
|
||||
except Exception as commit_err:
|
||||
# Transaction aborted (e.g. handler swallowed a ProgrammingError) — rollback
|
||||
logger.warning('⚠️ Не удалось commit после обработки, rollback', commit_err=commit_err)
|
||||
try:
|
||||
await db.rollback()
|
||||
except Exception:
|
||||
pass
|
||||
return result
|
||||
|
||||
except (InterfaceError, OperationalError) as conn_err:
|
||||
|
||||
+273
-235
@@ -9,10 +9,7 @@ from sqlalchemy.orm import selectinload
|
||||
|
||||
from app.config import settings
|
||||
from app.database.crud.promo_group import get_promo_group_by_id
|
||||
from app.database.crud.subscription import (
|
||||
decrement_subscription_server_counts,
|
||||
get_subscription_by_user_id,
|
||||
)
|
||||
from app.database.crud.subscription import get_subscription_by_user_id
|
||||
from app.database.crud.transaction import get_user_transactions_count
|
||||
from app.database.crud.user import (
|
||||
add_user_balance,
|
||||
@@ -796,144 +793,141 @@ class UserService:
|
||||
logger.error('❌ Ошибка деактивации RemnaWave как fallback', fallback_e=fallback_e)
|
||||
|
||||
try:
|
||||
sent_notifications_result = await db.execute(
|
||||
select(SentNotification).where(SentNotification.user_id == user_id)
|
||||
)
|
||||
sent_notifications = sent_notifications_result.scalars().all()
|
||||
async with db.begin_nested():
|
||||
sent_notifications_result = await db.execute(
|
||||
select(SentNotification).where(SentNotification.user_id == user_id)
|
||||
)
|
||||
sent_notifications = sent_notifications_result.scalars().all()
|
||||
|
||||
if sent_notifications:
|
||||
logger.info('🔄 Удаляем уведомлений', sent_notifications_count=len(sent_notifications))
|
||||
await db.execute(delete(SentNotification).where(SentNotification.user_id == user_id))
|
||||
await db.flush()
|
||||
if sent_notifications:
|
||||
logger.info('🔄 Удаляем уведомлений', sent_notifications_count=len(sent_notifications))
|
||||
await db.execute(delete(SentNotification).where(SentNotification.user_id == user_id))
|
||||
await db.flush()
|
||||
except Exception as e:
|
||||
logger.error('❌ Ошибка удаления уведомлений', error=e)
|
||||
|
||||
try:
|
||||
if user.subscription:
|
||||
subscription_servers_result = await db.execute(
|
||||
select(SubscriptionServer).where(SubscriptionServer.subscription_id == user.subscription.id)
|
||||
async with db.begin_nested():
|
||||
user_messages_result = await db.execute(
|
||||
update(UserMessage).where(UserMessage.created_by == user_id).values(created_by=None)
|
||||
)
|
||||
subscription_servers = subscription_servers_result.scalars().all()
|
||||
|
||||
await decrement_subscription_server_counts(
|
||||
db,
|
||||
user.subscription,
|
||||
subscription_servers=subscription_servers,
|
||||
)
|
||||
|
||||
if subscription_servers:
|
||||
logger.info(
|
||||
'🔄 Удаляем связей подписка-сервер', subscription_servers_count=len(subscription_servers)
|
||||
)
|
||||
await db.execute(
|
||||
delete(SubscriptionServer).where(SubscriptionServer.subscription_id == user.subscription.id)
|
||||
)
|
||||
await db.flush()
|
||||
except Exception as e:
|
||||
logger.error('❌ Ошибка удаления связей подписка-сервер', error=e)
|
||||
|
||||
try:
|
||||
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('🔄 Обновлено пользовательских сообщений', rowcount=user_messages_result.rowcount)
|
||||
await db.flush()
|
||||
if user_messages_result.rowcount > 0:
|
||||
logger.info('🔄 Обновлено пользовательских сообщений', rowcount=user_messages_result.rowcount)
|
||||
await db.flush()
|
||||
except Exception as e:
|
||||
logger.error('❌ Ошибка обновления пользовательских сообщений', error=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('🔄 Обновлено промокодов', rowcount=promocodes_result.rowcount)
|
||||
await db.flush()
|
||||
async with db.begin_nested():
|
||||
promocodes_result = await db.execute(
|
||||
update(PromoCode).where(PromoCode.created_by == user_id).values(created_by=None)
|
||||
)
|
||||
if promocodes_result.rowcount > 0:
|
||||
logger.info('🔄 Обновлено промокодов', rowcount=promocodes_result.rowcount)
|
||||
await db.flush()
|
||||
except Exception as e:
|
||||
logger.error('❌ Ошибка обновления промокодов', error=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('🔄 Обновлено приветственных текстов', rowcount=welcome_texts_result.rowcount)
|
||||
await db.flush()
|
||||
async with db.begin_nested():
|
||||
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('🔄 Обновлено приветственных текстов', rowcount=welcome_texts_result.rowcount)
|
||||
await db.flush()
|
||||
except Exception as e:
|
||||
logger.error('❌ Ошибка обновления приветственных текстов', error=e)
|
||||
|
||||
try:
|
||||
referrals_result = await db.execute(
|
||||
update(User).where(User.referred_by_id == user_id).values(referred_by_id=None)
|
||||
)
|
||||
if referrals_result.rowcount > 0:
|
||||
logger.info('🔗 Очищены реферальные ссылки у рефералов', rowcount=referrals_result.rowcount)
|
||||
await db.flush()
|
||||
async with db.begin_nested():
|
||||
referrals_result = await db.execute(
|
||||
update(User).where(User.referred_by_id == user_id).values(referred_by_id=None)
|
||||
)
|
||||
if referrals_result.rowcount > 0:
|
||||
logger.info('🔗 Очищены реферальные ссылки у рефералов', rowcount=referrals_result.rowcount)
|
||||
await db.flush()
|
||||
except Exception as e:
|
||||
logger.error('❌ Ошибка очистки реферальных ссылок', error=e)
|
||||
|
||||
try:
|
||||
yookassa_result = await db.execute(select(YooKassaPayment).where(YooKassaPayment.user_id == user_id))
|
||||
yookassa_payments = yookassa_result.scalars().all()
|
||||
|
||||
if yookassa_payments:
|
||||
logger.info('🔄 Удаляем YooKassa платежей', yookassa_payments_count=len(yookassa_payments))
|
||||
await db.execute(
|
||||
update(YooKassaPayment).where(YooKassaPayment.user_id == user_id).values(transaction_id=None)
|
||||
async with db.begin_nested():
|
||||
yookassa_result = await db.execute(
|
||||
select(YooKassaPayment).where(YooKassaPayment.user_id == user_id)
|
||||
)
|
||||
await db.flush()
|
||||
await db.execute(delete(YooKassaPayment).where(YooKassaPayment.user_id == user_id))
|
||||
await db.flush()
|
||||
yookassa_payments = yookassa_result.scalars().all()
|
||||
|
||||
if yookassa_payments:
|
||||
logger.info('🔄 Удаляем YooKassa платежей', yookassa_payments_count=len(yookassa_payments))
|
||||
await db.execute(
|
||||
update(YooKassaPayment)
|
||||
.where(YooKassaPayment.user_id == user_id)
|
||||
.values(transaction_id=None)
|
||||
)
|
||||
await db.flush()
|
||||
await db.execute(delete(YooKassaPayment).where(YooKassaPayment.user_id == user_id))
|
||||
await db.flush()
|
||||
except Exception as e:
|
||||
logger.error('❌ Ошибка удаления YooKassa платежей', error=e)
|
||||
|
||||
try:
|
||||
cryptobot_result = await db.execute(select(CryptoBotPayment).where(CryptoBotPayment.user_id == user_id))
|
||||
cryptobot_payments = cryptobot_result.scalars().all()
|
||||
|
||||
if cryptobot_payments:
|
||||
logger.info('🔄 Удаляем CryptoBot платежей', cryptobot_payments_count=len(cryptobot_payments))
|
||||
await db.execute(
|
||||
update(CryptoBotPayment).where(CryptoBotPayment.user_id == user_id).values(transaction_id=None)
|
||||
async with db.begin_nested():
|
||||
cryptobot_result = await db.execute(
|
||||
select(CryptoBotPayment).where(CryptoBotPayment.user_id == user_id)
|
||||
)
|
||||
await db.flush()
|
||||
await db.execute(delete(CryptoBotPayment).where(CryptoBotPayment.user_id == user_id))
|
||||
await db.flush()
|
||||
cryptobot_payments = cryptobot_result.scalars().all()
|
||||
|
||||
if cryptobot_payments:
|
||||
logger.info('🔄 Удаляем CryptoBot платежей', cryptobot_payments_count=len(cryptobot_payments))
|
||||
await db.execute(
|
||||
update(CryptoBotPayment)
|
||||
.where(CryptoBotPayment.user_id == user_id)
|
||||
.values(transaction_id=None)
|
||||
)
|
||||
await db.flush()
|
||||
await db.execute(delete(CryptoBotPayment).where(CryptoBotPayment.user_id == user_id))
|
||||
await db.flush()
|
||||
except Exception as e:
|
||||
logger.error('❌ Ошибка удаления CryptoBot платежей', error=e)
|
||||
|
||||
try:
|
||||
platega_result = await db.execute(select(PlategaPayment).where(PlategaPayment.user_id == user_id))
|
||||
platega_payments = platega_result.scalars().all()
|
||||
async with db.begin_nested():
|
||||
platega_result = await db.execute(select(PlategaPayment).where(PlategaPayment.user_id == user_id))
|
||||
platega_payments = platega_result.scalars().all()
|
||||
|
||||
if platega_payments:
|
||||
logger.info('🔄 Удаляем Platega платежей', platega_payments_count=len(platega_payments))
|
||||
await db.execute(
|
||||
update(PlategaPayment).where(PlategaPayment.user_id == user_id).values(transaction_id=None)
|
||||
)
|
||||
await db.flush()
|
||||
await db.execute(delete(PlategaPayment).where(PlategaPayment.user_id == user_id))
|
||||
await db.flush()
|
||||
if platega_payments:
|
||||
logger.info('🔄 Удаляем Platega платежей', platega_payments_count=len(platega_payments))
|
||||
await db.execute(
|
||||
update(PlategaPayment).where(PlategaPayment.user_id == user_id).values(transaction_id=None)
|
||||
)
|
||||
await db.flush()
|
||||
await db.execute(delete(PlategaPayment).where(PlategaPayment.user_id == user_id))
|
||||
await db.flush()
|
||||
except Exception as e:
|
||||
logger.error('❌ Ошибка удаления Platega платежей', error=e)
|
||||
|
||||
try:
|
||||
mulenpay_result = await db.execute(select(MulenPayPayment).where(MulenPayPayment.user_id == user_id))
|
||||
mulenpay_payments = mulenpay_result.scalars().all()
|
||||
async with db.begin_nested():
|
||||
mulenpay_result = await db.execute(
|
||||
select(MulenPayPayment).where(MulenPayPayment.user_id == user_id)
|
||||
)
|
||||
mulenpay_payments = mulenpay_result.scalars().all()
|
||||
|
||||
if mulenpay_payments:
|
||||
mulenpay_name = settings.get_mulenpay_display_name()
|
||||
logger.info(
|
||||
'🔄 Удаляем платежей',
|
||||
mulenpay_payments_count=len(mulenpay_payments),
|
||||
mulenpay_name=mulenpay_name,
|
||||
)
|
||||
await db.execute(
|
||||
update(MulenPayPayment).where(MulenPayPayment.user_id == user_id).values(transaction_id=None)
|
||||
)
|
||||
await db.flush()
|
||||
await db.execute(delete(MulenPayPayment).where(MulenPayPayment.user_id == user_id))
|
||||
await db.flush()
|
||||
if mulenpay_payments:
|
||||
mulenpay_name = settings.get_mulenpay_display_name()
|
||||
logger.info(
|
||||
'🔄 Удаляем платежей',
|
||||
mulenpay_payments_count=len(mulenpay_payments),
|
||||
mulenpay_name=mulenpay_name,
|
||||
)
|
||||
await db.execute(
|
||||
update(MulenPayPayment)
|
||||
.where(MulenPayPayment.user_id == user_id)
|
||||
.values(transaction_id=None)
|
||||
)
|
||||
await db.flush()
|
||||
await db.execute(delete(MulenPayPayment).where(MulenPayPayment.user_id == user_id))
|
||||
await db.flush()
|
||||
except Exception as e:
|
||||
logger.error(
|
||||
'❌ Ошибка удаления платежей',
|
||||
@@ -942,206 +936,250 @@ class UserService:
|
||||
)
|
||||
|
||||
try:
|
||||
pal24_result = await db.execute(select(Pal24Payment).where(Pal24Payment.user_id == user_id))
|
||||
pal24_payments = pal24_result.scalars().all()
|
||||
async with db.begin_nested():
|
||||
pal24_result = await db.execute(select(Pal24Payment).where(Pal24Payment.user_id == user_id))
|
||||
pal24_payments = pal24_result.scalars().all()
|
||||
|
||||
if pal24_payments:
|
||||
logger.info('🔄 Удаляем Pal24 платежей', pal24_payments_count=len(pal24_payments))
|
||||
await db.execute(
|
||||
update(Pal24Payment).where(Pal24Payment.user_id == user_id).values(transaction_id=None)
|
||||
)
|
||||
await db.flush()
|
||||
await db.execute(delete(Pal24Payment).where(Pal24Payment.user_id == user_id))
|
||||
await db.flush()
|
||||
if pal24_payments:
|
||||
logger.info('🔄 Удаляем Pal24 платежей', pal24_payments_count=len(pal24_payments))
|
||||
await db.execute(
|
||||
update(Pal24Payment).where(Pal24Payment.user_id == user_id).values(transaction_id=None)
|
||||
)
|
||||
await db.flush()
|
||||
await db.execute(delete(Pal24Payment).where(Pal24Payment.user_id == user_id))
|
||||
await db.flush()
|
||||
except Exception as e:
|
||||
logger.error('❌ Ошибка удаления Pal24 платежей', error=e)
|
||||
|
||||
try:
|
||||
heleket_result = await db.execute(select(HeleketPayment).where(HeleketPayment.user_id == user_id))
|
||||
heleket_payments = heleket_result.scalars().all()
|
||||
async with db.begin_nested():
|
||||
heleket_result = await db.execute(select(HeleketPayment).where(HeleketPayment.user_id == user_id))
|
||||
heleket_payments = heleket_result.scalars().all()
|
||||
|
||||
if heleket_payments:
|
||||
logger.info('🔄 Удаляем Heleket платежей', heleket_payments_count=len(heleket_payments))
|
||||
await db.execute(
|
||||
update(HeleketPayment).where(HeleketPayment.user_id == user_id).values(transaction_id=None)
|
||||
)
|
||||
await db.flush()
|
||||
await db.execute(delete(HeleketPayment).where(HeleketPayment.user_id == user_id))
|
||||
await db.flush()
|
||||
if heleket_payments:
|
||||
logger.info('🔄 Удаляем Heleket платежей', heleket_payments_count=len(heleket_payments))
|
||||
await db.execute(
|
||||
update(HeleketPayment).where(HeleketPayment.user_id == user_id).values(transaction_id=None)
|
||||
)
|
||||
await db.flush()
|
||||
await db.execute(delete(HeleketPayment).where(HeleketPayment.user_id == user_id))
|
||||
await db.flush()
|
||||
except Exception as e:
|
||||
logger.error('❌ Ошибка удаления Heleket платежей', error=e)
|
||||
|
||||
# Удаляем Freekassa платежи
|
||||
try:
|
||||
freekassa_payments_result = await db.execute(
|
||||
select(FreekassaPayment).where(FreekassaPayment.user_id == user_id)
|
||||
)
|
||||
freekassa_payments = freekassa_payments_result.scalars().all()
|
||||
|
||||
if freekassa_payments:
|
||||
logger.info('🔄 Удаляем Freekassa платежей', freekassa_payments_count=len(freekassa_payments))
|
||||
await db.execute(
|
||||
update(FreekassaPayment).where(FreekassaPayment.user_id == user_id).values(transaction_id=None)
|
||||
async with db.begin_nested():
|
||||
freekassa_payments_result = await db.execute(
|
||||
select(FreekassaPayment).where(FreekassaPayment.user_id == user_id)
|
||||
)
|
||||
await db.flush()
|
||||
await db.execute(delete(FreekassaPayment).where(FreekassaPayment.user_id == user_id))
|
||||
await db.flush()
|
||||
freekassa_payments = freekassa_payments_result.scalars().all()
|
||||
|
||||
if freekassa_payments:
|
||||
logger.info('🔄 Удаляем Freekassa платежей', freekassa_payments_count=len(freekassa_payments))
|
||||
await db.execute(
|
||||
update(FreekassaPayment)
|
||||
.where(FreekassaPayment.user_id == user_id)
|
||||
.values(transaction_id=None)
|
||||
)
|
||||
await db.flush()
|
||||
await db.execute(delete(FreekassaPayment).where(FreekassaPayment.user_id == user_id))
|
||||
await db.flush()
|
||||
except Exception as e:
|
||||
logger.error('❌ Ошибка удаления Freekassa платежей', error=e)
|
||||
|
||||
# Удаляем Wata платежи (до транзакций, т.к. wata_payments.transaction_id -> transactions.id)
|
||||
try:
|
||||
wata_payments_result = await db.execute(select(WataPayment).where(WataPayment.user_id == user_id))
|
||||
wata_payments = wata_payments_result.scalars().all()
|
||||
async with db.begin_nested():
|
||||
wata_payments_result = await db.execute(select(WataPayment).where(WataPayment.user_id == user_id))
|
||||
wata_payments = wata_payments_result.scalars().all()
|
||||
|
||||
if wata_payments:
|
||||
logger.info('🔄 Удаляем Wata платежей', wata_payments_count=len(wata_payments))
|
||||
await db.execute(
|
||||
update(WataPayment).where(WataPayment.user_id == user_id).values(transaction_id=None)
|
||||
)
|
||||
await db.flush()
|
||||
await db.execute(delete(WataPayment).where(WataPayment.user_id == user_id))
|
||||
await db.flush()
|
||||
if wata_payments:
|
||||
logger.info('🔄 Удаляем Wata платежей', wata_payments_count=len(wata_payments))
|
||||
await db.execute(
|
||||
update(WataPayment).where(WataPayment.user_id == user_id).values(transaction_id=None)
|
||||
)
|
||||
await db.flush()
|
||||
await db.execute(delete(WataPayment).where(WataPayment.user_id == user_id))
|
||||
await db.flush()
|
||||
except Exception as e:
|
||||
logger.error('❌ Ошибка удаления Wata платежей', error=e)
|
||||
|
||||
# Удаляем CloudPayments платежи
|
||||
try:
|
||||
cloudpayments_result = await db.execute(
|
||||
select(CloudPaymentsPayment).where(CloudPaymentsPayment.user_id == user_id)
|
||||
)
|
||||
cloudpayments_payments = cloudpayments_result.scalars().all()
|
||||
async with db.begin_nested():
|
||||
cloudpayments_result = await db.execute(
|
||||
select(CloudPaymentsPayment).where(CloudPaymentsPayment.user_id == user_id)
|
||||
)
|
||||
cloudpayments_payments = cloudpayments_result.scalars().all()
|
||||
|
||||
if cloudpayments_payments:
|
||||
logger.info(
|
||||
'🔄 Удаляем CloudPayments платежей', cloudpayments_payments_count=len(cloudpayments_payments)
|
||||
)
|
||||
await db.execute(
|
||||
update(CloudPaymentsPayment)
|
||||
.where(CloudPaymentsPayment.user_id == user_id)
|
||||
.values(transaction_id=None)
|
||||
)
|
||||
await db.flush()
|
||||
await db.execute(delete(CloudPaymentsPayment).where(CloudPaymentsPayment.user_id == user_id))
|
||||
await db.flush()
|
||||
if cloudpayments_payments:
|
||||
logger.info(
|
||||
'🔄 Удаляем CloudPayments платежей',
|
||||
cloudpayments_payments_count=len(cloudpayments_payments),
|
||||
)
|
||||
await db.execute(
|
||||
update(CloudPaymentsPayment)
|
||||
.where(CloudPaymentsPayment.user_id == user_id)
|
||||
.values(transaction_id=None)
|
||||
)
|
||||
await db.flush()
|
||||
await db.execute(delete(CloudPaymentsPayment).where(CloudPaymentsPayment.user_id == user_id))
|
||||
await db.flush()
|
||||
except Exception as e:
|
||||
logger.error('❌ Ошибка удаления CloudPayments платежей', error=e)
|
||||
|
||||
# Удаляем KassaAi платежи
|
||||
try:
|
||||
kassa_ai_result = await db.execute(select(KassaAiPayment).where(KassaAiPayment.user_id == user_id))
|
||||
kassa_ai_payments = kassa_ai_result.scalars().all()
|
||||
async with db.begin_nested():
|
||||
kassa_ai_result = await db.execute(select(KassaAiPayment).where(KassaAiPayment.user_id == user_id))
|
||||
kassa_ai_payments = kassa_ai_result.scalars().all()
|
||||
|
||||
if kassa_ai_payments:
|
||||
logger.info('🔄 Удаляем KassaAi платежей', kassa_ai_payments_count=len(kassa_ai_payments))
|
||||
await db.execute(
|
||||
update(KassaAiPayment).where(KassaAiPayment.user_id == user_id).values(transaction_id=None)
|
||||
)
|
||||
await db.flush()
|
||||
await db.execute(delete(KassaAiPayment).where(KassaAiPayment.user_id == user_id))
|
||||
await db.flush()
|
||||
if kassa_ai_payments:
|
||||
logger.info('🔄 Удаляем KassaAi платежей', kassa_ai_payments_count=len(kassa_ai_payments))
|
||||
await db.execute(
|
||||
update(KassaAiPayment).where(KassaAiPayment.user_id == user_id).values(transaction_id=None)
|
||||
)
|
||||
await db.flush()
|
||||
await db.execute(delete(KassaAiPayment).where(KassaAiPayment.user_id == user_id))
|
||||
await db.flush()
|
||||
except Exception as e:
|
||||
logger.error('❌ Ошибка удаления KassaAi платежей', error=e)
|
||||
|
||||
try:
|
||||
transactions_result = await db.execute(select(Transaction).where(Transaction.user_id == user_id))
|
||||
transactions = transactions_result.scalars().all()
|
||||
async with db.begin_nested():
|
||||
transactions_result = await db.execute(select(Transaction).where(Transaction.user_id == user_id))
|
||||
transactions = transactions_result.scalars().all()
|
||||
|
||||
if transactions:
|
||||
logger.info('🔄 Удаляем транзакций', transactions_count=len(transactions))
|
||||
await db.execute(delete(Transaction).where(Transaction.user_id == user_id))
|
||||
await db.flush()
|
||||
if transactions:
|
||||
logger.info('🔄 Удаляем транзакций', transactions_count=len(transactions))
|
||||
await db.execute(delete(Transaction).where(Transaction.user_id == user_id))
|
||||
await db.flush()
|
||||
except Exception as e:
|
||||
logger.error('❌ Ошибка удаления транзакций', error=e)
|
||||
|
||||
try:
|
||||
promocode_uses_result = await db.execute(select(PromoCodeUse).where(PromoCodeUse.user_id == user_id))
|
||||
promocode_uses = promocode_uses_result.scalars().all()
|
||||
async with db.begin_nested():
|
||||
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('🔄 Удаляем использований промокодов', promocode_uses_count=len(promocode_uses))
|
||||
await db.execute(delete(PromoCodeUse).where(PromoCodeUse.user_id == user_id))
|
||||
await db.flush()
|
||||
if promocode_uses:
|
||||
logger.info('🔄 Удаляем использований промокодов', promocode_uses_count=len(promocode_uses))
|
||||
await db.execute(delete(PromoCodeUse).where(PromoCodeUse.user_id == user_id))
|
||||
await db.flush()
|
||||
except Exception as e:
|
||||
logger.error('❌ Ошибка удаления использований промокодов', error=e)
|
||||
|
||||
try:
|
||||
referral_earnings_result = await db.execute(
|
||||
select(ReferralEarning).where(ReferralEarning.user_id == user_id)
|
||||
)
|
||||
referral_earnings = referral_earnings_result.scalars().all()
|
||||
async with db.begin_nested():
|
||||
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('🔄 Удаляем реферальных доходов', referral_earnings_count=len(referral_earnings))
|
||||
await db.execute(delete(ReferralEarning).where(ReferralEarning.user_id == user_id))
|
||||
await db.flush()
|
||||
if referral_earnings:
|
||||
logger.info('🔄 Удаляем реферальных доходов', referral_earnings_count=len(referral_earnings))
|
||||
await db.execute(delete(ReferralEarning).where(ReferralEarning.user_id == user_id))
|
||||
await db.flush()
|
||||
except Exception as e:
|
||||
logger.error('❌ Ошибка удаления реферальных доходов', error=e)
|
||||
|
||||
try:
|
||||
referral_records_result = await db.execute(
|
||||
select(ReferralEarning).where(ReferralEarning.referral_id == user_id)
|
||||
)
|
||||
referral_records = referral_records_result.scalars().all()
|
||||
async with db.begin_nested():
|
||||
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('🔄 Удаляем записей о рефералах', referral_records_count=len(referral_records))
|
||||
await db.execute(delete(ReferralEarning).where(ReferralEarning.referral_id == user_id))
|
||||
await db.flush()
|
||||
if referral_records:
|
||||
logger.info('🔄 Удаляем записей о рефералах', referral_records_count=len(referral_records))
|
||||
await db.execute(delete(ReferralEarning).where(ReferralEarning.referral_id == user_id))
|
||||
await db.flush()
|
||||
except Exception as e:
|
||||
logger.error('❌ Ошибка удаления записей о рефералах', error=e)
|
||||
|
||||
try:
|
||||
conversions_result = await db.execute(
|
||||
select(SubscriptionConversion).where(SubscriptionConversion.user_id == user_id)
|
||||
)
|
||||
conversions = conversions_result.scalars().all()
|
||||
async with db.begin_nested():
|
||||
conversions_result = await db.execute(
|
||||
select(SubscriptionConversion).where(SubscriptionConversion.user_id == user_id)
|
||||
)
|
||||
conversions = conversions_result.scalars().all()
|
||||
|
||||
if conversions:
|
||||
logger.info('🔄 Удаляем записей конверсий', conversions_count=len(conversions))
|
||||
await db.execute(delete(SubscriptionConversion).where(SubscriptionConversion.user_id == user_id))
|
||||
await db.flush()
|
||||
if conversions:
|
||||
logger.info('🔄 Удаляем записей конверсий', conversions_count=len(conversions))
|
||||
await db.execute(
|
||||
delete(SubscriptionConversion).where(SubscriptionConversion.user_id == user_id)
|
||||
)
|
||||
await db.flush()
|
||||
except Exception as e:
|
||||
logger.error('❌ Ошибка удаления записей конверсий', error=e)
|
||||
|
||||
try:
|
||||
broadcast_history_result = await db.execute(
|
||||
select(BroadcastHistory).where(BroadcastHistory.admin_id == user_id)
|
||||
)
|
||||
broadcast_history = broadcast_history_result.scalars().all()
|
||||
async with db.begin_nested():
|
||||
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('🔄 Удаляем записей истории рассылок', broadcast_history_count=len(broadcast_history))
|
||||
await db.execute(delete(BroadcastHistory).where(BroadcastHistory.admin_id == user_id))
|
||||
await db.flush()
|
||||
if broadcast_history:
|
||||
logger.info(
|
||||
'🔄 Удаляем записей истории рассылок', broadcast_history_count=len(broadcast_history)
|
||||
)
|
||||
await db.execute(delete(BroadcastHistory).where(BroadcastHistory.admin_id == user_id))
|
||||
await db.flush()
|
||||
except Exception as e:
|
||||
logger.error('❌ Ошибка удаления истории рассылок', error=e)
|
||||
|
||||
try:
|
||||
campaigns_result = await db.execute(
|
||||
select(AdvertisingCampaign).where(AdvertisingCampaign.created_by == user_id)
|
||||
)
|
||||
campaigns = campaigns_result.scalars().all()
|
||||
|
||||
if campaigns:
|
||||
logger.info('🔄 Очищаем создателя у рекламных кампаний', campaigns_count=len(campaigns))
|
||||
await db.execute(
|
||||
update(AdvertisingCampaign)
|
||||
.where(AdvertisingCampaign.created_by == user_id)
|
||||
.values(created_by=None)
|
||||
async with db.begin_nested():
|
||||
campaigns_result = await db.execute(
|
||||
select(AdvertisingCampaign).where(AdvertisingCampaign.created_by == user_id)
|
||||
)
|
||||
await db.flush()
|
||||
campaigns = campaigns_result.scalars().all()
|
||||
|
||||
if campaigns:
|
||||
logger.info('🔄 Очищаем создателя у рекламных кампаний', campaigns_count=len(campaigns))
|
||||
await db.execute(
|
||||
update(AdvertisingCampaign)
|
||||
.where(AdvertisingCampaign.created_by == user_id)
|
||||
.values(created_by=None)
|
||||
)
|
||||
await db.flush()
|
||||
except Exception as e:
|
||||
logger.error('❌ Ошибка обновления рекламных кампаний', error=e)
|
||||
|
||||
try:
|
||||
if user.subscription:
|
||||
logger.info('🔄 Удаляем подписку', subscription_id=user.subscription.id)
|
||||
await db.execute(
|
||||
delete(SubscriptionServer).where(SubscriptionServer.subscription_id == user.subscription.id)
|
||||
)
|
||||
await db.execute(delete(Subscription).where(Subscription.user_id == user_id))
|
||||
await db.flush()
|
||||
async with db.begin_nested():
|
||||
if user.subscription:
|
||||
logger.info('🔄 Удаляем подписку', subscription_id=user.subscription.id)
|
||||
|
||||
# Save squad info before deleting subscription
|
||||
squad_ids = user.subscription.connected_squads
|
||||
|
||||
# Delete subscription_servers and subscription FIRST
|
||||
# Lock order: subscriptions → server_squads (matches webhook order)
|
||||
await db.execute(
|
||||
delete(SubscriptionServer).where(SubscriptionServer.subscription_id == user.subscription.id)
|
||||
)
|
||||
await db.execute(delete(Subscription).where(Subscription.user_id == user_id))
|
||||
await db.flush()
|
||||
|
||||
# Decrement server_squads.current_users AFTER subscription delete
|
||||
# to match lock ordering with webhook and avoid deadlocks
|
||||
if squad_ids:
|
||||
try:
|
||||
from app.database.crud.server_squad import (
|
||||
get_server_ids_by_uuids,
|
||||
remove_user_from_servers,
|
||||
)
|
||||
|
||||
int_squad_ids = await get_server_ids_by_uuids(db, list(squad_ids))
|
||||
if int_squad_ids:
|
||||
await remove_user_from_servers(db, int_squad_ids)
|
||||
except Exception as sq_err:
|
||||
logger.warning('⚠️ Не удалось уменьшить счётчик серверов', error=sq_err)
|
||||
except Exception as e:
|
||||
logger.error('❌ Ошибка удаления подписки', error=e)
|
||||
|
||||
|
||||
@@ -20,42 +20,59 @@ branch_labels: Union[str, Sequence[str], None] = None
|
||||
depends_on: Union[str, Sequence[str], None] = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
# Add column (idempotent check)
|
||||
def _has_table(name: str) -> bool:
|
||||
conn = op.get_bind()
|
||||
inspector = sa.inspect(conn)
|
||||
columns = [c['name'] for c in inspector.get_columns('referral_earnings')]
|
||||
return name in inspector.get_table_names()
|
||||
|
||||
if 'campaign_id' not in columns:
|
||||
|
||||
def _has_column(table: str, column: str) -> bool:
|
||||
conn = op.get_bind()
|
||||
inspector = sa.inspect(conn)
|
||||
return column in [c['name'] for c in inspector.get_columns(table)]
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
# Skip if referral_earnings table doesn't exist yet
|
||||
# (fresh DBs create it via create_all in 0001 with campaign_id already present)
|
||||
if not _has_table('referral_earnings'):
|
||||
return
|
||||
|
||||
if not _has_column('referral_earnings', 'campaign_id'):
|
||||
op.add_column('referral_earnings', sa.Column('campaign_id', sa.Integer(), nullable=True))
|
||||
op.create_foreign_key(
|
||||
'fk_referral_earnings_campaign_id',
|
||||
'referral_earnings',
|
||||
'advertising_campaigns',
|
||||
['campaign_id'],
|
||||
['id'],
|
||||
ondelete='SET NULL',
|
||||
)
|
||||
|
||||
# Only create FK if target table exists
|
||||
if _has_table('advertising_campaigns'):
|
||||
op.create_foreign_key(
|
||||
'fk_referral_earnings_campaign_id',
|
||||
'referral_earnings',
|
||||
'advertising_campaigns',
|
||||
['campaign_id'],
|
||||
['id'],
|
||||
ondelete='SET NULL',
|
||||
)
|
||||
|
||||
op.create_index('ix_referral_earnings_campaign_id', 'referral_earnings', ['campaign_id'])
|
||||
|
||||
# Backfill existing data — pick earliest campaign registration per user
|
||||
# (matches runtime logic in get_user_campaign_id: ORDER BY created_at ASC LIMIT 1)
|
||||
op.execute(
|
||||
"""
|
||||
UPDATE referral_earnings re
|
||||
SET campaign_id = sub.campaign_id
|
||||
FROM (
|
||||
SELECT DISTINCT ON (user_id) user_id, campaign_id
|
||||
FROM advertising_campaign_registrations
|
||||
ORDER BY user_id, created_at ASC
|
||||
) sub
|
||||
WHERE sub.user_id = re.referral_id
|
||||
AND re.campaign_id IS NULL
|
||||
"""
|
||||
)
|
||||
# Backfill existing data — only if source table exists
|
||||
if _has_table('advertising_campaign_registrations') and _has_table('referral_earnings'):
|
||||
op.execute(
|
||||
sa.text("""
|
||||
UPDATE referral_earnings re
|
||||
SET campaign_id = sub.campaign_id
|
||||
FROM (
|
||||
SELECT DISTINCT ON (user_id) user_id, campaign_id
|
||||
FROM advertising_campaign_registrations
|
||||
ORDER BY user_id, created_at ASC
|
||||
) sub
|
||||
WHERE sub.user_id = re.referral_id
|
||||
AND re.campaign_id IS NULL
|
||||
""")
|
||||
)
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
op.drop_index('ix_referral_earnings_campaign_id', table_name='referral_earnings')
|
||||
op.drop_constraint('fk_referral_earnings_campaign_id', 'referral_earnings', type_='foreignkey')
|
||||
op.drop_column('referral_earnings', 'campaign_id')
|
||||
if _has_table('referral_earnings') and _has_column('referral_earnings', 'campaign_id'):
|
||||
op.drop_index('ix_referral_earnings_campaign_id', table_name='referral_earnings')
|
||||
op.drop_constraint('fk_referral_earnings_campaign_id', 'referral_earnings', type_='foreignkey')
|
||||
op.drop_column('referral_earnings', 'campaign_id')
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
[project]
|
||||
name = 'remnawave-bedolaga-telegram-bot'
|
||||
version = "3.16.2"
|
||||
version = "3.16.3"
|
||||
description = 'Telegram bot for RemnaWave VPN service'
|
||||
readme = 'README.md'
|
||||
license = { text = 'MIT' }
|
||||
|
||||
Reference in New Issue
Block a user