Stabilize staff concurrency and premium emoji; enable verified Drone deployment
Some checks failed
continuous-integration/drone/push Build is failing
Some checks failed
continuous-integration/drone/push Build is failing
This commit is contained in:
@@ -11,39 +11,22 @@ class UserService:
|
||||
"""Сервис для работы с пользователями"""
|
||||
|
||||
@staticmethod
|
||||
async def get_or_create_user(session: AsyncSession, telegram_id: int,
|
||||
username: str = None, first_name: str = None,
|
||||
last_name: str = None, nickname: str = None) -> User:
|
||||
"""Получить или создать пользователя"""
|
||||
# Пробуем найти существующего пользователя
|
||||
result = await session.execute(
|
||||
select(User).where(User.telegram_id == telegram_id)
|
||||
)
|
||||
user = result.scalar_one_or_none()
|
||||
|
||||
if user:
|
||||
# Обновляем информацию о пользователе
|
||||
user.username = username
|
||||
user.first_name = first_name
|
||||
user.last_name = last_name
|
||||
# Обновляем nickname только если он передан
|
||||
if nickname is not None:
|
||||
user.nickname = nickname
|
||||
await session.commit()
|
||||
return user
|
||||
|
||||
# Создаем нового пользователя
|
||||
user = User(
|
||||
telegram_id=telegram_id,
|
||||
username=username,
|
||||
first_name=first_name,
|
||||
last_name=last_name,
|
||||
nickname=nickname
|
||||
)
|
||||
session.add(user)
|
||||
async def get_or_create_user(session, telegram_id, username=None, first_name=None, last_name=None, nickname=None):
|
||||
"""Upsert without racing or erasing metadata when only an ID is supplied."""
|
||||
from sqlalchemy.dialects.postgresql import insert as pg_insert
|
||||
from sqlalchemy.dialects.sqlite import insert as sqlite_insert
|
||||
insert = pg_insert if session.bind.dialect.name == "postgresql" else sqlite_insert
|
||||
values = {key: value for key, value in dict(username=username, first_name=first_name,
|
||||
last_name=last_name, nickname=nickname).items() if value is not None}
|
||||
stmt = insert(User).values(telegram_id=telegram_id, **values)
|
||||
if values:
|
||||
stmt = stmt.on_conflict_do_update(index_elements=[User.telegram_id], set_=values)
|
||||
else:
|
||||
stmt = stmt.on_conflict_do_nothing(index_elements=[User.telegram_id])
|
||||
await session.execute(stmt)
|
||||
await session.commit()
|
||||
await session.refresh(user)
|
||||
return user
|
||||
return (await session.scalars(select(User).options(selectinload(User.accounts))
|
||||
.where(User.telegram_id == telegram_id).execution_options(populate_existing=True))).one()
|
||||
|
||||
@staticmethod
|
||||
async def get_user_by_telegram_id(session: AsyncSession, telegram_id: int) -> Optional[User]:
|
||||
@@ -101,6 +84,14 @@ class UserService:
|
||||
if not user:
|
||||
return False
|
||||
|
||||
# Keep records needed by draws, claims, chat history, or staff audit trails.
|
||||
# Cleanup may delete only users without linked data; moderation uses bans.
|
||||
from .database import Base
|
||||
for table in Base.metadata.sorted_tables:
|
||||
for foreign_key in table.foreign_keys:
|
||||
if foreign_key.column.table.name == "users":
|
||||
if await session.scalar(select(table.c.id).where(foreign_key.parent == user_id).limit(1)):
|
||||
return False
|
||||
# Удаляем все участия
|
||||
await session.execute(
|
||||
delete(Participation).where(Participation.user_id == user_id)
|
||||
@@ -128,72 +119,41 @@ class UserService:
|
||||
return result.rowcount > 0
|
||||
|
||||
@staticmethod
|
||||
async def set_account_number(session: AsyncSession, telegram_id: int, account_number: str) -> bool:
|
||||
"""Установить номер клиентского счета пользователю"""
|
||||
# Валидируем и форматируем номер
|
||||
formatted_number = format_account_number(account_number)
|
||||
if not formatted_number:
|
||||
async def set_account_number(session, telegram_id, account_number):
|
||||
from sqlalchemy.exc import IntegrityError
|
||||
formatted = format_account_number(account_number)
|
||||
user = await UserService.get_user_by_telegram_id(session, telegram_id)
|
||||
if not formatted or 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)
|
||||
)
|
||||
await session.commit()
|
||||
return result.rowcount > 0
|
||||
if await session.scalar(select(Account.id).where(Account.account_number == formatted)):
|
||||
return False
|
||||
session.add(Account(account_number=formatted, owner_id=user.id))
|
||||
try:
|
||||
await session.commit()
|
||||
except IntegrityError:
|
||||
await session.rollback()
|
||||
return False
|
||||
return True
|
||||
|
||||
@staticmethod
|
||||
async def get_user_by_account(session: AsyncSession, account_number: str) -> Optional[User]:
|
||||
"""Получить пользователя по номеру счета"""
|
||||
formatted_number = format_account_number(account_number)
|
||||
if not formatted_number:
|
||||
async def get_user_by_account(session, account_number):
|
||||
formatted = format_account_number(account_number)
|
||||
if not formatted:
|
||||
return None
|
||||
return await session.scalar(select(User).join(Account).options(selectinload(User.accounts)).where(
|
||||
Account.account_number == formatted, Account.is_active.is_(True)))
|
||||
|
||||
@staticmethod
|
||||
async def get_user_by_club_card(session: AsyncSession, club_card_number: str) -> Optional[User]:
|
||||
"""
|
||||
Получить пользователя по номеру клубной карты
|
||||
|
||||
Args:
|
||||
session: Сессия БД
|
||||
club_card_number: Номер клубной карты (4 цифры)
|
||||
|
||||
Returns:
|
||||
User или None если не найден
|
||||
"""
|
||||
result = await session.execute(
|
||||
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()
|
||||
async def get_user_by_club_card(session, club_card_number):
|
||||
return await session.scalar(select(User).where(User.club_card_number == club_card_number))
|
||||
|
||||
@staticmethod
|
||||
async def search_by_account(session: AsyncSession, account_pattern: str) -> List[User]:
|
||||
"""Поиск пользователей по части номера счета"""
|
||||
# Убираем все кроме цифр и дефисов
|
||||
clean_pattern = ''.join(c for c in account_pattern if c.isdigit() or c == '-')
|
||||
if not clean_pattern:
|
||||
async def search_by_account(session, account_pattern):
|
||||
clean = ''.join(c for c in account_pattern if c in '0123456789-')
|
||||
if not clean:
|
||||
return []
|
||||
|
||||
result = await session.execute(
|
||||
select(User).where(
|
||||
User.account_number.like(f'%{clean_pattern}%')
|
||||
).limit(20)
|
||||
)
|
||||
return result.scalars().all()
|
||||
return list((await session.scalars(select(User).join(Account).distinct()
|
||||
.where(Account.account_number.contains(clean, autoescape=True)).limit(20))).all())
|
||||
|
||||
|
||||
class LotteryService:
|
||||
@@ -221,6 +181,7 @@ class LotteryService:
|
||||
select(Lottery)
|
||||
.options(selectinload(Lottery.participations).selectinload(Participation.user))
|
||||
.where(Lottery.id == lottery_id)
|
||||
.execution_options(populate_existing=True)
|
||||
)
|
||||
return result.scalar_one_or_none()
|
||||
|
||||
@@ -245,14 +206,16 @@ class LotteryService:
|
||||
**updates
|
||||
) -> bool:
|
||||
"""Обновить данные розыгрыша"""
|
||||
if set(updates) - {"title", "description", "prizes", "start_date", "end_date", "is_active", "winner_display_type"}:
|
||||
return False
|
||||
try:
|
||||
await session.execute(
|
||||
result = await session.execute(
|
||||
update(Lottery)
|
||||
.where(Lottery.id == lottery_id)
|
||||
.where(Lottery.id == lottery_id, Lottery.is_completed.is_(False))
|
||||
.values(**updates)
|
||||
)
|
||||
await session.commit()
|
||||
return True
|
||||
return result.rowcount > 0
|
||||
except Exception:
|
||||
await session.rollback()
|
||||
return False
|
||||
@@ -269,126 +232,90 @@ class LotteryService:
|
||||
return result.scalars().all()
|
||||
|
||||
@staticmethod
|
||||
async def set_manual_winner(session: AsyncSession, lottery_id: int,
|
||||
place: int, telegram_id: int) -> bool:
|
||||
"""Установить ручного победителя для определенного места"""
|
||||
# Получаем пользователя
|
||||
user = await UserService.get_user_by_telegram_id(session, telegram_id)
|
||||
if not user:
|
||||
async def set_manual_winner(session, lottery_id, place, telegram_id):
|
||||
from .transactions import lock_open_lottery
|
||||
if not await lock_open_lottery(session, lottery_id):
|
||||
await session.rollback()
|
||||
return False
|
||||
|
||||
# Получаем розыгрыш
|
||||
lottery = await LotteryService.get_lottery(session, lottery_id)
|
||||
if not lottery:
|
||||
user = await UserService.get_user_by_telegram_id(session, telegram_id)
|
||||
if not user or place < 1 or place > len(lottery.prizes or [None]):
|
||||
await session.rollback()
|
||||
return False
|
||||
|
||||
# Обновляем ручных победителей
|
||||
if not lottery.manual_winners:
|
||||
lottery.manual_winners = {}
|
||||
|
||||
lottery.manual_winners[str(place)] = telegram_id
|
||||
if not any(p.user_id == user.id for p in lottery.participations):
|
||||
await session.rollback()
|
||||
return False
|
||||
# Reassign JSON: in-place edits are not tracked by a plain JSON column.
|
||||
winners = dict(lottery.manual_winners or {})
|
||||
if telegram_id in (value for key, value in winners.items() if key != str(place)):
|
||||
await session.rollback()
|
||||
return False
|
||||
winners[str(place)] = telegram_id
|
||||
lottery.manual_winners = winners
|
||||
await session.commit()
|
||||
return True
|
||||
|
||||
@staticmethod
|
||||
async def conduct_draw(session: AsyncSession, lottery_id: int) -> Dict[int, Dict[str, Any]]:
|
||||
"""Провести розыгрыш с учетом ручных победителей"""
|
||||
import logging
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
logger.info(f"conduct_draw: начало для lottery_id={lottery_id}")
|
||||
async def conduct_draw(session, lottery_id):
|
||||
"""Select and persist winners once, in one transaction, before notifications."""
|
||||
from types import SimpleNamespace
|
||||
from datetime import datetime, timezone
|
||||
from .transactions import lock_open_lottery
|
||||
from .models import WinnerVerification
|
||||
if not await lock_open_lottery(session, lottery_id):
|
||||
await session.rollback()
|
||||
return {}
|
||||
lottery = await LotteryService.get_lottery(session, lottery_id)
|
||||
if not lottery or lottery.is_completed:
|
||||
logger.warning(f"conduct_draw: lottery не найден или завершён")
|
||||
participations = list(lottery.participations)
|
||||
if not participations:
|
||||
await session.rollback()
|
||||
return {}
|
||||
|
||||
logger.info(f"conduct_draw: получаем участников")
|
||||
# Получаем всех участников (включая тех, у кого нет user)
|
||||
participants = []
|
||||
for p in lottery.participations:
|
||||
if p.user:
|
||||
participants.append(p.user)
|
||||
else:
|
||||
# Создаем временный объект для участников без пользователя
|
||||
# Храним только номер счета
|
||||
participants.append(type('obj', (object,), {
|
||||
'id': None,
|
||||
'telegram_id': None,
|
||||
'account_number': p.account_number
|
||||
})())
|
||||
|
||||
logger.info(f"conduct_draw: участников {len(participants)}")
|
||||
if not participants:
|
||||
logger.warning(f"conduct_draw: нет участников")
|
||||
return {}
|
||||
|
||||
# Определяем количество призовых мест
|
||||
num_prizes = len(lottery.prizes) if lottery.prizes else 1
|
||||
|
||||
results = {}
|
||||
remaining_participants = participants.copy()
|
||||
manual_winners = lottery.manual_winners or {}
|
||||
|
||||
# Сначала обрабатываем ручных победителей
|
||||
num_prizes = len(lottery.prizes or [None])
|
||||
existing = list((await session.scalars(select(Winner).where(Winner.lottery_id == lottery_id))).all())
|
||||
if any(w.is_claimed for w in existing):
|
||||
raise ValueError("В розыгрыше уже есть выданные призы")
|
||||
selected = {}
|
||||
available = participations.copy()
|
||||
for place in range(1, num_prizes + 1):
|
||||
place_str = str(place)
|
||||
if place_str in manual_winners:
|
||||
# Находим пользователя среди участников
|
||||
manual_winner = None
|
||||
for participant in remaining_participants:
|
||||
if hasattr(participant, 'telegram_id') and participant.telegram_id == manual_winners[place_str]:
|
||||
manual_winner = participant
|
||||
break
|
||||
|
||||
if manual_winner:
|
||||
results[place] = {
|
||||
'user': manual_winner,
|
||||
'prize': lottery.prizes[place - 1] if lottery.prizes and place <= len(lottery.prizes) else f"Приз {place} места",
|
||||
'is_manual': True
|
||||
}
|
||||
remaining_participants.remove(manual_winner)
|
||||
|
||||
# Заполняем оставшиеся места случайными участниками
|
||||
preset = next((w for w in existing if w.place == place and w.is_manual), None)
|
||||
manual = (lottery.manual_winners or {}).get(str(place))
|
||||
candidate = next((p for p in available if
|
||||
(preset and preset.account_number and p.account_number == preset.account_number) or
|
||||
(manual and p.user and p.user.telegram_id == manual)), None)
|
||||
if candidate:
|
||||
selected[place] = (candidate, True)
|
||||
available.remove(candidate)
|
||||
rng = random.SystemRandom()
|
||||
for place in range(1, num_prizes + 1):
|
||||
if place not in results and remaining_participants:
|
||||
winner = random.choice(remaining_participants)
|
||||
results[place] = {
|
||||
'user': winner,
|
||||
'prize': lottery.prizes[place - 1] if lottery.prizes and place <= len(lottery.prizes) else f"Приз {place} места",
|
||||
'is_manual': False
|
||||
}
|
||||
remaining_participants.remove(winner)
|
||||
|
||||
# Сохраняем победителей в базу данных
|
||||
for place, winner_info in results.items():
|
||||
user_obj = winner_info['user']
|
||||
winner = Winner(
|
||||
lottery_id=lottery_id,
|
||||
user_id=user_obj.id if hasattr(user_obj, 'id') and user_obj.id else None,
|
||||
account_number=user_obj.account_number if hasattr(user_obj, 'account_number') else None,
|
||||
place=place,
|
||||
prize=winner_info['prize'],
|
||||
is_manual=winner_info['is_manual']
|
||||
)
|
||||
session.add(winner)
|
||||
|
||||
# Обновляем статус розыгрыша
|
||||
logger.info(f"conduct_draw: обновляем статус lottery")
|
||||
if place not in selected and available:
|
||||
candidate = rng.choice(available)
|
||||
available.remove(candidate)
|
||||
selected[place] = (candidate, False)
|
||||
if existing:
|
||||
await session.execute(delete(WinnerVerification).where(WinnerVerification.winner_id.in_([w.id for w in existing])))
|
||||
await session.execute(delete(Winner).where(Winner.lottery_id == lottery_id))
|
||||
results, stored = {}, {}
|
||||
for place, (participation, manual) in sorted(selected.items()):
|
||||
owner = participation.user
|
||||
# Preserve the selected ticket, including for users with multiple accounts.
|
||||
actor = SimpleNamespace(id=participation.user_id,
|
||||
telegram_id=owner.telegram_id if owner else None,
|
||||
username=owner.username if owner else None,
|
||||
first_name=owner.first_name if owner else None,
|
||||
nickname=owner.nickname if owner else None,
|
||||
account_number=participation.account_number)
|
||||
prize = lottery.prizes[place - 1] if lottery.prizes else f"Приз {place} места"
|
||||
session.add(Winner(lottery_id=lottery_id, user_id=actor.id, account_number=actor.account_number,
|
||||
place=place, prize=prize, is_manual=manual))
|
||||
results[place] = dict(user=actor, prize=prize, is_manual=manual)
|
||||
stored[str(place)] = dict(user_id=actor.id, telegram_id=actor.telegram_id,
|
||||
username=actor.username, account_number=actor.account_number,
|
||||
prize=prize, is_manual=manual)
|
||||
lottery.is_completed = True
|
||||
lottery.draw_results = {}
|
||||
for place, info in results.items():
|
||||
user_obj = info['user']
|
||||
lottery.draw_results[str(place)] = {
|
||||
'user_id': user_obj.id if hasattr(user_obj, 'id') and user_obj.id else None,
|
||||
'telegram_id': user_obj.telegram_id if hasattr(user_obj, 'telegram_id') else None,
|
||||
'username': user_obj.username if hasattr(user_obj, 'username') else None,
|
||||
'account_number': user_obj.account_number if hasattr(user_obj, 'account_number') else None,
|
||||
'prize': info['prize'],
|
||||
'is_manual': info['is_manual']
|
||||
}
|
||||
|
||||
# НЕ коммитим здесь - это должно сделать вызывающая функция
|
||||
logger.info(f"conduct_draw: изменения подготовлены, победителей: {len(results)}")
|
||||
lottery.is_active = False
|
||||
lottery.end_date = datetime.now(timezone.utc)
|
||||
lottery.draw_results = stored
|
||||
await session.commit()
|
||||
return results
|
||||
|
||||
@staticmethod
|
||||
@@ -423,7 +350,7 @@ class LotteryService:
|
||||
"""Установить статус активности розыгрыша"""
|
||||
result = await session.execute(
|
||||
update(Lottery)
|
||||
.where(Lottery.id == lottery_id)
|
||||
.where(Lottery.id == lottery_id, Lottery.is_completed.is_(False))
|
||||
.values(is_active=is_active)
|
||||
)
|
||||
await session.commit()
|
||||
@@ -445,6 +372,9 @@ class LotteryService:
|
||||
@staticmethod
|
||||
async def delete_lottery(session: AsyncSession, lottery_id: int) -> bool:
|
||||
"""Удалить розыгрыш и все связанные данные"""
|
||||
from .models import WinnerVerification
|
||||
await session.execute(delete(WinnerVerification).where(WinnerVerification.winner_id.in_(
|
||||
select(Winner.id).where(Winner.lottery_id == lottery_id))))
|
||||
# Сначала удаляем все связанные данные
|
||||
# Удаляем победителей
|
||||
await session.execute(
|
||||
@@ -469,36 +399,31 @@ class ParticipationService:
|
||||
"""Сервис для работы с участием в розыгрышах"""
|
||||
|
||||
@staticmethod
|
||||
async def add_participant(session: AsyncSession, lottery_id: int, user_id: int) -> bool:
|
||||
"""Добавить участника в розыгрыш"""
|
||||
# Проверяем, не участвует ли уже пользователь
|
||||
existing = await session.execute(
|
||||
select(Participation)
|
||||
.where(Participation.lottery_id == lottery_id, Participation.user_id == user_id)
|
||||
)
|
||||
if existing.scalar_one_or_none():
|
||||
async def add_participant(session, lottery_id, user_id):
|
||||
from .transactions import lock_open_lottery
|
||||
if not await lock_open_lottery(session, lottery_id):
|
||||
await session.rollback()
|
||||
return False
|
||||
|
||||
participation = Participation(lottery_id=lottery_id, user_id=user_id)
|
||||
session.add(participation)
|
||||
exists = await session.scalar(select(Participation.id).where(
|
||||
Participation.lottery_id == lottery_id, Participation.user_id == user_id).limit(1))
|
||||
user = await session.get(User, user_id)
|
||||
if exists or not user:
|
||||
await session.rollback()
|
||||
return False
|
||||
session.add(Participation(lottery_id=lottery_id, user_id=user_id))
|
||||
await session.commit()
|
||||
return True
|
||||
|
||||
@staticmethod
|
||||
async def remove_participant(session: AsyncSession, lottery_id: int, user_id: int) -> bool:
|
||||
"""Удалить участника из розыгрыша"""
|
||||
participation = await session.execute(
|
||||
select(Participation)
|
||||
.where(Participation.lottery_id == lottery_id, Participation.user_id == user_id)
|
||||
)
|
||||
participation = participation.scalar_one_or_none()
|
||||
|
||||
if not participation:
|
||||
async def remove_participant(session, lottery_id, user_id):
|
||||
from .transactions import lock_open_lottery
|
||||
if not await lock_open_lottery(session, lottery_id):
|
||||
await session.rollback()
|
||||
return False
|
||||
|
||||
await session.delete(participation)
|
||||
result = await session.execute(delete(Participation).where(
|
||||
Participation.lottery_id == lottery_id, Participation.user_id == user_id))
|
||||
await session.commit()
|
||||
return True
|
||||
return result.rowcount > 0
|
||||
|
||||
@staticmethod
|
||||
async def get_participants(session: AsyncSession, lottery_id: int, limit: Optional[int] = None, offset: int = 0) -> List[User]:
|
||||
@@ -523,13 +448,9 @@ class ParticipationService:
|
||||
return list(result.scalars().all())
|
||||
|
||||
@staticmethod
|
||||
async def get_participants_count(session: AsyncSession, lottery_id: int) -> int:
|
||||
"""Получить количество участников в розыгрыше"""
|
||||
result = await session.execute(
|
||||
select(Participation)
|
||||
.where(Participation.lottery_id == lottery_id)
|
||||
)
|
||||
return len(result.scalars().all())
|
||||
async def get_participants_count(session, lottery_id):
|
||||
from sqlalchemy import func
|
||||
return await session.scalar(select(func.count()).select_from(Participation).where(Participation.lottery_id == lottery_id))
|
||||
|
||||
@staticmethod
|
||||
async def add_participants_bulk(session: AsyncSession, lottery_id: int, telegram_ids: List[int]) -> Dict[str, Any]:
|
||||
@@ -593,190 +514,24 @@ class ParticipationService:
|
||||
return results
|
||||
|
||||
@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,
|
||||
"errors": [],
|
||||
"details": [],
|
||||
"invalid_accounts": []
|
||||
}
|
||||
|
||||
for account_input in account_numbers:
|
||||
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
|
||||
from ..core.registration_services import AccountService
|
||||
user = await AccountService.get_account_owner(session, formatted_account)
|
||||
if not user:
|
||||
card_info = f" (карта: {card_number})" if card_number else ""
|
||||
results["errors"].append(f"Пользователь с счётом {formatted_account}{card_info} не найден")
|
||||
continue
|
||||
|
||||
# Получаем запись Account для этого счета
|
||||
account_record = await session.execute(
|
||||
select(Account).where(Account.account_number == formatted_account)
|
||||
)
|
||||
account_record = account_record.scalar_one_or_none()
|
||||
|
||||
if not account_record:
|
||||
card_info = f" (карта: {card_number})" if card_number else ""
|
||||
results["errors"].append(f"Запись счета {formatted_account}{card_info} не найдена в базе")
|
||||
continue
|
||||
|
||||
# Проверяем, не участвует ли уже этот счет
|
||||
existing = await session.execute(
|
||||
select(Participation).where(
|
||||
Participation.lottery_id == lottery_id,
|
||||
Participation.account_number == formatted_account
|
||||
)
|
||||
)
|
||||
if existing.scalar_one_or_none():
|
||||
results["skipped"] += 1
|
||||
detail = f"{user.first_name} ({formatted_account})"
|
||||
if card_number:
|
||||
detail = f"{user.first_name} (карта: {card_number}, счёт: {formatted_account})"
|
||||
results["details"].append(f"Уже участвует: {detail}")
|
||||
continue
|
||||
|
||||
# Добавляем участие по счету
|
||||
participation = Participation(
|
||||
lottery_id=lottery_id,
|
||||
user_id=user.id,
|
||||
account_id=account_record.id,
|
||||
account_number=formatted_account
|
||||
)
|
||||
session.add(participation)
|
||||
await session.commit()
|
||||
|
||||
results["added"] += 1
|
||||
detail = f"{user.first_name} ({formatted_account})"
|
||||
if card_number:
|
||||
detail = f"{user.first_name} (карта: {card_number}, счёт: {formatted_account})"
|
||||
results["details"].append(detail)
|
||||
|
||||
except Exception as e:
|
||||
results["errors"].append(f"Ошибка с {account_input}: {str(e)}")
|
||||
|
||||
return results
|
||||
async def add_participants_by_accounts_bulk(session, lottery_id, account_numbers):
|
||||
from src.handlers.account_services import AccountParticipationService
|
||||
result = await AccountParticipationService.add_accounts_bulk(session, lottery_id, account_numbers)
|
||||
result['invalid_accounts'] = [a for a in account_numbers if not a.split() or not format_account_number(a.split()[-1])]
|
||||
return result
|
||||
|
||||
@staticmethod
|
||||
async def remove_participants_by_accounts_bulk(session: AsyncSession, lottery_id: int, account_numbers: List[str]) -> Dict[str, Any]:
|
||||
"""Массовое удаление участников по номерам счетов"""
|
||||
results = {
|
||||
"removed": 0,
|
||||
"not_found": 0,
|
||||
"errors": [],
|
||||
"details": [],
|
||||
"invalid_accounts": []
|
||||
}
|
||||
|
||||
for account_input in account_numbers:
|
||||
account_input = account_input.strip()
|
||||
if not account_input:
|
||||
continue
|
||||
|
||||
try:
|
||||
# Разделяем по пробелу: левая часть - номер карты, правая - номер счета
|
||||
parts = account_input.split()
|
||||
if len(parts) == 2:
|
||||
card_number = parts[0] # Номер клубной карты
|
||||
account_number = parts[1] # Номер счета
|
||||
elif len(parts) == 1:
|
||||
# Если нет пробела, считаем что это просто номер счета
|
||||
card_number = None
|
||||
account_number = parts[0]
|
||||
else:
|
||||
results["invalid_accounts"].append(account_input)
|
||||
results["errors"].append(f"Неверный формат: {account_input}")
|
||||
continue
|
||||
|
||||
# Валидируем и форматируем номер счета
|
||||
formatted_account = format_account_number(account_number)
|
||||
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}")
|
||||
continue
|
||||
|
||||
# Ищем владельца счёта через таблицу Account
|
||||
from ..core.registration_services import AccountService
|
||||
user = await AccountService.get_account_owner(session, formatted_account)
|
||||
if not user:
|
||||
card_info = f" (карта: {card_number})" if card_number else ""
|
||||
results["not_found"] += 1
|
||||
results["details"].append(f"Не найден: {formatted_account}{card_info}")
|
||||
continue
|
||||
|
||||
# Ищем участие по номеру счета (не по user_id!)
|
||||
participation = await session.execute(
|
||||
select(Participation).where(
|
||||
Participation.lottery_id == lottery_id,
|
||||
Participation.account_number == formatted_account
|
||||
)
|
||||
)
|
||||
participation = participation.scalar_one_or_none()
|
||||
|
||||
if participation:
|
||||
await session.delete(participation)
|
||||
await session.commit()
|
||||
|
||||
results["removed"] += 1
|
||||
detail = f"{user.first_name} ({formatted_account})"
|
||||
if card_number:
|
||||
detail = f"{user.first_name} (карта: {card_number}, счёт: {formatted_account})"
|
||||
results["details"].append(detail)
|
||||
else:
|
||||
results["not_found"] += 1
|
||||
detail = f"{user.first_name} ({formatted_account})"
|
||||
if card_number:
|
||||
detail = f"{user.first_name} (карта: {card_number}, счёт: {formatted_account})"
|
||||
results["details"].append(f"Не участвовал: {detail}")
|
||||
|
||||
except Exception as e:
|
||||
results["errors"].append(f"Ошибка с {account_input}: {str(e)}")
|
||||
|
||||
return results
|
||||
async def remove_participants_by_accounts_bulk(session, lottery_id, account_numbers):
|
||||
from src.handlers.account_services import AccountParticipationService
|
||||
result = dict(removed=0, not_found=0, errors=[], details=[], invalid_accounts=[])
|
||||
for account in account_numbers:
|
||||
value = account.split()[-1] if account.split() else ''
|
||||
item = await AccountParticipationService.remove_account_from_lottery(session, lottery_id, value)
|
||||
result['removed' if item['success'] else 'not_found'] += 1
|
||||
result['details'].append(item['message'])
|
||||
if not item['success']:
|
||||
result['errors'].append(item['message'])
|
||||
return result
|
||||
|
||||
@staticmethod
|
||||
async def get_participant_stats(session: AsyncSession, user_id: int) -> Dict[str, Any]:
|
||||
@@ -804,4 +559,4 @@ class ParticipationService:
|
||||
"participations_count": participations_count,
|
||||
"wins_count": wins_count,
|
||||
"last_participation": last_participation.created_at if last_participation else None
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user