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
+141
View File
@@ -0,0 +1,141 @@
"""
Typing Router — раздел "Печать".
Пользователь печатает польские предложения, система проверяет.
Без AI — просто сравнение с правильным ответом из БД.
"""
import random
from fastapi import APIRouter, Request, HTTPException
from auth import require_auth
from database import get_pool
router = APIRouter(prefix="/typing", tags=["typing"])
def normalize_text(text: str) -> str:
"""Нормализует текст для сравнения: нижний регистр, убирает лишние пробелы."""
return ' '.join(text.strip().lower().split())
@router.get("/next")
async def get_next_typing_sentence(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"
# Находим случайное непройденное предложение
row = await conn.fetchrow("""
SELECT s.id, s.sentence_pl, s.translation_ru, s.translation_uk, s.translation_en, s.level
FROM sentences s
WHERE s.level = $1
AND s.id NOT IN (
SELECT sentence_id FROM typing_progress WHERE user_id = $2
)
ORDER BY RANDOM()
LIMIT 1
""", current_level, telegram_id)
if not row:
return {"completed": True, "message": "Все предложения пройдены!"}
translation_field = f"translation_{lang}"
translation = row.get(translation_field) or row.get("translation_en") or row["sentence_pl"]
return {
"completed": False,
"sentence_id": row["id"],
"translation": translation,
"level": row["level"],
}
@router.post("/check")
async def check_typing_sentence(request: Request):
"""Проверяет напечатанное предложение."""
telegram_id, _ = await require_auth(request)
body = await request.json()
sentence_id = body.get("sentence_id")
user_text = body.get("text", "")
if not sentence_id or not user_text:
raise HTTPException(400, "sentence_id and text required")
pool = await get_pool()
async with pool.acquire() as conn:
row = await conn.fetchrow("""
SELECT sentence_pl FROM sentences WHERE id = $1
""", sentence_id)
if not row:
raise HTTPException(404, "Sentence not found")
correct_text = normalize_text(row["sentence_pl"])
user_normalized = normalize_text(user_text)
is_correct = correct_text == user_normalized
if is_correct:
# Сохраняем прогресс
await conn.execute("""
INSERT INTO typing_progress (user_id, sentence_id, completed_at)
VALUES ($1, $2, NOW())
ON CONFLICT (user_id, sentence_id) DO NOTHING
""", telegram_id, sentence_id)
return {
"correct": is_correct,
"correct_answer": row["sentence_pl"],
}
@router.get("/hint/{sentence_id}")
async def get_typing_hint(request: Request, sentence_id: int):
"""Возвращает правильный ответ для подсказки."""
await require_auth(request)
pool = await get_pool()
async with pool.acquire() as conn:
row = await conn.fetchrow("""
SELECT sentence_pl FROM sentences WHERE id = $1
""", sentence_id)
if not row:
raise HTTPException(404, "Sentence not found")
return {"correct_answer": row["sentence_pl"]}
@router.get("/progress")
async def get_typing_progress(request: Request):
"""Статистика по печати."""
telegram_id, _ = await require_auth(request)
pool = await get_pool()
async with pool.acquire() as conn:
user = await conn.fetchrow("""
SELECT current_level FROM users WHERE telegram_id = $1
""", telegram_id)
current_level = user["current_level"] if user else "A1"
total = await conn.fetchval("""
SELECT COUNT(*) FROM sentences WHERE level = $1
""", current_level) or 0
completed = await conn.fetchval("""
SELECT COUNT(*) FROM typing_progress tp
JOIN sentences s ON tp.sentence_id = s.id
WHERE tp.user_id = $1 AND s.level = $2
""", telegram_id, current_level) or 0
return {
"total": total,
"completed": completed,
"remaining": total - completed,
}