Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d757352b8a |
@@ -0,0 +1,7 @@
|
|||||||
|
BOT_TOKEN=your_t…here
|
||||||
|
ADMIN_PASSWORD=your_s…word
|
||||||
|
DB_HOST=10.0.77.9
|
||||||
|
DB_PORT=5432
|
||||||
|
DB_USER=spabot
|
||||||
|
DB_PASSWORD=spabot_password
|
||||||
|
DB_NAME=spabot
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
name: Deploy SPA Bot
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- feat/prod-prep
|
||||||
|
- master
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
deploy:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Deploy via SSH
|
||||||
|
env:
|
||||||
|
SSH_HOST: 10.0.77.8
|
||||||
|
SSH_USER: devproject
|
||||||
|
SSH_KEY: \${{ secrets.DEPLOY_SSH_KEY }}
|
||||||
|
run: |
|
||||||
|
mkdir -p ~/.ssh
|
||||||
|
echo "\$SSH_KEY" > ~/.ssh/deploy_key
|
||||||
|
chmod 600 ~/.ssh/deploy_key
|
||||||
|
ssh -o StrictHostKeyChecking=no -i ~/.ssh/deploy_key \$SSH_USER@\$SSH_HOST << 'EOF'
|
||||||
|
cd /opt/projects/spa-telegram-bot
|
||||||
|
git fetch origin
|
||||||
|
git reset --hard origin/feat/prod-prep
|
||||||
|
source .venv/bin/activate
|
||||||
|
pip install -r requirements.txt --quiet
|
||||||
|
sudo -n systemctl restart spa-telegram-bot
|
||||||
|
echo "Deploy complete"
|
||||||
|
EOF
|
||||||
@@ -17,9 +17,3 @@ data/*.json
|
|||||||
logs/
|
logs/
|
||||||
backups/
|
backups/
|
||||||
.env
|
.env
|
||||||
|
|
||||||
# Backups
|
|
||||||
*.bak
|
|
||||||
|
|
||||||
# Runtime logs
|
|
||||||
*.log
|
|
||||||
|
|||||||
@@ -1,124 +0,0 @@
|
|||||||
# 💆 Demo SPA Bot
|
|
||||||
|
|
||||||
Telegram-бот для SPA-салона: запись на услуги, выбор мастера, управление бронированиями и админ-панель.
|
|
||||||
|
|
||||||
Построен на **aiogram** с JSON-хранилищем — лёгкий, без БД, готов к запуску.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 🚀 Возможности
|
|
||||||
|
|
||||||
- **📋 Услуги** — массаж, SPA, косметология (настраивается в `config.py`)
|
|
||||||
- **👩🔬 Мастера** — выбор специалиста с эмодзи-аватарками
|
|
||||||
- **📅 Запись** — календарь, слоты по 1 часу, мультиуслуги
|
|
||||||
- **👤 Профиль** — просмотр имени / ID
|
|
||||||
- **📄 Мои записи** — просмотр, отмена, редактирование броней
|
|
||||||
- **🔐 Админ-панель** — просмотр и отмена любых записей
|
|
||||||
- **📞 Контакты** — адрес и телефон салона
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 🧱 Структура
|
|
||||||
|
|
||||||
```
|
|
||||||
demo_spa_bot/
|
|
||||||
├── app/
|
|
||||||
│ ├── main.py # Точка входа
|
|
||||||
│ ├── config.py # Услуги, мастера, слоты
|
|
||||||
│ ├── logger.py # Логирование
|
|
||||||
│ ├── db/
|
|
||||||
│ │ ├── __init__.py
|
|
||||||
│ │ └── memory.py # JSON-хранилище (users, bookings)
|
|
||||||
│ ├── handlers/
|
|
||||||
│ │ ├── admin.py # Админ-панель
|
|
||||||
│ │ ├── about.py # "О нас"
|
|
||||||
│ │ ├── booking.py # Процесс записи (FSM)
|
|
||||||
│ │ ├── contacts.py # Контакты
|
|
||||||
│ │ ├── mybookings.py # Мои записи
|
|
||||||
│ │ ├── profile.py # Профиль
|
|
||||||
│ │ ├── services.py # Список услуг
|
|
||||||
│ │ └── start.py # Команда /start
|
|
||||||
│ ├── keyboards/ # Reply-клавиатуры
|
|
||||||
│ ├── services/
|
|
||||||
│ │ └── booking_service.py # Бизнес-логика бронирования
|
|
||||||
│ └── states/
|
|
||||||
│ └── booking.py # Состояния FSM
|
|
||||||
└── data/ # JSON-файлы (users.json, bookings.json)
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 🛠 Установка и запуск
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# 1. Клонировать
|
|
||||||
git clone https://git.labnethub.ru/grejoi4621/demo_spa_bot.git
|
|
||||||
cd demo_spa_bot
|
|
||||||
|
|
||||||
# 2. Виртуальное окружение
|
|
||||||
python -m venv .venv
|
|
||||||
source .venv/bin/activate # Linux/macOS
|
|
||||||
# .venv\Scripts\activate # Windows
|
|
||||||
|
|
||||||
# 3. Зависимости
|
|
||||||
pip install -r requirements.txt
|
|
||||||
|
|
||||||
# 4. Переменные окружения
|
|
||||||
cp .env.example .env # или создать вручную
|
|
||||||
# BOT_TOKEN=ваш_токен_бота
|
|
||||||
|
|
||||||
# 5. Запуск
|
|
||||||
python -m app.main
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## ⚙️ Конфигурация
|
|
||||||
|
|
||||||
В `app/config.py`:
|
|
||||||
|
|
||||||
| Переменная | Описание | Значение по умолчанию |
|
|
||||||
|--------------------|-----------------------------|-----------------------|
|
|
||||||
| `BOT_TOKEN` | Токен Telegram-бота | из `.env` |
|
|
||||||
| `ADMIN_PASSWORD` | Пароль для /admin | настраивается |
|
|
||||||
| `SERVICES` | Словарь услуг | массаж, SPA, косметология |
|
|
||||||
| `MASTERS` | Словарь мастеров | Анна, Мария, Алексей |
|
|
||||||
| `TIME_SLOTS` | Доступное время | 09:00–18:00, каждый час |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 🔐 Админ-панель
|
|
||||||
|
|
||||||
В чате с ботом: `/admin пароль`
|
|
||||||
|
|
||||||
Админ видит все бронирования и может отменять любые.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 🗄 Хранилище
|
|
||||||
|
|
||||||
Данные хранятся в JSON-файлах в папке `data/`:
|
|
||||||
- `users.json` — пользователи
|
|
||||||
- `bookings.json` — бронирования
|
|
||||||
|
|
||||||
Файлы создаются автоматически при первом запуске.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 🤝 Разработка
|
|
||||||
|
|
||||||
1. Создаёшь ветку от `master`
|
|
||||||
2. Вносишь изменения
|
|
||||||
3. Открываешь Pull Request в `master`
|
|
||||||
|
|
||||||
```bash
|
|
||||||
git checkout -b feat/my-feature
|
|
||||||
# ... работа ...
|
|
||||||
git push origin feat/my-feature
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 📄 Лицензия
|
|
||||||
|
|
||||||
MIT — делайте что хотите.
|
|
||||||
@@ -6,7 +6,6 @@ load_dotenv()
|
|||||||
|
|
||||||
BOT_TOKEN = os.getenv("BOT_TOKEN")
|
BOT_TOKEN = os.getenv("BOT_TOKEN")
|
||||||
ADMIN_PASSWORD = os.getenv("ADMIN_PASSWORD")
|
ADMIN_PASSWORD = os.getenv("ADMIN_PASSWORD")
|
||||||
ADMIN_CHAT_ID = int(os.getenv("ADMIN_CHAT_ID", "0")) if os.getenv("ADMIN_CHAT_ID", "") else 0
|
|
||||||
|
|
||||||
if not BOT_TOKEN:
|
if not BOT_TOKEN:
|
||||||
raise ValueError("BOT_TOKEN not found in environment")
|
raise ValueError("BOT_TOKEN not found in environment")
|
||||||
@@ -19,7 +18,7 @@ USE_POSTGRES = bool(os.getenv("DB_HOST", ""))
|
|||||||
SERVICES = {
|
SERVICES = {
|
||||||
"service_massage": {
|
"service_massage": {
|
||||||
"key": "service_massage",
|
"key": "service_massage",
|
||||||
"name": "\u041c\u0430\u0441\u0441\u0430\u0436",
|
"name": "\U0001f4aa \u041c\u0430\u0441\u0441\u0430\u0436",
|
||||||
"emoji": "\U0001f4aa",
|
"emoji": "\U0001f4aa",
|
||||||
"desc": "\u0420\u0430\u0441\u0441\u043b\u0430\u0431\u043b\u044f\u044e\u0449\u0438\u0439 \u0438 \u043b\u0435\u0447\u0435\u0431\u043d\u044b\u0439 \u043c\u0430\u0441\u0441\u0430\u0436.\n\u0421\u043d\u0438\u043c\u0430\u0435\u0442 \u043d\u0430\u043f\u0440\u044f\u0436\u0435\u043d\u0438\u0435, \u0443\u043b\u0443\u0447\u0448\u0430\u0435\u0442 \u043a\u0440\u043e\u0432\u043e\u043e\u0431\u0440\u0430\u0449\u0435\u043d\u0438\u0435 \u0438 \u0434\u0430\u0440\u0438\u0442 \u043b\u0451\u0433\u043a\u043e\u0441\u0442\u044c \u0432\u043e \u0432\u0441\u0451\u043c \u0442\u0435\u043b\u0435.",
|
"desc": "\u0420\u0430\u0441\u0441\u043b\u0430\u0431\u043b\u044f\u044e\u0449\u0438\u0439 \u0438 \u043b\u0435\u0447\u0435\u0431\u043d\u044b\u0439 \u043c\u0430\u0441\u0441\u0430\u0436.\n\u0421\u043d\u0438\u043c\u0430\u0435\u0442 \u043d\u0430\u043f\u0440\u044f\u0436\u0435\u043d\u0438\u0435, \u0443\u043b\u0443\u0447\u0448\u0430\u0435\u0442 \u043a\u0440\u043e\u0432\u043e\u043e\u0431\u0440\u0430\u0449\u0435\u043d\u0438\u0435 \u0438 \u0434\u0430\u0440\u0438\u0442 \u043b\u0451\u0433\u043a\u043e\u0441\u0442\u044c \u0432\u043e \u0432\u0441\u0451\u043c \u0442\u0435\u043b\u0435.",
|
||||||
"image": "assets/massage.png",
|
"image": "assets/massage.png",
|
||||||
@@ -29,7 +28,7 @@ SERVICES = {
|
|||||||
},
|
},
|
||||||
"service_spa": {
|
"service_spa": {
|
||||||
"key": "service_spa",
|
"key": "service_spa",
|
||||||
"name": "SPA-\u043a\u043e\u043c\u043f\u043b\u0435\u043a\u0441",
|
"name": "\U0001f388 SPA-\u043a\u043e\u043c\u043f\u043b\u0435\u043a\u0441",
|
||||||
"emoji": "\U0001f388",
|
"emoji": "\U0001f388",
|
||||||
"desc": "\u041a\u043e\u043c\u043f\u043b\u0435\u043a\u0441\u043d\u044b\u0435 SPA-\u043f\u0440\u043e\u0446\u0435\u0434\u0443\u0440\u044b: \u043e\u0431\u0451\u0440\u0442\u044b\u0432\u0430\u043d\u0438\u044f, \u0430\u0440\u043e\u043c\u0430\u0442\u0435\u0440\u0430\u043f\u0438\u044f, \u0433\u0438\u0434\u0440\u043e\u043c\u0430\u0441\u0441\u0430\u0436.\n\u041f\u043e\u043b\u043d\u043e\u0435 \u0432\u043e\u0441\u0441\u0442\u0430\u043d\u043e\u0432\u043b\u0435\u043d\u0438\u0435 \u0438 \u0440\u0435\u043b\u0430\u043a\u0441\u0430\u0446\u0438\u044f.",
|
"desc": "\u041a\u043e\u043c\u043f\u043b\u0435\u043a\u0441\u043d\u044b\u0435 SPA-\u043f\u0440\u043e\u0446\u0435\u0434\u0443\u0440\u044b: \u043e\u0431\u0451\u0440\u0442\u044b\u0432\u0430\u043d\u0438\u044f, \u0430\u0440\u043e\u043c\u0430\u0442\u0435\u0440\u0430\u043f\u0438\u044f, \u0433\u0438\u0434\u0440\u043e\u043c\u0430\u0441\u0441\u0430\u0436.\n\u041f\u043e\u043b\u043d\u043e\u0435 \u0432\u043e\u0441\u0441\u0442\u0430\u043d\u043e\u0432\u043b\u0435\u043d\u0438\u0435 \u0438 \u0440\u0435\u043b\u0430\u043a\u0441\u0430\u0446\u0438\u044f.",
|
||||||
"image": "assets/spa.png",
|
"image": "assets/spa.png",
|
||||||
@@ -39,7 +38,7 @@ SERVICES = {
|
|||||||
},
|
},
|
||||||
"service_cosmetology": {
|
"service_cosmetology": {
|
||||||
"key": "service_cosmetology",
|
"key": "service_cosmetology",
|
||||||
"name": "\u041a\u043e\u0441\u043c\u0435\u0442\u043e\u043b\u043e\u0433\u0438\u044f",
|
"name": "\U0001f484 \u041a\u043e\u0441\u043c\u0435\u0442\u043e\u043b\u043e\u0433\u0438\u044f",
|
||||||
"emoji": "\U0001f484",
|
"emoji": "\U0001f484",
|
||||||
"desc": "\u041f\u0440\u043e\u0444\u0435\u0441\u0441\u0438\u043e\u043d\u0430\u043b\u044c\u043d\u044b\u0439 \u0443\u0445\u043e\u0434 \u0437\u0430 \u043b\u0438\u0446\u043e\u043c: \u0447\u0438\u0441\u0442\u043a\u0430, \u043f\u0438\u043b\u0438\u043d\u0433, \u043c\u0430\u0441\u043a\u0438, \u043b\u0438\u043c\u0444\u043e\u0434\u0440\u0435\u043d\u0430\u0436.\n\u0421\u0438\u044f\u044e\u0449\u0430\u044f \u0438 \u0437\u0434\u043e\u0440\u043e\u0432\u0430\u044f \u043a\u043e\u0436\u0430.",
|
"desc": "\u041f\u0440\u043e\u0444\u0435\u0441\u0441\u0438\u043e\u043d\u0430\u043b\u044c\u043d\u044b\u0439 \u0443\u0445\u043e\u0434 \u0437\u0430 \u043b\u0438\u0446\u043e\u043c: \u0447\u0438\u0441\u0442\u043a\u0430, \u043f\u0438\u043b\u0438\u043d\u0433, \u043c\u0430\u0441\u043a\u0438, \u043b\u0438\u043c\u0444\u043e\u0434\u0440\u0435\u043d\u0430\u0436.\n\u0421\u0438\u044f\u044e\u0449\u0430\u044f \u0438 \u0437\u0434\u043e\u0440\u043e\u0432\u0430\u044f \u043a\u043e\u0436\u0430.",
|
||||||
"image": "assets/cosmetology.png",
|
"image": "assets/cosmetology.png",
|
||||||
|
|||||||
@@ -6,24 +6,12 @@ from typing import Optional
|
|||||||
import asyncpg
|
import asyncpg
|
||||||
from app.logger import logger
|
from app.logger import logger
|
||||||
|
|
||||||
DB_HOST = os.getenv("DB_HOST", "10.0.77.9")
|
|
||||||
DB_PORT = int(os.getenv("DB_PORT", "5433"))
|
|
||||||
DB_USER = os.getenv("DB_USER", "spabot")
|
|
||||||
DB_PASSWORD = os.getenv("DB_PASSWORD")
|
|
||||||
DB_NAME = os.getenv("DB_NAME", "spabot")
|
|
||||||
|
|
||||||
if not DB_PASSWORD:
|
|
||||||
raise ValueError(
|
|
||||||
"DB_PASSWORD not set in environment. "
|
|
||||||
"Set it in .env or export DB_PASSWORD=your_password"
|
|
||||||
)
|
|
||||||
|
|
||||||
DB_CONFIG = {
|
DB_CONFIG = {
|
||||||
"host": DB_HOST,
|
"host": os.getenv("DB_HOST", "10.0.77.9"),
|
||||||
"port": DB_PORT,
|
"port": int(os.getenv("DB_PORT", "5432")),
|
||||||
"user": DB_USER,
|
"user": os.getenv("DB_USER", "spabot"),
|
||||||
"password": DB_PASSWORD,
|
"password": os.getenv("DB_PASSWORD", "spabot_password"),
|
||||||
"database": DB_NAME,
|
"database": os.getenv("DB_NAME", "spabot"),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ async def about(message: Message, state: FSMContext):
|
|||||||
await state.clear()
|
await state.clear()
|
||||||
logger.info("User {uid} opened About", uid=message.from_user.id)
|
logger.info("User {uid} opened About", uid=message.from_user.id)
|
||||||
text = (
|
text = (
|
||||||
"\u262f\ufe0f SPA-\u0441\u0430\u043b\u043e\u043d Demo\n\n"
|
"\u262f\ufe0f **SPA-\u0441\u0430\u043b\u043e\u043d Demo**\n\n"
|
||||||
"\u041c\u044b - \u0443\u044e\u0442\u043d\u044b\u0439 SPA-\u0441\u0430\u043b\u043e\u043d \u0432 \u0446\u0435\u043d\u0442\u0440\u0435 \u0433\u043e\u0440\u043e\u0434\u0430.\n\n"
|
"\u041c\u044b - \u0443\u044e\u0442\u043d\u044b\u0439 SPA-\u0441\u0430\u043b\u043e\u043d \u0432 \u0446\u0435\u043d\u0442\u0440\u0435 \u0433\u043e\u0440\u043e\u0434\u0430.\n\n"
|
||||||
"\U0001f4aa \u041f\u0440\u043e\u0444\u0435\u0441\u0441\u0438\u043e\u043d\u0430\u043b\u044c\u043d\u044b\u0435 \u043c\u0430\u0441\u0442\u0435\u0440\u0430\n"
|
"\U0001f4aa \u041f\u0440\u043e\u0444\u0435\u0441\u0441\u0438\u043e\u043d\u0430\u043b\u044c\u043d\u044b\u0435 \u043c\u0430\u0441\u0442\u0435\u0440\u0430\n"
|
||||||
"\U0001f9ec \u041d\u0430\u0442\u0443\u0440\u0430\u043b\u044c\u043d\u0430\u044f \u043a\u043e\u0441\u043c\u0435\u0442\u0438\u043a\u0430\n"
|
"\U0001f9ec \u041d\u0430\u0442\u0443\u0440\u0430\u043b\u044c\u043d\u0430\u044f \u043a\u043e\u0441\u043c\u0435\u0442\u0438\u043a\u0430\n"
|
||||||
|
|||||||
@@ -1,14 +1,12 @@
|
|||||||
"""Booking handler - full FSM flow for service reservation."""
|
|
||||||
import re
|
import re
|
||||||
|
|
||||||
from aiogram import Router, F
|
from aiogram import Router, F
|
||||||
from aiogram.fsm.context import FSMContext
|
from aiogram.fsm.context import FSMContext
|
||||||
from aiogram.filters import StateFilter
|
from aiogram.filters import StateFilter
|
||||||
from aiogram.types import Message, CallbackQuery, FSInputFile, InlineKeyboardMarkup, InlineKeyboardButton
|
from aiogram.types import Message, CallbackQuery
|
||||||
from app.keyboards.services import get_services_keyboard, get_extra_services_keyboard
|
from app.keyboards.services import get_services_keyboard, get_extra_services_keyboard
|
||||||
from app.keyboards.masters import get_masters_keyboard
|
from app.keyboards.masters import get_masters_keyboard
|
||||||
from app.states.booking import BookingState
|
from app.states.booking import BookingState
|
||||||
from app.config import SERVICES as ALL_SERVICES, ADMIN_CHAT_ID
|
|
||||||
from app.services.booking_service import (
|
from app.services.booking_service import (
|
||||||
create_booking,
|
create_booking,
|
||||||
update_booking,
|
update_booking,
|
||||||
@@ -34,17 +32,6 @@ def _services_to_str(data):
|
|||||||
return main
|
return main
|
||||||
|
|
||||||
|
|
||||||
def _make_service_card(key):
|
|
||||||
"""Recreate the service card text for 'back' navigation from a photo card."""
|
|
||||||
svc = ALL_SERVICES[key]
|
|
||||||
return (
|
|
||||||
svc["emoji"] + " " + svc["name"] + "\n\n"
|
|
||||||
+ svc["desc"] + "\n\n"
|
|
||||||
+ "\u23f1 Продолжительность: " + str(svc["duration"]) + " мин\n"
|
|
||||||
+ "\U0001f4b0 Цена: " + str(svc["price"]) + "\u20bd"
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
# отмена записи на любом шаге
|
# отмена записи на любом шаге
|
||||||
@router.message(F.text.lower() == "отмена")
|
@router.message(F.text.lower() == "отмена")
|
||||||
async def cancel(message: Message, state: FSMContext):
|
async def cancel(message: Message, state: FSMContext):
|
||||||
@@ -66,14 +53,15 @@ async def start_booking(message: Message, state: FSMContext):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
# выбор первой услуги (через кнопку "📅 Записаться")
|
# выбор первой услуги
|
||||||
@router.callback_query(
|
@router.callback_query(
|
||||||
StateFilter(BookingState.service),
|
StateFilter(BookingState.service),
|
||||||
F.data.startswith("service_")
|
F.data.startswith("service_")
|
||||||
)
|
)
|
||||||
async def choose_service(callback: CallbackQuery, state: FSMContext):
|
async def choose_service(callback: CallbackQuery, state: FSMContext):
|
||||||
svc_data = ALL_SERVICES[callback.data]
|
from app.config import SERVICES
|
||||||
service_name = svc_data["name"] if isinstance(svc_data, dict) else svc_data
|
|
||||||
|
service_name = SERVICES[callback.data]
|
||||||
logger.debug("User {uid} selected first service: {srv}", uid=callback.from_user.id, srv=service_name)
|
logger.debug("User {uid} selected first service: {srv}", uid=callback.from_user.id, srv=service_name)
|
||||||
|
|
||||||
await state.update_data(service=service_name, extra_services=[])
|
await state.update_data(service=service_name, extra_services=[])
|
||||||
@@ -94,7 +82,10 @@ async def choose_service(callback: CallbackQuery, state: FSMContext):
|
|||||||
F.data.startswith("extra_")
|
F.data.startswith("extra_")
|
||||||
)
|
)
|
||||||
async def choose_extra_service(callback: CallbackQuery, state: FSMContext):
|
async def choose_extra_service(callback: CallbackQuery, state: FSMContext):
|
||||||
|
from app.config import SERVICES
|
||||||
|
|
||||||
if callback.data == "extra_done":
|
if callback.data == "extra_done":
|
||||||
|
# Go to master selection
|
||||||
await state.set_state(BookingState.master)
|
await state.set_state(BookingState.master)
|
||||||
await callback.message.edit_text(
|
await callback.message.edit_text(
|
||||||
"👤 Выберите специалиста:",
|
"👤 Выберите специалиста:",
|
||||||
@@ -103,12 +94,12 @@ async def choose_extra_service(callback: CallbackQuery, state: FSMContext):
|
|||||||
await callback.answer()
|
await callback.answer()
|
||||||
return
|
return
|
||||||
|
|
||||||
|
# Toggle extra service
|
||||||
actual_key = callback.data.replace("extra_", "", 1)
|
actual_key = callback.data.replace("extra_", "", 1)
|
||||||
svc_data = ALL_SERVICES.get(actual_key)
|
service_name = SERVICES.get(actual_key)
|
||||||
if not svc_data:
|
if not service_name:
|
||||||
await callback.answer("Услуга не найдена")
|
await callback.answer("Услуга не найдена")
|
||||||
return
|
return
|
||||||
service_name = svc_data["name"] if isinstance(svc_data, dict) else svc_data
|
|
||||||
|
|
||||||
data = await state.get_data()
|
data = await state.get_data()
|
||||||
extra = data.get("extra_services", [])
|
extra = data.get("extra_services", [])
|
||||||
@@ -142,8 +133,7 @@ async def choose_extra_service(callback: CallbackQuery, state: FSMContext):
|
|||||||
async def choose_master(callback: CallbackQuery, state: FSMContext):
|
async def choose_master(callback: CallbackQuery, state: FSMContext):
|
||||||
from app.config import MASTERS
|
from app.config import MASTERS
|
||||||
|
|
||||||
master_data = MASTERS[callback.data]
|
master_name = MASTERS[callback.data]
|
||||||
master_name = master_data["name"] if isinstance(master_data, dict) else master_data
|
|
||||||
logger.debug("User {uid} selected master: {m}", uid=callback.from_user.id, m=master_name)
|
logger.debug("User {uid} selected master: {m}", uid=callback.from_user.id, m=master_name)
|
||||||
|
|
||||||
await state.update_data(master=master_name)
|
await state.update_data(master=master_name)
|
||||||
@@ -232,14 +222,6 @@ async def confirm_booking_callback(callback: CallbackQuery, state: FSMContext):
|
|||||||
date = data.get("date", "?")
|
date = data.get("date", "?")
|
||||||
time = data.get("time", "?")
|
time = data.get("time", "?")
|
||||||
|
|
||||||
from app.db.postgres import PostgresDB as _PG
|
|
||||||
from app.config import USE_POSTGRES as _UPG
|
|
||||||
if _UPG:
|
|
||||||
_u = await _PG.get_user(callback.from_user.id)
|
|
||||||
if not _u:
|
|
||||||
_n = callback.from_user.full_name or callback.from_user.username or str(callback.from_user.id)
|
|
||||||
await _PG.set_user(callback.from_user.id, _n)
|
|
||||||
|
|
||||||
if await is_slot_busy(master, date, time):
|
if await is_slot_busy(master, date, time):
|
||||||
logger.warning("Slot already busy: {mst} {dt} {tm}", mst=master, dt=date, tm=time)
|
logger.warning("Slot already busy: {mst} {dt} {tm}", mst=master, dt=date, tm=time)
|
||||||
await callback.message.edit_text("❌ Это время уже занято. Пожалуйста, выберите другое время.")
|
await callback.message.edit_text("❌ Это время уже занято. Пожалуйста, выберите другое время.")
|
||||||
@@ -258,26 +240,6 @@ async def confirm_booking_callback(callback: CallbackQuery, state: FSMContext):
|
|||||||
cleanup_cancelled()
|
cleanup_cancelled()
|
||||||
result_text = "✅ Запись успешно создана"
|
result_text = "✅ Запись успешно создана"
|
||||||
|
|
||||||
# Notify admin about new booking
|
|
||||||
try:
|
|
||||||
if ADMIN_CHAT_ID:
|
|
||||||
from aiogram import Bot
|
|
||||||
from app.config import BOT_TOKEN
|
|
||||||
admin_bot = Bot(token=BOT_TOKEN)
|
|
||||||
user_str = callback.from_user.full_name or callback.from_user.username or str(callback.from_user.id)
|
|
||||||
await admin_bot.send_message(
|
|
||||||
ADMIN_CHAT_ID,
|
|
||||||
"🆕 Новая запись!\n\n"
|
|
||||||
f"👤 Клиент: {user_str} (id={callback.from_user.id})\n"
|
|
||||||
f"💆 Услуги: {services_str}\n"
|
|
||||||
f"👤 Мастер: {master}\n"
|
|
||||||
f"📅 Дата: {date}\n"
|
|
||||||
f"🕒 Время: {time}"
|
|
||||||
)
|
|
||||||
await admin_bot.session.close()
|
|
||||||
except Exception as e:
|
|
||||||
logger.warning("Failed to notify admin: {error}", error=e)
|
|
||||||
|
|
||||||
await state.clear()
|
await state.clear()
|
||||||
await callback.message.edit_text(result_text)
|
await callback.message.edit_text(result_text)
|
||||||
await callback.answer()
|
await callback.answer()
|
||||||
@@ -309,80 +271,24 @@ async def edit_booking_callback(callback: CallbackQuery, state: FSMContext):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
# прямая запись с карточки услуги (без предварительного выбора)
|
|
||||||
@router.callback_query(F.data.startswith("service_"))
|
|
||||||
async def quick_book_service(callback: CallbackQuery, state: FSMContext):
|
|
||||||
svc_data = ALL_SERVICES[callback.data]
|
|
||||||
service_name = svc_data["name"] if isinstance(svc_data, dict) else svc_data
|
|
||||||
svc_key = callback.data
|
|
||||||
logger.info("User {uid} quick book from card: {srv}", uid=callback.from_user.id, srv=service_name)
|
|
||||||
|
|
||||||
await state.clear()
|
|
||||||
await state.update_data(
|
|
||||||
service=service_name,
|
|
||||||
extra_services=[],
|
|
||||||
_from_card=True,
|
|
||||||
_card_key=svc_key
|
|
||||||
)
|
|
||||||
await state.set_state(BookingState.extra_services)
|
|
||||||
|
|
||||||
text = (
|
|
||||||
"✅ Выбрано: " + service_name + "\n\n"
|
|
||||||
"Хотите добавить ещё услугу?\n"
|
|
||||||
"Нажмите нужную или «Готово»:"
|
|
||||||
)
|
|
||||||
|
|
||||||
# Карточка с фото — удаляем и отправляем новое сообщение
|
|
||||||
try:
|
|
||||||
await callback.message.delete()
|
|
||||||
await callback.message.answer(text, reply_markup=get_extra_services_keyboard([]))
|
|
||||||
except Exception:
|
|
||||||
await callback.message.edit_text(text, reply_markup=get_extra_services_keyboard([]))
|
|
||||||
|
|
||||||
await callback.answer()
|
|
||||||
|
|
||||||
|
|
||||||
@router.callback_query(F.data == "go_back")
|
@router.callback_query(F.data == "go_back")
|
||||||
async def go_back_handler(callback: CallbackQuery, state: FSMContext):
|
async def go_back_handler(callback: CallbackQuery, state: FSMContext):
|
||||||
current_state = await state.get_state()
|
current_state = await state.get_state()
|
||||||
|
|
||||||
if current_state == BookingState.extra_services:
|
if current_state == BookingState.extra_services:
|
||||||
# Назад к списку услуг — проверяем, с карточки пришёл или из меню
|
# Back to first service
|
||||||
await state.set_state(BookingState.service)
|
await state.set_state(BookingState.service)
|
||||||
data = await state.get_data()
|
data = await state.get_data()
|
||||||
data.pop("extra_services", None)
|
data.pop("extra_services", None)
|
||||||
data.pop("service", None)
|
data.pop("service", None)
|
||||||
from_card = data.pop("_from_card", False)
|
|
||||||
card_key = data.pop("_card_key", None)
|
|
||||||
await state.set_data(data)
|
await state.set_data(data)
|
||||||
|
|
||||||
if from_card and card_key and card_key in ALL_SERVICES:
|
|
||||||
# Возвращаем карточку с фото
|
|
||||||
svc = ALL_SERVICES[card_key]
|
|
||||||
card_text = _make_service_card(card_key)
|
|
||||||
book_btn = InlineKeyboardMarkup(inline_keyboard=[[
|
|
||||||
InlineKeyboardButton(text="\U0001f4c5 Записаться", callback_data=card_key)
|
|
||||||
]])
|
|
||||||
try:
|
|
||||||
img = FSInputFile(svc["image"])
|
|
||||||
await callback.message.answer_photo(photo=img, caption=card_text, reply_markup=book_btn)
|
|
||||||
try:
|
|
||||||
await callback.message.delete()
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
except Exception:
|
|
||||||
await callback.message.edit_text(
|
|
||||||
"Выберите услугу:",
|
|
||||||
reply_markup=get_services_keyboard()
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
await callback.message.edit_text(
|
await callback.message.edit_text(
|
||||||
"Выберите услугу:",
|
"Выберите услугу:",
|
||||||
reply_markup=get_services_keyboard()
|
reply_markup=get_services_keyboard()
|
||||||
)
|
)
|
||||||
|
|
||||||
elif current_state == BookingState.master:
|
elif current_state == BookingState.master:
|
||||||
# Назад к доп. услугам
|
# Back to extra services
|
||||||
await state.set_state(BookingState.extra_services)
|
await state.set_state(BookingState.extra_services)
|
||||||
data = await state.get_data()
|
data = await state.get_data()
|
||||||
extra = data.get("extra_services", [])
|
extra = data.get("extra_services", [])
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ async def contacts(message: Message, state: FSMContext):
|
|||||||
await state.clear()
|
await state.clear()
|
||||||
logger.info("User {uid} requested contacts", uid=message.from_user.id)
|
logger.info("User {uid} requested contacts", uid=message.from_user.id)
|
||||||
text = (
|
text = (
|
||||||
"\U0001f4de \u041a\u043e\u043d\u0442\u0430\u043a\u0442\u044b\n\n"
|
"\U0001f4de **\u041a\u043e\u043d\u0442\u0430\u043a\u0442\u044b**\n\n"
|
||||||
"\U0001f4cd \u0433. \u041c\u043e\u0441\u043a\u0432\u0430, \u0443\u043b. \u0426\u0432\u0435\u0442\u043e\u0447\u043d\u0430\u044f, 15\n\n"
|
"\U0001f4cd \u0433. \u041c\u043e\u0441\u043a\u0432\u0430, \u0443\u043b. \u0426\u0432\u0435\u0442\u043e\u0447\u043d\u0430\u044f, 15\n\n"
|
||||||
"\U0001f4de +7 (999) 123-45-67\n\n"
|
"\U0001f4de +7 (999) 123-45-67\n\n"
|
||||||
"\U0001f4e7 info@spa-demo.ru\n\n"
|
"\U0001f4e7 info@spa-demo.ru\n\n"
|
||||||
|
|||||||
@@ -21,36 +21,24 @@ async def my_bookings(message: Message):
|
|||||||
logger.debug("Found {n} bookings", n=len(bookings))
|
logger.debug("Found {n} bookings", n=len(bookings))
|
||||||
|
|
||||||
if not bookings:
|
if not bookings:
|
||||||
await message.answer("У вас нет активных записей")
|
await message.answer("\u0423 \u0432\u0430\u0441 \u043d\u0435\u0442 \u0430\u043a\u0442\u0438\u0432\u043d\u044b\u0445 \u0437\u0430\u043f\u0438\u0441\u0435\u0439")
|
||||||
return
|
return
|
||||||
|
|
||||||
# Group all bookings into one message
|
for booking in bookings:
|
||||||
lines = []
|
svc = booking["service"]
|
||||||
for b in bookings:
|
mst = booking["master"]
|
||||||
bid = b["id"]
|
dat = booking["date"]
|
||||||
svc = b["service"]
|
tim = booking["time"]
|
||||||
mst = b["master"]
|
text = (
|
||||||
dat = b["date"]
|
"\U0001f486 " + svc + "\n"
|
||||||
tim = b["time"]
|
"\U0001f464 " + mst + "\n"
|
||||||
lines.append(
|
"\U0001f4c5 " + dat + "\n"
|
||||||
f"#{bid} \U0001f486 {svc}\n"
|
"\U0001f552 " + tim
|
||||||
f"\U0001f464 {mst}\n"
|
|
||||||
f"\U0001f4c5 {dat}\U0001f552 {tim}"
|
|
||||||
)
|
)
|
||||||
|
await message.answer(text, reply_markup=get_booking_actions(booking["id"]))
|
||||||
full_text = "\u0423 \u0432\u0430\u0441 " + str(len(bookings)) + " \u0437\u0430\u043f\u0438\u0441\u0438:\n\n" + "\n\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n".join(lines)
|
|
||||||
await message.answer(full_text)
|
|
||||||
|
|
||||||
# Send action buttons for each booking
|
|
||||||
for b in bookings:
|
|
||||||
await message.answer(
|
|
||||||
f"#{b['id']}: {b['service']}",
|
|
||||||
reply_markup=get_booking_actions(b["id"])
|
|
||||||
)
|
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.exception("Error in my_bookings: {error}", error=e)
|
logger.exception("Error in my_bookings: {error}", error=e)
|
||||||
await message.answer("Произошла ошибка. Попробуйте позже.")
|
await message.answer("\u041f\u0440\u043e\u0438\u0437\u043e\u0448\u043b\u0430 \u043e\u0448\u0438\u0431\u043a\u0430. \u041f\u043e\u043f\u0440\u043e\u0431\u0443\u0439\u0442\u0435 \u043f\u043e\u0437\u0436\u0435.")
|
||||||
|
|
||||||
|
|
||||||
@router.callback_query(F.data.startswith("cancel_booking_"))
|
@router.callback_query(F.data.startswith("cancel_booking_"))
|
||||||
@@ -58,7 +46,7 @@ async def cancel_booking_handler(callback: CallbackQuery):
|
|||||||
booking_id = int(callback.data.split("_")[2])
|
booking_id = int(callback.data.split("_")[2])
|
||||||
logger.info("User {uid} cancelled booking #{bid}", uid=callback.from_user.id, bid=booking_id)
|
logger.info("User {uid} cancelled booking #{bid}", uid=callback.from_user.id, bid=booking_id)
|
||||||
await cancel_booking(booking_id)
|
await cancel_booking(booking_id)
|
||||||
await callback.message.edit_text("\u2705 Запись отменена")
|
await callback.message.edit_text("\u2705 \u0417\u0430\u043f\u0438\u0441\u044c \u043e\u0442\u043c\u0435\u043d\u0435\u043d\u0430")
|
||||||
await callback.answer()
|
await callback.answer()
|
||||||
|
|
||||||
|
|
||||||
@@ -75,7 +63,7 @@ async def edit_booking_handler(callback: CallbackQuery, state: FSMContext):
|
|||||||
booking = next((b for b in bookings if b["id"] == booking_id), None)
|
booking = next((b for b in bookings if b["id"] == booking_id), None)
|
||||||
|
|
||||||
if not booking:
|
if not booking:
|
||||||
await callback.message.edit_text("Запись не найдена")
|
await callback.message.edit_text("\u0417\u0430\u043f\u0438\u0441\u044c \u043d\u0435 \u043d\u0430\u0439\u0434\u0435\u043d\u0430")
|
||||||
await callback.answer()
|
await callback.answer()
|
||||||
return
|
return
|
||||||
|
|
||||||
@@ -89,7 +77,7 @@ async def edit_booking_handler(callback: CallbackQuery, state: FSMContext):
|
|||||||
|
|
||||||
await state.set_state(BookingState.service)
|
await state.set_state(BookingState.service)
|
||||||
await callback.message.edit_text(
|
await callback.message.edit_text(
|
||||||
"\u270f\ufe0f Редактирование записи #" + str(booking_id) + "\n\nВыберите новую услугу:",
|
"\u270f\ufe0f \u0420\u0435\u0434\u0430\u043a\u0442\u0438\u0440\u043e\u0432\u0430\u043d\u0438\u0435 \u0437\u0430\u043f\u0438\u0441\u0438 #" + str(booking_id) + "\n\n\u0412\u044b\u0431\u0435\u0440\u0438\u0442\u0435 \u043d\u043e\u0432\u0443\u044e \u0443\u0441\u043b\u0443\u0433\u0443:",
|
||||||
reply_markup=get_services_keyboard()
|
reply_markup=get_services_keyboard()
|
||||||
)
|
)
|
||||||
await callback.answer()
|
await callback.answer()
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
"""User profile handler - auto-registers if missing."""
|
"""User profile handler."""
|
||||||
from aiogram import Router, F
|
from aiogram import Router, F
|
||||||
from aiogram.types import Message
|
from aiogram.types import Message
|
||||||
from aiogram.fsm.context import FSMContext
|
from aiogram.fsm.context import FSMContext
|
||||||
@@ -10,25 +10,16 @@ from app.logger import logger
|
|||||||
router = Router()
|
router = Router()
|
||||||
|
|
||||||
|
|
||||||
@router.message(F.text == "\U0001f464 \u041f\u0440\u043e\u0444\u0438\u043b\u044c")
|
@router.message(F.text == "👤 Профиль")
|
||||||
async def profile(message: Message, state: FSMContext):
|
async def profile(message: Message, state: FSMContext):
|
||||||
await state.clear()
|
await state.clear()
|
||||||
user_id = message.from_user.id
|
|
||||||
name = message.from_user.full_name or "\u0413\u043e\u0441\u0442\u044c"
|
|
||||||
|
|
||||||
if USE_POSTGRES:
|
if USE_POSTGRES:
|
||||||
user = await PostgresDB.get_user(user_id)
|
user = await PostgresDB.get_user(message.from_user.id)
|
||||||
if not user:
|
|
||||||
logger.info("Auto-registering user {uid} in PG", uid=user_id)
|
|
||||||
await PostgresDB.set_user(user_id, name)
|
|
||||||
user = await PostgresDB.get_user(user_id)
|
|
||||||
user_name = user.get("name") or user.get("full_name", "\u0413\u043e\u0441\u0442\u044c")
|
|
||||||
else:
|
else:
|
||||||
from app.db.memory import get_user, set_user
|
from app.db.memory import get_user
|
||||||
user = get_user(user_id)
|
user = get_user(message.from_user.id)
|
||||||
if not user:
|
if not user:
|
||||||
set_user(user_id, {"name": name})
|
await message.answer("Профиль не найден")
|
||||||
user = get_user(user_id)
|
return
|
||||||
user_name = user.get("name", "\u0413\u043e\u0441\u0442\u044c")
|
name = user.get("name") or user.get("full_name", "?")
|
||||||
|
await message.answer(f"👤 Имя: {name}")
|
||||||
await message.answer(f"\U0001f464 \u041f\u0440\u043e\u0444\u0438\u043b\u044c\n\n\U0001f464 \u0418\u043c\u044f: {user_name}")
|
|
||||||
|
|||||||
@@ -1,10 +1,11 @@
|
|||||||
"""Services handler - show rich service cards with images."""
|
"""Services handler - show rich service cards with images."""
|
||||||
from aiogram import Router, F
|
from aiogram import Router, F
|
||||||
from aiogram.types import Message, FSInputFile, InlineKeyboardMarkup, InlineKeyboardButton, CallbackQuery
|
from aiogram.types import Message, FSInputFile
|
||||||
from aiogram.fsm.context import FSMContext
|
from aiogram.fsm.context import FSMContext
|
||||||
from aiogram import Bot
|
from aiogram import Bot
|
||||||
|
|
||||||
from app.config import SERVICES, BOT_TOKEN
|
from app.config import SERVICES, BOT_TOKEN
|
||||||
|
from app.keyboards.services import get_services_keyboard
|
||||||
from app.logger import logger
|
from app.logger import logger
|
||||||
|
|
||||||
router = Router()
|
router = Router()
|
||||||
@@ -24,18 +25,21 @@ async def show_services(message: Message, state: FSMContext):
|
|||||||
await state.clear()
|
await state.clear()
|
||||||
logger.info("User {uid} requested services list", uid=message.from_user.id)
|
logger.info("User {uid} requested services list", uid=message.from_user.id)
|
||||||
|
|
||||||
|
await message.answer(
|
||||||
|
"\U0001f4cb **\u041d\u0430\u0448\u0438 \u0443\u0441\u043b\u0443\u0433\u0438**\n\n"
|
||||||
|
"\u0412\u044b\u0431\u0435\u0440\u0438\u0442\u0435 \u0438\u043d\u0442\u0435\u0440\u0435\u0441\u0443\u044e\u0449\u0443\u044e \u0443\u0441\u043b\u0443\u0433\u0443:",
|
||||||
|
reply_markup=get_services_keyboard()
|
||||||
|
)
|
||||||
|
|
||||||
for key, svc in SERVICES.items():
|
for key, svc in SERVICES.items():
|
||||||
try:
|
try:
|
||||||
card = (
|
card = (
|
||||||
svc["emoji"] + " " + svc["name"] + "\n\n"
|
svc["emoji"] + " **" + svc["name"] + "**\n\n"
|
||||||
+ svc["desc"] + "\n\n"
|
+ svc["desc"] + "\n\n"
|
||||||
+ "\u23f1 \u0414\u043b\u0438\u0442\u0435\u043b\u044c\u043d\u043e\u0441\u0442\u044c: " + str(svc["duration"]) + " \u043c\u0438\u043d\n"
|
+ "\u23f1 \u0414\u043b\u0438\u0442\u0435\u043b\u044c\u043d\u043e\u0441\u0442\u044c: " + str(svc["duration"]) + " \u043c\u0438\u043d\n"
|
||||||
+ "\U0001f4b0 \u0426\u0435\u043d\u0430: " + str(svc["price"]) + "\u20bd"
|
+ "\U0001f4b0 \u0426\u0435\u043d\u0430: " + str(svc["price"]) + "\u20bd"
|
||||||
)
|
)
|
||||||
book_btn = InlineKeyboardMarkup(inline_keyboard=[[
|
|
||||||
InlineKeyboardButton(text="\U0001f4c5 \u0417\u0430\u043f\u0438\u0441\u0430\u0442\u044c\u0441\u044f", callback_data=key)
|
|
||||||
]])
|
|
||||||
img = FSInputFile(svc["image"])
|
img = FSInputFile(svc["image"])
|
||||||
await bot().send_photo(message.chat.id, photo=img, caption=card, reply_markup=book_btn)
|
await bot().send_photo(message.chat.id, photo=img, caption=card)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.warning("Error sending service card for {key}: {err}", key=key, err=e)
|
logger.warning("Error sending service card for {key}: {err}", key=key, err=e)
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
"""Start handler with welcome greeting and prices."""
|
"""Start handler with welcome greeting and prices."""
|
||||||
from aiogram import Router
|
from aiogram import Router
|
||||||
from aiogram.filters import CommandStart
|
from aiogram.filters import CommandStart
|
||||||
from aiogram.types import Message, FSInputFile
|
from aiogram.types import Message
|
||||||
|
|
||||||
from app.keyboards.menu import menu
|
from app.keyboards.menu import menu
|
||||||
from app.db.postgres import PostgresDB
|
from app.db.postgres import PostgresDB
|
||||||
@@ -25,14 +25,10 @@ async def start(message: Message):
|
|||||||
|
|
||||||
greet = (
|
greet = (
|
||||||
"\U0001f44b \u0414\u043e\u0431\u0440\u043e \u043f\u043e\u0436\u0430\u043b\u043e\u0432\u0430\u0442\u044c, " + name + "!\n\n"
|
"\U0001f44b \u0414\u043e\u0431\u0440\u043e \u043f\u043e\u0436\u0430\u043b\u043e\u0432\u0430\u0442\u044c, " + name + "!\n\n"
|
||||||
"\u262f\ufe0f SPA-\u0441\u0430\u043b\u043e\u043d Demo - \u0432\u0430\u0448 \u043e\u0441\u0442\u0440\u043e\u0432\u043e\u043a \u0440\u0435\u043b\u0430\u043a\u0441\u0430\u0446\u0438\u0438\n\n"
|
"\u262f\ufe0f **SPA-\u0441\u0430\u043b\u043e\u043d Demo** - \u0432\u0430\u0448 \u043e\u0441\u0442\u0440\u043e\u0432\u043e\u043a \u0440\u0435\u043b\u0430\u043a\u0441\u0430\u0446\u0438\u0438\n\n"
|
||||||
"\U0001f4aa \u041c\u0430\u0441\u0441\u0430\u0436 - \u043e\u0442 " + str(SERVICES["service_massage"]["price"]) + "\u20bd\n"
|
"\U0001f4aa \u041c\u0430\u0441\u0441\u0430\u0436 - \u043e\u0442 " + str(SERVICES["service_massage"]["price"]) + "\u20bd\n"
|
||||||
"\U0001f388 SPA-\u043a\u043e\u043c\u043f\u043b\u0435\u043a\u0441 - \u043e\u0442 " + str(SERVICES["service_spa"]["price"]) + "\u20bd\n"
|
"\U0001f388 SPA-\u043a\u043e\u043c\u043f\u043b\u0435\u043a\u0441 - \u043e\u0442 " + str(SERVICES["service_spa"]["price"]) + "\u20bd\n"
|
||||||
"\U0001f484 \u041a\u043e\u0441\u043c\u0435\u0442\u043e\u043b\u043e\u0433\u0438\u044f - \u043e\u0442 " + str(SERVICES["service_cosmetology"]["price"]) + "\u20bd\n\n"
|
"\U0001f484 \u041a\u043e\u0441\u043c\u0435\u0442\u043e\u043b\u043e\u0433\u0438\u044f - \u043e\u0442 " + str(SERVICES["service_cosmetology"]["price"]) + "\u20bd\n\n"
|
||||||
"\U0001f447 \u041d\u0430\u0436\u043c\u0438\u0442\u0435 \u00ab\u0417\u0430\u043f\u0438\u0441\u0430\u0442\u044c\u0441\u00bb \u0447\u0442\u043e\u0431\u044b \u043d\u0430\u0447\u0430\u0442\u044c"
|
"\U0001f447 \u041d\u0430\u0436\u043c\u0438\u0442\u0435 \u00ab\u0417\u0430\u043f\u0438\u0441\u0430\u0442\u044c\u0441\u00bb \u0447\u0442\u043e\u0431\u044b \u043d\u0430\u0447\u0430\u0442\u044c"
|
||||||
)
|
)
|
||||||
try:
|
|
||||||
img = FSInputFile("assets/welcome.png")
|
|
||||||
await message.answer_photo(photo=img, caption=greet, reply_markup=menu)
|
|
||||||
except Exception:
|
|
||||||
await message.answer(greet, reply_markup=menu)
|
await message.answer(greet, reply_markup=menu)
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ def get_extra_services_keyboard(already_selected=None):
|
|||||||
buttons = []
|
buttons = []
|
||||||
for key, svc in SERVICES.items():
|
for key, svc in SERVICES.items():
|
||||||
if svc["name"] in already_selected:
|
if svc["name"] in already_selected:
|
||||||
label = "\u2705 " + svc["emoji"] + " " + svc["name"]
|
label = "\u2705 " + svc["name"]
|
||||||
else:
|
else:
|
||||||
label = svc["emoji"] + " " + svc["name"]
|
label = svc["emoji"] + " " + svc["name"]
|
||||||
buttons.append([InlineKeyboardButton(text=label, callback_data="extra_" + key)])
|
buttons.append([InlineKeyboardButton(text=label, callback_data="extra_" + key)])
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ import signal
|
|||||||
import sys
|
import sys
|
||||||
|
|
||||||
from aiogram import Bot, Dispatcher
|
from aiogram import Bot, Dispatcher
|
||||||
from aiogram.fsm.storage.redis import RedisStorage, DefaultKeyBuilder
|
|
||||||
|
|
||||||
from app.config import BOT_TOKEN, USE_POSTGRES
|
from app.config import BOT_TOKEN, USE_POSTGRES
|
||||||
from app.logger import logger
|
from app.logger import logger
|
||||||
@@ -20,13 +19,7 @@ from app.handlers.contacts import router as contacts_router
|
|||||||
from app.handlers.admin import router as admin_router
|
from app.handlers.admin import router as admin_router
|
||||||
|
|
||||||
bot = Bot(token=BOT_TOKEN)
|
bot = Bot(token=BOT_TOKEN)
|
||||||
|
dp = Dispatcher()
|
||||||
# Redis FSM storage — survives bot restarts, persistent across polling sessions
|
|
||||||
redis_storage = RedisStorage.from_url(
|
|
||||||
"redis://localhost:6379/0",
|
|
||||||
key_builder=DefaultKeyBuilder(with_destiny=True)
|
|
||||||
)
|
|
||||||
dp = Dispatcher(storage=redis_storage)
|
|
||||||
|
|
||||||
# Register routers
|
# Register routers
|
||||||
dp.include_routers(
|
dp.include_routers(
|
||||||
@@ -83,9 +76,6 @@ async def on_shutdown():
|
|||||||
if _health_runner:
|
if _health_runner:
|
||||||
await _health_runner.cleanup()
|
await _health_runner.cleanup()
|
||||||
logger.info("Healthcheck server stopped")
|
logger.info("Healthcheck server stopped")
|
||||||
if redis_storage:
|
|
||||||
await redis_storage.close()
|
|
||||||
logger.info("Redis storage closed")
|
|
||||||
pending = asyncio.all_tasks()
|
pending = asyncio.all_tasks()
|
||||||
if pending:
|
if pending:
|
||||||
logger.debug("Cancelling {count} pending tasks", count=len(pending))
|
logger.debug("Cancelling {count} pending tasks", count=len(pending))
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 155 KiB After Width: | Height: | Size: 180 B |
|
Before Width: | Height: | Size: 143 KiB After Width: | Height: | Size: 179 B |
|
Before Width: | Height: | Size: 157 KiB After Width: | Height: | Size: 179 B |
|
Before Width: | Height: | Size: 156 KiB |