bot prime refactor. Notification events & messages

This commit is contained in:
2025-08-03 21:42:07 +09:00
parent 842710fe5c
commit becf4f5c99
22 changed files with 431 additions and 139 deletions

View File

@@ -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('<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 +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"),