Files
AnonBot/services/infrastructure/database.py

256 lines
13 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""
Сервис для работы с базой данных SQLite
"""
import aiosqlite
from datetime import datetime
from typing import List, Optional, Dict, Any, Tuple
from contextlib import asynccontextmanager
from pathlib import Path
from models.user import User
from models.question import Question, QuestionStatus
from models.user_block import UserBlock
from models.user_settings import UserSettings
from database.crud import UserCRUD, QuestionCRUD, UserBlockCRUD, UserSettingsCRUD
from .logger import get_logger
logger = get_logger(__name__)
class DatabaseService:
"""Сервис для работы с базой данных"""
def __init__(self, db_path: str):
self.db_path = db_path
# Инициализируем CRUD операции с передачей логгера
self.users = UserCRUD(db_path, logger)
self.questions = QuestionCRUD(db_path, logger)
self.user_blocks = UserBlockCRUD(db_path, logger)
self.user_settings = UserSettingsCRUD(db_path, logger)
async def init(self):
"""Инициализация базы данных и создание таблиц"""
logger.info(f"💾 Инициализация базы данных: {self.db_path}")
# Создаем директорию для базы данных если её нет
db_path = Path(self.db_path)
db_path.parent.mkdir(parents=True, exist_ok=True)
async with self.get_connection() as conn:
await self._create_tables(conn)
logger.info("✅ База данных инициализирована")
@asynccontextmanager
async def get_connection(self):
"""Контекстный менеджер для подключения к БД с использованием пула"""
from database.crud import get_connection_pool
pool = get_connection_pool(self.db_path)
conn = await pool.get_connection()
try:
yield conn
finally:
await pool.return_connection(conn)
async def _create_tables(self, conn: aiosqlite.Connection):
"""Создание таблиц в базе данных"""
# Проверяем, существуют ли уже таблицы
cursor = await conn.execute("SELECT name FROM sqlite_master WHERE type='table' AND name='users';")
if await cursor.fetchone():
logger.info("📋 Таблицы уже существуют, пропускаем создание")
return
# Читаем схему из файла
schema_path = Path(__file__).parent.parent.parent / "database" / "schema.sql"
if schema_path.exists():
logger.info("📄 Создание таблиц из схемы")
with open(schema_path, 'r', encoding='utf-8') as f:
schema_sql = f.read()
# Выполняем SQL схему
await conn.executescript(schema_sql)
await conn.commit()
logger.info("✅ Таблицы созданы из схемы")
else:
logger.warning("⚠️ Файл схемы не найден, создаем таблицы вручную")
await self._create_tables_manual(conn)
async def _create_tables_manual(self, conn: aiosqlite.Connection):
"""Создание таблиц вручную если схема не найдена"""
# Простая схема для совместимости
await conn.execute("""
CREATE TABLE IF NOT EXISTS users (
id INTEGER PRIMARY KEY AUTOINCREMENT,
telegram_id INTEGER UNIQUE NOT NULL,
username TEXT,
first_name TEXT NOT NULL,
last_name TEXT,
chat_id INTEGER NOT NULL,
profile_link TEXT UNIQUE NOT NULL,
is_active BOOLEAN DEFAULT TRUE,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
banned_until DATETIME,
ban_reason TEXT
)
""")
await conn.execute("""
CREATE TABLE IF NOT EXISTS questions (
id INTEGER PRIMARY KEY AUTOINCREMENT,
from_user_id INTEGER,
to_user_id INTEGER NOT NULL,
message_text TEXT NOT NULL,
answer_text TEXT,
is_anonymous BOOLEAN DEFAULT TRUE,
message_id INTEGER,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
answered_at DATETIME,
is_read BOOLEAN DEFAULT FALSE,
status TEXT DEFAULT 'pending'
)
""")
await conn.commit()
# Обертки для CRUD операций (для совместимости)
# Пользователи
async def create_user(self, user: User) -> User:
"""Создание нового пользователя"""
return await self.users.create(user)
async def create_users_batch(self, users: List[User]) -> List[User]:
"""Создание нескольких пользователей за одну транзакцию (batch операция)"""
return await self.users.create_batch(users)
async def get_user(self, telegram_id: int) -> Optional[User]:
"""Получение пользователя по Telegram ID"""
return await self.users.get_by_telegram_id(telegram_id)
async def get_user_by_profile_link(self, profile_link: str) -> Optional[User]:
"""Получение пользователя по ссылке профиля"""
return await self.users.get_by_profile_link(profile_link)
async def update_user(self, user: User) -> User:
"""Обновление пользователя"""
return await self.users.update(user)
async def get_all_users(self, limit: int = 100, offset: int = 0) -> List[User]:
"""Получение всех пользователей"""
return await self.users.get_all(limit, offset)
async def get_all_users_cursor(self, last_id: int, last_created_at: str,
limit: int, direction: str = "desc") -> List[User]:
"""Получение пользователей с cursor-based пагинацией"""
return await self.users.get_all_users_cursor(last_id, last_created_at, limit, direction)
async def get_all_users_asc(self, limit: int = 100, offset: int = 0) -> List[User]:
"""Получение всех пользователей в порядке возрастания"""
return await self.users.get_all_users_asc(limit, offset)
async def get_users_stats(self) -> Dict[str, Any]:
"""Получение статистики пользователей"""
return await self.users.get_stats()
# Вопросы
async def create_question(self, question: Question) -> Question:
"""Создание нового вопроса"""
return await self.questions.create(question)
async def create_questions_batch(self, questions: List[Question]) -> List[Question]:
"""Создание нескольких вопросов за одну транзакцию (batch операция)"""
return await self.questions.create_batch(questions)
async def get_question(self, question_id: int) -> Optional[Question]:
"""Получение вопроса по ID"""
return await self.questions.get_by_id(question_id)
async def get_user_questions(self, user_id: int, status: Optional[QuestionStatus] = None,
limit: int = 50, offset: int = 0) -> List[Question]:
"""Получение вопросов пользователя"""
return await self.questions.get_by_to_user(user_id, status, limit, offset)
async def get_user_questions_with_authors(self, user_id: int, status: Optional[QuestionStatus] = None,
limit: int = 50, offset: int = 0) -> List[Tuple[Question, Optional[User]]]:
"""Получение вопросов пользователя с информацией об авторах (оптимизированный запрос)"""
return await self.questions.get_by_to_user_with_authors(user_id, status, limit, offset)
async def get_user_questions_cursor(self, user_id: int, last_id: int, last_created_at: str,
limit: int, direction: str = "desc") -> List[Question]:
"""Получение вопросов пользователя с cursor-based пагинацией"""
return await self.questions.get_by_to_user_cursor(user_id, last_id, last_created_at, limit, direction)
async def get_user_questions_asc(self, user_id: int, status: Optional[QuestionStatus] = None,
limit: int = 50, offset: int = 0) -> List[Question]:
"""Получение вопросов пользователя в порядке возрастания"""
return await self.questions.get_by_to_user_asc(user_id, status, limit, offset)
async def update_question(self, question: Question) -> Question:
"""Обновление вопроса"""
return await self.questions.update(question)
async def get_questions_stats(self) -> Dict[str, Any]:
"""Получение статистики вопросов"""
return await self.questions.get_stats()
async def get_unread_questions_count(self, user_id: int) -> int:
"""Получение количества непрочитанных вопросов"""
return await self.questions.get_unread_count(user_id)
async def get_user_questions_count(self, user_id: int, status: Optional[QuestionStatus] = None) -> int:
"""Получение общего количества вопросов пользователя"""
return await self.questions.get_count_by_to_user(user_id, status)
# Блокировки
async def block_user(self, blocker_id: int, blocked_id: int) -> UserBlock:
"""Блокировка пользователя"""
user_block = UserBlock(
blocker_id=blocker_id,
blocked_id=blocked_id,
created_at=datetime.now()
)
return await self.user_blocks.create(user_block)
async def unblock_user(self, blocker_id: int, blocked_id: int) -> bool:
"""Разблокировка пользователя"""
return await self.user_blocks.delete(blocker_id, blocked_id)
async def is_user_blocked(self, blocker_id: int, blocked_id: int) -> bool:
"""Проверка, заблокирован ли пользователь"""
return await self.user_blocks.is_blocked(blocker_id, blocked_id)
# Настройки
async def get_user_settings(self, user_id: int) -> Optional[UserSettings]:
"""Получение настроек пользователя"""
return await self.user_settings.get_by_user_id(user_id)
async def get_user_by_id(self, user_id: int) -> Optional[User]:
"""Получение пользователя по ID (для получения информации об авторах вопросов)"""
return await self.users.get_by_telegram_id(user_id)
async def update_user_settings(self, settings: UserSettings) -> UserSettings:
"""Обновление настроек пользователя"""
return await self.user_settings.update(settings)
async def create_user_settings(self, settings: UserSettings) -> UserSettings:
"""Создание настроек пользователя"""
return await self.user_settings.create(settings)
async def check_connection(self):
"""Проверка соединения с базой данных"""
try:
async with self.get_connection() as conn:
# Выполняем простой запрос для проверки соединения
cursor = await conn.execute("SELECT 1")
await cursor.fetchone()
logger.debug("Database connection check successful")
except Exception as e:
logger.error(f"Database connection check failed: {e}")
raise
async def close(self):
"""Закрытие соединения с БД"""
from database.crud import get_connection_pool
pool = get_connection_pool(self.db_path)
await pool.close_all()