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:
2025-09-06 18:35:12 +03:00
parent 50be010026
commit 596a2fa813
111 changed files with 16847 additions and 65 deletions

View File

@@ -331,21 +331,31 @@ class QuestionCRUD(BaseCRUD):
"""Создание нового вопроса"""
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)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
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.is_read, question.status.value, question.user_question_number
))
question.id = cursor.lastrowid
await conn.commit()
logger.info(f"✅ Вопрос создан с ID: {question.id}")
logger.info(f"✅ Вопрос создан с ID: {question.id}, номер для пользователя: {question.user_question_number}")
return question
async def create_batch(self, questions: List[Question]) -> List[Question]:
@@ -356,6 +366,27 @@ class QuestionCRUD(BaseCRUD):
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:
@@ -364,15 +395,15 @@ class QuestionCRUD(BaseCRUD):
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.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)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
message_id, created_at, answered_at, is_read, status, user_question_number)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
""", batch_data)
# Обновляем ID для всех созданных вопросов
@@ -393,7 +424,11 @@ class QuestionCRUD(BaseCRUD):
"""Получение вопроса по ID"""
async with self.get_connection() as conn:
async with conn.execute("""
SELECT * FROM questions WHERE id = ?
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:
@@ -408,7 +443,7 @@ class QuestionCRUD(BaseCRUD):
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.is_read, q.status, q.user_question_number
FROM questions q
WHERE q.to_user_id = ?
"""
@@ -418,7 +453,7 @@ class QuestionCRUD(BaseCRUD):
query += " AND q.status = ?"
params.append(status.value)
query += " ORDER BY q.created_at DESC LIMIT ? OFFSET ?"
query += " ORDER BY q.user_question_number DESC LIMIT ? OFFSET ?"
params.extend([limit, offset])
async with conn.execute(query, params) as cursor:
@@ -455,7 +490,7 @@ class QuestionCRUD(BaseCRUD):
query += " AND q.status = ?"
params.append(status.value)
query += " ORDER BY q.created_at DESC LIMIT ? OFFSET ?"
query += " ORDER BY q.user_question_number DESC LIMIT ? OFFSET ?"
params.extend([limit, offset])
async with conn.execute(query, params) as cursor:
@@ -467,28 +502,28 @@ class QuestionCRUD(BaseCRUD):
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
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))
return result
async def get_by_to_user_cursor(
@@ -506,7 +541,7 @@ class QuestionCRUD(BaseCRUD):
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.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 < ?))
@@ -519,7 +554,7 @@ class QuestionCRUD(BaseCRUD):
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.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 > ?))
@@ -555,7 +590,7 @@ class QuestionCRUD(BaseCRUD):
query += " AND q.status = ?"
params.append(status.value)
query += " ORDER BY q.created_at ASC LIMIT ? OFFSET ?"
query += " ORDER BY q.user_question_number ASC LIMIT ? OFFSET ?"
params.extend([limit, offset])
async with conn.execute(query, params) as cursor:
@@ -566,27 +601,105 @@ class QuestionCRUD(BaseCRUD):
"""Обновление вопроса"""
logger.info(f"📝 Обновление вопроса {question.id} (статус: {question.status.value})")
async with self.get_connection() as conn:
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
))
# Если вопрос помечается как удаленный, нужно пересчитать номера
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()
return cursor.rowcount > 0
logger.info(f"🗑️ Вопрос {question_id} удален, пересчитаны номера для пользователя {to_user_id}")
return True
async def get_unread_count(self, to_user_id: int) -> int:
"""Получение количества непрочитанных вопросов"""
@@ -641,7 +754,7 @@ class QuestionCRUD(BaseCRUD):
"""Преобразование строки БД в объект Question"""
# Проверяем, что все необходимые поля присутствуют
if len(row) < 11:
raise ValueError(f"Недостаточно данных в строке БД: {len(row)} колонок, ожидается 11")
raise ValueError(f"Недостаточно данных в строке БД: {len(row)} колонок, ожидается минимум 11")
# Проверяем статус
status_value = row[10]
@@ -665,7 +778,8 @@ class QuestionCRUD(BaseCRUD):
created_at=self._parse_datetime(row[7]),
answered_at=self._parse_datetime(row[8]),
is_read=bool(row[9]),
status=status
status=status,
user_question_number=row[11] if len(row) > 11 else None
)
return question