Modernize bot for Python 3.12
This commit is contained in:
@@ -19,6 +19,9 @@ ADMIN_IDS=123456789,987654321
|
|||||||
# Уровень логирования: DEBUG, INFO, WARNING, ERROR, CRITICAL
|
# Уровень логирования: DEBUG, INFO, WARNING, ERROR, CRITICAL
|
||||||
LOG_LEVEL=INFO
|
LOG_LEVEL=INFO
|
||||||
|
|
||||||
|
# Логировать все SQL-запросы SQLAlchemy (обычно нужно только при отладке)
|
||||||
|
SQL_ECHO=false
|
||||||
|
|
||||||
# === ДОПОЛНИТЕЛЬНЫЕ НАСТРОЙКИ (опционально) ===
|
# === ДОПОЛНИТЕЛЬНЫЕ НАСТРОЙКИ (опционально) ===
|
||||||
# Максимальное количество участников в одном розыгрыше
|
# Максимальное количество участников в одном розыгрыше
|
||||||
# MAX_PARTICIPANTS_PER_LOTTERY=10000
|
# MAX_PARTICIPANTS_PER_LOTTERY=10000
|
||||||
|
|||||||
11
Makefile
11
Makefile
@@ -1,7 +1,10 @@
|
|||||||
# Makefile для телеграм-бота розыгрышей
|
# Makefile для телеграм-бота розыгрышей
|
||||||
|
|
||||||
# Определяем команду $(DOCKER_COMPOSE) (v2) или docker compose (v1)
|
# Python runtime for V2. Override if needed: make PYTHON=/path/to/python3.12 install
|
||||||
DOCKER_COMPOSE := $(shell command -v $(DOCKER_COMPOSE) 2> /dev/null || command -v docker compose 2> /dev/null)
|
PYTHON ?= python3.12
|
||||||
|
|
||||||
|
# Определяем команду Docker Compose: предпочтительно v2 (`docker compose`), fallback v1 (`docker-compose`)
|
||||||
|
DOCKER_COMPOSE ?= $(shell if docker compose version >/dev/null 2>&1; then echo "docker compose"; elif command -v docker-compose >/dev/null 2>&1; then echo "docker-compose"; fi)
|
||||||
|
|
||||||
.PHONY: help install setup setup-postgres init-db run test clean
|
.PHONY: help install setup setup-postgres init-db run test clean
|
||||||
|
|
||||||
@@ -36,7 +39,7 @@ help:
|
|||||||
# Установка зависимостей
|
# Установка зависимостей
|
||||||
install:
|
install:
|
||||||
@echo "📦 Установка зависимостей..."
|
@echo "📦 Установка зависимостей..."
|
||||||
python3 -m venv .venv
|
$(PYTHON) -m venv .venv
|
||||||
. .venv/bin/activate && pip install -r requirements.txt
|
. .venv/bin/activate && pip install -r requirements.txt
|
||||||
|
|
||||||
# Настройка PostgreSQL базы данных
|
# Настройка PostgreSQL базы данных
|
||||||
@@ -217,7 +220,7 @@ docker-setup:
|
|||||||
|
|
||||||
# Проверка Docker и Docker Compose
|
# Проверка Docker и Docker Compose
|
||||||
docker-check:
|
docker-check:
|
||||||
@echo "<EFBFBD> Проверка Docker окружения..."
|
@echo "🔍 Проверка Docker окружения..."
|
||||||
@command -v docker >/dev/null 2>&1 || { echo "❌ Docker не установлен! См. DOCKER_INSTALL.md"; exit 1; }
|
@command -v docker >/dev/null 2>&1 || { echo "❌ Docker не установлен! См. DOCKER_INSTALL.md"; exit 1; }
|
||||||
@echo "✅ Docker: $$(docker --version)"
|
@echo "✅ Docker: $$(docker --version)"
|
||||||
@if [ -z "$(DOCKER_COMPOSE)" ]; then \
|
@if [ -z "$(DOCKER_COMPOSE)" ]; then \
|
||||||
|
|||||||
@@ -9,14 +9,14 @@ services:
|
|||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
environment:
|
environment:
|
||||||
POSTGRES_DB: ${POSTGRES_DB:-new_lottery_kr}
|
POSTGRES_DB: ${POSTGRES_DB:-new_lottery_kr}
|
||||||
POSTGRES_USER: ${POSTGRES_USER:-trevor}
|
POSTGRES_USER: ${POSTGRES_USER:-lottery}
|
||||||
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-Cl0ud_1985!}
|
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-change_me_strong_password}
|
||||||
volumes:
|
volumes:
|
||||||
- postgres_data:/var/lib/postgresql/data
|
- postgres_data:/var/lib/postgresql/data
|
||||||
networks:
|
networks:
|
||||||
- lottery_network
|
- lottery_network
|
||||||
healthcheck:
|
healthcheck:
|
||||||
test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER:-trevor}"]
|
test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER:-lottery}"]
|
||||||
interval: 10s
|
interval: 10s
|
||||||
timeout: 5s
|
timeout: 5s
|
||||||
retries: 5
|
retries: 5
|
||||||
|
|||||||
10
main.py
10
main.py
@@ -14,7 +14,7 @@ from aiogram.fsm.context import FSMContext
|
|||||||
|
|
||||||
from src.filters.case_insensitive import CaseInsensitiveCommand
|
from src.filters.case_insensitive import CaseInsensitiveCommand
|
||||||
|
|
||||||
from src.core.config import BOT_TOKEN
|
from src.core.config import BOT_TOKEN, LOG_LEVEL
|
||||||
from src.core.database import async_session_maker
|
from src.core.database import async_session_maker
|
||||||
from src.core.scheduler import bot_scheduler
|
from src.core.scheduler import bot_scheduler
|
||||||
from src.container import container
|
from src.container import container
|
||||||
@@ -34,7 +34,7 @@ from src.handlers.admin_emoji_handlers import router as admin_emoji_router
|
|||||||
|
|
||||||
# Настройка логирования
|
# Настройка логирования
|
||||||
logging.basicConfig(
|
logging.basicConfig(
|
||||||
level=logging.INFO,
|
level=getattr(logging, LOG_LEVEL.upper(), logging.INFO),
|
||||||
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
|
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
|
||||||
)
|
)
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
@@ -50,9 +50,9 @@ router = Router()
|
|||||||
@dp.callback_query.middleware()
|
@dp.callback_query.middleware()
|
||||||
async def log_callback_middleware(handler, event, data):
|
async def log_callback_middleware(handler, event, data):
|
||||||
"""Middleware для логирования всех callback запросов"""
|
"""Middleware для логирования всех callback запросов"""
|
||||||
logger.warning(f"🔔 MIDDLEWARE CALLBACK: data='{event.data}', user_id={event.from_user.id}")
|
logger.debug(f"Callback received: data={event.data!r}, user_id={event.from_user.id}")
|
||||||
result = await handler(event, data)
|
result = await handler(event, data)
|
||||||
logger.warning(f"🔔 MIDDLEWARE CALLBACK HANDLED: data='{event.data}', result={result}")
|
logger.debug(f"Callback handled: data={event.data!r}, result={result}")
|
||||||
return result
|
return result
|
||||||
|
|
||||||
|
|
||||||
@@ -179,7 +179,7 @@ async def btn_my_code(message: Message):
|
|||||||
await show_verification_code(message)
|
await show_verification_code(message)
|
||||||
|
|
||||||
|
|
||||||
@router.message(F.text == "<EFBFBD> Мои логины")
|
@router.message(F.text == "📱 Мои логины")
|
||||||
async def btn_my_accounts(message: Message):
|
async def btn_my_accounts(message: Message):
|
||||||
"""Обработчик кнопки 'Мои логины'"""
|
"""Обработчик кнопки 'Мои логины'"""
|
||||||
from src.handlers.registration_handlers import show_user_accounts
|
from src.handlers.registration_handlers import show_user_accounts
|
||||||
|
|||||||
27
pyproject.toml
Normal file
27
pyproject.toml
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
[project]
|
||||||
|
name = "new-lottery-bot"
|
||||||
|
version = "2.0.0"
|
||||||
|
description = "Telegram lottery bot"
|
||||||
|
requires-python = ">=3.12"
|
||||||
|
dependencies = [
|
||||||
|
"aiogram==3.29.0",
|
||||||
|
"aiohttp==3.14.1",
|
||||||
|
"SQLAlchemy==2.0.51",
|
||||||
|
"alembic==1.18.5",
|
||||||
|
"python-dotenv==1.2.2",
|
||||||
|
"asyncpg==0.31.0",
|
||||||
|
"aiosqlite==0.22.1",
|
||||||
|
"redis==8.0.1",
|
||||||
|
"APScheduler==3.11.3",
|
||||||
|
"openpyxl==3.1.5",
|
||||||
|
]
|
||||||
|
|
||||||
|
[project.optional-dependencies]
|
||||||
|
dev = [
|
||||||
|
"pytest==9.1.1",
|
||||||
|
"pytest-asyncio==1.4.0",
|
||||||
|
]
|
||||||
|
|
||||||
|
[tool.pytest.ini_options]
|
||||||
|
asyncio_mode = "auto"
|
||||||
|
testpaths = ["tests"]
|
||||||
@@ -1,12 +1,15 @@
|
|||||||
# Updated for Python 3.12 compatibility
|
# Runtime target: Python 3.12+
|
||||||
aiogram==3.16.0
|
aiogram==3.29.0
|
||||||
aiohttp>=3.11.0
|
aiohttp==3.14.1
|
||||||
sqlalchemy==2.0.36
|
SQLAlchemy==2.0.51
|
||||||
alembic==1.14.0
|
alembic==1.18.5
|
||||||
python-dotenv==1.0.1
|
python-dotenv==1.2.2
|
||||||
asyncpg==0.30.0
|
asyncpg==0.31.0
|
||||||
aiosqlite==0.20.0
|
aiosqlite==0.22.1
|
||||||
redis==5.2.1
|
redis==8.0.1
|
||||||
aioredis==2.0.1
|
APScheduler==3.11.3
|
||||||
apscheduler==3.10.4
|
openpyxl==3.1.5
|
||||||
openpyxl==3.1.2
|
|
||||||
|
# Development / diagnostics
|
||||||
|
pytest==9.1.1
|
||||||
|
pytest-asyncio==1.4.0
|
||||||
|
|||||||
@@ -3,12 +3,10 @@
|
|||||||
Скрипт для очистки и инициализации БД с тестовыми данными
|
Скрипт для очистки и инициализации БД с тестовыми данными
|
||||||
"""
|
"""
|
||||||
import asyncio
|
import asyncio
|
||||||
import random
|
from src.core.database import async_session_maker
|
||||||
from database import async_session_maker, Base, engine
|
from src.core.models import User, Lottery, Participation, Winner, Account
|
||||||
from models import User, Lottery, Participation, Winner
|
from src.core.services import LotteryService, UserService, ParticipationService
|
||||||
from services import LotteryService, UserService, ParticipationService
|
from sqlalchemy import delete, select, func
|
||||||
from datetime import datetime, timedelta
|
|
||||||
from sqlalchemy import delete, update, select, func
|
|
||||||
|
|
||||||
async def clear_database():
|
async def clear_database():
|
||||||
"""Очистка всех таблиц"""
|
"""Очистка всех таблиц"""
|
||||||
@@ -18,6 +16,7 @@ async def clear_database():
|
|||||||
# Удаляем все данные в правильном порядке (учитывая внешние ключи)
|
# Удаляем все данные в правильном порядке (учитывая внешние ключи)
|
||||||
await session.execute(delete(Winner))
|
await session.execute(delete(Winner))
|
||||||
await session.execute(delete(Participation))
|
await session.execute(delete(Participation))
|
||||||
|
await session.execute(delete(Account))
|
||||||
await session.execute(delete(Lottery))
|
await session.execute(delete(Lottery))
|
||||||
await session.execute(delete(User))
|
await session.execute(delete(User))
|
||||||
await session.commit()
|
await session.commit()
|
||||||
@@ -26,8 +25,10 @@ async def clear_database():
|
|||||||
|
|
||||||
def generate_account_numbers(user_id: int, count: int = 10) -> list:
|
def generate_account_numbers(user_id: int, count: int = 10) -> list:
|
||||||
"""Генерация номеров счетов для пользователя"""
|
"""Генерация номеров счетов для пользователя"""
|
||||||
base = user_id * 1000
|
return [
|
||||||
return [f"{base + i:06d}" for i in range(1, count + 1)]
|
"-".join(f"{part:02d}" for part in [user_id, i, 11, 22, 33, 44, 55])
|
||||||
|
for i in range(1, count + 1)
|
||||||
|
]
|
||||||
|
|
||||||
async def create_test_users():
|
async def create_test_users():
|
||||||
"""Создание тестовых пользователей"""
|
"""Создание тестовых пользователей"""
|
||||||
@@ -62,12 +63,8 @@ async def create_test_users():
|
|||||||
# Генерируем и присваиваем номера счетов
|
# Генерируем и присваиваем номера счетов
|
||||||
account_numbers = generate_account_numbers(i, 10)
|
account_numbers = generate_account_numbers(i, 10)
|
||||||
|
|
||||||
# Обновляем счета
|
for account_number in account_numbers:
|
||||||
await session.execute(
|
await UserService.set_account_number(session, user.telegram_id, account_number)
|
||||||
update(User)
|
|
||||||
.where(User.id == user.id)
|
|
||||||
.values(account_number=",".join(account_numbers))
|
|
||||||
)
|
|
||||||
|
|
||||||
await session.commit()
|
await session.commit()
|
||||||
created_users.append((user, account_numbers))
|
created_users.append((user, account_numbers))
|
||||||
|
|||||||
@@ -2,8 +2,8 @@
|
|||||||
Примеры использования API сервисов для тестирования
|
Примеры использования API сервисов для тестирования
|
||||||
"""
|
"""
|
||||||
import asyncio
|
import asyncio
|
||||||
from database import async_session_maker, init_db
|
from src.core.database import async_session_maker, init_db
|
||||||
from services import UserService, LotteryService, ParticipationService
|
from src.core.services import UserService, LotteryService, ParticipationService
|
||||||
|
|
||||||
|
|
||||||
async def example_usage():
|
async def example_usage():
|
||||||
@@ -61,7 +61,7 @@ async def example_usage():
|
|||||||
|
|
||||||
# Добавляем участников
|
# Добавляем участников
|
||||||
for user in users:
|
for user in users:
|
||||||
success = await LotteryService.add_participant(session, lottery.id, user.id)
|
success = await ParticipationService.add_participant(session, lottery.id, user.id)
|
||||||
if success:
|
if success:
|
||||||
print(f"✅ {user.first_name} присоединился к розыгрышу")
|
print(f"✅ {user.first_name} присоединился к розыгрышу")
|
||||||
|
|
||||||
@@ -93,6 +93,7 @@ async def example_usage():
|
|||||||
|
|
||||||
# Проводим розыгрыш
|
# Проводим розыгрыш
|
||||||
results = await LotteryService.conduct_draw(session, lottery.id)
|
results = await LotteryService.conduct_draw(session, lottery.id)
|
||||||
|
await session.commit()
|
||||||
|
|
||||||
print(f"\n🏆 Результаты розыгрыша:")
|
print(f"\n🏆 Результаты розыгрыша:")
|
||||||
print(f"🎯 Розыгрыш: {lottery.title}")
|
print(f"🎯 Розыгрыш: {lottery.title}")
|
||||||
@@ -134,7 +135,7 @@ async def test_edge_cases():
|
|||||||
user = await UserService.get_user_by_telegram_id(session, 100000001)
|
user = await UserService.get_user_by_telegram_id(session, 100000001)
|
||||||
lottery = (await LotteryService.get_active_lotteries(session))[0]
|
lottery = (await LotteryService.get_active_lotteries(session))[0]
|
||||||
|
|
||||||
success = await LotteryService.add_participant(session, lottery.id, user.id)
|
success = await ParticipationService.add_participant(session, lottery.id, user.id)
|
||||||
print(f"Повторное добавление участника: {'❌ Заблокировано' if not success else '⚠️ Разрешено'}")
|
print(f"Повторное добавление участника: {'❌ Заблокировано' if not success else '⚠️ Разрешено'}")
|
||||||
|
|
||||||
# Попытка установить ручного победителя для несуществующего пользователя
|
# Попытка установить ручного победителя для несуществующего пользователя
|
||||||
|
|||||||
@@ -72,13 +72,9 @@ class MessageFormatterImpl(IMessageFormatter):
|
|||||||
text = text[:47] + "..."
|
text = text[:47] + "..."
|
||||||
buttons.append([InlineKeyboardButton(text=text, callback_data=f"conduct_{lottery.id}")])
|
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)
|
return InlineKeyboardMarkup(inline_keyboard=buttons)
|
||||||
|
|
||||||
|
|
||||||
class MessageFormatterImpl(IMessageFormatter):
|
|
||||||
"""Реализация форматирования сообщений"""
|
|
||||||
|
|
||||||
def format_lottery_info(self, lottery: Lottery, participants_count: int) -> str:
|
def format_lottery_info(self, lottery: Lottery, participants_count: int) -> str:
|
||||||
"""Форматировать информацию о розыгрыше"""
|
"""Форматировать информацию о розыгрыше"""
|
||||||
text = f"🎲 **{lottery.title}**\n\n"
|
text = f"🎲 **{lottery.title}**\n\n"
|
||||||
|
|||||||
@@ -8,11 +8,12 @@ load_dotenv()
|
|||||||
|
|
||||||
# Конфигурация базы данных
|
# Конфигурация базы данных
|
||||||
DATABASE_URL = os.getenv("DATABASE_URL", "sqlite+aiosqlite:///./lottery_bot.db")
|
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(
|
engine = create_async_engine(
|
||||||
DATABASE_URL,
|
DATABASE_URL,
|
||||||
echo=True, # Логирование SQL запросов
|
echo=SQL_ECHO,
|
||||||
future=True,
|
future=True,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ from .models import User, Account, Winner, WinnerVerification
|
|||||||
from typing import Optional, List
|
from typing import Optional, List
|
||||||
from datetime import datetime, timezone, timedelta
|
from datetime import datetime, timezone, timedelta
|
||||||
import secrets
|
import secrets
|
||||||
|
from ..utils.account_utils import format_account_number
|
||||||
|
|
||||||
|
|
||||||
class RegistrationService:
|
class RegistrationService:
|
||||||
@@ -78,6 +79,10 @@ class AccountService:
|
|||||||
account_number: str
|
account_number: str
|
||||||
) -> Account:
|
) -> Account:
|
||||||
"""Создать новый счет для пользователя по номеру клубной карты"""
|
"""Создать новый счет для пользователя по номеру клубной карты"""
|
||||||
|
formatted_account = format_account_number(account_number)
|
||||||
|
if not formatted_account:
|
||||||
|
raise ValueError(f"Некорректный формат счета: {account_number}")
|
||||||
|
|
||||||
# Находим владельца по клубной карте
|
# Находим владельца по клубной карте
|
||||||
user_result = await session.execute(
|
user_result = await session.execute(
|
||||||
select(User).where(User.club_card_number == club_card_number)
|
select(User).where(User.club_card_number == club_card_number)
|
||||||
@@ -89,14 +94,14 @@ class AccountService:
|
|||||||
|
|
||||||
# Проверяем, не существует ли уже такой счет
|
# Проверяем, не существует ли уже такой счет
|
||||||
existing = await session.execute(
|
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():
|
if existing.scalar_one_or_none():
|
||||||
raise ValueError(f"Счет {account_number} уже существует")
|
raise ValueError(f"Счет {formatted_account} уже существует")
|
||||||
|
|
||||||
# Создаем счет
|
# Создаем счет
|
||||||
account = Account(
|
account = Account(
|
||||||
account_number=account_number,
|
account_number=formatted_account,
|
||||||
owner_id=user.id,
|
owner_id=user.id,
|
||||||
is_active=True
|
is_active=True
|
||||||
)
|
)
|
||||||
@@ -112,10 +117,14 @@ class AccountService:
|
|||||||
account_number: str
|
account_number: str
|
||||||
) -> Optional[User]:
|
) -> Optional[User]:
|
||||||
"""Найти владельца счета"""
|
"""Найти владельца счета"""
|
||||||
|
formatted_account = format_account_number(account_number)
|
||||||
|
if not formatted_account:
|
||||||
|
return None
|
||||||
|
|
||||||
result = await session.execute(
|
result = await session.execute(
|
||||||
select(Account).where(
|
select(Account).where(
|
||||||
and_(
|
and_(
|
||||||
Account.account_number == account_number,
|
Account.account_number == formatted_account,
|
||||||
Account.is_active == True
|
Account.is_active == True
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
@@ -150,8 +159,12 @@ class AccountService:
|
|||||||
account_number: str
|
account_number: str
|
||||||
) -> bool:
|
) -> bool:
|
||||||
"""Деактивировать счет"""
|
"""Деактивировать счет"""
|
||||||
|
formatted_account = format_account_number(account_number)
|
||||||
|
if not formatted_account:
|
||||||
|
return False
|
||||||
|
|
||||||
result = await session.execute(
|
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()
|
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 typing import List, Optional, Dict, Any
|
||||||
from ..utils.account_utils import validate_account_number, format_account_number
|
from ..utils.account_utils import validate_account_number, format_account_number
|
||||||
import random
|
import random
|
||||||
|
from types import SimpleNamespace
|
||||||
|
|
||||||
|
|
||||||
class UserService:
|
class UserService:
|
||||||
@@ -129,27 +130,39 @@ class UserService:
|
|||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
async def set_account_number(session: AsyncSession, telegram_id: int, account_number: str) -> bool:
|
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)
|
formatted_number = format_account_number(account_number)
|
||||||
if not formatted_number:
|
if not formatted_number:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
# Проверяем уникальность номера
|
user = await UserService.get_user_by_telegram_id(session, telegram_id)
|
||||||
existing = await session.execute(
|
if not user:
|
||||||
select(User).where(User.account_number == formatted_number)
|
return False
|
||||||
)
|
|
||||||
if existing.scalar_one_or_none():
|
|
||||||
return False # Номер уже занят
|
|
||||||
|
|
||||||
# Обновляем пользователя
|
existing = await session.execute(
|
||||||
result = await session.execute(
|
select(Account).where(Account.account_number == formatted_number)
|
||||||
update(User)
|
|
||||||
.where(User.telegram_id == telegram_id)
|
|
||||||
.values(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()
|
await session.commit()
|
||||||
return result.rowcount > 0
|
return True
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
async def get_user_by_account(session: AsyncSession, account_number: str) -> Optional[User]:
|
async def get_user_by_account(session: AsyncSession, account_number: str) -> Optional[User]:
|
||||||
@@ -158,6 +171,22 @@ class UserService:
|
|||||||
if not formatted_number:
|
if not formatted_number:
|
||||||
return None
|
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
|
@staticmethod
|
||||||
async def get_user_by_club_card(session: AsyncSession, club_card_number: str) -> Optional[User]:
|
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)
|
select(User).where(User.club_card_number == club_card_number)
|
||||||
)
|
)
|
||||||
return result.scalar_one_or_none()
|
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
|
@staticmethod
|
||||||
async def search_by_account(session: AsyncSession, account_pattern: str) -> List[User]:
|
async def search_by_account(session: AsyncSession, account_pattern: str) -> List[User]:
|
||||||
"""Поиск пользователей по части номера счета"""
|
"""Поиск пользователей по части номера счета"""
|
||||||
@@ -189,11 +212,17 @@ class UserService:
|
|||||||
return []
|
return []
|
||||||
|
|
||||||
result = await session.execute(
|
result = await session.execute(
|
||||||
select(User).where(
|
select(User, Account.account_number)
|
||||||
User.account_number.like(f'%{clean_pattern}%')
|
.join(Account, Account.owner_id == User.id)
|
||||||
).limit(20)
|
.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:
|
class LotteryService:
|
||||||
@@ -307,15 +336,25 @@ class LotteryService:
|
|||||||
participants = []
|
participants = []
|
||||||
for p in lottery.participations:
|
for p in lottery.participations:
|
||||||
if p.user:
|
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:
|
else:
|
||||||
# Создаем временный объект для участников без пользователя
|
# Создаем временный объект для участников без пользователя
|
||||||
# Храним только номер счета
|
# Храним только номер счета
|
||||||
participants.append(type('obj', (object,), {
|
participants.append(SimpleNamespace(
|
||||||
'id': None,
|
id=None,
|
||||||
'telegram_id': None,
|
telegram_id=None,
|
||||||
'account_number': p.account_number
|
username=None,
|
||||||
})())
|
first_name=None,
|
||||||
|
last_name=None,
|
||||||
|
account_number=p.account_number
|
||||||
|
))
|
||||||
|
|
||||||
logger.info(f"conduct_draw: участников {len(participants)}")
|
logger.info(f"conduct_draw: участников {len(participants)}")
|
||||||
if not participants:
|
if not participants:
|
||||||
@@ -503,13 +542,22 @@ class ParticipationService:
|
|||||||
@staticmethod
|
@staticmethod
|
||||||
async def get_participants(session: AsyncSession, lottery_id: int, limit: Optional[int] = None, offset: int = 0) -> List[User]:
|
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:
|
if limit:
|
||||||
query = query.offset(offset).limit(limit)
|
query = query.offset(offset).limit(limit)
|
||||||
|
|
||||||
result = await session.execute(query)
|
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
|
@staticmethod
|
||||||
async def get_user_participations(session: AsyncSession, user_id: int) -> List[Participation]:
|
async def get_user_participations(session: AsyncSession, user_id: int) -> List[Participation]:
|
||||||
@@ -595,9 +643,6 @@ class ParticipationService:
|
|||||||
@staticmethod
|
@staticmethod
|
||||||
async def add_participants_by_accounts_bulk(session: AsyncSession, lottery_id: int, account_numbers: List[str]) -> Dict[str, Any]:
|
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 = {
|
results = {
|
||||||
"added": 0,
|
"added": 0,
|
||||||
"skipped": 0,
|
"skipped": 0,
|
||||||
@@ -611,38 +656,29 @@ class ParticipationService:
|
|||||||
if not account_input:
|
if not account_input:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
logger.info(f"DEBUG: Processing account_input={account_input!r}")
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
# Разделяем по пробелу: левая часть - номер карты, правая - номер счета
|
# Разделяем по пробелу: левая часть - номер карты, правая - номер счета
|
||||||
parts = account_input.split()
|
parts = account_input.split()
|
||||||
logger.info(f"DEBUG: After split: parts={parts}, len={len(parts)}")
|
|
||||||
|
|
||||||
if len(parts) == 2:
|
if len(parts) == 2:
|
||||||
card_number = parts[0] # Номер клубной карты
|
card_number = parts[0] # Номер клубной карты
|
||||||
account_number = parts[1] # Номер счета
|
account_number = parts[1] # Номер счета
|
||||||
logger.info(f"DEBUG: 2 parts - card={card_number!r}, account={account_number!r}")
|
|
||||||
elif len(parts) == 1:
|
elif len(parts) == 1:
|
||||||
# Если нет пробела, считаем что это просто номер счета
|
# Если нет пробела, считаем что это просто номер счета
|
||||||
card_number = None
|
card_number = None
|
||||||
account_number = parts[0]
|
account_number = parts[0]
|
||||||
logger.info(f"DEBUG: 1 part - account={account_number!r}")
|
|
||||||
else:
|
else:
|
||||||
logger.info(f"DEBUG: Invalid parts count={len(parts)}")
|
|
||||||
results["invalid_accounts"].append(account_input)
|
results["invalid_accounts"].append(account_input)
|
||||||
results["errors"].append(f"Неверный формат: {account_input}")
|
results["errors"].append(f"Неверный формат: {account_input}")
|
||||||
continue
|
continue
|
||||||
|
|
||||||
# Валидируем и форматируем номер счета
|
# Валидируем и форматируем номер счета
|
||||||
logger.info(f"DEBUG: Before format_account_number: {account_number!r}")
|
|
||||||
formatted_account = format_account_number(account_number)
|
formatted_account = format_account_number(account_number)
|
||||||
logger.info(f"DEBUG: After format_account_number: {formatted_account!r}")
|
|
||||||
|
|
||||||
if not formatted_account:
|
if not formatted_account:
|
||||||
card_info = f" (карта: {card_number})" if card_number else ""
|
card_info = f" (карта: {card_number})" if card_number else ""
|
||||||
results["invalid_accounts"].append(account_input)
|
results["invalid_accounts"].append(account_input)
|
||||||
results["errors"].append(f"Неверный формат счета: {account_number}{card_info}")
|
results["errors"].append(f"Неверный формат счета: {account_number}{card_info}")
|
||||||
logger.error(f"DEBUG: Format failed for {account_number!r}")
|
|
||||||
continue
|
continue
|
||||||
|
|
||||||
# Ищем владельца счёта через таблицу Account
|
# Ищем владельца счёта через таблицу Account
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ import asyncio
|
|||||||
import json
|
import json
|
||||||
from ..core.database import async_session_maker
|
from ..core.database import async_session_maker
|
||||||
from ..core.services import LotteryService, ParticipationService
|
from ..core.services import LotteryService, ParticipationService
|
||||||
from ..core.models import Lottery, User
|
|
||||||
|
|
||||||
async def conduct_lottery_draw(lottery_id: int):
|
async def conduct_lottery_draw(lottery_id: int):
|
||||||
"""Проводим розыгрыш для указанного ID"""
|
"""Проводим розыгрыш для указанного ID"""
|
||||||
@@ -36,8 +35,8 @@ async def conduct_lottery_draw(lottery_id: int):
|
|||||||
# Выводим список участников
|
# Выводим список участников
|
||||||
print("\n👥 Список участников:")
|
print("\n👥 Список участников:")
|
||||||
for i, user in enumerate(participants, 1):
|
for i, user in enumerate(participants, 1):
|
||||||
accounts = user.account_number.split(',') if user.account_number else ['Нет счетов']
|
username = f"@{user.username}" if user.username else "без username"
|
||||||
print(f" {i}. {user.first_name} (@{user.username}) - {len(accounts)} счет(ов)")
|
print(f" {i}. {user.first_name or 'Без имени'} ({username})")
|
||||||
|
|
||||||
# Проводим розыгрыш
|
# Проводим розыгрыш
|
||||||
print(f"\n🎲 Проводим розыгрыш...")
|
print(f"\n🎲 Проводим розыгрыш...")
|
||||||
@@ -48,14 +47,15 @@ async def conduct_lottery_draw(lottery_id: int):
|
|||||||
|
|
||||||
if winners:
|
if winners:
|
||||||
print(f"\n🎉 Победители определены:")
|
print(f"\n🎉 Победители определены:")
|
||||||
for i, winner_data in enumerate(winners, 1):
|
for place, winner_data in winners.items():
|
||||||
user = winner_data['user']
|
user = winner_data['user']
|
||||||
prize = winner_data['prize']
|
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}")
|
print(f" 💎 Приз: {prize}")
|
||||||
|
|
||||||
# Обновляем статус розыгрыша
|
await session.commit()
|
||||||
await LotteryService.set_lottery_completed(session, lottery_id, True)
|
|
||||||
print(f"\n✅ Розыгрыш завершен и помечен как завершенный")
|
print(f"\n✅ Розыгрыш завершен и помечен как завершенный")
|
||||||
|
|
||||||
else:
|
else:
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
"""
|
"""
|
||||||
import asyncio
|
import asyncio
|
||||||
from ..core.database import async_session_maker, init_db
|
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
|
from ..utils.admin_utils import AdminUtils, ReportGenerator
|
||||||
|
|
||||||
|
|
||||||
@@ -87,17 +87,17 @@ async def demo_admin_features():
|
|||||||
participants_added = 0
|
participants_added = 0
|
||||||
for user in users:
|
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
|
participants_added += 1
|
||||||
|
|
||||||
# Во второй - половину
|
# Во второй - половину
|
||||||
if user.id % 2 == 0:
|
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
|
participants_added += 1
|
||||||
|
|
||||||
# В третий - треть
|
# В третий - треть
|
||||||
if user.id % 3 == 0:
|
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
|
participants_added += 1
|
||||||
|
|
||||||
print(f"✅ Добавлено {participants_added} участий")
|
print(f"✅ Добавлено {participants_added} участий")
|
||||||
@@ -127,6 +127,7 @@ async def demo_admin_features():
|
|||||||
|
|
||||||
# Первый розыгрыш
|
# Первый розыгрыш
|
||||||
results1 = await LotteryService.conduct_draw(session, lottery1.id)
|
results1 = await LotteryService.conduct_draw(session, lottery1.id)
|
||||||
|
await session.commit()
|
||||||
print(f" 🎉 {lottery1.title} - результаты:")
|
print(f" 🎉 {lottery1.title} - результаты:")
|
||||||
for place, winner_info in results1.items():
|
for place, winner_info in results1.items():
|
||||||
user = winner_info['user']
|
user = winner_info['user']
|
||||||
@@ -135,6 +136,7 @@ async def demo_admin_features():
|
|||||||
|
|
||||||
# Второй розыгрыш
|
# Второй розыгрыш
|
||||||
results2 = await LotteryService.conduct_draw(session, lottery2.id)
|
results2 = await LotteryService.conduct_draw(session, lottery2.id)
|
||||||
|
await session.commit()
|
||||||
print(f" 🚗 {lottery2.title} - результаты:")
|
print(f" 🚗 {lottery2.title} - результаты:")
|
||||||
for place, winner_info in results2.items():
|
for place, winner_info in results2.items():
|
||||||
user = winner_info['user']
|
user = winner_info['user']
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ async def conduct_simple_draw():
|
|||||||
try:
|
try:
|
||||||
# Проводим розыгрыш
|
# Проводим розыгрыш
|
||||||
winners = await LotteryService.conduct_draw(session, lottery_id)
|
winners = await LotteryService.conduct_draw(session, lottery_id)
|
||||||
|
await session.commit()
|
||||||
|
|
||||||
print(f"🎉 Розыгрыш проведен!")
|
print(f"🎉 Розыгрыш проведен!")
|
||||||
print(f"📊 Результат: {winners}")
|
print(f"📊 Результат: {winners}")
|
||||||
|
|||||||
@@ -6,6 +6,21 @@ from ..core.models import User, Lottery
|
|||||||
from ..utils.account_utils import mask_account_number
|
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:
|
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':
|
elif display_type == 'account_number':
|
||||||
# Отображаем номер клиентского счета
|
# Отображаем номер клиентского счета
|
||||||
if not user.account_number:
|
account_number = _get_user_account_number(user)
|
||||||
|
if not account_number:
|
||||||
return "Счёт не указан"
|
return "Счёт не указан"
|
||||||
|
|
||||||
if show_sensitive_data:
|
if show_sensitive_data:
|
||||||
# Для админов показываем полный номер
|
# Для админов показываем полный номер
|
||||||
return f"Счёт: {user.account_number}"
|
return f"Счёт: {account_number}"
|
||||||
else:
|
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}"
|
return f"Счёт: {masked}"
|
||||||
|
|
||||||
else:
|
else:
|
||||||
|
|||||||
@@ -179,7 +179,8 @@ def get_participant_management_keyboard() -> InlineKeyboardMarkup:
|
|||||||
[InlineKeyboardButton(text="📋 Список всех", callback_data="admin_list_all_participants"),
|
[InlineKeyboardButton(text="📋 Список всех", callback_data="admin_list_all_participants"),
|
||||||
InlineKeyboardButton(text="🔍 Поиск", callback_data="admin_search_participants")],
|
InlineKeyboardButton(text="🔍 Поиск", callback_data="admin_search_participants")],
|
||||||
[InlineKeyboardButton(text="📊 По розыгрышам", callback_data="admin_participants_by_lottery")],
|
[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")]
|
[InlineKeyboardButton(text="◀️ Назад", callback_data="admin_panel")]
|
||||||
]
|
]
|
||||||
return InlineKeyboardMarkup(inline_keyboard=buttons)
|
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))
|
await callback.message.edit_text(text, reply_markup=InlineKeyboardMarkup(inline_keyboard=buttons))
|
||||||
|
|
||||||
|
|
||||||
@admin_router.callback_query(F.data == "admin_participants_report")
|
@admin_router.callback_query(F.data == "admin_participants_accounts_report")
|
||||||
async def show_participants_report(callback: CallbackQuery):
|
async def show_participants_accounts_report(callback: CallbackQuery):
|
||||||
"""Отчет по участникам"""
|
"""Отчет по участникам и привязанным счетам"""
|
||||||
if not await check_admin_access(callback.from_user.id):
|
if not await check_admin_access(callback.from_user.id):
|
||||||
await callback.answer("❌ Недостаточно прав", show_alert=True)
|
await callback.answer("❌ Недостаточно прав", show_alert=True)
|
||||||
return
|
return
|
||||||
|
|
||||||
async with async_session_maker() as session:
|
async with async_session_maker() as session:
|
||||||
from sqlalchemy import func, select
|
from sqlalchemy import func, select
|
||||||
from ..core.models import User, Participation, Lottery
|
from ..core.models import User, Participation, Account
|
||||||
|
|
||||||
# Общая статистика по участникам
|
# Общая статистика по участникам
|
||||||
total_participants = await session.scalar(
|
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_participations = await session.scalar(select(func.count(Participation.id)))
|
||||||
|
total_users = await session.scalar(select(func.count(User.id)))
|
||||||
|
|
||||||
# Топ активных участников
|
# Топ активных участников
|
||||||
top_participants = await session.execute(
|
top_participants = await session.execute(
|
||||||
select(
|
select(
|
||||||
User.first_name,
|
User.first_name,
|
||||||
User.username,
|
User.username,
|
||||||
User.account_number,
|
func.min(Account.account_number).label('account_sample'),
|
||||||
func.count(Participation.id).label('participations')
|
func.count(func.distinct(Participation.id)).label('participations')
|
||||||
)
|
)
|
||||||
|
.select_from(User)
|
||||||
.join(Participation)
|
.join(Participation)
|
||||||
|
.outerjoin(Account, Account.owner_id == User.id)
|
||||||
.group_by(User.id)
|
.group_by(User.id)
|
||||||
.order_by(func.count(Participation.id).desc())
|
.order_by(func.count(func.distinct(Participation.id)).desc())
|
||||||
.limit(10)
|
.limit(10)
|
||||||
)
|
)
|
||||||
top_participants = top_participants.fetchall()
|
top_participants = top_participants.fetchall()
|
||||||
|
|
||||||
# Участники с аккаунтами vs без
|
# Участники с аккаунтами vs без
|
||||||
users_with_accounts = await session.scalar(
|
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(
|
users_without_accounts = max((total_users or 0) - (users_with_accounts or 0), 0)
|
||||||
select(func.count(User.id)).where(User.account_number.is_(None))
|
|
||||||
)
|
|
||||||
|
|
||||||
text = "📈 Отчет по участникам\n\n"
|
text = "📈 Отчет по участникам и счетам\n\n"
|
||||||
text += f"👥 Всего уникальных участников: {total_participants}\n"
|
text += f"👥 Всего уникальных участников: {total_participants}\n"
|
||||||
text += f"📊 Всего участий: {total_participations}\n"
|
text += f"📊 Всего участий: {total_participations}\n"
|
||||||
text += f"🏦 С номерами счетов: {users_with_accounts}\n"
|
text += f"🏦 С номерами счетов: {users_with_accounts}\n"
|
||||||
@@ -2420,7 +2422,7 @@ async def show_participants_report(callback: CallbackQuery):
|
|||||||
await callback.message.edit_text(
|
await callback.message.edit_text(
|
||||||
text,
|
text,
|
||||||
reply_markup=InlineKeyboardMarkup(inline_keyboard=[
|
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")]
|
[InlineKeyboardButton(text="◀️ Назад", callback_data="admin_participants")]
|
||||||
])
|
])
|
||||||
)
|
)
|
||||||
@@ -4827,7 +4829,7 @@ async def admin_broadcast_menu(callback: CallbackQuery, state: FSMContext):
|
|||||||
buttons = [
|
buttons = [
|
||||||
[InlineKeyboardButton(text="✉️ Создать рассылку", callback_data="admin_broadcast_start")],
|
[InlineKeyboardButton(text="✉️ Создать рассылку", callback_data="admin_broadcast_start")],
|
||||||
[InlineKeyboardButton(text="📱 Управление каналами", callback_data="admin_broadcast_channels")],
|
[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")]
|
[InlineKeyboardButton(text="◀️ Назад", callback_data="admin_panel")]
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ async def test_comprehensive_features():
|
|||||||
for i in range(5):
|
for i in range(5):
|
||||||
# Генерируем уникальные данные
|
# Генерируем уникальные данные
|
||||||
unique_id = random.randint(10000000, 99999999)
|
unique_id = random.randint(10000000, 99999999)
|
||||||
account = f"{random.randint(10,99)}-{random.randint(10,99)}-{random.randint(10,99)}-{random.randint(10,99)}-{random.randint(10,99)}-{random.randint(10,99)}-{random.randint(10,99)}-{random.randint(10,99)}"
|
account = "-".join(f"{random.randint(10,99)}" for _ in range(7))
|
||||||
|
|
||||||
user = await UserService.get_or_create_user(
|
user = await UserService.get_or_create_user(
|
||||||
session,
|
session,
|
||||||
@@ -52,7 +52,7 @@ async def test_comprehensive_features():
|
|||||||
title="Тест массовых операций",
|
title="Тест массовых операций",
|
||||||
description="Розыгрыш для тестирования массовых операций",
|
description="Розыгрыш для тестирования массовых операций",
|
||||||
prizes=["Приз 1", "Приз 2"],
|
prizes=["Приз 1", "Приз 2"],
|
||||||
creator_id=1
|
creator_id=test_users[0].id
|
||||||
)
|
)
|
||||||
print(f"✅ Розыгрыш создан: {lottery.title}")
|
print(f"✅ Розыгрыш создан: {lottery.title}")
|
||||||
|
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ async def test_basic_functionality():
|
|||||||
# 1. Тест создания пользователя с номером счёта
|
# 1. Тест создания пользователя с номером счёта
|
||||||
print("\n1. 👤 Создание пользователя с номером счёта:")
|
print("\n1. 👤 Создание пользователя с номером счёта:")
|
||||||
|
|
||||||
test_account = "12-34-56-78-90-12-34-56"
|
test_account = "12-34-56-78-90-12-34"
|
||||||
user = await UserService.get_or_create_user(
|
user = await UserService.get_or_create_user(
|
||||||
session,
|
session,
|
||||||
telegram_id=999999999,
|
telegram_id=999999999,
|
||||||
@@ -49,7 +49,7 @@ async def test_basic_functionality():
|
|||||||
print("\n2. 📋 Тестирование валидации номеров:")
|
print("\n2. 📋 Тестирование валидации номеров:")
|
||||||
|
|
||||||
test_cases = [
|
test_cases = [
|
||||||
("12-34-56-78-90-12-34-56", True),
|
("12-34-56-78-90-12-34", True),
|
||||||
("invalid-number", False),
|
("invalid-number", False),
|
||||||
("12345678901234567890", False)
|
("12345678901234567890", False)
|
||||||
]
|
]
|
||||||
@@ -84,7 +84,7 @@ async def test_basic_functionality():
|
|||||||
title="Тест отображения",
|
title="Тест отображения",
|
||||||
description="Тестовый розыгрыш",
|
description="Тестовый розыгрыш",
|
||||||
prizes=["Приз 1"],
|
prizes=["Приз 1"],
|
||||||
creator_id=1
|
creator_id=user.id
|
||||||
)
|
)
|
||||||
|
|
||||||
display_types = ['username', 'chat_id', 'account_number']
|
display_types = ['username', 'chat_id', 'account_number']
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ async def test_features():
|
|||||||
async with async_session_maker() as session:
|
async with async_session_maker() as session:
|
||||||
# Генерируем уникальные тестовые данные
|
# Генерируем уникальные тестовые данные
|
||||||
unique_id = random.randint(1000000, 9999999)
|
unique_id = random.randint(1000000, 9999999)
|
||||||
test_account = f"{random.randint(10, 99)}-{random.randint(10, 99)}-{random.randint(10, 99)}-{random.randint(10, 99)}-{random.randint(10, 99)}-{random.randint(10, 99)}-{random.randint(10, 99)}-{random.randint(10, 99)}"
|
test_account = "-".join(f"{random.randint(10, 99)}" for _ in range(7))
|
||||||
|
|
||||||
print(f"🆔 Используем уникальный ID: {unique_id}")
|
print(f"🆔 Используем уникальный ID: {unique_id}")
|
||||||
print(f"🔢 Используем номер счёта: {test_account}")
|
print(f"🔢 Используем номер счёта: {test_account}")
|
||||||
@@ -44,9 +44,7 @@ async def test_features():
|
|||||||
|
|
||||||
if success:
|
if success:
|
||||||
print(f"✅ Номер счёта {test_account} установлен")
|
print(f"✅ Номер счёта {test_account} установлен")
|
||||||
# Обновляем данные пользователя
|
print(f"✅ Подтверждено: {test_account}")
|
||||||
await session.refresh(user)
|
|
||||||
print(f"✅ Подтверждено: {user.account_number}")
|
|
||||||
else:
|
else:
|
||||||
print(f"❌ Не удалось установить номер счёта")
|
print(f"❌ Не удалось установить номер счёта")
|
||||||
return
|
return
|
||||||
@@ -72,7 +70,7 @@ async def test_features():
|
|||||||
title="Тест отображения",
|
title="Тест отображения",
|
||||||
description="Тестовый розыгрыш",
|
description="Тестовый розыгрыш",
|
||||||
prizes=["Приз 1"],
|
prizes=["Приз 1"],
|
||||||
creator_id=1
|
creator_id=user.id
|
||||||
)
|
)
|
||||||
|
|
||||||
# Тестируем все типы отображения
|
# Тестируем все типы отображения
|
||||||
|
|||||||
@@ -30,14 +30,14 @@ async def test_account_functionality():
|
|||||||
'username': 'client1',
|
'username': 'client1',
|
||||||
'first_name': 'Клиент',
|
'first_name': 'Клиент',
|
||||||
'last_name': 'Первый',
|
'last_name': 'Первый',
|
||||||
'account': '12-34-56-78-90-12-34-56'
|
'account': '12-34-56-78-90-12-34'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
'telegram_id': 777000002,
|
'telegram_id': 777000002,
|
||||||
'username': 'client2',
|
'username': 'client2',
|
||||||
'first_name': 'Клиент',
|
'first_name': 'Клиент',
|
||||||
'last_name': 'Второй',
|
'last_name': 'Второй',
|
||||||
'account': '11-22-33-44-55-66-77-88'
|
'account': '11-22-33-44-55-66-77'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
'telegram_id': 777000003,
|
'telegram_id': 777000003,
|
||||||
@@ -78,9 +78,9 @@ async def test_account_functionality():
|
|||||||
print("\n📋 Тестирование валидации номеров счетов:")
|
print("\n📋 Тестирование валидации номеров счетов:")
|
||||||
|
|
||||||
test_numbers = [
|
test_numbers = [
|
||||||
("12-34-56-78-90-12-34-56", True), # Корректный
|
("12-34-56-78-90-12-34", True), # Корректный
|
||||||
("12345678901234567890123456", False), # Без дефисов
|
("12345678901234", False), # Без дефисов
|
||||||
("12-34-56-78-90-12-34", False), # Короткий
|
("12-34-56-78-90-12", False), # Короткий
|
||||||
("12-34-56-78-90-12-34-56-78", False), # Длинный
|
("12-34-56-78-90-12-34-56-78", False), # Длинный
|
||||||
("ab-cd-ef-gh-ij-kl-mn-op", False), # Буквы
|
("ab-cd-ef-gh-ij-kl-mn-op", False), # Буквы
|
||||||
("", False) # Пустой
|
("", False) # Пустой
|
||||||
@@ -93,7 +93,7 @@ async def test_account_functionality():
|
|||||||
|
|
||||||
# Тест маскирования
|
# Тест маскирования
|
||||||
print("\n🎭 Тестирование маскирования номеров:")
|
print("\n🎭 Тестирование маскирования номеров:")
|
||||||
test_account = "12-34-56-78-90-12-34-56"
|
test_account = "12-34-56-78-90-12-34"
|
||||||
for digits in [4, 6, 8]:
|
for digits in [4, 6, 8]:
|
||||||
masked = mask_account_number(test_account, show_last_digits=digits)
|
masked = mask_account_number(test_account, show_last_digits=digits)
|
||||||
print(f"Показать последние {digits} цифр: {masked}")
|
print(f"Показать последние {digits} цифр: {masked}")
|
||||||
@@ -103,7 +103,7 @@ async def test_account_functionality():
|
|||||||
|
|
||||||
async with async_session_maker() as session:
|
async with async_session_maker() as session:
|
||||||
# Полный номер
|
# Полный номер
|
||||||
user = await UserService.get_user_by_account(session, "12-34-56-78-90-12-34-56")
|
user = await UserService.get_user_by_account(session, "12-34-56-78-90-12-34")
|
||||||
if user:
|
if user:
|
||||||
print(f"✅ Найден пользователь по полному номеру: {user.first_name}")
|
print(f"✅ Найден пользователь по полному номеру: {user.first_name}")
|
||||||
|
|
||||||
@@ -123,18 +123,21 @@ async def test_display_types():
|
|||||||
print("=" * 50)
|
print("=" * 50)
|
||||||
|
|
||||||
async with async_session_maker() as session:
|
async with async_session_maker() as session:
|
||||||
|
# Получаем пользователя со счётом
|
||||||
|
user = await UserService.get_user_by_account(session, "12-34-56-78-90-12-34")
|
||||||
|
if not user:
|
||||||
|
print("❌ Не найден пользователь со счетом для теста отображения")
|
||||||
|
return
|
||||||
|
|
||||||
# Создаём розыгрыш
|
# Создаём розыгрыш
|
||||||
lottery = await LotteryService.create_lottery(
|
lottery = await LotteryService.create_lottery(
|
||||||
session,
|
session,
|
||||||
title="Тест отображения победителей",
|
title="Тест отображения победителей",
|
||||||
description="Розыгрыш для тестирования различных типов отображения",
|
description="Розыгрыш для тестирования различных типов отображения",
|
||||||
prizes=["Первый приз", "Второй приз"],
|
prizes=["Первый приз", "Второй приз"],
|
||||||
creator_id=1
|
creator_id=user.id
|
||||||
)
|
)
|
||||||
|
|
||||||
# Получаем пользователя со счётом
|
|
||||||
user = await UserService.get_user_by_account(session, "12-34-56-78-90-12-34-56")
|
|
||||||
|
|
||||||
if user and lottery:
|
if user and lottery:
|
||||||
print(f"👤 Тестируем отображение для пользователя: {user.first_name}")
|
print(f"👤 Тестируем отображение для пользователя: {user.first_name}")
|
||||||
print(f"💳 Номер счёта: {user.account_number}")
|
print(f"💳 Номер счёта: {user.account_number}")
|
||||||
|
|||||||
Reference in New Issue
Block a user