Compare commits

...

17 Commits

280 changed files with 537 additions and 2111 deletions

1
lottery/.gitignore vendored
View File

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

View File

@@ -3,7 +3,7 @@ from django.urls import path
from django.shortcuts import redirect from django.shortcuts import redirect
from django.contrib import messages from django.contrib import messages
from .models import BotConfig from .models import BotConfig
from .models import BotConfig, WelcomeMessage from .models import BotConfig, WelcomeMessage, BotMessage, BotEventMessageConfig
from .tasks import restart_bot_container from .tasks import restart_bot_container
@admin.register(BotConfig) @admin.register(BotConfig)
@@ -36,3 +36,11 @@ class BotConfigAdmin(admin.ModelAdmin):
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

@@ -93,7 +93,7 @@ async def handle_client_card(update: Update, context: ContextTypes.DEFAULT_TYPE)
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:
@@ -124,7 +124,7 @@ 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:
message_text += f" • ID {inv.id} (*{inv.sum}*)\n" message_text += f" • ID {inv.ext_id} (*{inv.sum}*)\n"
else: else:
message_text += " _Нет счетов_\n" message_text += " _Нет счетов_\n"

View File

@@ -0,0 +1,33 @@
# Generated by Django 5.1.6 on 2025-08-03 05:29
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('bot', '0007_botconfig_active_welcome_botconfig_is_active_and_more'),
]
operations = [
migrations.CreateModel(
name='BotMessage',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=100, verbose_name='Название сообщения')),
('text', models.TextField(blank=True, verbose_name='Текст')),
('image', models.ImageField(blank=True, null=True, upload_to='bot_messages/', verbose_name='Картинка')),
('buttons_json', models.JSONField(blank=True, null=True, verbose_name='Кнопки (JSON-формат)')),
],
),
migrations.CreateModel(
name='BotEventMessageConfig',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('event', models.CharField(choices=[('welcome', 'Приветствие'), ('draw_started', 'Розыгрыш начат'), ('draw_finished', 'Розыгрыш завершен'), ('winner_announced', 'Объявление победителя')], max_length=50, unique=True, verbose_name='Событие')),
('enabled', models.BooleanField(default=True, verbose_name='Активно')),
('message', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='event_configs', to='bot.botmessage')),
],
),
]

View File

@@ -0,0 +1,26 @@
# Generated by Django 5.1.6 on 2025-08-03 05:46
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('bot', '0008_botmessage_boteventmessageconfig'),
]
operations = [
migrations.AlterModelOptions(
name='boteventmessageconfig',
options={'verbose_name': 'Событие сообщения', 'verbose_name_plural': 'События сообщений'},
),
migrations.AlterModelOptions(
name='botmessage',
options={'verbose_name': 'Сообщение', 'verbose_name_plural': 'Сообщения'},
),
migrations.AlterField(
model_name='boteventmessageconfig',
name='event',
field=models.CharField(choices=[('welcome', 'Приветствие'), ('draw_started', 'Розыгрыш начат'), ('draw_finished', 'Розыгрыш завершен'), ('winner_announced', 'Объявление победителя'), ('guest_binding', 'Подтверждение гостя')], max_length=50, unique=True, verbose_name='Событие'),
),
]

View File

@@ -0,0 +1,18 @@
# Generated by Django 5.1.6 on 2025-08-03 11:39
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('bot', '0009_alter_boteventmessageconfig_options_and_more'),
]
operations = [
migrations.AlterField(
model_name='welcomemessage',
name='welcome_image',
field=models.ImageField(blank=True, help_text='Загрузите изображение для приветствия', null=True, upload_to='welcome_messages/', verbose_name='Приветственное изображение'),
),
]

View File

@@ -1,6 +1,7 @@
from django.db import models 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 канала/чата, куда бот будет отправлять сообщения")
@@ -44,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,
@@ -98,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}")

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

@@ -1,6 +0,0 @@
[mariadb-client]
port=3306
socket=/run/mysqld/mysqld.sock
user=healthcheck
password=9hOHOe^WXq`lsXwRba/@]gB3"|[mOFe{

Binary file not shown.

Binary file not shown.

View File

@@ -1,269 +0,0 @@
59,5
59,4
59,3
59,2
59,1
59,0
56,4
56,3
56,2
56,1
56,0
55,4
55,3
55,2
55,1
55,0
53,4
53,3
53,2
53,1
53,0
52,5
52,4
52,3
52,2
52,1
52,0
51,5
51,4
51,3
51,2
51,1
51,0
50,6
50,5
50,4
50,3
50,2
50,1
50,0
37,3
37,2
37,1
37,0
36,4
36,3
36,2
36,1
36,0
35,4
35,3
35,2
35,1
35,0
34,3
34,2
34,1
34,0
32,4
32,3
32,2
32,1
32,0
28,4
28,3
28,2
28,1
28,0
27,4
27,3
27,2
27,1
27,0
26,5
26,4
26,3
26,2
26,1
26,0
25,5
25,4
25,3
25,2
25,1
25,0
22,5
22,4
22,3
22,2
22,1
22,0
20,5
20,4
20,3
20,2
20,1
20,0
18,5
18,4
18,3
18,2
18,1
18,0
11,4
11,3
11,2
11,1
11,0
8,3
8,2
8,1
8,0
7,3
5,5
5,4
5,3
5,2
5,0
4,3
3,2
2,2
1,2
0,9
0,2
1,45
3,44
2,44
1,44
3,43
2,43
1,43
3,42
2,42
1,42
3,41
2,41
1,41
3,40
2,40
1,40
3,39
2,39
1,39
3,38
2,38
1,38
3,37
2,37
1,37
3,36
2,36
1,36
3,35
2,35
1,35
3,34
2,34
1,34
3,33
2,33
1,33
3,32
2,32
1,32
3,31
2,31
1,31
3,30
2,30
1,30
3,29
2,29
1,29
3,28
2,28
1,28
3,27
2,27
1,27
3,26
2,26
1,26
3,25
2,25
1,25
3,24
2,24
1,24
3,23
2,23
1,23
3,22
2,22
1,22
3,21
2,21
1,21
3,20
2,20
1,20
3,19
2,19
1,19
3,18
2,18
1,18
3,17
2,17
1,17
3,16
2,16
1,16
3,15
2,15
1,15
3,14
2,14
1,14
3,13
2,13
1,13
3,12
2,12
1,12
3,11
2,11
1,11
3,10
2,10
1,10
3,9
2,9
1,9
3,8
2,8
1,8
3,7
2,7
1,7
3,6
2,6
1,6
3,5
2,5
1,5
3,4
2,4
1,4
3,3
3,0
2,3
2,0
1,3
1,0
0,6
0,0
0,47
0,46
0,49
0,48
0,45
0,12
0,10
0,8
0,11
0,5
0,7
0,4
0,3

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@@ -1,2 +0,0 @@
default-character-set=utf8mb4
default-collation=utf8mb4_uca1400_ai_ci

View File

@@ -1 +0,0 @@
11.6.2-MariaDB

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@@ -1,2 +0,0 @@
default-character-set=utf8mb4
default-collation=utf8mb4_uca1400_ai_ci

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Some files were not shown because too many files have changed in this diff Show More