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:
@@ -1,6 +1,7 @@
|
|||||||
"""
|
"""
|
||||||
Расширенная админ-панель для управления розыгрышами
|
Расширенная админ-панель для управления розыгрышами
|
||||||
"""
|
"""
|
||||||
|
import logging
|
||||||
from aiogram import Router, F
|
from aiogram import Router, F
|
||||||
from aiogram.types import (
|
from aiogram.types import (
|
||||||
CallbackQuery, Message, InlineKeyboardButton, InlineKeyboardMarkup
|
CallbackQuery, Message, InlineKeyboardButton, InlineKeyboardMarkup
|
||||||
@@ -17,6 +18,8 @@ from ..core.services import UserService, LotteryService, ParticipationService
|
|||||||
from ..core.config import ADMIN_IDS
|
from ..core.config import ADMIN_IDS
|
||||||
from ..core.models import User, Lottery, Participation, Account
|
from ..core.models import User, Lottery, Participation, Account
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
# Состояния для админки
|
# Состояния для админки
|
||||||
class AdminStates(StatesGroup):
|
class AdminStates(StatesGroup):
|
||||||
@@ -2620,6 +2623,14 @@ async def conduct_lottery_draw(callback: CallbackQuery):
|
|||||||
winners_dict = await LotteryService.conduct_draw(session, lottery_id)
|
winners_dict = await LotteryService.conduct_draw(session, lottery_id)
|
||||||
|
|
||||||
if winners_dict:
|
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)
|
winners = await LotteryService.get_winners(session, lottery_id)
|
||||||
text = f"🎉 Розыгрыш '{lottery.title}' завершён!\n\n"
|
text = f"🎉 Розыгрыш '{lottery.title}' завершён!\n\n"
|
||||||
@@ -2633,6 +2644,8 @@ async def conduct_lottery_draw(callback: CallbackQuery):
|
|||||||
else:
|
else:
|
||||||
text += f"{winner.place} место: ID {winner.user_id}\n"
|
text += f"{winner.place} место: ID {winner.user_id}\n"
|
||||||
|
|
||||||
|
text += "\n✅ Уведомления отправлены победителям"
|
||||||
|
|
||||||
await callback.message.edit_text(
|
await callback.message.edit_text(
|
||||||
text,
|
text,
|
||||||
reply_markup=InlineKeyboardMarkup(inline_keyboard=[
|
reply_markup=InlineKeyboardMarkup(inline_keyboard=[
|
||||||
|
|||||||
@@ -113,9 +113,11 @@ def parse_accounts_from_message(text: str) -> List[str]:
|
|||||||
return []
|
return []
|
||||||
|
|
||||||
accounts = []
|
accounts = []
|
||||||
|
found_accounts_only = set() # Для отслеживания уже найденных счетов
|
||||||
|
|
||||||
# Паттерн 1: номер карты (4 цифры) + пробел + счет (7 пар цифр)
|
# Паттерн 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)
|
matches_with_card = re.findall(pattern_with_card, text)
|
||||||
|
|
||||||
for card, account in matches_with_card:
|
for card, account in matches_with_card:
|
||||||
@@ -124,19 +126,17 @@ def parse_accounts_from_message(text: str) -> List[str]:
|
|||||||
full_account = f"{card} {formatted}"
|
full_account = f"{card} {formatted}"
|
||||||
if full_account not in accounts:
|
if full_account not in accounts:
|
||||||
accounts.append(full_account)
|
accounts.append(full_account)
|
||||||
|
found_accounts_only.add(formatted) # Запоминаем этот счет
|
||||||
|
|
||||||
# Паттерн 2: только счет (7 пар цифр) - для обратной совместимости
|
# Паттерн 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'
|
# Негативный 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)
|
matches_only = re.findall(pattern_only_account, text)
|
||||||
|
|
||||||
for match in matches_only:
|
for match in matches_only:
|
||||||
# Проверяем, что этот счет еще не был добавлен как часть "карта + счет"
|
|
||||||
formatted = format_account_number(match)
|
formatted = format_account_number(match)
|
||||||
if formatted:
|
if formatted and formatted not in found_accounts_only and formatted not in accounts:
|
||||||
# Проверяем, нет ли уже этого счета с картой
|
accounts.append(formatted)
|
||||||
already_added = any(formatted in acc for acc in accounts)
|
|
||||||
if not already_added and formatted not in accounts:
|
|
||||||
accounts.append(formatted)
|
|
||||||
|
|
||||||
return accounts
|
return accounts
|
||||||
|
|
||||||
|
|||||||
138
src/utils/notifications.py
Normal file
138
src/utils/notifications.py
Normal 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}")
|
||||||
Reference in New Issue
Block a user