Implement user-specific question numbering and update database schema. Added triggers for automatic question numbering and adjustments upon deletion. Enhanced CRUD operations to manage user_question_number effectively.
This commit is contained in:
255
services/infrastructure/database.py
Normal file
255
services/infrastructure/database.py
Normal file
@@ -0,0 +1,255 @@
|
||||
"""
|
||||
Сервис для работы с базой данных 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)
|
||||
self.questions = QuestionCRUD(db_path)
|
||||
self.user_blocks = UserBlockCRUD(db_path)
|
||||
self.user_settings = UserSettingsCRUD(db_path)
|
||||
|
||||
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 / "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()
|
||||
Reference in New Issue
Block a user