From 61d8eb48a95e2eb6e2997c04a619de7a8047a96d Mon Sep 17 00:00:00 2001 From: grigorij Date: Thu, 11 Jun 2026 13:50:24 +0400 Subject: [PATCH] =?UTF-8?q?feat:=20=D1=80=D0=B5=D0=B0=D0=BB=D0=B8=D0=B7?= =?UTF-8?q?=D0=BE=D0=B2=D0=B0=D0=BD=20MVP=20Telegram-=D0=B1=D0=BE=D1=82?= =?UTF-8?q?=D0=B0=20=D0=B4=D0=BB=D1=8F=20=D0=B7=D0=B0=D0=BF=D0=B8=D1=81?= =?UTF-8?q?=D0=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * настроена структура проекта * подключены handlers и routers * реализовано главное меню * добавлены разделы: профиль, контакты, о компании, мои записи * реализована FSM-машина состояний для записи * добавлен сервисный слой booking_service * реализовано сохранение записей в памяти * добавлено подтверждение записи * реализована отмена записи * исправлены конфликты FSM и кнопок меню * добавлена отдельная клавиатура во время записи * выполнен рефакторинг импортов и структуры проекта * улучшен пользовательский сценарий записи --- app/db/memory.py | 3 +- app/handlers/about.py | 11 +++ app/handlers/booking.py | 122 ++++++++++++++++++++++++++++++++ app/handlers/contacts.py | 11 +++ app/handlers/mybookings.py | 28 ++++++++ app/handlers/profile.py | 19 +++++ app/handlers/services.py | 2 + app/handlers/start.py | 17 ++++- app/keyboards/menu.py | 25 +++++++ app/main.py | 33 +++++++++ app/services/booking_service.py | 12 ++++ app/states/booking.py | 6 ++ main.py | 23 ------ 13 files changed, 285 insertions(+), 27 deletions(-) create mode 100644 app/handlers/about.py create mode 100644 app/handlers/booking.py create mode 100644 app/handlers/contacts.py create mode 100644 app/handlers/mybookings.py create mode 100644 app/handlers/profile.py create mode 100644 app/handlers/services.py create mode 100644 app/main.py create mode 100644 app/services/booking_service.py delete mode 100644 main.py diff --git a/app/db/memory.py b/app/db/memory.py index 34c0968..481f953 100644 --- a/app/db/memory.py +++ b/app/db/memory.py @@ -1 +1,2 @@ -BOOKINGS = {} \ No newline at end of file +users = {} +bookings = [] \ No newline at end of file diff --git a/app/handlers/about.py b/app/handlers/about.py new file mode 100644 index 0000000..c590b5d --- /dev/null +++ b/app/handlers/about.py @@ -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 бот 💆‍♂️") \ No newline at end of file diff --git a/app/handlers/booking.py b/app/handlers/booking.py new file mode 100644 index 0000000..f2afef0 --- /dev/null +++ b/app/handlers/booking.py @@ -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 + ) \ No newline at end of file diff --git a/app/handlers/contacts.py b/app/handlers/contacts.py new file mode 100644 index 0000000..762ff45 --- /dev/null +++ b/app/handlers/contacts.py @@ -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") \ No newline at end of file diff --git a/app/handlers/mybookings.py b/app/handlers/mybookings.py new file mode 100644 index 0000000..cf560f3 --- /dev/null +++ b/app/handlers/mybookings.py @@ -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) \ No newline at end of file diff --git a/app/handlers/profile.py b/app/handlers/profile.py new file mode 100644 index 0000000..206793a --- /dev/null +++ b/app/handlers/profile.py @@ -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']}") \ No newline at end of file diff --git a/app/handlers/services.py b/app/handlers/services.py new file mode 100644 index 0000000..ac5e150 --- /dev/null +++ b/app/handlers/services.py @@ -0,0 +1,2 @@ +from aiogram import Router +router = Router() \ No newline at end of file diff --git a/app/handlers/start.py b/app/handlers/start.py index 8e5223d..bf8cdda 100644 --- a/app/handlers/start.py +++ b/app/handlers/start.py @@ -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("👋 Бот запущен (чистый старт)") \ No newline at end of file + users[message.from_user.id] = { + "name": message.from_user.full_name + } + + await message.answer( + "👋 Добро пожаловать в Demo Bot", + reply_markup=menu + ) \ No newline at end of file diff --git a/app/keyboards/menu.py b/app/keyboards/menu.py index e69de29..66debc4 100644 --- a/app/keyboards/menu.py +++ b/app/keyboards/menu.py @@ -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 +) \ No newline at end of file diff --git a/app/main.py b/app/main.py new file mode 100644 index 0000000..614b1b3 --- /dev/null +++ b/app/main.py @@ -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()) \ No newline at end of file diff --git a/app/services/booking_service.py b/app/services/booking_service.py new file mode 100644 index 0000000..fe182df --- /dev/null +++ b/app/services/booking_service.py @@ -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 \ No newline at end of file diff --git a/app/states/booking.py b/app/states/booking.py index e69de29..12e607a 100644 --- a/app/states/booking.py +++ b/app/states/booking.py @@ -0,0 +1,6 @@ +from aiogram.fsm.state import StatesGroup, State + +class BookingState(StatesGroup): + service = State() + date = State() + confirm = State() \ No newline at end of file diff --git a/main.py b/main.py deleted file mode 100644 index b965ac6..0000000 --- a/main.py +++ /dev/null @@ -1,23 +0,0 @@ -import asyncio -import logging - -from aiogram import Bot, Dispatcher -from aiogram.fsm.storage.memory import MemoryStorage - -from app.config import BOT_TOKEN -from app.handlers import start - -logging.basicConfig(level=logging.INFO) - - -async def main(): - bot = Bot(token=BOT_TOKEN) - dp = Dispatcher(storage=MemoryStorage()) - - dp.include_router(start.router) - - await dp.start_polling(bot) - - -if __name__ == "__main__": - asyncio.run(main()) \ No newline at end of file