Мини-игры (/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>
193 lines
10 KiB
Python
193 lines
10 KiB
Python
from datetime import datetime
|
||
from typing import Optional
|
||
|
||
from sqlalchemy import String, BigInteger, DateTime, Integer, Boolean, JSON, Enum as SQLEnum, UniqueConstraint
|
||
from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column
|
||
import enum
|
||
|
||
|
||
class Base(DeclarativeBase):
|
||
"""Базовая модель"""
|
||
pass
|
||
|
||
|
||
class LanguageLevel(str, enum.Enum):
|
||
"""Уровни владения языком (CEFR)"""
|
||
A1 = "A1"
|
||
A2 = "A2"
|
||
B1 = "B1"
|
||
B2 = "B2"
|
||
C1 = "C1"
|
||
C2 = "C2"
|
||
|
||
|
||
class JLPTLevel(str, enum.Enum):
|
||
"""Уровни JLPT для японского языка"""
|
||
N5 = "N5" # Базовый
|
||
N4 = "N4" # Начальный
|
||
N3 = "N3" # Средний
|
||
N2 = "N2" # Продвинутый
|
||
N1 = "N1" # Свободный
|
||
|
||
|
||
# Языки, использующие JLPT вместо CEFR
|
||
JLPT_LANGUAGES = {"ja"}
|
||
|
||
# Дефолтные уровни для разных систем
|
||
DEFAULT_CEFR_LEVEL = "A1"
|
||
DEFAULT_JLPT_LEVEL = "N5"
|
||
|
||
|
||
class WordSource(str, enum.Enum):
|
||
"""Источник добавления слова"""
|
||
MANUAL = "manual" # Ручное добавление
|
||
SUGGESTED = "suggested" # Предложено ботом
|
||
CONTEXT = "context" # Из контекста диалога
|
||
IMPORT = "import" # Импорт из текста
|
||
ERROR = "error" # Из ошибок в заданиях
|
||
AI_TASK = "ai_task" # Из AI-задания
|
||
|
||
|
||
class AIProvider(str, enum.Enum):
|
||
"""Провайдеры AI моделей"""
|
||
openai = "openai"
|
||
google = "google"
|
||
|
||
|
||
class User(Base):
|
||
"""Модель пользователя"""
|
||
__tablename__ = "users"
|
||
|
||
id: Mapped[int] = mapped_column(primary_key=True)
|
||
telegram_id: Mapped[int] = mapped_column(BigInteger, unique=True, nullable=False)
|
||
username: Mapped[Optional[str]] = mapped_column(String(255))
|
||
language_interface: Mapped[str] = mapped_column(String(2), default="ru") # ru/en/ja - UI language
|
||
learning_language: Mapped[str] = mapped_column(String(2), default="en") # en/ja - language being learned
|
||
translation_language: Mapped[Optional[str]] = mapped_column(String(2), default=None) # ru/en/ja - translation target (defaults to language_interface if None)
|
||
level: Mapped[LanguageLevel] = mapped_column(SQLEnum(LanguageLevel), default=LanguageLevel.A1)
|
||
levels_by_language: Mapped[Optional[dict]] = mapped_column(JSON, default=None) # {"en": "B1", "ja": "N4"}
|
||
timezone: Mapped[str] = mapped_column(String(50), default="UTC")
|
||
daily_task_time: Mapped[Optional[str]] = mapped_column(String(5)) # HH:MM
|
||
reminders_enabled: Mapped[bool] = mapped_column(Boolean, default=False)
|
||
last_reminder_sent: Mapped[Optional[datetime]] = mapped_column(DateTime)
|
||
streak_days: Mapped[int] = mapped_column(Integer, default=0)
|
||
tasks_count: Mapped[int] = mapped_column(Integer, default=5) # Количество заданий (5-15)
|
||
ai_model_id: Mapped[Optional[int]] = mapped_column(Integer, default=None) # ID выбранной AI модели (NULL = глобальная)
|
||
is_premium: Mapped[bool] = mapped_column(Boolean, default=False) # Подписка (AI проверка в играх и др.)
|
||
premium_until: Mapped[Optional[datetime]] = mapped_column(DateTime, default=None) # Дата окончания подписки
|
||
created_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.utcnow)
|
||
last_active: Mapped[datetime] = mapped_column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
|
||
|
||
|
||
class Vocabulary(Base):
|
||
"""Модель словарного запаса"""
|
||
__tablename__ = "vocabulary"
|
||
__table_args__ = (
|
||
UniqueConstraint("user_id", "source_lang", "word_original", name="uq_vocab_user_lang_word"),
|
||
)
|
||
|
||
id: Mapped[int] = mapped_column(primary_key=True)
|
||
user_id: Mapped[int] = mapped_column(Integer, nullable=False, index=True)
|
||
word_original: Mapped[str] = mapped_column(String(255), nullable=False)
|
||
word_translation: Mapped[str] = mapped_column(String(255), nullable=False)
|
||
source_lang: Mapped[Optional[str]] = mapped_column(String(5)) # ISO2 языка слова (язык изучения)
|
||
translation_lang: Mapped[Optional[str]] = mapped_column(String(5)) # ISO2 языка перевода (обычно язык интерфейса)
|
||
transcription: Mapped[Optional[str]] = mapped_column(String(255))
|
||
difficulty_level: Mapped[Optional[LanguageLevel]] = mapped_column(SQLEnum(LanguageLevel))
|
||
source: Mapped[WordSource] = mapped_column(SQLEnum(WordSource), default=WordSource.MANUAL)
|
||
times_reviewed: Mapped[int] = mapped_column(Integer, default=0)
|
||
correct_answers: Mapped[int] = mapped_column(Integer, default=0)
|
||
last_reviewed: Mapped[Optional[datetime]] = mapped_column(DateTime)
|
||
created_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.utcnow)
|
||
notes: Mapped[Optional[str]] = mapped_column(String(500)) # Заметки пользователя
|
||
|
||
|
||
class WordTranslation(Base):
|
||
"""Модель перевода слова с контекстом"""
|
||
__tablename__ = "word_translations"
|
||
|
||
id: Mapped[int] = mapped_column(primary_key=True)
|
||
vocabulary_id: Mapped[int] = mapped_column(Integer, nullable=False, index=True)
|
||
translation: Mapped[str] = mapped_column(String(255), nullable=False)
|
||
context: Mapped[Optional[str]] = mapped_column(String(500)) # Пример предложения
|
||
context_translation: Mapped[Optional[str]] = mapped_column(String(500)) # Перевод примера
|
||
is_primary: Mapped[bool] = mapped_column(Boolean, default=False) # Основной перевод
|
||
created_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.utcnow)
|
||
|
||
|
||
class Task(Base):
|
||
"""Модель задания"""
|
||
__tablename__ = "tasks"
|
||
|
||
id: Mapped[int] = mapped_column(primary_key=True)
|
||
user_id: Mapped[int] = mapped_column(Integer, nullable=False, index=True)
|
||
task_type: Mapped[str] = mapped_column(String(50), nullable=False) # translate, sentence, fill, etc.
|
||
content: Mapped[dict] = mapped_column(JSON, nullable=False) # Содержание задания
|
||
correct_answer: Mapped[Optional[str]] = mapped_column(String(500))
|
||
user_answer: Mapped[Optional[str]] = mapped_column(String(500))
|
||
is_correct: Mapped[Optional[bool]] = mapped_column(Boolean)
|
||
ai_feedback: Mapped[Optional[str]] = mapped_column(String(1000))
|
||
completed_at: Mapped[Optional[datetime]] = mapped_column(DateTime)
|
||
created_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.utcnow)
|
||
|
||
|
||
class AIModel(Base):
|
||
"""Модель AI моделей для генерации"""
|
||
__tablename__ = "ai_models"
|
||
|
||
id: Mapped[int] = mapped_column(primary_key=True)
|
||
provider: Mapped[AIProvider] = mapped_column(SQLEnum(AIProvider), nullable=False) # openai / google
|
||
model_name: Mapped[str] = mapped_column(String(100), nullable=False) # gpt-4o-mini, gemini-2.5-flash-lite
|
||
display_name: Mapped[str] = mapped_column(String(100), nullable=False) # Название для отображения
|
||
is_active: Mapped[bool] = mapped_column(Boolean, default=False) # Только одна модель активна
|
||
created_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.utcnow)
|
||
|
||
|
||
class StoryGenre(str, enum.Enum):
|
||
"""Жанры мини-историй"""
|
||
dialogue = "dialogue" # 🗣 Диалоги
|
||
news = "news" # 📰 Новости
|
||
story = "story" # 🎭 Истории
|
||
letter = "letter" # 📧 Письма
|
||
recipe = "recipe" # 🍳 Рецепты
|
||
|
||
|
||
class MiniStory(Base):
|
||
"""Модель мини-истории для чтения"""
|
||
__tablename__ = "mini_stories"
|
||
|
||
id: Mapped[int] = mapped_column(primary_key=True)
|
||
user_id: Mapped[int] = mapped_column(Integer, nullable=False, index=True)
|
||
title: Mapped[str] = mapped_column(String(255), nullable=False)
|
||
content: Mapped[str] = mapped_column(String(5000), nullable=False) # Текст истории
|
||
translation: Mapped[Optional[str]] = mapped_column(String(5000), nullable=True) # Перевод истории
|
||
genre: Mapped[StoryGenre] = mapped_column(SQLEnum(StoryGenre), nullable=False)
|
||
learning_lang: Mapped[str] = mapped_column(String(5), nullable=False) # en/ja
|
||
level: Mapped[str] = mapped_column(String(5), nullable=False) # A1-C2 или N5-N1
|
||
word_count: Mapped[int] = mapped_column(Integer, default=0) # Количество слов
|
||
vocabulary: Mapped[Optional[dict]] = mapped_column(JSON) # [{word, translation, transcription}]
|
||
questions: Mapped[Optional[dict]] = mapped_column(JSON) # [{question, options[], correct}]
|
||
is_completed: Mapped[bool] = mapped_column(Boolean, default=False)
|
||
correct_answers: Mapped[int] = mapped_column(Integer, default=0)
|
||
created_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.utcnow)
|
||
|
||
|
||
class WordOfDay(Base):
|
||
"""Модель слова дня (глобальная для всех пользователей по уровню)"""
|
||
__tablename__ = "word_of_day"
|
||
__table_args__ = (
|
||
UniqueConstraint("date", "learning_lang", "level", name="uq_wod_date_lang_level"),
|
||
)
|
||
|
||
id: Mapped[int] = mapped_column(primary_key=True)
|
||
word: Mapped[str] = mapped_column(String(255), nullable=False)
|
||
transcription: Mapped[Optional[str]] = mapped_column(String(255))
|
||
translation: Mapped[str] = mapped_column(String(500), nullable=False)
|
||
examples: Mapped[Optional[dict]] = mapped_column(JSON) # [{sentence, translation}]
|
||
synonyms: Mapped[Optional[str]] = mapped_column(String(500)) # Синонимы через запятую
|
||
etymology: Mapped[Optional[str]] = mapped_column(String(500)) # Этимология/интересный факт
|
||
learning_lang: Mapped[str] = mapped_column(String(5), nullable=False, index=True) # en/ja
|
||
level: Mapped[str] = mapped_column(String(5), nullable=False, index=True) # A1-C2 или N5-N1
|
||
date: Mapped[datetime] = mapped_column(DateTime, nullable=False, index=True) # Дата слова (только дата, без времени)
|
||
created_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.utcnow)
|