Compare commits

...

18 Commits

Author SHA1 Message Date
53da914bea Merge branch 'bot_refactor' of ssh://git.smartsoltech.kr:2222/trevor/lottery_ycms into bot_refactor 2025-08-07 19:52:37 +09:00
8945fae0c0 template remake 2025-08-07 19:52:00 +09:00
8c00c92577 rollback 2025-08-07 17:53:14 +09:00
4c595b1797 some fixes in templates 2025-08-07 17:51:57 +09:00
a7137678b7 design refactor 2025-08-07 17:47:23 +09:00
b5f3b8ce22 container added 2025-08-07 17:45:20 +09:00
e08e54e4ad btn design 2025-08-07 17:42:30 +09:00
8b6c595184 bugfix 2025-08-06 12:10:18 +09:00
3f5c332ecd table data dysplay fix 2025-08-06 12:09:14 +09:00
8b37877b3e table rafactor 2025-08-06 12:05:59 +09:00
023966bc5b переделка шаблонов для админки лотереи 2025-08-06 12:02:48 +09:00
68caea5937 add_participants: Refactor admin interface for participant management 2025-08-06 11:55:15 +09:00
becf4f5c99 bot prime refactor. Notification events & messages 2025-08-03 21:42:07 +09:00
842710fe5c minor bugfix, UI fix 2025-08-03 14:16:52 +09:00
1d659ea5ee bugfix 2025-08-03 10:10:10 +09:00
7acdcc7465 fixed draw and draw-results notification logics 2025-07-22 05:09:28 +09:00
37b0507c07 db_data removed 2025-07-21 17:22:39 +09:00
421bebb770 Bot container restart from admin-panel 2025-07-21 17:16:07 +09:00
42 changed files with 919 additions and 486 deletions

1
lottery/.gitignore vendored
View File

@@ -12,3 +12,4 @@ var/mysql/
.db .db
.png .png
db_data/

2
lottery/__init__.py Normal file
View File

@@ -0,0 +1,2 @@
from .lottery.celery import app as celery_app
__all__ = ("celery_app",)

View File

@@ -1,37 +1,101 @@
# import requests
# import json
# from dotenv import load_dotenv
# import os
# load_dotenv()
# API_URL='http://106.245.250.243:8000/api/clients'
# API_KEY='g1XqG9lir4RneLuX01VAob+F0MpVCZTpG2is8UBWLZ0='
# def fetch_clients(api_url, api_key):
# """
# Подключаемся к API с использованием заданного X-API-Key
# """
# headers = {
# "X-API-Key": api_key
# }
# try:
# response = requests.get(api_url, headers=headers)
# response.raise_for_status()
# return response.json()
# except requests.exceptions.RequestException as e:
# print(f"Ошибка при запросе к API: {e}")
# return None
# if __name__ == '__main__':
# api_endpoint = f"{os.getenv('API_URL')}"
# api_key = os.getenv("API_KEY")
# print(api_endpoint, api_key)
# if not api_endpoint or not api_key:
# print("Необходимо задать API_URL и API_KEY в .env файле.")
# exit(1)
# clients = fetch_clients(api_endpoint, api_key)
# if clients is not None:
# print(json.dumps(clients, indent=4, ensure_ascii=False))
# else:
# print("Не удалось получить данные с API.")
import requests import requests
import json import json
from dotenv import load_dotenv
import os import os
from dotenv import load_dotenv
load_dotenv('.env') load_dotenv()
def fetch_clients(api_url, api_key): class ApiClient:
""" def __init__(self, base_url=None, api_key=None):
Подключаемся к API с использованием заданного X-API-Key self.base_url = base_url or os.getenv('API_URL', 'http://106.245.250.243:8000/api')
""" self.api_key = api_key or os.getenv('API_KEY')
headers = { if not self.api_key:
"X-API-Key": api_key raise ValueError("API_KEY не задан ни в .env, ни через параметры конструктора")
self.headers = {
"X-API-Key": self.api_key
} }
def fetch_clients(self):
"""Получение списка клиентов"""
try: try:
response = requests.get(api_url, headers=headers) url = f"{self.base_url}/clients"
response = requests.get(url, headers=self.headers)
response.raise_for_status() response.raise_for_status()
return response.json() return response.json()
except requests.exceptions.RequestException as e: except requests.exceptions.RequestException as e:
print(f"Ошибка при запросе к API: {e}") print(f"Ошибка при получении клиентов: {e}")
return None
def fetch_invoices(self, filters=None):
"""Получение списка счетов с возможными фильтрами"""
try:
url = f"{self.base_url}/invoices"
response = requests.get(url, headers=self.headers, params=filters)
response.raise_for_status()
return response.json()
except requests.exceptions.RequestException as e:
print(f"Ошибка при получении счетов: {e}")
return None return None
if __name__ == '__main__': if __name__ == '__main__':
api_endpoint = os.getenv("API_URL") client = ApiClient()
api_key = os.getenv("API_KEY")
print(api_endpoint, api_key) print("=== Клиенты ===")
if not api_endpoint or not api_key: clients = client.fetch_clients()
print("Необходимо задать API_URL и API_KEY в .env файле.") if clients:
exit(1)
clients = fetch_clients(api_endpoint, api_key)
if clients is not None:
print(json.dumps(clients, indent=4, ensure_ascii=False)) print(json.dumps(clients, indent=4, ensure_ascii=False))
else: else:
print("Не удалось получить данные с API.") print("Не удалось получить клиентов")
print("\n=== Счета ===")
filters = {
"page": 1,
# "created_at[after]": "2024-12-01T00:00:00",
# "created_at[before]": "2025-01-01T00:00:00"
}
invoices = client.fetch_invoices(filters)
if invoices:
print(json.dumps(invoices, indent=4, ensure_ascii=False))
else:
print("Не удалось получить счета")

View File

@@ -1,13 +1,46 @@
from django.contrib import admin from django.contrib import admin
from .models import BotConfig, WelcomeMessage from django.urls import path
from django.shortcuts import redirect
from django.contrib import messages
from .models import BotConfig
from .models import BotConfig, WelcomeMessage, BotMessage, BotEventMessageConfig
from .tasks import restart_bot_container
@admin.register(BotConfig) @admin.register(BotConfig)
class BotConfigAdmin(admin.ModelAdmin): class BotConfigAdmin(admin.ModelAdmin):
list_display = ("bot_name", "channel_id", "bot_token") change_form_template = "admin/botconfig_change_form.html"
search_fields = ("bot_name", "channel_id", "bot_token")
def get_urls(self):
urls = super().get_urls()
custom_urls = [
path(
"<int:botconfig_id>/restart/",
self.admin_site.admin_view(self.restart_bot),
name="botconfig-restart"
),
]
return custom_urls + urls
def restart_bot(self, request, botconfig_id):
try:
BotConfig.objects.get(id=botconfig_id)
except BotConfig.DoesNotExist:
self.message_user(request, "Настройка бота с таким ID не найдена.", level=messages.ERROR)
return redirect("..")
restart_bot_container.delay()
self.message_user(request, "Бот будет перезапущен в фоновом режиме.", messages.SUCCESS)
return redirect("..")
@admin.register(WelcomeMessage) @admin.register(WelcomeMessage)
class WWelcomeMessageAdmin(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") 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

@@ -1,11 +1,12 @@
# bot/handlers.py
import re import re
from telegram import Update, InlineKeyboardMarkup, InlineKeyboardButton from telegram import Update, InlineKeyboardMarkup, InlineKeyboardButton
from telegram.ext import ContextTypes from telegram.ext import ContextTypes
from asgiref.sync import sync_to_async from asgiref.sync import sync_to_async
from webapp.models import BindingRequest, Client, Invoice from webapp.models import BindingRequest, Client, Invoice
from draw.models import Lottery, DrawResult, LotteryParticipant from draw.models import DrawResult
from bot.state import BINDING_PENDING from bot.state import BINDING_PENDING
from bot.models import BotConfig
@sync_to_async @sync_to_async
def get_client_by_telegram_id(telegram_id: int): def get_client_by_telegram_id(telegram_id: int):
@@ -32,9 +33,11 @@ def create_binding_request(telegram_chat_id, client_card):
client_card=client_card client_card=client_card
) )
async def whoami(update: Update, context: ContextTypes.DEFAULT_TYPE): async def whoami(update: Update, context: ContextTypes.DEFAULT_TYPE):
if not update.message: if not update.message:
return return
chat_id = update.message.chat_id chat_id = update.message.chat_id
client = await get_client_by_telegram_id(chat_id) client = await get_client_by_telegram_id(chat_id)
if not client: if not client:
@@ -55,8 +58,7 @@ async def whoami(update: Update, context: ContextTypes.DEFAULT_TYPE):
show_all = len(invoices) > 5 show_all = len(invoices) > 5
displayed_invoices = invoices[:5] if show_all else invoices displayed_invoices = invoices[:5] if show_all else invoices
for inv in displayed_invoices: for inv in displayed_invoices:
invoice_str = str(inv).split('/')[0].strip() message_text += f" • ID {inv.id} (*{inv.sum}*)\n"
message_text += f"{invoice_str} (*{inv.sum}*)\n"
else: else:
message_text += " _Нет счетов_\n" message_text += " _Нет счетов_\n"
@@ -64,12 +66,8 @@ async def whoami(update: Update, context: ContextTypes.DEFAULT_TYPE):
if draws: if draws:
for draw in draws: for draw in draws:
lottery_name = draw.lottery.name if draw.lottery else "неизвестно" lottery_name = draw.lottery.name if draw.lottery else "неизвестно"
invoice_info = (draw.participant.invoice invoice_info = getattr(draw.participant.invoice, 'id', 'неизвестно') if draw.participant else 'неизвестно'
if draw.participant and hasattr(draw.participant, 'invoice') prize_info = getattr(draw.prize, 'reward', 'неизвестно')
else "неизвестно")
prize_info = (draw.prize.reward
if draw.prize and hasattr(draw.prize, 'reward')
else "неизвестно")
message_text += f" • Лотерея: *{lottery_name}*\n Счет: _{invoice_info}_\n Выигрыш: *{prize_info}*\n\n" message_text += f" • Лотерея: *{lottery_name}*\n Счет: _{invoice_info}_\n Выигрыш: *{prize_info}*\n\n"
else: else:
message_text += " _Нет результатов розыгрышей_\n" message_text += " _Нет результатов розыгрышей_\n"
@@ -82,43 +80,37 @@ async def whoami(update: Update, context: ContextTypes.DEFAULT_TYPE):
await update.message.reply_text(message_text, parse_mode="Markdown", reply_markup=reply_markup) await update.message.reply_text(message_text, parse_mode="Markdown", reply_markup=reply_markup)
async def handle_client_card(update: Update, context: ContextTypes.DEFAULT_TYPE): async def handle_client_card(update: Update, context: ContextTypes.DEFAULT_TYPE):
"""
Обработчик команды /bind.
После ввода команды бот запрашивает ввод номера КК и ожидает сообщение с кодом.
Здесь мы просто добавляем ID пользователя в pending.
"""
if not update.message: if not update.message:
return return
telegram_chat_id = update.message.chat_id telegram_chat_id = update.message.chat_id
# Если пользователь уже привязан, сообщаем об этом.
client = await get_client_by_telegram_id(telegram_chat_id) client = await get_client_by_telegram_id(telegram_chat_id)
if client: if client:
await update.message.reply_text("✅ Вы уже привязаны!") await update.message.reply_text("✅ Вы уже привязаны!")
return return
# Добавляем пользователя в список pending и просим ввести номер КК.
BINDING_PENDING.add(telegram_chat_id) BINDING_PENDING.add(telegram_chat_id)
await update.message.reply_text("Введите номер вашей клубной карты (КК):") await update.message.reply_text("Введите номер вашей клубной карты (КК):")
await send_event_message("binding_started", update, context)
async def process_binding_input(update: Update, context: ContextTypes.DEFAULT_TYPE): async def process_binding_input(update: Update, context: ContextTypes.DEFAULT_TYPE):
"""
Обрабатывает ввод номера КК от пользователя, который находится в состоянии ожидания.
"""
if not update.message or not update.message.text: if not update.message or not update.message.text:
return return
client_card = update.message.text.strip() client_card = update.message.text.strip()
telegram_chat_id = update.message.chat_id telegram_chat_id = update.message.chat_id
if not re.fullmatch(r'\d{3,}', client_card): if not re.fullmatch(r'\d{3,}', client_card):
await update.message.reply_text("❌ Неверный формат клиентской карты. Пожалуйста, введите корректный код (минимум 3 цифры).") await update.message.reply_text("❌ Неверный формат клиентской карты. Пожалуйста, введите минимум 3 цифры.")
return return
await create_binding_request(telegram_chat_id, client_card) await create_binding_request(telegram_chat_id, client_card)
if telegram_chat_id in BINDING_PENDING: BINDING_PENDING.discard(telegram_chat_id)
BINDING_PENDING.remove(telegram_chat_id)
await update.message.reply_text("✅ Заявка отправлена. После подтверждения вы сможете участвовать в чате.") await update.message.reply_text("✅ Заявка отправлена. После подтверждения вы сможете участвовать в чате.")
async def show_all_invoices_callback(update: Update, context: ContextTypes.DEFAULT_TYPE): async def show_all_invoices_callback(update: Update, context: ContextTypes.DEFAULT_TYPE):
query = update.callback_query query = update.callback_query
await query.answer() await query.answer()
@@ -132,9 +124,18 @@ async def show_all_invoices_callback(update: Update, context: ContextTypes.DEFAU
message_text = "💳 *Все счета:*\n" message_text = "💳 *Все счета:*\n"
if invoices: if invoices:
for inv in invoices: for inv in invoices:
invoice_str = str(inv).split('/')[0].strip() message_text += f" • ID {inv.ext_id} (*{inv.sum}*)\n"
message_text += f"{invoice_str} (*{inv.sum}*)\n"
else: else:
message_text += " _Нет счетов_\n" message_text += " _Нет счетов_\n"
await query.edit_message_text(message_text, parse_mode="Markdown") await query.edit_message_text(message_text, parse_mode="Markdown")
async def start(update: Update, context: ContextTypes.DEFAULT_TYPE):
if not update.message:
return
bot_config = await sync_to_async(BotConfig.objects.filter(is_active=True).first)()
if bot_config and bot_config.active_welcome:
await update.message.reply_text(bot_config.active_welcome.welcome_message)
else:
await update.message.reply_text("Привет! Бот работает.")

View File

@@ -0,0 +1,23 @@
# Generated by Django 5.1.6 on 2025-07-20 09:49
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('bot', '0005_alter_welcomemessage_welcome_image'),
]
operations = [
migrations.AlterField(
model_name='botconfig',
name='global_block_message',
field=models.TextField(blank=True, help_text='Сообщение, которое показывается пользователям, если чат отключён.', null=True, verbose_name='Сообщение о глобальном блоке'),
),
migrations.AlterField(
model_name='welcomemessage',
name='welcome_image',
field=models.ImageField(blank=True, help_text='Загрузите изображение для приветствия', null=True, upload_to='static/upload_image/', verbose_name='Приветственное изображение'),
),
]

View File

@@ -0,0 +1,29 @@
# Generated by Django 5.1.6 on 2025-07-20 23:33
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('bot', '0006_alter_botconfig_global_block_message_and_more'),
]
operations = [
migrations.AddField(
model_name='botconfig',
name='active_welcome',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='used_by', to='bot.welcomemessage', verbose_name='Активное приветствие'),
),
migrations.AddField(
model_name='botconfig',
name='is_active',
field=models.BooleanField(default=False, verbose_name='Включен'),
),
migrations.AddField(
model_name='botconfig',
name='welcome_messages',
field=models.ManyToManyField(blank=True, to='bot.welcomemessage', verbose_name='Варианты приветствия'),
),
]

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,5 +1,7 @@
from django.db import models from django.db import models
class BotConfig(models.Model): class BotConfig(models.Model):
bot_token = models.CharField(max_length=255, help_text="Токен для подключения к Telegram API") bot_token = models.CharField(max_length=255, help_text="Токен для подключения к Telegram API")
channel_id = models.CharField(max_length=100, help_text="ID канала/чата, куда бот будет отправлять сообщения") channel_id = models.CharField(max_length=100, help_text="ID канала/чата, куда бот будет отправлять сообщения")
@@ -15,8 +17,20 @@ class BotConfig(models.Model):
verbose_name="Сообщение о глобальном блоке", verbose_name="Сообщение о глобальном блоке",
help_text="Сообщение, которое показывается пользователям, если чат отключён." help_text="Сообщение, которое показывается пользователям, если чат отключён."
) )
welcome_messages = models.ManyToManyField('WelcomeMessage', blank=True, verbose_name="Варианты приветствия")
active_welcome = models.ForeignKey(
'WelcomeMessage',
null=True,
blank=True,
on_delete=models.SET_NULL,
related_name="used_by",
verbose_name="Активное приветствие"
)
is_active = models.BooleanField(default=False, verbose_name="Включен")
def __str__(self): def __str__(self):
return self.bot_name return self.bot_name
class Meta: class Meta:
verbose_name = "Настройка бота" verbose_name = "Настройка бота"
verbose_name_plural = "Настройки Ботов" verbose_name_plural = "Настройки Ботов"
@@ -31,7 +45,7 @@ class WelcomeMessage(models.Model):
help_text="Текст, который будет отправлен при запуске команды /start" help_text="Текст, который будет отправлен при запуске команды /start"
) )
welcome_image = models.ImageField( welcome_image = models.ImageField(
upload_to='static/upload_image/', upload_to='welcome_messages/',
verbose_name="Приветственное изображение", verbose_name="Приветственное изображение",
blank=True, blank=True,
null=True, null=True,
@@ -85,3 +99,38 @@ class WelcomeMessage(models.Model):
def __str__(self): def __str__(self):
return f"Приветствие для {self.bot}" 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 import logging
from asgiref.sync import sync_to_async 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: class NotificationService:
def __init__(self, bot): def __init__(self, bot):
"""
Инициализация сервиса уведомлений.
:param bot: экземпляр telegram.Bot, используемый для отправки сообщений.
"""
self.bot = 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 @sync_to_async
def _get_all_clients(self): def _get_all_clients(self):
from webapp.models import Client from webapp.models import Client
# Возвращаем только клиентов, у которых telegram_id заполнен корректно clients = list(Client.objects.exclude(telegram_id__in=[None, "", "NULL"]))
return list(Client.objects.exclude(telegram_id__in=[None, "", "NULL"])) self.logger.info(f"Найдено клиентов для уведомления: {len(clients)}")
return clients
@sync_to_async @sync_to_async
def _prepare_results(self, results): def _prepare_results(self, results):
# Принудительно загружаем связанные объекты через select_related
return list(results.select_related('prize', 'participant__invoice')) return list(results.select_related('prize', 'participant__invoice'))
async def notify_draw_start(self, lottery): async def notify_draw_start(self, lottery):
"""
Уведомляет всех клиентов о запуске розыгрыша.
:param lottery: объект розыгрыша (например, модель Lottery)
"""
message_text = f"🎉 *Розыгрыш '{lottery.name}' начался!* Следите за обновлениями."
clients = await self._get_all_clients() clients = await self._get_all_clients()
for client in clients: for client in clients:
if not client.telegram_id: if not client.telegram_id:
continue continue
try: try:
await self.bot.send_message( self.logger.debug(f"Отправка 'draw_started' клиенту {client.telegram_id}")
chat_id=client.telegram_id, await send_event_message_async(
text=message_text, event="draw_started",
parse_mode="Markdown" bot=self.bot,
chat_id=int(client.telegram_id),
context_vars={"lottery": lottery, "client": client}
) )
except Exception as e: 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): # async def notify_draw_results(self, lottery, results):
""" # results_list = await self._prepare_results(results)
Отправляет результаты розыгрыша всем клиентам. # clients = await self._get_all_clients()
:param lottery: объект розыгрыша (Lottery) # for client in clients:
:param results: QuerySet с результатами розыгрыша (например, объекты DrawResult) # 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) 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() 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: for client in clients:
if not client.telegram_id: if not client.telegram_id:
continue continue
try: try:
await self.bot.send_message( self.logger.debug(f"Отправка 'draw_finished' клиенту {client.telegram_id}")
chat_id=client.telegram_id, await send_event_message_async(
text=message_text, event="draw_finished",
parse_mode="Markdown" 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: 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): 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: try:
if client.telegram_id: if client.telegram_id:
await self.bot.send_message( self.logger.debug(f"Отправка 'winner_announced' клиенту {client.telegram_id}")
chat_id=client.telegram_id, await send_event_message_async(
text=message_text, event="winner_announced",
parse_mode="Markdown" bot=self.bot,
chat_id=int(client.telegram_id),
context_vars={"client": client, "result": result, "prize": result.prize}
) )
except Exception as e: 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): async def notify_binding_complete(self, client):
"""
Уведомляет пользователя об окончании привязки клубной карты к Telegram ID.
:param client: объект клиента (Client)
"""
message_text = "✅ *Привязка завершена!* Ваша клубная карта успешно привязана к Telegram. Теперь вы можете участвовать в розыгрышах и чате."
try: try:
if client.telegram_id: if client.telegram_id:
await self.bot.send_message( self.logger.debug(f"Отправка 'binding_complete' клиенту {client.telegram_id}")
chat_id=client.telegram_id, await send_event_message_async(
text=message_text, event="binding_complete",
parse_mode="Markdown" bot=self.bot,
chat_id=int(client.telegram_id),
context_vars={"client": client}
) )
except Exception as e: except Exception as e:
self.logger.error(f"Ошибка отправки уведомления о привязке пользователю {client.telegram_id}: {e}") self.logger.error(f"Ошибка отправки binding_complete: {e}")

21
lottery/bot/tasks.py Normal file
View File

@@ -0,0 +1,21 @@
from celery import shared_task
import subprocess
@shared_task
def restart_bot_container(container_name="bot"):
try:
result = subprocess.run(
["docker", "restart", container_name],
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
text=True,
check=True
)
return f"Success: {result.stdout.strip()}"
except subprocess.CalledProcessError as e:
return f"Error restarting container: {e.stderr.strip()}"
@shared_task
def ping():
return "pong"

View File

@@ -1,39 +1,99 @@
# bot/utils.py from telegram import Bot, InlineKeyboardMarkup, InlineKeyboardButton
from telegram import Bot
from django.conf import settings from django.conf import settings
from telegram import Bot
from bot.models import BotConfig
from django.core.exceptions import ImproperlyConfigured 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(): def create_bot_instance():
""" config = BotConfig.objects.first()
Получает настройки бота из БД и создаёт экземпляр Telegram Bot.
"""
config = BotConfig.objects.first() # предполагается, что конфигурация одна
if not config: if not config:
raise ImproperlyConfigured("Настройки бота (BotConfig) не сконфигурированы в базе данных.") raise ImproperlyConfigured("BotConfig не задан.")
# Можно дополнительно использовать config.channel_id и config.bot_name при необходимости. logger.debug("Создан экземпляр бота с токеном.")
bot = Bot(token=config.bot_token) return Bot(token=config.bot_token)
return bot
def notify_user(binding_request, approved=True): def get_event_message_sync(event_key: str, context_vars: dict = None):
bot_token = settings.BOT_CONFIG.bot_token # можно использовать модель BotConfig или настройку из settings logger.debug(f"get_event_message_sync для события: {event_key}")
bot = Bot(token=bot_token)
message = "Ваша заявка на привязку успешно подтверждена!" if approved else "Ваша заявка на привязку отклонена."
try: 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: except Exception as e:
# Обработка ошибок отправки сообщения logger.error(f"Ошибка get_event_message_sync для события '{event_key}': {e}")
print(f"Ошибка уведомления: {e}") return None
# Пример использования в точке входа приложения: def send_event_message_sync(event: str, bot: Bot, chat_id: int, context_vars: dict = None):
if __name__ == '__main__': msg = get_event_message_sync(event, context_vars)
# Предполагается, что Django уже настроен (например, через manage.py shell или management command) if not msg:
bot = create_bot_instance() logger.warning(f"Пропущена отправка: шаблон события '{event}' не найден.")
# Теперь можно использовать объект bot для отправки сообщений, обработки обновлений и т.д. return
print(f"Бот {bot.name} успешно создан!") 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

@@ -20,6 +20,7 @@ services:
image: lottery_py image: lottery_py
build: . build: .
restart: unless-stopped restart: unless-stopped
container_name: bot
entrypoint: ["./wait-for-db.sh"] entrypoint: ["./wait-for-db.sh"]
command: ["python3", "manage.py", "runbot"] command: ["python3", "manage.py", "runbot"]
depends_on: depends_on:
@@ -31,7 +32,26 @@ services:
- ./var:/app/var - ./var:/app/var
networks: networks:
default: default:
celery:
build:
context: .
dockerfile: docker/celery.Dockerfile
container_name: celery_worker
volumes:
- .:/app
- /var/run/docker.sock:/var/run/docker.sock
depends_on:
- db
- bot
- redis
environment:
- DJANGO_SETTINGS_MODULE=lottery.settings
networks:
- default
redis:
image: redis:7
container_name: redis
restart: unless-stopped
web: web:
image: lottery_py image: lottery_py
build: . build: .
@@ -42,13 +62,36 @@ services:
- "${DOCKER_HTTP_BIND:-8000}:8000" - "${DOCKER_HTTP_BIND:-8000}:8000"
depends_on: depends_on:
- db - db
- redis
env_file: env_file:
- .env - .env
environment:
- DJANGO_SETTINGS_MODULE=lottery.settings
- CELERY_BROKER_URL=redis://redis:6379/0
volumes: volumes:
- .:/app - .:/app
networks: networks:
default: default:
flower:
build:
context: .
dockerfile: docker/flower.Dockerfile
container_name: flower
restart: unless-stopped
working_dir: /app
volumes:
- .:/app
ports:
- "5555:5555"
command: ["python", "-m", "celery", "-A", "lottery", "-b", "redis://redis:6379/0", "flower", "--port=5555", "--basic_auth=admin:secret"]
depends_on:
- redis
- celery
networks:
- default
volumes: volumes:
db_data: db_data:

View File

@@ -0,0 +1,23 @@
FROM python:3.12-slim
# Установим необходимые системные зависимости
RUN apt-get update && apt-get install -y \
build-essential \
libmariadb-dev \
default-libmysqlclient-dev \
pkg-config \
docker.io \
&& rm -rf /var/lib/apt/lists/*
# Создание рабочей директории
WORKDIR /app
# Копирование зависимостей и установка Python-библиотек
COPY requirements.txt ./
RUN pip install --no-cache-dir -r requirements.txt
# Копируем весь проект
COPY . .
# Запуск Celery worker
CMD ["celery", "-A", "lottery", "worker", "--loglevel=info"]

View File

@@ -0,0 +1,16 @@
FROM python:3.12-slim
# Установим системные зависимости, нужные для mysqlclient и других пакетов
RUN apt-get update && apt-get install -y \
build-essential \
default-libmysqlclient-dev \
libmariadb-dev \
pkg-config \
&& rm -rf /var/lib/apt/lists/*
WORKDIR /app
COPY docker/flower.requirements.txt ./
RUN pip install --no-cache-dir -r flower.requirements.txt
CMD ["python", "-m", "celery", "-A", "lottery", "flower", "--port=5555", "--basic_auth=admin:secret", "-b", "redis://redis:6379/0"]

View File

@@ -0,0 +1,14 @@
Django
django-jazzmin
django-celery-beat
django-celery-results
django-redis
pillow
python-dotenv
python-telegram-bot
requests
mysqlclient
celery
redis
docker
flower

View File

@@ -1,236 +1,27 @@
# import random
# import logging
# from asgiref.sync import async_to_sync
# 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.html import format_html
# 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
# # Настройка логгера
# logger = logging.getLogger(__name__)
# logger.setLevel(logging.DEBUG)
# if not logger.handlers:
# console_handler = logging.StreamHandler()
# console_handler.setLevel(logging.DEBUG)
# formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')
# console_handler.setFormatter(formatter)
# logger.addHandler(console_handler)
# def add_participants_view(request):
# lottery_id = request.GET.get("lottery_id")
# if not lottery_id:
# return HttpResponse("Не указан параметр lottery_id", status=400)
# lottery = get_object_or_404(Lottery, id=lottery_id)
# used_invoice_ids = LotteryParticipant.objects.filter(lottery=lottery).values_list("invoice_id", flat=True)
# qs = Invoice.objects.exclude(id__in=used_invoice_ids)
# deposit_min = request.GET.get("deposit_min")
# deposit_max = request.GET.get("deposit_max")
# if deposit_min:
# qs = qs.filter(deposit_sum__gte=deposit_min)
# if deposit_max:
# qs = qs.filter(deposit_sum__lte=deposit_max)
# 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:
# LotteryParticipant.objects.create(lottery=lottery, invoice=invoice)
# messages.success(request, "Участники успешно добавлены.")
# return redirect("admin:draw_lotteryparticipant_changelist")
# else:
# form = AddParticipantsForm()
# form.fields["invoices"].queryset = qs
# context = {"form": form, "lottery": lottery}
# 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)
# notifier = NotificationService(bot=create_bot_instance())
# # Используем async_to_sync для уведомления о запуске розыгрыша
# async_to_sync(notifier.notify_draw_start)(lottery)
# 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
# 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:
# logger.info("Приз '%s' уже подтвержден.", prize.prize_place)
# continue
# except DrawResult.DoesNotExist:
# draw_result = None
# participants = list(lottery.participants.filter(used=False))
# logger.info("Найдено свободных участников для приза '%s': %d", prize.prize_place, len(participants))
# 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,
# prize=prize,
# participant=winner_participant,
# 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)
# 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)
# result.confirmed = True
# result.save()
# prize = result.prize
# 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)
# notifier = NotificationService(bot=create_bot_instance())
# if client:
# async_to_sync(notifier.notify_prize_status_update)(client, result)
# return HttpResponseRedirect(reverse("admin:start_draw", args=[result.lottery.id]))
# @admin.register(Lottery)
# class LotteryAdmin(admin.ModelAdmin):
# list_display = ("name", "description", "created_at", "start_draw_button")
# search_fields = ("name", "description")
# def get_urls(self):
# urls = super().get_urls()
# custom_urls = [
# 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'),
# ]
# return custom_urls + urls
# def start_draw_button(self, obj):
# url = reverse("admin:start_draw", args=[obj.pk])
# return format_html('<a class="btn btn-primary" href="{}">Запуск розыгрыша</a>', url)
# start_draw_button.short_description = "Розыгрыш"
# @admin.register(Prize)
# class PrizeAdmin(admin.ModelAdmin):
# list_display = ("lottery", "prize_place", "reward", "winner")
# list_filter = ("lottery",)
# search_fields = ("prize_place", "description")
# @admin.register(LotteryParticipant)
# class LotteryParticipantAdmin(admin.ModelAdmin):
# list_display = ("lottery", "invoice", "added_at", "used")
# list_filter = ("lottery", "used")
# search_fields = ("invoice__api_id", "lottery__name")
# change_list_template = "admin/draw/lotteryparticipant/change_list.html"
# 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"),
# ]
# return custom_urls + urls
# @admin.register(DrawResult)
# class DrawResultAdmin(admin.ModelAdmin):
# list_display = ("lottery", "prize", "participant", "confirmed", "drawn_at")
# list_filter = ("lottery", "confirmed")
# search_fields = ("prize__prize_place", "participant__invoice__api_id")
import random import random
import logging import logging
from asgiref.sync import async_to_sync from asgiref.sync import async_to_sync
from datetime import datetime
from django import forms
from django.contrib import admin, messages from django.contrib import admin, messages
from django.urls import path, reverse from django.urls import path, reverse
from django.shortcuts import render, redirect, get_object_or_404 from django.shortcuts import render, redirect, get_object_or_404
from django.utils import timezone 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.utils.html import format_html
from django.http import HttpResponseRedirect, HttpResponse
from .models import Lottery, Prize, LotteryParticipant, DrawResult from .models import Lottery, Prize, LotteryParticipant, DrawResult
from .forms import AddParticipantsForm 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
import os
from django.db.models import Q
# Настройка логгера
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
logger.setLevel(logging.DEBUG) logger.setLevel(logging.DEBUG)
if not logger.handlers: if not logger.handlers:
@@ -247,66 +38,86 @@ def add_participants_view(request):
return HttpResponse("Не указан параметр lottery_id", status=400) return HttpResponse("Не указан параметр lottery_id", status=400)
lottery = get_object_or_404(Lottery, id=lottery_id) 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) 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) qs = Invoice.objects.filter(used=False).exclude(id__in=used_invoice_ids)
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__date__gte=dt)
except ValueError:
pass
if created_before:
try:
dt = parse_date(created_before)
if 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": if request.method == "POST":
form = AddParticipantsForm(request.POST) selected_ids = request.POST.getlist("invoices")
form.fields["invoices"].queryset = qs selected_invoices = qs.filter(id__in=selected_ids)
if form.is_valid(): added_count = 0
selected_invoices = form.cleaned_data["invoices"]
for invoice in selected_invoices: for invoice in selected_invoices:
# При добавлении участника отмечаем счет как использованный if not LotteryParticipant.objects.filter(lottery=lottery, invoice=invoice).exists():
invoice.used = True invoice.used = True
invoice.save() invoice.save()
LotteryParticipant.objects.create(lottery=lottery, invoice=invoice) LotteryParticipant.objects.create(lottery=lottery, invoice=invoice)
messages.success(request, "Участники успешно добавлены.") added_count += 1
messages.success(request, f"Добавлено {added_count} участников.")
return redirect("admin:draw_lotteryparticipant_changelist") return redirect("admin:draw_lotteryparticipant_changelist")
else:
form = AddParticipantsForm()
form.fields["invoices"].queryset = qs
context = {"form": form, "lottery": lottery} context = {
"lottery": lottery,
"invoices": qs.order_by("-created_at"),
"invoice_count": qs.count(),
"request": request,
}
return render(request, "admin/add_participants.html", context) return render(request, "admin/add_participants.html", context)
def get_client_by_invoice(invoice): def get_client_by_invoice(invoice):
"""
Возвращает клиента, используя значение поля client_club_card_number у счета.
Предполагается, что у модели Invoice есть поле client_club_card_number.
"""
try: try:
return Client.objects.get(club_card_number=invoice.client_club_card_number) return Client.objects.get(club_card_number=invoice.client_club_card_number)
except Client.DoesNotExist: except Client.DoesNotExist:
return None return None
def start_draw(request, lottery_id): 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("..")
notifier = NotificationService(bot=create_bot_instance()) 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(): 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)
try: try:
draw_result = lottery.draw_results.get(prize=prize) draw_result = lottery.draw_results.get(prize=prize)
draw_result.participant = prize.winner draw_result.participant = prize.winner
@@ -326,28 +137,26 @@ def start_draw(request, lottery_id):
try: try:
draw_result = lottery.draw_results.get(prize=prize) draw_result = lottery.draw_results.get(prize=prize)
if draw_result.confirmed: if draw_result.confirmed:
logger.info("Приз '%s' уже подтвержден.", prize.prize_place)
continue continue
except DrawResult.DoesNotExist: except DrawResult.DoesNotExist:
draw_result = None draw_result = None
participants = list(lottery.participants.filter(used=False)) participants = list(
logger.info("Найдено свободных участников для приза '%s': %d", prize.prize_place, len(participants)) lottery.participants.filter(used=False).exclude(invoice_id__in=manually_assigned_invoice_ids)
)
logger.info(f"Найдено участников: {len(participants)} для приза {prize.prize_place}")
if not participants: if not participants:
logger.warning("Нет свободных участников для приза '%s'.", prize.prize_place)
continue continue
winner_participant = random.choice(participants) winner_participant = random.choice(participants)
winner_participant.used = True winner_participant.used = True
winner_participant.save() winner_participant.save()
logger.info("Выбран участник с счетом '%s' для приза '%s'.", winner_participant.invoice.api_id, prize.prize_place)
if draw_result: if draw_result:
draw_result.participant = winner_participant draw_result.participant = winner_participant
draw_result.drawn_at = timezone.now() draw_result.drawn_at = timezone.now()
draw_result.confirmed = False draw_result.confirmed = False
draw_result.save() draw_result.save()
logger.info("Обновлен результат розыгрыша для приза '%s'.", prize.prize_place)
else: else:
DrawResult.objects.create( DrawResult.objects.create(
lottery=lottery, lottery=lottery,
@@ -356,12 +165,31 @@ def start_draw(request, lottery_id):
confirmed=False, confirmed=False,
drawn_at=timezone.now() drawn_at=timezone.now()
) )
logger.info("Создан результат розыгрыша для приза '%s'.", prize.prize_place)
draw_results = lottery.draw_results.all() draw_results = lottery.draw_results.select_related('prize', 'participant__invoice')
async_to_sync(notifier.notify_draw_results)(lottery, draw_results)
# 🧠 Считаем сумму призов и формируем список победителей
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(): if not lottery.prizes.filter(winner__isnull=True).exists():
lottery.finished = True lottery.finished = True
lottery.save() lottery.save()
@@ -369,11 +197,10 @@ def start_draw(request, lottery_id):
return render(request, "admin/draw_result.html", {"lottery": lottery, "draw_results": draw_results}) return render(request, "admin/draw_result.html", {"lottery": lottery, "draw_results": draw_results})
def confirm_draw_result(request, result_id): def confirm_draw_result(request, result_id):
from django.http import HttpResponseRedirect
result = get_object_or_404(DrawResult, id=result_id) result = get_object_or_404(DrawResult, id=result_id)
# Проверяем, что для результата задан участник и его счет
if not result.participant or not result.participant.invoice: if not result.participant or not result.participant.invoice:
messages.error(request, "Невозможно подтвердить результат: отсутствует участник или его счет.") messages.error(request, "Невозможно подтвердить результат: отсутствует участник или его счет.")
return HttpResponseRedirect(reverse("admin:start_draw", args=[result.lottery.id])) return HttpResponseRedirect(reverse("admin:start_draw", args=[result.lottery.id]))
@@ -385,21 +212,15 @@ def confirm_draw_result(request, result_id):
prize.winner = result.participant prize.winner = result.participant
prize.save() prize.save()
logger.info("Подтвержден результат розыгрыша для приза '%s'.", prize.prize_place)
messages.success(request, f"Результат для приза '{prize.prize_place}' подтвержден.")
# Получаем клиента по счету участника
client = get_client_by_invoice(result.participant.invoice) client = get_client_by_invoice(result.participant.invoice)
# Если уведомление вызывает ошибки, можно временно его отключить,
# чтобы проверить базовую функциональность подтверждения.
if client: if client:
try: try:
# Можно попробовать запуск уведомления в отдельном потоке или отключить его временно:
async_to_sync(NotificationService(bot=create_bot_instance()).notify_prize_status_update)(client, result) async_to_sync(NotificationService(bot=create_bot_instance()).notify_prize_status_update)(client, result)
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:start_draw", args=[result.lottery.id])) messages.success(request, f"Результат для приза '{prize.prize_place}' подтвержден.")
return HttpResponseRedirect(reverse("admin:view_draw_results", args=[result.lottery.id]))
@admin.register(Lottery) @admin.register(Lottery)
@@ -412,15 +233,16 @@ 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
def start_draw_button(self, obj): def start_draw_button(self, obj):
# Если лотерея завершена, кнопку скрываем
if obj.finished: if obj.finished:
return "" return ""
url = reverse("admin:start_draw", args=[obj.pk]) url = reverse("admin:start_draw", args=[obj.pk])
return format_html('<a class="btn btn-primary" href="{}">Запуск розыгрыша</a>', url) return format_html('<a class="btn btn-primary" href="{}">Запуск розыгрыша</a>', url)
start_draw_button.short_description = "Розыгрыш" start_draw_button.short_description = "Розыгрыш"
@@ -432,7 +254,6 @@ class PrizeAdmin(admin.ModelAdmin):
def get_form(self, request, obj=None, **kwargs): def get_form(self, request, obj=None, **kwargs):
form = super().get_form(request, obj, **kwargs) form = super().get_form(request, obj, **kwargs)
# При создании нового приза поле winner скрываем, так как участников ещё нет
if obj is None: if obj is None:
form.base_fields.pop('winner', None) form.base_fields.pop('winner', None)
return form return form
@@ -442,7 +263,6 @@ class PrizeAdmin(admin.ModelAdmin):
obj_id = request.resolver_match.kwargs.get('object_id') obj_id = request.resolver_match.kwargs.get('object_id')
try: try:
prize_obj = self.model.objects.get(pk=obj_id) prize_obj = self.model.objects.get(pk=obj_id)
# Ограничиваем выбор участниками данной лотереи
kwargs["queryset"] = prize_obj.lottery.participants.all() kwargs["queryset"] = prize_obj.lottery.participants.all()
except self.model.DoesNotExist: except self.model.DoesNotExist:
kwargs["queryset"] = self.model.objects.none() kwargs["queryset"] = self.model.objects.none()
@@ -458,13 +278,11 @@ class LotteryParticipantAdmin(admin.ModelAdmin):
def changelist_view(self, request, extra_context=None): def changelist_view(self, request, extra_context=None):
extra_context = extra_context or {} extra_context = extra_context or {}
from draw.models import Lottery
active_lotteries = Lottery.objects.filter(prizes__winner__isnull=True).distinct() active_lotteries = Lottery.objects.filter(prizes__winner__isnull=True).distinct()
extra_context['active_lotteries'] = active_lotteries extra_context['active_lotteries'] = active_lotteries
return super().changelist_view(request, extra_context=extra_context) return super().changelist_view(request, extra_context=extra_context)
def get_urls(self): def get_urls(self):
from django.urls import path
urls = super().get_urls() urls = super().get_urls()
custom_urls = [ custom_urls = [
path('add-participants/', self.admin_site.admin_view(add_participants_view), name="add_participants"), 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_min = forms.DecimalField(label="Минимальный депозит", required=False)
deposit_max = forms.DecimalField(label="Максимальный депозит", required=False) deposit_max = forms.DecimalField(label="Максимальный депозит", required=False)
invoices = forms.ModelMultipleChoiceField( invoices = forms.ModelMultipleChoiceField(
queryset=Invoice.objects.none(), queryset=Invoice.objects.none(), # устанавливается в представлении
widget=forms.CheckboxSelectMultiple, widget=forms.CheckboxSelectMultiple,
required=False, required=False,
label="Доступные счета" label="Доступные счета"

View File

@@ -1,3 +1,12 @@
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

@@ -0,0 +1,8 @@
import os
from celery import Celery
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "lottery.settings")
app = Celery("lottery")
app.config_from_object("django.conf:settings", namespace="CELERY")
app.autodiscover_tasks()

View File

@@ -108,6 +108,11 @@ else: # по умолчанию SQLite
} }
} }
CELERY_BROKER_URL = 'redis://redis:6379/0'
CELERY_RESULT_BACKEND = 'redis://redis:6379/0'
CELERY_ACCEPT_CONTENT = ['json']
CELERY_TASK_SERIALIZER = 'json'
# Password validation # Password validation
# https://docs.djangoproject.com/en/5.1/ref/settings/#auth-password-validators # https://docs.djangoproject.com/en/5.1/ref/settings/#auth-password-validators
@@ -152,6 +157,11 @@ STATIC_ROOT = os.path.join(BASE_DIR, 'staticfiles')
DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField' DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'
MEDIA_URL = "/media/"
MEDIA_ROOT = os.path.join(BASE_DIR, "media")
JAZZMIN_SETTINGS = { JAZZMIN_SETTINGS = {
# Общие настройки # Общие настройки
"site_title": "LuckyTicket", "site_title": "LuckyTicket",
@@ -187,6 +197,8 @@ JAZZMIN_SETTINGS = {
"icons": { "icons": {
"bot" : "fa-solid fa-robot", "bot" : "fa-solid fa-robot",
"bot.BotConfig" : "fa-solid fa-wrench", "bot.BotConfig" : "fa-solid fa-wrench",
"bot.BotMessage" : "fas fa-envelope",
"bot.BotEventMessageConfig" : "fa-solid fa-screwdriver-wrench",
"bot.WelcomeMessage" : "fa-solid fa-message", "bot.WelcomeMessage" : "fa-solid fa-message",
"auth": "fas fa-user", "auth": "fas fa-user",
"auth.user": "fas fa-user", "auth.user": "fas fa-user",

View File

@@ -17,9 +17,13 @@ Including another URLconf
from django.contrib import admin from django.contrib import admin
from django.urls import path, include from django.urls import path, include
from django.shortcuts import redirect from django.shortcuts import redirect
from django.conf import settings
from django.conf.urls.static import static
urlpatterns = [ urlpatterns = [
path('', lambda request: redirect('admin:index')), path('', lambda request: redirect('admin:index')),
path('admin/', admin.site.urls), path('admin/', admin.site.urls),
path('invoices/', include('webapp.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

@@ -17,4 +17,11 @@ 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
celery
redis
docker
django-celery-beat
django-celery-results
django-redis
flower
mysqlclient mysqlclient

Binary file not shown.

After

Width:  |  Height:  |  Size: 120 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 80 KiB

View File

@@ -23,62 +23,90 @@
{% endblock extrahead %} {% endblock extrahead %}
{% block content %} {% block content %}
<div class="container-fluid"> <div class="container-fluid custom-container">
<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="form-group mr-3"> <div class="row">
<label for="id_deposit_min" class="mr-2">Минимальный депозит:</label> <div class="col-md-3">
<input type="number" step="0.01" name="deposit_min" id="id_deposit_min" class="form-control"> <label>Минимальная сумма депозита:</label>
<input type="number" step="0.01" name="deposit_min" value="{{ request.GET.deposit_min }}" class="form-control">
</div> </div>
<div class="form-group mr-3"> <div class="col-md-3">
<label for="id_deposit_max" class="mr-2">Максимальный депозит:</label> <label>Максимальная сумма депозита:</label>
<input type="number" step="0.01" name="deposit_max" id="id_deposit_max" class="form-control"> <input type="number" step="0.01" name="deposit_max" value="{{ request.GET.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="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> </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 %}
<div class="table-responsive"> <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> <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 invoices %}
<tr> <tr>
<td> <td><input type="checkbox" name="invoices" value="{{ invoice.id }}"></td>
<input type="checkbox" name="invoices" value="{{ invoice.id }}" /> <td>{{ invoice.created_at|date:"Y-m-d H:i" }}</td>
</td> <td>{% if invoice.closed_at %}{{ invoice.closed_at|date:"Y-m-d H:i" }}{% else %}—{% endif %}</td>
<td>{{ invoice.ext_id }}</td> <td>{{ invoice.ext_id|default:"—" }}</td>
<td> <td>{{ invoice.client.name|default:"Не указан" }}</td>
{% if invoice.client_name %} <td>{{ invoice.client.club_card_number|default:"—" }}</td>
{{ invoice.client_name }} <td>{{ invoice.sum|floatformat:2|default:"—" }}</td>
{% else %} <td>{{ invoice.bonus|floatformat:2|default:"—" }}</td>
Не указан <td>{{ invoice.start_bonus|floatformat:2|default:"—" }}</td>
{% endif %} <td>{{ invoice.deposit_sum|floatformat:2|default:"—" }}</td>
</td> <td>{{ invoice.notes|default:"—" }}</td>
<td>{{ invoice.deposit_sum }}</td>
</tr> </tr>
{% empty %} {% empty %}
<tr> <tr><td colspan="11" class="text-center">Нет доступных счетов</td></tr>
<td colspan="5" class="text-center">Нет доступных счетов</td>
</tr>
{% endfor %} {% endfor %}
</tbody> </tbody>
</table> </table>
</div> </div>
<button type="submit" class="btn btn-primary">Добавить выбранные счета</button> <button type="submit" class="btn btn-success">Добавить выбранные счета</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>

View File

@@ -0,0 +1,38 @@
{% extends "admin/change_form.html" %}
{% load static %}
{% block object-tools %}
{{ block.super }}
{% if original %}
<div class="container-fluid">
<a class="btn btn-warning d-block w-100 text-center my-2"
href="{% url 'admin:botconfig-restart' original.pk %}">
Перезапустить бота
</a>
</div>
{% endif %}
{% endblock %}
{% block extrahead %}
{{ block.super }}
<style>
.container-fluid {
margin-top: 20px;
}
</style>
{% endblock %}
{% block footer %}
{{ block.super }}
<script>
document.addEventListener("DOMContentLoaded", function() {
const restartButton = document.querySelector('.btn-warning');
if (restartButton) {
restartButton.addEventListener('click', function(event) {
if (!confirm('Вы уверены, что хотите перезапустить бота?')) {
event.preventDefault();
}
});
}
});
</script>
{% endblock %}

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

@@ -0,0 +1,17 @@
# 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

@@ -1,6 +1,7 @@
from django.db import models from django.db import models
from django.core.validators import MinValueValidator from django.core.validators import MinValueValidator
from decimal import Decimal from decimal import Decimal
class Client(models.Model): class Client(models.Model):
name = models.CharField("Имя", max_length=255) name = models.CharField("Имя", max_length=255)
club_card_number = models.CharField("Номер клубной карты", max_length=100, unique=True) club_card_number = models.CharField("Номер клубной карты", max_length=100, unique=True)
@@ -57,10 +58,13 @@ 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,14 +95,19 @@ 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
# Используем update_or_create для обновления существующей записи defaults = {
'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)
@@ -112,6 +117,7 @@ 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()