diff --git a/lottery/bot/admin.py b/lottery/bot/admin.py index 7af5328..aa1eb41 100644 --- a/lottery/bot/admin.py +++ b/lottery/bot/admin.py @@ -3,7 +3,7 @@ from django.urls import path from django.shortcuts import redirect from django.contrib import messages from .models import BotConfig -from .models import BotConfig, WelcomeMessage +from .models import BotConfig, WelcomeMessage, BotMessage, BotEventMessageConfig from .tasks import restart_bot_container @admin.register(BotConfig) @@ -35,4 +35,12 @@ class BotConfigAdmin(admin.ModelAdmin): @admin.register(WelcomeMessage) class WWelcomeMessageAdmin(admin.ModelAdmin): list_display = ("bot", "welcome_message", "welcome_image", "admin_contact", "channel_link", "group_link", "custom_link1_name", "custom_link1_url") - \ No newline at end of file + +@admin.register(BotMessage) +class BotMessageAdmin(admin.ModelAdmin): + list_display = ("name", "text", "image", "buttons_json") + +@admin.register(BotEventMessageConfig) +class BotEventMessageConfigAdmin(admin.ModelAdmin): + list_display = ("event", "message", "enabled") + list_filter = ("event", "enabled") \ No newline at end of file diff --git a/lottery/bot/handlers.py b/lottery/bot/handlers.py index f2b337c..eab8bd4 100644 --- a/lottery/bot/handlers.py +++ b/lottery/bot/handlers.py @@ -93,7 +93,7 @@ async def handle_client_card(update: Update, context: ContextTypes.DEFAULT_TYPE) BINDING_PENDING.add(telegram_chat_id) await update.message.reply_text("Введите номер вашей клубной карты (КК):") - + await send_event_message("binding_started", update, context) async def process_binding_input(update: Update, context: ContextTypes.DEFAULT_TYPE): if not update.message or not update.message.text: @@ -124,7 +124,7 @@ async def show_all_invoices_callback(update: Update, context: ContextTypes.DEFAU message_text = "💳 *Все счета:*\n" if invoices: for inv in invoices: - message_text += f" • ID {inv.id} (*{inv.sum}*)\n" + message_text += f" • ID {inv.ext_id} (*{inv.sum}*)\n" else: message_text += " _Нет счетов_\n" diff --git a/lottery/bot/migrations/0008_botmessage_boteventmessageconfig.py b/lottery/bot/migrations/0008_botmessage_boteventmessageconfig.py new file mode 100644 index 0000000..858409c --- /dev/null +++ b/lottery/bot/migrations/0008_botmessage_boteventmessageconfig.py @@ -0,0 +1,33 @@ +# Generated by Django 5.1.6 on 2025-08-03 05:29 + +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('bot', '0007_botconfig_active_welcome_botconfig_is_active_and_more'), + ] + + operations = [ + migrations.CreateModel( + name='BotMessage', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name', models.CharField(max_length=100, verbose_name='Название сообщения')), + ('text', models.TextField(blank=True, verbose_name='Текст')), + ('image', models.ImageField(blank=True, null=True, upload_to='bot_messages/', verbose_name='Картинка')), + ('buttons_json', models.JSONField(blank=True, null=True, verbose_name='Кнопки (JSON-формат)')), + ], + ), + migrations.CreateModel( + name='BotEventMessageConfig', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('event', models.CharField(choices=[('welcome', 'Приветствие'), ('draw_started', 'Розыгрыш начат'), ('draw_finished', 'Розыгрыш завершен'), ('winner_announced', 'Объявление победителя')], max_length=50, unique=True, verbose_name='Событие')), + ('enabled', models.BooleanField(default=True, verbose_name='Активно')), + ('message', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='event_configs', to='bot.botmessage')), + ], + ), + ] diff --git a/lottery/bot/migrations/0009_alter_boteventmessageconfig_options_and_more.py b/lottery/bot/migrations/0009_alter_boteventmessageconfig_options_and_more.py new file mode 100644 index 0000000..7d9ec2d --- /dev/null +++ b/lottery/bot/migrations/0009_alter_boteventmessageconfig_options_and_more.py @@ -0,0 +1,26 @@ +# Generated by Django 5.1.6 on 2025-08-03 05:46 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('bot', '0008_botmessage_boteventmessageconfig'), + ] + + operations = [ + migrations.AlterModelOptions( + name='boteventmessageconfig', + options={'verbose_name': 'Событие сообщения', 'verbose_name_plural': 'События сообщений'}, + ), + migrations.AlterModelOptions( + name='botmessage', + options={'verbose_name': 'Сообщение', 'verbose_name_plural': 'Сообщения'}, + ), + migrations.AlterField( + model_name='boteventmessageconfig', + name='event', + field=models.CharField(choices=[('welcome', 'Приветствие'), ('draw_started', 'Розыгрыш начат'), ('draw_finished', 'Розыгрыш завершен'), ('winner_announced', 'Объявление победителя'), ('guest_binding', 'Подтверждение гостя')], max_length=50, unique=True, verbose_name='Событие'), + ), + ] diff --git a/lottery/bot/migrations/0010_alter_welcomemessage_welcome_image.py b/lottery/bot/migrations/0010_alter_welcomemessage_welcome_image.py new file mode 100644 index 0000000..9a98a05 --- /dev/null +++ b/lottery/bot/migrations/0010_alter_welcomemessage_welcome_image.py @@ -0,0 +1,18 @@ +# Generated by Django 5.1.6 on 2025-08-03 11:39 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('bot', '0009_alter_boteventmessageconfig_options_and_more'), + ] + + operations = [ + migrations.AlterField( + model_name='welcomemessage', + name='welcome_image', + field=models.ImageField(blank=True, help_text='Загрузите изображение для приветствия', null=True, upload_to='welcome_messages/', verbose_name='Приветственное изображение'), + ), + ] diff --git a/lottery/bot/models.py b/lottery/bot/models.py index 2036b41..0b4a8ad 100644 --- a/lottery/bot/models.py +++ b/lottery/bot/models.py @@ -1,6 +1,7 @@ from django.db import models + class BotConfig(models.Model): bot_token = models.CharField(max_length=255, help_text="Токен для подключения к Telegram API") channel_id = models.CharField(max_length=100, help_text="ID канала/чата, куда бот будет отправлять сообщения") @@ -44,7 +45,7 @@ class WelcomeMessage(models.Model): help_text="Текст, который будет отправлен при запуске команды /start" ) welcome_image = models.ImageField( - upload_to='static/upload_image/', + upload_to='welcome_messages/', verbose_name="Приветственное изображение", blank=True, null=True, @@ -98,3 +99,38 @@ class WelcomeMessage(models.Model): def __str__(self): return f"Приветствие для {self.bot}" + +class BotMessage(models.Model): + name = models.CharField("Название сообщения", max_length=100) + text = models.TextField("Текст", blank=True) + image = models.ImageField("Картинка", upload_to="bot_messages/", blank=True, null=True) + buttons_json = models.JSONField("Кнопки (JSON-формат)", blank=True, null=True) + + class Meta: + verbose_name = "Сообщение" + verbose_name_plural = "Сообщения" + + def __str__(self): + return self.name + +class BotEventMessageConfig(models.Model): + EVENT_CHOICES = [ + ('welcome', 'Приветствие'), + ('draw_started', 'Розыгрыш начат'), + ('draw_finished', 'Розыгрыш завершен'), + ('winner_announced', 'Объявление победителя'), + ('guest_binding', 'Подтверждение гостя'), + # и другие + ] + + event = models.CharField("Событие", max_length=50, choices=EVENT_CHOICES, unique=True) + message = models.ForeignKey(BotMessage, on_delete=models.CASCADE, related_name="event_configs") + enabled = models.BooleanField("Активно", default=True) + + class Meta: + verbose_name = "Событие сообщения" + verbose_name_plural = "События сообщений" + + def __str__(self): + return f"{self.get_event_display()} → {self.message.name}" + \ No newline at end of file diff --git a/lottery/bot/notifications.py b/lottery/bot/notifications.py index af5d152..bcac32a 100644 --- a/lottery/bot/notifications.py +++ b/lottery/bot/notifications.py @@ -1,105 +1,131 @@ -# notifications.py import logging from asgiref.sync import sync_to_async +from bot.utils import send_event_message_async + +logger = logging.getLogger(__name__) +logger.setLevel(logging.DEBUG) + class NotificationService: def __init__(self, bot): - """ - Инициализация сервиса уведомлений. - :param bot: экземпляр telegram.Bot, используемый для отправки сообщений. - """ self.bot = bot - self.logger = logging.getLogger(__name__) + self.logger = logging.getLogger("NotificationService") + self.logger.setLevel(logging.DEBUG) + if not self.logger.handlers: + handler = logging.StreamHandler() + handler.setLevel(logging.DEBUG) + handler.setFormatter(logging.Formatter('%(asctime)s - %(levelname)s - %(message)s')) + self.logger.addHandler(handler) @sync_to_async def _get_all_clients(self): from webapp.models import Client - # Возвращаем только клиентов, у которых telegram_id заполнен корректно - return list(Client.objects.exclude(telegram_id__in=[None, "", "NULL"])) + clients = list(Client.objects.exclude(telegram_id__in=[None, "", "NULL"])) + self.logger.info(f"Найдено клиентов для уведомления: {len(clients)}") + return clients @sync_to_async def _prepare_results(self, results): - # Принудительно загружаем связанные объекты через select_related return list(results.select_related('prize', 'participant__invoice')) async def notify_draw_start(self, lottery): - """ - Уведомляет всех клиентов о запуске розыгрыша. - :param lottery: объект розыгрыша (например, модель Lottery) - """ - message_text = f"🎉 *Розыгрыш '{lottery.name}' начался!* Следите за обновлениями." clients = await self._get_all_clients() for client in clients: if not client.telegram_id: continue try: - await self.bot.send_message( - chat_id=client.telegram_id, - text=message_text, - parse_mode="Markdown" + self.logger.debug(f"Отправка 'draw_started' клиенту {client.telegram_id}") + await send_event_message_async( + event="draw_started", + bot=self.bot, + chat_id=int(client.telegram_id), + context_vars={"lottery": lottery, "client": client} ) except Exception as e: - self.logger.error(f"Ошибка отправки уведомления о запуске розыгрыша пользователю {client.telegram_id}: {e}") + self.logger.error(f"Ошибка отправки draw_started: {e}") - async def notify_draw_results(self, lottery, results): - """ - Отправляет результаты розыгрыша всем клиентам. - :param lottery: объект розыгрыша (Lottery) - :param results: QuerySet с результатами розыгрыша (например, объекты DrawResult) - """ - # Принудительно загружаем связанные объекты + # async def notify_draw_results(self, lottery, results): + # results_list = await self._prepare_results(results) + # clients = await self._get_all_clients() + # for client in clients: + # if not client.telegram_id: + # continue + # try: + # self.logger.debug(f"Отправка 'draw_finished' клиенту {client.telegram_id}") + # await send_event_message_async( + # event="draw_finished", + # bot=self.bot, + # chat_id=int(client.telegram_id), + # context_vars={"lottery": lottery, "client": client, "results": results_list} + # ) + # except Exception as e: + # self.logger.error(f"Ошибка отправки draw_finished: {e}") + + async def notify_draw_results(self, lottery, results, context_vars=None): results_list = await self._prepare_results(results) - message_text = f"🎉 *Результаты розыгрыша '{lottery.name}':*\n" - for result in results_list: - status = "✅ Подтвержден" if result.confirmed else "⏳ В ожидании подтверждения" - prize = result.prize.reward if result.prize and hasattr(result.prize, "reward") else "неизвестно" - account = (str(result.participant.invoice).split('/')[0].strip() - if result.participant and hasattr(result.participant, "invoice") else "неизвестно") - message_text += f"• Приз: *{prize}*, Счет: _{account}_, Статус: *{status}*\n" clients = await self._get_all_clients() + + # 🔢 Подготовка суммы и списка победителей + total_reward = sum( + r.prize.reward for r in results_list if r.prize and r.prize.reward + ) + + winners_lines = [] + for r in results_list: + try: + account_id = r.participant.invoice.ext_id + prize_amount = r.prize.reward + winners_lines.append(f"• Счёт {account_id} — {prize_amount:,.2f}₩") + except Exception: + continue + + winners_list = "\n".join(winners_lines) + for client in clients: if not client.telegram_id: continue try: - await self.bot.send_message( - chat_id=client.telegram_id, - text=message_text, - parse_mode="Markdown" + self.logger.debug(f"Отправка 'draw_finished' клиенту {client.telegram_id}") + await send_event_message_async( + event="draw_finished", + bot=self.bot, + chat_id=int(client.telegram_id), + context_vars={ + **(context_vars or {}), + "lottery": lottery, + "client": client, + "results": results_list, + "total_reward": f"{total_reward:,.2f}", + "winners_list": winners_list + } ) except Exception as e: - self.logger.error(f"Ошибка отправки результатов розыгрыша пользователю {client.telegram_id}: {e}") + self.logger.error(f"Ошибка отправки draw_finished: {e}") + + async def notify_prize_status_update(self, client, result): - """ - Отправляет конкретному пользователю обновление по статусу подтверждения приза. - :param client: объект клиента (Client) - :param result: объект результата розыгрыша (DrawResult) - """ - status = "✅ Подтвержден" if result.confirmed else "⏳ В ожидании подтверждения" - prize = result.prize.reward if result.prize and hasattr(result.prize, "reward") else "неизвестно" - message_text = f"🎉 *Обновление розыгрыша:*\nВаш приз *{prize}* имеет статус: *{status}*." try: if client.telegram_id: - await self.bot.send_message( - chat_id=client.telegram_id, - text=message_text, - parse_mode="Markdown" + self.logger.debug(f"Отправка 'winner_announced' клиенту {client.telegram_id}") + await send_event_message_async( + event="winner_announced", + bot=self.bot, + chat_id=int(client.telegram_id), + context_vars={"client": client, "result": result, "prize": result.prize} ) except Exception as e: - self.logger.error(f"Ошибка отправки обновления статуса приза пользователю {client.telegram_id}: {e}") + self.logger.error(f"Ошибка отправки winner_announced: {e}") async def notify_binding_complete(self, client): - """ - Уведомляет пользователя об окончании привязки клубной карты к Telegram ID. - :param client: объект клиента (Client) - """ - message_text = "✅ *Привязка завершена!* Ваша клубная карта успешно привязана к Telegram. Теперь вы можете участвовать в розыгрышах и чате." try: if client.telegram_id: - await self.bot.send_message( - chat_id=client.telegram_id, - text=message_text, - parse_mode="Markdown" + self.logger.debug(f"Отправка 'binding_complete' клиенту {client.telegram_id}") + await send_event_message_async( + event="binding_complete", + bot=self.bot, + chat_id=int(client.telegram_id), + context_vars={"client": client} ) except Exception as e: - self.logger.error(f"Ошибка отправки уведомления о привязке пользователю {client.telegram_id}: {e}") + self.logger.error(f"Ошибка отправки binding_complete: {e}") diff --git a/lottery/bot/utils.py b/lottery/bot/utils.py index 679cf00..a011004 100644 --- a/lottery/bot/utils.py +++ b/lottery/bot/utils.py @@ -1,39 +1,99 @@ -# bot/utils.py -from telegram import Bot +from telegram import Bot, InlineKeyboardMarkup, InlineKeyboardButton from django.conf import settings -from telegram import Bot -from bot.models import BotConfig from django.core.exceptions import ImproperlyConfigured +from urllib.parse import urljoin +from bot.models import BotConfig, BotEventMessageConfig +import logging +from asgiref.sync import sync_to_async +from telegram import InputFile +import os + +logger = logging.getLogger(__name__) +logger.setLevel(logging.DEBUG) + def create_bot_instance(): - """ - Получает настройки бота из БД и создаёт экземпляр Telegram Bot. - """ - config = BotConfig.objects.first() # предполагается, что конфигурация одна + config = BotConfig.objects.first() if not config: - raise ImproperlyConfigured("Настройки бота (BotConfig) не сконфигурированы в базе данных.") + raise ImproperlyConfigured("BotConfig не задан.") - # Можно дополнительно использовать config.channel_id и config.bot_name при необходимости. - bot = Bot(token=config.bot_token) - return bot + logger.debug("Создан экземпляр бота с токеном.") + return Bot(token=config.bot_token) -def notify_user(binding_request, approved=True): - bot_token = settings.BOT_CONFIG.bot_token # можно использовать модель BotConfig или настройку из settings - bot = Bot(token=bot_token) - message = "Ваша заявка на привязку успешно подтверждена!" if approved else "Ваша заявка на привязку отклонена." +def get_event_message_sync(event_key: str, context_vars: dict = None): + logger.debug(f"get_event_message_sync для события: {event_key}") try: - bot.send_message(chat_id=binding_request.telegram_chat_id, text=message) + config = BotEventMessageConfig.objects.select_related("message").get(event=event_key, enabled=True) + msg = config.message + context_vars = context_vars or {} + + text = msg.text.format(**context_vars) if msg.text else None + image_name = msg.image.name if msg.image else None # только имя файла + + keyboard = None + if msg.buttons_json: + keyboard = InlineKeyboardMarkup([ + [InlineKeyboardButton(btn["text"], url=btn["url"])] + for btn in msg.buttons_json + ]) + + return { + "text": text, + "image": image_name, + "keyboard": keyboard + } + except Exception as e: - # Обработка ошибок отправки сообщения - print(f"Ошибка уведомления: {e}") + logger.error(f"Ошибка get_event_message_sync для события '{event_key}': {e}") + return None -# Пример использования в точке входа приложения: -if __name__ == '__main__': - # Предполагается, что Django уже настроен (например, через manage.py shell или management command) - bot = create_bot_instance() - # Теперь можно использовать объект bot для отправки сообщений, обработки обновлений и т.д. - print(f"Бот {bot.name} успешно создан!") - - \ No newline at end of file +def send_event_message_sync(event: str, bot: Bot, chat_id: int, context_vars: dict = None): + msg = get_event_message_sync(event, context_vars) + if not msg: + logger.warning(f"Пропущена отправка: шаблон события '{event}' не найден.") + return + try: + if msg["image"]: + bot.send_photo(chat_id=chat_id, photo=msg["image"], caption=msg["text"], reply_markup=msg["keyboard"]) + elif msg["text"]: + bot.send_message(chat_id=chat_id, text=msg["text"], reply_markup=msg["keyboard"]) + logger.info(f"Сообщение '{event}' успешно отправлено пользователю {chat_id}") + except Exception as e: + logger.error(f"Ошибка при отправке сообщения '{event}' пользователю {chat_id}: {e}") + +@sync_to_async +def get_event_message_async(event_key: str, context_vars: dict = None): + return get_event_message_sync(event_key, context_vars) + +async def send_event_message_async(event: str, bot: Bot, chat_id: int, context_vars: dict = None): + msg = await get_event_message_async(event, context_vars) + if not msg: + logger.warning(f"[ASYNC] Пропущена отправка: шаблон события '{event}' не найден.") + return + + try: + if msg["image"]: + # локальный путь к файлу + from django.conf import settings + file_path = os.path.join(settings.MEDIA_ROOT, msg["image"]) + logger.debug(f"[ASYNC] Отправка фото из {file_path}") + with open(file_path, "rb") as photo: + await bot.send_photo( + chat_id=chat_id, + photo=InputFile(photo), + caption=msg["text"], + reply_markup=msg["keyboard"] + ) + elif msg["text"]: + await bot.send_message( + chat_id=chat_id, + text=msg["text"], + reply_markup=msg["keyboard"] + ) + + logger.info(f"[ASYNC] Сообщение '{event}' отправлено пользователю {chat_id}") + + except Exception as e: + logger.error(f"[ASYNC] Ошибка Telegram при отправке '{event}' пользователю {chat_id}: {e}") diff --git a/lottery/bot_messages/photo_2025-07-10_09-33-13.jpg b/lottery/bot_messages/photo_2025-07-10_09-33-13.jpg new file mode 100644 index 0000000..24e7a30 Binary files /dev/null and b/lottery/bot_messages/photo_2025-07-10_09-33-13.jpg differ diff --git a/lottery/bot_messages/photo_2025-07-10_09-33-22.jpg b/lottery/bot_messages/photo_2025-07-10_09-33-22.jpg new file mode 100644 index 0000000..311b4ee Binary files /dev/null and b/lottery/bot_messages/photo_2025-07-10_09-33-22.jpg differ diff --git a/lottery/bot_messages/photo_2025-07-10_09-33-33.jpg b/lottery/bot_messages/photo_2025-07-10_09-33-33.jpg new file mode 100644 index 0000000..c0ebf8f Binary files /dev/null and b/lottery/bot_messages/photo_2025-07-10_09-33-33.jpg differ diff --git a/lottery/bot_messages/photo_2025-07-10_09-33-35.jpg b/lottery/bot_messages/photo_2025-07-10_09-33-35.jpg new file mode 100644 index 0000000..56ce4d6 Binary files /dev/null and b/lottery/bot_messages/photo_2025-07-10_09-33-35.jpg differ diff --git a/lottery/draw/admin.py b/lottery/draw/admin.py index 679eb8a..9bacd3a 100644 --- a/lottery/draw/admin.py +++ b/lottery/draw/admin.py @@ -1,23 +1,25 @@ import random import logging from asgiref.sync import async_to_sync +from datetime import datetime + +from django import forms from django.contrib import admin, messages from django.urls import path, reverse from django.shortcuts import render, redirect, get_object_or_404 from django.utils import timezone -from django.http import HttpResponseRedirect, HttpResponse +from django.utils.dateparse import parse_date from django.utils.html import format_html +from django.http import HttpResponseRedirect, HttpResponse + from .models import Lottery, Prize, LotteryParticipant, DrawResult from .forms import AddParticipantsForm from webapp.models import Invoice, Client, BindingRequest from bot.notifications import NotificationService from bot.utils import create_bot_instance from .views import view_draw_results -from django import forms -from django.utils.dateparse import parse_datetime, parse_date -from datetime import datetime +import os -# Настройка логгера logger = logging.getLogger(__name__) logger.setLevel(logging.DEBUG) if not logger.handlers: @@ -34,7 +36,6 @@ def add_participants_view(request): return HttpResponse("Не указан параметр lottery_id", status=400) lottery = get_object_or_404(Lottery, id=lottery_id) - # Только счета, у которых used=False, и не добавленные в данную лотерею used_invoice_ids = LotteryParticipant.objects.filter(lottery=lottery).values_list("invoice_id", flat=True) qs = Invoice.objects.filter(used=False).exclude(id__in=used_invoice_ids) @@ -47,7 +48,6 @@ def add_participants_view(request): qs = qs.filter(deposit_sum__gte=deposit_min) if deposit_max: qs = qs.filter(deposit_sum__lte=deposit_max) - if created_after: try: dt = parse_date(created_after) @@ -55,7 +55,6 @@ def add_participants_view(request): qs = qs.filter(created_at__gte=dt) except ValueError: pass - if created_before: try: dt = parse_date(created_before) @@ -63,14 +62,13 @@ def add_participants_view(request): qs = qs.filter(created_at__lte=dt) except ValueError: pass - + if request.method == "POST": form = AddParticipantsForm(request.POST) form.fields["invoices"].queryset = qs if form.is_valid(): selected_invoices = form.cleaned_data["invoices"] for invoice in selected_invoices: - # При добавлении участника отмечаем счет как использованный invoice.used = True invoice.save() LotteryParticipant.objects.create(lottery=lottery, invoice=invoice) @@ -85,16 +83,90 @@ def add_participants_view(request): def get_client_by_invoice(invoice): - """ - Возвращает клиента, используя значение поля client_club_card_number у счета. - Предполагается, что у модели Invoice есть поле client_club_card_number. - """ try: return Client.objects.get(club_card_number=invoice.client_club_card_number) except Client.DoesNotExist: return None +# def start_draw(request, lottery_id): +# lottery = get_object_or_404(Lottery, id=lottery_id) +# logger.info("Запуск розыгрыша для лотереи: %s", lottery.name) + +# if lottery.finished: +# messages.warning(request, "Розыгрыш уже завершён.") +# return redirect("..") + +# notifier = NotificationService(bot=create_bot_instance()) +# async_to_sync(notifier.notify_draw_start)(lottery) + +# manually_assigned_invoice_ids = set() +# for prize in lottery.prizes.all(): +# if prize.winner and prize.winner.invoice: +# manually_assigned_invoice_ids.add(prize.winner.invoice_id) +# prize.winner.used = True +# prize.winner.save() + +# for prize in lottery.prizes.all(): +# logger.info("Обработка приза: %s", prize.prize_place) + +# if prize.winner: +# try: +# draw_result = lottery.draw_results.get(prize=prize) +# draw_result.participant = prize.winner +# draw_result.drawn_at = timezone.now() +# draw_result.confirmed = False +# draw_result.save() +# except DrawResult.DoesNotExist: +# DrawResult.objects.create( +# lottery=lottery, +# prize=prize, +# participant=prize.winner, +# confirmed=False, +# drawn_at=timezone.now() +# ) +# continue + +# try: +# draw_result = lottery.draw_results.get(prize=prize) +# if draw_result.confirmed: +# continue +# except DrawResult.DoesNotExist: +# draw_result = None + +# participants = list( +# lottery.participants.filter(used=False).exclude(invoice_id__in=manually_assigned_invoice_ids) +# ) +# if not participants: +# continue + +# winner_participant = random.choice(participants) +# winner_participant.used = True +# winner_participant.save() + +# if draw_result: +# draw_result.participant = winner_participant +# draw_result.drawn_at = timezone.now() +# draw_result.confirmed = False +# draw_result.save() +# else: +# DrawResult.objects.create( +# lottery=lottery, +# prize=prize, +# participant=winner_participant, +# confirmed=False, +# drawn_at=timezone.now() +# ) + +# draw_results = lottery.draw_results.all() +# async_to_sync(notifier.notify_draw_results)(lottery, draw_results) + +# if not lottery.prizes.filter(winner__isnull=True).exists(): +# lottery.finished = True +# lottery.save() + +# return render(request, "admin/draw_result.html", {"lottery": lottery, "draw_results": draw_results}) + def start_draw(request, lottery_id): lottery = get_object_or_404(Lottery, id=lottery_id) logger.info("Запуск розыгрыша для лотереи: %s", lottery.name) @@ -104,23 +176,18 @@ def start_draw(request, lottery_id): return redirect("..") notifier = NotificationService(bot=create_bot_instance()) - async_to_sync(notifier.notify_draw_start)(lottery) - # Собираем ID счетов вручную назначенных победителей manually_assigned_invoice_ids = set() for prize in lottery.prizes.all(): if prize.winner and prize.winner.invoice: manually_assigned_invoice_ids.add(prize.winner.invoice_id) - # Помечаем вручную выбранного участника как использованного prize.winner.used = True prize.winner.save() for prize in lottery.prizes.all(): logger.info("Обработка приза: %s", prize.prize_place) - # Если у приза уже установлен победитель вручную — сохраняем его в таблицу результатов if prize.winner: - logger.info("Приз '%s' имеет установленного вручную победителя. Сохраняем в таблице результатов.", prize.prize_place) try: draw_result = lottery.draw_results.get(prize=prize) draw_result.participant = prize.winner @@ -140,31 +207,26 @@ def start_draw(request, lottery_id): try: draw_result = lottery.draw_results.get(prize=prize) if draw_result.confirmed: - logger.info("Приз '%s' уже подтвержден.", prize.prize_place) continue except DrawResult.DoesNotExist: draw_result = None - # Получаем всех неиспользованных участников, исключая вручную выбранных participants = list( lottery.participants.filter(used=False).exclude(invoice_id__in=manually_assigned_invoice_ids) ) - logger.info("Найдено свободных участников для приза '%s': %d", prize.prize_place, len(participants)) + logger.info(f"Найдено участников: {len(participants)} для приза {prize.prize_place}") if not participants: - logger.warning("Нет свободных участников для приза '%s'.", prize.prize_place) continue winner_participant = random.choice(participants) winner_participant.used = True winner_participant.save() - logger.info("Выбран участник с счетом '%s' для приза '%s'.", winner_participant.invoice.api_id, prize.prize_place) if draw_result: draw_result.participant = winner_participant draw_result.drawn_at = timezone.now() draw_result.confirmed = False draw_result.save() - logger.info("Обновлен результат розыгрыша для приза '%s'.", prize.prize_place) else: DrawResult.objects.create( lottery=lottery, @@ -173,10 +235,30 @@ def start_draw(request, lottery_id): confirmed=False, drawn_at=timezone.now() ) - logger.info("Создан результат розыгрыша для приза '%s'.", prize.prize_place) - draw_results = lottery.draw_results.all() - async_to_sync(notifier.notify_draw_results)(lottery, draw_results) + draw_results = lottery.draw_results.select_related('prize', 'participant__invoice') + + # 🧠 Считаем сумму призов и формируем список победителей + total_reward = sum([r.prize.reward for r in draw_results if r.prize and r.prize.reward]) + winners_list = "" + for r in draw_results: + try: + winners_list += f"• Счёт {r.participant.invoice.id} — {r.prize.reward}₩\\n" + except Exception: + continue + + async def notify_all(): + await notifier.notify_draw_start(lottery) + await notifier.notify_draw_results( + lottery, + results=draw_results, + context_vars={ + "total_reward": f"{total_reward:,}", + "winners_list": winners_list.strip() + } + ) + + async_to_sync(notify_all)() if not lottery.prizes.filter(winner__isnull=True).exists(): lottery.finished = True @@ -185,15 +267,14 @@ def start_draw(request, lottery_id): return render(request, "admin/draw_result.html", {"lottery": lottery, "draw_results": draw_results}) + def confirm_draw_result(request, result_id): - from django.http import HttpResponseRedirect result = get_object_or_404(DrawResult, id=result_id) - - # Проверяем, что для результата задан участник и его счет + if not result.participant or not result.participant.invoice: messages.error(request, "Невозможно подтвердить результат: отсутствует участник или его счет.") return HttpResponseRedirect(reverse("admin:start_draw", args=[result.lottery.id])) - + result.confirmed = True result.save() @@ -201,18 +282,14 @@ def confirm_draw_result(request, result_id): prize.winner = result.participant prize.save() - logger.info("Подтвержден результат розыгрыша для приза '%s'.", prize.prize_place) - messages.success(request, f"Результат для приза '{prize.prize_place}' подтвержден.") - - # Получаем клиента по счету участника client = get_client_by_invoice(result.participant.invoice) if client: try: - # Можно попробовать запуск уведомления в отдельном потоке или отключить его временно: async_to_sync(NotificationService(bot=create_bot_instance()).notify_prize_status_update)(client, result) except Exception as e: logger.error(f"Ошибка отправки уведомления о статусе приза пользователю {client.telegram_id}: {e}") + messages.success(request, f"Результат для приза '{prize.prize_place}' подтвержден.") return HttpResponseRedirect(reverse("admin:view_draw_results", args=[result.lottery.id])) @@ -227,16 +304,15 @@ class LotteryAdmin(admin.ModelAdmin): path('/start-draw/', self.admin_site.admin_view(start_draw), name='start_draw'), path('confirm-draw-result//', self.admin_site.admin_view(confirm_draw_result), name='confirm_draw_result'), path('/view-draw-results/', self.admin_site.admin_view(view_draw_results), name='view_draw_results'), - ] return custom_urls + urls def start_draw_button(self, obj): - # Если лотерея завершена, кнопку скрываем if obj.finished: return "" url = reverse("admin:start_draw", args=[obj.pk]) return format_html('Запуск розыгрыша', url) + start_draw_button.short_description = "Розыгрыш" @@ -248,7 +324,6 @@ class PrizeAdmin(admin.ModelAdmin): def get_form(self, request, obj=None, **kwargs): form = super().get_form(request, obj, **kwargs) - # При создании нового приза поле winner скрываем, так как участников ещё нет if obj is None: form.base_fields.pop('winner', None) return form @@ -258,13 +333,12 @@ class PrizeAdmin(admin.ModelAdmin): obj_id = request.resolver_match.kwargs.get('object_id') try: prize_obj = self.model.objects.get(pk=obj_id) - # Ограничиваем выбор участниками данной лотереи kwargs["queryset"] = prize_obj.lottery.participants.all() except self.model.DoesNotExist: kwargs["queryset"] = self.model.objects.none() return super().formfield_for_foreignkey(db_field, request, **kwargs) - - + + @admin.register(LotteryParticipant) class LotteryParticipantAdmin(admin.ModelAdmin): list_display = ("lottery", "invoice", "added_at", "used") @@ -274,13 +348,11 @@ class LotteryParticipantAdmin(admin.ModelAdmin): def changelist_view(self, request, extra_context=None): extra_context = extra_context or {} - from draw.models import Lottery active_lotteries = Lottery.objects.filter(prizes__winner__isnull=True).distinct() extra_context['active_lotteries'] = active_lotteries return super().changelist_view(request, extra_context=extra_context) def get_urls(self): - from django.urls import path urls = super().get_urls() custom_urls = [ path('add-participants/', self.admin_site.admin_view(add_participants_view), name="add_participants"), diff --git a/lottery/lottery/settings.py b/lottery/lottery/settings.py index 5b1b7a9..331d706 100644 --- a/lottery/lottery/settings.py +++ b/lottery/lottery/settings.py @@ -157,6 +157,11 @@ STATIC_ROOT = os.path.join(BASE_DIR, 'staticfiles') DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField' +MEDIA_URL = "/media/" +MEDIA_ROOT = os.path.join(BASE_DIR, "media") + + + JAZZMIN_SETTINGS = { # Общие настройки "site_title": "LuckyTicket", @@ -192,6 +197,8 @@ JAZZMIN_SETTINGS = { "icons": { "bot" : "fa-solid fa-robot", "bot.BotConfig" : "fa-solid fa-wrench", + "bot.BotMessage" : "fas fa-envelope", + "bot.BotEventMessageConfig" : "fa-solid fa-screwdriver-wrench", "bot.WelcomeMessage" : "fa-solid fa-message", "auth": "fas fa-user", "auth.user": "fas fa-user", diff --git a/lottery/lottery/urls.py b/lottery/lottery/urls.py index 29028d5..1b75069 100644 --- a/lottery/lottery/urls.py +++ b/lottery/lottery/urls.py @@ -17,9 +17,13 @@ Including another URLconf from django.contrib import admin from django.urls import path, include from django.shortcuts import redirect +from django.conf import settings +from django.conf.urls.static import static urlpatterns = [ path('', lambda request: redirect('admin:index')), path('admin/', admin.site.urls), path('invoices/', include('webapp.urls')) ] +if settings.DEBUG: + urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT) \ No newline at end of file diff --git a/lottery/media/bot_messages/photo_2025-07-10_09-33-13.jpg b/lottery/media/bot_messages/photo_2025-07-10_09-33-13.jpg new file mode 100644 index 0000000..24e7a30 Binary files /dev/null and b/lottery/media/bot_messages/photo_2025-07-10_09-33-13.jpg differ diff --git a/lottery/media/bot_messages/photo_2025-07-10_09-33-15.jpg b/lottery/media/bot_messages/photo_2025-07-10_09-33-15.jpg new file mode 100644 index 0000000..cb5e8b0 Binary files /dev/null and b/lottery/media/bot_messages/photo_2025-07-10_09-33-15.jpg differ diff --git a/lottery/media/bot_messages/photo_2025-07-10_09-33-17_2.jpg b/lottery/media/bot_messages/photo_2025-07-10_09-33-17_2.jpg new file mode 100644 index 0000000..83cbab8 Binary files /dev/null and b/lottery/media/bot_messages/photo_2025-07-10_09-33-17_2.jpg differ diff --git a/lottery/media/bot_messages/photo_2025-07-10_09-33-34.jpg b/lottery/media/bot_messages/photo_2025-07-10_09-33-34.jpg new file mode 100644 index 0000000..56934b5 Binary files /dev/null and b/lottery/media/bot_messages/photo_2025-07-10_09-33-34.jpg differ diff --git a/lottery/media/welcome_messages/photo_2025-07-10_09-33-33.jpg b/lottery/media/welcome_messages/photo_2025-07-10_09-33-33.jpg new file mode 100644 index 0000000..c0ebf8f Binary files /dev/null and b/lottery/media/welcome_messages/photo_2025-07-10_09-33-33.jpg differ diff --git a/lottery/requirements.txt b/lottery/requirements.txt index 6e033fb..0f0b80e 100644 --- a/lottery/requirements.txt +++ b/lottery/requirements.txt @@ -23,4 +23,5 @@ docker django-celery-beat django-celery-results django-redis -flower \ No newline at end of file +flower +mysqlclient \ No newline at end of file diff --git a/lottery/webapp/models.py b/lottery/webapp/models.py index 2afdaa2..f658709 100644 --- a/lottery/webapp/models.py +++ b/lottery/webapp/models.py @@ -1,6 +1,7 @@ from django.db import models from django.core.validators import MinValueValidator from decimal import Decimal + class Client(models.Model): name = models.CharField("Имя", max_length=255) club_card_number = models.CharField("Номер клубной карты", max_length=100, unique=True)