initial commit (backend)

This commit is contained in:
2026-09-20 11:45:58 +00:00
commit 033adf023e
65 changed files with 28292 additions and 0 deletions
+870
View File
@@ -0,0 +1,870 @@
import asyncpg
import logging
from config import DB_CONFIG
logger = logging.getLogger(__name__)
db_pool = None
async def get_pool():
global db_pool
if db_pool is None:
db_pool = await asyncpg.create_pool(**DB_CONFIG, min_size=2, max_size=10)
return db_pool
async def init_db():
pool = await get_pool()
async with pool.acquire() as conn:
# ============ users ============
await conn.execute("""
CREATE TABLE IF NOT EXISTS users (
id SERIAL PRIMARY KEY,
telegram_id BIGINT UNIQUE NOT NULL,
username TEXT,
app_language TEXT DEFAULT 'pl',
current_level TEXT DEFAULT 'A1',
last_completed_level INTEGER DEFAULT 0,
created_at TIMESTAMP DEFAULT NOW()
)
""")
# ============ Авто-миграция: добавляем current_level в users ============
col_exists = await conn.fetchval("""
SELECT 1 FROM information_schema.columns
WHERE table_name = 'users' AND column_name = 'current_level'
""")
if not col_exists:
await conn.execute("""
ALTER TABLE users
ADD COLUMN current_level TEXT DEFAULT 'A1'
""")
logger.info("Added current_level column to users")
# ============ levels ============
await conn.execute("""
CREATE TABLE IF NOT EXISTS levels (
id SERIAL PRIMARY KEY,
level_number INTEGER NOT NULL,
grid_size INTEGER NOT NULL,
game_type TEXT DEFAULT 'wordsearch',
level TEXT DEFAULT 'A1',
grid_letters JSONB NOT NULL DEFAULT '[]',
word_paths JSONB NOT NULL DEFAULT '{}'
)
""")
# ============ Убираем старый UNIQUE constraint с levels.level_number ============
await conn.execute("""
ALTER TABLE levels DROP CONSTRAINT IF EXISTS levels_level_number_key
""")
# ============ Добавляем составной UNIQUE (level, level_number) ============
await conn.execute("""
DO $$
BEGIN
IF NOT EXISTS (
SELECT 1 FROM pg_constraint
WHERE conname = 'levels_level_number_unique'
) THEN
ALTER TABLE levels ADD CONSTRAINT levels_level_number_unique UNIQUE (level, level_number);
END IF;
END $$;
""")
# ============ level_words ============
await conn.execute("""
CREATE TABLE IF NOT EXISTS level_words (
id SERIAL PRIMARY KEY,
level_id INTEGER REFERENCES levels(id) ON DELETE CASCADE,
word_pl TEXT NOT NULL,
translation_ru TEXT DEFAULT '',
translation_uk TEXT DEFAULT '',
translation_en TEXT DEFAULT '',
level TEXT DEFAULT 'A1'
)
""")
# ============ Авто-миграция: добавляем level в level_words ============
col_exists = await conn.fetchval("""
SELECT 1 FROM information_schema.columns
WHERE table_name = 'level_words' AND column_name = 'level'
""")
if not col_exists:
await conn.execute("""
ALTER TABLE level_words
ADD COLUMN level TEXT DEFAULT 'A1'
""")
logger.info("Added level column to level_words")
# ============ user_progress ============
existing = await conn.fetchrow("""
SELECT data_type
FROM information_schema.columns
WHERE table_name = 'user_progress' AND column_name = 'user_id'
""")
if existing and existing['data_type'] != 'bigint':
logger.info("Migrating user_progress.user_id from INTEGER to BIGINT")
await conn.execute("""
ALTER TABLE user_progress
DROP CONSTRAINT IF EXISTS user_progress_pkey CASCADE
""")
await conn.execute("""
ALTER TABLE user_progress
DROP CONSTRAINT IF EXISTS user_progress_user_id_fkey
""")
await conn.execute("""
ALTER TABLE user_progress
ALTER COLUMN user_id TYPE BIGINT
""")
await conn.execute("""
ALTER TABLE user_progress
ADD PRIMARY KEY (user_id, level_id)
""")
await conn.execute("""
ALTER TABLE user_progress
ADD FOREIGN KEY (user_id) REFERENCES users(telegram_id) ON DELETE CASCADE
""")
elif not existing:
await conn.execute("""
CREATE TABLE IF NOT EXISTS user_progress (
user_id BIGINT REFERENCES users(telegram_id) ON DELETE CASCADE,
level_id INTEGER REFERENCES levels(id) ON DELETE CASCADE,
completed_at TIMESTAMP,
hints_used INTEGER DEFAULT 0,
score INTEGER DEFAULT 0,
level TEXT DEFAULT 'A1',
PRIMARY KEY (user_id, level_id)
)
""")
# ============ Авто-миграция: добавляем level в user_progress ============
col_exists = await conn.fetchval("""
SELECT 1 FROM information_schema.columns
WHERE table_name = 'user_progress' AND column_name = 'level'
""")
if not col_exists:
await conn.execute("""
ALTER TABLE user_progress
ADD COLUMN level TEXT DEFAULT 'A1'
""")
logger.info("Added level column to user_progress")
# ============ sentences ============
await conn.execute("""
CREATE TABLE IF NOT EXISTS sentences (
id SERIAL PRIMARY KEY,
sentence_number INTEGER NOT NULL,
sentence_pl TEXT NOT NULL,
translation_ru TEXT DEFAULT '',
translation_uk TEXT DEFAULT '',
translation_en TEXT DEFAULT '',
difficulty INTEGER DEFAULT 1,
level TEXT DEFAULT 'A1'
)
""")
# ============ Убираем старый UNIQUE constraint с sentences.sentence_number ============
await conn.execute("""
ALTER TABLE sentences DROP CONSTRAINT IF EXISTS sentences_sentence_number_key
""")
# ============ Добавляем составной UNIQUE (level, sentence_number) ============
await conn.execute("""
DO $$
BEGIN
IF NOT EXISTS (
SELECT 1 FROM pg_constraint
WHERE conname = 'sentences_sentence_number_unique'
) THEN
ALTER TABLE sentences ADD CONSTRAINT sentences_sentence_number_unique UNIQUE (level, sentence_number);
END IF;
END $$;
""")
# ============ sentence_progress ============
await conn.execute("""
CREATE TABLE IF NOT EXISTS sentence_progress (
user_id BIGINT REFERENCES users(telegram_id) ON DELETE CASCADE,
sentence_id INTEGER REFERENCES sentences(id) ON DELETE CASCADE,
completed_at TIMESTAMP,
score INTEGER DEFAULT 0,
level TEXT DEFAULT 'A1',
PRIMARY KEY (user_id, sentence_id)
)
""")
# ============ Авто-миграция: добавляем level в sentence_progress ============
col_exists = await conn.fetchval("""
SELECT 1 FROM information_schema.columns
WHERE table_name = 'sentence_progress' AND column_name = 'level'
""")
if not col_exists:
await conn.execute("""
ALTER TABLE sentence_progress
ADD COLUMN level TEXT DEFAULT 'A1'
""")
logger.info("Added level column to sentence_progress")
# ============ typing_progress ============
await conn.execute("""
CREATE TABLE IF NOT EXISTS typing_progress (
user_id BIGINT REFERENCES users(telegram_id) ON DELETE CASCADE,
sentence_id INTEGER REFERENCES sentences(id) ON DELETE CASCADE,
completed_at TIMESTAMP DEFAULT NOW(),
PRIMARY KEY (user_id, sentence_id)
)
""")
# ============ Индекс для typing_progress ============
await conn.execute("CREATE INDEX IF NOT EXISTS idx_typing_progress_user ON typing_progress(user_id)")
# ============ chat_history ============
await conn.execute("""
CREATE TABLE IF NOT EXISTS chat_history (
id SERIAL PRIMARY KEY,
user_id BIGINT REFERENCES users(telegram_id) ON DELETE CASCADE,
message TEXT NOT NULL,
is_user BOOLEAN DEFAULT TRUE,
created_at TIMESTAMP DEFAULT NOW()
)
""")
# ============ Индекс для chat_history ============
await conn.execute("CREATE INDEX IF NOT EXISTS idx_chat_history_user ON chat_history(user_id, created_at DESC)")
# ============ chat_usage ============
await conn.execute("""
CREATE TABLE IF NOT EXISTS chat_usage (
id SERIAL PRIMARY KEY,
user_id BIGINT REFERENCES users(telegram_id) ON DELETE CASCADE,
usage_date DATE DEFAULT CURRENT_DATE,
message_count INTEGER DEFAULT 0,
UNIQUE (user_id, usage_date)
)
""")
# ============ Индекс для chat_usage ============
await conn.execute("CREATE INDEX IF NOT EXISTS idx_chat_usage_user ON chat_usage(user_id, usage_date DESC)")
# ============ learning_levels ============
await conn.execute("""
CREATE TABLE IF NOT EXISTS learning_levels (
id SERIAL PRIMARY KEY,
code VARCHAR(10) UNIQUE NOT NULL,
name_pl TEXT NOT NULL,
name_ru TEXT NOT NULL,
name_uk TEXT NOT NULL,
name_en TEXT NOT NULL,
sort_order INT NOT NULL,
is_active BOOLEAN DEFAULT TRUE
)
""")
# ============ learning_topics ============
await conn.execute("""
CREATE TABLE IF NOT EXISTS learning_topics (
id SERIAL PRIMARY KEY,
topic_id VARCHAR(100) UNIQUE NOT NULL,
level_code VARCHAR(10) NOT NULL,
order_number INT NOT NULL,
name_pl TEXT NOT NULL,
name_ru TEXT NOT NULL,
name_uk TEXT NOT NULL,
name_en TEXT NOT NULL,
description_pl TEXT,
description_ru TEXT,
description_uk TEXT,
description_en TEXT,
is_active BOOLEAN DEFAULT TRUE,
UNIQUE(level_code, order_number)
)
""")
# ============ learning_lessons ============
await conn.execute("""
CREATE TABLE IF NOT EXISTS learning_lessons (
id SERIAL PRIMARY KEY,
lesson_id VARCHAR(120) UNIQUE NOT NULL,
topic_id VARCHAR(100) NOT NULL REFERENCES learning_topics(topic_id),
order_number INT NOT NULL,
skill_tag VARCHAR(100) NOT NULL,
title_pl TEXT NOT NULL,
title_ru TEXT NOT NULL,
title_uk TEXT NOT NULL,
title_en TEXT NOT NULL,
objective_pl TEXT,
objective_ru TEXT,
objective_uk TEXT,
objective_en TEXT,
explanation_pl TEXT,
explanation_ru TEXT,
explanation_uk TEXT,
explanation_en TEXT,
is_active BOOLEAN DEFAULT TRUE,
UNIQUE(topic_id, order_number)
)
""")
# ============ lesson_examples ============
await conn.execute("""
CREATE TABLE IF NOT EXISTS lesson_examples (
id SERIAL PRIMARY KEY,
lesson_id VARCHAR(120) NOT NULL REFERENCES learning_lessons(lesson_id),
order_number INT NOT NULL,
text_pl TEXT NOT NULL,
text_ru TEXT,
text_uk TEXT,
text_en TEXT,
audio_url TEXT
)
""")
# ============ learning_questions ============
await conn.execute("""
CREATE TABLE IF NOT EXISTS learning_questions (
id SERIAL PRIMARY KEY,
question_id VARCHAR(150) UNIQUE NOT NULL,
lesson_id VARCHAR(120) REFERENCES learning_lessons(lesson_id),
topic_id VARCHAR(100) NOT NULL REFERENCES learning_topics(topic_id),
context_type VARCHAR(50) NOT NULL,
question_type VARCHAR(50) NOT NULL,
skill_tag VARCHAR(100) NOT NULL,
difficulty INT NOT NULL CHECK (difficulty BETWEEN 1 AND 3),
prompt_pl TEXT,
prompt_ru TEXT,
prompt_uk TEXT,
prompt_en TEXT,
correct_answer TEXT,
hint_type VARCHAR(50),
hint_value TEXT,
is_test_question BOOLEAN DEFAULT FALSE,
test_order INT,
options_language TEXT DEFAULT 'pl'
)
""")
# ============ Авто-миграция: добавляем options_language ============
col_exists = await conn.fetchval("""
SELECT 1 FROM information_schema.columns
WHERE table_name = 'learning_questions' AND column_name = 'options_language'
""")
if not col_exists:
await conn.execute("""
ALTER TABLE learning_questions
ADD COLUMN options_language TEXT DEFAULT 'pl'
""")
logger.info("Added options_language column to learning_questions")
# ============ question_options ============
await conn.execute("""
CREATE TABLE IF NOT EXISTS question_options (
id SERIAL PRIMARY KEY,
question_id VARCHAR(150) NOT NULL REFERENCES learning_questions(question_id),
option_key VARCHAR(20) NOT NULL,
text_pl TEXT,
text_ru TEXT,
text_uk TEXT,
text_en TEXT,
is_correct BOOLEAN NOT NULL,
wrong_because VARCHAR(100),
UNIQUE(question_id, option_key)
)
""")
# ============ learning_sessions ============
await conn.execute("""
CREATE TABLE IF NOT EXISTS learning_sessions (
id BIGSERIAL PRIMARY KEY,
user_id BIGINT NOT NULL,
level_code VARCHAR(10) NOT NULL,
topic_id VARCHAR(100) NOT NULL,
session_type VARCHAR(30) NOT NULL,
started_at TIMESTAMP NOT NULL DEFAULT NOW(),
completed_at TIMESTAMP,
is_completed BOOLEAN DEFAULT FALSE
)
""")
# ============ user_learning_answers ============
await conn.execute("""
CREATE TABLE IF NOT EXISTS user_learning_answers (
id BIGSERIAL PRIMARY KEY,
user_id BIGINT NOT NULL,
session_id BIGINT NOT NULL REFERENCES learning_sessions(id),
level_code VARCHAR(10) NOT NULL,
topic_id VARCHAR(100) NOT NULL,
lesson_id VARCHAR(120),
question_id VARCHAR(150) NOT NULL,
selected_answer TEXT,
correct BOOLEAN,
skill_tag VARCHAR(100) NOT NULL,
question_type VARCHAR(50) NOT NULL,
context_type VARCHAR(50) NOT NULL,
difficulty INT NOT NULL,
time_taken_ms INT,
attempt_number INT DEFAULT 1,
hint_used BOOLEAN DEFAULT FALSE,
wrong_because VARCHAR(100),
is_final_answer BOOLEAN DEFAULT TRUE,
created_at TIMESTAMP DEFAULT NOW()
)
""")
# ============ user_lesson_progress ============
await conn.execute("""
CREATE TABLE IF NOT EXISTS user_lesson_progress (
id BIGSERIAL PRIMARY KEY,
user_id BIGINT NOT NULL,
lesson_id VARCHAR(120) NOT NULL,
started_at TIMESTAMP,
completed_at TIMESTAMP,
attempts_count INT DEFAULT 0,
correct_count INT DEFAULT 0,
best_score NUMERIC(5,2),
average_time_ms INT,
completed BOOLEAN DEFAULT FALSE,
UNIQUE(user_id, lesson_id)
)
""")
# ============ user_topic_progress ============
await conn.execute("""
CREATE TABLE IF NOT EXISTS user_topic_progress (
id BIGSERIAL PRIMARY KEY,
user_id BIGINT NOT NULL,
topic_id VARCHAR(100) NOT NULL,
lessons_total INT DEFAULT 0,
lessons_completed INT DEFAULT 0,
test_started BOOLEAN DEFAULT FALSE,
test_completed BOOLEAN DEFAULT FALSE,
test_score NUMERIC(5,2),
topic_mastery NUMERIC(5,2),
confidence NUMERIC(5,2),
completed_at TIMESTAMP,
UNIQUE(user_id, topic_id)
)
""")
# ============ user_skill_mastery (Analytics 2.0 + Этап 2) ============
await conn.execute("""
CREATE TABLE IF NOT EXISTS user_skill_mastery (
id BIGSERIAL PRIMARY KEY,
user_id BIGINT NOT NULL,
skill_tag VARCHAR(100) NOT NULL,
attempts_count INT DEFAULT 0,
correct_count INT DEFAULT 0,
recognition_attempts INT DEFAULT 0,
recognition_correct INT DEFAULT 0,
recognition_score NUMERIC(5,2),
production_attempts INT DEFAULT 0,
production_correct INT DEFAULT 0,
production_score NUMERIC(5,2),
application_attempts INT DEFAULT 0,
application_correct INT DEFAULT 0,
application_score NUMERIC(5,2),
retention_score NUMERIC(5,2) DEFAULT 0,
hint_dependency NUMERIC(5,2) DEFAULT 0,
average_time_ms INT DEFAULT 0,
median_time_ms INT DEFAULT 0,
median_correct_time_ms INT DEFAULT 0,
median_wrong_time_ms INT DEFAULT 0,
recent_accuracy NUMERIC(5,2) DEFAULT 0,
last_3_accuracy NUMERIC(5,2),
last_5_accuracy NUMERIC(5,2),
last_10_accuracy NUMERIC(5,2),
long_term_accuracy NUMERIC(5,2) DEFAULT 0,
mastery_score NUMERIC(5,2) DEFAULT 0,
previous_mastery_score NUMERIC(5,2) DEFAULT 0,
mastery_delta NUMERIC(5,2) DEFAULT 0,
trend TEXT DEFAULT 'insufficient_data',
retention_status TEXT DEFAULT 'fresh',
fluency_score NUMERIC(5,2) DEFAULT 0,
passive_knowledge_score NUMERIC(5,2) DEFAULT 0,
active_knowledge_score NUMERIC(5,2) DEFAULT 0,
confidence NUMERIC(5,2) DEFAULT 0,
error_count INT DEFAULT 0,
repeated_error_count INT DEFAULT 0,
days_since_last_attempt INT DEFAULT 0,
last_attempt_at TIMESTAMP,
UNIQUE(user_id, skill_tag)
)
""")
# ============ Авто-миграции для user_skill_mastery (Этап 2) ============
stage2_columns = {
"previous_mastery_score": "NUMERIC(5,2) DEFAULT 0",
"mastery_delta": "NUMERIC(5,2) DEFAULT 0",
"trend": "TEXT DEFAULT 'insufficient_data'",
"retention_status": "TEXT DEFAULT 'fresh'",
"fluency_score": "NUMERIC(5,2) DEFAULT 0",
"passive_knowledge_score": "NUMERIC(5,2) DEFAULT 0",
"active_knowledge_score": "NUMERIC(5,2) DEFAULT 0",
}
for col_name, col_type in stage2_columns.items():
col_exists = await conn.fetchval("""
SELECT 1 FROM information_schema.columns
WHERE table_name = 'user_skill_mastery' AND column_name = $1
""", col_name)
if not col_exists:
await conn.execute(f"""
ALTER TABLE user_skill_mastery
ADD COLUMN {col_name} {col_type}
""")
logger.info(f"Added {col_name} column to user_skill_mastery")
# ============ ai_user_context ============
await conn.execute("""
CREATE TABLE IF NOT EXISTS ai_user_context (
user_id BIGINT PRIMARY KEY,
current_level VARCHAR(10),
completed_topics INT DEFAULT 0,
level_topics_total INT DEFAULT 0,
level_completion_percent NUMERIC(5,2) DEFAULT 0,
completed_lessons INT DEFAULT 0,
overall_mastery NUMERIC(5,2) DEFAULT 0,
strong_skills JSONB DEFAULT '[]',
weak_skills JSONB DEFAULT '[]',
uncertain_skills JSONB DEFAULT '[]',
recurring_errors JSONB DEFAULT '[]',
recommended_topics JSONB DEFAULT '[]',
recommended_lessons JSONB DEFAULT '[]',
last_analysis_at TIMESTAMP,
analytics_enabled BOOLEAN DEFAULT FALSE
)
""")
# ============ Авто-миграция: добавляем level_topics_total ============
col_exists = await conn.fetchval("""
SELECT 1 FROM information_schema.columns
WHERE table_name = 'ai_user_context' AND column_name = 'level_topics_total'
""")
if not col_exists:
await conn.execute("""
ALTER TABLE ai_user_context
ADD COLUMN level_topics_total INT DEFAULT 0
""")
logger.info("Added level_topics_total column to ai_user_context")
# ============ Авто-миграция: добавляем level_completion_percent ============
col_exists = await conn.fetchval("""
SELECT 1 FROM information_schema.columns
WHERE table_name = 'ai_user_context' AND column_name = 'level_completion_percent'
""")
if not col_exists:
await conn.execute("""
ALTER TABLE ai_user_context
ADD COLUMN level_completion_percent NUMERIC(5,2) DEFAULT 0
""")
logger.info("Added level_completion_percent column to ai_user_context")
# ============ achievements ============
await conn.execute("""
CREATE TABLE IF NOT EXISTS achievements (
id SERIAL PRIMARY KEY,
code TEXT UNIQUE NOT NULL,
category TEXT DEFAULT 'general',
name_pl TEXT NOT NULL,
name_ru TEXT NOT NULL,
name_uk TEXT NOT NULL,
name_en TEXT NOT NULL,
description_pl TEXT DEFAULT '',
description_ru TEXT DEFAULT '',
description_uk TEXT DEFAULT '',
description_en TEXT DEFAULT '',
icon TEXT DEFAULT '🏆',
requirement_type TEXT NOT NULL,
requirement_value INTEGER DEFAULT 0
)
""")
# ============ user_achievements ============
await conn.execute("""
CREATE TABLE IF NOT EXISTS user_achievements (
user_id BIGINT REFERENCES users(telegram_id) ON DELETE CASCADE,
achievement_id INTEGER REFERENCES achievements(id) ON DELETE CASCADE,
unlocked_at TIMESTAMP DEFAULT NOW(),
PRIMARY KEY (user_id, achievement_id)
)
""")
# ============ daily_logins ============
await conn.execute("""
CREATE TABLE IF NOT EXISTS daily_logins (
user_id BIGINT REFERENCES users(telegram_id) ON DELETE CASCADE,
login_date DATE DEFAULT CURRENT_DATE,
PRIMARY KEY (user_id, login_date)
)
""")
# ============ word_quiz_progress ============
await conn.execute("""
CREATE TABLE IF NOT EXISTS word_quiz_progress (
user_id BIGINT REFERENCES users(telegram_id) ON DELETE CASCADE,
words_guessed INTEGER DEFAULT 0,
total_score INTEGER DEFAULT 0,
PRIMARY KEY (user_id)
)
""")
# ============ Авто-миграция: добавляем correct_streak ============
col_exists = await conn.fetchval("""
SELECT 1 FROM information_schema.columns
WHERE table_name = 'word_quiz_progress' AND column_name = 'correct_streak'
""")
if not col_exists:
await conn.execute("""
ALTER TABLE word_quiz_progress
ADD COLUMN correct_streak INTEGER DEFAULT 0
""")
logger.info("Added correct_streak column to word_quiz_progress")
# ============ Авто-миграция: добавляем recent_words ============
col_exists = await conn.fetchval("""
SELECT 1 FROM information_schema.columns
WHERE table_name = 'word_quiz_progress' AND column_name = 'recent_words'
""")
if not col_exists:
await conn.execute("""
ALTER TABLE word_quiz_progress
ADD COLUMN recent_words TEXT[] DEFAULT '{}'
""")
logger.info("Added recent_words column to word_quiz_progress")
# ============ Авто-миграция: добавляем level в word_quiz_progress ============
col_exists = await conn.fetchval("""
SELECT 1 FROM information_schema.columns
WHERE table_name = 'word_quiz_progress' AND column_name = 'level'
""")
if not col_exists:
await conn.execute("""
ALTER TABLE word_quiz_progress
ADD COLUMN level TEXT DEFAULT 'A1'
""")
logger.info("Added level column to word_quiz_progress")
# ============ bubble_words_progress ============
await conn.execute("""
CREATE TABLE IF NOT EXISTS bubble_words_progress (
user_id BIGINT REFERENCES users(telegram_id) ON DELETE CASCADE,
levels_completed INTEGER DEFAULT 0,
total_score INTEGER DEFAULT 0,
PRIMARY KEY (user_id)
)
""")
# ============ Авто-миграция: добавляем level в bubble_words_progress ============
col_exists = await conn.fetchval("""
SELECT 1 FROM information_schema.columns
WHERE table_name = 'bubble_words_progress' AND column_name = 'level'
""")
if not col_exists:
await conn.execute("""
ALTER TABLE bubble_words_progress
ADD COLUMN level TEXT DEFAULT 'A1'
""")
logger.info("Added level column to bubble_words_progress")
# ============ wrong_because_explanations ============
await conn.execute("""
CREATE TABLE IF NOT EXISTS wrong_because_explanations (
id SERIAL PRIMARY KEY,
wrong_because VARCHAR(100) UNIQUE NOT NULL,
explanation_pl TEXT NOT NULL,
explanation_ru TEXT NOT NULL,
explanation_uk TEXT NOT NULL,
explanation_en TEXT NOT NULL
)
""")
# ============ teacher_reactions ============
await conn.execute("""
CREATE TABLE IF NOT EXISTS teacher_reactions (
id SERIAL PRIMARY KEY,
reaction_type VARCHAR(50) NOT NULL,
text_pl TEXT NOT NULL,
text_ru TEXT NOT NULL,
text_uk TEXT NOT NULL,
text_en TEXT NOT NULL
)
""")
# ============ Индекс для teacher_reactions ============
await conn.execute("CREATE INDEX IF NOT EXISTS idx_teacher_reactions_type ON teacher_reactions(reaction_type)")
# ============ teacher_chat_history ============
await conn.execute("""
CREATE TABLE IF NOT EXISTS teacher_chat_history (
id BIGSERIAL PRIMARY KEY,
user_id BIGINT NOT NULL UNIQUE,
messages JSONB NOT NULL DEFAULT '[]',
updated_at TIMESTAMP DEFAULT NOW()
)
""")
# ============ teacher_ai_usage ============
await conn.execute("""
CREATE TABLE IF NOT EXISTS teacher_ai_usage (
id BIGSERIAL PRIMARY KEY,
user_id BIGINT NOT NULL,
usage_date DATE DEFAULT CURRENT_DATE,
command_count INTEGER DEFAULT 0,
UNIQUE(user_id, usage_date)
)
""")
# ============ daily_phrases — фраза дня ============
await conn.execute("""
CREATE TABLE IF NOT EXISTS daily_phrases (
id SERIAL PRIMARY KEY,
phrase_date DATE UNIQUE NOT NULL,
phrase_pl TEXT NOT NULL,
translations JSONB NOT NULL DEFAULT '{}',
grammar_breakdowns JSONB NOT NULL DEFAULT '{}',
created_at TIMESTAMP DEFAULT NOW()
)
""")
# ============ Индекс для daily_phrases ============
await conn.execute("CREATE INDEX IF NOT EXISTS idx_daily_phrases_date ON daily_phrases(phrase_date DESC)")
# ============ СИСТЕМА ПОДДЕРЖКИ ============
# ============ support_tickets ============
await conn.execute("""
CREATE TABLE IF NOT EXISTS support_tickets (
id BIGSERIAL PRIMARY KEY,
user_id BIGINT NOT NULL,
status TEXT DEFAULT 'open',
created_at TIMESTAMP DEFAULT NOW(),
updated_at TIMESTAMP DEFAULT NOW()
)
""")
# ============ Индекс для support_tickets ============
await conn.execute("CREATE INDEX IF NOT EXISTS idx_support_tickets_user ON support_tickets(user_id)")
await conn.execute("CREATE INDEX IF NOT EXISTS idx_support_tickets_status ON support_tickets(status)")
# ============ support_messages ============
await conn.execute("""
CREATE TABLE IF NOT EXISTS support_messages (
id BIGSERIAL PRIMARY KEY,
ticket_id BIGINT NOT NULL REFERENCES support_tickets(id) ON DELETE CASCADE,
sender_id BIGINT NOT NULL,
message TEXT NOT NULL,
is_read BOOLEAN DEFAULT FALSE,
created_at TIMESTAMP DEFAULT NOW()
)
""")
# ============ Индекс для support_messages ============
await conn.execute("CREATE INDEX IF NOT EXISTS idx_support_messages_ticket ON support_messages(ticket_id)")
await conn.execute("CREATE INDEX IF NOT EXISTS idx_support_messages_sender ON support_messages(sender_id)")
await conn.execute("CREATE INDEX IF NOT EXISTS idx_support_messages_unread ON support_messages(ticket_id, is_read)")
# ============ 🔧 НОВОЕ (18.09.2026): feed_posts — посты для карусели ============
await conn.execute("""
CREATE TABLE IF NOT EXISTS feed_posts (
id BIGSERIAL PRIMARY KEY,
section TEXT NOT NULL,
source_language TEXT NOT NULL DEFAULT 'ru',
title_pl TEXT,
title_ru TEXT,
title_uk TEXT,
title_en TEXT,
body_pl TEXT,
body_ru TEXT,
body_uk TEXT,
body_en TEXT,
link_url TEXT,
telegram_url TEXT,
author_id BIGINT NOT NULL,
is_pinned BOOLEAN DEFAULT FALSE,
is_published BOOLEAN DEFAULT TRUE,
created_at TIMESTAMP DEFAULT NOW(),
updated_at TIMESTAMP DEFAULT NOW(),
edited_at TIMESTAMP,
expires_at TIMESTAMP
)
""")
# ============ Индексы для feed_posts ============
await conn.execute("""
CREATE INDEX IF NOT EXISTS idx_feed_posts_section
ON feed_posts(section, created_at DESC)
""")
await conn.execute("""
CREATE INDEX IF NOT EXISTS idx_feed_posts_pinned
ON feed_posts(section, is_pinned DESC, created_at DESC)
""")
await conn.execute("""
CREATE INDEX IF NOT EXISTS idx_feed_posts_published
ON feed_posts(section, is_published)
""")
# ============ 🔧 НОВОЕ (18.09.2026): feed_attachments — картинки к постам (до 3) ============
await conn.execute("""
CREATE TABLE IF NOT EXISTS feed_attachments (
id BIGSERIAL PRIMARY KEY,
post_id BIGINT NOT NULL REFERENCES feed_posts(id) ON DELETE CASCADE,
file_path TEXT NOT NULL,
file_type TEXT DEFAULT 'image',
order_number INT NOT NULL DEFAULT 0,
created_at TIMESTAMP DEFAULT NOW()
)
""")
# ============ Индекс для feed_attachments ============
await conn.execute("""
CREATE INDEX IF NOT EXISTS idx_feed_attachments_post
ON feed_attachments(post_id, order_number)
""")
# ============ user_learning_answers_archive ============
await conn.execute("""
CREATE TABLE IF NOT EXISTS user_learning_answers_archive (
id BIGSERIAL PRIMARY KEY,
user_id BIGINT NOT NULL,
skill_tag VARCHAR(100) NOT NULL,
correct BOOLEAN,
wrong_because VARCHAR(100),
created_at DATE,
archived_at TIMESTAMP DEFAULT NOW()
)
""")
# ============ Индексы для архива ============
await conn.execute("CREATE INDEX IF NOT EXISTS idx_archive_user_date ON user_learning_answers_archive(user_id, created_at)")
await conn.execute("CREATE INDEX IF NOT EXISTS idx_archive_skill_tag ON user_learning_answers_archive(skill_tag)")
# ============ Индексы ============
await conn.execute("CREATE INDEX IF NOT EXISTS idx_levels_number ON levels(level_number)")
await conn.execute("CREATE INDEX IF NOT EXISTS idx_levels_level ON levels(level)")
await conn.execute("CREATE INDEX IF NOT EXISTS idx_levels_level_number ON levels(level, level_number)")
await conn.execute("CREATE INDEX IF NOT EXISTS idx_user_progress_user ON user_progress(user_id)")
await conn.execute("CREATE INDEX IF NOT EXISTS idx_level_words_level_id ON level_words(level_id)")
await conn.execute("CREATE INDEX IF NOT EXISTS idx_level_words_level ON level_words(level)")
await conn.execute("CREATE INDEX IF NOT EXISTS idx_users_telegram ON users(telegram_id)")
await conn.execute("CREATE INDEX IF NOT EXISTS idx_sentences_number ON sentences(sentence_number)")
await conn.execute("CREATE INDEX IF NOT EXISTS idx_sentences_level ON sentences(level)")
await conn.execute("CREATE INDEX IF NOT EXISTS idx_sentences_level_number ON sentences(level, sentence_number)")
await conn.execute("CREATE INDEX IF NOT EXISTS idx_sentence_progress_user ON sentence_progress(user_id)")
await conn.execute("CREATE INDEX IF NOT EXISTS idx_typing_progress_user ON typing_progress(user_id)")
await conn.execute("CREATE INDEX IF NOT EXISTS idx_user_progress_score ON user_progress(score DESC)")
await conn.execute("CREATE INDEX IF NOT EXISTS idx_sentence_progress_score ON sentence_progress(score DESC)")
await conn.execute("CREATE INDEX IF NOT EXISTS idx_user_achievements_user ON user_achievements(user_id)")
await conn.execute("CREATE INDEX IF NOT EXISTS idx_daily_logins_user ON daily_logins(user_id)")
await conn.execute("CREATE INDEX IF NOT EXISTS idx_daily_logins_date ON daily_logins(login_date DESC)")
await conn.execute("CREATE INDEX IF NOT EXISTS idx_learning_topics_level ON learning_topics(level_code)")
await conn.execute("CREATE INDEX IF NOT EXISTS idx_learning_lessons_topic ON learning_lessons(topic_id)")
await conn.execute("CREATE INDEX IF NOT EXISTS idx_learning_questions_lesson ON learning_questions(lesson_id)")
await conn.execute("CREATE INDEX IF NOT EXISTS idx_learning_questions_topic ON learning_questions(topic_id)")
await conn.execute("CREATE INDEX IF NOT EXISTS idx_learning_answers_user ON user_learning_answers(user_id, created_at DESC)")
await conn.execute("CREATE INDEX IF NOT EXISTS idx_skill_mastery_user ON user_skill_mastery(user_id)")
logger.info("Database initialized")