Compare commits
5 Commits
20f67ed96c
...
dev
| Author | SHA1 | Date | |
|---|---|---|---|
| 842710fe5c | |||
| 1d659ea5ee | |||
| 7acdcc7465 | |||
| 37b0507c07 | |||
| 421bebb770 |
1
lottery/.gitignore
vendored
1
lottery/.gitignore
vendored
@@ -12,3 +12,4 @@ var/mysql/
|
|||||||
.db
|
.db
|
||||||
.png
|
.png
|
||||||
|
|
||||||
|
db_data/
|
||||||
|
|||||||
2
lottery/__init__.py
Normal file
2
lottery/__init__.py
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
from .lottery.celery import app as celery_app
|
||||||
|
__all__ = ("celery_app",)
|
||||||
@@ -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 = {
|
||||||
try:
|
"X-API-Key": self.api_key
|
||||||
response = requests.get(api_url, headers=headers)
|
}
|
||||||
response.raise_for_status()
|
|
||||||
return response.json()
|
def fetch_clients(self):
|
||||||
except requests.exceptions.RequestException as e:
|
"""Получение списка клиентов"""
|
||||||
print(f"Ошибка при запросе к API: {e}")
|
try:
|
||||||
return None
|
url = f"{self.base_url}/clients"
|
||||||
|
response = requests.get(url, headers=self.headers)
|
||||||
|
response.raise_for_status()
|
||||||
|
return response.json()
|
||||||
|
except requests.exceptions.RequestException as 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
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
api_endpoint = os.getenv("API_URL")
|
client = ApiClient()
|
||||||
api_key = os.getenv("API_KEY")
|
|
||||||
|
print("=== Клиенты ===")
|
||||||
print(api_endpoint, api_key)
|
clients = client.fetch_clients()
|
||||||
if not api_endpoint or not api_key:
|
if clients:
|
||||||
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))
|
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("Не удалось получить счета")
|
||||||
|
|||||||
@@ -1,11 +1,36 @@
|
|||||||
from django.contrib import admin
|
from django.contrib import admin
|
||||||
|
from django.urls import path
|
||||||
|
from django.shortcuts import redirect
|
||||||
|
from django.contrib import messages
|
||||||
|
from .models import BotConfig
|
||||||
from .models import BotConfig, WelcomeMessage
|
from .models import BotConfig, WelcomeMessage
|
||||||
|
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):
|
||||||
|
|||||||
@@ -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:
|
||||||
@@ -45,7 +48,7 @@ async def whoami(update: Update, context: ContextTypes.DEFAULT_TYPE):
|
|||||||
|
|
||||||
invoices = await get_invoices_for_client(client)
|
invoices = await get_invoices_for_client(client)
|
||||||
draws = await get_draws_for_client(client)
|
draws = await get_draws_for_client(client)
|
||||||
|
|
||||||
message_text = f"👤 *Профиль клиента:*\n" \
|
message_text = f"👤 *Профиль клиента:*\n" \
|
||||||
f"• *Имя:* {client.name}\n" \
|
f"• *Имя:* {client.name}\n" \
|
||||||
f"• *Клубная карта:* {client.club_card_number}\n\n"
|
f"• *Клубная карта:* {client.club_card_number}\n\n"
|
||||||
@@ -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("Введите номер вашей клубной карты (КК):")
|
||||||
|
|
||||||
|
|
||||||
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.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("Привет! Бот работает.")
|
||||||
|
|||||||
@@ -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='Приветственное изображение'),
|
||||||
|
),
|
||||||
|
]
|
||||||
@@ -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='Варианты приветствия'),
|
||||||
|
),
|
||||||
|
]
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
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,67 +16,79 @@ 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 = "Настройки Ботов"
|
||||||
|
|
||||||
|
|
||||||
class WelcomeMessage(models.Model):
|
class WelcomeMessage(models.Model):
|
||||||
bot = models.ForeignKey(BotConfig, on_delete=models.CASCADE, verbose_name="Бот")
|
bot = models.ForeignKey(BotConfig, on_delete=models.CASCADE, verbose_name="Бот")
|
||||||
welcome_message = models.TextField(
|
welcome_message = models.TextField(
|
||||||
verbose_name="Приветственное сообщение",
|
verbose_name="Приветственное сообщение",
|
||||||
blank=True,
|
blank=True,
|
||||||
null=True,
|
null=True,
|
||||||
help_text="Текст, который будет отправлен при запуске команды /start"
|
help_text="Текст, который будет отправлен при запуске команды /start"
|
||||||
)
|
)
|
||||||
welcome_image = models.ImageField(
|
welcome_image = models.ImageField(
|
||||||
upload_to='static/upload_image/',
|
upload_to='static/upload_image/',
|
||||||
verbose_name="Приветственное изображение",
|
verbose_name="Приветственное изображение",
|
||||||
blank=True,
|
blank=True,
|
||||||
null=True,
|
null=True,
|
||||||
help_text="Загрузите изображение для приветствия"
|
help_text="Загрузите изображение для приветствия"
|
||||||
)
|
)
|
||||||
admin_contact = models.CharField(
|
admin_contact = models.CharField(
|
||||||
max_length=255,
|
max_length=255,
|
||||||
verbose_name="Контакт администратора",
|
verbose_name="Контакт администратора",
|
||||||
blank=True,
|
blank=True,
|
||||||
null=True,
|
null=True,
|
||||||
help_text="Ссылка или контакт для связи с администратором"
|
help_text="Ссылка или контакт для связи с администратором"
|
||||||
)
|
)
|
||||||
channel_link = models.URLField(
|
channel_link = models.URLField(
|
||||||
verbose_name="Ссылка на канал",
|
verbose_name="Ссылка на канал",
|
||||||
blank=True,
|
blank=True,
|
||||||
null=True,
|
null=True,
|
||||||
help_text="URL канала бота"
|
help_text="URL канала бота"
|
||||||
)
|
)
|
||||||
group_link = models.URLField(
|
group_link = models.URLField(
|
||||||
verbose_name="Ссылка на группу",
|
verbose_name="Ссылка на группу",
|
||||||
blank=True,
|
blank=True,
|
||||||
null=True,
|
null=True,
|
||||||
help_text="URL группы бота"
|
help_text="URL группы бота"
|
||||||
)
|
)
|
||||||
custom_link1_name = models.CharField(
|
custom_link1_name = models.CharField(
|
||||||
max_length=100,
|
max_length=100,
|
||||||
verbose_name="Название кастомной ссылки 1",
|
verbose_name="Название кастомной ссылки 1",
|
||||||
blank=True,
|
blank=True,
|
||||||
null=True
|
null=True
|
||||||
)
|
)
|
||||||
custom_link1_url = models.URLField(
|
custom_link1_url = models.URLField(
|
||||||
verbose_name="URL кастомной ссылки 1",
|
verbose_name="URL кастомной ссылки 1",
|
||||||
blank=True,
|
blank=True,
|
||||||
null=True
|
null=True
|
||||||
)
|
)
|
||||||
custom_link2_name = models.CharField(
|
custom_link2_name = models.CharField(
|
||||||
max_length=100,
|
max_length=100,
|
||||||
verbose_name="Название кастомной ссылки 2",
|
verbose_name="Название кастомной ссылки 2",
|
||||||
blank=True,
|
blank=True,
|
||||||
null=True
|
null=True
|
||||||
)
|
)
|
||||||
custom_link2_url = models.URLField(
|
custom_link2_url = models.URLField(
|
||||||
verbose_name="URL кастомной ссылки 2",
|
verbose_name="URL кастомной ссылки 2",
|
||||||
blank=True,
|
blank=True,
|
||||||
null=True
|
null=True
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -84,4 +97,4 @@ class WelcomeMessage(models.Model):
|
|||||||
verbose_name_plural = "Приветственные сообщения"
|
verbose_name_plural = "Приветственные сообщения"
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return f"Приветствие для {self.bot}"
|
return f"Приветствие для {self.bot}"
|
||||||
|
|||||||
21
lottery/bot/tasks.py
Normal file
21
lottery/bot/tasks.py
Normal 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"
|
||||||
@@ -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:
|
||||||
|
|
||||||
|
|||||||
23
lottery/docker/celery.Dockerfile
Normal file
23
lottery/docker/celery.Dockerfile
Normal 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"]
|
||||||
16
lottery/docker/flower.Dockerfile
Normal file
16
lottery/docker/flower.Dockerfile
Normal 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"]
|
||||||
14
lottery/docker/flower.requirements.txt
Normal file
14
lottery/docker/flower.requirements.txt
Normal 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
|
||||||
@@ -1,220 +1,3 @@
|
|||||||
# 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
|
||||||
@@ -229,6 +12,10 @@ from .forms import AddParticipantsForm
|
|||||||
from webapp.models import Invoice, Client, BindingRequest
|
from webapp.models import Invoice, Client, BindingRequest
|
||||||
from bot.notifications import NotificationService
|
from bot.notifications import NotificationService
|
||||||
from bot.utils import create_bot_instance
|
from bot.utils import create_bot_instance
|
||||||
|
from .views import view_draw_results
|
||||||
|
from django import forms
|
||||||
|
from django.utils.dateparse import parse_datetime, parse_date
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
# Настройка логгера
|
# Настройка логгера
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
@@ -253,11 +40,30 @@ def add_participants_view(request):
|
|||||||
|
|
||||||
deposit_min = request.GET.get("deposit_min")
|
deposit_min = request.GET.get("deposit_min")
|
||||||
deposit_max = request.GET.get("deposit_max")
|
deposit_max = request.GET.get("deposit_max")
|
||||||
|
created_after = request.GET.get("created_after")
|
||||||
|
created_before = request.GET.get("created_before")
|
||||||
|
|
||||||
if deposit_min:
|
if deposit_min:
|
||||||
qs = qs.filter(deposit_sum__gte=deposit_min)
|
qs = qs.filter(deposit_sum__gte=deposit_min)
|
||||||
if deposit_max:
|
if deposit_max:
|
||||||
qs = qs.filter(deposit_sum__lte=deposit_max)
|
qs = qs.filter(deposit_sum__lte=deposit_max)
|
||||||
|
|
||||||
|
if created_after:
|
||||||
|
try:
|
||||||
|
dt = parse_date(created_after)
|
||||||
|
if dt:
|
||||||
|
qs = qs.filter(created_at__gte=dt)
|
||||||
|
except ValueError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
if created_before:
|
||||||
|
try:
|
||||||
|
dt = parse_date(created_before)
|
||||||
|
if dt:
|
||||||
|
qs = qs.filter(created_at__lte=dt)
|
||||||
|
except ValueError:
|
||||||
|
pass
|
||||||
|
|
||||||
if request.method == "POST":
|
if request.method == "POST":
|
||||||
form = AddParticipantsForm(request.POST)
|
form = AddParticipantsForm(request.POST)
|
||||||
form.fields["invoices"].queryset = qs
|
form.fields["invoices"].queryset = qs
|
||||||
@@ -293,7 +99,6 @@ 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("..")
|
||||||
@@ -301,10 +106,19 @@ def start_draw(request, lottery_id):
|
|||||||
notifier = NotificationService(bot=create_bot_instance())
|
notifier = NotificationService(bot=create_bot_instance())
|
||||||
async_to_sync(notifier.notify_draw_start)(lottery)
|
async_to_sync(notifier.notify_draw_start)(lottery)
|
||||||
|
|
||||||
|
# Собираем ID счетов вручную назначенных победителей
|
||||||
|
manually_assigned_invoice_ids = set()
|
||||||
|
for prize in lottery.prizes.all():
|
||||||
|
if prize.winner and prize.winner.invoice:
|
||||||
|
manually_assigned_invoice_ids.add(prize.winner.invoice_id)
|
||||||
|
# Помечаем вручную выбранного участника как использованного
|
||||||
|
prize.winner.used = True
|
||||||
|
prize.winner.save()
|
||||||
|
|
||||||
for prize in lottery.prizes.all():
|
for prize in lottery.prizes.all():
|
||||||
logger.info("Обработка приза: %s", prize.prize_place)
|
logger.info("Обработка приза: %s", prize.prize_place)
|
||||||
|
|
||||||
# Если для приза уже назначен победитель вручную, сохраняем его в таблице результатов
|
# Если у приза уже установлен победитель вручную — сохраняем его в таблицу результатов
|
||||||
if prize.winner:
|
if prize.winner:
|
||||||
logger.info("Приз '%s' имеет установленного вручную победителя. Сохраняем в таблице результатов.", prize.prize_place)
|
logger.info("Приз '%s' имеет установленного вручную победителя. Сохраняем в таблице результатов.", prize.prize_place)
|
||||||
try:
|
try:
|
||||||
@@ -331,7 +145,10 @@ def start_draw(request, lottery_id):
|
|||||||
except DrawResult.DoesNotExist:
|
except DrawResult.DoesNotExist:
|
||||||
draw_result = None
|
draw_result = None
|
||||||
|
|
||||||
participants = list(lottery.participants.filter(used=False))
|
# Получаем всех неиспользованных участников, исключая вручную выбранных
|
||||||
|
participants = list(
|
||||||
|
lottery.participants.filter(used=False).exclude(invoice_id__in=manually_assigned_invoice_ids)
|
||||||
|
)
|
||||||
logger.info("Найдено свободных участников для приза '%s': %d", prize.prize_place, len(participants))
|
logger.info("Найдено свободных участников для приза '%s': %d", prize.prize_place, len(participants))
|
||||||
if not participants:
|
if not participants:
|
||||||
logger.warning("Нет свободных участников для приза '%s'.", prize.prize_place)
|
logger.warning("Нет свободных участников для приза '%s'.", prize.prize_place)
|
||||||
@@ -361,7 +178,6 @@ def start_draw(request, lottery_id):
|
|||||||
draw_results = lottery.draw_results.all()
|
draw_results = lottery.draw_results.all()
|
||||||
async_to_sync(notifier.notify_draw_results)(lottery, draw_results)
|
async_to_sync(notifier.notify_draw_results)(lottery, draw_results)
|
||||||
|
|
||||||
# Если все призы розыгрыша подтверждены, устанавливаем флаг завершения лотереи
|
|
||||||
if not lottery.prizes.filter(winner__isnull=True).exists():
|
if not lottery.prizes.filter(winner__isnull=True).exists():
|
||||||
lottery.finished = True
|
lottery.finished = True
|
||||||
lottery.save()
|
lottery.save()
|
||||||
@@ -390,8 +206,6 @@ def confirm_draw_result(request, result_id):
|
|||||||
|
|
||||||
# Получаем клиента по счету участника
|
# Получаем клиента по счету участника
|
||||||
client = get_client_by_invoice(result.participant.invoice)
|
client = get_client_by_invoice(result.participant.invoice)
|
||||||
# Если уведомление вызывает ошибки, можно временно его отключить,
|
|
||||||
# чтобы проверить базовую функциональность подтверждения.
|
|
||||||
if client:
|
if client:
|
||||||
try:
|
try:
|
||||||
# Можно попробовать запуск уведомления в отдельном потоке или отключить его временно:
|
# Можно попробовать запуск уведомления в отдельном потоке или отключить его временно:
|
||||||
@@ -399,7 +213,7 @@ def confirm_draw_result(request, result_id):
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Ошибка отправки уведомления о статусе приза пользователю {client.telegram_id}: {e}")
|
logger.error(f"Ошибка отправки уведомления о статусе приза пользователю {client.telegram_id}: {e}")
|
||||||
|
|
||||||
return HttpResponseRedirect(reverse("admin:start_draw", args=[result.lottery.id]))
|
return HttpResponseRedirect(reverse("admin:view_draw_results", args=[result.lottery.id]))
|
||||||
|
|
||||||
|
|
||||||
@admin.register(Lottery)
|
@admin.register(Lottery)
|
||||||
@@ -412,6 +226,8 @@ 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
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
})
|
||||||
8
lottery/lottery/celery.py
Normal file
8
lottery/lottery/celery.py
Normal 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()
|
||||||
@@ -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
|
||||||
|
|||||||
@@ -17,4 +17,10 @@ sniffio==1.3.1
|
|||||||
sqlparse==0.5.3
|
sqlparse==0.5.3
|
||||||
typing_extensions==4.12.2
|
typing_extensions==4.12.2
|
||||||
urllib3==2.3.0
|
urllib3==2.3.0
|
||||||
mysqlclient
|
celery
|
||||||
|
redis
|
||||||
|
docker
|
||||||
|
django-celery-beat
|
||||||
|
django-celery-results
|
||||||
|
django-redis
|
||||||
|
flower
|
||||||
BIN
lottery/static/upload_image/photo_2025-06-24_17-53-21.jpg
Normal file
BIN
lottery/static/upload_image/photo_2025-06-24_17-53-21.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 120 KiB |
BIN
lottery/static/upload_image/photo_2025-07-10_09-33-33.jpg
Normal file
BIN
lottery/static/upload_image/photo_2025-07-10_09-33-33.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 80 KiB |
@@ -14,7 +14,7 @@
|
|||||||
const selectAllCheckbox = document.getElementById("select-all");
|
const selectAllCheckbox = document.getElementById("select-all");
|
||||||
if (selectAllCheckbox) {
|
if (selectAllCheckbox) {
|
||||||
selectAllCheckbox.addEventListener("click", function(){
|
selectAllCheckbox.addEventListener("click", function(){
|
||||||
const checkboxes = document.querySelectorAll("input[name='invoices']");
|
const checkboxes = document.querySelectorAll("input[name='invoices[]']");
|
||||||
checkboxes.forEach(chk => chk.checked = selectAllCheckbox.checked);
|
checkboxes.forEach(chk => chk.checked = selectAllCheckbox.checked);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -23,64 +23,92 @@
|
|||||||
{% 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="form-inline mb-3">
|
<form method="get" class="mb-4">
|
||||||
<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 class="col-md-3">
|
||||||
|
<label>Максимальная сумма депозита:</label>
|
||||||
|
<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>
|
||||||
<div class="form-group mr-3">
|
<div class="mt-3">
|
||||||
<label for="id_deposit_max" class="mr-2">Максимальный депозит:</label>
|
<button type="submit" class="btn btn-primary">Применить фильтр</button>
|
||||||
<input type="number" step="0.01" name="deposit_max" id="id_deposit_max" class="form-control">
|
|
||||||
</div>
|
</div>
|
||||||
<button type="submit" class="btn btn-info">Фильтровать</button>
|
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
<!-- Форма добавления участников -->
|
<p><strong>Найдено подходящих счетов: {{ invoice_count }}</strong></p>
|
||||||
<form method="post">
|
|
||||||
{% csrf_token %}
|
<!-- Форма добавления участников -->
|
||||||
<div class="table-responsive">
|
<form method="post">
|
||||||
<table class="table table-striped table-bordered table-sm">
|
{% csrf_token %}
|
||||||
<thead>
|
<div class="table-responsive">
|
||||||
|
<table class="table table-striped table-bordered table-sm">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<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>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{% for invoice in form.fields.invoices.queryset %}
|
||||||
<tr>
|
<tr>
|
||||||
<th><input type="checkbox" id="select-all" /></th>
|
<input type="checkbox" name="invoices" value="{{ invoice.id }}">
|
||||||
<th>Счет</th>
|
<td>{{ invoice.created_at|date:"d.m.Y H:i" }}</td>
|
||||||
<th>Владелец счета</th>
|
<td>
|
||||||
<th>Депозит</th>
|
{% if invoice.closed_at %}
|
||||||
|
{{ invoice.closed_at|date:"d.m.Y H:i" }}
|
||||||
|
{% else %}
|
||||||
|
—
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
|
<td>{{ invoice.ext_id|default:"—" }}</td>
|
||||||
|
<td>{{ invoice.client.name|default:"Не указан" }}</td>
|
||||||
|
<td>{{ invoice.client.club_card_number|default:"—" }}</td>
|
||||||
|
<td>{{ invoice.sum|default:"—" }}</td>
|
||||||
|
<td>{{ invoice.bonus|default:"—" }}</td>
|
||||||
|
<td>{{ invoice.start_bonus|default:"—" }}</td>
|
||||||
|
<td>{{ invoice.deposit_sum|default:"—" }}</td>
|
||||||
|
<td>{{ invoice.notes|default:"—" }}</td>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
{% empty %}
|
||||||
<tbody>
|
<tr>
|
||||||
{% for invoice in form.fields.invoices.queryset %}
|
<td colspan="11" class="text-center">Нет доступных счетов</td>
|
||||||
<tr>
|
</tr>
|
||||||
<td>
|
{% endfor %}
|
||||||
<input type="checkbox" name="invoices" value="{{ invoice.id }}" />
|
</tbody>
|
||||||
</td>
|
</table>
|
||||||
<td>{{ invoice.ext_id }}</td>
|
</div>
|
||||||
<td>
|
<button type="submit" class="btn btn-primary">Добавить выбранные счета</button>
|
||||||
{% if invoice.client_name %}
|
</form>
|
||||||
{{ invoice.client_name }}
|
|
||||||
{% else %}
|
|
||||||
Не указан
|
|
||||||
{% endif %}
|
|
||||||
</td>
|
|
||||||
<td>{{ invoice.deposit_sum }}</td>
|
|
||||||
</tr>
|
|
||||||
{% empty %}
|
|
||||||
<tr>
|
|
||||||
<td colspan="5" class="text-center">Нет доступных счетов</td>
|
|
||||||
</tr>
|
|
||||||
{% endfor %}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
<button type="submit" class="btn btn-primary">Добавить выбранные счета</button>
|
|
||||||
</form>
|
|
||||||
|
|
||||||
<a href="{% url 'admin:draw_lotteryparticipant_changelist' %}" class="btn btn-secondary mt-3">Вернуться к списку участников</a>
|
<a href="{% url 'admin:draw_lotteryparticipant_changelist' %}" class="btn btn-secondary mt-3">Вернуться к списку участников</a>
|
||||||
</div>
|
</div>
|
||||||
{% endblock content %}
|
{% endblock content %}
|
||||||
|
|||||||
14
lottery/templates/admin/botconfig_change_form.html
Normal file
14
lottery/templates/admin/botconfig_change_form.html
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
{% extends "admin/change_form.html" %}
|
||||||
|
{% load static %}
|
||||||
|
|
||||||
|
{% block object-tools %}
|
||||||
|
{{ block.super }}
|
||||||
|
{% if original %}
|
||||||
|
<li>
|
||||||
|
<a class="button"
|
||||||
|
href="{% url 'admin:botconfig-restart' original.pk %}">
|
||||||
|
🔁 Перезапустить бота
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
{% endif %}
|
||||||
|
{% endblock %}
|
||||||
@@ -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='Основная информация'
|
||||||
17
lottery/webapp/migrations/0009_alter_apisettings_options.py
Normal file
17
lottery/webapp/migrations/0009_alter_apisettings_options.py
Normal 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'},
|
||||||
|
),
|
||||||
|
]
|
||||||
@@ -57,10 +57,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', 'Ожидает проверки'),
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
Reference in New Issue
Block a user