initial commit (backend)
This commit is contained in:
+870
@@ -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")
|
||||
Reference in New Issue
Block a user