Compare commits

12 Commits

24 changed files with 505 additions and 193 deletions

View File

@@ -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)
@@ -36,3 +36,11 @@ class BotConfigAdmin(admin.ModelAdmin):
class WWelcomeMessageAdmin(admin.ModelAdmin):
list_display = ("bot", "welcome_message", "welcome_image", "admin_contact", "channel_link", "group_link", "custom_link1_name", "custom_link1_url")
@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")

View File

@@ -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"

View File

@@ -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')),
],
),
]

View File

@@ -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='Событие'),
),
]

View File

@@ -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='Приветственное изображение'),
),
]

View File

@@ -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}"

View File

@@ -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}")

View File

@@ -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} успешно создан!")
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}")

Binary file not shown.

After

Width:  |  Height:  |  Size: 118 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 129 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 80 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 96 KiB

View File

@@ -1,23 +1,27 @@
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
from django.db.models import Q
# Настройка логгера
logger = logging.getLogger(__name__)
logger.setLevel(logging.DEBUG)
if not logger.handlers:
@@ -34,10 +38,11 @@ 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)
# Фильтрация
deposit_min = request.GET.get("deposit_min")
deposit_max = request.GET.get("deposit_max")
created_after = request.GET.get("created_after")
@@ -52,7 +57,7 @@ def add_participants_view(request):
try:
dt = parse_date(created_after)
if dt:
qs = qs.filter(created_at__gte=dt)
qs = qs.filter(created_at__date__gte=dt)
except ValueError:
pass
@@ -60,17 +65,23 @@ def add_participants_view(request):
try:
dt = parse_date(created_before)
if dt:
qs = qs.filter(created_at__lte=dt)
qs = qs.filter(created_at__date__lte=dt)
except ValueError:
pass
if request.GET.get("without_bonus"):
qs = qs.filter(Q(bonus__isnull=True) | Q(bonus=0))
if request.GET.get("without_fd"):
qs = qs.filter(Q(start_bonus__isnull=True) | Q(start_bonus=0))
# Обработка формы
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)
@@ -80,21 +91,98 @@ def add_participants_view(request):
form = AddParticipantsForm()
form.fields["invoices"].queryset = qs
context = {"form": form, "lottery": lottery}
context = {
"form": form,
"lottery": lottery,
"invoice_count": qs.count(), # Для отображения числа найденных
}
return render(request, "admin/add_participants.html", context)
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 +192,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 +223,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 +251,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,11 +283,10 @@ 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]))
@@ -201,18 +298,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 +320,15 @@ class LotteryAdmin(admin.ModelAdmin):
path('<int:lottery_id>/start-draw/', self.admin_site.admin_view(start_draw), name='start_draw'),
path('confirm-draw-result/<int:result_id>/', self.admin_site.admin_view(confirm_draw_result), name='confirm_draw_result'),
path('<int:lottery_id>/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('<a class="btn btn-primary" href="{}">Запуск розыгрыша</a>', url)
start_draw_button.short_description = "Розыгрыш"
@@ -248,7 +340,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,7 +349,6 @@ 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()
@@ -274,13 +364,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"),

View File

@@ -6,7 +6,7 @@ class AddParticipantsForm(forms.Form):
deposit_min = forms.DecimalField(label="Минимальный депозит", required=False)
deposit_max = forms.DecimalField(label="Максимальный депозит", required=False)
invoices = forms.ModelMultipleChoiceField(
queryset=Invoice.objects.none(),
queryset=Invoice.objects.none(), # устанавливается в представлении
widget=forms.CheckboxSelectMultiple,
required=False,
label="Доступные счета"

View File

@@ -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",

View File

@@ -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)

Binary file not shown.

After

Width:  |  Height:  |  Size: 118 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 106 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 135 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 119 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 80 KiB

View File

@@ -24,3 +24,4 @@ django-celery-beat
django-celery-results
django-redis
flower
mysqlclient

View File

@@ -48,6 +48,18 @@
<input type="date" name="created_before" value="{{ request.GET.created_before }}" class="form-control">
</div>
</div>
<div class="row mt-3">
<div class="col-md-3 form-check mt-2">
<input type="checkbox" class="form-check-input" name="without_bonus" id="without_bonus" {% if request.GET.without_bonus %}checked{% endif %}>
<label class="form-check-label" for="without_bonus">Только без бонуса</label>
</div>
<div class="col-md-3 form-check mt-2">
<input type="checkbox" class="form-check-input" name="without_fd" id="without_fd" {% if request.GET.without_fd %}checked{% endif %}>
<label class="form-check-label" for="without_fd">Только без ФД</label>
</div>
</div>
<div class="mt-3">
<button type="submit" class="btn btn-primary">Применить фильтр</button>
</div>
@@ -59,13 +71,13 @@
<form method="post">
{% csrf_token %}
<div class="table-responsive">
<table class="table table-striped table-bordered table-sm">
<table class="table table-striped table-bordered table-sm align-middle">
<thead>
<tr>
<th><input type="checkbox" id="select-all" /></th>
<th>Создан</th>
<th>Закрыт</th>
<th>Счет</th>
<th>Счёт</th>
<th>Клиент</th>
<th>Номер клиента</th>
<th>Сумма счета</th>
@@ -78,15 +90,9 @@
<tbody>
{% for invoice in form.fields.invoices.queryset %}
<tr>
<input type="checkbox" name="invoices" value="{{ invoice.id }}">
<td><input type="checkbox" name="invoices[]" value="{{ invoice.id }}"></td>
<td>{{ invoice.created_at|date:"d.m.Y H:i" }}</td>
<td>
{% if invoice.closed_at %}
{{ invoice.closed_at|date:"d.m.Y H:i" }}
{% else %}
{% endif %}
</td>
<td>{% if invoice.closed_at %}{{ invoice.closed_at|date:"d.m.Y H:i"}}{% else %}—{% endif %}</td>
<td>{{ invoice.ext_id|default:"—" }}</td>
<td>{{ invoice.client.name|default:"Не указан" }}</td>
<td>{{ invoice.client.club_card_number|default:"—" }}</td>
@@ -107,8 +113,6 @@
<button type="submit" class="btn btn-primary">Добавить выбранные счета</button>
</form>
<a href="{% url 'admin:draw_lotteryparticipant_changelist' %}" class="btn btn-secondary mt-3">Вернуться к списку участников</a>
</div>
{% endblock content %}

View File

@@ -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)