Тексты воронки по ТЗ + смена ссылок из админки

Тексты:
- «Сколько лет игроку?» (было «…будущему ученику?») во всех местах;
- сообщение для «Младше 12 лет» приведено дословно к ТЗ (§4);
- убраны отсебятины-преамбулы, которых нет в ТЗ (приветствие, старт воронки,
  цены «Что дальше?», интенсив «Хотите записаться?»); кнопка младше-12 «Вернуться назад».

Смена ссылок в админке:
- таблица bot_links (key, url) + методы get_link/get_all_links/set_link;
- _link(key): БД-переопределение → дефолт из config/.env; используется на шагах оплаты;
- раздел «Ссылки 🔗» в админ-панели: список текущих адресов + «✏️ Изменить ссылку»
  (выбор из 8 кнопок → ввод URL с валидацией http/https). UTM добавляются автоматически.

Тесты: добавлен test_admin_edit_link. pytest 31 passed.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Kenik
2026-07-23 19:45:47 +03:00
parent fac2e3808b
commit 261e5c2da0
5 changed files with 144 additions and 17 deletions
+79 -15
View File
@@ -247,7 +247,8 @@ class VKBotApplication:
"Выберите раздел на кнопках ниже:\n"
"• 📊 Статистика — заявки и диалоги\n"
"• 💬 Быстрые ответы — авто-ответы по ключевым словам\n"
"• 📣 Рассылка — сообщение подписчикам"
"• 📣 Рассылка — сообщение подписчикам\n"
"• 🔗 Ссылки — куда ведут кнопки записи/оплаты"
+ extra,
keyboard=get_admin_keyboard(is_super=is_super)
)
@@ -444,6 +445,14 @@ class VKBotApplication:
elif text_lower.startswith("рассылка"):
self._handle_admin_broadcast(user_id, text_clean)
return
elif text_lower in ["ссылки 🔗", "ссылки", "ссылка"]:
self._send_links_admin(user_id)
return
elif text_lower in ["✏️ изменить ссылку", "изменить ссылку"]:
items = [(k, config.LINK_LABELS.get(k, k)) for k in config.LINK_LABELS]
db.update_user_state(user_id, "ADMIN_LINK_PICK")
self.send_message(user_id, "✏️ Какую ссылку изменить? Выберите:", keyboard=get_pick_keyboard(items))
return
elif text_lower in ["быстрые ответы 💬", "быстрые ответы", "фак"]:
self._send_faq_admin(user_id)
return
@@ -663,10 +672,14 @@ class VKBotApplication:
result = f"{base}{sep}{utm}"
return f"{result}#{frag}" if sep_frag else result
def _link(self, key: str) -> str:
"""Актуальный URL кнопки: переопределение из БД (админка) или дефолт из config/.env."""
return db.get_link(key) or config.LINKS.get(key, config.LINKS["tariffs"])
def _baza_send_openlink(self, user_id: int, link_key: str, message: str, button: str,
campaign: str = "main"):
"""Отправляет сообщение с инлайн-кнопкой-ссылкой (без перевода на оплату/лид)."""
link = self._utm(config.LINKS.get(link_key, config.LINKS["tariffs"]), campaign)
link = self._utm(self._link(link_key), campaign)
kb = VkKeyboard(inline=True)
kb.add_openlink_button(button, link)
self.send_message(user_id, message, keyboard=kb.get_keyboard())
@@ -682,7 +695,7 @@ class VKBotApplication:
self._baza_started(user_id)
self.send_message(
user_id,
config.GREETING_BAZA + "\n\nВыберите действие на кнопках ниже 👇",
config.GREETING_BAZA,
keyboard=get_main_keyboard(is_admin=self._is_admin(user_id)),
attachment=self.get_logo_attachment(user_id)
)
@@ -712,7 +725,6 @@ class VKBotApplication:
db.update_user_state(user_id, "BAZA_FORWHO")
self.send_message(
user_id,
"Отлично! Задам пару коротких вопросов и подберу подходящий формат 🎯\n\n"
"Для кого подбираем тренировки?",
keyboard=get_baza_forwho_keyboard()
)
@@ -730,7 +742,7 @@ class VKBotApplication:
self.send_message(user_id, "Выберите вариант на кнопках ниже 👇", keyboard=get_baza_forwho_keyboard())
return
db.update_user_state(user_id, "BAZA_AGE")
self.send_message(user_id, "Сколько лет будущему ученику?", keyboard=get_baza_age_keyboard())
self.send_message(user_id, "Сколько лет игроку?", keyboard=get_baza_age_keyboard())
def _handle_baza_age(self, user_id: int, text: str, text_lower: str):
if self._baza_nav(user_id, text_lower):
@@ -741,12 +753,12 @@ class VKBotApplication:
return
if "младше 12" in text_lower or "меньше 12" in text_lower:
db.set_profile_field(user_id, "age", "Младше 12 лет")
db.add_metka(user_id, "age_under12")
db.update_user_state(user_id, "BAZA_YOUNG")
self.send_message(
user_id,
"Обычно мы занимаемся с ребятами от 12 лет. Если ученику чуть меньше — это не проблема, "
"лучше подобрать формат вместе с менеджером.",
"Основные программы Кибершколы БАЗА рассчитаны на игроков от 12 лет. Но мы можем "
"отдельно посмотреть, получится ли подобрать подходящий формат.\n"
"Передать запрос менеджеру?",
keyboard=get_baza_younger12_keyboard()
)
return
@@ -769,7 +781,7 @@ class VKBotApplication:
return
if text_lower.startswith("назад") or text_lower.startswith("вернуться"):
db.update_user_state(user_id, "BAZA_AGE")
self.send_message(user_id, "Сколько лет будущему ученику?", keyboard=get_baza_age_keyboard())
self.send_message(user_id, "Сколько лет игроку?", keyboard=get_baza_age_keyboard())
return
if self._baza_nav(user_id, text_lower):
return
@@ -780,7 +792,7 @@ class VKBotApplication:
return
if text_lower.startswith("назад"):
db.update_user_state(user_id, "BAZA_AGE")
self.send_message(user_id, "Сколько лет будущему ученику?", keyboard=get_baza_age_keyboard())
self.send_message(user_id, "Сколько лет игроку?", keyboard=get_baza_age_keyboard())
return
if "cs" in text_lower or "кс" in text_lower or "контр" in text_lower:
game, metka = "CS2", "game_cs2"
@@ -939,7 +951,7 @@ class VKBotApplication:
def _baza_send_payment(self, user_id: int, link_key: str, tariff_label: str, campaign: str = "main"):
"""Показывает ссылку на оплату/запись + кнопки «проблема с оплатой» / «вопрос менеджеру»."""
link = self._utm(config.LINKS.get(link_key, config.LINKS["tariffs"]), campaign)
link = self._utm(self._link(link_key), campaign)
db.set_profile_field(user_id, "tariff", tariff_label)
db.add_metka(user_id, "clicked_payment")
db.add_metka(user_id, "lead_hot")
@@ -987,7 +999,7 @@ class VKBotApplication:
db.update_user_state(user_id, "BAZA_INT_INFO")
self.send_message(
user_id,
config.INTENSIVE_INFO + "\n\nХотите записаться на интенсив?",
config.INTENSIVE_INFO,
keyboard=get_baza_intensive_keyboard()
)
@@ -996,7 +1008,7 @@ class VKBotApplication:
return
if "участвовать" in text_lower or "записаться" in text_lower or "хочу" in text_lower or "да" == text_lower:
db.update_user_state(user_id, "BAZA_INT_AGE")
self.send_message(user_id, "Отлично! Сколько лет игроку?", keyboard=get_baza_age_keyboard())
self.send_message(user_id, "Сколько лет игроку?", keyboard=get_baza_age_keyboard())
return
self.send_message(user_id, "Выберите действие на кнопках ниже 👇", keyboard=get_baza_intensive_keyboard())
@@ -1005,7 +1017,7 @@ class VKBotApplication:
return
if text_lower.startswith("назад"):
db.update_user_state(user_id, "BAZA_INT_INFO")
self.send_message(user_id, "Хотите записаться на интенсив?", keyboard=get_baza_intensive_keyboard())
self.send_message(user_id, config.INTENSIVE_INFO, keyboard=get_baza_intensive_keyboard())
return
if "младше 12" in text_lower or "меньше 12" in text_lower:
db.set_profile_field(user_id, "age", "Младше 12 лет")
@@ -1113,7 +1125,7 @@ class VKBotApplication:
def _baza_show_prices(self, user_id: int):
db.update_user_state(user_id, "FREE_CHAT")
self.send_message(user_id, config.PRICES_TEXT + "\n\nЧто дальше?", keyboard=get_baza_prices_keyboard())
self.send_message(user_id, config.PRICES_TEXT, keyboard=get_baza_prices_keyboard())
def _baza_manager_card(self, user_id: int, reason: str = "") -> str:
p = db.get_profile(user_id)
@@ -1383,6 +1395,41 @@ class VKBotApplication:
back_to_faq(f"✅ Ответ №{fid} удалён.")
return
# ---------- Ссылки кнопок ----------
if state == "ADMIN_LINK_PICK":
key = pick_id(text)
if key not in config.LINK_LABELS:
self.send_message(user_id, "⚠️ Выберите ссылку кнопкой ниже.")
return
self._admin_ctx[user_id] = {"link_key": key}
db.update_user_state(user_id, "ADMIN_LINK_VAL")
self.send_message(
user_id,
f"✏️ «{config.LINK_LABELS[key]}»\nТекущий адрес:\n{self._link(key)}\n\n"
"Пришлите новый URL (начинается с http:// или https://). UTM добавится автоматически.",
keyboard=get_cancel_admin_keyboard()
)
return
if state == "ADMIN_LINK_VAL":
key = self._admin_ctx.get(user_id, {}).get("link_key")
url = text.strip()
if not key or key not in config.LINK_LABELS:
self._admin_ctx.pop(user_id, None)
db.update_user_state(user_id, "FREE_CHAT")
self._send_links_admin(user_id)
return
if not re.match(r"^https?://", url, re.I):
self.send_message(user_id, "⚠️ Ссылка должна начинаться с http:// или https://",
keyboard=get_cancel_admin_keyboard())
return
db.set_link(key, url)
self._admin_ctx.pop(user_id, None)
db.update_user_state(user_id, "FREE_CHAT")
self.send_message(user_id, f"✅ Ссылка «{config.LINK_LABELS[key]}» обновлена.")
self._send_links_admin(user_id)
return
# ---------- Администраторы (супер-админ) ----------
if state == "ADMIN_ADMIN_ADD":
if not self._is_super_admin(user_id):
@@ -1446,6 +1493,23 @@ class VKBotApplication:
)
self.send_message(user_id, msg)
def _send_links_admin(self, user_id: int):
"""Раздел «Ссылки»: текущие адреса кнопок записи/оплаты + кнопка изменения."""
lines = []
for key, label in config.LINK_LABELS.items():
overridden = " (изменена)" if db.get_link(key) else ""
lines.append(f"🔗 {label}{overridden}\n {self._link(key)}")
kb = VkKeyboard(inline=True)
kb.add_button("✏️ Изменить ссылку", color=VkKeyboardColor.PRIMARY)
kb.add_line()
kb.add_button("В админку ↩️", color=VkKeyboardColor.SECONDARY)
self.send_message(
user_id,
"🔗 Ссылки кнопок записи/оплаты\n\n" + "\n\n".join(lines) +
"\n\nUTM-метки добавляются автоматически. Нажмите «Изменить ссылку», чтобы задать новый адрес.",
keyboard=kb.get_keyboard()
)
def _send_faq_admin(self, user_id: int):
"""Раздел «Быстрые ответы»: список + инструкции."""
rows = db.get_custom_faq()