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
+282
View File
@@ -0,0 +1,282 @@
import random
from fastapi import APIRouter, Request, HTTPException
from auth import require_auth
from database import get_pool
router = APIRouter(prefix="/wordquiz", tags=["wordquiz"])
@router.get("/next")
async def get_next_quiz(request: Request):
"""Выдаёт случайное слово из базы выбранного уровня. Исключает недавно показанные."""
telegram_id, _ = await require_auth(request)
pool = await get_pool()
async with pool.acquire() as conn:
user = await conn.fetchrow(
"SELECT app_language, current_level FROM users WHERE telegram_id = $1", telegram_id
)
if not user:
raise HTTPException(401, "User not registered")
lang = user["app_language"] or "pl"
current_level = user["current_level"] or "A1"
progress = await conn.fetchrow(
"SELECT words_guessed, recent_words FROM word_quiz_progress WHERE user_id = $1",
telegram_id
)
total_guessed = progress["words_guessed"] if progress else 0
recent_words = list(progress["recent_words"]) if progress and progress["recent_words"] else []
async def pick_word(exclude, respect_exclude=True):
if respect_exclude and exclude:
placeholders = ",".join([f"${i+2}" for i in range(len(exclude))])
return await conn.fetchrow(f"""
SELECT word_pl, translation_ru, translation_uk, translation_en
FROM (
SELECT DISTINCT ON (word_pl) word_pl, translation_ru, translation_uk, translation_en
FROM level_words
WHERE level = $1 AND level_id IS NULL AND word_pl NOT IN ({placeholders})
) AS unique_words
ORDER BY RANDOM()
LIMIT 1
""", current_level, *exclude)
return await conn.fetchrow("""
SELECT word_pl, translation_ru, translation_uk, translation_en
FROM (
SELECT DISTINCT ON (word_pl) word_pl, translation_ru, translation_uk, translation_en
FROM level_words
WHERE level = $1 AND level_id IS NULL
) AS unique_words
ORDER BY RANDOM()
LIMIT 1
""", current_level)
word = await pick_word(recent_words, respect_exclude=True)
if not word:
word = await pick_word([], respect_exclude=False)
if not word:
raise HTTPException(404, "No words found")
correct_polish = word["word_pl"]
translation_field = f"translation_{lang}"
translation = word.get(translation_field) or word.get("translation_en") or correct_polish
# Ищем неправильные варианты
exclude_for_wrong = recent_words + [correct_polish]
placeholders = ",".join([f"${i+3}" for i in range(len(exclude_for_wrong))])
wrong_words = await conn.fetch(f"""
SELECT word_pl
FROM (
SELECT DISTINCT ON (word_pl) word_pl
FROM level_words
WHERE level = $1 AND level_id IS NULL AND word_pl != $2 AND word_pl NOT IN ({placeholders})
) AS unique_wrong
ORDER BY RANDOM()
LIMIT 5
""", current_level, correct_polish, *exclude_for_wrong)
if len(wrong_words) < 2:
wrong_words = await conn.fetch("""
SELECT word_pl
FROM (
SELECT DISTINCT ON (word_pl) word_pl
FROM level_words
WHERE level = $1 AND level_id IS NULL AND word_pl != $2
) AS unique_wrong
ORDER BY RANDOM()
LIMIT 5
""", current_level, correct_polish)
wrong_options = []
seen = {correct_polish.lower()}
for w in wrong_words:
wl = w["word_pl"].lower()
if wl not in seen:
wrong_options.append(w["word_pl"])
seen.add(wl)
if len(wrong_options) >= 2:
break
recent_words = recent_words + [correct_polish] + wrong_options
if len(recent_words) > 45:
recent_words = recent_words[-45:]
await conn.execute("""
INSERT INTO word_quiz_progress (user_id, words_guessed, total_score, recent_words)
VALUES ($1, 0, 0, $2::text[])
ON CONFLICT (user_id) DO UPDATE
SET recent_words = $2::text[]
""", telegram_id, recent_words)
options = wrong_options + [correct_polish]
random.shuffle(options)
if total_guessed <= 10:
difficulty = "easy"
elif total_guessed <= 30:
difficulty = "medium"
elif total_guessed <= 60:
difficulty = "hard"
else:
difficulty = "expert"
return {
"correct_polish": correct_polish,
"translation": translation,
"options": options,
"difficulty": difficulty,
}
@router.post("/check")
async def check_quiz_answer(request: Request):
"""Проверяет ответ, начисляет очки и проверяет достижения"""
telegram_id, _ = await require_auth(request)
body = await request.json()
correct_polish = body.get("correct_polish", "")
user_answer = body.get("answer", "")
mistakes = body.get("mistakes", 0)
completion_time = body.get("completion_time")
if not correct_polish or not user_answer:
raise HTTPException(400, "correct_polish and answer required")
# Защита от читерства: минимальное время 1 секунда
if completion_time is not None and completion_time < 1:
return {
"correct": False,
"correct_streak": 0,
"error": "Too fast. Please take your time."
}
is_correct = user_answer.strip().lower() == correct_polish.strip().lower()
pool = await get_pool()
async with pool.acquire() as conn:
current_streak = await conn.fetchval(
"SELECT correct_streak FROM word_quiz_progress WHERE user_id = $1",
telegram_id
) or 0
if is_correct:
current_streak += 1
else:
current_streak = 0
if is_correct:
score = max(10, 50 - (mistakes * 20))
await conn.execute("""
INSERT INTO word_quiz_progress (user_id, words_guessed, total_score, correct_streak)
VALUES ($1, 1, $2, $3)
ON CONFLICT (user_id) DO UPDATE
SET words_guessed = word_quiz_progress.words_guessed + 1,
total_score = word_quiz_progress.total_score + $2,
correct_streak = $3
""", telegram_id, score, current_streak)
from routers.achievements import check_and_award_achievements
new_achievements = []
total_guessed = await conn.fetchval(
"SELECT words_guessed FROM word_quiz_progress WHERE user_id = $1",
telegram_id
) or 0
ach = await check_and_award_achievements(telegram_id, "quiz_guessed", total_guessed)
new_achievements.extend(ach)
if current_streak >= 10:
ach = await check_and_award_achievements(telegram_id, "quiz_perfect_streak", current_streak)
new_achievements.extend(ach)
if mistakes == 0:
ach = await check_and_award_achievements(telegram_id, "quiz_no_mistakes", 1)
new_achievements.extend(ach)
if completion_time is not None and completion_time > 0 and int(completion_time) <= 3:
ach = await check_and_award_achievements(telegram_id, "quiz_speed", 3)
new_achievements.extend(ach)
wordsearch_score = await conn.fetchval(
"SELECT COALESCE(SUM(score), 0) FROM user_progress WHERE user_id = $1 AND completed_at IS NOT NULL",
telegram_id
) or 0
sentence_score = await conn.fetchval(
"SELECT COALESCE(SUM(score), 0) FROM sentence_progress WHERE user_id = $1 AND completed_at IS NOT NULL",
telegram_id
) or 0
wordquiz_score = await conn.fetchval(
"SELECT COALESCE(total_score, 0) FROM word_quiz_progress WHERE user_id = $1",
telegram_id
) or 0
bubblewords_score = await conn.fetchval(
"SELECT COALESCE(total_score, 0) FROM bubble_words_progress WHERE user_id = $1",
telegram_id
) or 0
grand_total = wordsearch_score + sentence_score + wordquiz_score + bubblewords_score
ach = await check_and_award_achievements(telegram_id, "total_score", grand_total)
new_achievements.extend(ach)
return {
"correct": True,
"score": score,
"correct_streak": current_streak,
"new_achievements": new_achievements,
}
else:
# При 3 ошибках — начисляем утешительные 10 очков
if mistakes >= 3:
await conn.execute("""
INSERT INTO word_quiz_progress (user_id, words_guessed, total_score, correct_streak)
VALUES ($1, 0, 10, 0)
ON CONFLICT (user_id) DO UPDATE
SET total_score = word_quiz_progress.total_score + 10,
correct_streak = 0
""", telegram_id)
else:
await conn.execute("""
INSERT INTO word_quiz_progress (user_id, words_guessed, total_score, correct_streak)
VALUES ($1, 0, 0, $2)
ON CONFLICT (user_id) DO UPDATE
SET correct_streak = $2
""", telegram_id, current_streak)
return {
"correct": False,
"correct_streak": current_streak,
"consolation_score": 10 if mistakes >= 3 else 0,
}
@router.get("/progress")
async def get_quiz_progress(request: Request):
"""Статистика пользователя в квизе"""
telegram_id, _ = await require_auth(request)
pool = await get_pool()
async with pool.acquire() as conn:
progress = await conn.fetchrow(
"SELECT words_guessed, total_score, correct_streak FROM word_quiz_progress WHERE user_id = $1",
telegram_id
)
total_guessed = progress["words_guessed"] if progress else 0
if total_guessed <= 10:
difficulty = "easy"
elif total_guessed <= 30:
difficulty = "medium"
elif total_guessed <= 60:
difficulty = "hard"
else:
difficulty = "expert"
return {
"words_guessed": total_guessed,
"total_score": progress["total_score"] if progress else 0,
"correct_streak": progress["correct_streak"] if progress else 0,
"difficulty": difficulty
}