459 lines
16 KiB
Markdown
459 lines
16 KiB
Markdown
```markdown
|
||
# Telegram Bot "Что почитать" - Техническое задание
|
||
|
||
## Описание проекта
|
||
Telegram-бот для просмотра книг с описаниями, обложками и рейтингами. Бот является интеграцией к существующему веб-сервису на Django с общей PostgreSQL базой данных.
|
||
|
||
## Технологический стек
|
||
- **Python 3.10+**
|
||
- **aiogram 3.x** - асинхронный фреймворк для Telegram Bot API [web:1][web:3]
|
||
- **asyncpg** или **psycopg2** - для работы с PostgreSQL [web:6]
|
||
- **SQLAlchemy 2.0** (асинхронный режим) - ORM для работы с БД
|
||
- **python-dotenv** - для хранения конфигурации
|
||
|
||
## Структура проекта
|
||
```
|
||
|
||
telegram_bot/
|
||
├── .env \# Переменные окружения
|
||
├── .env.example \# Пример конфигурации
|
||
├── requirements.txt \# Зависимости
|
||
├── main.py \# Точка входа
|
||
├── config.py \# Конфигурация
|
||
├── database/
|
||
│ ├── __init__.py
|
||
│ ├── connection.py \# Подключение к БД
|
||
│ └── models.py \# SQLAlchemy модели
|
||
├── handlers/
|
||
│ ├── __init__.py
|
||
│ ├── auth.py \# Авторизация/регистрация
|
||
│ ├── books.py \# Просмотр книг
|
||
│ └── favorites.py \# Избранное
|
||
├── keyboards/
|
||
│ ├── __init__.py
|
||
│ └── inline.py \# Inline-клавиатуры[^1][^2]
|
||
├── middlewares/
|
||
│ ├── __init__.py
|
||
│ └── auth.py \# Проверка авторизации
|
||
└── utils/
|
||
├── __init__.py
|
||
└── states.py \# FSM состояния
|
||
|
||
```
|
||
|
||
## Схема базы данных (уже существует)
|
||
|
||
### Таблица: users (из Django)
|
||
- `id` - PRIMARY KEY
|
||
- `username` - уникальное имя пользователя
|
||
- `email` - уникальный email (используется для входа)
|
||
- `password` - хешированный пароль
|
||
- `telegram_id` - BigInteger, уникальный, nullable
|
||
- `first_name`, `last_name` - имя и фамилия
|
||
- остальные поля Django User
|
||
|
||
### Таблица: books_book
|
||
- `id` - PRIMARY KEY
|
||
- `title` - название книги (max 500)
|
||
- `description` - описание (TEXT)
|
||
- `cover_url` - ссылка на обложку (URL)
|
||
- `average_rating` - средний рейтинг (Decimal 0.00-5.00)
|
||
- `rating_count` - количество оценок
|
||
- `language` - язык книги
|
||
- `created_at`, `updated_at` - временные метки
|
||
|
||
### Таблица: books_author
|
||
- `id` - PRIMARY KEY
|
||
- `name` - имя автора
|
||
|
||
### Таблица: books_genre
|
||
- `id` - PRIMARY KEY
|
||
- `name` - название жанра
|
||
|
||
### Таблица: books_book_authors (ManyToMany)
|
||
- `book_id` - FK на books_book
|
||
- `author_id` - FK на books_author
|
||
|
||
### Таблица: books_book_genres (ManyToMany)
|
||
- `book_id` - FK на books_book
|
||
- `genre_id` - FK на books_genre
|
||
|
||
### Таблица: books_favorite
|
||
- `id` - PRIMARY KEY
|
||
- `user_id` - FK на users
|
||
- `book_id` - FK на books_book
|
||
- `created_at` - дата добавления
|
||
- UNIQUE(user_id, book_id)
|
||
|
||
## Функциональные требования
|
||
|
||
### 1. Авторизация и регистрация
|
||
**Команда:** `/start`
|
||
|
||
**Логика:**
|
||
- Проверить, есть ли у пользователя `telegram_id` в таблице `users`
|
||
- Если да → авторизован, показать главное меню
|
||
- Если нет → показать кнопки "Войти" и "Зарегистрироваться"
|
||
|
||
**Регистрация:**
|
||
1. Кнопка "Зарегистрироваться" → запросить email
|
||
2. Проверить уникальность email в БД
|
||
3. Запросить username (проверить уникальность)
|
||
4. Запросить пароль (минимум 8 символов)
|
||
5. Создать запись в таблице `users` с `telegram_id = message.from_user.id`
|
||
6. Хешировать пароль через `bcrypt` или `django.contrib.auth.hashers`
|
||
|
||
**Вход:**
|
||
1. Кнопка "Войти" → запросить email
|
||
2. Запросить пароль
|
||
3. Проверить credentials в БД
|
||
4. Если успешно → обновить `telegram_id` для этого пользователя
|
||
5. Показать главное меню
|
||
|
||
### 2. Главное меню (после авторизации)
|
||
Inline-клавиатура [web:7]:
|
||
- 📚 Все книги
|
||
- ⭐ Избранное
|
||
- 🔍 Поиск по жанру
|
||
- 👤 Мой профиль
|
||
|
||
### 3. Просмотр всех книг
|
||
**Запрос к БД:**
|
||
```
|
||
|
||
SELECT
|
||
b.id, b.title, b.description, b.cover_url,
|
||
b.average_rating, b.rating_count,
|
||
STRING_AGG(DISTINCT a.name, ', ') as authors,
|
||
STRING_AGG(DISTINCT g.name, ', ') as genres
|
||
FROM books_book b
|
||
LEFT JOIN books_book_authors ba ON b.id = ba.book_id
|
||
LEFT JOIN books_author a ON ba.author_id = a.id
|
||
LEFT JOIN books_book_genres bg ON b.id = bg.book_id
|
||
LEFT JOIN books_genre g ON bg.genre_id = g.id
|
||
WHERE b.deleted_at IS NULL
|
||
GROUP BY b.id
|
||
ORDER BY b.created_at DESC
|
||
LIMIT 10 OFFSET ?
|
||
|
||
```
|
||
|
||
**Формат вывода:**
|
||
```
|
||
|
||
📖 [Название книги]
|
||
|
||
✍️ Авторы: [список через запятую]
|
||
🏷 Жанры: [список через запятую]
|
||
⭐ Рейтинг: X.XX (Y оценок)
|
||
|
||
[Описание книги, максимум 200 символов]
|
||
|
||
[Картинка по cover_url]
|
||
|
||
Кнопки: [❤️ В избранное] [➡️ След. страница]
|
||
|
||
```
|
||
|
||
**Пагинация:** по 10 книг на страницу, кнопки "⬅️ Назад" / "➡️ Вперед"
|
||
|
||
### 4. Избранное
|
||
**Запрос к БД:**
|
||
```
|
||
|
||
SELECT
|
||
b.id, b.title, b.cover_url, b.average_rating,
|
||
STRING_AGG(DISTINCT a.name, ', ') as authors
|
||
FROM books_favorite f
|
||
JOIN books_book b ON f.book_id = b.id
|
||
LEFT JOIN books_book_authors ba ON b.id = ba.book_id
|
||
LEFT JOIN books_author a ON ba.author_id = a.id
|
||
WHERE f.user_id = ? AND b.deleted_at IS NULL
|
||
GROUP BY b.id, f.created_at
|
||
ORDER BY f.created_at DESC
|
||
|
||
```
|
||
|
||
**Действия:**
|
||
- Показать список избранных книг (формат как в п.3)
|
||
- Кнопка "❌ Удалить из избранного" → DELETE FROM books_favorite
|
||
|
||
### 5. Добавление в избранное
|
||
**Callback:** `add_fav:{book_id}`
|
||
|
||
**Запрос:**
|
||
```
|
||
|
||
INSERT INTO books_favorite (user_id, book_id, created_at)
|
||
VALUES (?, ?, NOW())
|
||
ON CONFLICT (user_id, book_id) DO NOTHING
|
||
|
||
```
|
||
|
||
**Ответ:** "✅ Книга добавлена в избранное!"
|
||
|
||
### 6. Поиск по жанру
|
||
1. Получить список жанров: `SELECT id, name FROM books_genre ORDER BY name`
|
||
2. Показать inline-клавиатуру с жанрами (по 2 в строке) [web:7]
|
||
3. После выбора жанра → фильтровать книги:
|
||
```
|
||
|
||
SELECT DISTINCT b.* FROM books_book b
|
||
JOIN books_book_genres bg ON b.id = bg.book_id
|
||
WHERE bg.genre_id = ? AND b.deleted_at IS NULL
|
||
|
||
```
|
||
|
||
## Технические требования
|
||
|
||
### 1. Подключение к базе данных
|
||
**Файл:** `database/connection.py`
|
||
|
||
```
|
||
|
||
from sqlalchemy.ext.asyncio import create_async_engine, AsyncSession, async_sessionmaker
|
||
from config import DATABASE_URL
|
||
|
||
engine = create_async_engine(DATABASE_URL, echo=False)
|
||
async_session = async_sessionmaker(engine, class_=AsyncSession, expire_on_commit=False)
|
||
|
||
async def get_session():
|
||
async with async_session() as session:
|
||
yield session
|
||
|
||
```
|
||
|
||
**Конфигурация .env:**
|
||
```
|
||
|
||
BOT_TOKEN=your_bot_token_here
|
||
DATABASE_URL=postgresql+asyncpg://user:password@host:port/dbname
|
||
|
||
```
|
||
|
||
### 2. Middleware для проверки авторизации [web:3]
|
||
**Файл:** `middlewares/auth.py`
|
||
|
||
```
|
||
|
||
from aiogram import BaseMiddleware
|
||
from aiogram.types import Message, CallbackQuery
|
||
|
||
class AuthMiddleware(BaseMiddleware):
|
||
async def __call__(self, handler, event, data):
|
||
\# Пропускать команду /start
|
||
if isinstance(event, Message) and event.text == '/start':
|
||
return await handler(event, data)
|
||
|
||
# Проверить telegram_id в БД
|
||
user = await get_user_by_telegram_id(event.from_user.id)
|
||
if not user:
|
||
await event.answer("⛔ Сначала авторизуйтесь через /start")
|
||
return
|
||
|
||
data['user'] = user
|
||
return await handler(event, data)
|
||
```
|
||
|
||
### 3. FSM для авторизации
|
||
**Файл:** `utils/states.py`
|
||
|
||
```
|
||
|
||
from aiogram.fsm.state import State, StatesGroup
|
||
|
||
class RegistrationStates(StatesGroup):
|
||
waiting_email = State()
|
||
waiting_username = State()
|
||
waiting_password = State()
|
||
|
||
class LoginStates(StatesGroup):
|
||
waiting_email = State()
|
||
waiting_password = State()
|
||
|
||
```
|
||
|
||
### 4. Inline-клавиатуры [web:7][web:10]
|
||
**Файл:** `keyboards/inline.py`
|
||
|
||
```
|
||
|
||
from aiogram.utils.keyboard import InlineKeyboardBuilder
|
||
from aiogram.types import InlineKeyboardButton
|
||
|
||
def get_main_menu():
|
||
builder = InlineKeyboardBuilder()
|
||
builder.button(text="📚 Все книги", callback_data="books_all")
|
||
builder.button(text="⭐ Избранное", callback_data="favorites")
|
||
builder.button(text="🔍 Поиск по жанру", callback_data="search_genre")
|
||
builder.button(text="👤 Мой профиль", callback_data="profile")
|
||
builder.adjust(2, 2) \# 2 кнопки в ряду
|
||
return builder.as_markup()
|
||
|
||
```
|
||
|
||
### 5. Обработка картинок
|
||
Использовать `bot.send_photo()` с параметром `photo=cover_url` (если URL публичный).
|
||
|
||
Если картинка не загружается → показать текстовую заглушку: "🖼 [Обложка недоступна]"
|
||
|
||
## Требования к коду
|
||
|
||
1. **Асинхронность:** все функции БД и хендлеры должны быть `async/await` [web:3]
|
||
2. **Обработка ошибок:** try/except для всех запросов к БД
|
||
3. **Логирование:** использовать `logging` для отладки
|
||
4. **Типизация:** использовать type hints везде
|
||
5. **Документация:** docstrings для всех функций
|
||
6. **Безопасность:**
|
||
- Хешировать пароли через `bcrypt`
|
||
- Не логировать пароли
|
||
- SQL-инъекции предотвращаются через SQLAlchemy
|
||
7. **Простота:** избегать избыточных абстракций, код должен быть понятным
|
||
|
||
## Примеры запросов SQLAlchemy
|
||
|
||
**Получить пользователя:**
|
||
```
|
||
|
||
from sqlalchemy import select
|
||
from database.models import User
|
||
|
||
async def get_user_by_telegram_id(telegram_id: int):
|
||
async with async_session() as session:
|
||
result = await session.execute(
|
||
select(User).where(User.telegram_id == telegram_id)
|
||
)
|
||
return result.scalar_one_or_none()
|
||
|
||
```
|
||
|
||
**Получить книги с авторами (raw SQL через SQLAlchemy):**
|
||
```
|
||
|
||
from sqlalchemy import text
|
||
|
||
async def get_books_with_details(offset: int = 0, limit: int = 10):
|
||
async with async_session() as session:
|
||
query = text("""
|
||
SELECT b.id, b.title, b.description, b.cover_url,
|
||
b.average_rating, b.rating_count,
|
||
STRING_AGG(DISTINCT a.name, ', ') as authors,
|
||
STRING_AGG(DISTINCT g.name, ', ') as genres
|
||
FROM books_book b
|
||
LEFT JOIN books_book_authors ba ON b.id = ba.book_id
|
||
LEFT JOIN books_author a ON ba.author_id = a.id
|
||
LEFT JOIN books_book_genres bg ON b.id = bg.book_id
|
||
LEFT JOIN books_genre g ON bg.genre_id = g.id
|
||
WHERE b.deleted_at IS NULL
|
||
GROUP BY b.id
|
||
ORDER BY b.created_at DESC
|
||
LIMIT :limit OFFSET :offset
|
||
""")
|
||
result = await session.execute(query, {"limit": limit, "offset": offset})
|
||
return result.mappings().all()
|
||
|
||
```
|
||
|
||
## Зависимости (requirements.txt)
|
||
```
|
||
|
||
aiogram>=3.4.0
|
||
asyncpg>=0.29.0
|
||
SQLAlchemy>=2.0.0
|
||
python-dotenv>=1.0.0
|
||
bcrypt>=4.1.0
|
||
|
||
```
|
||
|
||
## Дополнительные указания
|
||
|
||
1. **Начните с файла `main.py`** - инициализация бота, диспетчера, подключение роутеров [web:8]
|
||
2. **Используйте роутеры aiogram** для группировки хендлеров по функциональности [web:2]
|
||
3. **Тестируйте каждую функцию по отдельности** перед интеграцией
|
||
4. **Используйте callback_data формата:** `action:param` (например, `add_fav:123`)
|
||
5. **Обрабатывайте длинные сообщения:** если описание книги > 1024 символов, обрезать с "..."
|
||
6. **Escape специальных символов** в Markdown, если используете `parse_mode='Markdown'`
|
||
|
||
## Важно
|
||
|
||
- НЕ модифицировать существующую Django БД
|
||
- НЕ создавать новые таблицы
|
||
- Использовать только SELECT, INSERT, UPDATE, DELETE для существующих таблиц
|
||
- Следовать naming conventions Django (таблицы `app_model`, например `books_book`)
|
||
- При обновлении `telegram_id` проверять уникальность (один Telegram аккаунт = один пользователь)
|
||
|
||
## Пример main.py
|
||
```
|
||
|
||
import asyncio
|
||
import logging
|
||
from aiogram import Bot, Dispatcher
|
||
from aiogram.fsm.storage.memory import MemoryStorage
|
||
from config import BOT_TOKEN
|
||
from handlers import auth, books, favorites
|
||
from middlewares.auth import AuthMiddleware
|
||
|
||
logging.basicConfig(level=logging.INFO)
|
||
|
||
async def main():
|
||
bot = Bot(token=BOT_TOKEN)
|
||
dp = Dispatcher(storage=MemoryStorage())
|
||
|
||
# Регистрация middleware
|
||
dp.message.middleware(AuthMiddleware())
|
||
dp.callback_query.middleware(AuthMiddleware())
|
||
|
||
# Регистрация роутеров
|
||
dp.include_router(auth.router)
|
||
dp.include_router(books.router)
|
||
dp.include_router(favorites.router)
|
||
|
||
await bot.delete_webhook(drop_pending_updates=True)
|
||
await dp.start_polling(bot)
|
||
if __name__ == '__main__':
|
||
asyncio.run(main())
|
||
|
||
```
|
||
|
||
Начни реализацию с базовой структуры проекта и авторизации!
|
||
```
|
||
|
||
Этот MD-файл содержит полную спецификацию для Cursor. Он включает:[^3][^4][^5]
|
||
|
||
- Четкую структуру проекта с организацией файлов[^6]
|
||
- Схему существующей Django БД (на основе ваших моделей)
|
||
- Детальные функциональные требования для каждой фичи
|
||
- Примеры SQL-запросов и SQLAlchemy кода[^7]
|
||
- Инструкции по работе с inline-клавиатурами aiogram[^2][^1]
|
||
- Требования к асинхронности и middleware[^5]
|
||
- Готовый пример точки входа приложения
|
||
|
||
Просто скопируйте этот контент в `.cursorrules` или отдельный MD-файл в корне проекта, и Cursor сможет генерировать код согласно спецификации!
|
||
<span style="display:none">[^10][^8][^9]</span>
|
||
|
||
<div align="center">⁂</div>
|
||
|
||
[^1]: https://docs.aiogram.dev/en/latest/utils/keyboard.html
|
||
|
||
[^2]: https://stackoverflow.com/questions/67652875/how-to-make-an-inline-keyboard-in-aiogram
|
||
|
||
[^3]: https://habr.com/ru/articles/953902/
|
||
|
||
[^4]: https://github.com/MasterGroosha/aiogram-3-guide
|
||
|
||
[^5]: https://aiogram.dev
|
||
|
||
[^6]: https://www.youtube.com/watch?v=wRatZLMa4oE
|
||
|
||
[^7]: https://stackoverflow.com/questions/75900203/how-do-i-connect-my-telegram-bot-telebot-to-postgresql-url
|
||
|
||
[^8]: https://www.youtube.com/watch?v=LufjNEv0OuQ
|
||
|
||
[^9]: https://www.youtube.com/watch?v=mNrqcTl13Wg
|
||
|
||
[^10]: https://www.youtube.com/watch?v=z5JT-bPdusY
|
||
|
||
## Дополнительные указания
|
||
|
||
- Не пиши тесты
|
||
- Не пиши объясняющие файлы любого рода(.txt,.md и тд.) |