initial commit
This commit is contained in:
459
CLAUDE.md
Normal file
459
CLAUDE.md
Normal file
@@ -0,0 +1,459 @@
|
||||
```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 и тд.)
|
||||
Reference in New Issue
Block a user