init commit

This commit is contained in:
2025-06-13 21:10:20 +09:00
commit d52c611afb
269 changed files with 37162 additions and 0 deletions

View File

89
lottery/webapp/admin.py Normal file
View File

@@ -0,0 +1,89 @@
from django.contrib import admin, messages
from django.urls import path, reverse
from django.shortcuts import redirect
from .models import Client, Invoice, BindingRequest, APISettings
from .services import API_SYNC
from django.utils import timezone
@admin.register(Client)
class ClientAdmin(admin.ModelAdmin):
list_display = ('name', 'club_card_number', 'telegram_id')
change_list_template = "admin/clients_change_list.html"
def get_urls(self):
urls = super().get_urls()
custom_urls = [
path('sync-clients/', self.admin_site.admin_view(self.sync_clients), name='sync_clients'),
]
return custom_urls + urls
def sync_clients(self, request):
syncer = API_SYNC()
new_count = syncer.sync_clients()
self.message_user(request, f"Синхронизировано. Добавлено новых клиентов: {new_count}", level=messages.INFO)
return redirect("..")
@admin.register(Invoice)
class InvoiceAdmin(admin.ModelAdmin):
list_display = ('api_id', 'client_name', 'sum', 'created_at', 'closed_at')
change_list_template = "admin/invoices_change_list.html"
def get_urls(self):
urls = super().get_urls()
custom_urls = [
path('sync-invoices/', self.admin_site.admin_view(self.sync_invoices), name='sync_invoices'),
]
return custom_urls + urls
def sync_invoices(self, request):
syncer = API_SYNC()
count = syncer.sync_invoices()
self.message_user(request, f"Синхронизировано. Обновлено/создано записей: {count}", level=messages.INFO)
return redirect("..")
@admin.action(description="Подтвердить выбранные заявки и обновить Telegram ID клиента")
def approve_binding_requests(modeladmin, request, queryset):
count = 0
for binding_request in queryset:
if binding_request.status != 'approved':
binding_request.status = 'approved'
binding_request.processed_at = timezone.now()
binding_request.save()
# Если у заявки связан клиент, обновляем его Telegram ID
if binding_request.client:
client = binding_request.client
client.telegram_id = binding_request.telegram_chat_id
client.save()
count += 1
modeladmin.message_user(request, f"Подтверждено {count} заявок", level=messages.INFO)
@admin.register(BindingRequest)
class BindingRequestAdmin(admin.ModelAdmin):
list_display = ('client_card', 'client', 'telegram_chat_id', 'status', 'created_at')
actions = [approve_binding_requests]
change_list_template = "admin/bindingrequests_change_list.html"
def get_urls(self):
urls = super().get_urls()
custom_urls = [
# Пример кастомного URL для синхронизации заявок через API, если требуется
path('sync-requests/', self.admin_site.admin_view(self.sync_requests), name='sync_requests'),
]
return custom_urls + urls
def sync_requests(self, request):
syncer = API_SYNC()
count = syncer.sync_binding_requests()
self.message_user(request, f"Синхронизировано. Обновлено/создано записей: {count}", level=messages.INFO)
return redirect("..")
def changelist_view(self, request, extra_context=None):
extra_context = extra_context or {}
# Например, можно добавить количество заявок в контекст:
extra_context['total_requests'] = BindingRequest.objects.count()
return super().changelist_view(request, extra_context=extra_context)
@admin.register(APISettings)
class APISettingsAdmin(admin.ModelAdmin):
list_display = ('api_url', 'api_key')

7
lottery/webapp/apps.py Normal file
View File

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

View File

@@ -0,0 +1,31 @@
# Generated by Django 5.1.6 on 2025-03-03 12:53
from django.db import migrations, models
class Migration(migrations.Migration):
initial = True
dependencies = [
]
operations = [
migrations.CreateModel(
name='APISettings',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('api_url', models.URLField(verbose_name='API URL')),
('api_key', models.CharField(max_length=255, verbose_name='API KEY')),
],
),
migrations.CreateModel(
name='Client',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=255, verbose_name='Имя')),
('club_card_number', models.CharField(max_length=100, unique=True, verbose_name='Номер клубной карты')),
('telegram_id', models.CharField(blank=True, max_length=50, null=True, verbose_name='Telegram ID')),
],
),
]

View File

@@ -0,0 +1,42 @@
# Generated by Django 5.1.6 on 2025-03-06 10:01
import django.core.validators
from decimal import Decimal
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('webapp', '0001_initial'),
]
operations = [
migrations.CreateModel(
name='Invoice',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('api_id', models.CharField(max_length=255, unique=True, verbose_name='API Invoice ID')),
('invoice_type', models.CharField(default='Invoice', max_length=50, verbose_name='Type')),
('created_at', models.DateTimeField(verbose_name='Created at')),
('closed_at', models.DateTimeField(blank=True, null=True, verbose_name='Closed at')),
('ext_id', models.CharField(blank=True, max_length=255, null=True, verbose_name='External ID')),
('ext_type', models.CharField(blank=True, max_length=255, null=True, verbose_name='External Type')),
('client_api_id', models.CharField(blank=True, max_length=255, null=True, verbose_name='Client API ID')),
('client_type', models.CharField(blank=True, max_length=50, null=True, verbose_name='Client Type')),
('client_name', models.CharField(blank=True, max_length=255, null=True, verbose_name='Client Name')),
('client_club_card_number', models.CharField(blank=True, max_length=100, null=True, verbose_name='Club Card Number')),
('sum', models.DecimalField(decimal_places=2, default=Decimal('0.00'), max_digits=10, validators=[django.core.validators.MinValueValidator(Decimal('0.00'))], verbose_name='Sum')),
('company_number', models.PositiveIntegerField(default=0, verbose_name='Company Number')),
('bonus', models.DecimalField(blank=True, decimal_places=2, max_digits=10, null=True, validators=[django.core.validators.MinValueValidator(Decimal('0.00'))], verbose_name='Bonus')),
('start_bonus', models.DecimalField(blank=True, decimal_places=2, max_digits=10, null=True, validators=[django.core.validators.MinValueValidator(Decimal('0.00'))], verbose_name='Start Bonus')),
('deposit_sum', models.DecimalField(decimal_places=2, default=Decimal('0.00'), max_digits=10, validators=[django.core.validators.MinValueValidator(Decimal('0.00'))], verbose_name='Deposit Sum')),
('notes', models.TextField(blank=True, null=True, verbose_name='Notes')),
],
options={
'verbose_name': 'Invoice',
'verbose_name_plural': 'Invoices',
'ordering': ['-created_at'],
},
),
]

View File

@@ -0,0 +1,21 @@
# Generated by Django 5.1.6 on 2025-03-06 10:52
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('webapp', '0002_invoice'),
]
operations = [
migrations.AlterModelOptions(
name='client',
options={'verbose_name': 'Клиент', 'verbose_name_plural': 'Клиенты'},
),
migrations.AlterModelOptions(
name='invoice',
options={'ordering': ['-created_at'], 'verbose_name': 'Счет', 'verbose_name_plural': 'Счета'},
),
]

View File

@@ -0,0 +1,18 @@
# Generated by Django 5.1.6 on 2025-03-21 03:44
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('webapp', '0003_alter_client_options_alter_invoice_options'),
]
operations = [
migrations.AddField(
model_name='client',
name='bot_admin',
field=models.BooleanField(default=False, help_text='Является ли пользователь администратором бота'),
),
]

View File

@@ -0,0 +1,28 @@
# Generated by Django 5.1.6 on 2025-03-21 03:48
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('webapp', '0004_client_bot_admin'),
]
operations = [
migrations.CreateModel(
name='BindingRequest',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('telegram_chat_id', models.CharField(help_text='Идентификатор чата Telegram пользователя', max_length=50)),
('client_card', models.CharField(help_text='Номер клиентской карты', max_length=100)),
('status', models.CharField(choices=[('pending', 'Ожидает проверки'), ('approved', 'Подтверждён'), ('rejected', 'Отклонён')], default='pending', max_length=20)),
('created_at', models.DateTimeField(auto_now_add=True)),
('processed_at', models.DateTimeField(blank=True, null=True)),
],
options={
'verbose_name': 'Запрос на сопоставление',
'verbose_name_plural': 'Запросы на сопоставления',
},
),
]

View File

@@ -0,0 +1,19 @@
# Generated by Django 5.1.6 on 2025-03-21 04:15
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('webapp', '0005_bindingrequest'),
]
operations = [
migrations.AddField(
model_name='bindingrequest',
name='client',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='webapp.client'),
),
]

View File

@@ -0,0 +1,18 @@
# Generated by Django 5.1.6 on 2025-03-21 12:49
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('webapp', '0006_bindingrequest_client'),
]
operations = [
migrations.AddField(
model_name='client',
name='chat_disabled',
field=models.BooleanField(default=False, help_text='Если установлено, пользователь не может отправлять сообщения в чат.', verbose_name='Блокировка отправки сообщений'),
),
]

View File

@@ -0,0 +1,18 @@
# Generated by Django 5.1.6 on 2025-03-22 22:22
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('webapp', '0007_client_chat_disabled'),
]
operations = [
migrations.AddField(
model_name='invoice',
name='used',
field=models.BooleanField(default=False, verbose_name='Использован в розыгрыше'),
),
]

View File

83
lottery/webapp/models.py Normal file
View File

@@ -0,0 +1,83 @@
from django.db import models
from django.core.validators import MinValueValidator
from decimal import Decimal
class Client(models.Model):
name = models.CharField("Имя", max_length=255)
club_card_number = models.CharField("Номер клубной карты", max_length=100, unique=True)
telegram_id = models.CharField("Telegram ID", max_length=50, blank=True, null=True)
bot_admin = models.BooleanField(default=False, help_text="Является ли пользователь администратором бота")
chat_disabled = models.BooleanField(
default=False,
verbose_name="Блокировка отправки сообщений",
help_text="Если установлено, пользователь не может отправлять сообщения в чат."
)
def __str__(self):
return f"{self.name} ({self.club_card_number})"
class Meta:
verbose_name = "Клиент"
verbose_name_plural = "Клиенты"
class Invoice(models.Model):
# Поля, полученные из API
api_id = models.CharField("API Invoice ID", max_length=255, unique=True)
invoice_type = models.CharField("Type", max_length=50, default="Invoice")
created_at = models.DateTimeField("Created at")
closed_at = models.DateTimeField("Closed at", null=True, blank=True)
ext_id = models.CharField("External ID", max_length=255, blank=True, null=True)
ext_type = models.CharField("External Type", max_length=255, blank=True, null=True)
# Данные клиента, полученные из объекта "client"
client_api_id = models.CharField("Client API ID", max_length=255, blank=True, null=True)
client_type = models.CharField("Client Type", max_length=50, blank=True, null=True)
client_name = models.CharField("Client Name", max_length=255, blank=True, null=True)
client_club_card_number = models.CharField("Club Card Number", max_length=100, blank=True, null=True)
# Финансовые поля
sum = models.DecimalField("Sum", max_digits=10, decimal_places=2, default=Decimal("0.00"),
validators=[MinValueValidator(Decimal("0.00"))])
company_number = models.PositiveIntegerField("Company Number", default=0)
bonus = models.DecimalField("Bonus", max_digits=10, decimal_places=2, null=True, blank=True,
validators=[MinValueValidator(Decimal("0.00"))])
start_bonus = models.DecimalField("Start Bonus", max_digits=10, decimal_places=2, null=True, blank=True,
validators=[MinValueValidator(Decimal("0.00"))])
deposit_sum = models.DecimalField("Deposit Sum", max_digits=10, decimal_places=2, default=Decimal("0.00"),
validators=[MinValueValidator(Decimal("0.00"))])
notes = models.TextField("Notes", blank=True, null=True)
used = models.BooleanField(default=False, verbose_name="Использован в розыгрыше")
def __str__(self):
return f"{self.ext_id} / {self.client_name or 'Unknown Client'} ({self.client_club_card_number})"
class Meta:
verbose_name = "Счет"
verbose_name_plural = "Счета"
ordering = ['-created_at']
class APISettings(models.Model):
api_url = models.URLField("API URL")
api_key = models.CharField("API KEY", max_length=255)
def __str__(self):
return f"Настройки API: {self.api_url}"
class BindingRequest(models.Model):
STATUS_CHOICES = (
('pending', 'Ожидает проверки'),
('approved', 'Подтверждён'),
('rejected', 'Отклонён'),
)
telegram_chat_id = models.CharField(max_length=50, help_text="Идентификатор чата Telegram пользователя")
client_card = models.CharField(max_length=100, help_text="Номер клиентской карты")
client = models.ForeignKey(Client, on_delete=models.SET_NULL, null=True, blank=True)
status = models.CharField(max_length=20, choices=STATUS_CHOICES, default='pending')
created_at = models.DateTimeField(auto_now_add=True)
processed_at = models.DateTimeField(null=True, blank=True)
def __str__(self):
return f"КК: {self.client_card} | Статус: {self.get_status_display()}"
class Meta:
verbose_name = "Запрос на сопоставление"
verbose_name_plural = "Запросы на сопоставления"

184
lottery/webapp/services.py Normal file
View File

@@ -0,0 +1,184 @@
import json
import logging
from django.core.exceptions import ImproperlyConfigured
from .models import APISettings, Client, Invoice
import requests
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)
class APIClient:
"""
Класс для подключения к API и получения данных.
Данные API_URL и API_KEY берутся из модели APISettings.
"""
def __init__(self):
self.settings = APISettings.objects.first()
if not self.settings:
logger.error("Настройки API не сконфигурированы. Заполните модель APISettings.")
raise ImproperlyConfigured("Настройки API не сконфигурированы.")
self.api_url = self.settings.api_url.rstrip("/")
self.api_key = self.settings.api_key
self.headers = {"X-API-Key": self.api_key}
logger.debug(f"APIClient инициализирован с базовым URL: {self.api_url}")
def get_clients(self):
url = f"{self.api_url}/api/clients"
logger.debug(f"Запрос клиентов по адресу: {url} с заголовками: {self.headers}")
try:
response = requests.get(url, headers=self.headers, timeout=10)
response.raise_for_status()
data = response.json()
logger.debug("Запрос клиентов успешно выполнен. Данные получены.")
return data
except requests.exceptions.RequestException as e:
logger.error(f"Ошибка при запросе клиентов: {e}")
return None
def get_invoices(self):
url = f"{self.api_url}/api/invoices"
logger.debug(f"Запрос счетов по адресу: {url} с заголовками: {self.headers}")
try:
response = requests.get(url, headers=self.headers, timeout=10)
response.raise_for_status()
data = response.json()
logger.debug("Запрос счетов успешно выполнен. Данные получены.")
return data
except requests.exceptions.RequestException as e:
logger.error(f"Ошибка при запросе счетов: {e}")
return None
class API_SYNC:
"""
Класс для импорта и обновления данных из API.
Методы sync_clients и sync_invoices обновляют существующие записи или создают новые.
"""
def __init__(self):
self.logger = logger
def sync_clients(self):
api_client = APIClient()
data = api_client.get_clients()
new_or_updated = 0
if isinstance(data, dict):
clients_list = data.get("member", [])
self.logger.debug(f"Извлечен список клиентов из ключа 'member', найдено записей: {len(clients_list)}")
elif isinstance(data, list):
clients_list = data
self.logger.debug(f"Получен список клиентов (list), количество записей: {len(clients_list)}")
else:
self.logger.error("Неожиданный формат данных клиентов от API: %s", type(data))
return new_or_updated
for index, item in enumerate(clients_list, start=1):
self.logger.debug(f"Обработка записи клиента {index}: {item}")
if isinstance(item, str):
try:
item = json.loads(item)
self.logger.debug(f"Запись клиента {index} успешно преобразована из строки в словарь.")
except Exception as e:
self.logger.error("Невозможно преобразовать запись клиента %s в словарь: %s; ошибка: %s", index, item, e)
continue
if isinstance(item, dict):
club_card_number = item.get("club_card_num")
if not club_card_number:
self.logger.warning("Запись клиента %s пропущена: отсутствует club_card_num. Запись: %s", index, item)
continue
# Используем update_or_create для обновления существующей записи
obj, created = Client.objects.update_or_create(
club_card_number=club_card_number,
defaults={
'name': item.get("name"),
'telegram_id': item.get("telegram_id"),
}
)
new_or_updated += 1
if created:
self.logger.info("Запись клиента %s создана: club_card_num %s.", index, club_card_number)
else:
self.logger.info("Запись клиента %s обновлена: club_card_num %s.", index, club_card_number)
else:
self.logger.error("Запись клиента %s имеет неожиданный тип: %s. Значение: %s", index, type(item), item)
return new_or_updated
def sync_invoices(self):
api_client = APIClient()
data = api_client.get_invoices()
new_or_updated = 0
if isinstance(data, dict):
invoices_list = data.get("member", [])
self.logger.debug(f"Извлечен список счетов из ключа 'member', найдено записей: {len(invoices_list)}")
elif isinstance(data, list):
invoices_list = data
self.logger.debug(f"Получен список счетов (list), количество записей: {len(invoices_list)}")
else:
self.logger.error("Неожиданный формат данных счетов от API: %s", type(data))
return new_or_updated
for index, invoice in enumerate(invoices_list, start=1):
self.logger.debug(f"Обработка счета {index}: {invoice}")
if isinstance(invoice, str):
try:
invoice = json.loads(invoice)
self.logger.debug(f"Счет {index} успешно преобразован из строки в словарь.")
except Exception as e:
self.logger.error("Невозможно преобразовать счет %s в словарь: %s; ошибка: %s", index, invoice, e)
continue
if not isinstance(invoice, dict):
self.logger.error("Счет %s имеет неожиданный тип: %s", index, type(invoice))
continue
api_id = invoice.get("@id")
if not api_id:
self.logger.warning("Счет %s пропущен: отсутствует '@id'.", index)
continue
# Извлекаем данные клиента из вложенного объекта
client_data = invoice.get("client", {})
client_name = client_data.get("name")
client_club_card_number = client_data.get("club_card_num")
# Приводим даты (при необходимости, можно использовать парсинг)
created_at = invoice.get("created_at")
closed_at = invoice.get("closed_at")
if closed_at in [None, "N/A"]:
closed_at = None
defaults = {
'invoice_type': invoice.get("@type"),
'created_at': created_at,
'closed_at': closed_at,
'ext_id': invoice.get("ext_id"),
'ext_type': invoice.get("ext_type"),
'client_name': client_name,
'client_club_card_number': client_club_card_number,
'sum': invoice.get("sum"),
'company_number': invoice.get("comp_num"),
'bonus': invoice.get("bonus"),
'start_bonus': invoice.get("start_bonus"),
'deposit_sum': invoice.get("deposit_sum"),
'notes': invoice.get("notes"),
}
obj, created = Invoice.objects.update_or_create(
api_id=api_id,
defaults=defaults
)
new_or_updated += 1
if created:
self.logger.info("Счет %s создан: api_id=%s", index, api_id)
else:
self.logger.info("Счет %s обновлен: api_id=%s", index, api_id)
return new_or_updated

3
lottery/webapp/tests.py Normal file
View File

@@ -0,0 +1,3 @@
from django.test import TestCase
# Create your tests here.

7
lottery/webapp/urls.py Normal file
View File

@@ -0,0 +1,7 @@
from django.urls import path
from . import views
urlpatterns = [
# Другие URL вашего приложения
path('filtered-invoices/', views.filtered_invoices, name='filtered_invoices'),
]

60
lottery/webapp/views.py Normal file
View File

@@ -0,0 +1,60 @@
from django.http import JsonResponse
from django.views.decorators.http import require_GET
from .models import Invoice
import datetime
@require_GET
def filtered_invoices(request):
"""
Фильтрует счета по начальной/конечной дате и имени клиента,
возвращает JSON с полем "member" как список счетов со всеми полями.
"""
start_date = request.GET.get('start_date')
end_date = request.GET.get('end_date')
client = request.GET.get('client')
qs = Invoice.objects.all()
if start_date:
try:
start_date_obj = datetime.datetime.strptime(start_date, "%Y-%m-%d")
qs = qs.filter(created_at__gte=start_date_obj)
except ValueError:
pass
if end_date:
try:
end_date_obj = datetime.datetime.strptime(end_date, "%Y-%m-%d")
qs = qs.filter(created_at__lte=end_date_obj)
except ValueError:
pass
if client:
qs = qs.filter(client_name__icontains=client)
invoices = []
for invoice in qs:
invoices.append({
'api_id': invoice.api_id, # Номер счета (API ID)
'invoice_type': invoice.invoice_type,
'created_at': invoice.created_at.strftime('%Y-%m-%d %H:%M:%S') if invoice.created_at else '',
'closed_at': invoice.closed_at.strftime('%Y-%m-%d %H:%M:%S') if invoice.closed_at else 'Не закрыт!',
'ext_id': invoice.ext_id,
'ext_type': invoice.ext_type,
'client': {
'name': invoice.client_name,
'club_card_num': invoice.client_club_card_number,
},
'sum': str(invoice.sum),
'company_number': invoice.company_number,
'bonus': str(invoice.bonus) if invoice.bonus is not None else '',
'start_bonus': str(invoice.start_bonus) if invoice.start_bonus is not None else '',
'deposit_sum': str(invoice.deposit_sum),
'notes': invoice.notes,
})
data = {
"member": invoices,
"totalItems": qs.count(),
}
return JsonResponse(data)