Compare commits

2 Commits

Author SHA1 Message Date
422389ecfb Merge pull request 'db_data removed' (#2) from dev into master
Reviewed-on: #2
2025-07-21 08:23:45 +00:00
dd849e00f3 Merge pull request 'Bot container restart from admin-panel' (#1) from dev into master
Reviewed-on: #1
2025-07-21 08:20:18 +00:00
9 changed files with 66 additions and 161 deletions

View File

@@ -12,10 +12,6 @@ from .forms import AddParticipantsForm
from webapp.models import Invoice, Client, BindingRequest from webapp.models import Invoice, Client, BindingRequest
from bot.notifications import NotificationService from bot.notifications import NotificationService
from bot.utils import create_bot_instance 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
# Настройка логгера # Настройка логгера
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@@ -40,30 +36,11 @@ def add_participants_view(request):
deposit_min = request.GET.get("deposit_min") deposit_min = request.GET.get("deposit_min")
deposit_max = request.GET.get("deposit_max") deposit_max = request.GET.get("deposit_max")
created_after = request.GET.get("created_after")
created_before = request.GET.get("created_before")
if deposit_min: if deposit_min:
qs = qs.filter(deposit_sum__gte=deposit_min) qs = qs.filter(deposit_sum__gte=deposit_min)
if deposit_max: if deposit_max:
qs = qs.filter(deposit_sum__lte=deposit_max) qs = qs.filter(deposit_sum__lte=deposit_max)
if created_after:
try:
dt = parse_date(created_after)
if dt:
qs = qs.filter(created_at__gte=dt)
except ValueError:
pass
if created_before:
try:
dt = parse_date(created_before)
if dt:
qs = qs.filter(created_at__lte=dt)
except ValueError:
pass
if request.method == "POST": if request.method == "POST":
form = AddParticipantsForm(request.POST) form = AddParticipantsForm(request.POST)
form.fields["invoices"].queryset = qs form.fields["invoices"].queryset = qs
@@ -99,6 +76,7 @@ def start_draw(request, lottery_id):
lottery = get_object_or_404(Lottery, id=lottery_id) lottery = get_object_or_404(Lottery, id=lottery_id)
logger.info("Запуск розыгрыша для лотереи: %s", lottery.name) logger.info("Запуск розыгрыша для лотереи: %s", lottery.name)
# Если лотерея уже завершена, кнопку запускать не показываем
if lottery.finished: if lottery.finished:
messages.warning(request, "Розыгрыш уже завершён.") messages.warning(request, "Розыгрыш уже завершён.")
return redirect("..") return redirect("..")
@@ -106,19 +84,10 @@ def start_draw(request, lottery_id):
notifier = NotificationService(bot=create_bot_instance()) notifier = NotificationService(bot=create_bot_instance())
async_to_sync(notifier.notify_draw_start)(lottery) 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(): for prize in lottery.prizes.all():
logger.info("Обработка приза: %s", prize.prize_place) logger.info("Обработка приза: %s", prize.prize_place)
# Если у приза уже установлен победитель вручную сохраняем его в таблицу результатов # Если для приза уже назначен победитель вручную, сохраняем его в таблице результатов
if prize.winner: if prize.winner:
logger.info("Приз '%s' имеет установленного вручную победителя. Сохраняем в таблице результатов.", prize.prize_place) logger.info("Приз '%s' имеет установленного вручную победителя. Сохраняем в таблице результатов.", prize.prize_place)
try: try:
@@ -145,10 +114,7 @@ def start_draw(request, lottery_id):
except DrawResult.DoesNotExist: except DrawResult.DoesNotExist:
draw_result = None draw_result = None
# Получаем всех неиспользованных участников, исключая вручную выбранных participants = list(lottery.participants.filter(used=False))
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("Найдено свободных участников для приза '%s': %d", prize.prize_place, len(participants))
if not participants: if not participants:
logger.warning("Нет свободных участников для приза '%s'.", prize.prize_place) logger.warning("Нет свободных участников для приза '%s'.", prize.prize_place)
@@ -178,6 +144,7 @@ def start_draw(request, lottery_id):
draw_results = lottery.draw_results.all() draw_results = lottery.draw_results.all()
async_to_sync(notifier.notify_draw_results)(lottery, draw_results) async_to_sync(notifier.notify_draw_results)(lottery, draw_results)
# Если все призы розыгрыша подтверждены, устанавливаем флаг завершения лотереи
if not lottery.prizes.filter(winner__isnull=True).exists(): if not lottery.prizes.filter(winner__isnull=True).exists():
lottery.finished = True lottery.finished = True
lottery.save() lottery.save()
@@ -206,6 +173,8 @@ def confirm_draw_result(request, result_id):
# Получаем клиента по счету участника # Получаем клиента по счету участника
client = get_client_by_invoice(result.participant.invoice) client = get_client_by_invoice(result.participant.invoice)
# Если уведомление вызывает ошибки, можно временно его отключить,
# чтобы проверить базовую функциональность подтверждения.
if client: if client:
try: try:
# Можно попробовать запуск уведомления в отдельном потоке или отключить его временно: # Можно попробовать запуск уведомления в отдельном потоке или отключить его временно:
@@ -213,7 +182,7 @@ def confirm_draw_result(request, result_id):
except Exception as e: except Exception as e:
logger.error(f"Ошибка отправки уведомления о статусе приза пользователю {client.telegram_id}: {e}") logger.error(f"Ошибка отправки уведомления о статусе приза пользователю {client.telegram_id}: {e}")
return HttpResponseRedirect(reverse("admin:view_draw_results", args=[result.lottery.id])) return HttpResponseRedirect(reverse("admin:start_draw", args=[result.lottery.id]))
@admin.register(Lottery) @admin.register(Lottery)
@@ -226,8 +195,6 @@ class LotteryAdmin(admin.ModelAdmin):
custom_urls = [ custom_urls = [
path('<int:lottery_id>/start-draw/', self.admin_site.admin_view(start_draw), name='start_draw'), 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('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 return custom_urls + urls

View File

@@ -1,12 +1,3 @@
from django.shortcuts import render from django.shortcuts import render
from django.shortcuts import get_object_or_404
from .models import Lottery, DrawResult
# Create your views here.
def view_draw_results(request, lottery_id):
lottery = get_object_or_404(Lottery, id=lottery_id)
draw_results = lottery.draw_results.all()
return render(request, "admin/draw_result.html", {
"lottery": lottery,
"draw_results": draw_results
})

View File

@@ -17,6 +17,7 @@ sniffio==1.3.1
sqlparse==0.5.3 sqlparse==0.5.3
typing_extensions==4.12.2 typing_extensions==4.12.2
urllib3==2.3.0 urllib3==2.3.0
mysqlclient
celery celery
redis redis
docker docker

Binary file not shown.

Before

Width:  |  Height:  |  Size: 80 KiB

View File

@@ -14,7 +14,7 @@
const selectAllCheckbox = document.getElementById("select-all"); const selectAllCheckbox = document.getElementById("select-all");
if (selectAllCheckbox) { if (selectAllCheckbox) {
selectAllCheckbox.addEventListener("click", function(){ selectAllCheckbox.addEventListener("click", function(){
const checkboxes = document.querySelectorAll("input[name='invoices[]']"); const checkboxes = document.querySelectorAll("input[name='invoices']");
checkboxes.forEach(chk => chk.checked = selectAllCheckbox.checked); checkboxes.forEach(chk => chk.checked = selectAllCheckbox.checked);
}); });
} }
@@ -23,38 +23,24 @@
{% endblock extrahead %} {% endblock extrahead %}
{% block content %} {% block content %}
<div class="container-fluid custom-container"> <div class="container-fluid">
<h1>Добавление участников лотереи: {{ lottery.name }}</h1> <h1>Добавление участников лотереи: {{ lottery.name }}</h1>
<p>{{ lottery.description }}</p> <p>{{ lottery.description }}</p>
<!-- Форма фильтрации --> <!-- Форма фильтрации -->
<form method="get" class="mb-4"> <form method="get" class="form-inline mb-3">
<input type="hidden" name="lottery_id" value="{{ lottery.id }}"> <input type="hidden" name="lottery_id" value="{{ lottery.id }}">
<div class="row"> <div class="form-group mr-3">
<div class="col-md-3"> <label for="id_deposit_min" class="mr-2">Минимальный депозит:</label>
<label>Минимальная сумма депозита:</label> <input type="number" step="0.01" name="deposit_min" id="id_deposit_min" class="form-control">
<input type="number" step="0.01" name="deposit_min" value="{{ request.GET.deposit_min }}" class="form-control">
</div> </div>
<div class="col-md-3"> <div class="form-group mr-3">
<label>Максимальная сумма депозита:</label> <label for="id_deposit_max" class="mr-2">Максимальный депозит:</label>
<input type="number" step="0.01" name="deposit_max" value="{{ request.GET.deposit_max }}" class="form-control"> <input type="number" step="0.01" name="deposit_max" id="id_deposit_max" class="form-control">
</div>
<div class="col-md-3">
<label>Дата создания от:</label>
<input type="date" name="created_after" value="{{ request.GET.created_after }}" class="form-control">
</div>
<div class="col-md-3">
<label>Дата создания до:</label>
<input type="date" name="created_before" value="{{ request.GET.created_before }}" class="form-control">
</div>
</div>
<div class="mt-3">
<button type="submit" class="btn btn-primary">Применить фильтр</button>
</div> </div>
<button type="submit" class="btn btn-info">Фильтровать</button>
</form> </form>
<p><strong>Найдено подходящих счетов: {{ invoice_count }}</strong></p>
<!-- Форма добавления участников --> <!-- Форма добавления участников -->
<form method="post"> <form method="post">
{% csrf_token %} {% csrf_token %}
@@ -63,42 +49,30 @@
<thead> <thead>
<tr> <tr>
<th><input type="checkbox" id="select-all" /></th> <th><input type="checkbox" id="select-all" /></th>
<th>Создан</th>
<th>Закрыт</th>
<th>Счет</th> <th>Счет</th>
<th>Клиент</th> <th>Владелец счета</th>
<th>Номер клиента</th>
<th>Сумма счета</th>
<th>Бонус</th>
<th>ФД</th>
<th>Депозит</th> <th>Депозит</th>
<th>Примечание</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
{% for invoice in form.fields.invoices.queryset %} {% for invoice in form.fields.invoices.queryset %}
<tr> <tr>
<input type="checkbox" name="invoices" value="{{ invoice.id }}">
<td>{{ invoice.created_at|date:"d.m.Y H:i" }}</td>
<td> <td>
{% if invoice.closed_at %} <input type="checkbox" name="invoices" value="{{ invoice.id }}" />
{{ invoice.closed_at|date:"d.m.Y H:i" }} </td>
<td>{{ invoice.ext_id }}</td>
<td>
{% if invoice.client_name %}
{{ invoice.client_name }}
{% else %} {% else %}
Не указан
{% endif %} {% endif %}
</td> </td>
<td>{{ invoice.ext_id|default:"—" }}</td> <td>{{ invoice.deposit_sum }}</td>
<td>{{ invoice.client.name|default:"Не указан" }}</td>
<td>{{ invoice.client.club_card_number|default:"—" }}</td>
<td>{{ invoice.sum|default:"—" }}</td>
<td>{{ invoice.bonus|default:"—" }}</td>
<td>{{ invoice.start_bonus|default:"—" }}</td>
<td>{{ invoice.deposit_sum|default:"—" }}</td>
<td>{{ invoice.notes|default:"—" }}</td>
</tr> </tr>
{% empty %} {% empty %}
<tr> <tr>
<td colspan="11" class="text-center">Нет доступных счетов</td> <td colspan="5" class="text-center">Нет доступных счетов</td>
</tr> </tr>
{% endfor %} {% endfor %}
</tbody> </tbody>
@@ -107,8 +81,6 @@
<button type="submit" class="btn btn-primary">Добавить выбранные счета</button> <button type="submit" class="btn btn-primary">Добавить выбранные счета</button>
</form> </form>
<a href="{% url 'admin:draw_lotteryparticipant_changelist' %}" class="btn btn-secondary mt-3">Вернуться к списку участников</a> <a href="{% url 'admin:draw_lotteryparticipant_changelist' %}" class="btn btn-secondary mt-3">Вернуться к списку участников</a>
</div> </div>
{% endblock content %} {% endblock content %}

View File

@@ -4,4 +4,4 @@ from django.apps import AppConfig
class WebappConfig(AppConfig): class WebappConfig(AppConfig):
default_auto_field = 'django.db.models.BigAutoField' default_auto_field = 'django.db.models.BigAutoField'
name = 'webapp' name = 'webapp'
verbose_name='Основная информация' verbose_name='Основноая информация'

View File

@@ -1,17 +0,0 @@
# Generated by Django 5.1.6 on 2025-08-03 05:05
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('webapp', '0008_invoice_used'),
]
operations = [
migrations.AlterModelOptions(
name='apisettings',
options={'verbose_name': 'Настройка API', 'verbose_name_plural': 'Настройки API'},
),
]

View File

@@ -57,13 +57,10 @@ class Invoice(models.Model):
class APISettings(models.Model): class APISettings(models.Model):
api_url = models.URLField("API URL") api_url = models.URLField("API URL")
api_key = models.CharField("API KEY", max_length=255) api_key = models.CharField("API KEY", max_length=255)
class Meta:
verbose_name = "Настройка API"
verbose_name_plural = "Настройки API"
def __str__(self): def __str__(self):
return f"Настройки API: {self.api_url}" return f"Настройки API: {self.api_url}"
class BindingRequest(models.Model): class BindingRequest(models.Model):
STATUS_CHOICES = ( STATUS_CHOICES = (
('pending', 'Ожидает проверки'), ('pending', 'Ожидает проверки'),

View File

@@ -95,19 +95,14 @@ class API_SYNC:
self.logger.warning("Запись клиента %s пропущена: отсутствует club_card_num. Запись: %s", index, item) self.logger.warning("Запись клиента %s пропущена: отсутствует club_card_num. Запись: %s", index, item)
continue continue
defaults = { # Используем update_or_create для обновления существующей записи
'name': item.get("name"),
}
telegram_id = item.get("telegram_id")
if telegram_id:
defaults['telegram_id'] = telegram_id # Обновим только если значение есть
obj, created = Client.objects.update_or_create( obj, created = Client.objects.update_or_create(
club_card_number=club_card_number, club_card_number=club_card_number,
defaults=defaults defaults={
'name': item.get("name"),
'telegram_id': item.get("telegram_id"),
}
) )
new_or_updated += 1 new_or_updated += 1
if created: if created:
self.logger.info("Запись клиента %s создана: club_card_num %s.", index, club_card_number) self.logger.info("Запись клиента %s создана: club_card_num %s.", index, club_card_number)
@@ -117,7 +112,6 @@ class API_SYNC:
self.logger.error("Запись клиента %s имеет неожиданный тип: %s. Значение: %s", index, type(item), item) self.logger.error("Запись клиента %s имеет неожиданный тип: %s. Значение: %s", index, type(item), item)
return new_or_updated return new_or_updated
def sync_invoices(self): def sync_invoices(self):
api_client = APIClient() api_client = APIClient()
data = api_client.get_invoices() data = api_client.get_invoices()