905 lines
42 KiB
Python
905 lines
42 KiB
Python
"""
|
||
CRUD операции для работы с базой данных
|
||
"""
|
||
import asyncio
|
||
from contextlib import asynccontextmanager
|
||
from datetime import datetime
|
||
from functools import lru_cache
|
||
from typing import Any, Dict, List, Optional, Tuple
|
||
|
||
import aiosqlite
|
||
|
||
from config.constants import DEFAULT_CONNECTION_POOL_SIZE, DATABASE_TIMEOUT, SQLITE_CACHE_SIZE, EMPTY_VALUES
|
||
from models.question import Question, QuestionStatus
|
||
from models.user import User
|
||
from models.user_block import UserBlock
|
||
from models.user_settings import UserSettings
|
||
from services.infrastructure.logger import get_logger
|
||
|
||
logger = get_logger(__name__)
|
||
|
||
|
||
class ConnectionPool:
|
||
"""Пул соединений для SQLite"""
|
||
|
||
def __init__(self, db_path: str, pool_size: int = DEFAULT_CONNECTION_POOL_SIZE):
|
||
self.db_path = db_path
|
||
self.pool_size = pool_size
|
||
self._pool = asyncio.Queue(maxsize=pool_size)
|
||
self._created_connections = 0
|
||
self._lock = asyncio.Lock()
|
||
|
||
async def _create_connection(self):
|
||
"""Создание нового соединения"""
|
||
conn = await aiosqlite.connect(
|
||
self.db_path,
|
||
timeout=DATABASE_TIMEOUT, # Увеличиваем timeout
|
||
isolation_level=None # Автокоммит для лучшей производительности
|
||
)
|
||
# Настройки для лучшей производительности
|
||
await conn.execute("PRAGMA journal_mode=WAL")
|
||
await conn.execute("PRAGMA synchronous=NORMAL")
|
||
await conn.execute(f"PRAGMA cache_size={SQLITE_CACHE_SIZE}")
|
||
await conn.execute("PRAGMA temp_store=MEMORY")
|
||
return conn
|
||
|
||
async def get_connection(self):
|
||
"""Получение соединения из пула"""
|
||
try:
|
||
# Пытаемся получить соединение из пула
|
||
return self._pool.get_nowait()
|
||
except asyncio.QueueEmpty:
|
||
# Если пул пуст, создаем новое соединение
|
||
async with self._lock:
|
||
if self._created_connections < self.pool_size:
|
||
self._created_connections += 1
|
||
return await self._create_connection()
|
||
else:
|
||
# Ждем освобождения соединения
|
||
return await self._pool.get()
|
||
|
||
async def return_connection(self, conn):
|
||
"""Возврат соединения в пул"""
|
||
try:
|
||
self._pool.put_nowait(conn)
|
||
except asyncio.QueueFull:
|
||
# Если пул полон, закрываем соединение
|
||
await conn.close()
|
||
async with self._lock:
|
||
self._created_connections -= 1
|
||
|
||
async def close_all(self):
|
||
"""Закрытие всех соединений"""
|
||
while not self._pool.empty():
|
||
conn = await self._pool.get()
|
||
await conn.close()
|
||
self._created_connections = 0
|
||
|
||
|
||
# Глобальный пул соединений
|
||
_connection_pools = {}
|
||
|
||
|
||
def get_connection_pool(db_path: str, pool_size: int = DEFAULT_CONNECTION_POOL_SIZE) -> ConnectionPool:
|
||
"""Получение пула соединений для базы данных"""
|
||
if db_path not in _connection_pools:
|
||
_connection_pools[db_path] = ConnectionPool(db_path, pool_size)
|
||
return _connection_pools[db_path]
|
||
|
||
|
||
class BaseCRUD:
|
||
"""Базовый класс для CRUD операций"""
|
||
|
||
def __init__(self, db_path: str):
|
||
self.db_path = db_path
|
||
self.pool = get_connection_pool(db_path)
|
||
|
||
@asynccontextmanager
|
||
async def get_connection(self):
|
||
"""Контекстный менеджер для подключения к БД с использованием пула"""
|
||
conn = await self.pool.get_connection()
|
||
try:
|
||
yield conn
|
||
finally:
|
||
await self.pool.return_connection(conn)
|
||
|
||
def _parse_datetime(self, date_str) -> Optional[datetime]:
|
||
"""Безопасный парсинг datetime из строки"""
|
||
if not date_str or date_str in EMPTY_VALUES:
|
||
return None
|
||
try:
|
||
return datetime.fromisoformat(date_str)
|
||
except (ValueError, TypeError):
|
||
return None
|
||
|
||
|
||
class UserCRUD(BaseCRUD):
|
||
"""CRUD операции для пользователей"""
|
||
|
||
async def create(self, user: User) -> User:
|
||
"""Создание нового пользователя"""
|
||
logger.info(f"👤 Создание пользователя: {user.telegram_id} ({user.first_name})")
|
||
async with self.get_connection() as conn:
|
||
cursor = await conn.execute("""
|
||
INSERT INTO users
|
||
(telegram_id, username, first_name, last_name, chat_id, profile_link,
|
||
is_active, is_superuser, created_at, updated_at, banned_until, ban_reason)
|
||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||
""", (
|
||
user.telegram_id, user.username, user.first_name, user.last_name,
|
||
user.chat_id, user.profile_link, user.is_active, user.is_superuser,
|
||
user.created_at.isoformat() if user.created_at else None,
|
||
user.updated_at.isoformat() if user.updated_at else None,
|
||
user.banned_until.isoformat() if user.banned_until else None,
|
||
user.ban_reason
|
||
))
|
||
user.id = cursor.lastrowid
|
||
await conn.commit()
|
||
logger.info(f"✅ Пользователь создан с ID: {user.id}")
|
||
return user
|
||
|
||
async def create_batch(self, users: List[User]) -> List[User]:
|
||
"""Создание нескольких пользователей за одну транзакцию (batch операция)"""
|
||
if not users:
|
||
return []
|
||
|
||
logger.info(f"📦 Создание {len(users)} пользователей batch операцией")
|
||
async with self.get_connection() as conn:
|
||
try:
|
||
# Подготавливаем данные для batch вставки
|
||
batch_data = []
|
||
for user in users:
|
||
batch_data.append((
|
||
user.telegram_id, user.username, user.first_name, user.last_name,
|
||
user.chat_id, user.profile_link, user.is_active, user.is_superuser,
|
||
user.created_at.isoformat() if user.created_at else None,
|
||
user.updated_at.isoformat() if user.updated_at else None,
|
||
user.banned_until.isoformat() if user.banned_until else None,
|
||
user.ban_reason
|
||
))
|
||
|
||
# Выполняем batch вставку
|
||
cursor = await conn.executemany("""
|
||
INSERT INTO users
|
||
(telegram_id, username, first_name, last_name, chat_id, profile_link,
|
||
is_active, is_superuser, created_at, updated_at, banned_until, ban_reason)
|
||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||
""", batch_data)
|
||
|
||
# Обновляем ID для всех созданных пользователей
|
||
first_id = cursor.lastrowid - len(users) + 1
|
||
for i, user in enumerate(users):
|
||
user.id = first_id + i
|
||
|
||
await conn.commit()
|
||
logger.info(f"✅ Создано {len(users)} пользователей batch операцией")
|
||
return users
|
||
|
||
except Exception as e:
|
||
await conn.rollback()
|
||
logger.error(f"❌ Ошибка при batch создании пользователей: {e}")
|
||
raise
|
||
|
||
async def get_by_telegram_id(self, telegram_id: int) -> Optional[User]:
|
||
"""Получение пользователя по Telegram ID"""
|
||
async with self.get_connection() as conn:
|
||
async with conn.execute("""
|
||
SELECT * FROM users WHERE telegram_id = ?
|
||
""", (telegram_id,)) as cursor:
|
||
row = await cursor.fetchone()
|
||
if row:
|
||
return self._row_to_user(row)
|
||
return None
|
||
|
||
async def get_by_profile_link(self, profile_link: str) -> Optional[User]:
|
||
"""Получение пользователя по ссылке профиля"""
|
||
async with self.get_connection() as conn:
|
||
async with conn.execute("""
|
||
SELECT * FROM users WHERE profile_link = ?
|
||
""", (profile_link,)) as cursor:
|
||
row = await cursor.fetchone()
|
||
if row:
|
||
return self._row_to_user(row)
|
||
return None
|
||
|
||
async def update(self, user: User) -> User:
|
||
"""Обновление пользователя"""
|
||
async with self.get_connection() as conn:
|
||
await conn.execute("""
|
||
UPDATE users SET
|
||
username = ?, first_name = ?, last_name = ?, chat_id = ?,
|
||
profile_link = ?, is_active = ?, is_superuser = ?, updated_at = ?,
|
||
banned_until = ?, ban_reason = ?
|
||
WHERE telegram_id = ?
|
||
""", (
|
||
user.username, user.first_name, user.last_name, user.chat_id,
|
||
user.profile_link, user.is_active, user.is_superuser,
|
||
user.updated_at.isoformat() if user.updated_at else None,
|
||
user.banned_until.isoformat() if user.banned_until else None,
|
||
user.ban_reason, user.telegram_id
|
||
))
|
||
await conn.commit()
|
||
return user
|
||
|
||
async def delete(self, telegram_id: int) -> bool:
|
||
"""Удаление пользователя"""
|
||
async with self.get_connection() as conn:
|
||
cursor = await conn.execute("""
|
||
DELETE FROM users WHERE telegram_id = ?
|
||
""", (telegram_id,))
|
||
await conn.commit()
|
||
return cursor.rowcount > 0
|
||
|
||
async def get_all(self, limit: int = 100, offset: int = 0) -> List[User]:
|
||
"""Получение всех пользователей"""
|
||
async with self.get_connection() as conn:
|
||
async with conn.execute("""
|
||
SELECT * FROM users
|
||
ORDER BY created_at DESC
|
||
LIMIT ? OFFSET ?
|
||
""", (limit, offset)) as cursor:
|
||
rows = await cursor.fetchall()
|
||
return [self._row_to_user(row) for row in rows]
|
||
|
||
async def get_all_users_cursor(
|
||
self,
|
||
last_id: int,
|
||
last_created_at: str,
|
||
limit: int,
|
||
direction: str = "desc"
|
||
) -> List[User]:
|
||
"""Получение пользователей с cursor-based пагинацией"""
|
||
async with self.get_connection() as conn:
|
||
if direction == "desc":
|
||
query = """
|
||
SELECT * FROM users
|
||
WHERE (created_at < ? OR (created_at = ? AND id < ?))
|
||
ORDER BY created_at DESC, id DESC
|
||
LIMIT ?
|
||
"""
|
||
params = [last_created_at, last_created_at, last_id, limit]
|
||
else:
|
||
query = """
|
||
SELECT * FROM users
|
||
WHERE (created_at > ? OR (created_at = ? AND id > ?))
|
||
ORDER BY created_at ASC, id ASC
|
||
LIMIT ?
|
||
"""
|
||
params = [last_created_at, last_created_at, last_id, limit]
|
||
|
||
async with conn.execute(query, params) as cursor:
|
||
rows = await cursor.fetchall()
|
||
return [self._row_to_user(row) for row in rows]
|
||
|
||
async def get_all_users_asc(self, limit: int = 100, offset: int = 0) -> List[User]:
|
||
"""Получение всех пользователей в порядке возрастания"""
|
||
async with self.get_connection() as conn:
|
||
async with conn.execute("""
|
||
SELECT * FROM users
|
||
ORDER BY created_at ASC
|
||
LIMIT ? OFFSET ?
|
||
""", (limit, offset)) as cursor:
|
||
rows = await cursor.fetchall()
|
||
return [self._row_to_user(row) for row in rows]
|
||
|
||
async def get_stats(self) -> Dict[str, Any]:
|
||
"""Получение статистики пользователей"""
|
||
async with self.get_connection() as conn:
|
||
async with conn.execute("""
|
||
SELECT
|
||
COUNT(*) as total_users,
|
||
COUNT(CASE WHEN is_active = TRUE THEN 1 END) as active_users,
|
||
COUNT(CASE WHEN is_superuser = TRUE THEN 1 END) as superusers,
|
||
COUNT(CASE WHEN created_at > datetime('now', '-7 days') THEN 1 END) as new_users_week,
|
||
COUNT(CASE WHEN created_at > datetime('now', '-1 day') THEN 1 END) as new_users_today,
|
||
COUNT(CASE WHEN banned_until IS NOT NULL AND banned_until > datetime('now') THEN 1 END) as banned_users
|
||
FROM users
|
||
""") as cursor:
|
||
row = await cursor.fetchone()
|
||
return {
|
||
'total_users': row[0],
|
||
'active_users': row[1],
|
||
'superusers': row[2],
|
||
'new_users_week': row[3],
|
||
'new_users_today': row[4],
|
||
'banned_users': row[5]
|
||
}
|
||
|
||
def _row_to_user(self, row) -> User:
|
||
"""Преобразование строки БД в объект User"""
|
||
return User(
|
||
id=row[0],
|
||
telegram_id=row[1],
|
||
username=row[2],
|
||
first_name=row[3],
|
||
last_name=row[4],
|
||
chat_id=row[5],
|
||
profile_link=row[6],
|
||
is_active=bool(row[7]),
|
||
is_superuser=bool(row[12]), # Исправлено: is_superuser находится на позиции 12
|
||
created_at=self._parse_datetime(row[8]),
|
||
updated_at=self._parse_datetime(row[9]),
|
||
banned_until=self._parse_datetime(row[10]),
|
||
ban_reason=row[11]
|
||
)
|
||
|
||
|
||
class QuestionCRUD(BaseCRUD):
|
||
"""CRUD операции для вопросов"""
|
||
|
||
async def create(self, question: Question) -> Question:
|
||
"""Создание нового вопроса"""
|
||
logger.info(f"❓ Создание вопроса от {question.from_user_id} к {question.to_user_id}")
|
||
async with self.get_connection() as conn:
|
||
# Вычисляем user_question_number для получателя
|
||
if question.user_question_number is None:
|
||
cursor = await conn.execute("""
|
||
SELECT COALESCE(MAX(user_question_number), 0) + 1
|
||
FROM questions
|
||
WHERE to_user_id = ? AND status != 'deleted'
|
||
""", (question.to_user_id,))
|
||
result = await cursor.fetchone()
|
||
question.user_question_number = result[0] if result else 1
|
||
|
||
cursor = await conn.execute("""
|
||
INSERT INTO questions
|
||
(from_user_id, to_user_id, message_text, answer_text, is_anonymous,
|
||
message_id, created_at, answered_at, is_read, status, user_question_number)
|
||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||
""", (
|
||
question.from_user_id, question.to_user_id, question.message_text,
|
||
question.answer_text, question.is_anonymous, question.message_id,
|
||
question.created_at.isoformat() if question.created_at else None,
|
||
question.answered_at.isoformat() if question.answered_at else None,
|
||
question.is_read, question.status.value, question.user_question_number
|
||
))
|
||
question.id = cursor.lastrowid
|
||
await conn.commit()
|
||
logger.info(f"✅ Вопрос создан с ID: {question.id}, номер для пользователя: {question.user_question_number}")
|
||
return question
|
||
|
||
async def create_batch(self, questions: List[Question]) -> List[Question]:
|
||
"""Создание нескольких вопросов за одну транзакцию (batch операция)"""
|
||
if not questions:
|
||
return []
|
||
|
||
logger.info(f"📦 Создание {len(questions)} вопросов batch операцией")
|
||
async with self.get_connection() as conn:
|
||
try:
|
||
# Группируем вопросы по получателям для вычисления user_question_number
|
||
questions_by_user = {}
|
||
for question in questions:
|
||
if question.to_user_id not in questions_by_user:
|
||
questions_by_user[question.to_user_id] = []
|
||
questions_by_user[question.to_user_id].append(question)
|
||
|
||
# Вычисляем user_question_number для каждого пользователя
|
||
for to_user_id, user_questions in questions_by_user.items():
|
||
cursor = await conn.execute("""
|
||
SELECT COALESCE(MAX(user_question_number), 0)
|
||
FROM questions
|
||
WHERE to_user_id = ? AND status != 'deleted'
|
||
""", (to_user_id,))
|
||
result = await cursor.fetchone()
|
||
start_number = (result[0] if result else 0) + 1
|
||
|
||
for i, question in enumerate(user_questions):
|
||
if question.user_question_number is None:
|
||
question.user_question_number = start_number + i
|
||
|
||
# Подготавливаем данные для batch вставки
|
||
batch_data = []
|
||
for question in questions:
|
||
batch_data.append((
|
||
question.from_user_id, question.to_user_id, question.message_text,
|
||
question.answer_text, question.is_anonymous, question.message_id,
|
||
question.created_at.isoformat() if question.created_at else None,
|
||
question.answered_at.isoformat() if question.answered_at else None,
|
||
question.is_read, question.status.value, question.user_question_number
|
||
))
|
||
|
||
# Выполняем batch вставку
|
||
cursor = await conn.executemany("""
|
||
INSERT INTO questions
|
||
(from_user_id, to_user_id, message_text, answer_text, is_anonymous,
|
||
message_id, created_at, answered_at, is_read, status, user_question_number)
|
||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||
""", batch_data)
|
||
|
||
# Обновляем ID для всех созданных вопросов
|
||
first_id = cursor.lastrowid - len(questions) + 1
|
||
for i, question in enumerate(questions):
|
||
question.id = first_id + i
|
||
|
||
await conn.commit()
|
||
logger.info(f"✅ Создано {len(questions)} вопросов batch операцией")
|
||
return questions
|
||
|
||
except Exception as e:
|
||
await conn.rollback()
|
||
logger.error(f"❌ Ошибка при batch создании вопросов: {e}")
|
||
raise
|
||
|
||
async def get_by_id(self, question_id: int) -> Optional[Question]:
|
||
"""Получение вопроса по ID"""
|
||
async with self.get_connection() as conn:
|
||
async with conn.execute("""
|
||
SELECT
|
||
id, from_user_id, to_user_id, message_text, answer_text,
|
||
is_anonymous, message_id, created_at, answered_at,
|
||
is_read, status, user_question_number
|
||
FROM questions WHERE id = ?
|
||
""", (question_id,)) as cursor:
|
||
row = await cursor.fetchone()
|
||
if row:
|
||
return self._row_to_question(row)
|
||
return None
|
||
|
||
async def get_by_to_user(self, to_user_id: int, status: Optional[QuestionStatus] = None,
|
||
limit: int = 50, offset: int = 0) -> List[Question]:
|
||
"""Получение вопросов для пользователя (оптимизированная версия с JOIN)"""
|
||
async with self.get_connection() as conn:
|
||
query = """
|
||
SELECT
|
||
q.id, q.from_user_id, q.to_user_id, q.message_text, q.answer_text,
|
||
q.is_anonymous, q.message_id, q.created_at, q.answered_at,
|
||
q.is_read, q.status, q.user_question_number
|
||
FROM questions q
|
||
WHERE q.to_user_id = ?
|
||
"""
|
||
params = [to_user_id]
|
||
|
||
if status:
|
||
query += " AND q.status = ?"
|
||
params.append(status.value)
|
||
|
||
query += " ORDER BY q.user_question_number DESC LIMIT ? OFFSET ?"
|
||
params.extend([limit, offset])
|
||
|
||
async with conn.execute(query, params) as cursor:
|
||
rows = await cursor.fetchall()
|
||
return [self._row_to_question(row) for row in rows]
|
||
|
||
async def get_by_to_user_with_authors(self, to_user_id: int, status: Optional[QuestionStatus] = None,
|
||
limit: int = 50, offset: int = 0) -> List[Tuple[Question, Optional[User]]]:
|
||
"""Получение вопросов для пользователя с информацией об авторах (оптимизированный запрос)"""
|
||
async with self.get_connection() as conn:
|
||
query = """
|
||
SELECT
|
||
q.*,
|
||
u.id as author_id,
|
||
u.telegram_id as author_telegram_id,
|
||
u.username as author_username,
|
||
u.first_name as author_first_name,
|
||
u.last_name as author_last_name,
|
||
u.chat_id as author_chat_id,
|
||
u.profile_link as author_profile_link,
|
||
u.is_active as author_is_active,
|
||
u.is_superuser as author_is_superuser,
|
||
u.created_at as author_created_at,
|
||
u.updated_at as author_updated_at,
|
||
u.banned_until as author_banned_until,
|
||
u.ban_reason as author_ban_reason
|
||
FROM questions q
|
||
LEFT JOIN users u ON q.from_user_id = u.telegram_id
|
||
WHERE q.to_user_id = ?
|
||
"""
|
||
params = [to_user_id]
|
||
|
||
if status:
|
||
query += " AND q.status = ?"
|
||
params.append(status.value)
|
||
|
||
query += " ORDER BY q.user_question_number DESC LIMIT ? OFFSET ?"
|
||
params.extend([limit, offset])
|
||
|
||
async with conn.execute(query, params) as cursor:
|
||
rows = await cursor.fetchall()
|
||
result = []
|
||
for row in rows:
|
||
try:
|
||
question = self._row_to_question(row[:11]) # Первые 11 колонок - это вопрос
|
||
if question is None:
|
||
print(f"Предупреждение: вопрос не создан для строки {row[:11]}")
|
||
continue
|
||
|
||
author = None
|
||
if row[11]: # Если есть author_id
|
||
author = User(
|
||
id=row[11],
|
||
telegram_id=row[12],
|
||
username=row[13],
|
||
first_name=row[14] or "",
|
||
last_name=row[15],
|
||
chat_id=row[16],
|
||
profile_link=row[17] or "",
|
||
is_active=bool(row[18]),
|
||
is_superuser=bool(row[19]),
|
||
created_at=self._parse_datetime(row[20]),
|
||
updated_at=self._parse_datetime(row[21]),
|
||
banned_until=self._parse_datetime(row[22]),
|
||
ban_reason=row[23]
|
||
)
|
||
result.append((question, author))
|
||
except Exception as e:
|
||
print(f"Ошибка при создании вопроса из строки {row[:11]}: {e}")
|
||
continue
|
||
return result
|
||
|
||
async def get_by_to_user_cursor(
|
||
self,
|
||
to_user_id: int,
|
||
last_id: int,
|
||
last_created_at: str,
|
||
limit: int,
|
||
direction: str = "desc"
|
||
) -> List[Question]:
|
||
"""Получение вопросов с cursor-based пагинацией"""
|
||
async with self.get_connection() as conn:
|
||
if direction == "desc":
|
||
query = """
|
||
SELECT
|
||
q.id, q.from_user_id, q.to_user_id, q.message_text, q.answer_text,
|
||
q.is_anonymous, q.message_id, q.created_at, q.answered_at,
|
||
q.is_read, q.status, q.user_question_number
|
||
FROM questions q
|
||
WHERE q.to_user_id = ?
|
||
AND (q.created_at < ? OR (q.created_at = ? AND q.id < ?))
|
||
ORDER BY q.created_at DESC, q.id DESC
|
||
LIMIT ?
|
||
"""
|
||
params = [to_user_id, last_created_at, last_created_at, last_id, limit]
|
||
else:
|
||
query = """
|
||
SELECT
|
||
q.id, q.from_user_id, q.to_user_id, q.message_text, q.answer_text,
|
||
q.is_anonymous, q.message_id, q.created_at, q.answered_at,
|
||
q.is_read, q.status, q.user_question_number
|
||
FROM questions q
|
||
WHERE q.to_user_id = ?
|
||
AND (q.created_at > ? OR (q.created_at = ? AND q.id > ?))
|
||
ORDER BY q.created_at ASC, q.id ASC
|
||
LIMIT ?
|
||
"""
|
||
params = [to_user_id, last_created_at, last_created_at, last_id, limit]
|
||
|
||
async with conn.execute(query, params) as cursor:
|
||
rows = await cursor.fetchall()
|
||
return [self._row_to_question(row) for row in rows]
|
||
|
||
async def get_by_to_user_asc(
|
||
self,
|
||
to_user_id: int,
|
||
status: Optional[QuestionStatus] = None,
|
||
limit: int = 50,
|
||
offset: int = 0
|
||
) -> List[Question]:
|
||
"""Получение вопросов для пользователя в порядке возрастания"""
|
||
async with self.get_connection() as conn:
|
||
query = """
|
||
SELECT
|
||
q.id, q.from_user_id, q.to_user_id, q.message_text, q.answer_text,
|
||
q.is_anonymous, q.message_id, q.created_at, q.answered_at,
|
||
q.is_read, q.status
|
||
FROM questions q
|
||
WHERE q.to_user_id = ?
|
||
"""
|
||
params = [to_user_id]
|
||
|
||
if status:
|
||
query += " AND q.status = ?"
|
||
params.append(status.value)
|
||
|
||
query += " ORDER BY q.user_question_number ASC LIMIT ? OFFSET ?"
|
||
params.extend([limit, offset])
|
||
|
||
async with conn.execute(query, params) as cursor:
|
||
rows = await cursor.fetchall()
|
||
return [self._row_to_question(row) for row in rows]
|
||
|
||
async def update(self, question: Question) -> Question:
|
||
"""Обновление вопроса"""
|
||
logger.info(f"📝 Обновление вопроса {question.id} (статус: {question.status.value})")
|
||
async with self.get_connection() as conn:
|
||
# Если вопрос помечается как удаленный, нужно пересчитать номера
|
||
if question.status.value == 'deleted':
|
||
# Получаем текущий статус вопроса
|
||
cursor = await conn.execute("""
|
||
SELECT status, to_user_id, user_question_number FROM questions WHERE id = ?
|
||
""", (question.id,))
|
||
old_info = await cursor.fetchone()
|
||
|
||
if old_info and old_info[0] != 'deleted':
|
||
# Вопрос переходит в статус 'deleted', пересчитываем номера
|
||
to_user_id, deleted_number = old_info[1], old_info[2]
|
||
|
||
# Сначала обновляем вопрос, устанавливая user_question_number в NULL
|
||
await conn.execute("""
|
||
UPDATE questions SET
|
||
answer_text = ?, status = ?, answered_at = ?, is_read = ?,
|
||
user_question_number = NULL
|
||
WHERE id = ?
|
||
""", (
|
||
question.answer_text, question.status.value,
|
||
question.answered_at.isoformat() if question.answered_at else None,
|
||
question.is_read, question.id
|
||
))
|
||
|
||
# Обновляем объект question, устанавливая user_question_number в None
|
||
question.user_question_number = None
|
||
|
||
# Пересчитываем номера для всех вопросов пользователя после удаленного
|
||
await conn.execute("""
|
||
UPDATE questions
|
||
SET user_question_number = user_question_number - 1
|
||
WHERE to_user_id = ?
|
||
AND user_question_number > ?
|
||
AND status != 'deleted'
|
||
AND id != ?
|
||
""", (to_user_id, deleted_number, question.id))
|
||
|
||
logger.info(f"🗑️ Вопрос {question.id} помечен как удаленный, пересчитаны номера для пользователя {to_user_id}")
|
||
else:
|
||
# Обычное обновление
|
||
await conn.execute("""
|
||
UPDATE questions SET
|
||
answer_text = ?, status = ?, answered_at = ?, is_read = ?
|
||
WHERE id = ?
|
||
""", (
|
||
question.answer_text, question.status.value,
|
||
question.answered_at.isoformat() if question.answered_at else None,
|
||
question.is_read, question.id
|
||
))
|
||
else:
|
||
# Обычное обновление
|
||
await conn.execute("""
|
||
UPDATE questions SET
|
||
answer_text = ?, status = ?, answered_at = ?, is_read = ?
|
||
WHERE id = ?
|
||
""", (
|
||
question.answer_text, question.status.value,
|
||
question.answered_at.isoformat() if question.answered_at else None,
|
||
question.is_read, question.id
|
||
))
|
||
|
||
await conn.commit()
|
||
logger.info(f"✅ Вопрос {question.id} обновлен")
|
||
return question
|
||
|
||
async def delete(self, question_id: int) -> bool:
|
||
"""Удаление вопроса с пересчетом user_question_number"""
|
||
async with self.get_connection() as conn:
|
||
# Сначала получаем информацию о вопросе
|
||
cursor = await conn.execute("""
|
||
SELECT to_user_id, user_question_number FROM questions WHERE id = ?
|
||
""", (question_id,))
|
||
question_info = await cursor.fetchone()
|
||
|
||
if not question_info:
|
||
return False
|
||
|
||
to_user_id, deleted_number = question_info
|
||
|
||
# Удаляем вопрос
|
||
cursor = await conn.execute("""
|
||
DELETE FROM questions WHERE id = ?
|
||
""", (question_id,))
|
||
|
||
if cursor.rowcount == 0:
|
||
return False
|
||
|
||
# Пересчитываем номера для всех вопросов пользователя после удаленного
|
||
await conn.execute("""
|
||
UPDATE questions
|
||
SET user_question_number = user_question_number - 1
|
||
WHERE to_user_id = ?
|
||
AND user_question_number > ?
|
||
AND status != 'deleted'
|
||
""", (to_user_id, deleted_number))
|
||
|
||
await conn.commit()
|
||
logger.info(f"🗑️ Вопрос {question_id} удален, пересчитаны номера для пользователя {to_user_id}")
|
||
return True
|
||
|
||
async def get_unread_count(self, to_user_id: int) -> int:
|
||
"""Получение количества непрочитанных вопросов"""
|
||
async with self.get_connection() as conn:
|
||
async with conn.execute("""
|
||
SELECT COUNT(*) FROM questions
|
||
WHERE to_user_id = ? AND is_read = FALSE AND status = 'pending'
|
||
""", (to_user_id,)) as cursor:
|
||
row = await cursor.fetchone()
|
||
return row[0]
|
||
|
||
async def get_count_by_to_user(self, to_user_id: int, status: Optional[QuestionStatus] = None) -> int:
|
||
"""Получение общего количества вопросов для пользователя"""
|
||
async with self.get_connection() as conn:
|
||
query = "SELECT COUNT(*) FROM questions WHERE to_user_id = ?"
|
||
params = [to_user_id]
|
||
|
||
if status:
|
||
query += " AND status = ?"
|
||
params.append(status.value)
|
||
|
||
async with conn.execute(query, params) as cursor:
|
||
row = await cursor.fetchone()
|
||
return row[0]
|
||
|
||
async def get_stats(self) -> Dict[str, Any]:
|
||
"""Получение статистики вопросов"""
|
||
async with self.get_connection() as conn:
|
||
async with conn.execute("""
|
||
SELECT
|
||
COUNT(*) as total_questions,
|
||
COUNT(CASE WHEN status = 'pending' THEN 1 END) as pending_questions,
|
||
COUNT(CASE WHEN status = 'answered' THEN 1 END) as answered_questions,
|
||
COUNT(CASE WHEN status = 'rejected' THEN 1 END) as rejected_questions,
|
||
COUNT(CASE WHEN created_at > datetime('now', '-1 day') THEN 1 END) as questions_today,
|
||
COUNT(CASE WHEN created_at > datetime('now', '-7 days') THEN 1 END) as questions_week,
|
||
COUNT(CASE WHEN is_anonymous = TRUE THEN 1 END) as anonymous_questions
|
||
FROM questions
|
||
""") as cursor:
|
||
row = await cursor.fetchone()
|
||
return {
|
||
'total_questions': row[0],
|
||
'pending_questions': row[1],
|
||
'answered_questions': row[2],
|
||
'rejected_questions': row[3],
|
||
'questions_today': row[4],
|
||
'questions_week': row[5],
|
||
'anonymous_questions': row[6]
|
||
}
|
||
|
||
def _row_to_question(self, row) -> Question:
|
||
"""Преобразование строки БД в объект Question"""
|
||
# Проверяем, что все необходимые поля присутствуют
|
||
if len(row) < 11:
|
||
raise ValueError(f"Недостаточно данных в строке БД: {len(row)} колонок, ожидается минимум 11")
|
||
|
||
# Проверяем статус
|
||
status_value = row[10]
|
||
if status_value is None:
|
||
status = QuestionStatus.PENDING # Значение по умолчанию
|
||
else:
|
||
try:
|
||
status = QuestionStatus(status_value)
|
||
except ValueError:
|
||
print(f"Неизвестный статус вопроса: {status_value}, используем PENDING")
|
||
status = QuestionStatus.PENDING
|
||
|
||
question = Question(
|
||
id=row[0],
|
||
from_user_id=row[1],
|
||
to_user_id=row[2],
|
||
message_text=row[3],
|
||
answer_text=row[4],
|
||
is_anonymous=bool(row[5]),
|
||
message_id=row[6],
|
||
created_at=self._parse_datetime(row[7]),
|
||
answered_at=self._parse_datetime(row[8]),
|
||
is_read=bool(row[9]),
|
||
status=status,
|
||
user_question_number=row[11] if len(row) > 11 else None
|
||
)
|
||
return question
|
||
|
||
|
||
class UserBlockCRUD(BaseCRUD):
|
||
"""CRUD операции для блокировок пользователей"""
|
||
|
||
async def create(self, user_block: UserBlock) -> UserBlock:
|
||
"""Создание блокировки"""
|
||
async with self.get_connection() as conn:
|
||
cursor = await conn.execute("""
|
||
INSERT INTO user_blocks (blocker_id, blocked_id, created_at)
|
||
VALUES (?, ?, ?)
|
||
""", (
|
||
user_block.blocker_id, user_block.blocked_id,
|
||
user_block.created_at.isoformat() if user_block.created_at else None
|
||
))
|
||
user_block.id = cursor.lastrowid
|
||
await conn.commit()
|
||
return user_block
|
||
|
||
async def is_blocked(self, blocker_id: int, blocked_id: int) -> bool:
|
||
"""Проверка, заблокирован ли пользователь"""
|
||
async with self.get_connection() as conn:
|
||
async with conn.execute("""
|
||
SELECT COUNT(*) FROM user_blocks
|
||
WHERE blocker_id = ? AND blocked_id = ?
|
||
""", (blocker_id, blocked_id)) as cursor:
|
||
row = await cursor.fetchone()
|
||
return row[0] > 0
|
||
|
||
async def get_blocked_users(self, blocker_id: int) -> List[int]:
|
||
"""Получение списка заблокированных пользователей"""
|
||
async with self.get_connection() as conn:
|
||
async with conn.execute("""
|
||
SELECT blocked_id FROM user_blocks WHERE blocker_id = ?
|
||
""", (blocker_id,)) as cursor:
|
||
rows = await cursor.fetchall()
|
||
return [row[0] for row in rows]
|
||
|
||
|
||
async def delete(self, blocker_id: int, blocked_id: int) -> bool:
|
||
"""Удаление блокировки"""
|
||
async with self.get_connection() as conn:
|
||
cursor = await conn.execute("""
|
||
DELETE FROM user_blocks
|
||
WHERE blocker_id = ? AND blocked_id = ?
|
||
""", (blocker_id, blocked_id))
|
||
await conn.commit()
|
||
return cursor.rowcount > 0
|
||
|
||
|
||
class UserSettingsCRUD(BaseCRUD):
|
||
"""CRUD операции для настроек пользователей"""
|
||
|
||
async def create(self, settings: UserSettings) -> UserSettings:
|
||
"""Создание настроек пользователя"""
|
||
async with self.get_connection() as conn:
|
||
cursor = await conn.execute("""
|
||
INSERT INTO user_settings
|
||
(user_id, allow_questions, notify_new_questions,
|
||
notify_answers, language, created_at, updated_at)
|
||
VALUES (?, ?, ?, ?, ?, ?, ?)
|
||
""", (
|
||
settings.user_id, settings.allow_questions,
|
||
settings.notify_new_questions, settings.notify_answers, settings.language,
|
||
settings.created_at.isoformat() if settings.created_at else None,
|
||
settings.updated_at.isoformat() if settings.updated_at else None
|
||
))
|
||
settings.id = cursor.lastrowid
|
||
await conn.commit()
|
||
return settings
|
||
|
||
async def get_by_user_id(self, user_id: int) -> Optional[UserSettings]:
|
||
"""Получение настроек пользователя"""
|
||
async with self.get_connection() as conn:
|
||
async with conn.execute("""
|
||
SELECT * FROM user_settings WHERE user_id = ?
|
||
""", (user_id,)) as cursor:
|
||
row = await cursor.fetchone()
|
||
if row:
|
||
return self._row_to_settings(row)
|
||
return None
|
||
|
||
async def update(self, settings: UserSettings) -> UserSettings:
|
||
"""Обновление настроек пользователя"""
|
||
async with self.get_connection() as conn:
|
||
await conn.execute("""
|
||
UPDATE user_settings SET
|
||
allow_questions = ?, notify_new_questions = ?,
|
||
notify_answers = ?, language = ?, updated_at = ?
|
||
WHERE user_id = ?
|
||
""", (
|
||
settings.allow_questions,
|
||
settings.notify_new_questions, settings.notify_answers,
|
||
settings.language, settings.updated_at.isoformat() if settings.updated_at else None,
|
||
settings.user_id
|
||
))
|
||
await conn.commit()
|
||
return settings
|
||
|
||
async def delete(self, user_id: int) -> bool:
|
||
"""Удаление настроек пользователя"""
|
||
async with self.get_connection() as conn:
|
||
cursor = await conn.execute("""
|
||
DELETE FROM user_settings WHERE user_id = ?
|
||
""", (user_id,))
|
||
await conn.commit()
|
||
return cursor.rowcount > 0
|
||
|
||
def _row_to_settings(self, row) -> UserSettings:
|
||
"""Преобразование строки БД в объект UserSettings"""
|
||
return UserSettings(
|
||
id=row[0],
|
||
user_id=row[1],
|
||
allow_questions=bool(row[2]),
|
||
notify_new_questions=bool(row[3]),
|
||
notify_answers=bool(row[4]),
|
||
language=row[5],
|
||
created_at=self._parse_datetime(row[6]),
|
||
updated_at=self._parse_datetime(row[7])
|
||
)
|