feat: мини-игры, premium подписка, улучшенные контексты

Мини-игры (/games):
- Speed Round: 10 раундов, 10 секунд на ответ, очки за скорость
- Match Pairs: 5 слов + 5 переводов, соединить пары

Premium-функции:
- Поля is_premium и premium_until для пользователей
- AI режим проверки ответов (учитывает синонимы)
- Batch проверка всех ответов одним запросом

Улучшения:
- Примеры использования для всех добавляемых слов
- Разбиение переводов по запятой на отдельные записи
- Полные предложения в контекстах (без ___)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2025-12-10 19:42:10 +03:00
parent b74ea2170c
commit adc8a6bf8e
18 changed files with 1819 additions and 34 deletions

View File

@@ -1,8 +1,11 @@
from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession
from database.models import Vocabulary, WordSource, LanguageLevel, WordTranslation
from typing import List, Optional, Dict
import random
import re
from typing import List, Optional, Dict
from sqlalchemy import select, func
from sqlalchemy.ext.asyncio import AsyncSession
from database.models import Vocabulary, WordSource, LanguageLevel, WordTranslation
class VocabularyService:
@@ -263,6 +266,61 @@ class VocabularyService:
return new_translation
@staticmethod
async def add_translation_split(
session: AsyncSession,
vocabulary_id: int,
translation: str,
context: Optional[str] = None,
context_translation: Optional[str] = None,
is_primary: bool = True
) -> List[WordTranslation]:
"""
Добавить перевод(ы) к слову, разбивая строку по разделителям.
Если translation содержит несколько переводов через запятую или точку с запятой,
каждый перевод добавляется отдельной записью.
Args:
session: Сессия базы данных
vocabulary_id: ID слова в словаре
translation: Перевод (может содержать несколько через запятую)
context: Пример использования на языке изучения
context_translation: Перевод примера
is_primary: Является ли первый перевод основным
Returns:
Список созданных переводов
"""
import re
# Разбиваем по запятой или точке с запятой
parts = re.split(r'[,;]\s*', translation)
# Очищаем и фильтруем пустые
translations = [p.strip() for p in parts if p.strip()]
if not translations:
return []
created = []
for i, tr in enumerate(translations):
new_translation = WordTranslation(
vocabulary_id=vocabulary_id,
translation=tr,
context=context if i == 0 else None, # Контекст только для первого перевода
context_translation=context_translation if i == 0 else None,
is_primary=(is_primary and i == 0) # Только первый - основной
)
session.add(new_translation)
created.append(new_translation)
await session.commit()
for t in created:
await session.refresh(t)
return created
@staticmethod
async def add_translations_bulk(
session: AsyncSession,
@@ -368,3 +426,103 @@ class VocabularyService:
await session.commit()
return True
return False
@staticmethod
async def get_user_word_count(session: AsyncSession, user_id: int) -> int:
"""
Получить количество слов пользователя
Args:
session: Сессия базы данных
user_id: ID пользователя
Returns:
Количество слов
"""
result = await session.execute(
select(func.count(Vocabulary.id)).where(Vocabulary.user_id == user_id)
)
return result.scalar() or 0
@staticmethod
async def get_random_words_for_game(
session: AsyncSession,
user_id: int,
count: int = 10,
learning_lang: Optional[str] = None
) -> List[Vocabulary]:
"""
Получить случайные слова для мини-игры
Args:
session: Сессия базы данных
user_id: ID пользователя
count: Количество слов
learning_lang: Язык изучения для фильтрации
Returns:
Список случайных слов
"""
result = await session.execute(
select(Vocabulary).where(Vocabulary.user_id == user_id)
)
words = list(result.scalars().all())
if learning_lang:
words = VocabularyService._filter_by_learning_lang(words, learning_lang)
# Перемешиваем и берём нужное количество
random.shuffle(words)
return words[:count]
@staticmethod
async def get_random_words_with_translations(
session: AsyncSession,
user_id: int,
count: int = 10,
learning_lang: Optional[str] = None
) -> List[dict]:
"""
Получить случайные слова для мини-игры вместе со всеми переводами
Args:
session: Сессия базы данных
user_id: ID пользователя
count: Количество слов
learning_lang: Язык изучения для фильтрации
Returns:
Список словарей с информацией о словах и их переводах
"""
# Получаем слова
words = await VocabularyService.get_random_words_for_game(
session, user_id, count, learning_lang
)
result = []
for word in words:
# Получаем все переводы для слова
translations = await VocabularyService.get_word_translations(session, word.id)
# Собираем все варианты перевода
all_translations = []
# Основной перевод из vocabulary
if word.word_translation:
all_translations.append(word.word_translation.lower().strip())
# Переводы из word_translations
for tr in translations:
tr_text = tr.translation.lower().strip()
if tr_text not in all_translations:
all_translations.append(tr_text)
result.append({
'id': word.id,
'word': word.word_original,
'translation': word.word_translation, # Основной перевод для отображения
'all_translations': all_translations, # Все варианты для проверки
'transcription': word.transcription
})
return result