Тикеты заявок, «пробное занятие», доработка ветки вебинара
- Тикеты: add_lead возвращает id заявки; при передаче менеджеру клиент получает «🎫 Номер вашей заявки: №N», тот же номер — в карточке оператору и в письме. Один тикет на заход (сбрасывается при новом старте воронки/интенсива). - Кнопка «Записаться на пробное» → «Записаться на пробное занятие» (лейблы + распознавание). - Ветка вебинара: вход по callback-кнопке и по тексту «записаться на вебинар», текст выровнен по ТЗ, после регистрации состояние FREE_CHAT — основное меню остаётся доступным (не блокирует продающую ветку). Дата/время — в config. Тесты: test_baza_question_ticket, test_webinar_registration. pytest 36 passed. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -141,6 +141,25 @@ class VKBotApplication:
|
||||
tmpl_str = ", Carousel: Да" if template else ""
|
||||
logger.info(f"[MOCK SEND] to {peer_id}: {message} (Keyboard: {'Да' if keyboard else 'Нет'}{attachment_str}{tmpl_str})")
|
||||
|
||||
def _handle_message_event(self, event):
|
||||
"""Обработка callback-событий от inline-кнопок (MESSAGE_EVENT)."""
|
||||
user_id = event.obj.user_id
|
||||
payload = event.obj.payload
|
||||
event_id = event.obj.event_id
|
||||
cmd = payload.get("cmd") if isinstance(payload, dict) else None
|
||||
# Отвечаем VK, чтобы убрать «часики» у кнопки
|
||||
try:
|
||||
self.vk.messages.sendMessageEventAnswer(
|
||||
user_id=user_id,
|
||||
event_id=event_id,
|
||||
peer_id=user_id,
|
||||
event_data='{"type":"show_snackbar","text":"Обработка..."}'
|
||||
)
|
||||
except Exception:
|
||||
pass
|
||||
if cmd == config.WEBINAR_PAYLOAD:
|
||||
self._handle_webinar_register(user_id)
|
||||
|
||||
def _is_flooding(self, user_id: int) -> bool:
|
||||
"""True, если пользователь шлёт сообщения слишком часто (защита от флуда/DDoS)."""
|
||||
now = time.time()
|
||||
@@ -562,6 +581,11 @@ class VKBotApplication:
|
||||
self._baza_start_funnel(user_id)
|
||||
return
|
||||
|
||||
# 1.5 Регистрация на вебинар (текстовый триггер; основной вход — callback-кнопка из поста)
|
||||
if text_lower in ["записаться на вебинар", "вебинар", "хочу на вебинар", "регистрация на вебинар"]:
|
||||
self._handle_webinar_register(user_id)
|
||||
return
|
||||
|
||||
# 2. Летний интенсив CS2 (кнопка меню)
|
||||
if text_lower in ["летний интенсив cs2 🔥", "летний интенсив cs2", "летний интенсив"]:
|
||||
self._baza_start_intensive(user_id, via_keyword=False)
|
||||
@@ -577,7 +601,7 @@ class VKBotApplication:
|
||||
return
|
||||
|
||||
# 3.1 Кнопки под ценами
|
||||
if text_lower in ["записаться на пробное", "пробное", "пробное занятие"]:
|
||||
if text_lower in ["записаться на пробное занятие", "записаться на пробное", "пробное", "пробное занятие"]:
|
||||
db.add_metka(user_id, "interest_trial")
|
||||
self._baza_send_payment(user_id, "trial", "Пробное занятие")
|
||||
return
|
||||
@@ -730,6 +754,7 @@ class VKBotApplication:
|
||||
db.set_profile_field(user_id, "source", "Основное меню")
|
||||
db.add_metka(user_id, "source_main_menu")
|
||||
db.set_profile_field(user_id, "lead_recorded", False) # новый заход = новая возможная заявка
|
||||
db.set_profile_field(user_id, "ticket", None)
|
||||
p = db.get_profile(user_id)
|
||||
if any(p.get(f) for f in ("role", "age", "game", "level")):
|
||||
self.send_message(
|
||||
@@ -955,18 +980,21 @@ class VKBotApplication:
|
||||
self._baza_to_manager(user_id, reason="Нужен совет по формату",
|
||||
client_text="Менеджер скоро подключится к диалогу. Можете дополнить запрос здесь.")
|
||||
|
||||
def _baza_record_lead(self, user_id: int):
|
||||
"""Фиксирует заявку в журнале leads (для статистики) — один раз на заход воронки."""
|
||||
def _baza_record_lead(self, user_id: int) -> Optional[int]:
|
||||
"""Фиксирует заявку в журнале leads (один раз на заход). Возвращает номер тикета."""
|
||||
p = db.get_profile(user_id)
|
||||
if p.get("lead_recorded"):
|
||||
return
|
||||
return p.get("ticket")
|
||||
name = self._vk_name(user_id) or "—"
|
||||
service = p.get("tariff") or p.get("format") or p.get("game") or "—"
|
||||
try:
|
||||
db.add_lead(user_id, source="bot", name=name, phone="", service=service)
|
||||
ticket = db.add_lead(user_id, source="bot", name=name, phone="", service=service)
|
||||
db.set_profile_field(user_id, "lead_recorded", True)
|
||||
db.set_profile_field(user_id, "ticket", ticket)
|
||||
return ticket
|
||||
except Exception as e:
|
||||
logger.error(f"Не удалось записать лид в статистику: {e}")
|
||||
return None
|
||||
|
||||
def _baza_send_payment(self, user_id: int, link_key: str, tariff_label: str, campaign: str = "main"):
|
||||
"""Показывает ссылку на оплату/запись + кнопки «проблема с оплатой» / «вопрос менеджеру»."""
|
||||
@@ -1017,6 +1045,7 @@ class VKBotApplication:
|
||||
db.set_profile_field(user_id, "interest_summer", True)
|
||||
db.add_metka(user_id, "interest_summer_intensive")
|
||||
db.set_profile_field(user_id, "lead_recorded", False) # новый заход = новая возможная заявка
|
||||
db.set_profile_field(user_id, "ticket", None)
|
||||
db.update_user_state(user_id, "BAZA_INT_INFO")
|
||||
self.send_message(
|
||||
user_id,
|
||||
@@ -1158,6 +1187,8 @@ class VKBotApplication:
|
||||
p = db.get_profile(user_id)
|
||||
vk_name = self._vk_name(user_id) or "—"
|
||||
lines = ["🎯 Новый запрос из бота — Кибершкола БАЗА", ""]
|
||||
if p.get("ticket"):
|
||||
lines.append(f"🎫 Заявка №{p['ticket']}")
|
||||
if reason:
|
||||
lines.append(f"❗ Повод: {reason}")
|
||||
lines.append(f"👤 Клиент: {vk_name}")
|
||||
@@ -1195,7 +1226,7 @@ class VKBotApplication:
|
||||
db.add_metka(user_id, "manager_requested")
|
||||
from datetime import datetime
|
||||
db.set_profile_field(user_id, "manager_at", datetime.now().strftime("%Y-%m-%d %H:%M"))
|
||||
self._baza_record_lead(user_id)
|
||||
ticket = self._baza_record_lead(user_id)
|
||||
card = self._baza_manager_card(user_id, reason)
|
||||
self._notify_operators("💬 Запрос менеджеру\n\n" + card)
|
||||
try:
|
||||
@@ -1215,6 +1246,8 @@ class VKBotApplication:
|
||||
elif not self._within_work_hours():
|
||||
client_text += (f"\n\n🌙 Сейчас нерабочее время (работаем с {getattr(config, 'WORK_HOUR_START', 9)}:00 "
|
||||
f"до {getattr(config, 'WORK_HOUR_END', 21)}:00) — ответим утром.")
|
||||
if ticket:
|
||||
client_text += f"\n\n🎫 Номер вашей заявки: №{ticket} — сохраните его для связи."
|
||||
self.send_message(user_id, client_text, keyboard=get_operator_keyboard())
|
||||
|
||||
|
||||
@@ -1271,6 +1304,29 @@ class VKBotApplication:
|
||||
logger.error(f"Не удалось предзагрузить медиа: {e}")
|
||||
|
||||
|
||||
# ---------- Вебинар «Точка входа в киберспорт» ----------
|
||||
|
||||
def _handle_webinar_register(self, user_id: int):
|
||||
"""Регистрация на вебинар «Точка входа в киберспорт» (кнопка из поста/клипа)."""
|
||||
self._baza_started(user_id)
|
||||
already = "webinar_registered" in db.get_profile(user_id).get("metki", [])
|
||||
db.add_metka(user_id, "webinar_registered")
|
||||
db.set_profile_field(user_id, "source", "Вебинар: " + config.WEBINAR_TITLE)
|
||||
db.update_user_state(user_id, "FREE_CHAT") # остаётся в боте, основное меню доступно
|
||||
prefix = "Ты уже записан(а) на вебинар 🙌\n\n" if already else ""
|
||||
self.send_message(
|
||||
user_id,
|
||||
prefix +
|
||||
"Привет! 👋\n"
|
||||
f"Будем рады видеть тебя на вебинаре «{config.WEBINAR_TITLE}».\n\n"
|
||||
f"📅 {config.WEBINAR_DATE}\n"
|
||||
f"⏰ {config.WEBINAR_TIME}\n\n"
|
||||
"Ссылку на эфир пришлём прямо в этот чат. До встречи! 🎮\n\n"
|
||||
"А пока можешь посмотреть направления и цены — кнопки ниже 👇",
|
||||
keyboard=get_main_keyboard(is_admin=self._is_admin(user_id))
|
||||
)
|
||||
logger.info(f"Webinar registration: user {user_id} ({'repeat' if already else 'new'})")
|
||||
|
||||
def _handle_operator_chat(self, user_id: int, text: str, text_lower: str):
|
||||
"""Поведение бота в режиме активного диалога с живым человеком."""
|
||||
# Пользователь хочет выйти из диалога с оператором
|
||||
@@ -1644,8 +1700,13 @@ class VKBotApplication:
|
||||
try:
|
||||
self.handle_message(user_id, text, attachments)
|
||||
except Exception as e:
|
||||
# Ошибка обработки одного сообщения не должна останавливать бота
|
||||
# Ошибка обработки одного сообщения не должна ронять бота
|
||||
logger.error(f"Ошибка обработки сообщения от {user_id}: {e}")
|
||||
elif event.type == VkBotEventType.MESSAGE_EVENT:
|
||||
try:
|
||||
self._handle_message_event(event)
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка обработки MESSAGE_EVENT: {e}")
|
||||
except KeyboardInterrupt:
|
||||
logger.info("Остановка бота по запросу (Ctrl+C).")
|
||||
break
|
||||
|
||||
Reference in New Issue
Block a user