feat: реализован MVP Telegram-бота для записи

* настроена структура проекта
* подключены handlers и routers
* реализовано главное меню
* добавлены разделы: профиль, контакты, о компании, мои записи
* реализована FSM-машина состояний для записи
* добавлен сервисный слой booking_service
* реализовано сохранение записей в памяти
* добавлено подтверждение записи
* реализована отмена записи
* исправлены конфликты FSM и кнопок меню
* добавлена отдельная клавиатура во время записи
* выполнен рефакторинг импортов и структуры проекта
* улучшен пользовательский сценарий записи
This commit is contained in:
grigorij
2026-06-11 13:50:24 +04:00
parent 2c6f82bfb5
commit 61d8eb48a9
13 changed files with 285 additions and 27 deletions
+2 -1
View File
@@ -1 +1,2 @@
BOOKINGS = {}
users = {}
bookings = []
+11
View File
@@ -0,0 +1,11 @@
from aiogram import Router, F
from aiogram.types import Message
from aiogram.fsm.context import FSMContext
router = Router()
@router.message(F.text == "О компании")
async def about(message: Message, state: FSMContext):
await state.clear() # 🔥 СБРОС FSM
await message.answer("Мы demo SPA бот 💆‍♂️")
+122
View File
@@ -0,0 +1,122 @@
from aiogram import Router, F
from aiogram.types import Message
from aiogram.fsm.context import FSMContext
from aiogram.filters import StateFilter
from app.states.booking import BookingState
from app.services.booking_service import create_booking
from app.keyboards.menu import menu, cancel_menu
router = Router()
# отмена записи на любом шаге
@router.message(F.text.lower() == "отмена")
async def cancel(message: Message, state: FSMContext):
await state.clear()
await message.answer(
"❌ Запись отменена",
reply_markup=menu
)
# старт записи
@router.message(F.text == "📅 Записаться")
async def start_booking(message: Message, state: FSMContext):
await state.clear()
await state.set_state(BookingState.service)
await message.answer(
"Выберите услугу:\n"
"1. Массаж\n"
"2. SPA\n"
"3. Косметология",
reply_markup=cancel_menu
)
# выбор услуги
@router.message(StateFilter(BookingState.service))
async def choose_service(message: Message, state: FSMContext):
forbidden = [
"📅 Записаться",
"👤 Профиль",
"📖 Мои записи",
"О компании",
"📞 Контакты"
]
if message.text in forbidden:
await message.answer(
"⚠️ Сначала завершите запись или напишите 'отмена'"
)
return
await state.update_data(service=message.text)
await state.set_state(BookingState.date)
await message.answer(
"Введите дату (например 20.06)"
)
# выбор даты
@router.message(StateFilter(BookingState.date))
async def choose_date(message: Message, state: FSMContext):
forbidden = [
"📅 Записаться",
"👤 Профиль",
"📖 Мои записи",
"О компании",
"📞 Контакты"
]
if message.text in forbidden:
await message.answer(
"⚠️ Сначала завершите запись или напишите 'отмена'"
)
return
await state.update_data(date=message.text)
data = await state.get_data()
await state.set_state(BookingState.confirm)
await message.answer(
f"Услуга: {data['service']}\n"
f"Дата: {data['date']}\n\n"
f"Напишите 'да' для подтверждения"
)
# подтверждение
@router.message(StateFilter(BookingState.confirm))
async def confirm_booking(message: Message, state: FSMContext):
if message.text.lower() != "да":
await state.clear()
await message.answer(
"❌ Запись отменена",
reply_markup=menu
)
data = await state.get_data()
create_booking(
user_id=message.from_user.id,
service=data["service"],
date=data["date"]
)
await state.clear()
await message.answer(
"✅ Вы успешно записаны!",
reply_markup=menu
)
+11
View File
@@ -0,0 +1,11 @@
from aiogram import Router, F
from aiogram.types import Message
from aiogram.fsm.context import FSMContext
router = Router()
@router.message(F.text == "📞 Контакты")
async def contacts(message: Message, state: FSMContext):
await state.clear()
await message.answer("📍 Москва\n☎ +7 999 999 99 99")
+28
View File
@@ -0,0 +1,28 @@
from aiogram import Router, F
from aiogram.types import Message
from aiogram.fsm.context import FSMContext
from app.db.memory import bookings
router = Router()
@router.message(F.text == "📖 Мои записи")
async def my_bookings(message: Message, state: FSMContext):
await state.clear()
user_id = message.from_user.id
user_bookings = [
b for b in bookings if b["user_id"] == user_id
]
if not user_bookings:
await message.answer("У вас нет записей")
return
text = "📖 Ваши записи:\n\n"
for b in user_bookings:
text += f"💆 {b['service']} — 📅 {b['date']}\n"
await message.answer(text)
+19
View File
@@ -0,0 +1,19 @@
from aiogram import Router, F
from aiogram.types import Message
from aiogram.fsm.context import FSMContext
from app.db.memory import users
router = Router()
@router.message(F.text == "👤 Профиль")
async def profile(message: Message, state: FSMContext):
await state.clear()
user = users.get(message.from_user.id)
if not user:
await message.answer("Профиль не найден")
return
await message.answer(f"👤 Имя: {user['name']}")
+2
View File
@@ -0,0 +1,2 @@
from aiogram import Router
router = Router()
+14 -3
View File
@@ -1,8 +1,19 @@
from aiogram import Router, F
from aiogram import Router
from aiogram.filters import CommandStart
from aiogram.types import Message
from app.keyboards.menu import menu
from app.db.memory import users
router = Router()
@router.message(F.text == "/start")
@router.message(CommandStart())
async def start(message: Message):
await message.answer("👋 Бот запущен (чистый старт)")
users[message.from_user.id] = {
"name": message.from_user.full_name
}
await message.answer(
"👋 Добро пожаловать в Demo Bot",
reply_markup=menu
)
+25
View File
@@ -0,0 +1,25 @@
from aiogram.types import ReplyKeyboardMarkup, KeyboardButton
# Главное меню
menu = ReplyKeyboardMarkup(
keyboard=[
[KeyboardButton(text="📅 Записаться")],
[
KeyboardButton(text="👤 Профиль"),
KeyboardButton(text="📖 Мои записи")
],
[
KeyboardButton(text="О компании"),
KeyboardButton(text="📞 Контакты")
]
],
resize_keyboard=True
)
# Меню во время записи
cancel_menu = ReplyKeyboardMarkup(
keyboard=[
[KeyboardButton(text="Отмена")]
],
resize_keyboard=True
)
+33
View File
@@ -0,0 +1,33 @@
import asyncio
from aiogram import Bot, Dispatcher
from app.config import BOT_TOKEN
from app.handlers.start import router as start_router
from app.handlers.booking import router as booking_router
from app.handlers.profile import router as profile_router
from app.handlers.mybookings import router as mybooking_router
from app.handlers.about import router as about_router
from app.handlers.contacts import router as contacts_router
async def main():
bot = Bot(token=BOT_TOKEN)
dp = Dispatcher()
# подключаем все роутеры
dp.include_router(start_router)
dp.include_router(booking_router)
dp.include_router(profile_router)
dp.include_router(mybooking_router)
dp.include_router(about_router)
dp.include_router(contacts_router)
print("Bot started...")
await dp.start_polling(bot)
if __name__ == "__main__":
asyncio.run(main())
+12
View File
@@ -0,0 +1,12 @@
from app.db.memory import bookings
def create_booking(user_id: int, service: str, date: str):
booking = {
"user_id": user_id,
"service": service,
"date": date,
"status": "active"
}
bookings.append(booking)
return booking
+6
View File
@@ -0,0 +1,6 @@
from aiogram.fsm.state import StatesGroup, State
class BookingState(StatesGroup):
service = State()
date = State()
confirm = State()