создание подтверждение qr кода (админка)

Внедрение PROMO QR
main
pilot 2 months ago
parent d3413ae3fa
commit cbd6b358fa

@ -1,12 +1,16 @@
import calendar
import json
import os
from pathlib import Path
import requests
from max_bot.models import Client
from restoran_max_bot.common import common_get_menu, common_get_data
from restoran_max_bot.settings import DEBUG
BASE_DIR = Path(__file__).resolve().parent.parent
CACERT_PATH = os.path.join(BASE_DIR, 'cacert.pem')
max_verify = CACERT_PATH
url_max = "https://platform-api2.max.ru"
max_verify = False
def maxbot_send_text_message(chat_id: str, max_token: str, message: str):

@ -644,6 +644,7 @@ class Qr(models.Model):
class PromoCode(models.Model):
objects = None
client = models.ForeignKey(Client, on_delete=models.CASCADE)
promo = models.CharField(max_length=50, unique=True)
chat_id = models.CharField(max_length=50)

@ -12,6 +12,7 @@ from restoran_max_bot.bot_message import bot_message, bot_promo
from restoran_max_bot.common import common_get_contact, common_check_registration
from restoran_max_bot.settings import DEBUG
from restoran_max_bot.utils import is_json, has_key
from django.shortcuts import render
def api_decorator(func):
@ -61,6 +62,7 @@ def api_decorator(func):
@csrf_exempt
@api_decorator
def api_start_max_v1(request, token, **kwargs):
# основной обработчик MAX-БОТ
data = json.loads(request.body.decode('utf-8'))
client = kwargs['client']
settings_max = kwargs['settings_max']
@ -137,58 +139,22 @@ def iiko_send_message(request: WSGIRequest, token):
return JsonResponse(rt, status=200)
def promo_status(request, promo_code, chat_id):
def promo_status(request, promo_code, chat_id, client_id):
try:
promo = PromoCode.objects.get(promo=promo_code, chat_id=chat_id)
promo = PromoCode.objects.get(
promo=promo_code,
chat_id=chat_id,
client_id=client_id
)
except PromoCode.DoesNotExist:
return HttpResponse("Промокод не найден для данного чата", status=404)
# Если промокод уже использован, просто показываем статус
if promo.used:
html = f"""
<html>
<body>
<h1>Промокод: {promo.promo}</h1>
<p>ID чата: {promo.chat_id}</p>
<p>Статус: <strong>Использован</strong></p>
<p>Время использования: {promo.used_at}</p>
</body>
</html>
"""
return HttpResponse(html)
# Если не использован, показываем кнопку с запросом PIN-кода
html = f"""
<html>
<body>
<h1>Промокод: {promo.promo}</h1>
<p>ID чата: {promo.chat_id}</p>
<p>Статус: <strong style="color:green;">Не использован</strong></p>
<button onclick="activate()">Активировать</button>
<script>
function activate() {{
var pin = prompt("Введите PIN-код администратора:");
if (pin) {{
fetch('/promo/activate/{promo_code}/{chat_id}', {{
method: 'POST',
headers: {{'Content-Type': 'application/json'}},
body: JSON.stringify({{pin: pin}})
}})
.then(res => res.json())
.then(data => {{
alert(data.message);
if (data.success) location.reload();
}});
}}
}}
</script>
</body>
</html>
"""
return HttpResponse(html)
return HttpResponse("Промокод не найден", status=404)
context = {'promo': promo}
return render(request, 'max_bot/promo_status.html', context)
@csrf_exempt
def promo_activate(request, promo_code, chat_id):
def promo_activate(request, promo_code, chat_id, client_id):
if request.method != 'POST':
return JsonResponse({'error': 'Method not allowed'}, status=405)
@ -202,7 +168,12 @@ def promo_activate(request, promo_code, chat_id):
return JsonResponse({'error': 'Неверный PIN-код'}, status=403)
try:
promo = PromoCode.objects.get(promo=promo_code, chat_id=chat_id, used=False)
promo = PromoCode.objects.get(
promo=promo_code,
chat_id=chat_id,
client_id=client_id,
used=False
)
except PromoCode.DoesNotExist:
return JsonResponse({'error': 'Промокод не найден или уже использован'}, status=404)
@ -210,13 +181,12 @@ def promo_activate(request, promo_code, chat_id):
promo.used_at = datetime.now()
promo.save()
# Отправляем уведомление клиенту
integration = Integration.objects.filter(client=promo.client, partner__slug='max_bot', status=True).first()
if integration:
maxbot_send_text_message(
chat_id=promo.chat_id,
max_token=integration.token,
message="🎉 Ваш подарок активирован! Спасибо, что посетили нас."
message="🎉 Ваш QR код активирован! Спасибо, что посетили нас!"
)
return JsonResponse({'success': True, 'message': 'Промокод активирован, клиент получил уведомление'})
return JsonResponse({'success': True, 'message': 'Промокод активирован, клиент получил уведомление'})

@ -3,11 +3,12 @@ import re
import qrcode
from ai_agent.api_common import get_ai_agent, call_ai_agent, get_contact_meta
from max_bot.max_api import *
from max_bot.models import PromoCode
from restoran_max_bot.bot_started import check_registration
from restoran_max_bot.common import *
from restoran_max_bot.iiko_api import customer_info, customer_create, customer_wallet
from restoran_max_bot.rkiper_api import rkiper_customer_info, rkiper_customer_create
from restoran_max_bot.settings import STATICFILES_DIRS
from restoran_max_bot.settings import STATICFILES_DIRS, BASE_URL
from restoran_max_bot.utils import has_key, is_json
@ -257,58 +258,70 @@ def bot_message(client: Client, message: dict, settings_max: dict, message_type)
return True
from max_bot.models import PromoCode
def bot_promo(client: Client, message: dict, settings_max: dict):
chat_id = message['chat_id']
promo = message['payload'].lower()
messenger = 'max-bot'
client_id = client.id
# Проверяем, существует ли промокод в системе (через common_get_data)
data = common_get_data(client=client, key=promo)
if not data:
# Если промокод не заведён ничего не отправляем (скрываем факт)
return True
# Создаём запись, используя пару (promo, chat_id) как уникальный ключ
# Пытаемся найти или создать запись в БД
promo_obj, created = PromoCode.objects.get_or_create(
promo=promo,
chat_id=chat_id,
client=client,
defaults={
'client': client,
'messenger': messenger,
'used': False,
}
)
if not created and promo_obj.used:
maxbot_send_text_message(
chat_id=chat_id,
max_token=settings_max['token'],
message="⚠️ Этот промокод уже был получен вами."
)
# Если запись уже существовала
if not created:
if promo_obj.used:
# Уже использован уведомляем
maxbot_send_text_message(
chat_id=chat_id,
max_token=settings_max['token'],
message="⚠️ Этот промокод уже был использован."
)
else:
# QR-код уже был отправлен ранее напоминаем
maxbot_send_text_message(
chat_id=chat_id,
max_token=settings_max['token'],
message=" QR-код уже был отправлен."
)
return True
# Формируем ссылку с обоими параметрами
base_url = "https://maxbot.telefon-ip.ru/promo/status/"
url_promo = f"{base_url}{promo}/{chat_id}"
# Генерируем QR-код
# Если запись создана впервые генерируем QR-код
base_url = f"{BASE_URL}/promo/status/"
url_promo = f"{base_url}{promo}/{chat_id}/{client_id}"
img = qrcode.make(url_promo)
file_name = f"{promo}-{chat_id}.png"
file_name = f"{promo}-{chat_id}-{client_id}.png"
img.save(f"{STATICFILES_DIRS[0]}/client_qr/{file_name}")
qr_url = f"https://maxbot.telefon-ip.ru/static/client_qr/{file_name}"
qr_url = f"{BASE_URL}/static/client_qr/{file_name}"
maxbot_send_img_message(
chat_id=chat_id,
max_token=settings_max['token'],
img=qr_url,
message='Ваш QR-код для получения подарка. Покажите его администратору.'
message='Ваш QR-код.'
)
# Отправка дополнительного контента (если есть)
data = common_get_data(client=client, key=promo)
if data:
for dt in data:
if dt.img:
img_url = f"https://cdn.telefon-ip.ru/{dt.img}?thumb=600"
maxbot_send_img_message(chat_id=chat_id, max_token=settings_max['token'], img=img_url, message=dt.descr)
if dt.title:
maxbot_send_text_message(chat_id=chat_id, max_token=settings_max['token'], message=dt.descr)
if dt.url:
maxbot_send_text_message(chat_id=chat_id, max_token=settings_max['token'], message=dt.url)
# Отправляем дополнительный контент, если есть
for dt in data:
if dt.img:
img_url = f"https://cdn.telefon-ip.ru/{dt.img}?thumb=600"
maxbot_send_img_message(chat_id=chat_id, max_token=settings_max['token'], img=img_url, message=dt.descr)
if dt.title:
maxbot_send_text_message(chat_id=chat_id, max_token=settings_max['token'], message=dt.descr)
if dt.url:
maxbot_send_text_message(chat_id=chat_id, max_token=settings_max['token'], message=dt.url)
return True

@ -21,6 +21,11 @@ if str(env('DEBUG')).lower() == 'false':
else:
DEBUG = True
if DEBUG:
BASE_URL = "https://home.telefon-ip.ru"
else:
BASE_URL = "https://maxbot.telefon-ip.ru"
ALLOWED_HOSTS = ['*']
@ -53,7 +58,7 @@ ROOT_URLCONF = 'restoran_max_bot.urls'
TEMPLATES = [
{
'BACKEND': 'django.template.backends.django.DjangoTemplates',
'DIRS': [],
'DIRS': [BASE_DIR / 'templates'],
'APP_DIRS': True,
'OPTIONS': {
'context_processors': [
@ -127,4 +132,4 @@ STATICFILES_DIRS = [(os.path.join(BASE_DIR, "static"))]
DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'
LOG_SYSTEM_PROMPT = False # False - не логировать системный промт
PROMO_ADMIN_PIN = os.environ.get('PROMO_ADMIN_PIN', '1111')
PROMO_ADMIN_PIN = os.environ.get('PROMO_ADMIN_PIN', '1111')

@ -7,6 +7,6 @@ urlpatterns = [
# path('admin/', admin.site.urls),
path('max/', include('max_bot.urls')),
path('content/', include('content_bot.urls')),
path('promo/status/<str:promo_code>/<str:chat_id>', promo_status),
path('promo/activate/<str:promo_code>/<str:chat_id>', promo_activate),
path('promo/status/<str:promo_code>/<str:chat_id>/<int:client_id>', promo_status),
path('promo/activate/<str:promo_code>/<str:chat_id>/<int:client_id>', promo_activate),
]

@ -0,0 +1,215 @@
<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
<title>Промокод</title>
<style>
* {
box-sizing: border-box;
margin: 0;
padding: 0;
}
body {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
background: #f5f5f5;
display: flex;
justify-content: center;
align-items: center;
min-height: 100vh;
margin: 0;
padding: 16px;
}
.card {
background: #ffffff;
border-radius: 16px;
box-shadow: 0 4px 12px rgba(0,0,0,0.1);
padding: 24px;
max-width: 400px;
width: 100%;
text-align: center;
}
.header {
display: flex;
align-items: center;
justify-content: center;
gap: 12px;
margin-bottom: 16px;
flex-wrap: wrap;
}
.logo {
max-height: 60px;
max-width: 120px;
object-fit: contain;
}
.brand-name {
font-size: 20px;
font-weight: 600;
color: #1a1a1a;
}
.promo-code {
font-size: 24px;
font-weight: 600;
color: #1a1a1a;
margin-bottom: 8px;
word-break: break-all;
}
.meta {
font-size: 14px;
color: #666;
margin-bottom: 16px;
}
.status {
font-size: 18px;
font-weight: 500;
margin: 16px 0;
}
.status.used {
color: #d32f2f;
}
.status.available {
color: #2e7d32;
}
.btn {
display: inline-block;
background: #1976d2;
color: white;
border: none;
padding: 14px 28px;
border-radius: 40px;
font-size: 16px;
font-weight: 500;
cursor: pointer;
transition: background 0.2s;
width: 100%;
margin-top: 12px;
touch-action: manipulation;
}
.btn:active {
background: #1565c0;
}
.btn:disabled {
background: #bdbdbd;
cursor: not-allowed;
}
.pin-input-container {
display: none;
margin-top: 16px;
}
.pin-input-container.active {
display: block;
}
.pin-input {
width: 100%;
padding: 12px;
border: 1px solid #ccc;
border-radius: 8px;
font-size: 16px;
text-align: center;
letter-spacing: 4px;
}
.footer {
margin-top: 20px;
font-size: 12px;
color: #999;
}
@media (max-width: 480px) {
.card {
padding: 16px;
}
.promo-code {
font-size: 20px;
}
.btn {
padding: 16px;
font-size: 18px;
}
.brand-name {
font-size: 17px;
}
}
</style>
</head>
<body>
<div class="card">
<!-- Логотип и название компании -->
<div class="header">
{% if promo.client.logo %}
{% if "http" in promo.client.logo %}
<img src="{{ promo.client.logo }}" alt="{{ promo.client.brand }}" class="logo">
{% else %}
<img src="https://cdn.telefon-ip.ru/{{ promo.client.logo }}" alt="{{ promo.client.brand }}" class="logo">
{% endif %}
{% endif %}
<span class="brand-name">{{ promo.client.brand|default:"Ресторан" }}</span>
</div>
<div class="promo-code">{{ promo.promo }}</div>
<div class="meta">
<span>Чат: {{ promo.chat_id }}</span>
<span style="margin-left: 12px;">Мессенджер: {{ promo.messenger }}</span>
</div>
<div class="status {% if promo.used %}used{% else %}available{% endif %}">
{% if promo.used %}
Использован
<div style="font-size:14px; font-weight:normal; margin-top:4px;">
Время: {{ promo.used_at|date:"d.m.Y H:i" }}
</div>
{% else %}
Не использован
{% endif %}
</div>
{% if not promo.used %}
<button class="btn" id="activateBtn">Активировать</button>
<div class="pin-input-container" id="pinContainer">
<input type="password" class="pin-input" id="pinInput" placeholder="Введите PIN" maxlength="6" inputmode="numeric">
<button class="btn" id="confirmPinBtn" style="background:#2e7d32;">Подтвердить</button>
</div>
{% endif %}
<div class="footer">* Активация доступна только администратору</div>
</div>
<script>
(function() {
const activateBtn = document.getElementById('activateBtn');
const pinContainer = document.getElementById('pinContainer');
const pinInput = document.getElementById('pinInput');
const confirmBtn = document.getElementById('confirmPinBtn');
if (activateBtn) {
activateBtn.addEventListener('click', function() {
pinContainer.classList.toggle('active');
if (pinContainer.classList.contains('active')) {
pinInput.focus();
}
});
}
if (confirmBtn) {
confirmBtn.addEventListener('click', function() {
const pin = pinInput.value.trim();
if (!pin) {
alert('Введите PIN-код');
return;
}
fetch('/promo/activate/{{ promo.promo }}/{{ promo.chat_id }}/{{ promo.client.id }}', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({pin: pin})
})
.then(res => res.json())
.then(data => {
alert(data.message);
if (data.success) {
location.reload();
}
})
.catch(() => alert('Ошибка сети'));
});
}
})();
</script>
</body>
</html>
Loading…
Cancel
Save