Админка, защита и UX: быстрые ответы, статусы кандидатов, мульти-операторы, бан, прогресс-бар

Бот:
- Прогресс-бар (▰▰▱ Шаг N из 3) в шагах заявки
- Быстрые ответы по ключевым словам (таблица custom_faq) — отвечают до зова оператора;
  редактирование через админ-панель (раздел «Быстрые ответы»: добавить/список/удалить)
- Отклики в статистике: счётчик игроков/сотрудников по дисциплинам и должностям
- Статусы кандидатов (новый/на собесе/отказ/принят) — раздел «Кандидаты» в админке
- Несколько операторов: уведомления уходят всем из OPERATOR_PEER_IDS
- Часы работы (WORK_HOUR_START/END): вне часов хендофф сообщает «ответим утром»
- Временный бан при повторном флуде (усиление анти-DDoS)
- Старт-пинг администратору «бот запущен» (лёгкий healthcheck)

Док и зависимости:
- README.md (запуск, .env, автозапуск, админ-команды, структура)
- requirements.txt

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Kenik
2026-06-23 10:00:56 +03:00
parent 377188a015
commit 3d709267ff
6 changed files with 397 additions and 30 deletions
+81
View File
@@ -113,6 +113,22 @@ class DatabaseManager:
role TEXT,
experience TEXT,
contact TEXT,
status TEXT DEFAULT 'новый',
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
)
""")
# Миграция: статус для существующих таблиц applications
try:
cursor.execute("ALTER TABLE applications ADD COLUMN status TEXT DEFAULT 'новый'")
except sqlite3.OperationalError:
pass
# Table for editable quick FAQ answers (быстрые ответы по ключевым словам)
cursor.execute("""
CREATE TABLE IF NOT EXISTS custom_faq (
id INTEGER PRIMARY KEY AUTOINCREMENT,
keyword TEXT NOT NULL,
answer TEXT NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
)
""")
@@ -410,6 +426,71 @@ class DatabaseManager:
)
conn.commit()
# --- Быстрые ответы (custom FAQ) ---
def add_custom_faq(self, keyword: str, answer: str) -> int:
with self._get_connection() as conn:
cursor = conn.cursor()
cursor.execute("INSERT INTO custom_faq (keyword, answer) VALUES (?, ?)",
(keyword.strip().lower(), answer.strip()))
conn.commit()
return cursor.lastrowid
def get_custom_faq(self) -> list:
"""Список (id, keyword, answer) всех быстрых ответов."""
with self._get_connection() as conn:
cursor = conn.cursor()
cursor.execute("SELECT id, keyword, answer FROM custom_faq ORDER BY id")
return cursor.fetchall()
def delete_custom_faq(self, faq_id: int) -> bool:
with self._get_connection() as conn:
cursor = conn.cursor()
cursor.execute("DELETE FROM custom_faq WHERE id = ?", (faq_id,))
conn.commit()
return cursor.rowcount > 0
def match_custom_faq(self, text_lower: str) -> Optional[str]:
"""Возвращает ответ, если в тексте встречается одно из ключевых слов."""
for _id, keyword, answer in self.get_custom_faq():
if keyword and keyword in text_lower:
return answer
return None
# --- Отклики: статусы и статистика ---
def set_application_status(self, app_id: int, status: str) -> bool:
with self._get_connection() as conn:
cursor = conn.cursor()
cursor.execute("UPDATE applications SET status = ? WHERE id = ?", (status, app_id))
conn.commit()
return cursor.rowcount > 0
def get_recent_applications(self, limit: int = 10) -> list:
with self._get_connection() as conn:
conn.row_factory = sqlite3.Row
cursor = conn.cursor()
cursor.execute(
"SELECT id, track, discipline, position, contact, status, created_at "
"FROM applications ORDER BY id DESC LIMIT ?", (limit,)
)
return [dict(r) for r in cursor.fetchall()]
def get_application_stats(self) -> Dict[str, Any]:
"""Статистика откликов: всего, игроки/сотрудники, по дисциплинам и должностям."""
with self._get_connection() as conn:
cursor = conn.cursor()
stats = {
"total": cursor.execute("SELECT COUNT(*) FROM applications").fetchone()[0],
"players": cursor.execute("SELECT COUNT(*) FROM applications WHERE track='player'").fetchone()[0],
"workers": cursor.execute("SELECT COUNT(*) FROM applications WHERE track='worker'").fetchone()[0],
}
cursor.execute("SELECT discipline, COUNT(*) FROM applications WHERE track='player' "
"AND discipline IS NOT NULL GROUP BY discipline ORDER BY COUNT(*) DESC")
stats["by_discipline"] = cursor.fetchall()
cursor.execute("SELECT position, COUNT(*) FROM applications WHERE track='worker' "
"AND position IS NOT NULL GROUP BY position ORDER BY COUNT(*) DESC")
stats["by_position"] = cursor.fetchall()
return stats
def add_web_registration(self, reg_id: str, data: dict):
"""Сохранить заявку с веб-формы в базу данных SQLite."""
with self._get_connection() as conn: