Optimize user activity updates and fix traffic top-up

This commit is contained in:
Vladless
2026-01-23 04:22:59 +03:00
parent af2d6507da
commit 858968665b
8 changed files with 420 additions and 353 deletions
+78 -56
View File
@@ -30,7 +30,8 @@ async def add_user(
language_code: str = None, language_code: str = None,
is_bot: bool = False, is_bot: bool = False,
source_code: str = None, source_code: str = None,
): commit: bool = True,
) -> bool:
try: try:
stmt = ( stmt = (
insert(User) insert(User)
@@ -43,11 +44,17 @@ async def add_user(
is_bot=is_bot, is_bot=is_bot,
source_code=source_code, source_code=source_code,
) )
.on_conflict_do_nothing(index_elements=[User.tg_id]) .on_conflict_do_nothing(index_elements=["tg_id"])
.returning(User.tg_id)
) )
await session.execute(stmt) res = await session.execute(stmt)
await session.commit() inserted_tg_id = res.scalar_one_or_none()
if inserted_tg_id is None:
return False
if commit:
await session.commit()
logger.info(f"[DB] Новый пользователь добавлен: {tg_id} (source: {source_code})") logger.info(f"[DB] Новый пользователь добавлен: {tg_id} (source: {source_code})")
return True
except SQLAlchemyError as e: except SQLAlchemyError as e:
logger.error(f"[DB] Ошибка при добавлении пользователя {tg_id}: {e}") logger.error(f"[DB] Ошибка при добавлении пользователя {tg_id}: {e}")
await session.rollback() await session.rollback()
@@ -121,58 +128,64 @@ async def upsert_user(
is_bot: bool = False, is_bot: bool = False,
only_if_exists: bool = False, only_if_exists: bool = False,
) -> dict | None: ) -> dict | None:
"""Создаёт пользователя или обновляет поля профиля."""
try: try:
now = datetime.utcnow()
returning_cols = list(User.__table__.c)
if only_if_exists: if only_if_exists:
result = await session.execute(select(User).where(User.tg_id == tg_id)) username_value = username if username else User.username
user = result.scalar_one_or_none() first_name_value = first_name if first_name else User.first_name
if not user: last_name_value = last_name if last_name else User.last_name
return None language_code_value = language_code if language_code else User.language_code
await session.execute(
res = await session.execute(
update(User) update(User)
.where(User.tg_id == tg_id) .where(User.tg_id == tg_id)
.values( .values(
username=username or user.username, username=username_value,
first_name=first_name or user.first_name, first_name=first_name_value,
last_name=last_name or user.last_name, last_name=last_name_value,
language_code=language_code or user.language_code, language_code=language_code_value,
is_bot=is_bot, is_bot=is_bot,
updated_at=datetime.utcnow(), updated_at=now,
) )
.returning(*returning_cols)
) )
row = res.mappings().one_or_none()
if row is None:
return None
await session.commit() await session.commit()
result = await session.execute(select(User).where(User.tg_id == tg_id)) return dict(row)
return dict(result.scalar_one().__dict__)
else: res = await session.execute(
res = await session.execute( insert(User)
insert(User) .values(
.values( tg_id=tg_id,
tg_id=tg_id, username=username,
username=username, first_name=first_name,
first_name=first_name, last_name=last_name,
last_name=last_name, language_code=language_code,
language_code=language_code, is_bot=is_bot,
is_bot=is_bot, created_at=now,
created_at=datetime.utcnow(), updated_at=now,
updated_at=datetime.utcnow(),
)
.on_conflict_do_update(
index_elements=[User.tg_id],
set_={
"username": username,
"first_name": first_name,
"last_name": last_name,
"language_code": language_code,
"is_bot": is_bot,
"updated_at": datetime.utcnow(),
},
)
.returning(User)
) )
obj = res.scalar_one() .on_conflict_do_update(
await session.commit() index_elements=[User.tg_id],
d = obj.__dict__.copy() set_={
d.pop("_sa_instance_state", None) "username": username,
return d "first_name": first_name,
"last_name": last_name,
"language_code": language_code,
"is_bot": is_bot,
"updated_at": now,
},
)
.returning(*returning_cols)
)
row = res.mappings().one()
await session.commit()
return dict(row)
except SQLAlchemyError as e: except SQLAlchemyError as e:
logger.error(f"[DB] Ошибка при UPSERT пользователя {tg_id}: {e}") logger.error(f"[DB] Ошибка при UPSERT пользователя {tg_id}: {e}")
await session.rollback() await session.rollback()
@@ -210,12 +223,10 @@ async def mark_trial_extended(tg_id: int, session: AsyncSession):
async def get_user_snapshot(session: AsyncSession, tg_id: int) -> tuple[int, int] | None: async def get_user_snapshot(session: AsyncSession, tg_id: int) -> tuple[int, int] | None:
keys_count_sq = select(func.count(Key.client_id)).where(Key.tg_id == tg_id).scalar_subquery()
res = await session.execute( res = await session.execute(
select(func.coalesce(User.trial, 0), func.count(Key.client_id)) select(func.coalesce(User.trial, 0), keys_count_sq).where(User.tg_id == tg_id)
.select_from(User)
.join(Key, Key.tg_id == User.tg_id, isouter=True)
.where(User.tg_id == tg_id)
.group_by(User.tg_id, User.trial)
) )
row = res.first() row = res.first()
if row is None: if row is None:
@@ -223,17 +234,28 @@ async def get_user_snapshot(session: AsyncSession, tg_id: int) -> tuple[int, int
return int(row[0]), int(row[1]) return int(row[0]), int(row[1])
async def upsert_source_if_empty(session: AsyncSession, tg_id: int, source_code: str) -> None: async def upsert_source_if_empty(
session: AsyncSession,
tg_id: int,
source_code: str,
commit: bool = True,
) -> bool:
if not source_code: if not source_code:
return return False
stmt = ( stmt = (
insert(User) insert(User)
.values(tg_id=tg_id, source_code=source_code) .values(tg_id=tg_id, source_code=source_code)
.on_conflict_do_update( .on_conflict_do_update(
index_elements=[User.tg_id], index_elements=["tg_id"],
set_={"source_code": insert(User).excluded.source_code}, set_={"source_code": insert(User).excluded.source_code},
where=(User.source_code.is_(None)), where=(User.source_code.is_(None)),
) )
.returning(User.tg_id)
) )
await session.execute(stmt) res = await session.execute(stmt)
await session.commit() changed_tg_id = res.scalar_one_or_none()
if changed_tg_id is None:
return False
if commit:
await session.commit()
return True
+12 -15
View File
@@ -18,7 +18,6 @@ from database import (
add_payment, add_payment,
add_user, add_user,
check_coupon_usage, check_coupon_usage,
check_user_exists,
create_coupon_usage, create_coupon_usage,
get_coupon_by_code, get_coupon_by_code,
get_keys, get_keys,
@@ -108,20 +107,18 @@ async def activate_coupon(
await state.clear() await state.clear()
return return
user_exists = await check_user_exists(session, user_id) if isinstance(user, dict):
if not user_exists: await add_user(session=session, **user)
if isinstance(user, dict): else:
await add_user(session=session, **user) await add_user(
else: session=session,
await add_user( tg_id=user.id,
session=session, username=getattr(user, "username", None),
tg_id=user.id, first_name=getattr(user, "first_name", None),
username=getattr(user, "username", None), last_name=getattr(user, "last_name", None),
first_name=getattr(user, "first_name", None), language_code=getattr(user, "language_code", None),
last_name=getattr(user, "last_name", None), is_bot=getattr(user, "is_bot", False),
language_code=getattr(user, "language_code", None), )
is_bot=getattr(user, "is_bot", False),
)
if coupon.amount > 0: if coupon.amount > 0:
try: try:
+11 -13
View File
@@ -20,7 +20,6 @@ from config import (
from core.bootstrap import MODES_CONFIG, NOTIFICATIONS_CONFIG from core.bootstrap import MODES_CONFIG, NOTIFICATIONS_CONFIG
from database import ( from database import (
add_user, add_user,
check_user_exists,
get_tariffs_for_cluster, get_tariffs_for_cluster,
get_trial, get_trial,
) )
@@ -393,18 +392,17 @@ async def create_key(
selected_traffic_gb: int | None = None, selected_traffic_gb: int | None = None,
selected_price_rub: int | None = None, selected_price_rub: int | None = None,
): ):
if not await check_user_exists(session, tg_id): from_user = message_or_query.from_user if isinstance(message_or_query, CallbackQuery | Message) else None
from_user = message_or_query.from_user if isinstance(message_or_query, CallbackQuery | Message) else None if from_user:
if from_user: await add_user(
await add_user( tg_id=from_user.id,
tg_id=from_user.id, username=from_user.username,
username=from_user.username, first_name=from_user.first_name,
first_name=from_user.first_name, last_name=from_user.last_name,
last_name=from_user.last_name, language_code=from_user.language_code,
language_code=from_user.language_code, is_bot=from_user.is_bot,
is_bot=from_user.is_bot, session=session,
session=session, )
)
use_country_selection = bool(MODES_CONFIG.get("COUNTRY_SELECTION_ENABLED", USE_COUNTRY_SELECTION)) use_country_selection = bool(MODES_CONFIG.get("COUNTRY_SELECTION_ENABLED", USE_COUNTRY_SELECTION))
+14 -16
View File
@@ -25,7 +25,6 @@ from core.bootstrap import BUTTONS_CONFIG, MODES_CONFIG
from database import ( from database import (
add_referral, add_referral,
add_user, add_user,
check_user_exists,
get_referral_by_referred_id, get_referral_by_referred_id,
get_referral_stats, get_referral_stats,
) )
@@ -271,23 +270,22 @@ async def handle_referral_link(
await message.answer("❌ Вы уже использовали реферальную ссылку.") await message.answer("❌ Вы уже использовали реферальную ссылку.")
return return
user_exists = await check_user_exists(session, user_id) if isinstance(user, dict):
if user_exists: inserted = await add_user(session=session, **user)
else:
inserted = await add_user(
session=session,
tg_id=user.id,
username=getattr(user, "username", None),
first_name=getattr(user, "first_name", None),
last_name=getattr(user, "last_name", None),
language_code=getattr(user, "language_code", None),
is_bot=getattr(user, "is_bot", False),
)
if not inserted:
await message.answer("❌ Вы уже зарегистрированы и не можете стать рефералом.") await message.answer("❌ Вы уже зарегистрированы и не можете стать рефералом.")
return return
if not user_exists:
if isinstance(user, dict):
await add_user(session=session, **user)
else:
await add_user(
session=session,
tg_id=user.id,
username=getattr(user, "username", None),
first_name=getattr(user, "first_name", None),
last_name=getattr(user, "last_name", None),
language_code=getattr(user, "language_code", None),
is_bot=getattr(user, "is_bot", False),
)
await add_referral(session, user_id, referrer_tg_id) await add_referral(session, user_id, referrer_tg_id)
+250 -226
View File
@@ -5,6 +5,7 @@ from aiogram import F, Router
from aiogram.fsm.context import FSMContext from aiogram.fsm.context import FSMContext
from aiogram.types import CallbackQuery, InlineKeyboardButton from aiogram.types import CallbackQuery, InlineKeyboardButton
from aiogram.utils.keyboard import InlineKeyboardBuilder from aiogram.utils.keyboard import InlineKeyboardBuilder
import sqlalchemy as sa
from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.ext.asyncio import AsyncSession
from config import USE_NEW_PAYMENT_FLOW from config import USE_NEW_PAYMENT_FLOW
@@ -17,6 +18,7 @@ from database import (
save_key_config_with_mode, save_key_config_with_mode,
update_balance, update_balance,
) )
from database.models import User
from handlers.buttons import BACK, CONFIRM_ADDON_BUTTON_TEXT, PAYMENT from handlers.buttons import BACK, CONFIRM_ADDON_BUTTON_TEXT, PAYMENT
from handlers.keys.key_view import render_key_info from handlers.keys.key_view import render_key_info
from handlers.payments.currency_rates import format_for_user from handlers.payments.currency_rates import format_for_user
@@ -613,252 +615,274 @@ async def handle_addons_confirm(callback: CallbackQuery, state: FSMContext, sess
tg_id = callback.from_user.id tg_id = callback.from_user.id
data = await state.get_data() data = await state.get_data()
email = data.get("addon_key_email") if data.get("addon_confirm_in_progress"):
tariff_id = data.get("addon_tariff_id") await callback.answer("⏳ Уже обрабатываю...", show_alert=False)
selected_devices = data.get("addon_selected_device_limit")
selected_traffic_gb = data.get("addon_selected_traffic_gb")
current_devices = data.get("addon_current_device_limit")
current_traffic_gb = data.get("addon_current_traffic_gb")
logger.info(
"[ADDONS] PACK_MODE handle_addons_confirm: "
f"tg_id={tg_id} email={email} tariff_id={tariff_id} "
f"selected_devices={selected_devices} selected_traffic_gb={selected_traffic_gb} "
f"current_devices={current_devices} current_traffic_gb={current_traffic_gb}"
)
if not email or not tariff_id:
await callback.message.answer("❌ Данные для изменения подписки не найдены.")
await state.clear()
return return
record = await get_key_details(session, email) await state.update_data(addon_confirm_in_progress=True)
if not record:
logger.warning(f"[ADDONS] PACK_MODE: подписка {email} не найдена в handle_addons_confirm")
await callback.message.answer("❌ Подписка не найдена.")
await state.clear()
return
tariff = await get_tariff_by_id(session, int(tariff_id))
if not tariff:
logger.error(f"[ADDONS] PACK_MODE: тариф {tariff_id} не найден в handle_addons_confirm")
await callback.message.answer("❌ Тариф не найден.")
await state.clear()
return
cfg = data.get("addon_tariff_config") or {}
device_options = cfg.get("device_options") or []
traffic_options = cfg.get("traffic_options_gb") or []
pack_devices, pack_traffic, pack_mode = get_pack_flags()
has_device_option = pack_devices and bool(device_options)
has_traffic_option = pack_traffic and bool(traffic_options)
if has_device_option and current_devices is not None and int(current_devices) == 0:
has_device_option = False
selected_devices = None
if has_traffic_option and current_traffic_gb is not None and int(current_traffic_gb) == 0:
has_traffic_option = False
selected_traffic_gb = None
current_devices_for_price = int(current_devices) if current_devices is not None else None
current_traffic_for_price = int(current_traffic_gb) if current_traffic_gb is not None else None
base_price_for_current = calculate_config_price(
tariff=tariff,
selected_device_limit=current_devices_for_price,
selected_traffic_gb=current_traffic_for_price,
)
try: try:
base_price_for_current_int = int(base_price_for_current) if base_price_for_current is not None else 0 email = data.get("addon_key_email")
except (TypeError, ValueError): tariff_id = data.get("addon_tariff_id")
base_price_for_current_int = 0 selected_devices = data.get("addon_selected_device_limit")
selected_traffic_gb = data.get("addon_selected_traffic_gb")
recalc_enabled = bool( current_devices = data.get("addon_current_device_limit")
MODES_CONFIG.get( current_traffic_gb = data.get("addon_current_traffic_gb")
"KEY_ADDONS_RECALC_PRICE",
TARIFFS_CONFIG.get("KEY_ADDONS_RECALC_PRICE", False),
)
)
diff_full = calc_pack_full_price_rub(
tariff=tariff,
has_device_option=has_device_option,
has_traffic_option=has_traffic_option,
selected_devices=int(selected_devices) if selected_devices is not None else None,
selected_traffic_gb=int(selected_traffic_gb) if selected_traffic_gb is not None else None,
)
if recalc_enabled:
remaining_seconds, total_seconds = calc_remaining_ratio_seconds(record.get("expiry_time"), tariff)
extra_price = int((diff_full * remaining_seconds + total_seconds - 1) // total_seconds)
total_price_after_purchase = base_price_for_current_int + extra_price
else:
extra_price = int(diff_full)
total_price_after_purchase = base_price_for_current_int + diff_full
logger.debug(
"[ADDONS] PACK_MODE confirm prices: "
f"base_price_for_current={base_price_for_current_int} diff_full={diff_full} "
f"extra_price={extra_price} recalc_enabled={recalc_enabled} total_price_after_purchase={total_price_after_purchase} "
f"has_device_option={has_device_option} has_traffic_option={has_traffic_option} "
f"pack_mode={pack_mode!r}"
)
if extra_price <= 0:
logger.info(f"[ADDONS] PACK_MODE: extra_price <= 0, доплата не требуется, email={email}")
await state.clear()
await render_key_info(callback.message, session, email, "img/pic_view.jpg")
await callback.answer(ADDONS_NO_EXTRA_PAYMENT_TEXT, show_alert=True)
return
balance = await get_balance(session, tg_id)
logger.debug(f"[ADDONS] PACK_MODE balance check: tg_id={tg_id} balance={balance} extra_price={extra_price}")
if balance < extra_price:
required_amount = ceil(extra_price - balance)
language_code = getattr(callback.from_user, "language_code", None)
required_amount_text = await format_for_user(session, tg_id, float(required_amount), language_code)
logger.info( logger.info(
"[ADDONS] PACK_MODE: недостаточно средств " "[ADDONS] PACK_MODE handle_addons_confirm: "
f"balance={balance} extra_price={extra_price} required_amount={required_amount} " f"tg_id={tg_id} email={email} tariff_id={tariff_id} "
f"tg_id={tg_id} USE_NEW_PAYMENT_FLOW={USE_NEW_PAYMENT_FLOW}" f"selected_devices={selected_devices} selected_traffic_gb={selected_traffic_gb} "
f"current_devices={current_devices} current_traffic_gb={current_traffic_gb}"
) )
if USE_NEW_PAYMENT_FLOW: if not email or not tariff_id:
handled = await try_fast_payment_flow( await callback.message.answer("❌ Данные для изменения подписки не найдены.")
callback, await state.clear()
session, return
state,
tg_id=tg_id,
temp_key="waiting_for_addons_payment",
temp_payload={
"email": email,
"tariff_id": int(tariff_id),
"selected_device_limit": selected_devices,
"selected_traffic_gb": selected_traffic_gb,
"current_device_limit": current_devices,
"current_traffic_gb": current_traffic_gb,
"required_amount": required_amount,
},
required_amount=required_amount,
)
logger.debug(f"[ADDONS] PACK_MODE try_fast_payment_flow handled={handled} tg_id={tg_id} email={email}")
if handled:
return
builder = InlineKeyboardBuilder() record = await get_key_details(session, email)
builder.row(InlineKeyboardButton(text=PAYMENT, callback_data="pay")) if not record:
await edit_or_send_message( logger.warning(f"[ADDONS] PACK_MODE: подписка {email} не найдена в handle_addons_confirm")
target_message=callback.message, await callback.message.answer("❌ Подписка не найдена.")
text=INSUFFICIENT_FUNDS_RENEWAL_MSG.format(required_amount=required_amount_text), await state.clear()
reply_markup=builder.as_markup(), return
tariff = await get_tariff_by_id(session, int(tariff_id))
if not tariff:
logger.error(f"[ADDONS] PACK_MODE: тариф {tariff_id} не найден в handle_addons_confirm")
await callback.message.answer("❌ Тариф не найден.")
await state.clear()
return
cfg = data.get("addon_tariff_config") or {}
device_options = cfg.get("device_options") or []
traffic_options = cfg.get("traffic_options_gb") or []
pack_devices, pack_traffic, pack_mode = get_pack_flags()
has_device_option = pack_devices and bool(device_options)
has_traffic_option = pack_traffic and bool(traffic_options)
if has_device_option and current_devices is not None and int(current_devices) == 0:
has_device_option = False
selected_devices = None
if has_traffic_option and current_traffic_gb is not None and int(current_traffic_gb) == 0:
has_traffic_option = False
selected_traffic_gb = None
current_devices_for_price = int(current_devices) if current_devices is not None else None
current_traffic_for_price = int(current_traffic_gb) if current_traffic_gb is not None else None
base_price_for_current = calculate_config_price(
tariff=tariff,
selected_device_limit=current_devices_for_price,
selected_traffic_gb=current_traffic_for_price,
) )
return
try:
expiry_time = record["expiry_time"]
client_id = record["client_id"]
server_id = record["server_id"]
device_limit_effective_current, traffic_limit_bytes_effective_current = await get_effective_limits_for_key(
session=session,
tariff_id=int(tariff_id),
selected_device_limit=int(current_devices) if current_devices is not None else None,
selected_traffic_gb=int(current_traffic_gb) if current_traffic_gb is not None else None,
)
traffic_limit_gb_effective_current = (
int(traffic_limit_bytes_effective_current / GB) if traffic_limit_bytes_effective_current else 0
)
new_device_limit_effective = device_limit_effective_current
new_traffic_limit_gb_effective = traffic_limit_gb_effective_current
if has_device_option and selected_devices is not None:
pack_devices_val = int(selected_devices)
if pack_devices_val <= 0 or (new_device_limit_effective is not None and new_device_limit_effective <= 0):
new_device_limit_effective = 0
else:
if new_device_limit_effective is None:
new_device_limit_effective = pack_devices_val
else:
new_device_limit_effective = new_device_limit_effective + pack_devices_val
if has_traffic_option and selected_traffic_gb is not None:
pack_traffic_val = int(selected_traffic_gb)
if pack_traffic_val <= 0 or new_traffic_limit_gb_effective <= 0:
new_traffic_limit_gb_effective = 0
else:
new_traffic_limit_gb_effective = new_traffic_limit_gb_effective + pack_traffic_val
current_subgroup = None
try: try:
current_tariff_id = record.get("tariff_id") base_price_for_current_int = int(base_price_for_current) if base_price_for_current is not None else 0
if current_tariff_id: except (TypeError, ValueError):
current_tariff = await get_tariff_by_id(session, int(current_tariff_id)) base_price_for_current_int = 0
if current_tariff:
current_subgroup = current_tariff.get("subgroup_title")
except Exception as error:
logger.warning(f"[ADDONS] PACK_MODE: не удалось определить текущую подгруппу: {error}")
target_subgroup = tariff.get("subgroup_title") recalc_enabled = bool(
old_subgroup = current_subgroup MODES_CONFIG.get(
"KEY_ADDONS_RECALC_PRICE",
TARIFFS_CONFIG.get("KEY_ADDONS_RECALC_PRICE", False),
)
)
total_gb = new_traffic_limit_gb_effective diff_full = calc_pack_full_price_rub(
hwid_device_limit_to_set = new_device_limit_effective tariff=tariff,
has_device_option=has_device_option,
has_traffic_option=has_traffic_option,
selected_devices=int(selected_devices) if selected_devices is not None else None,
selected_traffic_gb=int(selected_traffic_gb) if selected_traffic_gb is not None else None,
)
if recalc_enabled:
remaining_seconds, total_seconds = calc_remaining_ratio_seconds(record.get("expiry_time"), tariff)
extra_price = int((diff_full * remaining_seconds + total_seconds - 1) // total_seconds)
total_price_after_purchase = base_price_for_current_int + extra_price
else:
extra_price = int(diff_full)
total_price_after_purchase = base_price_for_current_int + diff_full
logger.debug( logger.debug(
"[ADDONS] PACK_MODE renew_key_in_cluster params: " "[ADDONS] PACK_MODE confirm prices: "
f"server_id={server_id} email={email} client_id={client_id} total_gb={total_gb} " f"base_price_for_current={base_price_for_current_int} diff_full={diff_full} "
f"hwid_device_limit_to_set={hwid_device_limit_to_set} target_subgroup={target_subgroup} " f"extra_price={extra_price} recalc_enabled={recalc_enabled} total_price_after_purchase={total_price_after_purchase} "
f"old_subgroup={old_subgroup}" f"has_device_option={has_device_option} has_traffic_option={has_traffic_option} "
f"pack_mode={pack_mode!r}"
) )
await renew_key_in_cluster( if extra_price <= 0:
cluster_id=server_id, logger.info(f"[ADDONS] PACK_MODE: extra_price <= 0, доплата не требуется, email={email}")
email=email, await state.clear()
client_id=client_id, await render_key_info(callback.message, session, email, "img/pic_view.jpg")
new_expiry_time=expiry_time, await callback.answer(ADDONS_NO_EXTRA_PAYMENT_TEXT, show_alert=True)
total_gb=total_gb, return
session=session,
hwid_device_limit=hwid_device_limit_to_set,
reset_traffic=False,
target_subgroup=target_subgroup,
old_subgroup=old_subgroup,
plan=int(tariff_id),
)
await update_balance(session, tg_id, -extra_price) balance = await get_balance(session, tg_id)
logger.debug(f"[ADDONS] PACK_MODE balance check: tg_id={tg_id} balance={balance} extra_price={extra_price}")
await save_key_config_with_mode( if balance < extra_price:
session=session, required_amount = ceil(extra_price - balance)
email=email, language_code = getattr(callback.from_user, "language_code", None)
selected_devices=new_device_limit_effective, required_amount_text = await format_for_user(session, tg_id, float(required_amount), language_code)
selected_traffic_gb=new_traffic_limit_gb_effective,
total_price=int(total_price_after_purchase),
has_device_choice=has_device_option,
has_traffic_choice=has_traffic_option,
config_mode="pack",
)
await session.commit() logger.info(
"[ADDONS] PACK_MODE: недостаточно средств "
f"balance={balance} extra_price={extra_price} required_amount={required_amount} "
f"tg_id={tg_id} USE_NEW_PAYMENT_FLOW={USE_NEW_PAYMENT_FLOW}"
)
logger.info( if USE_NEW_PAYMENT_FLOW:
"[ADDONS] PACK_MODE успешная покупка пакета: " handled = await try_fast_payment_flow(
f"tg_id={tg_id} email={email} extra_price={extra_price} " callback,
f"new_device_limit_effective={new_device_limit_effective} " session,
f"new_traffic_limit_gb_effective={new_traffic_limit_gb_effective} " state,
f"recalc_enabled={recalc_enabled} pack_mode={pack_mode!r}" tg_id=tg_id,
) temp_key="waiting_for_addons_payment",
temp_payload={
"email": email,
"tariff_id": int(tariff_id),
"selected_device_limit": selected_devices,
"selected_traffic_gb": selected_traffic_gb,
"current_device_limit": current_devices,
"current_traffic_gb": current_traffic_gb,
"required_amount": required_amount,
},
required_amount=required_amount,
)
logger.debug(f"[ADDONS] PACK_MODE try_fast_payment_flow handled={handled} tg_id={tg_id} email={email}")
if handled:
return
await state.clear() builder = InlineKeyboardBuilder()
await render_key_info(callback.message, session, email, "img/pic_view.jpg") builder.row(InlineKeyboardButton(text=PAYMENT, callback_data="pay"))
await callback.answer(ADDONS_PACK_SUCCESS_TEXT, show_alert=True) await edit_or_send_message(
target_message=callback.message,
text=INSUFFICIENT_FUNDS_RENEWAL_MSG.format(required_amount=required_amount_text),
reply_markup=builder.as_markup(),
)
return
except Exception as error: try:
logger.error(f"[ADDONS] PACK_MODE ошибка при покупке пакета для {email}: {error}") expiry_time = record["expiry_time"]
await callback.message.answer("❌ Ошибка при обновлении подписки. Попробуйте позже.") client_id = record["client_id"]
await state.clear() server_id = record["server_id"]
device_limit_effective_current, traffic_limit_bytes_effective_current = await get_effective_limits_for_key(
session=session,
tariff_id=int(tariff_id),
selected_device_limit=int(current_devices) if current_devices is not None else None,
selected_traffic_gb=int(current_traffic_gb) if current_traffic_gb is not None else None,
)
traffic_limit_gb_effective_current = (
int(traffic_limit_bytes_effective_current / GB) if traffic_limit_bytes_effective_current else 0
)
new_device_limit_effective = device_limit_effective_current
new_traffic_limit_gb_effective = traffic_limit_gb_effective_current
if has_device_option and selected_devices is not None:
pack_devices_val = int(selected_devices)
if pack_devices_val <= 0 or (new_device_limit_effective is not None and new_device_limit_effective <= 0):
new_device_limit_effective = 0
else:
if new_device_limit_effective is None:
new_device_limit_effective = pack_devices_val
else:
new_device_limit_effective = new_device_limit_effective + pack_devices_val
if has_traffic_option and selected_traffic_gb is not None:
pack_traffic_val = int(selected_traffic_gb)
if pack_traffic_val <= 0 or new_traffic_limit_gb_effective <= 0:
new_traffic_limit_gb_effective = 0
else:
new_traffic_limit_gb_effective = new_traffic_limit_gb_effective + pack_traffic_val
current_subgroup = None
try:
current_tariff_id = record.get("tariff_id")
if current_tariff_id:
current_tariff = await get_tariff_by_id(session, int(current_tariff_id))
if current_tariff:
current_subgroup = current_tariff.get("subgroup_title")
except Exception as error:
logger.warning(f"[ADDONS] PACK_MODE: не удалось определить текущую подгруппу: {error}")
target_subgroup = tariff.get("subgroup_title")
old_subgroup = current_subgroup
total_gb = new_traffic_limit_gb_effective
hwid_device_limit_to_set = new_device_limit_effective
logger.debug(
"[ADDONS] PACK_MODE renew_key_in_cluster params: "
f"server_id={server_id} email={email} client_id={client_id} total_gb={total_gb} "
f"hwid_device_limit_to_set={hwid_device_limit_to_set} target_subgroup={target_subgroup} "
f"old_subgroup={old_subgroup}"
)
await renew_key_in_cluster(
cluster_id=server_id,
email=email,
client_id=client_id,
new_expiry_time=expiry_time,
total_gb=total_gb,
session=session,
hwid_device_limit=hwid_device_limit_to_set,
reset_traffic=False,
target_subgroup=target_subgroup,
old_subgroup=old_subgroup,
plan=int(tariff_id),
)
result = await session.execute(
sa.update(User)
.where(User.tg_id == tg_id, User.balance >= extra_price)
.values(balance=User.balance - extra_price)
)
if (result.rowcount or 0) != 1:
await session.rollback()
await callback.answer("❌ Недостаточно средств.", show_alert=True)
await state.clear()
return
await save_key_config_with_mode(
session=session,
email=email,
selected_devices=new_device_limit_effective,
selected_traffic_gb=new_traffic_limit_gb_effective,
total_price=int(total_price_after_purchase),
has_device_choice=has_device_option,
has_traffic_choice=has_traffic_option,
config_mode="pack",
)
await session.commit()
logger.info(
"[ADDONS] PACK_MODE успешная покупка пакета: "
f"tg_id={tg_id} email={email} extra_price={extra_price} "
f"new_device_limit_effective={new_device_limit_effective} "
f"new_traffic_limit_gb_effective={new_traffic_limit_gb_effective} "
f"recalc_enabled={recalc_enabled} pack_mode={pack_mode!r}"
)
await state.clear()
await render_key_info(callback.message, session, email, "img/pic_view.jpg")
await callback.answer(ADDONS_PACK_SUCCESS_TEXT, show_alert=True)
except Exception as error:
logger.error(f"[ADDONS] PACK_MODE ошибка при покупке пакета для {email}: {error}")
await callback.message.answer("❌ Ошибка при обновлении подписки. Попробуйте позже.")
await state.clear()
finally:
try:
await state.update_data(addon_confirm_in_progress=False)
except Exception:
pass
+13 -10
View File
@@ -5,27 +5,30 @@ from cachetools import TTLCache
class ThrottlingMiddleware(BaseMiddleware): class ThrottlingMiddleware(BaseMiddleware):
def __init__(self) -> None: def __init__(self) -> None:
self.cache = TTLCache(maxsize=10_000, ttl=1.0) self.cache = TTLCache(maxsize=50_000, ttl=1.0)
self.throttle_notice_cache = TTLCache(maxsize=10_000, ttl=1.0) self.throttle_notice_cache = TTLCache(maxsize=50_000, ttl=1.0)
async def __call__(self, handler, event, data): async def __call__(self, handler, event, data):
if not isinstance(event, CallbackQuery):
return await handler(event, data)
user_id = event.from_user.id if event.from_user else None user_id = event.from_user.id if event.from_user else None
if user_id is None: if user_id is None:
return await handler(event, data) return await handler(event, data)
current_count = self.cache.get(user_id, 0) key = (user_id, event.data or "")
current_count = self.cache.get(key, 0)
if current_count >= 3: if current_count >= 2:
if isinstance(event, CallbackQuery) and user_id not in self.throttle_notice_cache: if key not in self.throttle_notice_cache:
self.throttle_notice_cache[user_id] = None self.throttle_notice_cache[key] = None
bot: Bot = data["bot"] bot: Bot = data["bot"]
await bot.answer_callback_query( await bot.answer_callback_query(
callback_query_id=event.id, callback_query_id=event.id,
text="Слишком много запросов! Пожалуйста, подождите...", text="Слишком много нажатий, подождите...",
show_alert=False, show_alert=False,
) )
return None return
else:
self.cache[user_id] = current_count + 1
self.cache[key] = current_count + 1
return await handler(event, data) return await handler(event, data)
+41 -16
View File
@@ -1,19 +1,22 @@
from collections.abc import Awaitable, Callable from collections.abc import Awaitable, Callable
from datetime import datetime
from time import monotonic from time import monotonic
from typing import Any from typing import Any
from aiogram import BaseMiddleware from aiogram import BaseMiddleware
from aiogram.types import TelegramObject, User from aiogram.types import TelegramObject, User
from sqlalchemy import update
from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.ext.asyncio import AsyncSession
from database import upsert_user from database import upsert_user
from database.models import User as DbUser
from logger import logger from logger import logger
class UserMiddleware(BaseMiddleware): class UserMiddleware(BaseMiddleware):
def __init__(self, debounce_sec: float = 60.0) -> None: def __init__(self, debounce_sec: float = 60.0) -> None:
self._debounce = float(debounce_sec) self._debounce = float(debounce_sec)
self._cache: dict[int, tuple[str, float, dict | None]] = {} self._cache: dict[int, tuple[str, float, float, dict | None]] = {}
async def __call__( async def __call__(
self, self,
@@ -40,11 +43,17 @@ class UserMiddleware(BaseMiddleware):
cached = self._cache.get(uid) cached = self._cache.get(uid)
if cached: if cached:
cached_fingerprint, ts, cached_db_user = cached cached_fingerprint, profile_ts, touch_ts, cached_db_user = cached
if fingerprint == cached_fingerprint and now - ts < self._debounce:
return cached_db_user if fingerprint == cached_fingerprint:
if now - touch_ts >= self._debounce:
db_user = await self._touch_user(uid, session)
self._cache[uid] = (cached_fingerprint, profile_ts, now, db_user or cached_db_user)
return db_user or cached_db_user
if now - profile_ts < self._debounce:
return cached_db_user
logger.debug(f"Обработка пользователя: {uid}")
db_user = await upsert_user( db_user = await upsert_user(
tg_id=uid, tg_id=uid,
username=user.username, username=user.username,
@@ -55,17 +64,33 @@ class UserMiddleware(BaseMiddleware):
session=session, session=session,
only_if_exists=True, only_if_exists=True,
) )
self._cache[uid] = (fingerprint, now, db_user) self._cache[uid] = (fingerprint, now, now, db_user)
if db_user:
logger.debug(f"Получены данные пользователя из БД: {uid}")
return db_user return db_user
async def _touch_user(self, tg_id: int, session: AsyncSession) -> dict | None:
now = datetime.utcnow()
res = await session.execute(
update(DbUser)
.where(DbUser.tg_id == tg_id)
.values(updated_at=now)
.returning(DbUser)
)
obj = res.scalar_one_or_none()
if obj is None:
return None
await session.commit()
d = obj.__dict__.copy()
d.pop("_sa_instance_state", None)
return d
def _fingerprint(self, user: User) -> str: def _fingerprint(self, user: User) -> str:
return "|".join([ return "|".join(
str(user.id), [
user.username or "", str(user.id),
user.first_name or "", user.username or "",
user.last_name or "", user.first_name or "",
user.language_code or "", user.last_name or "",
"1" if user.is_bot else "0", user.language_code or "",
]) "1" if user.is_bot else "0",
]
)
+1 -1
View File
@@ -92,4 +92,4 @@ def get_git_commit_number() -> str:
def get_version() -> str: def get_version() -> str:
return f"v.5.1-b261235 {get_git_commit_number()}" return f"v.5.1-b23012603 {get_git_commit_number()}"