This commit is contained in:
2025-09-14 17:30:02 +05:00
commit 8ad2bfbe59
25 changed files with 462 additions and 0 deletions

286
bot.py Normal file
View File

@@ -0,0 +1,286 @@
import os
from io import BytesIO
from aiogram.types import KeyboardButton, ReplyKeyboardMarkup
import asyncio
from datetime import datetime
import psycopg2
import psycopg2.extras
from aiogram import Bot, Dispatcher, types
from aiogram.filters import Command
from aiogram.fsm.context import FSMContext
from aiogram.fsm.state import State, StatesGroup
from aiogram.fsm.storage.memory import MemoryStorage
from aiogram.types import KeyboardButton, ReplyKeyboardMarkup, ReplyKeyboardRemove
from aiogram.filters.state import StateFilter
TOKEN = "8433819654:AAGbK2UVrGiP-S5cfLn_B6pki2PR6cV2syQ"
# ===== PostgreSQL =====
conn = psycopg2.connect(
dbname="botdb",
user="botuser",
password="tpg44dm*oOas&2geWc^Paw7",
host="127.0.0.1"
)
cur = conn.cursor()
bot = Bot(token=TOKEN)
dp = Dispatcher(storage=MemoryStorage())
# ===== FSM =====
class Form(StatesGroup):
consent = State()
fio = State()
dob = State()
phone = State()
city = State()
klimov = State()
# ===== /start =====
@dp.message(Command("start"))
async def start(message: types.Message, state: FSMContext):
kb = [
[KeyboardButton(text="✅ Да, согласен"), KeyboardButton(text="❌ Нет")]
]
keyboard = ReplyKeyboardMarkup(keyboard=kb, resize_keyboard=True)
await message.answer(
"Привет! Я помогу пройти тест Климова.\n\n"
"Сначала — согласие на обработку персональных данных.\n"
"Ты согласен?",
reply_markup=keyboard
)
await state.set_state(Form.consent)
# ===== Согласие =====
@dp.message(Form.consent)
async def consent(message: types.Message, state: FSMContext):
if message.text.startswith(""):
await state.set_state(Form.fio)
await message.answer("Введите своё ФИО:", reply_markup=ReplyKeyboardRemove())
else:
await message.answer("Без согласия мы не можем продолжить 🙏", reply_markup=ReplyKeyboardRemove())
await state.clear()
# ===== ФИО =====
@dp.message(Form.fio)
async def fio(message: types.Message, state: FSMContext):
await state.update_data(fio=message.text)
await state.set_state(Form.dob)
await message.answer("Введите дату рождения (ДД.MM.ГГГГ):")
# ===== Дата рождения =====
@dp.message(Form.dob)
async def dob(message: types.Message, state: FSMContext):
await state.update_data(dob=message.text)
await state.set_state(Form.phone)
await message.answer("Введите номер телефона:")
# ===== Телефон =====
@dp.message(Form.phone)
async def phone(message: types.Message, state: FSMContext):
await state.update_data(phone=message.text)
await state.set_state(Form.city)
await message.answer("Из какого ты населённого пункта?")
# ===== Город + меню тестов =====
@dp.message(Form.city)
async def city(message: types.Message, state: FSMContext):
await state.update_data(city=message.text)
data = await state.get_data()
# Преобразуем дату в формат PostgreSQL
try:
dob_parsed = datetime.strptime(data["dob"], "%d.%m.%Y").date()
except ValueError:
await message.answer("Неверный формат даты. Введите дату в формате ДД.MM.ГГГГ")
await state.set_state(Form.dob)
return
try:
cur.execute(
"""
INSERT INTO users (telegram_id, fio, dob, phone, city)
VALUES (%s, %s, %s, %s, %s)
ON CONFLICT (telegram_id) DO UPDATE
SET fio=EXCLUDED.fio, dob=EXCLUDED.dob, phone=EXCLUDED.phone, city=EXCLUDED.city
""",
(message.from_user.id, data["fio"], dob_parsed, data["phone"], data["city"])
)
conn.commit()
await message.answer("Анкета сохранена ✅")
except Exception as e:
conn.rollback()
await message.answer(f"Ошибка при сохранении: {e}")
return
kb = [
[KeyboardButton(text="📘 Тест Климова")],
[KeyboardButton(text="📗 Тест №2"), KeyboardButton(text="📙 Тест №3")]
]
keyboard = ReplyKeyboardMarkup(keyboard=kb, resize_keyboard=True)
await state.clear()
await message.answer("Теперь выбери тест:", reply_markup=keyboard)
# ===== Выбор теста после анкеты =====
# Выбор теста — только когда FSM пустой (после анкеты)
@dp.message(StateFilter(None))
async def select_test(message: types.Message, state: FSMContext):
if message.text == "📘 Тест Климова":
cur.execute("SELECT id, text1, category1, text2, category2, image FROM klimov_questions ORDER BY id")
questions = cur.fetchall()
await state.update_data(klimov_questions=questions, klimov_index=0, klimov_result={})
await state.set_state(Form.klimov)
await send_klimov_question(message.from_user.id, state)
else:
await message.answer("Пока этот тест не реализован.")
import os
from aiogram.types import KeyboardButton, ReplyKeyboardMarkup, FSInputFile
from aiogram.fsm.context import FSMContext
async def send_klimov_question(user_id, state: FSMContext):
data = await state.get_data()
index = data["klimov_index"]
questions = data["klimov_questions"]
if index >= len(questions):
await send_klimov_result(user_id, state)
await state.clear()
return
q = questions[index] # (id, text1, category1, text2, category2, image)
# Клавиатура с вариантами
kb = [
[KeyboardButton(text=q[1])],
[KeyboardButton(text=q[3])]
]
keyboard = ReplyKeyboardMarkup(keyboard=kb, resize_keyboard=True)
# Отправка картинки, если есть
image_filename = os.path.basename(q[5].strip()) if q[5] else None
if image_filename:
image_path = os.path.join(os.getcwd(), "images", image_filename)
print("DEBUG: image_path =", image_path, "exists =", os.path.isfile(image_path))
if os.path.isfile(image_path):
photo = FSInputFile(image_path) # вот ключевой момент
await bot.send_photo(
chat_id=user_id,
photo=photo,
caption=f"Вопрос {index+1} из {len(questions)}:\nВыберите вариант",
reply_markup=keyboard
)
return
# Если картинки нет, просто отправляем текст
await bot.send_message(
chat_id=user_id,
text=f"Вопрос {index+1} из {len(questions)}:\nВыберите вариант",
reply_markup=keyboard
)
# ===== Ответ на вопросы =====
@dp.message(StateFilter(Form.klimov))
async def klimov_answer(message: types.Message, state: FSMContext):
data = await state.get_data()
index = data["klimov_index"]
questions = data["klimov_questions"]
result = data["klimov_result"]
q = questions[index]
# Проверяем какой вариант выбрал пользователь
if message.text == q[1]:
result[q[2]] = result.get(q[2], 0) + 1
elif message.text == q[3]:
result[q[4]] = result.get(q[4], 0) + 1
index += 1
await state.update_data(klimov_index=index, klimov_result=result)
# Переходим к следующему вопросу
await send_klimov_question(message.from_user.id, state)
# ===== Функция показа вопроса =====
# Функция показа вопроса
# ===== Функция для сохранения результата в БД =====
async def save_klimov_result(user_id, state: FSMContext):
data = await state.get_data()
result = data.get("klimov_result", {})
conn = psycopg2.connect(dbname="botdb", user="botuser", password="tpg44dm*oOas&2geWc^Paw7")
cur = conn.cursor()
# Получаем user_id из таблицы users
cur.execute("SELECT id FROM users WHERE telegram_id = %s", (user_id,))
user_row = cur.fetchone()
if user_row:
db_user_id = user_row[0]
cur.execute(
"INSERT INTO results (user_id, test_name, result) VALUES (%s, %s, %s)",
(db_user_id, "Тест Климова", psycopg2.extras.Json(result))
)
conn.commit()
cur.close()
conn.close()
# Словарь с описаниями профессий по категориям
professions_desc = {
"Человек-природа": "К типу 'человек-природа' можно отнести профессии, связанные с изучением живой и неживой природы (микробиолог, агрохимик, геолог), с уходом за растениями и животными (лесовод, овощевод, зоотехник).",
"Человек-техника": "Тип 'человек-техника' включает в себя профессии, связанные с созданием, монтажом, сборкой и наладкой технических устройств (каменщик, монтажник, сварщик, инженер-конструктор), эксплуатацией технических средств (водитель, крановщик, токарь, швея), ремонтом техники.",
"Человек-человек": "К профессиям типа 'человек-человек' относятся профессии, связанные с медицинским обслуживанием (врач, медсестра, санитарка), обучением и воспитанием (воспитатель, няня, учитель, преподаватель, тренер), бытовым обслуживанием (продавец, проводник, официант).",
"Человек-знаковая система": "Тип 'человек - знаковая система' объединяет профессии, связанные с текстами (корректор, оператор ПК, переводчик, библиотекарь), с цифрами, формулами и таблицами (программист, экономист, бухгалтер, кассир), с чертежами, картами, схемами (штурман, чертежник).",
"Человек-художественный образ": "К типу 'человек - художественный образ' можно отнести профессии, связанные с созданием, проектированием, моделированием художественных произведений (художник, журналист, модельер, композитор), с воспроизведением, изготовлением различных изделий по эскизу, образцу."
}
# ===== Функция для форматированного вывода результатов =====
# ===== Форматированный вывод результатов с процентами по максимуму =====
async def send_klimov_result(user_id, state: FSMContext):
data = await state.get_data()
result = data.get("klimov_result", {})
questions = data.get("klimov_questions", [])
if not questions:
await bot.send_message(user_id, "Вы не ответили ни на один вопрос.")
return
# Подсчёт максимального количества очков для каждой категории
max_points = {}
for q in questions:
# q[2] и q[4] — категории для вариантов
max_points[q[2]] = max_points.get(q[2], 0) + 1
max_points[q[4]] = max_points.get(q[4], 0) + 1
output = "Ваши результаты:\n"
for category, max_pt in max_points.items():
pts = result.get(category, 0)
percent = (pts / max_pt) * 100
output += f"- {category} - {percent:.1f}%\n"
# Определяем категорию с наибольшим результатом
if result:
top_category = max(result, key=result.get)
output += f"\nТип профессии: {top_category}"
await bot.send_message(user_id, output, reply_markup=types.ReplyKeyboardRemove())
# Затем по очереди отправляем описания
for category in ["Человек-природа", "Человек-техника", "Человек-человек",
"Человек-знаковая система", "Человек-художественный образ"]:
await bot.send_message(user_id, f"🟢 {category}\n\n{professions_desc[category]}")
# Последнее сообщение с контактами / ссылками
await bot.send_message(
user_id,
" Задать вопрос или записаться на индивидуальную консультацию по построению образовательного маршрута можно здесь: @chrtakova11\n\n"
"Узнать подробнее о профессиях: https://postupi.online/professii/so/"
)
# ===== Запуск бота =====
async def main():
await dp.start_polling(bot)
if __name__ == "__main__":
asyncio.run(main())

176
bot2.py Normal file
View File

@@ -0,0 +1,176 @@
from aiogram import Bot, Dispatcher, types
from aiogram.filters import Command, StateFilter
from aiogram.fsm.state import State, StatesGroup
from aiogram.fsm.context import FSMContext
from aiogram.fsm.storage.memory import MemoryStorage
from aiogram.types import KeyboardButton, ReplyKeyboardMarkup, ReplyKeyboardRemove
import psycopg2
import psycopg2.extras
from datetime import datetime
import os
from aiogram.types import FSInputFile
TOKEN = "ВАШ_TOKEN"
bot = Bot(token=TOKEN)
dp = Dispatcher(storage=MemoryStorage())
# ===== FSM =====
class Form(StatesGroup):
consent = State()
fio = State()
dob = State()
phone = State()
city = State()
klimov = State()
@dp.message(Command("start"))
async def start(message: types.Message, state: FSMContext):
kb = [[KeyboardButton("✅ Да, согласен"), KeyboardButton("❌ Нет")]]
keyboard = ReplyKeyboardMarkup(keyboard=kb, resize_keyboard=True)
await message.answer(
"Привет! Сначала нужно согласие на обработку персональных данных. Согласны?",
reply_markup=keyboard
)
await state.set_state(Form.consent)
@dp.message(Form.consent)
async def consent(message: types.Message, state: FSMContext):
if message.text.startswith(""):
await state.set_state(Form.fio)
await message.answer("Введите своё ФИО:", reply_markup=ReplyKeyboardRemove())
else:
await message.answer("Без согласия мы не можем продолжить 🙏", reply_markup=ReplyKeyboardRemove())
await state.clear()
@dp.message(Form.fio)
async def fio(message: types.Message, state: FSMContext):
await state.update_data(fio=message.text)
await state.set_state(Form.dob)
await message.answer("Введите дату рождения (ДД.MM.ГГГГ):")
@dp.message(Form.dob)
async def dob(message: types.Message, state: FSMContext):
await state.update_data(dob=message.text)
await state.set_state(Form.phone)
await message.answer("Введите номер телефона:")
@dp.message(Form.phone)
async def phone(message: types.Message, state: FSMContext):
await state.update_data(phone=message.text)
await state.set_state(Form.city)
await message.answer("Из какого ты города?")
@dp.message(Form.city)
async def city(message: types.Message, state: FSMContext):
await state.update_data(city=message.text)
data = await state.get_data()
# Сохраняем в БД
try:
dob_parsed = datetime.strptime(data["dob"], "%d.%m.%Y").date()
except ValueError:
await message.answer("Неверный формат даты. Введите ДД.MM.ГГГГ")
await state.set_state(Form.dob)
return
conn = psycopg2.connect(dbname="botdb", user="botuser", password="tpg44dm*oOas&2geWc^Paw7")
cur = conn.cursor()
cur.execute("""
INSERT INTO users (telegram_id, fio, dob, phone, city)
VALUES (%s,%s,%s,%s,%s)
ON CONFLICT (telegram_id) DO UPDATE
SET fio=EXCLUDED.fio, dob=EXCLUDED.dob, phone=EXCLUDED.phone, city=EXCLUDED.city
""", (message.from_user.id, data["fio"], dob_parsed, data["phone"], data["city"]))
conn.commit()
cur.close()
conn.close()
kb = [[KeyboardButton("📘 Тест Климова")]]
keyboard = ReplyKeyboardMarkup(keyboard=kb, resize_keyboard=True)
await state.clear()
await message.answer("Анкета сохранена ✅ Выберите тест:", reply_markup=keyboard)
@dp.message(StateFilter(None))
async def select_test(message: types.Message, state: FSMContext):
if message.text == "📘 Тест Климова":
conn = psycopg2.connect(dbname="botdb", user="botuser", password="ваш_пароль")
cur = conn.cursor()
cur.execute("SELECT id, text1, category1, text2, category2, image FROM klimov_questions ORDER BY id")
questions = cur.fetchall()
cur.close()
conn.close()
await state.update_data(klimov_questions=questions, klimov_index=0, klimov_result={})
await state.set_state(Form.klimov)
await send_klimov_question(message.from_user.id, state)
else:
await message.answer("Пока этот тест не реализован.")
async def send_klimov_question(user_id, state: FSMContext):
data = await state.get_data()
index = data.get("klimov_index", 0)
questions = data.get("klimov_questions", [])
if index >= len(questions):
await send_klimov_result(user_id, state)
await state.clear()
return
q = questions[index]
kb = [[KeyboardButton(q[1])], [KeyboardButton(q[3])]]
keyboard = ReplyKeyboardMarkup(keyboard=kb, resize_keyboard=True)
image_path = q[5]
if image_path and os.path.exists(image_path):
photo = FSInputFile(image_path)
await bot.send_photo(
chat_id=user_id,
photo=photo,
caption=f"Вопрос {index+1} из {len(questions)}:\nВыберите вариант",
reply_markup=keyboard
)
return
await bot.send_message(
chat_id=user_id,
text=f"Вопрос {index+1} из {len(questions)}:\nВыберите вариант",
reply_markup=keyboard
)
@dp.message(Form.klimov)
async def klimov_answer(message: types.Message, state: FSMContext):
data = await state.get_data()
index = data["klimov_index"]
questions = data["klimov_questions"]
result = data["klimov_result"]
q = questions[index]
if message.text == q[1]:
result[q[2]] = result.get(q[2], 0) + 1
elif message.text == q[3]:
result[q[4]] = result.get(q[4], 0) + 1
index += 1
await state.update_data(klimov_index=index, klimov_result=result)
await send_klimov_question(message.from_user.id, state)
async def send_klimov_result(user_id, state: FSMContext):
data = await state.get_data()
result = data.get("klimov_result", {})
questions = data.get("klimov_questions", [])
if not questions:
await bot.send_message(user_id, "Вы не ответили ни на один вопрос.")
return
max_points = {}
for q in questions:
max_points[q[2]] = max_points.get(q[2], 0) + 1
max_points[q[4]] = max_points.get(q[4], 0) + 1
output = "Ваши результаты:\n"
for cat, max_pt in max_points.items():
pts = result.get(cat, 0)
percent = (pts / max_pt) * 100
output += f"- {cat}: {percent:.1f}%\n"
if result:
top_cat = max(result, key=result.get)
output += f"\nТип профессии: {top_cat}"
await bot.send_message(user_id, output, reply_markup=ReplyKeyboardRemove())

BIN
images/10.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 39 KiB

BIN
images/11.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 37 KiB

BIN
images/12.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 37 KiB

BIN
images/13.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

BIN
images/14.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 31 KiB

BIN
images/15.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

BIN
images/16.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

BIN
images/17.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 37 KiB

BIN
images/18.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 35 KiB

BIN
images/19.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 38 KiB

BIN
images/2.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

BIN
images/20.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 33 KiB

BIN
images/3.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 46 KiB

BIN
images/4.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 37 KiB

BIN
images/5.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

BIN
images/6.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 39 KiB

BIN
images/7.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 42 KiB

BIN
images/8.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 31 KiB

BIN
images/9.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 31 KiB

BIN
images/images/10.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 39 KiB

BIN
images/images/2.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

BIN
images/images/3.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 46 KiB

BIN
images/images/4.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 37 KiB