Modernize bot for Python 3.12
This commit is contained in:
@@ -45,7 +45,7 @@ class KeyboardBuilderImpl(IKeyboardBuilder):
|
||||
|
||||
class MessageFormatterImpl(IMessageFormatter):
|
||||
"""Реализация форматирования сообщений"""
|
||||
|
||||
|
||||
def get_lottery_keyboard(self, lottery_id: int, is_admin: bool = False):
|
||||
"""Получить клавиатуру для конкретного розыгрыша"""
|
||||
buttons = [
|
||||
@@ -72,12 +72,8 @@ class MessageFormatterImpl(IMessageFormatter):
|
||||
text = text[:47] + "..."
|
||||
buttons.append([InlineKeyboardButton(text=text, callback_data=f"conduct_{lottery.id}")])
|
||||
|
||||
buttons.append([InlineKeyboardButton(text="◀️ Назад", callback_data="lottery_management")])
|
||||
buttons.append([InlineKeyboardButton(text="◀️ Назад", callback_data="admin_lotteries")])
|
||||
return InlineKeyboardMarkup(inline_keyboard=buttons)
|
||||
|
||||
|
||||
class MessageFormatterImpl(IMessageFormatter):
|
||||
"""Реализация форматирования сообщений"""
|
||||
|
||||
def format_lottery_info(self, lottery: Lottery, participants_count: int) -> str:
|
||||
"""Форматировать информацию о розыгрыше"""
|
||||
@@ -134,4 +130,4 @@ class MessageFormatterImpl(IMessageFormatter):
|
||||
text += f"✅ Завершенных розыгрышей: {stats.get('completed_lotteries', 0)}\n"
|
||||
text += f"🎯 Всего участий: {stats.get('total_participations', 0)}\n"
|
||||
|
||||
return text
|
||||
return text
|
||||
|
||||
@@ -8,11 +8,12 @@ load_dotenv()
|
||||
|
||||
# Конфигурация базы данных
|
||||
DATABASE_URL = os.getenv("DATABASE_URL", "sqlite+aiosqlite:///./lottery_bot.db")
|
||||
SQL_ECHO = os.getenv("SQL_ECHO", "false").lower() in {"1", "true", "yes", "on"}
|
||||
|
||||
# Создаем асинхронный движок
|
||||
engine = create_async_engine(
|
||||
DATABASE_URL,
|
||||
echo=True, # Логирование SQL запросов
|
||||
echo=SQL_ECHO,
|
||||
future=True,
|
||||
)
|
||||
|
||||
@@ -38,4 +39,4 @@ async def init_db():
|
||||
|
||||
async def close_db():
|
||||
"""Закрытие соединения с базой данных"""
|
||||
await engine.dispose()
|
||||
await engine.dispose()
|
||||
|
||||
@@ -5,6 +5,7 @@ from .models import User, Account, Winner, WinnerVerification
|
||||
from typing import Optional, List
|
||||
from datetime import datetime, timezone, timedelta
|
||||
import secrets
|
||||
from ..utils.account_utils import format_account_number
|
||||
|
||||
|
||||
class RegistrationService:
|
||||
@@ -78,6 +79,10 @@ class AccountService:
|
||||
account_number: str
|
||||
) -> Account:
|
||||
"""Создать новый счет для пользователя по номеру клубной карты"""
|
||||
formatted_account = format_account_number(account_number)
|
||||
if not formatted_account:
|
||||
raise ValueError(f"Некорректный формат счета: {account_number}")
|
||||
|
||||
# Находим владельца по клубной карте
|
||||
user_result = await session.execute(
|
||||
select(User).where(User.club_card_number == club_card_number)
|
||||
@@ -89,14 +94,14 @@ class AccountService:
|
||||
|
||||
# Проверяем, не существует ли уже такой счет
|
||||
existing = await session.execute(
|
||||
select(Account).where(Account.account_number == account_number)
|
||||
select(Account).where(Account.account_number == formatted_account)
|
||||
)
|
||||
if existing.scalar_one_or_none():
|
||||
raise ValueError(f"Счет {account_number} уже существует")
|
||||
raise ValueError(f"Счет {formatted_account} уже существует")
|
||||
|
||||
# Создаем счет
|
||||
account = Account(
|
||||
account_number=account_number,
|
||||
account_number=formatted_account,
|
||||
owner_id=user.id,
|
||||
is_active=True
|
||||
)
|
||||
@@ -112,10 +117,14 @@ class AccountService:
|
||||
account_number: str
|
||||
) -> Optional[User]:
|
||||
"""Найти владельца счета"""
|
||||
formatted_account = format_account_number(account_number)
|
||||
if not formatted_account:
|
||||
return None
|
||||
|
||||
result = await session.execute(
|
||||
select(Account).where(
|
||||
and_(
|
||||
Account.account_number == account_number,
|
||||
Account.account_number == formatted_account,
|
||||
Account.is_active == True
|
||||
)
|
||||
)
|
||||
@@ -150,8 +159,12 @@ class AccountService:
|
||||
account_number: str
|
||||
) -> bool:
|
||||
"""Деактивировать счет"""
|
||||
formatted_account = format_account_number(account_number)
|
||||
if not formatted_account:
|
||||
return False
|
||||
|
||||
result = await session.execute(
|
||||
select(Account).where(Account.account_number == account_number)
|
||||
select(Account).where(Account.account_number == formatted_account)
|
||||
)
|
||||
account = result.scalar_one_or_none()
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@ from .models import User, Lottery, Participation, Winner, Account
|
||||
from typing import List, Optional, Dict, Any
|
||||
from ..utils.account_utils import validate_account_number, format_account_number
|
||||
import random
|
||||
from types import SimpleNamespace
|
||||
|
||||
|
||||
class UserService:
|
||||
@@ -129,27 +130,39 @@ class UserService:
|
||||
|
||||
@staticmethod
|
||||
async def set_account_number(session: AsyncSession, telegram_id: int, account_number: str) -> bool:
|
||||
"""Установить номер клиентского счета пользователю"""
|
||||
"""Привязать номер клиентского счета к пользователю.
|
||||
|
||||
Исторически номер счета хранился в User.account_number. В актуальной
|
||||
схеме счета живут в отдельной таблице Account, поэтому метод оставлен
|
||||
как совместимый фасад для старых обработчиков и тестов.
|
||||
"""
|
||||
# Валидируем и форматируем номер
|
||||
formatted_number = format_account_number(account_number)
|
||||
if not formatted_number:
|
||||
return False
|
||||
|
||||
# Проверяем уникальность номера
|
||||
|
||||
user = await UserService.get_user_by_telegram_id(session, telegram_id)
|
||||
if not user:
|
||||
return False
|
||||
|
||||
existing = await session.execute(
|
||||
select(User).where(User.account_number == formatted_number)
|
||||
)
|
||||
if existing.scalar_one_or_none():
|
||||
return False # Номер уже занят
|
||||
|
||||
# Обновляем пользователя
|
||||
result = await session.execute(
|
||||
update(User)
|
||||
.where(User.telegram_id == telegram_id)
|
||||
.values(account_number=formatted_number)
|
||||
select(Account).where(Account.account_number == formatted_number)
|
||||
)
|
||||
account = existing.scalar_one_or_none()
|
||||
if account:
|
||||
if account.owner_id != user.id:
|
||||
return False
|
||||
account.is_active = True
|
||||
else:
|
||||
session.add(Account(
|
||||
account_number=formatted_number,
|
||||
owner_id=user.id,
|
||||
is_active=True
|
||||
))
|
||||
|
||||
user.account_number = formatted_number
|
||||
await session.commit()
|
||||
return result.rowcount > 0
|
||||
return True
|
||||
|
||||
@staticmethod
|
||||
async def get_user_by_account(session: AsyncSession, account_number: str) -> Optional[User]:
|
||||
@@ -157,6 +170,22 @@ class UserService:
|
||||
formatted_number = format_account_number(account_number)
|
||||
if not formatted_number:
|
||||
return None
|
||||
|
||||
result = await session.execute(
|
||||
select(User, Account.account_number)
|
||||
.join(Account, Account.owner_id == User.id)
|
||||
.where(
|
||||
Account.account_number == formatted_number,
|
||||
Account.is_active == True
|
||||
)
|
||||
)
|
||||
row = result.one_or_none()
|
||||
if not row:
|
||||
return None
|
||||
|
||||
user, account = row
|
||||
user.account_number = account
|
||||
return user
|
||||
|
||||
@staticmethod
|
||||
async def get_user_by_club_card(session: AsyncSession, club_card_number: str) -> Optional[User]:
|
||||
@@ -174,12 +203,6 @@ class UserService:
|
||||
select(User).where(User.club_card_number == club_card_number)
|
||||
)
|
||||
return result.scalar_one_or_none()
|
||||
|
||||
result = await session.execute(
|
||||
select(User).where(User.account_number == formatted_number)
|
||||
)
|
||||
return result.scalar_one_or_none()
|
||||
|
||||
@staticmethod
|
||||
async def search_by_account(session: AsyncSession, account_pattern: str) -> List[User]:
|
||||
"""Поиск пользователей по части номера счета"""
|
||||
@@ -189,11 +212,17 @@ class UserService:
|
||||
return []
|
||||
|
||||
result = await session.execute(
|
||||
select(User).where(
|
||||
User.account_number.like(f'%{clean_pattern}%')
|
||||
).limit(20)
|
||||
select(User, Account.account_number)
|
||||
.join(Account, Account.owner_id == User.id)
|
||||
.where(Account.account_number.like(f'%{clean_pattern}%'))
|
||||
.limit(20)
|
||||
)
|
||||
return result.scalars().all()
|
||||
|
||||
users = []
|
||||
for user, account_number in result.all():
|
||||
user.account_number = account_number
|
||||
users.append(user)
|
||||
return users
|
||||
|
||||
|
||||
class LotteryService:
|
||||
@@ -307,15 +336,25 @@ class LotteryService:
|
||||
participants = []
|
||||
for p in lottery.participations:
|
||||
if p.user:
|
||||
participants.append(p.user)
|
||||
participants.append(SimpleNamespace(
|
||||
id=p.user.id,
|
||||
telegram_id=p.user.telegram_id,
|
||||
username=p.user.username,
|
||||
first_name=p.user.first_name,
|
||||
last_name=p.user.last_name,
|
||||
account_number=p.account_number
|
||||
))
|
||||
else:
|
||||
# Создаем временный объект для участников без пользователя
|
||||
# Храним только номер счета
|
||||
participants.append(type('obj', (object,), {
|
||||
'id': None,
|
||||
'telegram_id': None,
|
||||
'account_number': p.account_number
|
||||
})())
|
||||
participants.append(SimpleNamespace(
|
||||
id=None,
|
||||
telegram_id=None,
|
||||
username=None,
|
||||
first_name=None,
|
||||
last_name=None,
|
||||
account_number=p.account_number
|
||||
))
|
||||
|
||||
logger.info(f"conduct_draw: участников {len(participants)}")
|
||||
if not participants:
|
||||
@@ -503,13 +542,22 @@ class ParticipationService:
|
||||
@staticmethod
|
||||
async def get_participants(session: AsyncSession, lottery_id: int, limit: Optional[int] = None, offset: int = 0) -> List[User]:
|
||||
"""Получить участников розыгрыша"""
|
||||
query = select(User).join(Participation).where(Participation.lottery_id == lottery_id)
|
||||
query = (
|
||||
select(Participation)
|
||||
.options(selectinload(Participation.user))
|
||||
.where(Participation.lottery_id == lottery_id)
|
||||
)
|
||||
|
||||
if limit:
|
||||
query = query.offset(offset).limit(limit)
|
||||
|
||||
result = await session.execute(query)
|
||||
return list(result.scalars().all())
|
||||
participants = []
|
||||
for participation in result.scalars().all():
|
||||
if participation.user:
|
||||
participation.user.account_number = participation.account_number
|
||||
participants.append(participation.user)
|
||||
return participants
|
||||
|
||||
@staticmethod
|
||||
async def get_user_participations(session: AsyncSession, user_id: int) -> List[Participation]:
|
||||
@@ -595,9 +643,6 @@ class ParticipationService:
|
||||
@staticmethod
|
||||
async def add_participants_by_accounts_bulk(session: AsyncSession, lottery_id: int, account_numbers: List[str]) -> Dict[str, Any]:
|
||||
"""Массовое добавление участников по номерам счетов"""
|
||||
import logging
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
results = {
|
||||
"added": 0,
|
||||
"skipped": 0,
|
||||
@@ -610,39 +655,30 @@ class ParticipationService:
|
||||
account_input = account_input.strip()
|
||||
if not account_input:
|
||||
continue
|
||||
|
||||
logger.info(f"DEBUG: Processing account_input={account_input!r}")
|
||||
|
||||
try:
|
||||
# Разделяем по пробелу: левая часть - номер карты, правая - номер счета
|
||||
parts = account_input.split()
|
||||
logger.info(f"DEBUG: After split: parts={parts}, len={len(parts)}")
|
||||
|
||||
if len(parts) == 2:
|
||||
card_number = parts[0] # Номер клубной карты
|
||||
account_number = parts[1] # Номер счета
|
||||
logger.info(f"DEBUG: 2 parts - card={card_number!r}, account={account_number!r}")
|
||||
elif len(parts) == 1:
|
||||
# Если нет пробела, считаем что это просто номер счета
|
||||
card_number = None
|
||||
account_number = parts[0]
|
||||
logger.info(f"DEBUG: 1 part - account={account_number!r}")
|
||||
else:
|
||||
logger.info(f"DEBUG: Invalid parts count={len(parts)}")
|
||||
results["invalid_accounts"].append(account_input)
|
||||
results["errors"].append(f"Неверный формат: {account_input}")
|
||||
continue
|
||||
|
||||
# Валидируем и форматируем номер счета
|
||||
logger.info(f"DEBUG: Before format_account_number: {account_number!r}")
|
||||
formatted_account = format_account_number(account_number)
|
||||
logger.info(f"DEBUG: After format_account_number: {formatted_account!r}")
|
||||
|
||||
if not formatted_account:
|
||||
card_info = f" (карта: {card_number})" if card_number else ""
|
||||
results["invalid_accounts"].append(account_input)
|
||||
results["errors"].append(f"Неверный формат счета: {account_number}{card_info}")
|
||||
logger.error(f"DEBUG: Format failed for {account_number!r}")
|
||||
continue
|
||||
|
||||
# Ищем владельца счёта через таблицу Account
|
||||
@@ -804,4 +840,4 @@ class ParticipationService:
|
||||
"participations_count": participations_count,
|
||||
"wins_count": wins_count,
|
||||
"last_participation": last_participation.created_at if last_participation else None
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,7 +6,6 @@ import asyncio
|
||||
import json
|
||||
from ..core.database import async_session_maker
|
||||
from ..core.services import LotteryService, ParticipationService
|
||||
from ..core.models import Lottery, User
|
||||
|
||||
async def conduct_lottery_draw(lottery_id: int):
|
||||
"""Проводим розыгрыш для указанного ID"""
|
||||
@@ -36,8 +35,8 @@ async def conduct_lottery_draw(lottery_id: int):
|
||||
# Выводим список участников
|
||||
print("\n👥 Список участников:")
|
||||
for i, user in enumerate(participants, 1):
|
||||
accounts = user.account_number.split(',') if user.account_number else ['Нет счетов']
|
||||
print(f" {i}. {user.first_name} (@{user.username}) - {len(accounts)} счет(ов)")
|
||||
username = f"@{user.username}" if user.username else "без username"
|
||||
print(f" {i}. {user.first_name or 'Без имени'} ({username})")
|
||||
|
||||
# Проводим розыгрыш
|
||||
print(f"\n🎲 Проводим розыгрыш...")
|
||||
@@ -48,14 +47,15 @@ async def conduct_lottery_draw(lottery_id: int):
|
||||
|
||||
if winners:
|
||||
print(f"\n🎉 Победители определены:")
|
||||
for i, winner_data in enumerate(winners, 1):
|
||||
for place, winner_data in winners.items():
|
||||
user = winner_data['user']
|
||||
prize = winner_data['prize']
|
||||
print(f" 🏆 {i} место: {user.first_name} (@{user.username})")
|
||||
name = user.first_name or getattr(user, "account_number", None) or "Неизвестный участник"
|
||||
username = f" (@{user.username})" if getattr(user, 'username', None) else ""
|
||||
print(f" 🏆 {place} место: {name}{username}")
|
||||
print(f" 💎 Приз: {prize}")
|
||||
|
||||
# Обновляем статус розыгрыша
|
||||
await LotteryService.set_lottery_completed(session, lottery_id, True)
|
||||
await session.commit()
|
||||
print(f"\n✅ Розыгрыш завершен и помечен как завершенный")
|
||||
|
||||
else:
|
||||
@@ -99,4 +99,4 @@ async def main():
|
||||
print("❌ Неверный ID розыгрыша")
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(main())
|
||||
asyncio.run(main())
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
"""
|
||||
import asyncio
|
||||
from ..core.database import async_session_maker, init_db
|
||||
from ..core.services import UserService, LotteryService
|
||||
from ..core.services import UserService, LotteryService, ParticipationService
|
||||
from ..utils.admin_utils import AdminUtils, ReportGenerator
|
||||
|
||||
|
||||
@@ -87,17 +87,17 @@ async def demo_admin_features():
|
||||
participants_added = 0
|
||||
for user in users:
|
||||
# В первый розыгрыш добавляем всех
|
||||
if await LotteryService.add_participant(session, lottery1.id, user.id):
|
||||
if await ParticipationService.add_participant(session, lottery1.id, user.id):
|
||||
participants_added += 1
|
||||
|
||||
# Во второй - половину
|
||||
if user.id % 2 == 0:
|
||||
if await LotteryService.add_participant(session, lottery2.id, user.id):
|
||||
if await ParticipationService.add_participant(session, lottery2.id, user.id):
|
||||
participants_added += 1
|
||||
|
||||
# В третий - треть
|
||||
if user.id % 3 == 0:
|
||||
if await LotteryService.add_participant(session, lottery3.id, user.id):
|
||||
if await ParticipationService.add_participant(session, lottery3.id, user.id):
|
||||
participants_added += 1
|
||||
|
||||
print(f"✅ Добавлено {participants_added} участий")
|
||||
@@ -127,6 +127,7 @@ async def demo_admin_features():
|
||||
|
||||
# Первый розыгрыш
|
||||
results1 = await LotteryService.conduct_draw(session, lottery1.id)
|
||||
await session.commit()
|
||||
print(f" 🎉 {lottery1.title} - результаты:")
|
||||
for place, winner_info in results1.items():
|
||||
user = winner_info['user']
|
||||
@@ -135,6 +136,7 @@ async def demo_admin_features():
|
||||
|
||||
# Второй розыгрыш
|
||||
results2 = await LotteryService.conduct_draw(session, lottery2.id)
|
||||
await session.commit()
|
||||
print(f" 🚗 {lottery2.title} - результаты:")
|
||||
for place, winner_info in results2.items():
|
||||
user = winner_info['user']
|
||||
@@ -188,4 +190,4 @@ async def demo_admin_features():
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(demo_admin_features())
|
||||
asyncio.run(demo_admin_features())
|
||||
|
||||
@@ -19,6 +19,7 @@ async def conduct_simple_draw():
|
||||
try:
|
||||
# Проводим розыгрыш
|
||||
winners = await LotteryService.conduct_draw(session, lottery_id)
|
||||
await session.commit()
|
||||
|
||||
print(f"🎉 Розыгрыш проведен!")
|
||||
print(f"📊 Результат: {winners}")
|
||||
@@ -29,4 +30,4 @@ async def conduct_simple_draw():
|
||||
traceback.print_exc()
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(conduct_simple_draw())
|
||||
asyncio.run(conduct_simple_draw())
|
||||
|
||||
@@ -6,6 +6,21 @@ from ..core.models import User, Lottery
|
||||
from ..utils.account_utils import mask_account_number
|
||||
|
||||
|
||||
def _get_user_account_number(user: User) -> Optional[str]:
|
||||
"""Вернуть номер счета из совместимого поля или уже загруженной связи."""
|
||||
account_number = getattr(user, 'account_number', None)
|
||||
if account_number:
|
||||
return account_number
|
||||
|
||||
# Не инициируем lazy-load в async SQLAlchemy: берем только уже загруженную связь.
|
||||
accounts = getattr(user, '__dict__', {}).get('accounts') or []
|
||||
for account in accounts:
|
||||
if getattr(account, 'is_active', True) and getattr(account, 'account_number', None):
|
||||
return account.account_number
|
||||
|
||||
return None
|
||||
|
||||
|
||||
def format_winner_display(user: User, lottery: Lottery, show_sensitive_data: bool = False) -> str:
|
||||
"""
|
||||
Форматирует отображение победителя в зависимости от настроек розыгрыша
|
||||
@@ -33,15 +48,16 @@ def format_winner_display(user: User, lottery: Lottery, show_sensitive_data: boo
|
||||
|
||||
elif display_type == 'account_number':
|
||||
# Отображаем номер клиентского счета
|
||||
if not user.account_number:
|
||||
account_number = _get_user_account_number(user)
|
||||
if not account_number:
|
||||
return "Счёт не указан"
|
||||
|
||||
if show_sensitive_data:
|
||||
# Для админов показываем полный номер
|
||||
return f"Счёт: {user.account_number}"
|
||||
return f"Счёт: {account_number}"
|
||||
else:
|
||||
# Для публичного показа маскируем номер
|
||||
masked = mask_account_number(user.account_number, show_last_digits=4)
|
||||
masked = mask_account_number(account_number, show_last_digits=4)
|
||||
return f"Счёт: {masked}"
|
||||
|
||||
else:
|
||||
@@ -113,4 +129,4 @@ def validate_display_type(display_type: str) -> bool:
|
||||
Returns:
|
||||
bool: True если тип корректен
|
||||
"""
|
||||
return display_type in ['username', 'chat_id', 'account_number']
|
||||
return display_type in ['username', 'chat_id', 'account_number']
|
||||
|
||||
@@ -179,7 +179,8 @@ def get_participant_management_keyboard() -> InlineKeyboardMarkup:
|
||||
[InlineKeyboardButton(text="📋 Список всех", callback_data="admin_list_all_participants"),
|
||||
InlineKeyboardButton(text="🔍 Поиск", callback_data="admin_search_participants")],
|
||||
[InlineKeyboardButton(text="📊 По розыгрышам", callback_data="admin_participants_by_lottery")],
|
||||
[InlineKeyboardButton(text="📄 Отчет", callback_data="admin_participants_report")],
|
||||
[InlineKeyboardButton(text="📄 Отчет", callback_data="admin_participants_report"),
|
||||
InlineKeyboardButton(text="🏦 Счета", callback_data="admin_participants_accounts_report")],
|
||||
[InlineKeyboardButton(text="◀️ Назад", callback_data="admin_panel")]
|
||||
]
|
||||
return InlineKeyboardMarkup(inline_keyboard=buttons)
|
||||
@@ -2359,16 +2360,16 @@ async def show_participants_by_lottery(callback: CallbackQuery):
|
||||
await callback.message.edit_text(text, reply_markup=InlineKeyboardMarkup(inline_keyboard=buttons))
|
||||
|
||||
|
||||
@admin_router.callback_query(F.data == "admin_participants_report")
|
||||
async def show_participants_report(callback: CallbackQuery):
|
||||
"""Отчет по участникам"""
|
||||
@admin_router.callback_query(F.data == "admin_participants_accounts_report")
|
||||
async def show_participants_accounts_report(callback: CallbackQuery):
|
||||
"""Отчет по участникам и привязанным счетам"""
|
||||
if not await check_admin_access(callback.from_user.id):
|
||||
await callback.answer("❌ Недостаточно прав", show_alert=True)
|
||||
return
|
||||
|
||||
async with async_session_maker() as session:
|
||||
from sqlalchemy import func, select
|
||||
from ..core.models import User, Participation, Lottery
|
||||
from ..core.models import User, Participation, Account
|
||||
|
||||
# Общая статистика по участникам
|
||||
total_participants = await session.scalar(
|
||||
@@ -2378,32 +2379,33 @@ async def show_participants_report(callback: CallbackQuery):
|
||||
)
|
||||
|
||||
total_participations = await session.scalar(select(func.count(Participation.id)))
|
||||
total_users = await session.scalar(select(func.count(User.id)))
|
||||
|
||||
# Топ активных участников
|
||||
top_participants = await session.execute(
|
||||
select(
|
||||
User.first_name,
|
||||
User.username,
|
||||
User.account_number,
|
||||
func.count(Participation.id).label('participations')
|
||||
func.min(Account.account_number).label('account_sample'),
|
||||
func.count(func.distinct(Participation.id)).label('participations')
|
||||
)
|
||||
.select_from(User)
|
||||
.join(Participation)
|
||||
.outerjoin(Account, Account.owner_id == User.id)
|
||||
.group_by(User.id)
|
||||
.order_by(func.count(Participation.id).desc())
|
||||
.order_by(func.count(func.distinct(Participation.id)).desc())
|
||||
.limit(10)
|
||||
)
|
||||
top_participants = top_participants.fetchall()
|
||||
|
||||
# Участники с аккаунтами vs без
|
||||
users_with_accounts = await session.scalar(
|
||||
select(func.count(User.id)).where(User.account_number.isnot(None))
|
||||
select(func.count(func.distinct(Account.owner_id)))
|
||||
)
|
||||
|
||||
users_without_accounts = await session.scalar(
|
||||
select(func.count(User.id)).where(User.account_number.is_(None))
|
||||
)
|
||||
users_without_accounts = max((total_users or 0) - (users_with_accounts or 0), 0)
|
||||
|
||||
text = "📈 Отчет по участникам\n\n"
|
||||
text = "📈 Отчет по участникам и счетам\n\n"
|
||||
text += f"👥 Всего уникальных участников: {total_participants}\n"
|
||||
text += f"📊 Всего участий: {total_participations}\n"
|
||||
text += f"🏦 С номерами счетов: {users_with_accounts}\n"
|
||||
@@ -2420,7 +2422,7 @@ async def show_participants_report(callback: CallbackQuery):
|
||||
await callback.message.edit_text(
|
||||
text,
|
||||
reply_markup=InlineKeyboardMarkup(inline_keyboard=[
|
||||
[InlineKeyboardButton(text="🔃 Обновить", callback_data="admin_participants_report")],
|
||||
[InlineKeyboardButton(text="🔃 Обновить", callback_data="admin_participants_accounts_report")],
|
||||
[InlineKeyboardButton(text="◀️ Назад", callback_data="admin_participants")]
|
||||
])
|
||||
)
|
||||
@@ -4827,7 +4829,7 @@ async def admin_broadcast_menu(callback: CallbackQuery, state: FSMContext):
|
||||
buttons = [
|
||||
[InlineKeyboardButton(text="✉️ Создать рассылку", callback_data="admin_broadcast_start")],
|
||||
[InlineKeyboardButton(text="📱 Управление каналами", callback_data="admin_broadcast_channels")],
|
||||
[InlineKeyboardButton(text="<EFBFBD> Статистика рассылок", callback_data="admin_broadcast_stats")],
|
||||
[InlineKeyboardButton(text="📊 Статистика рассылок", callback_data="admin_broadcast_stats")],
|
||||
[InlineKeyboardButton(text="◀️ Назад", callback_data="admin_panel")]
|
||||
]
|
||||
|
||||
@@ -6085,4 +6087,4 @@ async def confirm_remove_admin(callback: CallbackQuery, state: FSMContext):
|
||||
|
||||
|
||||
# Экспорт роутера
|
||||
__all__ = ['admin_router']
|
||||
__all__ = ['admin_router']
|
||||
|
||||
Reference in New Issue
Block a user