fix: исправлен парсинг счетов и добавлены уведомления победителям

Исправления:
1. Парсинг счетов (parse_accounts_from_message):
   - Исправлено дублирование счетов при формате 'КАРТА СЧЕТ'
   - Добавлены негативные lookbehind для корректного разбора
   - Теперь '2521 11-22-33-44-55-66-77' парсится только 1 раз

2. Уведомления победителям:
   - Создан новый модуль src/utils/notifications.py
   - Добавлена функция notify_winners_async()
   - Уведомления отправляются автоматически после розыгрыша
   - Поддержка счетов и обычных пользователей
   - Включает кнопки подтверждения для победителей по счетам
This commit is contained in:
2025-11-17 08:32:11 +09:00
parent 2d03c3e14c
commit 712577e694
4 changed files with 161 additions and 10 deletions

View File

@@ -1 +1 @@
822376
832585

View File

@@ -1,6 +1,7 @@
"""
Расширенная админ-панель для управления розыгрышами
"""
import logging
from aiogram import Router, F
from aiogram.types import (
CallbackQuery, Message, InlineKeyboardButton, InlineKeyboardMarkup
@@ -17,6 +18,8 @@ from ..core.services import UserService, LotteryService, ParticipationService
from ..core.config import ADMIN_IDS
from ..core.models import User, Lottery, Participation, Account
logger = logging.getLogger(__name__)
# Состояния для админки
class AdminStates(StatesGroup):
@@ -2620,6 +2623,14 @@ async def conduct_lottery_draw(callback: CallbackQuery):
winners_dict = await LotteryService.conduct_draw(session, lottery_id)
if winners_dict:
# Отправляем уведомления победителям
from ..utils.notifications import notify_winners_async
try:
await notify_winners_async(callback.bot, session, lottery_id)
logger.info(f"Уведомления отправлены для розыгрыша {lottery_id}")
except Exception as e:
logger.error(f"Ошибка при отправке уведомлений: {e}")
# Получаем победителей из базы
winners = await LotteryService.get_winners(session, lottery_id)
text = f"🎉 Розыгрыш '{lottery.title}' завершён!\n\n"
@@ -2633,6 +2644,8 @@ async def conduct_lottery_draw(callback: CallbackQuery):
else:
text += f"{winner.place} место: ID {winner.user_id}\n"
text += "\n✅ Уведомления отправлены победителям"
await callback.message.edit_text(
text,
reply_markup=InlineKeyboardMarkup(inline_keyboard=[

View File

@@ -113,9 +113,11 @@ def parse_accounts_from_message(text: str) -> List[str]:
return []
accounts = []
found_accounts_only = set() # Для отслеживания уже найденных счетов
# Паттерн 1: номер карты (4 цифры) + пробел + счет (7 пар цифр)
pattern_with_card = r'\b(\d{4})\s+(\d{2}[-\s]?\d{2}[-\s]?\d{2}[-\s]?\d{2}[-\s]?\d{2}[-\s]?\d{2}[-\s]?\d{2})\b'
# Используем негативный lookbehind чтобы не ловить цифры перед картой
pattern_with_card = r'(?<!\d)(\d{4})\s+(\d{2}[-\s]?\d{2}[-\s]?\d{2}[-\s]?\d{2}[-\s]?\d{2}[-\s]?\d{2}[-\s]?\d{2})(?!\d)'
matches_with_card = re.findall(pattern_with_card, text)
for card, account in matches_with_card:
@@ -124,18 +126,16 @@ def parse_accounts_from_message(text: str) -> List[str]:
full_account = f"{card} {formatted}"
if full_account not in accounts:
accounts.append(full_account)
found_accounts_only.add(formatted) # Запоминаем этот счет
# Паттерн 2: только счет (7 пар цифр) - для обратной совместимости
pattern_only_account = r'\b\d{2}[-\s]?\d{2}[-\s]?\d{2}[-\s]?\d{2}[-\s]?\d{2}[-\s]?\d{2}[-\s]?\d{2}\b'
# Паттерн 2: только счет (7 пар цифр) БЕЗ карты перед ним
# Негативный lookbehind для проверки что перед счетом нет "4 цифры + пробел"
pattern_only_account = r'(?<!\d{4}\s)(?<!\d)\d{2}[-\s]?\d{2}[-\s]?\d{2}[-\s]?\d{2}[-\s]?\d{2}[-\s]?\d{2}[-\s]?\d{2}(?!\d)'
matches_only = re.findall(pattern_only_account, text)
for match in matches_only:
# Проверяем, что этот счет еще не был добавлен как часть "карта + счет"
formatted = format_account_number(match)
if formatted:
# Проверяем, нет ли уже этого счета с картой
already_added = any(formatted in acc for acc in accounts)
if not already_added and formatted not in accounts:
if formatted and formatted not in found_accounts_only and formatted not in accounts:
accounts.append(formatted)
return accounts

138
src/utils/notifications.py Normal file
View File

@@ -0,0 +1,138 @@
"""
Модуль для отправки уведомлений победителям
"""
import logging
from aiogram import Bot
from aiogram.types import InlineKeyboardMarkup, InlineKeyboardButton
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy import select
from ..core.models import Winner, User
from ..core.services import LotteryService
from ..core.registration_services import AccountService, WinnerNotificationService
from config import ADMIN_IDS
logger = logging.getLogger(__name__)
async def notify_winners_async(bot: Bot, session: AsyncSession, lottery_id: int):
"""
Асинхронно отправить уведомления победителям с кнопкой подтверждения.
Вызывается после проведения розыгрыша.
Args:
bot: Экземпляр бота для отправки сообщений
session: Сессия БД
lottery_id: ID розыгрыша
"""
# Получаем информацию о розыгрыше
lottery = await LotteryService.get_lottery(session, lottery_id)
if not lottery:
logger.error(f"Розыгрыш {lottery_id} не найден")
return
# Получаем всех победителей из БД
winners_result = await session.execute(
select(Winner).where(Winner.lottery_id == lottery_id)
)
winners = winners_result.scalars().all()
logger.info(f"Найдено {len(winners)} победителей для розыгрыша {lottery_id}")
for winner in winners:
try:
# Если у победителя есть account_number, ищем владельца
if winner.account_number:
owner = await AccountService.get_account_owner(session, winner.account_number)
if owner and owner.telegram_id:
# Создаем токен верификации
verification = await WinnerNotificationService.create_verification_token(
session,
winner.id
)
# Формируем сообщение с кнопкой подтверждения
message = (
f"🎉 **Поздравляем! Ваш счет выиграл!**\n\n"
f"🎯 Розыгрыш: {lottery.title}\n"
f"🏆 Место: {winner.place}\n"
f"🎁 Приз: {winner.prize}\n"
f"💳 **Выигрышный счет: {winner.account_number}**\n\n"
f"⏰ **У вас есть 24 часа для подтверждения!**\n\n"
f"Нажмите кнопку ниже, чтобы подтвердить получение приза по этому счету.\n"
f"Если вы не подтвердите в течение 24 часов, "
f"приз будет разыгран заново.\n\n"
f" Если у вас несколько выигрышных счетов, "
f"подтвердите каждый из них отдельно."
)
# Создаем кнопку подтверждения с указанием счета
keyboard = InlineKeyboardMarkup(inline_keyboard=[
[InlineKeyboardButton(
text=f"✅ Подтвердить счет {winner.account_number}",
callback_data=f"confirm_win_{winner.id}"
)],
[InlineKeyboardButton(
text="📞 Связаться с администратором",
url=f"tg://user?id={ADMIN_IDS[0]}"
)]
])
# Отправляем уведомление с кнопкой
await bot.send_message(
owner.telegram_id,
message,
reply_markup=keyboard,
parse_mode="Markdown"
)
# Отмечаем, что уведомление отправлено
winner.is_notified = True
await session.commit()
logger.info(f"✅ Отправлено уведомление победителю {owner.telegram_id} за счет {winner.account_number}")
else:
logger.warning(f"⚠️ Владелец счета {winner.account_number} не найден или нет telegram_id")
# Если победитель - обычный пользователь (старая система)
elif winner.user_id:
user_result = await session.execute(
select(User).where(User.id == winner.user_id)
)
user = user_result.scalar_one_or_none()
if user and user.telegram_id:
message = (
f"🎉 Поздравляем! Вы выиграли!\n\n"
f"🎯 Розыгрыш: {lottery.title}\n"
f"🏆 Место: {winner.place}\n"
f"🎁 Приз: {winner.prize}\n\n"
f"Свяжитесь с администратором для получения приза."
)
keyboard = InlineKeyboardMarkup(inline_keyboard=[
[InlineKeyboardButton(
text="📞 Связаться с администратором",
url=f"tg://user?id={ADMIN_IDS[0]}"
)]
])
await bot.send_message(
user.telegram_id,
message,
reply_markup=keyboard
)
winner.is_notified = True
await session.commit()
logger.info(f"✅ Отправлено уведомление победителю {user.telegram_id} (user_id={user.id})")
else:
logger.warning(f"⚠️ Пользователь {winner.user_id} не найден или нет telegram_id")
except Exception as e:
logger.error(f"❌ Ошибка при отправке уведомления победителю {winner.id}: {e}")
continue
logger.info(f"Завершена отправка уведомлений для розыгрыша {lottery_id}")