From 493c385cb1a818edef0ac9ce146e6097c8e61340 Mon Sep 17 00:00:00 2001 From: bokho Date: Tue, 23 Dec 2025 22:20:42 +0300 Subject: [PATCH] temp --- .../versions/002_add_premium_fields.py | 28 +++ backend/requirements.txt | 2 +- backend/run.py | 23 ++ .../application/use_cases/user_use_cases.py | 24 ++ backend/src/domain/entities/user.py | 8 +- backend/src/infrastructure/database/models.py | 4 + .../postgresql/user_repository.py | 13 +- backend/src/presentation/api/v1/admin.py | 8 +- .../src/presentation/api/v1/collections.py | 26 +-- .../src/presentation/api/v1/conversations.py | 16 +- backend/src/presentation/api/v1/documents.py | 20 +- backend/src/presentation/api/v1/messages.py | 14 +- backend/src/presentation/api/v1/rag.py | 8 +- backend/src/presentation/api/v1/users.py | 209 +++++++++++------- backend/src/presentation/main.py | 6 +- .../src/presentation/schemas/user_schemas.py | 8 +- backend/src/shared/di_container.py | 7 +- tg_bot/application/services/rag_service.py | 8 +- tg_bot/config/settings.py | 3 +- tg_bot/domain/services/user_service.py | 129 +++++++---- tg_bot/infrastructure/database/database.py | 19 -- tg_bot/infrastructure/database/models.py | 39 ---- tg_bot/infrastructure/telegram/bot.py | 10 + .../telegram/handlers/buy_handler.py | 141 ++++-------- .../telegram/handlers/collection_handler.py | 9 +- .../telegram/handlers/question_handler.py | 97 ++++---- .../telegram/handlers/start_handler.py | 31 ++- .../telegram/handlers/stats_handler.py | 86 ++++--- tg_bot/payment/webhooks/handler.py | 63 +++--- tg_bot/requirements.txt | 2 - tg_bot/run.py | 20 ++ 31 files changed, 574 insertions(+), 507 deletions(-) create mode 100644 backend/alembic/versions/002_add_premium_fields.py create mode 100644 backend/run.py delete mode 100644 tg_bot/infrastructure/database/database.py delete mode 100644 tg_bot/infrastructure/database/models.py create mode 100644 tg_bot/run.py diff --git a/backend/alembic/versions/002_add_premium_fields.py b/backend/alembic/versions/002_add_premium_fields.py new file mode 100644 index 0000000..313a824 --- /dev/null +++ b/backend/alembic/versions/002_add_premium_fields.py @@ -0,0 +1,28 @@ +"""Add premium fields to users + +Revision ID: 002 +Revises: 001 +Create Date: 2024-01-02 00:00:00.000000 + +""" +from alembic import op +import sqlalchemy as sa + + +revision = '002' +down_revision = '001' +branch_labels = None +depends_on = None + + +def upgrade() -> None: + op.add_column('users', sa.Column('is_premium', sa.Boolean(), nullable=False, server_default='false')) + op.add_column('users', sa.Column('premium_until', sa.DateTime(), nullable=True)) + op.add_column('users', sa.Column('questions_used', sa.Integer(), nullable=False, server_default='0')) + + +def downgrade() -> None: + op.drop_column('users', 'questions_used') + op.drop_column('users', 'premium_until') + op.drop_column('users', 'is_premium') + diff --git a/backend/requirements.txt b/backend/requirements.txt index 05f1776..d495c20 100644 --- a/backend/requirements.txt +++ b/backend/requirements.txt @@ -1,4 +1,4 @@ -fastapi==0.104.1 +fastapi==0.100.1 uvicorn[standard]==0.24.0 sqlalchemy[asyncio]==2.0.23 asyncpg==0.29.0 diff --git a/backend/run.py b/backend/run.py new file mode 100644 index 0000000..ee0c7eb --- /dev/null +++ b/backend/run.py @@ -0,0 +1,23 @@ +#!/usr/bin/env python3 + +import sys +import os +from pathlib import Path + +backend_dir = Path(__file__).parent +sys.path.insert(0, str(backend_dir)) + +if __name__ == "__main__": + import uvicorn + + + uvicorn.run( + "src.presentation.main:app", + host="0.0.0.0", + port=8000, + reload=True, + log_level="info" + ) + + + diff --git a/backend/src/application/use_cases/user_use_cases.py b/backend/src/application/use_cases/user_use_cases.py index 293263f..810164d 100644 --- a/backend/src/application/use_cases/user_use_cases.py +++ b/backend/src/application/use_cases/user_use_cases.py @@ -3,6 +3,7 @@ Use cases для работы с пользователями """ from uuid import UUID from typing import Optional +from datetime import datetime, timedelta from src.domain.entities.user import User, UserRole from src.domain.repositories.user_repository import IUserRepository from src.shared.exceptions import NotFoundError, ValidationError @@ -52,4 +53,27 @@ class UserUseCases: async def list_users(self, skip: int = 0, limit: int = 100) -> list[User]: """Получить список пользователей""" return await self.user_repository.list_all(skip=skip, limit=limit) + + async def increment_questions_used(self, telegram_id: str) -> User: + """Увеличить счетчик использованных вопросов""" + user = await self.user_repository.get_by_telegram_id(telegram_id) + if not user: + raise NotFoundError(f"Пользователь с telegram_id {telegram_id} не найден") + + user.questions_used += 1 + return await self.user_repository.update(user) + + async def activate_premium(self, telegram_id: str, days: int = 30) -> User: + """Активировать premium статус""" + user = await self.user_repository.get_by_telegram_id(telegram_id) + if not user: + raise NotFoundError(f"Пользователь с telegram_id {telegram_id} не найден") + + user.is_premium = True + if user.premium_until and user.premium_until > datetime.utcnow(): + user.premium_until = user.premium_until + timedelta(days=days) + else: + user.premium_until = datetime.utcnow() + timedelta(days=days) + + return await self.user_repository.update(user) diff --git a/backend/src/domain/entities/user.py b/backend/src/domain/entities/user.py index 5c3680d..3cc17a6 100644 --- a/backend/src/domain/entities/user.py +++ b/backend/src/domain/entities/user.py @@ -20,12 +20,18 @@ class User: telegram_id: str, role: UserRole = UserRole.USER, user_id: UUID | None = None, - created_at: datetime | None = None + created_at: datetime | None = None, + is_premium: bool = False, + premium_until: datetime | None = None, + questions_used: int = 0 ): self.user_id = user_id or uuid4() self.telegram_id = telegram_id self.role = role self.created_at = created_at or datetime.utcnow() + self.is_premium = is_premium + self.premium_until = premium_until + self.questions_used = questions_used def is_admin(self) -> bool: """проверка, является ли пользователь администратором""" diff --git a/backend/src/infrastructure/database/models.py b/backend/src/infrastructure/database/models.py index 8671da0..278b6ea 100644 --- a/backend/src/infrastructure/database/models.py +++ b/backend/src/infrastructure/database/models.py @@ -17,6 +17,10 @@ class UserModel(Base): telegram_id = Column(String, unique=True, nullable=False, index=True) role = Column(String, nullable=False, default="user") created_at = Column(DateTime, nullable=False, default=datetime.utcnow) + is_premium = Column(Boolean, default=False, nullable=False) + premium_until = Column(DateTime, nullable=True) + questions_used = Column(Integer, default=0, nullable=False) + collections = relationship("CollectionModel", back_populates="owner", cascade="all, delete-orphan") conversations = relationship("ConversationModel", back_populates="user", cascade="all, delete-orphan") collection_accesses = relationship("CollectionAccessModel", back_populates="user", cascade="all, delete-orphan") diff --git a/backend/src/infrastructure/repositories/postgresql/user_repository.py b/backend/src/infrastructure/repositories/postgresql/user_repository.py index d27801c..a7de61e 100644 --- a/backend/src/infrastructure/repositories/postgresql/user_repository.py +++ b/backend/src/infrastructure/repositories/postgresql/user_repository.py @@ -23,7 +23,10 @@ class PostgreSQLUserRepository(IUserRepository): user_id=user.user_id, telegram_id=user.telegram_id, role=user.role.value, - created_at=user.created_at + created_at=user.created_at, + is_premium=user.is_premium, + premium_until=user.premium_until, + questions_used=user.questions_used ) self.session.add(db_user) await self.session.commit() @@ -57,6 +60,9 @@ class PostgreSQLUserRepository(IUserRepository): db_user.telegram_id = user.telegram_id db_user.role = user.role.value + db_user.is_premium = user.is_premium + db_user.premium_until = user.premium_until + db_user.questions_used = user.questions_used await self.session.commit() await self.session.refresh(db_user) return self._to_entity(db_user) @@ -90,6 +96,9 @@ class PostgreSQLUserRepository(IUserRepository): user_id=db_user.user_id, telegram_id=db_user.telegram_id, role=UserRole(db_user.role), - created_at=db_user.created_at + created_at=db_user.created_at, + is_premium=db_user.is_premium, + premium_until=db_user.premium_until, + questions_used=db_user.questions_used ) diff --git a/backend/src/presentation/api/v1/admin.py b/backend/src/presentation/api/v1/admin.py index 1eed96e..89ac153 100644 --- a/backend/src/presentation/api/v1/admin.py +++ b/backend/src/presentation/api/v1/admin.py @@ -24,8 +24,8 @@ router = APIRouter(prefix="/admin", tags=["admin"]) async def admin_list_users( skip: int = 0, limit: int = 100, - current_user: FromDishka[User] = FromDishka(), - use_cases: FromDishka[UserUseCases] = FromDishka() + current_user: User = FromDishka(), + use_cases: UserUseCases = FromDishka() ): """Получить список всех пользователей (только для админов)""" if not current_user.is_admin(): @@ -38,8 +38,8 @@ async def admin_list_users( async def admin_list_collections( skip: int = 0, limit: int = 100, - current_user: FromDishka[User] = FromDishka(), - use_cases: FromDishka[CollectionUseCases] = FromDishka() + current_user: User = FromDishka(), + use_cases: CollectionUseCases = FromDishka() ): """Получить список всех коллекций (только для админов)""" from src.infrastructure.database.base import AsyncSessionLocal diff --git a/backend/src/presentation/api/v1/collections.py b/backend/src/presentation/api/v1/collections.py index 0b0431e..91d756a 100644 --- a/backend/src/presentation/api/v1/collections.py +++ b/backend/src/presentation/api/v1/collections.py @@ -24,8 +24,8 @@ router = APIRouter(prefix="/collections", tags=["collections"]) @router.post("", response_model=CollectionResponse, status_code=status.HTTP_201_CREATED) async def create_collection( collection_data: CollectionCreate, - current_user: FromDishka[User] = FromDishka(), - use_cases: FromDishka[CollectionUseCases] = FromDishka() + current_user: User = FromDishka(), + use_cases: CollectionUseCases = FromDishka() ): """Создать коллекцию""" collection = await use_cases.create_collection( @@ -40,7 +40,7 @@ async def create_collection( @router.get("/{collection_id}", response_model=CollectionResponse) async def get_collection( collection_id: UUID, - use_cases: FromDishka[CollectionUseCases] = FromDishka() + use_cases: CollectionUseCases = FromDishka() ): """Получить коллекцию по ID""" collection = await use_cases.get_collection(collection_id) @@ -51,8 +51,8 @@ async def get_collection( async def update_collection( collection_id: UUID, collection_data: CollectionUpdate, - current_user: FromDishka[User] = FromDishka(), - use_cases: FromDishka[CollectionUseCases] = FromDishka() + current_user: User = FromDishka(), + use_cases: CollectionUseCases = FromDishka() ): """Обновить коллекцию""" collection = await use_cases.update_collection( @@ -68,8 +68,8 @@ async def update_collection( @router.delete("/{collection_id}", status_code=status.HTTP_204_NO_CONTENT) async def delete_collection( collection_id: UUID, - current_user: FromDishka[User] = FromDishka(), - use_cases: FromDishka[CollectionUseCases] = FromDishka() + current_user: User = FromDishka(), + use_cases: CollectionUseCases = FromDishka() ): """Удалить коллекцию""" await use_cases.delete_collection(collection_id, current_user.user_id) @@ -80,8 +80,8 @@ async def delete_collection( async def list_collections( skip: int = 0, limit: int = 100, - current_user: FromDishka[User] = FromDishka(), - use_cases: FromDishka[CollectionUseCases] = FromDishka() + current_user: User = FromDishka(), + use_cases: CollectionUseCases = FromDishka() ): """Получить список коллекций, доступных пользователю""" collections = await use_cases.list_user_collections( @@ -96,8 +96,8 @@ async def list_collections( async def grant_access( collection_id: UUID, access_data: CollectionAccessGrant, - current_user: FromDishka[User] = FromDishka(), - use_cases: FromDishka[CollectionUseCases] = FromDishka() + current_user: User = FromDishka(), + use_cases: CollectionUseCases = FromDishka() ): """Предоставить доступ пользователю к коллекции""" access = await use_cases.grant_access( @@ -112,8 +112,8 @@ async def grant_access( async def revoke_access( collection_id: UUID, user_id: UUID, - current_user: FromDishka[User] = FromDishka(), - use_cases: FromDishka[CollectionUseCases] = FromDishka() + current_user: User = FromDishka(), + use_cases: CollectionUseCases = FromDishka() ): """Отозвать доступ пользователя к коллекции""" await use_cases.revoke_access(collection_id, user_id, current_user.user_id) diff --git a/backend/src/presentation/api/v1/conversations.py b/backend/src/presentation/api/v1/conversations.py index a48a661..75b1792 100644 --- a/backend/src/presentation/api/v1/conversations.py +++ b/backend/src/presentation/api/v1/conversations.py @@ -21,8 +21,8 @@ router = APIRouter(prefix="/conversations", tags=["conversations"]) @router.post("", response_model=ConversationResponse, status_code=status.HTTP_201_CREATED) async def create_conversation( conversation_data: ConversationCreate, - current_user: FromDishka[User] = FromDishka(), - use_cases: FromDishka[ConversationUseCases] = FromDishka() + current_user: User = FromDishka(), + use_cases: ConversationUseCases = FromDishka() ): """Создать беседу""" conversation = await use_cases.create_conversation( @@ -35,8 +35,8 @@ async def create_conversation( @router.get("/{conversation_id}", response_model=ConversationResponse) async def get_conversation( conversation_id: UUID, - current_user: FromDishka[User] = FromDishka(), - use_cases: FromDishka[ConversationUseCases] = FromDishka() + current_user: User = FromDishka(), + use_cases: ConversationUseCases = FromDishka() ): """Получить беседу по ID""" conversation = await use_cases.get_conversation(conversation_id, current_user.user_id) @@ -46,8 +46,8 @@ async def get_conversation( @router.delete("/{conversation_id}", status_code=status.HTTP_204_NO_CONTENT) async def delete_conversation( conversation_id: UUID, - current_user: FromDishka[User] = FromDishka(), - use_cases: FromDishka[ConversationUseCases] = FromDishka() + current_user: User = FromDishka(), + use_cases: ConversationUseCases = FromDishka() ): """Удалить беседу""" await use_cases.delete_conversation(conversation_id, current_user.user_id) @@ -58,8 +58,8 @@ async def delete_conversation( async def list_conversations( skip: int = 0, limit: int = 100, - current_user: FromDishka[User] = FromDishka(), - use_cases: FromDishka[ConversationUseCases] = FromDishka() + current_user: User = FromDishka(), + use_cases: ConversationUseCases = FromDishka() ): """Получить список бесед пользователя""" conversations = await use_cases.list_user_conversations( diff --git a/backend/src/presentation/api/v1/documents.py b/backend/src/presentation/api/v1/documents.py index a2eedcf..80d991e 100644 --- a/backend/src/presentation/api/v1/documents.py +++ b/backend/src/presentation/api/v1/documents.py @@ -22,8 +22,8 @@ router = APIRouter(prefix="/documents", tags=["documents"]) @router.post("", response_model=DocumentResponse, status_code=status.HTTP_201_CREATED) async def create_document( document_data: DocumentCreate, - current_user: FromDishka[User] = FromDishka(), - use_cases: FromDishka[DocumentUseCases] = FromDishka() + current_user: User = FromDishka(), + use_cases: DocumentUseCases = FromDishka() ): """Создать документ""" document = await use_cases.create_document( @@ -39,8 +39,8 @@ async def create_document( async def upload_document( collection_id: UUID, file: UploadFile = File(...), - current_user: FromDishka[User] = FromDishka(), - use_cases: FromDishka[DocumentUseCases] = FromDishka() + current_user: User = FromDishka(), + use_cases: DocumentUseCases = FromDishka() ): """Загрузить и распарсить PDF документ или изображение""" if not file.filename: @@ -70,7 +70,7 @@ async def upload_document( @router.get("/{document_id}", response_model=DocumentResponse) async def get_document( document_id: UUID, - use_cases: FromDishka[DocumentUseCases] = FromDishka() + use_cases: DocumentUseCases = FromDishka() ): """Получить документ по ID""" document = await use_cases.get_document(document_id) @@ -81,8 +81,8 @@ async def get_document( async def update_document( document_id: UUID, document_data: DocumentUpdate, - current_user: FromDishka[User] = FromDishka(), - use_cases: FromDishka[DocumentUseCases] = FromDishka() + current_user: User = FromDishka(), + use_cases: DocumentUseCases = FromDishka() ): """Обновить документ""" document = await use_cases.update_document( @@ -98,8 +98,8 @@ async def update_document( @router.delete("/{document_id}", status_code=status.HTTP_204_NO_CONTENT) async def delete_document( document_id: UUID, - current_user: FromDishka[User] = FromDishka(), - use_cases: FromDishka[DocumentUseCases] = FromDishka() + current_user: User = FromDishka(), + use_cases: DocumentUseCases = FromDishka() ): """Удалить документ""" await use_cases.delete_document(document_id, current_user.user_id) @@ -111,7 +111,7 @@ async def list_collection_documents( collection_id: UUID, skip: int = 0, limit: int = 100, - use_cases: FromDishka[DocumentUseCases] = FromDishka() + use_cases: DocumentUseCases = FromDishka() ): """Получить документы коллекции""" documents = await use_cases.list_collection_documents( diff --git a/backend/src/presentation/api/v1/messages.py b/backend/src/presentation/api/v1/messages.py index a7e11d7..84f5f02 100644 --- a/backend/src/presentation/api/v1/messages.py +++ b/backend/src/presentation/api/v1/messages.py @@ -22,8 +22,8 @@ router = APIRouter(prefix="/messages", tags=["messages"]) @router.post("", response_model=MessageResponse, status_code=status.HTTP_201_CREATED) async def create_message( message_data: MessageCreate, - current_user: FromDishka[User] = FromDishka(), - use_cases: FromDishka[MessageUseCases] = FromDishka() + current_user: User = FromDishka(), + use_cases: MessageUseCases = FromDishka() ): """Создать сообщение""" message = await use_cases.create_message( @@ -39,7 +39,7 @@ async def create_message( @router.get("/{message_id}", response_model=MessageResponse) async def get_message( message_id: UUID, - use_cases: FromDishka[MessageUseCases] = FromDishka() + use_cases: MessageUseCases = FromDishka() ): """Получить сообщение по ID""" message = await use_cases.get_message(message_id) @@ -50,7 +50,7 @@ async def get_message( async def update_message( message_id: UUID, message_data: MessageUpdate, - use_cases: FromDishka[MessageUseCases] = FromDishka() + use_cases: MessageUseCases = FromDishka() ): """Обновить сообщение""" message = await use_cases.update_message( @@ -64,7 +64,7 @@ async def update_message( @router.delete("/{message_id}", status_code=status.HTTP_204_NO_CONTENT) async def delete_message( message_id: UUID, - use_cases: FromDishka[MessageUseCases] = FromDishka() + use_cases: MessageUseCases = FromDishka() ): """Удалить сообщение""" await use_cases.delete_message(message_id) @@ -76,8 +76,8 @@ async def list_conversation_messages( conversation_id: UUID, skip: int = 0, limit: int = 100, - current_user: FromDishka[User] = FromDishka(), - use_cases: FromDishka[MessageUseCases] = FromDishka() + current_user: User = FromDishka(), + use_cases: MessageUseCases = FromDishka() ): """Получить сообщения беседы""" messages = await use_cases.list_conversation_messages( diff --git a/backend/src/presentation/api/v1/rag.py b/backend/src/presentation/api/v1/rag.py index d859374..c0e7371 100644 --- a/backend/src/presentation/api/v1/rag.py +++ b/backend/src/presentation/api/v1/rag.py @@ -21,8 +21,8 @@ router = APIRouter(prefix="/rag", tags=["rag"]) @router.post("/index", response_model=IndexDocumentResponse, status_code=status.HTTP_200_OK) async def index_document( body: IndexDocumentRequest, - use_cases: FromDishka[RAGUseCases] = FromDishka(), - current_user: FromDishka[User] = FromDishka(), + use_cases: RAGUseCases = FromDishka(), + current_user: User = FromDishka(), ): """Индексирование идет через чанкирование, далее эмбеддинг и загрузка в векторную бд""" result = await use_cases.index_document(body.document_id) @@ -32,8 +32,8 @@ async def index_document( @router.post("/question", response_model=RAGAnswer, status_code=status.HTTP_200_OK) async def ask_question( body: QuestionRequest, - use_cases: FromDishka[RAGUseCases] = FromDishka(), - current_user: FromDishka[User] = FromDishka(), + use_cases: RAGUseCases = FromDishka(), + current_user: User = FromDishka(), ): """Отвечает на вопрос, используя RAG в рамках беседы""" result = await use_cases.ask_question( diff --git a/backend/src/presentation/api/v1/users.py b/backend/src/presentation/api/v1/users.py index c8d2b12..7895e7a 100644 --- a/backend/src/presentation/api/v1/users.py +++ b/backend/src/presentation/api/v1/users.py @@ -1,83 +1,126 @@ -""" -API роутеры для работы с пользователями -""" -from __future__ import annotations - -from uuid import UUID -from fastapi import APIRouter, status -from fastapi.responses import JSONResponse -from typing import List -from dishka.integrations.fastapi import FromDishka -from src.presentation.schemas.user_schemas import UserCreate, UserUpdate, UserResponse -from src.application.use_cases.user_use_cases import UserUseCases -from src.domain.entities.user import User - -router = APIRouter(prefix="/users", tags=["users"]) - - -@router.post("", response_model=UserResponse, status_code=status.HTTP_201_CREATED) -async def create_user( - user_data: UserCreate, - use_cases: FromDishka[UserUseCases] = FromDishka() -): - """Создать пользователя""" - user = await use_cases.create_user( - telegram_id=user_data.telegram_id, - role=user_data.role - ) - return UserResponse.from_entity(user) - - -@router.get("/me", response_model=UserResponse) -async def get_current_user_info( - current_user: FromDishka[User] = FromDishka() -): - """Получить информацию о текущем пользователе""" - return UserResponse.from_entity(current_user) - - -@router.get("/{user_id}", response_model=UserResponse) -async def get_user( - user_id: UUID, - use_cases: FromDishka[UserUseCases] = FromDishka() -): - """Получить пользователя по ID""" - user = await use_cases.get_user(user_id) - return UserResponse.from_entity(user) - - -@router.put("/{user_id}", response_model=UserResponse) -async def update_user( - user_id: UUID, - user_data: UserUpdate, - use_cases: FromDishka[UserUseCases] = FromDishka() -): - """Обновить пользователя""" - user = await use_cases.update_user( - user_id=user_id, - telegram_id=user_data.telegram_id, - role=user_data.role - ) - return UserResponse.from_entity(user) - - -@router.delete("/{user_id}", status_code=status.HTTP_204_NO_CONTENT) -async def delete_user( - user_id: UUID, - use_cases: FromDishka[UserUseCases] = FromDishka() -): - """Удалить пользователя""" - await use_cases.delete_user(user_id) - return JSONResponse(status_code=status.HTTP_204_NO_CONTENT, content=None) - - -@router.get("", response_model=List[UserResponse]) -async def list_users( - skip: int = 0, - limit: int = 100, - use_cases: FromDishka[UserUseCases] = FromDishka() -): - """Получить список пользователей""" - users = await use_cases.list_users(skip=skip, limit=limit) - return [UserResponse.from_entity(user) for user in users] - +""" +API роутеры для работы с пользователями +""" +from __future__ import annotations + +from uuid import UUID +from fastapi import APIRouter, status, Depends +from fastapi.responses import JSONResponse +from typing import List, Annotated +from dishka.integrations.fastapi import FromDishka, inject +from src.presentation.schemas.user_schemas import UserCreate, UserUpdate, UserResponse +from src.application.use_cases.user_use_cases import UserUseCases +from src.domain.entities.user import User + +router = APIRouter(prefix="/users", tags=["users"]) + + +@router.post("", response_model=UserResponse, status_code=status.HTTP_201_CREATED) +@inject +async def create_user( + user_data: UserCreate, + use_cases: UserUseCases = FromDishka() +) -> UserResponse: + """Создать пользователя""" + user = await use_cases.create_user( + telegram_id=user_data.telegram_id, + role=user_data.role + ) + return UserResponse.from_entity(user) + + +@router.get("/me", response_model=UserResponse) +@inject +async def get_current_user_info( + current_user: User = FromDishka() +): + """Получить информацию о текущем пользователе""" + return UserResponse.from_entity(current_user) + + +@router.get("/telegram/{telegram_id}", response_model=UserResponse) +@inject +async def get_user_by_telegram_id( + telegram_id: str, + use_cases: UserUseCases = FromDishka() +): + """Получить пользователя по Telegram ID""" + user = await use_cases.get_user_by_telegram_id(telegram_id) + if not user: + from fastapi import HTTPException + raise HTTPException(status_code=404, detail=f"Пользователь с telegram_id {telegram_id} не найден") + return UserResponse.from_entity(user) + + +@router.post("/telegram/{telegram_id}/increment-questions", response_model=UserResponse) +@inject +async def increment_questions( + telegram_id: str, + use_cases: UserUseCases = FromDishka() +): + """Увеличить счетчик использованных вопросов""" + user = await use_cases.increment_questions_used(telegram_id) + return UserResponse.from_entity(user) + + +@router.post("/telegram/{telegram_id}/activate-premium", response_model=UserResponse) +@inject +async def activate_premium( + telegram_id: str, + days: int = 30, + use_cases: UserUseCases = FromDishka() +): + """Активировать premium статус""" + user = await use_cases.activate_premium(telegram_id, days=days) + return UserResponse.from_entity(user) + + +@router.get("/{user_id}", response_model=UserResponse) +@inject +async def get_user( + user_id: UUID, + use_cases: UserUseCases = FromDishka() +): + """Получить пользователя по ID""" + user = await use_cases.get_user(user_id) + return UserResponse.from_entity(user) + + +@router.put("/{user_id}", response_model=UserResponse) +@inject +async def update_user( + user_id: UUID, + user_data: UserUpdate, + use_cases: UserUseCases = FromDishka() +): + """Обновить пользователя""" + user = await use_cases.update_user( + user_id=user_id, + telegram_id=user_data.telegram_id, + role=user_data.role + ) + return UserResponse.from_entity(user) + + +@router.delete("/{user_id}", status_code=status.HTTP_204_NO_CONTENT) +@inject +async def delete_user( + user_id: UUID, + use_cases: UserUseCases = FromDishka() +): + """Удалить пользователя""" + await use_cases.delete_user(user_id) + return JSONResponse(status_code=status.HTTP_204_NO_CONTENT, content=None) + + +@router.get("", response_model=List[UserResponse]) +@inject +async def list_users( + skip: int = 0, + limit: int = 100, + use_cases: UserUseCases = FromDishka() +): + """Получить список пользователей""" + users = await use_cases.list_users(skip=skip, limit=limit) + return [UserResponse.from_entity(user) for user in users] + diff --git a/backend/src/presentation/main.py b/backend/src/presentation/main.py index 359fe35..8f58e66 100644 --- a/backend/src/presentation/main.py +++ b/backend/src/presentation/main.py @@ -2,9 +2,11 @@ from __future__ import annotations import sys import os +from pathlib import Path -if '/app' not in sys.path: - sys.path.insert(0, '/app') +backend_dir = Path(__file__).parent.parent.parent +if str(backend_dir) not in sys.path: + sys.path.insert(0, str(backend_dir)) from fastapi import FastAPI from fastapi.middleware.cors import CORSMiddleware diff --git a/backend/src/presentation/schemas/user_schemas.py b/backend/src/presentation/schemas/user_schemas.py index 16a14f0..3cd9e07 100644 --- a/backend/src/presentation/schemas/user_schemas.py +++ b/backend/src/presentation/schemas/user_schemas.py @@ -30,6 +30,9 @@ class UserResponse(BaseModel): telegram_id: str role: UserRole created_at: datetime + is_premium: bool = False + premium_until: datetime | None = None + questions_used: int = 0 @classmethod def from_entity(cls, user: "User") -> "UserResponse": @@ -38,7 +41,10 @@ class UserResponse(BaseModel): user_id=user.user_id, telegram_id=user.telegram_id, role=user.role, - created_at=user.created_at + created_at=user.created_at, + is_premium=user.is_premium, + premium_until=user.premium_until, + questions_used=user.questions_used ) class Config: diff --git a/backend/src/shared/di_container.py b/backend/src/shared/di_container.py index a5589e0..eeaaa56 100644 --- a/backend/src/shared/di_container.py +++ b/backend/src/shared/di_container.py @@ -1,7 +1,7 @@ from dishka import Container, Provider, Scope, provide from fastapi import Request from sqlalchemy.ext.asyncio import AsyncSession -from contextlib import asynccontextmanager +from collections.abc import AsyncIterator from src.infrastructure.database.base import AsyncSessionLocal from src.infrastructure.repositories.postgresql.user_repository import PostgreSQLUserRepository @@ -39,8 +39,7 @@ from src.application.use_cases.rag_use_cases import RAGUseCases class DatabaseProvider(Provider): @provide(scope=Scope.REQUEST) - @asynccontextmanager - async def get_db(self) -> AsyncSession: + async def get_db(self) -> AsyncIterator[AsyncSession]: async with AsyncSessionLocal() as session: try: yield session @@ -77,7 +76,7 @@ class RepositoryProvider(Provider): class ServiceProvider(Provider): @provide(scope=Scope.APP) def get_redis_client(self) -> RedisClient: - return RedisClient() + return RedisClient(host=settings.REDIS_HOST, port=settings.REDIS_PORT) @provide(scope=Scope.APP) def get_cache_service(self, redis_client: RedisClient) -> CacheService: diff --git a/tg_bot/application/services/rag_service.py b/tg_bot/application/services/rag_service.py index c5771ed..9f0f024 100644 --- a/tg_bot/application/services/rag_service.py +++ b/tg_bot/application/services/rag_service.py @@ -2,8 +2,6 @@ import aiohttp from tg_bot.infrastructure.external.deepseek_client import DeepSeekClient from tg_bot.config.settings import settings -BACKEND_URL = "http://localhost:8001/api/v1" - class RAGService: @@ -19,7 +17,7 @@ class RAGService: try: async with aiohttp.ClientSession() as session: async with session.get( - f"{BACKEND_URL}/users/telegram/{user_telegram_id}" + f"{settings.BACKEND_URL}/users/telegram/{user_telegram_id}" ) as user_response: if user_response.status != 200: return [] @@ -31,7 +29,7 @@ class RAGService: return [] async with session.get( - f"{BACKEND_URL}/collections/", + f"{settings.BACKEND_URL}/collections/", headers={"X-Telegram-ID": user_telegram_id} ) as collections_response: if collections_response.status != 200: @@ -48,7 +46,7 @@ class RAGService: try: async with aiohttp.ClientSession() as search_session: async with search_session.get( - f"{BACKEND_URL}/documents/collection/{collection_id}", + f"{settings.BACKEND_URL}/documents/collection/{collection_id}", params={"search": query, "limit": limit_per_collection}, headers={"X-Telegram-ID": user_telegram_id} ) as search_response: diff --git a/tg_bot/config/settings.py b/tg_bot/config/settings.py index 908bfd5..bf695df 100644 --- a/tg_bot/config/settings.py +++ b/tg_bot/config/settings.py @@ -17,7 +17,6 @@ class Settings(BaseSettings): TELEGRAM_BOT_TOKEN: str = "" FREE_QUESTIONS_LIMIT: int = 5 PAYMENT_AMOUNT: float = 500.0 - DATABASE_URL: str = "sqlite:///data/bot.db" LOG_LEVEL: str = "INFO" LOG_FILE: str = "logs/bot.log" @@ -28,6 +27,8 @@ class Settings(BaseSettings): DEEPSEEK_API_KEY: Optional[str] = None DEEPSEEK_API_URL: str = "https://api.deepseek.com/v1/chat/completions" + + BACKEND_URL: str = "http://localhost:8001/api/v1" ADMIN_IDS_STR: str = "" diff --git a/tg_bot/domain/services/user_service.py b/tg_bot/domain/services/user_service.py index 77c5dbc..3fb2a25 100644 --- a/tg_bot/domain/services/user_service.py +++ b/tg_bot/domain/services/user_service.py @@ -1,20 +1,60 @@ -from sqlalchemy.ext.asyncio import AsyncSession -from sqlalchemy import select -from datetime import datetime, timedelta + +import aiohttp +from datetime import datetime from typing import Optional -from tg_bot.infrastructure.database.models import UserModel +from tg_bot.config.settings import settings + + +class User: + """Модель пользователя для телеграм-бота""" + def __init__(self, data: dict): + self.user_id = data.get("user_id") + self.telegram_id = data.get("telegram_id") + self.role = data.get("role") + created_at_str = data.get("created_at") + if created_at_str: + try: + created_at_str = created_at_str.replace("Z", "+00:00") + self.created_at = datetime.fromisoformat(created_at_str) + except (ValueError, AttributeError): + self.created_at = None + else: + self.created_at = None + + premium_until_str = data.get("premium_until") + if premium_until_str: + try: + premium_until_str = premium_until_str.replace("Z", "+00:00") + self.premium_until = datetime.fromisoformat(premium_until_str) + except (ValueError, AttributeError): + self.premium_until = None + else: + self.premium_until = None + + self.is_premium = data.get("is_premium", False) + self.questions_used = data.get("questions_used", 0) class UserService: + """Сервис для работы с пользователями через API бэкенда""" - def __init__(self, session: AsyncSession): - self.session = session + def __init__(self): + self.backend_url = settings.BACKEND_URL - async def get_user_by_telegram_id(self, telegram_id: int) -> Optional[UserModel]: - result = await self.session.execute( - select(UserModel).filter_by(telegram_id=str(telegram_id)) - ) - return result.scalar_one_or_none() + async def get_user_by_telegram_id(self, telegram_id: int) -> Optional[User]: + """Получить пользователя по Telegram ID""" + try: + async with aiohttp.ClientSession() as session: + async with session.get( + f"{self.backend_url}/users/telegram/{telegram_id}" + ) as response: + if response.status == 200: + data = await response.json() + return User(data) + return None + except Exception as e: + print(f"Error getting user: {e}") + return None async def get_or_create_user( self, @@ -22,46 +62,45 @@ class UserService: username: str = "", first_name: str = "", last_name: str = "" - ) -> UserModel: + ) -> User: + """Получить или создать пользователя""" user = await self.get_user_by_telegram_id(telegram_id) if not user: - user = UserModel( - telegram_id=str(telegram_id), - username=username, - first_name=first_name, - last_name=last_name - ) - self.session.add(user) - await self.session.commit() - else: - user.username = username - user.first_name = first_name - user.last_name = last_name - await self.session.commit() + try: + async with aiohttp.ClientSession() as session: + async with session.post( + f"{self.backend_url}/users", + json={"telegram_id": str(telegram_id), "role": "user"} + ) as response: + if response.status in [200, 201]: + data = await response.json() + return User(data) + except Exception as e: + print(f"Error creating user: {e}") + raise return user async def update_user_questions(self, telegram_id: int) -> bool: - user = await self.get_user_by_telegram_id(telegram_id) - if user: - user.questions_used += 1 - await self.session.commit() - return True - return False - - async def activate_premium(self, telegram_id: int) -> bool: + """Увеличить счетчик использованных вопросов""" try: - user = await self.get_user_by_telegram_id(telegram_id) - if user: - user.is_premium = True - if user.premium_until and user.premium_until > datetime.now(): - user.premium_until = user.premium_until + timedelta(days=30) - else: - user.premium_until = datetime.now() + timedelta(days=30) - await self.session.commit() - return True - else: - return False + async with aiohttp.ClientSession() as session: + async with session.post( + f"{self.backend_url}/users/telegram/{telegram_id}/increment-questions" + ) as response: + return response.status == 200 + except Exception as e: + print(f"Error updating questions: {e}") + return False + + async def activate_premium(self, telegram_id: int, days: int = 30) -> bool: + """Активировать premium статус""" + try: + async with aiohttp.ClientSession() as session: + async with session.post( + f"{self.backend_url}/users/telegram/{telegram_id}/activate-premium", + params={"days": days} + ) as response: + return response.status == 200 except Exception as e: print(f"Error activating premium: {e}") - await self.session.rollback() return False diff --git a/tg_bot/infrastructure/database/database.py b/tg_bot/infrastructure/database/database.py deleted file mode 100644 index f49a009..0000000 --- a/tg_bot/infrastructure/database/database.py +++ /dev/null @@ -1,19 +0,0 @@ -from sqlalchemy.ext.asyncio import create_async_engine, async_sessionmaker, AsyncSession -from tg_bot.config.settings import settings - -database_url = settings.DATABASE_URL -if database_url.startswith("sqlite:///"): - database_url = database_url.replace("sqlite:///", "sqlite+aiosqlite:///") - -engine = create_async_engine( - database_url, - echo=settings.DEBUG -) - -AsyncSessionLocal = async_sessionmaker(engine, class_=AsyncSession, expire_on_commit=False) - -async def create_tables(): - from .models import Base - async with engine.begin() as conn: - await conn.run_sync(Base.metadata.create_all) - print(f"Таблицы созданы: {settings.DATABASE_URL}") \ No newline at end of file diff --git a/tg_bot/infrastructure/database/models.py b/tg_bot/infrastructure/database/models.py deleted file mode 100644 index f681729..0000000 --- a/tg_bot/infrastructure/database/models.py +++ /dev/null @@ -1,39 +0,0 @@ -import uuid -from datetime import datetime -from sqlalchemy import Column, String, DateTime, Boolean, Integer, Text -from sqlalchemy.ext.declarative import declarative_base - -Base = declarative_base() - - -class UserModel(Base): - __tablename__ = "users" - user_id = Column("user_id", String(36), primary_key=True, default=lambda: str(uuid.uuid4())) - telegram_id = Column("telegram_id", String(100), nullable=False, unique=True) - created_at = Column("created_at", DateTime, default=datetime.utcnow, nullable=False) - role = Column("role", String(20), default="user", nullable=False) - - is_premium = Column(Boolean, default=False, nullable=False) - premium_until = Column(DateTime, nullable=True) - questions_used = Column(Integer, default=0, nullable=False) - - username = Column(String(100), nullable=True) - first_name = Column(String(100), nullable=True) - last_name = Column(String(100), nullable=True) - - -class PaymentModel(Base): - __tablename__ = "payments" - - id = Column(Integer, primary_key=True, autoincrement=True) - payment_id = Column(String(36), default=lambda: str(uuid.uuid4()), nullable=False, unique=True) - user_id = Column(Integer, nullable=False) - amount = Column(String(20), nullable=False) - currency = Column(String(3), default="RUB", nullable=False) - status = Column(String(20), default="pending", nullable=False) - created_at = Column(DateTime, default=datetime.utcnow, nullable=False) - yookassa_payment_id = Column(String(100), unique=True, nullable=True) - description = Column(Text, nullable=True) - - def __repr__(self): - return f"" \ No newline at end of file diff --git a/tg_bot/infrastructure/telegram/bot.py b/tg_bot/infrastructure/telegram/bot.py index 3490201..c36228a 100644 --- a/tg_bot/infrastructure/telegram/bot.py +++ b/tg_bot/infrastructure/telegram/bot.py @@ -34,8 +34,18 @@ async def create_bot() -> tuple[Bot, Dispatcher]: async def start_bot(): bot = None try: + if not settings.TELEGRAM_BOT_TOKEN or not settings.TELEGRAM_BOT_TOKEN.strip(): + raise ValueError("TELEGRAM_BOT_TOKEN не установлен в переменных окружения или файле .env") + bot, dp = await create_bot() + try: + bot_info = await bot.get_me() + username = bot_info.username if bot_info.username else f"ID: {bot_info.id}" + logger.info(f"Бот успешно подключен: @{username}") + except Exception as e: + raise ValueError(f"Неверный токен Telegram бота: {e}") + try: webhook_info = await bot.get_webhook_info() if webhook_info.url: diff --git a/tg_bot/infrastructure/telegram/handlers/buy_handler.py b/tg_bot/infrastructure/telegram/handlers/buy_handler.py index 0411d00..f7d9b6e 100644 --- a/tg_bot/infrastructure/telegram/handlers/buy_handler.py +++ b/tg_bot/infrastructure/telegram/handlers/buy_handler.py @@ -4,14 +4,11 @@ from aiogram.types import Message, InlineKeyboardMarkup, InlineKeyboardButton from decimal import Decimal from tg_bot.config.settings import settings from tg_bot.payment.yookassa.client import yookassa_client -from tg_bot.infrastructure.database.database import AsyncSessionLocal -from tg_bot.infrastructure.database.models import PaymentModel from tg_bot.domain.services.user_service import UserService -from sqlalchemy import select -import uuid -from datetime import datetime, timedelta +from datetime import datetime router = Router() +user_service = UserService() @router.message(Command("buy")) @@ -19,23 +16,21 @@ async def cmd_buy(message: Message): user_id = message.from_user.id username = message.from_user.username or f"user_{user_id}" - async with AsyncSessionLocal() as session: - try: - user_service = UserService(session) - user = await user_service.get_user_by_telegram_id(user_id) + try: + user = await user_service.get_user_by_telegram_id(user_id) - if user and user.is_premium and user.premium_until and user.premium_until > datetime.now(): - days_left = (user.premium_until - datetime.now()).days - await message.answer( - f"У вас уже есть активная подписка!\n\n" - f"• Статус: Premium активен\n" - f"• Действует до: {user.premium_until.strftime('%d.%m.%Y')}\n" - f"• Осталось дней: {days_left}\n\n" - f"Новая подписка будет добавлена к текущей.", - parse_mode="HTML" - ) - except Exception: - pass + if user and user.is_premium and user.premium_until and user.premium_until > datetime.now(): + days_left = (user.premium_until - datetime.now()).days + await message.answer( + f"У вас уже есть активная подписка!\n\n" + f"• Статус: Premium активен\n" + f"• Действует до: {user.premium_until.strftime('%d.%m.%Y')}\n" + f"• Осталось дней: {days_left}\n\n" + f"Новая подписка будет добавлена к текущей.", + parse_mode="HTML" + ) + except Exception: + pass await message.answer( "*Создаю ссылку для оплаты...*\n\n" @@ -49,24 +44,8 @@ async def cmd_buy(message: Message): description=f"Подписка VibeLawyerBot для @{username}", user_id=user_id ) - - async with AsyncSessionLocal() as session: - try: - payment = PaymentModel( - payment_id=str(uuid.uuid4()), - user_id=user_id, - amount=str(settings.PAYMENT_AMOUNT), - currency="RUB", - status="pending", - yookassa_payment_id=payment_data["id"], - description="Оплата подписки VibeLawyerBot" - ) - session.add(payment) - await session.commit() - print(f"Платёж сохранён в БД: {payment.payment_id}") - except Exception as e: - print(f"Ошибка сохранения платежа в БД: {e}") - await session.rollback() + + print(f"Платёж создан в ЮKассе: {payment_data['id']}") keyboard = InlineKeyboardMarkup( inline_keyboard=[ @@ -139,27 +118,15 @@ async def check_payment_status(callback_query: types.CallbackQuery): payment = YooPayment.find_one(yookassa_id) if payment.status == "succeeded": - async with AsyncSessionLocal() as session: - try: - result = await session.execute( - select(PaymentModel).filter_by(yookassa_payment_id=yookassa_id) - ) - db_payment = result.scalar_one_or_none() - - if db_payment: - db_payment.status = "succeeded" - user_service = UserService(session) - success = await user_service.activate_premium(user_id) - if success: - user = await user_service.get_user_by_telegram_id(user_id) - await session.commit() - if not user: - user = await user_service.get_user_by_telegram_id(user_id) - + try: + success = await user_service.activate_premium(user_id) + if success: + user = await user_service.get_user_by_telegram_id(user_id) + if user: await callback_query.message.answer( "Оплата подтверждена!\n\n" f"Ваш premium-доступ активирован до: " - f"{user.premium_until.strftime('%d.%m.%Y')}\n\n" + f"{user.premium_until.strftime('%d.%m.%Y') if user.premium_until else 'Не указано'}\n\n" "Теперь вы можете:\n" "• Задавать неограниченное количество вопросов\n" "• Получать приоритетные ответы\n" @@ -169,12 +136,23 @@ async def check_payment_status(callback_query: types.CallbackQuery): ) else: await callback_query.message.answer( - "Платёж найден в ЮKассе, но не в нашей БД\n\n" + "Оплата подтверждена, но не удалось активировать premium\n\n" "Пожалуйста, обратитесь к администратору.", parse_mode="HTML" ) - except Exception as e: - print(f"Ошибка обработки платежа: {e}") + else: + await callback_query.message.answer( + "Оплата подтверждена, но не удалось активировать premium\n\n" + "Пожалуйста, обратитесь к администратору.", + parse_mode="HTML" + ) + except Exception as e: + print(f"Ошибка обработки платежа: {e}") + await callback_query.message.answer( + "Ошибка активации premium\n\n" + "Пожалуйста, обратитесь к администратору.", + parse_mode="HTML" + ) elif payment.status == "pending": await callback_query.message.answer( @@ -206,42 +184,13 @@ async def check_payment_status(callback_query: types.CallbackQuery): @router.message(Command("mypayments")) async def cmd_my_payments(message: Message): - user_id = message.from_user.id - - async with AsyncSessionLocal() as session: - try: - result = await session.execute( - select(PaymentModel).filter_by(user_id=user_id).order_by(PaymentModel.created_at.desc()).limit(10) - ) - payments = result.scalars().all() - - if not payments: - await message.answer( - "У вас пока нет платежей\n\n" - "Используйте команду /buy чтобы оформить подписку.", - parse_mode="HTML" - ) - return - - response = ["Ваши последние платежи:\n"] - - for i, payment in enumerate(payments, 1): - status_text = "Успешно" if payment.status == "succeeded" else "Ожидание" if payment.status == "pending" else "Ошибка" - response.append( - f"\n{i}. {payment.amount} руб. ({status_text})\n" - f"Статус: {payment.status}\n" - f"Дата: {payment.created_at.strftime('%d.%m.%Y %H:%M')}\n" - f"ID: {payment.payment_id[:8]}..." - ) - - response.append("\n\nПолный доступ открывается после успешной оплаты") - - await message.answer( - "\n".join(response), - parse_mode="HTML" - ) - except Exception as e: - print(f"Ошибка получения платежей: {e}") + await message.answer( + "История платежей\n\n" + "История платежей хранится в системе оплаты ЮKassa.\n" + "Для проверки статуса подписки используйте команду /stats.\n\n" + "Для оформления новой подписки используйте команду /buy", + parse_mode="HTML" + ) @router.message(Command("testcards")) diff --git a/tg_bot/infrastructure/telegram/handlers/collection_handler.py b/tg_bot/infrastructure/telegram/handlers/collection_handler.py index 8f270ae..eea2ab8 100644 --- a/tg_bot/infrastructure/telegram/handlers/collection_handler.py +++ b/tg_bot/infrastructure/telegram/handlers/collection_handler.py @@ -2,17 +2,16 @@ from aiogram import Router from aiogram.types import Message, InlineKeyboardMarkup, InlineKeyboardButton, CallbackQuery from aiogram.filters import Command import aiohttp +from tg_bot.config.settings import settings router = Router() -BACKEND_URL = "http://localhost:8001/api/v1" - async def get_user_collections(telegram_id: str): try: async with aiohttp.ClientSession() as session: async with session.get( - f"{BACKEND_URL}/collections/", + f"{settings.BACKEND_URL}/collections/", headers={"X-Telegram-ID": telegram_id} ) as response: if response.status == 200: @@ -27,7 +26,7 @@ async def get_collection_documents(collection_id: str, telegram_id: str): try: async with aiohttp.ClientSession() as session: async with session.get( - f"{BACKEND_URL}/documents/collection/{collection_id}", + f"{settings.BACKEND_URL}/documents/collection/{collection_id}", headers={"X-Telegram-ID": telegram_id} ) as response: if response.status == 200: @@ -42,7 +41,7 @@ async def search_in_collection(collection_id: str, query: str, telegram_id: str) try: async with aiohttp.ClientSession() as session: async with session.get( - f"{BACKEND_URL}/documents/collection/{collection_id}", + f"{settings.BACKEND_URL}/documents/collection/{collection_id}", params={"search": query}, headers={"X-Telegram-ID": telegram_id} ) as response: diff --git a/tg_bot/infrastructure/telegram/handlers/question_handler.py b/tg_bot/infrastructure/telegram/handlers/question_handler.py index 540aac7..be85726 100644 --- a/tg_bot/infrastructure/telegram/handlers/question_handler.py +++ b/tg_bot/infrastructure/telegram/handlers/question_handler.py @@ -3,14 +3,12 @@ from aiogram.types import Message from datetime import datetime import aiohttp from tg_bot.config.settings import settings -from tg_bot.infrastructure.database.database import AsyncSessionLocal -from tg_bot.infrastructure.database.models import UserModel -from tg_bot.domain.services.user_service import UserService +from tg_bot.domain.services.user_service import UserService, User from tg_bot.application.services.rag_service import RAGService router = Router() -BACKEND_URL = "http://localhost:8001/api/v1" rag_service = RAGService() +user_service = UserService() @router.message() async def handle_question(message: Message): @@ -19,58 +17,37 @@ async def handle_question(message: Message): if question_text.startswith('/'): return - async with AsyncSessionLocal() as session: - try: - user_service = UserService(session) - user = await user_service.get_user_by_telegram_id(user_id) + try: + user = await user_service.get_user_by_telegram_id(user_id) - if not user: - user = await user_service.get_or_create_user( - user_id, - message.from_user.username or "", - message.from_user.first_name or "", - message.from_user.last_name or "" - ) - await ensure_user_in_backend(str(user_id), message.from_user) - - if user.is_premium: - await process_premium_question(message, user, question_text, user_service) - - elif user.questions_used < settings.FREE_QUESTIONS_LIMIT: - await process_free_question(message, user, question_text, user_service) - - else: - await handle_limit_exceeded(message, user) - - except Exception as e: - print(f"Error processing question: {e}") - await message.answer( - "Произошла ошибка. Попробуйте позже.", - parse_mode="HTML" + if not user: + user = await user_service.get_or_create_user( + user_id, + message.from_user.username or "", + message.from_user.first_name or "", + message.from_user.last_name or "" ) + if user.is_premium: + await process_premium_question(message, user, question_text) + + elif user.questions_used < settings.FREE_QUESTIONS_LIMIT: + await process_free_question(message, user, question_text) + + else: + await handle_limit_exceeded(message, user) -async def ensure_user_in_backend(telegram_id: str, telegram_user): - try: - async with aiohttp.ClientSession() as session: - async with session.get( - f"{BACKEND_URL}/users/telegram/{telegram_id}" - ) as response: - if response.status == 200: - return - - async with session.post( - f"{BACKEND_URL}/users", - json={"telegram_id": telegram_id, "role": "user"} - ) as create_response: - if create_response.status in [200, 201]: - print(f"Пользователь {telegram_id} создан в backend") except Exception as e: - print(f"Error creating user in backend: {e}") + print(f"Error processing question: {e}") + await message.answer( + "Произошла ошибка. Попробуйте позже.", + parse_mode="HTML" + ) -async def process_premium_question(message: Message, user: UserModel, question_text: str, user_service: UserService): - await user_service.update_user_questions(user.telegram_id) +async def process_premium_question(message: Message, user: User, question_text: str): + await user_service.update_user_questions(int(user.telegram_id)) + user = await user_service.get_user_by_telegram_id(int(user.telegram_id)) await message.bot.send_chat_action(message.chat.id, "typing") @@ -129,9 +106,9 @@ async def process_premium_question(message: Message, user: UserModel, question_t await message.answer(response, parse_mode="HTML") -async def process_free_question(message: Message, user: UserModel, question_text: str, user_service: UserService): - await user_service.update_user_questions(user.telegram_id) - user = await user_service.get_user_by_telegram_id(user.telegram_id) +async def process_free_question(message: Message, user: User, question_text: str): + await user_service.update_user_questions(int(user.telegram_id)) + user = await user_service.get_user_by_telegram_id(int(user.telegram_id)) remaining = settings.FREE_QUESTIONS_LIMIT - user.questions_used await message.bot.send_chat_action(message.chat.id, "typing") @@ -201,9 +178,11 @@ async def process_free_question(message: Message, user: UserModel, question_text async def save_conversation_to_backend(telegram_id: str, question: str, answer: str, sources: list): try: + from tg_bot.config.settings import settings + backend_url = settings.BACKEND_URL async with aiohttp.ClientSession() as session: async with session.get( - f"{BACKEND_URL}/users/telegram/{telegram_id}" + f"{backend_url}/users/telegram/{telegram_id}" ) as user_response: if user_response.status != 200: return @@ -211,7 +190,7 @@ async def save_conversation_to_backend(telegram_id: str, question: str, answer: user_uuid = user_data.get("user_id") async with session.get( - f"{BACKEND_URL}/collections/", + f"{backend_url}/collections/", headers={"X-Telegram-ID": telegram_id} ) as collections_response: collections = [] @@ -223,7 +202,7 @@ async def save_conversation_to_backend(telegram_id: str, question: str, answer: collection_id = collections[0].get("collection_id") else: async with session.post( - f"{BACKEND_URL}/collections", + f"{backend_url}/collections", json={ "name": "Основная коллекция", "description": "Коллекция по умолчанию", @@ -239,7 +218,7 @@ async def save_conversation_to_backend(telegram_id: str, question: str, answer: return async with session.post( - f"{BACKEND_URL}/conversations", + f"{backend_url}/conversations", json={"collection_id": str(collection_id)}, headers={"X-Telegram-ID": telegram_id} ) as conversation_response: @@ -252,7 +231,7 @@ async def save_conversation_to_backend(telegram_id: str, question: str, answer: return await session.post( - f"{BACKEND_URL}/messages", + f"{backend_url}/messages", json={ "conversation_id": str(conversation_id), "content": question, @@ -262,7 +241,7 @@ async def save_conversation_to_backend(telegram_id: str, question: str, answer: ) await session.post( - f"{BACKEND_URL}/messages", + f"{backend_url}/messages", json={ "conversation_id": str(conversation_id), "content": answer, @@ -276,7 +255,7 @@ async def save_conversation_to_backend(telegram_id: str, question: str, answer: print(f"Error saving conversation: {e}") -async def handle_limit_exceeded(message: Message, user: UserModel): +async def handle_limit_exceeded(message: Message, user: User): response = ( f"Лимит бесплатных вопросов исчерпан!\n\n" diff --git a/tg_bot/infrastructure/telegram/handlers/start_handler.py b/tg_bot/infrastructure/telegram/handlers/start_handler.py index 8bc3a36..6f5af3b 100644 --- a/tg_bot/infrastructure/telegram/handlers/start_handler.py +++ b/tg_bot/infrastructure/telegram/handlers/start_handler.py @@ -4,10 +4,10 @@ from aiogram.types import Message from datetime import datetime from tg_bot.config.settings import settings -from tg_bot.infrastructure.database.database import AsyncSessionLocal from tg_bot.domain.services.user_service import UserService router = Router() +user_service = UserService() @router.message(Command("start")) async def cmd_start(message: Message): @@ -16,22 +16,19 @@ async def cmd_start(message: Message): username = message.from_user.username or "" first_name = message.from_user.first_name or "" last_name = message.from_user.last_name or "" - async with AsyncSessionLocal() as session: - try: - user_service = UserService(session) - existing_user = await user_service.get_user_by_telegram_id(user_id) - user = await user_service.get_or_create_user( - user_id, - username, - first_name, - last_name - ) - if not existing_user: - print(f"Новый пользователь: {user_id}") - - except Exception as e: - print(f"Ошибка сохранения пользователя: {e}") - await session.rollback() + try: + existing_user = await user_service.get_user_by_telegram_id(user_id) + user = await user_service.get_or_create_user( + user_id, + username, + first_name, + last_name + ) + if not existing_user: + print(f"Новый пользователь: {user_id}") + + except Exception as e: + print(f"Ошибка сохранения пользователя: {e}") welcome_text = ( f"Привет, {first_name}!\n\n" f"Я VibeLawyerBot - ваш помощник в юридических вопросах.\n\n" diff --git a/tg_bot/infrastructure/telegram/handlers/stats_handler.py b/tg_bot/infrastructure/telegram/handlers/stats_handler.py index 58adfdc..aaa8ee5 100644 --- a/tg_bot/infrastructure/telegram/handlers/stats_handler.py +++ b/tg_bot/infrastructure/telegram/handlers/stats_handler.py @@ -4,58 +4,56 @@ from aiogram.filters import Command from aiogram.types import Message from tg_bot.config.settings import settings -from tg_bot.infrastructure.database.database import AsyncSessionLocal from tg_bot.domain.services.user_service import UserService router = Router() +user_service = UserService() @router.message(Command("stats")) async def cmd_stats(message: Message): user_id = message.from_user.id - async with AsyncSessionLocal() as session: - try: - user_service = UserService(session) - user = await user_service.get_user_by_telegram_id(user_id) + try: + user = await user_service.get_user_by_telegram_id(user_id) - if user: - stats_text = ( - f"Ваша статистика\n\n" - f"Основное:\n" - f"• ID: {user_id}\n" - f"• Premium: {'Да' if user.is_premium else 'Нет'}\n" - f"• Вопросов использовано: {user.questions_used}/{settings.FREE_QUESTIONS_LIMIT}\n\n" - ) - - if user.is_premium: - stats_text += ( - f"Premium статус:\n" - f"• Активен до: {user.premium_until.strftime('%d.%m.%Y') if user.premium_until else 'Не указано'}\n" - f"• Лимит вопросов: безлимитно\n\n" - ) - else: - remaining = max(0, settings.FREE_QUESTIONS_LIMIT - user.questions_used) - stats_text += ( - f"Бесплатный доступ:\n" - f"• Осталось вопросов: {remaining}\n" - f"• Для безлимита: /buy\n\n" - ) - else: - stats_text = ( - f"Добро пожаловать!\n\n" - f"Вы новый пользователь.\n" - f"• Ваш ID: {user_id}\n" - f"• Бесплатных вопросов: {settings.FREE_QUESTIONS_LIMIT}\n" - f"• Для начала работы просто задайте вопрос!\n\n" - f"Используйте /buy для получения полного доступа" - ) - - await message.answer(stats_text, parse_mode="HTML") - - except Exception as e: - await message.answer( - f"Ошибка получения статистики\n\n" - f"Попробуйте позже.", - parse_mode="HTML" + if user: + stats_text = ( + f"Ваша статистика\n\n" + f"Основное:\n" + f"• ID: {user_id}\n" + f"• Premium: {'Да' if user.is_premium else 'Нет'}\n" + f"• Вопросов использовано: {user.questions_used}/{settings.FREE_QUESTIONS_LIMIT}\n\n" ) + + if user.is_premium: + stats_text += ( + f"Premium статус:\n" + f"• Активен до: {user.premium_until.strftime('%d.%m.%Y') if user.premium_until else 'Не указано'}\n" + f"• Лимит вопросов: безлимитно\n\n" + ) + else: + remaining = max(0, settings.FREE_QUESTIONS_LIMIT - user.questions_used) + stats_text += ( + f"Бесплатный доступ:\n" + f"• Осталось вопросов: {remaining}\n" + f"• Для безлимита: /buy\n\n" + ) + else: + stats_text = ( + f"Добро пожаловать!\n\n" + f"Вы новый пользователь.\n" + f"• Ваш ID: {user_id}\n" + f"• Бесплатных вопросов: {settings.FREE_QUESTIONS_LIMIT}\n" + f"• Для начала работы просто задайте вопрос!\n\n" + f"Используйте /buy для получения полного доступа" + ) + + await message.answer(stats_text, parse_mode="HTML") + + except Exception as e: + await message.answer( + f"Ошибка получения статистики\n\n" + f"Попробуйте позже.", + parse_mode="HTML" + ) diff --git a/tg_bot/payment/webhooks/handler.py b/tg_bot/payment/webhooks/handler.py index c0cb1eb..4bbcb7f 100644 --- a/tg_bot/payment/webhooks/handler.py +++ b/tg_bot/payment/webhooks/handler.py @@ -19,9 +19,6 @@ async def handle_yookassa_webhook(request: Request): try: from tg_bot.config.settings import settings from tg_bot.domain.services.user_service import UserService - from tg_bot.infrastructure.database.database import AsyncSessionLocal - from tg_bot.infrastructure.database.models import UserModel - from sqlalchemy import select from aiogram import Bot if event_type == "payment.succeeded": @@ -29,38 +26,34 @@ async def handle_yookassa_webhook(request: Request): user_id = payment.get("metadata", {}).get("user_id") if user_id: - async with AsyncSessionLocal() as session: - user_service = UserService(session) - success = await user_service.activate_premium(int(user_id)) - if success: - print(f"Premium activated for user {user_id}") - - result = await session.execute( - select(UserModel).filter_by(telegram_id=str(user_id)) - ) - user = result.scalar_one_or_none() - - if user and settings.TELEGRAM_BOT_TOKEN: - try: - bot = Bot(token=settings.TELEGRAM_BOT_TOKEN) - premium_until = user.premium_until or datetime.now() + timedelta(days=30) - - notification = ( - f"Оплата подтверждена!\n\n" - f"Premium активирован до {premium_until.strftime('%d.%m.%Y')}" - ) - - await bot.send_message( - chat_id=int(user_id), - text=notification, - parse_mode="HTML" - ) - print(f"Notification sent to user {user_id}") - await bot.session.close() - except Exception as e: - print(f"Error sending notification: {e}") - else: - print(f"User {user_id} not found") + user_service = UserService() + success = await user_service.activate_premium(int(user_id)) + if success: + print(f"Premium activated for user {user_id}") + + user = await user_service.get_user_by_telegram_id(int(user_id)) + + if user and settings.TELEGRAM_BOT_TOKEN: + try: + bot = Bot(token=settings.TELEGRAM_BOT_TOKEN) + premium_until = user.premium_until or datetime.now() + timedelta(days=30) + + notification = ( + f"Оплата подтверждена!\n\n" + f"Premium активирован до {premium_until.strftime('%d.%m.%Y')}" + ) + + await bot.send_message( + chat_id=int(user_id), + text=notification, + parse_mode="HTML" + ) + print(f"Notification sent to user {user_id}") + await bot.session.close() + except Exception as e: + print(f"Error sending notification: {e}") + else: + print(f"User {user_id} not found or failed to activate premium") except ImportError as e: print(f"Import error: {e}") diff --git a/tg_bot/requirements.txt b/tg_bot/requirements.txt index 8a95e17..3cfc47f 100644 --- a/tg_bot/requirements.txt +++ b/tg_bot/requirements.txt @@ -2,8 +2,6 @@ pydantic>=2.5.0 pydantic-settings>=2.1.0 python-dotenv>=1.0.0 aiogram>=3.10.0 -sqlalchemy>=2.0.0 -aiosqlite>=0.19.0 httpx>=0.25.2 yookassa>=2.4.0 aiohttp>=3.9.1 diff --git a/tg_bot/run.py b/tg_bot/run.py new file mode 100644 index 0000000..d62b7ef --- /dev/null +++ b/tg_bot/run.py @@ -0,0 +1,20 @@ + +""" +Скрипт для запуска Telegram бота без Docker +""" +import sys +import os +from pathlib import Path + +tg_bot_dir = Path(__file__).parent +sys.path.insert(0, str(tg_bot_dir)) + +if __name__ == "__main__": + + from main import main + import asyncio + + asyncio.run(main()) + + +